Appearance
Vue3 使用经验
一、权限指令
- 定义钩子函数
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 };
}- 定义指令函数
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)
}- 第一种方式注册全局指令
js
// main.ts
import { setUpDirectives } from "@/directives";
// 第一种方式注册全局指令
const app = createApp(App);
setUpDirectives(app);- 第二种方式注册为全局函数
js
// main.ts
import { userPermission } from "@/hooks/use-permission";
const app = createApp(App);
// 第二种方式注册为全局函数
const { hasPermission } = userPermission();
app.config.globalProperties.$globalPermission = hasPermission;- 使用方式
html
<!-- v-if指令使用 -->
<div v-if="globalProperties.$globalPermission({permissionKey: '权限KEY'})">
......
</div>
<!-- v-permission指令使用 -->
<div v-permission="{permissionKey: '权限KEY'}">......</div>二、table 滚动条超过屏幕后右侧和底部固定显示
为了解决一页内容太多了,滑动到底部看不到 table 的表头和左右滑动的滚动条在底部不方便操作,需要固定底部横向滚动条,并且右侧滚动条滑动到一定位置表头吸顶
- 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)
}- 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)
}- 使用方式
html
<el-table scrollbar-always-on v-table-sticky-header v-table-sticky-scroller>
......
</el-table>三、Svg 图标组件
- 定义组件
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>- 注册全局组件
js
// main.ts
app.component("svg-icon", SvgIcon);- 使用
html
<svg-icon name="smc-info" className="ml5" />- 加载全局 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>