Skip to content

Vue3 使用经验

一、权限指令

  1. 定义钩子函数
js
// src/hooks/use-permission/index.ts

import { getUserLocal } from "@/utils/localStorage";

export function userPermission() {
  // 没有permissionKey,直接返回true 把dom节点删除
  function hasPermission(params) {
    if (!params || !params.permissionKey) {
      return true;
    }
    const systemUser = getUserLocal();
    // 被禁用
    if (systemUser.userStatus === 1) {
      return false;
    }

    // 判断action
    const actionList = systemUser[SYSTEM_ACTIONS];
    if (!actionList || actionList.length === 0) {
      return false;
    }
    const { permissionKey } = params;
    let hasPermission = undefined;
    if (Array.isArray(hasPermission)) {
      hasPermission = permissionKey.some((ite) => actionList.includes(item));
    } else {
      hasPermission = actionList.includes(permissionKey);
    }

    if (hasPermission) {
      return true;
    } else {
      return false;
    }
  }

  return { hasPermission };
}
  1. 定义指令函数
js
// src/directives/index.ts
import { userPermission } from "@/hooks/use-permission";
import { DirectiveBinding, ObjectDirective, App } from "vue";

// 定义指令函数
export const permission: ObjectDirective = {
  mounted(el: HTMLButtonElement, binding: DirectiveBinding) {
    if (!binding.value) {
      return;
    }
    const code = binding.value;
    const { hasPermission } = userPermission();
    // 将获取到的值传到权限判断函数中,如果是false,删除对应的dom节点
    if (!hasPermission(code)) {
      el.remove();
    }
  },
};

// 注册全局自定义指令
export const setUpDirectives(app:App) => {
	app.directive('permission', permission)
}
  1. 第一种方式注册全局指令
js
// main.ts
import { setUpDirectives } from "@/directives";

// 第一种方式注册全局指令
const app = createApp(App);
setUpDirectives(app);
  1. 第二种方式注册为全局函数
js
// main.ts
import { userPermission } from "@/hooks/use-permission";

const app = createApp(App);

// 第二种方式注册为全局函数
const { hasPermission } = userPermission();
app.config.globalProperties.$globalPermission = hasPermission;
  1. 使用方式
html
<!-- v-if指令使用 -->
<div v-if="globalProperties.$globalPermission({permissionKey: '权限KEY'})">
  ......
</div>

<!-- v-permission指令使用 -->
<div v-permission="{permissionKey: '权限KEY'}">......</div>

二、table 滚动条超过屏幕后右侧和底部固定显示

为了解决一页内容太多了,滑动到底部看不到 table 的表头和左右滑动的滚动条在底部不方便操作,需要固定底部横向滚动条,并且右侧滚动条滑动到一定位置表头吸顶

  1. table 横向滚动吸底功能
js
// src/directives/table-sticky-scroll.ts

import { userPermission } from "@/hooks/use-permission";
import { DirectiveBinding, ObjectDirective, App } from "vue";
import { useGlobalConfig } from 'element-plus'

/**
 * table横向滚动吸底功能
 * el: 获取绑定的dom
 * bind.value 通过指令绑定之后传过来的参数
 */
export const tableStickyScroll: ObjectDirective = {
  mounted(el: HTMLButtonElement, binding: DirectiveBinding) {
    if (!el || !binding.value) {
      return;
    }

	// 底部的距离
    const stickyBottom = binding.value && binding.value.bottom ? binding.value.bottom : 0;
	// 底部的层级
    const stickyZIndex = binding.value && binding.value.zIndex ? binding.value.zIndex : 999;

	// 滚动条盒子
    const namespace = useGlobalConfig('namespace')
	const scrollBox = document.createElement('div')
	scrollBox.style.position = 'sticky'
	scrollBox.style.bottom = stickyBottom + 'px'
	scrollBox.style.zIndex = stickyZIndex

	// 滚动条DOM
	const scrollBar = el.querySelector(`.${namespace.value}-scrollbar__bar.is-horizontal`) as HTMLElement
	if (scrollBar) {
		scrollBox.appendChild(scrollBar)
	}

	el.querySelector(`.${namespace.value}-table__body-wrapper`)?.parentNode?.appendChild(scrollBox)
  },
};

// 注册全局自定义指令
export const setUpDirectives(app:App) => {
	app.directive('tableStickyScroll', tableStickyScroll)
}
  1. table 表头吸顶功能
js
// src/directives/table-sticky-header.ts

import { userPermission } from "@/hooks/use-permission";
import { DirectiveBinding, ObjectDirective, App } from "vue";
import { useGlobalConfig } from 'element-plus'

/**
 * table表头吸顶功能
 * el: 获取绑定的dom
 * bind.value 通过指令绑定之后传过来的参数
 */
