263 lines
8.0 KiB
Markdown
263 lines
8.0 KiB
Markdown
# Hooks 组合式函数
|
||
|
||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||
|
||
:::tip 说明
|
||
项目封装了一系列组合式函数(Hooks),位于 `src/hooks/` 和 `src/utils/` 目录,提供权限检查、异步 loading、防重复提交、实时时钟、项目配置等可复用逻辑。
|
||
:::
|
||
|
||
## Hooks 目录结构
|
||
|
||
```
|
||
src/hooks/
|
||
├── event/
|
||
│ ├── useEventListener.ts # DOM 事件监听
|
||
│ ├── useBreakpoint.ts # 响应式断点
|
||
│ └── useWindowSizeFn.ts # 窗口大小变化回调
|
||
├── setting/
|
||
│ ├── useProjectSetting.ts # 项目配置(权限模式、水印等)
|
||
│ └── useDesignSetting.ts # 设计配置(主题色、深色模式)
|
||
├── web/
|
||
│ ├── usePermission.ts # 权限检查(hasPermission、hasSomePermission)
|
||
│ ├── useECharts.ts # ECharts 封装
|
||
│ ├── usePage.ts # 页面跳转
|
||
│ ├── useTags.ts # 标签页管理
|
||
│ └── useCopyToClipboard.ts # 剪贴板复制
|
||
├── core/
|
||
│ └── useTimeout.ts # 定时器封装
|
||
├── useDomWidth.ts # DOM 宽度
|
||
├── useOnline.ts # 网络状态
|
||
├── useBattery.ts # 电量信息
|
||
├── useTime.ts # 实时时钟
|
||
└── use-async.ts # 异步 loading 管理
|
||
```
|
||
|
||
## usePermission
|
||
|
||
权限检查 Hook,从 user store 获取权限列表进行判断:
|
||
|
||
```typescript
|
||
// src/hooks/web/usePermission.ts
|
||
|
||
import { useUserStore } from '@/store/modules/user';
|
||
|
||
export function usePermission() {
|
||
const userStore = useUserStore();
|
||
|
||
function _somePermissions(accesses: string[]) {
|
||
const permissionsList = userStore.getPermissions;
|
||
if (permissionsList[0] == '*:*:*') return true; // 超级管理员
|
||
return accesses.some((item) => permissionsList.includes(item));
|
||
}
|
||
|
||
// 是否包含其中某个权限(用于 v-perm 指令)
|
||
function hasSomePermission(accesses?: string | string[]): boolean {
|
||
const permissionsList = userStore.getPermissions;
|
||
if (permissionsList[0] === '*:*:*') return true;
|
||
const list = Array.isArray(accesses) ? accesses : [];
|
||
return list.some((access) => permissionsList.includes(access));
|
||
}
|
||
|
||
// 是否包含所有权限(用于 v-perms 指令)
|
||
function hasEveryPermission(accesses?: string | string[]): boolean {
|
||
const list = Array.isArray(accesses) ? accesses : [];
|
||
const permissionsList = userStore.getPermissions;
|
||
if (permissionsList[0] === '*:*:*') return true;
|
||
return list.every((access) => permissionsList.includes(access));
|
||
}
|
||
|
||
// 判断是否存在权限(可用于 v-if 显示逻辑)
|
||
function hasPermission(accesses: string[]): boolean {
|
||
if (!accesses || !accesses.length) return true;
|
||
return _somePermissions(accesses);
|
||
}
|
||
|
||
return { hasPermission, hasEveryPermission, hasSomePermission };
|
||
}
|
||
```
|
||
|
||
使用示例:
|
||
|
||
```typescript
|
||
import { usePermission } from '@/hooks/web/usePermission';
|
||
|
||
const { hasPermission, hasSomePermission } = usePermission();
|
||
|
||
// v-if 条件渲染
|
||
const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
||
|
||
// 检查是否有任一权限
|
||
const canOperate = hasSomePermission(['sys:position:update', 'sys:position:delete']);
|
||
```
|
||
|
||
## useLockFn
|
||
|
||
防重复提交 Hook,函数执行期间自动锁定,位于 `src/utils/useLockFn.ts`:
|
||
|
||
```typescript
|
||
// src/utils/useLockFn.ts
|
||
import { ref } from 'vue';
|
||
|
||
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
|
||
const isLock = ref(false);
|
||
const lockFn = async (...args: any[]) => {
|
||
if (isLock.value) return; // 锁定期间忽略调用
|
||
isLock.value = true;
|
||
try {
|
||
await fn(...args);
|
||
} finally {
|
||
isLock.value = false; // 无论成功失败都解锁
|
||
}
|
||
};
|
||
return { isLock, lockFn };
|
||
}
|
||
```
|
||
|
||
实际用法(edit.vue 中的提交按钮):
|
||
|
||
```typescript
|
||
// src/views/system/position/edit.vue
|
||
import { useLockFn } from '@/utils/useLockFn';
|
||
|
||
const handleSubmit = async () => {
|
||
await formRef.value?.validate();
|
||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||
message('操作成功');
|
||
emit('update:visible', false);
|
||
emit('success');
|
||
};
|
||
|
||
// isLock 绑定按钮的 loading 状态,lockFn 作为点击处理函数
|
||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||
```
|
||
|
||
```vue
|
||
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
|
||
```
|
||
|
||
## useAsync
|
||
|
||
异步操作 loading 管理,位于 `src/hooks/use-async.ts`:
|
||
|
||
```typescript
|
||
// src/hooks/use-async.ts
|
||
import { isReactive, isRef } from 'vue';
|
||
|
||
function setLoading(loading, val) {
|
||
if (loading != undefined && isRef(loading)) {
|
||
loading.value = val;
|
||
} else if (loading != undefined && isReactive(loading)) {
|
||
loading.loading = val;
|
||
}
|
||
}
|
||
|
||
export const useAsync = async (func: Promise<any>, loading: any): Promise<any> => {
|
||
setLoading(loading, true);
|
||
return await func.finally(() => setLoading(loading, false));
|
||
};
|
||
```
|
||
|
||
使用示例:
|
||
|
||
```typescript
|
||
const loading = ref(false);
|
||
const fetchData = async () => {
|
||
await useAsync(getPositionList(params), loading);
|
||
};
|
||
```
|
||
|
||
## useTime
|
||
|
||
实时时钟 Hook,每秒更新当前时间,位于 `src/hooks/useTime.ts`:
|
||
|
||
```typescript
|
||
// src/hooks/useTime.ts
|
||
import { ref, onMounted, onUnmounted } from 'vue';
|
||
|
||
export function useTime() {
|
||
let timer;
|
||
const year = ref(0);
|
||
const month = ref(0);
|
||
const week = ref('');
|
||
const day = ref(0);
|
||
const hour = ref<number | string>(0);
|
||
const minute = ref<number | string>(0);
|
||
const second = ref(0);
|
||
|
||
const updateTime = () => {
|
||
const date = new Date();
|
||
year.value = date.getFullYear();
|
||
month.value = date.getMonth() + 1;
|
||
week.value = '日一二三四五六'.charAt(date.getDay());
|
||
day.value = date.getDate();
|
||
hour.value = (date.getHours() + '')?.padStart(2, '0');
|
||
minute.value = (date.getMinutes() + '')?.padStart(2, '0');
|
||
second.value = date.getSeconds();
|
||
};
|
||
|
||
updateTime();
|
||
onMounted(() => { timer = setInterval(() => updateTime(), 1000); });
|
||
onUnmounted(() => { clearInterval(timer); });
|
||
|
||
return { month, day, hour, minute, second, week };
|
||
}
|
||
```
|
||
|
||
## useProjectSetting
|
||
|
||
项目配置 Hook,读取项目全局设置:
|
||
|
||
```typescript
|
||
// src/hooks/setting/useProjectSetting.ts
|
||
export function useProjectSetting() {
|
||
const projectStore = useProjectSettingStore();
|
||
return {
|
||
getPermissionMode: computed(() => projectStore.permissionMode), // 'BACK' | 'FRONT'
|
||
getIsWaterMark: computed(() => projectStore.isWaterMark),
|
||
// ... 其他配置
|
||
};
|
||
}
|
||
```
|
||
|
||
## useDesignSetting
|
||
|
||
设计配置 Hook,管理主题色和深色模式:
|
||
|
||
```typescript
|
||
// src/hooks/setting/useDesignSetting.ts
|
||
export function useDesignSetting() {
|
||
return {
|
||
getAppTheme: computed(() => designStore.appTheme),
|
||
getDarkTheme: computed(() => designStore.darkTheme),
|
||
// ... 其他设计配置
|
||
};
|
||
}
|
||
```
|
||
|
||
## useWindowSizeFn
|
||
|
||
窗口大小变化回调 Hook,用于表格高度自适应:
|
||
|
||
```typescript
|
||
// src/hooks/event/useWindowSizeFn.ts
|
||
|
||
export function useWindowSizeFn(callback: Fn, wait?: number) {
|
||
// 监听 window.resize 事件,防抖执行回调
|
||
}
|
||
```
|
||
|
||
BasicTable 组件中使用此 Hook 自动计算表格高度:
|
||
|
||
```typescript
|
||
// src/components/Table/src/Table.vue
|
||
useWindowSizeFn(computeTableHeight, 280);
|
||
```
|
||
|
||
:::tip 温馨提示
|
||
Hooks 是 Vue3 组合式 API 的核心模式,将可复用逻辑抽离为函数。`usePermission` 供 `v-perm` 指令和 `v-if` 条件使用,`useLockFn` 防止按钮重复点击,`useAsync` 统一管理 loading 状态。
|
||
:::
|
||
|
||
## 总结
|
||
|
||
项目提供了 usePermission(权限检查)、useLockFn(防重复提交)、useAsync(异步 loading)、useTime(实时时钟)、useProjectSetting(项目配置)、useDesignSetting(设计配置)、useWindowSizeFn(窗口变化)等组合式函数,将常用逻辑封装为可复用的函数,避免 Mixin 的命名冲突问题。
|