diff --git a/inventory-web/src/directives/permission.ts b/inventory-web/src/directives/permission.ts new file mode 100644 index 0000000..ca78ad6 --- /dev/null +++ b/inventory-web/src/directives/permission.ts @@ -0,0 +1,45 @@ +import type { Directive, DirectiveBinding } from 'vue' +import { useUserStore } from '@/stores/user' + +/** + * v-permission 指令 + * 用法: v-permission="'inbound_buy:delete'" 或 v-permission="['inbound_buy:delete', 'inbound_buy:edit']" + * 支持数组形式(满足任一权限即显示)和字符串形式 + */ +const permissionDirective: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const userStore = useUserStore() + const value = binding.value + + // 没有绑定值,不做任何处理 + if (value === undefined || value === null) { + return + } + + // 解析权限码数组 + let permissionCodes: string[] = [] + + if (typeof value === 'string') { + // 字符串形式: "inbound_buy:delete" + permissionCodes = [value] + } else if (Array.isArray(value)) { + // 数组形式: ['inbound_buy:delete', 'inbound_buy:edit'] + permissionCodes = value + } + + // 超级管理员拥有所有权限 + if (userStore.role === 'SUPER_ADMIN') { + return + } + + // 检查是否有任意一个权限码 + const hasAuth = permissionCodes.some((code) => userStore.hasPermission(code)) + + if (!hasAuth) { + // 没有权限,从 DOM 中移除该元素 + el.parentNode?.removeChild(el) + } + } +} + +export default permissionDirective diff --git a/inventory-web/src/main.ts b/inventory-web/src/main.ts index 01dc574..29648f7 100644 --- a/inventory-web/src/main.ts +++ b/inventory-web/src/main.ts @@ -17,6 +17,9 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue' // 4. 引入全局样式 (通常建议加上,如果没有可忽略) import './style.css' +// 5. 引入全局自定义指令 +import permissionDirective from './directives/permission' + const app = createApp(App) // ========================================================= @@ -41,4 +44,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } +// 5. 注册全局自定义指令 +app.directive('permission', permissionDirective) + app.mount('#app') \ No newline at end of file diff --git a/inventory-web/src/utils/request.ts b/inventory-web/src/utils/request.ts index eb462bf..7d12b4b 100644 --- a/inventory-web/src/utils/request.ts +++ b/inventory-web/src/utils/request.ts @@ -13,7 +13,21 @@ const service = axios.create({ }) // ============================================================ -// 2. 无感刷新 Token 核心逻辑 +// 2. 防重复提交 - Pending 请求池 +// ============================================================ +const pendingRequests = new Map() + +// 生成唯一请求 Key:方法 + URL + 序列化参数 +const generateRequestKey = (config: InternalAxiosRequestConfig): string => { + const method = (config.method || 'get').toLowerCase() + const url = config.url || '' + const params = config.params ? JSON.stringify(config.params) : '' + const data = config.data ? JSON.stringify(config.data) : '' + return `${method}:${url}:${params}:${data}` +} + +// ============================================================ +// 3. 无感刷新 Token 核心逻辑 // ============================================================ // 标记是否正在刷新 Token @@ -54,17 +68,37 @@ const refreshToken = async (refreshTokenValue: string): Promise => { } // ============================================================ -// 3. 请求拦截器 +// 4. 请求拦截器(添加 Token + 防重复提交) // ============================================================ service.interceptors.request.use( (config) => { - // 在发送请求之前做些什么 + // 1. 添加 Token const token = localStorage.getItem('access_token') || localStorage.getItem('token') - if (token && config.headers) { // Flask-JWT-Extended 默认需要 'Bearer ' 格式 config.headers['Authorization'] = 'Bearer ' + token } + + // 2. 防重复提交检查 + const requestKey = generateRequestKey(config) + + // 排除一些不需要防重复的请求(如查询类 GET 请求可以根据需求调整) + const ignoreMethods = ['get', 'head'] + if (!ignoreMethods.includes((config.method || 'get').toLowerCase())) { + if (pendingRequests.has(requestKey)) { + // 取消之前的请求 + const controller = pendingRequests.get(requestKey) + controller?.abort('正在处理中,请勿重复操作') + pendingRequests.delete(requestKey) + console.warn(`[防重复] 取消重复请求: ${requestKey}`) + } + + // 创建新的 AbortController 并存储 + const controller = new AbortController() + config.signal = controller.signal + pendingRequests.set(requestKey, controller) + } + return config }, (error) => { @@ -73,10 +107,16 @@ service.interceptors.request.use( ) // ============================================================ -// 4. 响应拦截器(核心:无感刷新) +// 5. 响应拦截器(核心:无感刷新 + 清理 pending) // ============================================================ service.interceptors.response.use( (response) => { + // 清理 pending 请求池 + const requestKey = generateRequestKey(response.config) + if (pendingRequests.has(requestKey)) { + pendingRequests.delete(requestKey) + } + // Axios 默认包了一层 data,所以这里取 response.data const res = response.data @@ -94,6 +134,14 @@ service.interceptors.response.use( async (error: AxiosError) => { console.log('err: ' + error) // for debug + // 清理 pending 请求池(无论成功还是失败都要清理) + if (error.config) { + const requestKey = generateRequestKey(error.config) + if (pendingRequests.has(requestKey)) { + pendingRequests.delete(requestKey) + } + } + // 如果不是 axios 错误,直接抛出 if (!error.response) { return Promise.reject(error) diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index a8fe42f..46dd5ac 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -219,7 +219,7 @@ 编辑