feat(admin): art-design-pro integration with local session auth
P1b Task 9: apps/admin based on art-design-pro v3.0.2 (tag, commit d224dd4; source recorded in apps/admin/SOURCE.md), trimmed to layout/login/i18n/request/table-form core. Login wired to real POST /v1/auth/password (single-token session), Bearer header on all requests, 401 interceptor logout+redirect, request-driven sliding renewal (refresh at ~80% TTL, single-flight), best-effort POST /v1/auth/logout on user-menu logout. One protected stub page (相册管理). Dev port 5176 strictPort + /v1 proxy to 8080. Admin keeps its own eslint9 flat config; root eslint ignores apps/admin/**. vitest unit tests for renewal pacing + Playwright e2e (guard redirect, localized wrong-password error, login lands on stub, token persists across reload, logout clears session). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* v-ripple 水波纹效果指令
|
||||
*
|
||||
* 为元素添加 Material Design 风格的水波纹点击效果。
|
||||
* 点击时从点击位置扩散出圆形水波纹动画,提升交互体验。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 水波纹动画 - 点击时从点击位置扩散圆形波纹
|
||||
* - 自适应大小 - 根据元素尺寸自动调整波纹大小和动画时长
|
||||
* - 智能配色 - 自动识别按钮类型,使用合适的波纹颜色
|
||||
* - 自定义颜色 - 支持通过参数自定义波纹颜色
|
||||
* - 性能优化 - 使用 requestAnimationFrame 和自动清理机制
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```vue
|
||||
* <template>
|
||||
* <!-- 基础用法 - 使用默认颜色 -->
|
||||
* <el-button v-ripple>点击我</el-button>
|
||||
*
|
||||
* <!-- 自定义颜色 -->
|
||||
* <el-button v-ripple="{ color: 'rgba(255, 0, 0, 0.3)' }">自定义颜色</el-button>
|
||||
*
|
||||
* <!-- 应用到任意元素 -->
|
||||
* <div v-ripple class="custom-card">卡片内容</div>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* ## 颜色规则
|
||||
*
|
||||
* - 有色按钮(primary、success、warning 等):使用白色半透明波纹
|
||||
* - 默认按钮:使用主题色半透明波纹
|
||||
* - 自定义:通过 color 参数指定任意颜色
|
||||
*
|
||||
* @module directives/ripple
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import type { App, Directive, DirectiveBinding } from 'vue'
|
||||
|
||||
export interface RippleOptions {
|
||||
/** 水波纹颜色 */
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type RippleDirective = Directive<HTMLElement, RippleOptions>
|
||||
|
||||
export const vRipple: RippleDirective = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
// 获取指令的配置参数
|
||||
const options: RippleOptions = binding.value || {}
|
||||
|
||||
// 设置元素为相对定位,并隐藏溢出部分
|
||||
el.style.position = 'relative'
|
||||
el.style.overflow = 'hidden'
|
||||
|
||||
// 点击事件处理
|
||||
el.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const left = e.clientX - rect.left
|
||||
const top = e.clientY - rect.top
|
||||
|
||||
// 创建水波纹元素
|
||||
const ripple = document.createElement('div')
|
||||
const diameter = Math.max(el.clientWidth, el.clientHeight)
|
||||
const radius = diameter / 2
|
||||
|
||||
// 根据直径计算动画时间(直径越大,动画时间越长)
|
||||
const baseTime = 600 // 基础动画时间(毫秒)
|
||||
const scaleFactor = 0.5 // 缩放因子
|
||||
const animationDuration = baseTime + diameter * scaleFactor
|
||||
|
||||
// 设置水波纹的尺寸和位置
|
||||
ripple.style.width = ripple.style.height = `${diameter}px`
|
||||
ripple.style.left = `${left - radius}px`
|
||||
ripple.style.top = `${top - radius}px`
|
||||
ripple.style.position = 'absolute'
|
||||
ripple.style.borderRadius = '50%'
|
||||
ripple.style.pointerEvents = 'none'
|
||||
|
||||
// 判断是否为有色按钮(Element Plus 按钮类型)
|
||||
const buttonTypes = ['primary', 'info', 'warning', 'danger', 'success'].map(
|
||||
(type) => `el-button--${type}`
|
||||
)
|
||||
const isColoredButton = buttonTypes.some((type) => el.classList.contains(type))
|
||||
const defaultColor = isColoredButton
|
||||
? 'rgba(255, 255, 255, 0.25)' // 有色按钮使用白色水波纹
|
||||
: 'var(--el-color-primary-light-7)' // 默认按钮使用主题色水波纹
|
||||
|
||||
// 设置水波纹颜色、初始状态和过渡效果
|
||||
ripple.style.backgroundColor = options.color || defaultColor
|
||||
ripple.style.transform = 'scale(0)'
|
||||
ripple.style.transition = `transform ${animationDuration}ms cubic-bezier(0.3, 0, 0.2, 1), opacity ${animationDuration}ms cubic-bezier(0.3, 0, 0.5, 1)`
|
||||
ripple.style.zIndex = '1'
|
||||
|
||||
// 添加水波纹元素到DOM中
|
||||
el.appendChild(ripple)
|
||||
|
||||
// 触发动画
|
||||
requestAnimationFrame(() => {
|
||||
ripple.style.transform = 'scale(2)'
|
||||
ripple.style.opacity = '0'
|
||||
})
|
||||
|
||||
// 动画结束后移除水波纹元素
|
||||
setTimeout(() => {
|
||||
ripple.remove()
|
||||
}, animationDuration + 500) // 增加500ms缓冲时间
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function setupRippleDirective(app: App) {
|
||||
app.directive('ripple', vRipple)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* v-auth 权限指令
|
||||
*
|
||||
* 适用于后端权限控制模式,基于权限标识控制 DOM 元素的显示和隐藏。
|
||||
* 如果用户没有对应权限,元素将从 DOM 中移除。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 权限验证 - 根据路由 meta 中的权限列表验证用户权限
|
||||
* - DOM 控制 - 无权限时自动移除元素,而非隐藏
|
||||
* - 响应式更新 - 权限变化时自动更新元素状态
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```vue
|
||||
* <!-- 只有拥有 'add' 权限的用户才能看到新增按钮 -->
|
||||
* <el-button v-auth="'add'">新增</el-button>
|
||||
*
|
||||
* <!-- 只有拥有 'edit' 权限的用户才能看到编辑按钮 -->
|
||||
* <el-button v-auth="'edit'">编辑</el-button>
|
||||
*
|
||||
* <!-- 只有拥有 'delete' 权限的用户才能看到删除按钮 -->
|
||||
* <el-button v-auth="'delete'">删除</el-button>
|
||||
* ```
|
||||
*
|
||||
* ## 注意事项
|
||||
*
|
||||
* - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏
|
||||
* - 权限列表从当前路由的 meta.authList 中获取
|
||||
*
|
||||
* @module directives/auth
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { router } from '@/router'
|
||||
import { App, Directive, DirectiveBinding } from 'vue'
|
||||
|
||||
export type AuthDirective = Directive<HTMLElement, string>
|
||||
|
||||
function checkAuthPermission(el: HTMLElement, binding: DirectiveBinding<string>): void {
|
||||
// 获取当前路由的权限列表
|
||||
const authList = (router.currentRoute.value.meta.authList as Array<{ authMark: string }>) || []
|
||||
|
||||
// 检查是否有对应的权限标识
|
||||
const hasPermission = authList.some((item) => item.authMark === binding.value)
|
||||
|
||||
// 如果没有权限,移除元素
|
||||
if (!hasPermission) {
|
||||
removeElement(el)
|
||||
}
|
||||
}
|
||||
|
||||
function removeElement(el: HTMLElement): void {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
const authDirective: AuthDirective = {
|
||||
mounted: checkAuthPermission,
|
||||
updated: checkAuthPermission
|
||||
}
|
||||
|
||||
export function setupAuthDirective(app: App): void {
|
||||
app.directive('auth', authDirective)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* v-roles 角色权限指令
|
||||
*
|
||||
* 基于用户角色控制 DOM 元素的显示和隐藏。
|
||||
* 只要用户拥有指定角色中的任意一个,元素就会显示,否则从 DOM 中移除。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* - 角色验证 - 检查用户是否拥有指定角色
|
||||
* - 多角色支持 - 支持单个角色或多个角色(满足其一即可)
|
||||
* - DOM 控制 - 无权限时自动移除元素,而非隐藏
|
||||
* - 响应式更新 - 角色变化时自动更新元素状态
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```vue
|
||||
* <template>
|
||||
* <!-- 单个角色 - 只有超级管理员可见 -->
|
||||
* <el-button v-roles="'R_SUPER'">超级管理员功能</el-button>
|
||||
*
|
||||
* <!-- 多个角色 - 超级管理员或普通管理员可见 -->
|
||||
* <el-button v-roles="['R_SUPER', 'R_ADMIN']">管理员功能</el-button>
|
||||
*
|
||||
* <!-- 应用到任意元素 -->
|
||||
* <div v-roles="['R_SUPER', 'R_ADMIN', 'R_USER']">
|
||||
* 所有登录用户可见的内容
|
||||
* </div>
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* ## 权限逻辑
|
||||
*
|
||||
* - 用户角色从 userStore.getUserInfo.roles 获取
|
||||
* - 只要用户拥有指定角色中的任意一个,元素就会显示
|
||||
* - 如果用户没有任何角色或不满足条件,元素将被移除
|
||||
*
|
||||
* ## 注意事项
|
||||
*
|
||||
* - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏
|
||||
* - 适用于基于角色的粗粒度权限控制
|
||||
* - 如需基于具体操作的细粒度权限控制,请使用 v-auth 指令
|
||||
*
|
||||
* @module directives/roles
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { App, Directive, DirectiveBinding } from 'vue'
|
||||
|
||||
export type RolesDirective = Directive<HTMLElement, string | string[]>
|
||||
|
||||
function checkRolePermission(el: HTMLElement, binding: DirectiveBinding<string | string[]>): void {
|
||||
const userStore = useUserStore()
|
||||
const userRoles = userStore.getUserInfo.roles
|
||||
|
||||
// 如果用户角色为空或未定义,移除元素
|
||||
if (!userRoles?.length) {
|
||||
removeElement(el)
|
||||
return
|
||||
}
|
||||
|
||||
// 确保指令值为数组格式
|
||||
const requiredRoles = Array.isArray(binding.value) ? binding.value : [binding.value]
|
||||
|
||||
// 检查用户是否具有所需角色之一
|
||||
const hasPermission = requiredRoles.some((role: string) => userRoles.includes(role))
|
||||
|
||||
// 如果没有权限,安全地移除元素
|
||||
if (!hasPermission) {
|
||||
removeElement(el)
|
||||
}
|
||||
}
|
||||
|
||||
function removeElement(el: HTMLElement): void {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
const rolesDirective: RolesDirective = {
|
||||
mounted: checkRolePermission,
|
||||
updated: checkRolePermission
|
||||
}
|
||||
|
||||
export function setupRolesDirective(app: App): void {
|
||||
app.directive('roles', rolesDirective)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { App } from 'vue'
|
||||
import { setupAuthDirective, type AuthDirective } from './core/auth'
|
||||
import { setupRippleDirective, type RippleDirective } from './business/ripple'
|
||||
import { setupRolesDirective, type RolesDirective } from './core/roles'
|
||||
|
||||
export function setupGlobDirectives(app: App) {
|
||||
setupAuthDirective(app) // 权限指令
|
||||
setupRolesDirective(app) // 角色权限指令
|
||||
setupRippleDirective(app) // 水波纹指令
|
||||
}
|
||||
|
||||
export type { AuthDirective, RippleDirective, RolesDirective }
|
||||
Reference in New Issue
Block a user