export const tableStickyHeader: ObjectDirective = {
  mounted(el: HTMLButtonElement, binding: DirectiveBinding) {
    if (!el || !binding.value) {
      return;
    }

	// 距离顶部的距离
    const stickyTop = binding.value && binding.value.top ? binding.value.top : 64;
	// 顶部的层级
    const stickyZIndex = binding.value && binding.value.zIndex ? binding.value.zIndex : 999;

	el.setAttribute('style', 'overflow:visible !important')

	// 表头处理
    const namespace = useGlobalConfig('namespace')
	const headerWrapper = el.querySelector(`.${namespace.value}-table__header-wrapper`) as HTMLElement
	headerWrapper.style.overflow = 'hidden'
	headerWrapper.style.position = 'sticky'
	headerWrapper.style.top = stickyTop + 'px'
	headerWrapper.style.zIndex = stickyZIndex
  },
};

// 注册全局自定义指令
export const setUpDirectives(app:App) => {
	app.directive('tableStickyHeader', tableStickyHeader)
}
  1. 使用方式
html
<el-table scrollbar-always-on v-table-sticky-header v-table-sticky-scroller>
  ......
</el-table>

三、Svg 图标组件

  1. 定义组件
html
// src/components/SvgIcon.vue

<template>
  <svg class="svg-icon" :class="className" aria-hidden="">
    <use :href="`#${name}`"></use>
  </svg>
</template>
<script setup lang="ts">
  defineProps({
    // 图标名称
    name: {
      type: String,
      required: true,
      default: "",
    },
    // 图标类名
    className: {
      type: String,
      default: "",
    },
  });
</script>
<style scoped lang="scss">
  .svg-icon {
    width: 1em;
    height: 1em;
    fill: currentColor;
    vertical-align: middle;
    overflow: hidden;
  }
</style>
  1. 注册全局组件
js
// main.ts

app.component("svg-icon", SvgIcon);
  1. 使用
html
<svg-icon name="smc-info" className="ml5" />
  1. 加载全局 svg 图标库
js
// vite.config.ts
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

export default defineConfig({
	const name = 'vue-app-admin';
	plugins: [
		createSvgIconsPlugin({
			iconDirs: [path.resolve(process.cwd(), 'src/assets/image/icons')],
			symbolId: '[name]',
			inject: 'body-last',
			customDomId: `${workspace}__svg__icons__dom__`
		})
	]
})

四 文件上传组件

js
<template>
  <div class="custom-upload-card">
    <el-upload
      class="custom-upload-box"
      :style="boxStyle"
      drag
      :limit="multiple ? 100 : 1"
      :multiple="multiple"
      :before-upload="handleBeforeUpload"
      :accept="computeAccept().join(',')"
      list-type="picture"
    >
      <scg-icon name="smc-add" slot="trigger"></scg-icon>
      <span class="text-tip" slot="trigger">添加素材</span>
    </el-upload>
  </div>
</template>
<script setup lang="ts">
const COMMON_FILE_TYPES = {
  mp4: "video/mp4",
  mov: "video/mov",
  avi: "video/avi",
  png: "image/png",
  jpeg: "image/jpeg",
  jpg: "image/jpg",
  webp: "image/webp",
  mp3: "audio/mpeg",
};

const props = defineProps({
  label: {
    type: String,
    default: "视频",
  },
  width: {
    type: Number,
    default: 0,
  },
  height: {
    type: Number,
    default: 0,
  },
  maxWidth: {
    type: Number,
    default: 0,
  },
  maxHeight: {
    type: Number,
    default: 0,
  },
  size: {
    type: Number,
    default: 300, // 单位kb
  },
  checkSize: {
    type: Boolean,
    default: false,
  },
  customProps: {
    type: Object,
    default: () => {},
  },
  multiple: {
    type: Boolean,
    default: false,
  },
  boxStyle: {
    type: String,
    default: "width:90px;height:120px",
  },
  accept: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmit(["clickUpload"]);

const computeAccept = () => {
  const fileTypes = props.accept.map((item: any) => COMMON_FILE_TYPES[item]);
  return fileTypes;
};

const handleBeforeUpload = (file: any) => {
  // 校验格式
  const fileTypes = computeAccept();
  if (!file.type || fileTypes.length === 0 || !fileTypes.includes(file.type)) {
    // 提示信息
    console.log(`上传${props.label}只能是${props.accept.join("、")}格式`);
    return false;
  }

  if (props.checkSize) {
    const size = file.size / 1024 / 1024;
    if (size > props.size) {
      console.log(`上传${props.label}大小不能超过${props.size}MB`);
      return false;
    }
  }

  emit("clickUpload", {
    file: file,
    customProps: { ...props.customProps },
  });
  return false;
};
</script>
<style scoped lang="scss">
.custom-upload-card {
  display: inline-flex;
  position: relative;

  .custom-upload-box {
    //....
  }
}
</style>

五、

Released under the MIT License.