diff --git a/CLAUDE.md b/CLAUDE.md index 4b64201..3c12a38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,35 +1,5 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -### 第一部分:核心编程原则 (Guiding Principles) -这是我们合作的顶层思想,指导所有具体的行为。 -可读性优先 (Readability First):始终牢记“代码是写给人看的,只是恰好机器可以执行”。清晰度高于一切。 -DRY (Don't Repeat Yourself):绝不复制代码片段。通过抽象(如函数、类、模块)来封装和复用通用逻辑。 -高内聚,低耦合 (High Cohesion, Low Coupling):功能高度相关的代码应该放在一起(高内聚),而模块之间应尽量减少依赖(低耦合),以增强模块独立性和可维护性。 - -### 第二部分:具体执行指令 (Actionable Instructions) -这是 Claude 在日常工作中需要严格遵守的具体操作指南。 -沟通与语言规范 -默认语言:请默认使用简体中文进行所有交流、解释和思考过程的陈述。 -代码与术语:所有代码实体(变量名、函数名、类名等)及技术术语(如库名、框架名、设计模式等)必须保持英文原文。 -注释规范:代码注释应使用中文。 -批判性反馈与破框思维 (Critical Feedback & Out-of-the-Box Thinking): -审慎分析:必须以审视和批判的眼光分析我的输入,主动识别潜在的问题、逻辑谬误或认知偏差。 -坦率直言:需要明确、直接地指出我思考中的盲点,并提供显著超越我当前思考框架的建议,以挑战我的预设。 -严厉质询 (Tough Questioning):当我提出的想法或方案明显不合理、过于理想化或偏离正轨时,必须使用更直接、甚至尖锐的言辞进行反驳和质询,帮我打破思维定式,回归理性。 -开发与调试策略 (Development & Debugging Strategy) -坚韧不拔的解决问题 (Tenacious Problem-Solving):当面对编译错误、逻辑不通或多次尝试失败时,绝不允许通过简化或伪造实现来“绕过”问题。 -逐个击破 (Incremental Debugging):必须坚持对错误和问题进行逐一分析、定位和修复。 - -### 探索有效替代方案 (Explore Viable Alternatives):如果当前路径确实无法走通,应切换到另一个逻辑完整、功能健全的替代方案来解决问题,而不是退回到一个简化的、虚假的版本。 -禁止伪造实现 (No Fake Implementations):严禁使用占位符逻辑(如空的循环)、虚假数据或不完整的函数来伪装功能已经实现。所有交付的代码都必须是意图明确且具备真实逻辑的。 -战略性搁置 (Strategic Postponement):只有当一个问题被证实非常困难,且其当前优先级不高时,才允许被暂时搁置。搁置时,必须以 TODO 形式在代码中或任务列表中明确标记,并清晰说明遇到的问题。在核心任务完成后,必须回过头来重新审视并解决这些被搁置的问题。 -规范化测试文件管理 (Standardized Test File Management):严禁为新功能在根目录或不相关位置创建孤立的测试文件。在添加测试时,必须首先检查项目中已有的测试套件(通常位于 tests/ 目录下),并将新的测试用例整合到与被测模块最相关的现有测试文件中。只有当确实没有合适的宿主文件时,才允许在 tests/ 目录下创建符合项目命名规范的新测试文件。 -项目与代码维护 (Project & Code Maintenance) -统一文档维护 (Unified Documentation Maintenance):严禁为每个独立任务(如重构、功能实现)创建新的总结文档(例如 CODE_REFACTORING_SUMMARY.md)。在任务完成后,必须优先检查项目中已有的相关文档(如 README.md、既有的设计文档等),并将新的总结、变更或补充内容直接整合到现有文档中,维护其完整性和时效性。 -及时清理 (Timely Cleanup):在完成开发任务时,如果发现任何已无用(过时)的代码、文件或注释,应主动提出清理建议。 - ## 项目概述 Coi Admin 是一个基于 Vue3、Vite5、TypeScript 和 Naive UI 的简洁后台管理模板,实现了完整的认证、权限管理、路由管理等功能。 @@ -1247,6 +1217,198 @@ import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' **遵循此规范确保所有表格列表页面具有统一的按钮样式和用户体验!** +## 📋 表格操作按钮标准样式规范(强制要求) + +**所有新建的表格列表页面中的操作按钮都必须严格参考用户管理页面的按钮样式标准,确保界面一致性** + +### 参考标准页面 + +- **主要参考**:用户管理页面 `src/views/system/user/index.vue`(1021-1100行) +- **辅助参考**:角色管理页面、菜单管理页面 + +### 标准按钮实现规范 + +**✅ 正确的表格操作列按钮实现方式**: +```typescript +// 表格列定义 - 操作列 +{ + title: '操作', + key: 'actions', + width: 280, + align: 'center', + fixed: 'right', + render: (row) => { + const buttons = [] + + // 编辑按钮 - 主要操作按钮 + if (hasPermission('edit')) { + buttons.push(h(NButton, { + type: 'primary', + size: 'small', + class: 'action-btn-primary', + onClick: () => handleEdit(row), + }, { + icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { + default: () => h(IconParkOutlineEdit) + }), + default: () => '编辑', + })) + } + + // 删除按钮 - 危险操作按钮 + if (hasPermission('delete')) { + buttons.push(h(NPopconfirm, { + onPositiveClick: () => handleDelete(row.id), + negativeText: '取消', + positiveText: '确定', + }, { + default: () => '确定删除此记录吗?', + trigger: () => h(NButton, { + type: 'error', + secondary: true, + size: 'small', + class: 'action-btn-secondary action-btn-danger', + }, { + icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { + default: () => h(IconParkOutlineDelete), + }), + default: () => '删除', + }), + })) + } + + // 其他功能按钮 - 辅助操作按钮 + if (hasPermission('other')) { + buttons.push(h(NButton, { + type: 'warning', + secondary: true, + size: 'small', + class: 'action-btn-secondary action-btn-warning', + onClick: () => handleOtherAction(row), + }, { + icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { + default: () => h(IconParkOutlineSetting), + }), + default: () => '设置', + })) + } + + return h('div', { class: 'flex items-center justify-center gap-2' }, buttons) + }, +} +``` + +### 按钮样式核心标准 + +**1. 按钮基础属性**: +- ✅ `size: 'small'` - 统一小尺寸按钮 +- ✅ 不使用 `round` 属性 - 标准方形按钮 +- ✅ 必须包含 `class` 样式类用于主题适配 + +**2. 按钮类型规范**: +- ✅ 主要编辑操作:`type: 'primary'` +- ✅ 删除危险操作:`type: 'error', secondary: true` +- ✅ 警告类操作:`type: 'warning', secondary: true` +- ✅ 信息类操作:`type: 'info', secondary: true` + +**3. 图标规范**: +- ✅ 图标尺寸:`size: 14` +- ✅ 图标位置调整:`style: 'transform: translateY(-1px)'` +- ✅ 图标必须语义化,与操作功能匹配 + +**4. 按钮布局规范**: +- ✅ 容器样式:`class: 'flex items-center justify-center gap-2'` +- ✅ 按钮间距:`gap-2`(8px间距) +- ✅ 垂直居中对齐 +- ✅ 水平居中对齐 + +**5. 操作列配置**: +- ✅ 列宽度:`width: 280` 或根据按钮数量调整 +- ✅ 对齐方式:`align: 'center'` +- ✅ 固定位置:`fixed: 'right'` + +### 必需的图标导入 + +```typescript +// 在文件顶部导入所需图标组件 +import IconParkOutlineEdit from '~icons/icon-park-outline/edit' +import IconParkOutlineDelete from '~icons/icon-park-outline/delete' +import IconParkOutlineSetting from '~icons/icon-park-outline/setting' +import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh' +import IconParkOutlineUserPositioning from '~icons/icon-park-outline/user-positioning' +``` + +### 强制要求检查清单 + +开发新的表格列表页面时,必须确保: +- [ ] 参考用户管理页面的按钮样式实现 +- [ ] 按钮使用标准方形样式(不使用 `round` 属性) +- [ ] 所有按钮都有图标 + 文字的组合形式 +- [ ] 图标尺寸和位置调整一致:`size: 14, style: 'transform: translateY(-1px)'` +- [ ] 按钮容器使用标准布局:`flex items-center justify-center gap-2` +- [ ] 按钮类型和颜色符合功能语义 +- [ ] 操作列配置符合标准(宽度、对齐、固定位置) +- [ ] 删除等危险操作使用确认弹框(NPopconfirm) + +### 严格禁止行为 + +- ❌ 偏离用户管理页面的按钮样式标准 +- ❌ 使用圆角按钮(`round` 属性) +- ❌ 按钮缺少图标或文字 +- ❌ 图标尺寸和位置不统一 +- ❌ 按钮间距不一致 +- ❌ 不使用语义化的按钮类型 +- ❌ 危险操作不使用确认弹框 + +### 按钮样式类参考 + +```css +/* 这些样式类已在用户管理页面中定义 */ +.action-btn-primary /* 主要按钮样式 */ +.action-btn-secondary /* 辅助按钮样式 */ +.action-btn-danger /* 危险按钮样式 */ +.action-btn-warning /* 警告按钮样式 */ +.action-btn-info /* 信息按钮样式 */ +``` + +### 典型按钮组合示例 + +```typescript +// 常见的CRUD操作按钮组合 +const buttons = [] + +// 1. 编辑(主要操作) +buttons.push(h(NButton, { + type: 'primary', + size: 'small', + class: 'action-btn-primary' + // ... 其他属性 +})) + +// 2. 删除(危险操作,带确认) +buttons.push(h(NPopconfirm, { + // ... 确认属性 + trigger: () => h(NButton, { + type: 'error', + secondary: true, + size: 'small', + class: 'action-btn-secondary action-btn-danger' + // ... 其他属性 + }) +})) + +// 3. 其他功能按钮(辅助操作) +buttons.push(h(NButton, { + type: 'info', + secondary: true, + size: 'small', + class: 'action-btn-secondary action-btn-info' + // ... 其他属性 +})) +``` + +**严格遵循用户管理页面的按钮样式标准,确保所有表格操作按钮具有统一的外观和交互体验!** + ## 📝 表单布局紧凑设计规范(强制要求) **所有新创建的表单页面和弹框表单都必须严格遵循紧凑布局设计规范,确保界面的一致性和空间利用率** diff --git a/src/service/http/handle.ts b/src/service/http/handle.ts index 420f972..eb8e450 100644 --- a/src/service/http/handle.ts +++ b/src/service/http/handle.ts @@ -49,6 +49,13 @@ export async function handleResponseError(response: Response) { Object.assign(error, { code: errorCode, message }) + // 检查是否是401未授权错误,直接执行登出 + if (response.status === 401) { + const authStore = useAuthStore() + authStore.logout() + return error + } + showError(error) return error @@ -69,6 +76,21 @@ export function handleBusinessError(data: Record, config: Required< data: data.data, } + // 检查是否是token失效的业务错误 + const errorMessage = data[msgKey] || '' + const isTokenExpired = errorMessage.includes('token 无效') + || errorMessage.includes('Token无效') + || errorMessage.includes('未提供有效的Token') + || errorMessage.includes('登录已过期') + || errorMessage.includes('未登录') + + if (isTokenExpired) { + // Token失效,执行登出逻辑 + const authStore = useAuthStore() + authStore.logout() + return error + } + showError(error) return error diff --git a/src/views/system/file/index.vue b/src/views/system/file/index.vue index c6ff670..917a495 100644 --- a/src/views/system/file/index.vue +++ b/src/views/system/file/index.vue @@ -518,6 +518,7 @@ const columns: DataTableColumns = [ buttons.push(h(NButton, { type: 'primary', size: 'small', + class: 'action-btn-primary', onClick: () => handleDownload(row), }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { @@ -537,7 +538,9 @@ const columns: DataTableColumns = [ default: () => '确定删除此文件吗?', trigger: () => h(NButton, { type: 'error', + secondary: true, size: 'small', + class: 'action-btn-secondary action-btn-danger', }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { default: () => h(IconParkOutlineDelete), @@ -972,4 +975,84 @@ onMounted(() => { background-color: #f5f5f5; color: #333; } + +/* 统一按钮样式系统 - 支持动态主题色彩 */ +.action-btn-primary { + background: var(--primary-color) !important; + border-color: var(--primary-color) !important; + color: #ffffff !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 2px 4px var(--primary-color-suppl) !important; +} + +.action-btn-primary:hover { + background: var(--primary-color-hover) !important; + border-color: var(--primary-color-hover) !important; + transform: translateY(-1px) !important; + box-shadow: 0 4px 8px var(--primary-color-suppl) !important; +} + +.action-btn-primary:active { + background: var(--primary-color-pressed) !important; + border-color: var(--primary-color-pressed) !important; + transform: translateY(0) !important; +} + +.action-btn-secondary { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important; +} + +.action-btn-secondary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important; +} + +.action-btn-secondary:active { + transform: translateY(0) !important; +} + +.action-btn-danger { + border-color: var(--error-color) !important; + color: var(--error-color) !important; +} + +.action-btn-danger:hover { + border-color: var(--error-color-hover) !important; + color: var(--error-color-hover) !important; + background: var(--error-color-suppl) !important; +} + +.action-btn-warning { + border-color: var(--warning-color) !important; + color: var(--warning-color) !important; +} + +.action-btn-warning:hover { + border-color: var(--warning-color-hover) !important; + color: var(--warning-color-hover) !important; + background: var(--warning-color-suppl) !important; +} + +.action-btn-info { + border-color: var(--info-color) !important; + color: var(--info-color) !important; +} + +.action-btn-info:hover { + border-color: var(--info-color-hover) !important; + color: var(--info-color-hover) !important; + background: var(--info-color-suppl) !important; +} + +.action-btn-success { + border-color: var(--success-color) !important; + color: var(--success-color) !important; +} + +.action-btn-success:hover { + border-color: var(--success-color-hover) !important; + color: var(--success-color-hover) !important; + background: var(--success-color-suppl) !important; +} diff --git a/src/views/system/loginlog/index.vue b/src/views/system/loginlog/index.vue index 2d209e5..2318761 100644 --- a/src/views/system/loginlog/index.vue +++ b/src/views/system/loginlog/index.vue @@ -370,7 +370,9 @@ const columns: DataTableColumns = [ default: () => '确定删除此登录记录吗?', trigger: () => h(NButton, { type: 'error', + secondary: true, size: 'small', + class: 'action-btn-secondary action-btn-danger', }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { default: () => h(IconParkOutlineDelete), @@ -380,7 +382,7 @@ const columns: DataTableColumns = [ })) } - return h('div', { class: 'flex items-center justify-center gap-1' }, buttons) + return h('div', { class: 'flex items-center justify-center gap-2' }, buttons) }, }, ] @@ -643,6 +645,32 @@ onMounted(() => { padding: 2px 6px; } +/* 统一按钮样式系统 - 支持动态主题色彩 */ +.action-btn-secondary { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important; +} + +.action-btn-secondary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important; +} + +.action-btn-secondary:active { + transform: translateY(0) !important; +} + +.action-btn-danger { + border-color: var(--error-color) !important; + color: var(--error-color) !important; +} + +.action-btn-danger:hover { + border-color: var(--error-color-hover) !important; + color: var(--error-color-hover) !important; + background: var(--error-color-suppl) !important; +} + /* 登录日志详情样式 */ .login-log-detail :deep(.n-descriptions-item-label) { font-weight: 600; diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 0f9828e..83ddf3a 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -835,7 +835,9 @@ const columns: DataTableColumns = [ if (row.menuType !== '3' && hasButton(PERMISSIONS.MENU.ADD)) { buttons.push(h(NButton, { type: 'success', + secondary: true, size: 'small', + class: 'action-btn-secondary action-btn-success', onClick: () => handleAddChild(row), }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { @@ -850,6 +852,7 @@ const columns: DataTableColumns = [ buttons.push(h(NButton, { type: 'primary', size: 'small', + class: 'action-btn-primary', onClick: () => handleEdit(row), }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { @@ -869,7 +872,9 @@ const columns: DataTableColumns = [ default: () => '确定删除此菜单吗?', trigger: () => h(NButton, { type: 'error', + secondary: true, size: 'small', + class: 'action-btn-secondary action-btn-danger', }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { default: () => h(IconParkOutlineDelete), @@ -879,7 +884,7 @@ const columns: DataTableColumns = [ })) } - return h('div', { class: 'flex items-center justify-center gap-1' }, buttons) + return h('div', { class: 'flex items-center justify-center gap-2' }, buttons) }, }, ] @@ -1763,4 +1768,84 @@ onMounted(() => { .custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover { background: #adb5bd; } + +/* 统一按钮样式系统 - 支持动态主题色彩 */ +.action-btn-primary { + background: var(--primary-color) !important; + border-color: var(--primary-color) !important; + color: #ffffff !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 2px 4px var(--primary-color-suppl) !important; +} + +.action-btn-primary:hover { + background: var(--primary-color-hover) !important; + border-color: var(--primary-color-hover) !important; + transform: translateY(-1px) !important; + box-shadow: 0 4px 8px var(--primary-color-suppl) !important; +} + +.action-btn-primary:active { + background: var(--primary-color-pressed) !important; + border-color: var(--primary-color-pressed) !important; + transform: translateY(0) !important; +} + +.action-btn-secondary { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important; +} + +.action-btn-secondary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important; +} + +.action-btn-secondary:active { + transform: translateY(0) !important; +} + +.action-btn-success { + border-color: var(--success-color) !important; + color: var(--success-color) !important; +} + +.action-btn-success:hover { + border-color: var(--success-color-hover) !important; + color: var(--success-color-hover) !important; + background: var(--success-color-suppl) !important; +} + +.action-btn-danger { + border-color: var(--error-color) !important; + color: var(--error-color) !important; +} + +.action-btn-danger:hover { + border-color: var(--error-color-hover) !important; + color: var(--error-color-hover) !important; + background: var(--error-color-suppl) !important; +} + +.action-btn-warning { + border-color: var(--warning-color) !important; + color: var(--warning-color) !important; +} + +.action-btn-warning:hover { + border-color: var(--warning-color-hover) !important; + color: var(--warning-color-hover) !important; + background: var(--warning-color-suppl) !important; +} + +.action-btn-info { + border-color: var(--info-color) !important; + color: var(--info-color) !important; +} + +.action-btn-info:hover { + border-color: var(--info-color-hover) !important; + color: var(--info-color-hover) !important; + background: var(--info-color-suppl) !important; +} diff --git a/src/views/system/operlog/index.vue b/src/views/system/operlog/index.vue index aa4e879..5a86b5f 100644 --- a/src/views/system/operlog/index.vue +++ b/src/views/system/operlog/index.vue @@ -460,6 +460,7 @@ const columns: DataTableColumns = [ buttons.push(h(NButton, { type: 'primary', size: 'small', + class: 'action-btn-primary', onClick: () => handleViewDetail(row), }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { @@ -478,7 +479,9 @@ const columns: DataTableColumns = [ default: () => '确定删除此操作记录吗?', trigger: () => h(NButton, { type: 'error', + secondary: true, size: 'small', + class: 'action-btn-secondary action-btn-danger', }, { icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, { default: () => h(IconParkOutlineDelete), @@ -488,7 +491,7 @@ const columns: DataTableColumns = [ })) } - return h('div', { class: 'flex items-center justify-center gap-1' }, buttons) + return h('div', { class: 'flex items-center justify-center gap-2' }, buttons) }, }, ] @@ -805,6 +808,53 @@ onMounted(() => { padding: 2px 6px; } +/* 统一按钮样式系统 - 支持动态主题色彩 */ +.action-btn-primary { + background: var(--primary-color) !important; + border-color: var(--primary-color) !important; + color: #ffffff !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 2px 4px var(--primary-color-suppl) !important; +} + +.action-btn-primary:hover { + background: var(--primary-color-hover) !important; + border-color: var(--primary-color-hover) !important; + transform: translateY(-1px) !important; + box-shadow: 0 4px 8px var(--primary-color-suppl) !important; +} + +.action-btn-primary:active { + background: var(--primary-color-pressed) !important; + border-color: var(--primary-color-pressed) !important; + transform: translateY(0) !important; +} + +.action-btn-secondary { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important; +} + +.action-btn-secondary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important; +} + +.action-btn-secondary:active { + transform: translateY(0) !important; +} + +.action-btn-danger { + border-color: var(--error-color) !important; + color: var(--error-color) !important; +} + +.action-btn-danger:hover { + border-color: var(--error-color-hover) !important; + color: var(--error-color-hover) !important; + background: var(--error-color-suppl) !important; +} + /* 操作日志详情样式 */ .oper-log-detail :deep(.n-descriptions-item-label) { font-weight: 600; diff --git a/src/views/system/picture/index.vue b/src/views/system/picture/index.vue index 8951d87..88a76f6 100644 --- a/src/views/system/picture/index.vue +++ b/src/views/system/picture/index.vue @@ -218,6 +218,7 @@ size="tiny" circle type="primary" + class="action-btn-primary" @click.stop="handleDownload(picture)" >