feat: 新增按钮级权限指令 v-permission 与 Axios 全局防重复提交机制
This commit is contained in:
45
inventory-web/src/directives/permission.ts
Normal file
45
inventory-web/src/directives/permission.ts
Normal file
@ -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
|
||||
@ -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')
|
||||
@ -13,7 +13,21 @@ const service = axios.create({
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 2. 无感刷新 Token 核心逻辑
|
||||
// 2. 防重复提交 - Pending 请求池
|
||||
// ============================================================
|
||||
const pendingRequests = new Map<string, AbortController>()
|
||||
|
||||
// 生成唯一请求 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<string> => {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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 <token>' 格式
|
||||
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)
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="default">删除</el-button>
|
||||
<el-button link type="danger" size="default" v-permission="'inbound_buy:delete'">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user