chore: 初始化平台管理端
25
.env
Normal file
@@ -0,0 +1,25 @@
|
||||
# 【通用】环境变量
|
||||
|
||||
# 版本号
|
||||
VITE_VERSION = 3.0.1
|
||||
|
||||
# 端口号
|
||||
VITE_PORT = 3006
|
||||
|
||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
||||
VITE_BASE_URL = /
|
||||
|
||||
# 权限模式【 frontend 前端模式 / backend 后端模式 】
|
||||
VITE_ACCESS_MODE = backend
|
||||
|
||||
# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
|
||||
VITE_WITH_CREDENTIALS = false
|
||||
|
||||
# 是否打开路由信息
|
||||
VITE_OPEN_ROUTE_INFO = false
|
||||
|
||||
# 锁屏加密密钥
|
||||
VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
|
||||
|
||||
# 腾讯地图 Key
|
||||
VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ
|
||||
19
.env.development
Normal file
@@ -0,0 +1,19 @@
|
||||
# 【开发】环境变量
|
||||
|
||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
||||
VITE_BASE_URL = /
|
||||
|
||||
# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址)
|
||||
VITE_API_URL = http://127.0.0.1:7801/
|
||||
|
||||
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
||||
VITE_API_PROXY_URL = http://127.0.0.1:7801/
|
||||
|
||||
# 腾讯地图 Key
|
||||
VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ
|
||||
|
||||
# 权限模式【 frontend 前端模式 / backend 后端模式 】
|
||||
VITE_ACCESS_MODE = backend
|
||||
|
||||
# Delete console
|
||||
VITE_DROP_CONSOLE = false
|
||||
16
.env.production
Normal file
@@ -0,0 +1,16 @@
|
||||
# 【生产】环境变量
|
||||
|
||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
||||
VITE_BASE_URL = /
|
||||
|
||||
# API 地址前缀
|
||||
VITE_API_URL = https://kjkj.qiyuesns.cn/
|
||||
|
||||
# 腾讯地图 Key
|
||||
VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ
|
||||
|
||||
# 权限模式【 frontend 前端模式 / backend 后端模式 】
|
||||
VITE_ACCESS_MODE = backend
|
||||
|
||||
# Delete console
|
||||
VITE_DROP_CONSOLE = true
|
||||
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.html linguist-detectable=false
|
||||
*.vue linguist-detectable=true
|
||||
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.cursorrules
|
||||
完整项目备份/
|
||||
|
||||
# Auto-generated files
|
||||
src/types/import/auto-imports.d.ts
|
||||
src/types/import/components.d.ts
|
||||
.auto-import.json
|
||||
|
||||
# Swagger exports
|
||||
document/swagger/
|
||||
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
pnpm dlx commitlint --edit $1
|
||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
pnpm run lint:lint-staged
|
||||
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/node_modules/*
|
||||
/dist/*
|
||||
/src/main.ts
|
||||
20
.prettierrc
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"vueIndentScriptAndStyle": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"insertPragma": false,
|
||||
"requirePragma": false,
|
||||
"proseWrap": "never",
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"endOfLine": "auto",
|
||||
"rangeStart": 0
|
||||
}
|
||||
9
.stylelintignore
Normal file
@@ -0,0 +1,9 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.husky
|
||||
.vscode
|
||||
|
||||
src/components/Layout/MenuLeft/index.vue
|
||||
src/assets
|
||||
stats.html
|
||||
82
.stylelintrc.cjs
Normal file
@@ -0,0 +1,82 @@
|
||||
module.exports = {
|
||||
// 继承推荐规范配置
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-recommended-scss',
|
||||
'stylelint-config-recommended-vue/scss',
|
||||
'stylelint-config-html/vue',
|
||||
'stylelint-config-recess-order'
|
||||
],
|
||||
// 指定不同文件对应的解析器
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.{vue,html}'],
|
||||
customSyntax: 'postcss-html'
|
||||
},
|
||||
{
|
||||
files: ['**/*.{css,scss}'],
|
||||
customSyntax: 'postcss-scss'
|
||||
}
|
||||
],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
|
||||
'selector-class-pattern': null, // 选择器类名命名规则
|
||||
'custom-property-pattern': null, // 自定义属性命名规则
|
||||
'keyframes-name-pattern': null, // 动画帧节点样式命名规则
|
||||
'no-descending-specificity': null, // 允许无降序特异性
|
||||
'no-empty-source': null, // 允许空样式
|
||||
'property-no-vendor-prefix': null, // 允许属性前缀
|
||||
// 允许 global 、export 、deep伪类
|
||||
'selector-pseudo-class-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ['global', 'export', 'deep']
|
||||
}
|
||||
],
|
||||
// 允许未知属性
|
||||
'property-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreProperties: []
|
||||
}
|
||||
],
|
||||
// 允许未知规则
|
||||
'at-rule-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: [
|
||||
'apply',
|
||||
'use',
|
||||
'mixin',
|
||||
'include',
|
||||
'extend',
|
||||
'each',
|
||||
'if',
|
||||
'else',
|
||||
'for',
|
||||
'while',
|
||||
'reference'
|
||||
]
|
||||
}
|
||||
],
|
||||
'scss/at-rule-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: [
|
||||
'apply',
|
||||
'use',
|
||||
'mixin',
|
||||
'include',
|
||||
'extend',
|
||||
'each',
|
||||
'if',
|
||||
'else',
|
||||
'for',
|
||||
'while',
|
||||
'reference'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
164
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Repository expectations
|
||||
|
||||
# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版
|
||||
|
||||
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
|
||||
|
||||
## 0. AI 交互核心约束 (元规则)
|
||||
|
||||
1. **语言**:必须使用**中文**回复与注释。
|
||||
2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。
|
||||
3. **环境感知**:
|
||||
- PowerShell 读取文件命令必须带 `-Encoding UTF8`。
|
||||
- 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。
|
||||
4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。
|
||||
5. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。
|
||||
|
||||
## 1. 技术栈详细版本
|
||||
|
||||
| 组件 | 版本/选型 | 用途说明 |
|
||||
| :-- | :-- | :-- |
|
||||
| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 |
|
||||
| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) |
|
||||
| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) |
|
||||
| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
|
||||
| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 |
|
||||
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
|
||||
| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 |
|
||||
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
|
||||
| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 |
|
||||
|
||||
## 2. 目录与分层(Strict Mapping)
|
||||
|
||||
**生成的代码必须严格归类到以下目录:**
|
||||
|
||||
- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。
|
||||
- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。
|
||||
- `src/config/`:全局配置、常量(如主题、布局)。
|
||||
- `src/directives/`:自定义指令,按功能拆文件。
|
||||
- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。
|
||||
- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。
|
||||
- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。
|
||||
- `src/mock/`:本地 mock 数据/接口。
|
||||
- `src/plugins/`:Vue 插件注册。
|
||||
- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。
|
||||
- `src/store/`:Pinia store,模块化放 `modules/`。
|
||||
- `src/types/`:公共类型定义(如 `types/common/response.ts`)。
|
||||
- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。
|
||||
- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。
|
||||
|
||||
## 3. 命名与代码风格
|
||||
|
||||
- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。
|
||||
- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。
|
||||
- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。
|
||||
- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。
|
||||
- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。
|
||||
- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。
|
||||
|
||||
## 4. 接口与 HTTP 规范 (含 .NET 兼容)
|
||||
|
||||
1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。
|
||||
2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。
|
||||
3. **Snowflake ID 精度处理 (关键)**:
|
||||
- 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。
|
||||
- **接收时**:确保后端 DTO 已序列化为 String。
|
||||
- **发送时**:前端保持 String 传输,由后端反序列化。
|
||||
4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。
|
||||
5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。
|
||||
6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。
|
||||
|
||||
## 5. 组件/页面开发规范
|
||||
|
||||
- **组织**:页面放 `views/`,复用组件抽到 `components/`。
|
||||
- **脚本语法**:全线使用 `<script setup lang="ts">`,禁止 Options API。
|
||||
- **Vue 3.5 新特性**:使用 Props 解构 (`const { count = 0 } = defineProps<{...}>`)。
|
||||
- **表单交互**:使用 Element Plus 表单校验;避免直接操作 DOM。
|
||||
- **Loading**:所有修改类操作必须绑定 `loading` 状态。
|
||||
|
||||
## 6. 状态管理规范 (Pinia)
|
||||
|
||||
- **定义**:Store 命名 `useXxxStore`,文件 `modules/xxx.ts`,使用 **Setup Store** 写法。
|
||||
- **持久化 (核心)**:
|
||||
- 默认通过 `pinia-plugin-persistedstate`。
|
||||
- **强制**:Storage Key **必须**由 `src/utils/StorageKeyManager` 生成,**严禁**硬编码字符串 Key。
|
||||
- **职责**:Store 保持纯逻辑,UI 只负责触发。
|
||||
|
||||
## 7. 路由与导航守卫
|
||||
|
||||
- **中心化鉴权**:动态路由加载、Token 校验、登出逻辑均在守卫中处理。
|
||||
- **回退机制**:401 登出后,守卫应记录 `redirect` 参数,登录后自动跳回。
|
||||
- **禁止硬跳**:禁止 `window.location`,统一用 router 实例。
|
||||
|
||||
## 8. 类型与可维护性
|
||||
|
||||
- **TypeScript 强制**:**严禁 Any**。接口/类型定义放 `types/` 或同级 `types.ts`。
|
||||
- **类型收窄**:必要时使用 `unknown` + 类型断言/守卫。
|
||||
- **API 类型**:请求必须泛型声明返回类型 `api.get<UserDto>(...)`。
|
||||
- **导入**:优先使用 `import type`。
|
||||
|
||||
## 9. 国际化与文案
|
||||
|
||||
- **全量覆盖**:所有可见文本走 `$t('key')`。
|
||||
- **同步维护**:新增 Key 时,必须同时更新 `zh-CN` 和 `en` (或其他语言) 文件。
|
||||
- **动态优先**:确认错误/成功提示复用后端返回 `message` 优先。
|
||||
|
||||
## 10. 样式与设计约束 (Tailwind First)
|
||||
|
||||
- **优先级**:
|
||||
1. **Tailwind CSS 4** (原子类) —— **首选**。
|
||||
2. Element Plus 变量 (`var(--el-color-primary)`)。
|
||||
3. Scoped SCSS (仅用于复杂定制)。
|
||||
- **主题配置**:禁止硬编码 HEX 色值,必须引用 `config` 或 CSS 变量。
|
||||
- **布局安全**:禁止内联 `style` 操作布局。
|
||||
|
||||
## 11. 工程化与脚本
|
||||
|
||||
- **常用脚本**:`pnpm dev`、`pnpm build`、`pnpm lint`、`pnpm commit`。
|
||||
- **提交规范**:Husky + lint-staged 已启用。
|
||||
- **Commitizen**:提交信息必须遵循 Conventional Commits (`feat:`, `fix:`, `docs:` 等)。
|
||||
|
||||
## 12. 性能与可用性
|
||||
|
||||
- **虚拟滚动**:长列表(>100条)使用 `vue-draggable-plus` 或 Virtual Table。
|
||||
- **按需加载**:路由组件使用 `() => import(...)`;ECharts 按需引入。
|
||||
- **资源优化**:导出/加密等计算密集型任务,若卡顿则考虑 Web Worker。
|
||||
- **反馈**:长链路操作补充进度条 (`nprogress`) 或 Loading。
|
||||
|
||||
## 13. 安全与合规
|
||||
|
||||
- **零信任**:前端**严禁**硬编码 Token、密钥、内网地址。
|
||||
- **XSS 防御**:使用 `v-html` 必须经过 `DOMPurify` 清洗。
|
||||
- **文件处理**:下载/导出使用 `file-saver`,避免在主线程进行大数据解析。
|
||||
|
||||
## 14. 绝对禁止事项 (AI 自检清单)
|
||||
|
||||
**生成代码前,请自查是否违反以下红线:**
|
||||
|
||||
1. [ ] **裸连 API**:是否直接使用 `axios` 或绕过 `utils/http`?
|
||||
2. [ ] **雪花算法灾难**:是否直接把后端的 `long` ID 当数字处理(导致精度丢失)?
|
||||
3. [ ] **持久化隐患**:是否手写了 Storage Key 而非调用 `StorageKeyManager`?
|
||||
4. [ ] **401 冗余**:是否在组件里重复处理了登出跳转?
|
||||
5. [ ] **文案写死**:是否在 Template 中直接写了中文?
|
||||
6. [ ] **路径地狱**:是否使用了 `../../` 而非 `@` 别名?
|
||||
7. [ ] **类型偷懒**:API 返回值是否标记为 `any`?
|
||||
8. [ ] **逻辑混乱**:是否在 `views` 下堆砌了本该在 `components` 的通用组件?
|
||||
9. [ ] **配置硬编码**:是否忽略了 `.env` 中的 `VITE_API_URL`?
|
||||
|
||||
## 15. Swagger 更新要求(新增)
|
||||
|
||||
- 每次进行任何开发动作前,必须先从 `http://127.0.0.1:7801/swagger/v1/swagger.json` 拉取最新 Swagger。
|
||||
- 拉取后的文件统一存放到 `document/swagger/` 目录,命名规范:`swaggerYYYYMMDDHHmmss.json`(当前年月日时分秒)。
|
||||
- 开发接口时必须以最新拉取的该 JSON 为对照源进行字段校对与更新。
|
||||
|
||||
---
|
||||
|
||||
# Working agreements
|
||||
|
||||
- 严格遵循上述 15 条规范与目录职责。
|
||||
- 保持代码可读、可测、可维护。
|
||||
- 你的目标是协助我构建一个企业级、健壮的 `TakeoutAdmin` 后台管理系统前端。
|
||||
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint",
|
||||
"lokalise.i18n-ally",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
12
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"volar.inlayHints.eventArgumentInInlineHandlers": true,
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"i18n-ally.localesPaths": ["src/locales/langs", "src/locales"],
|
||||
"i18n-ally.enabledParsers": ["json"],
|
||||
"i18n-ally.sourceLanguage": "zh",
|
||||
"i18n-ally.displayLanguage": "zh",
|
||||
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.namespace": true
|
||||
}
|
||||
157
AGENTS.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Repository expectations
|
||||
|
||||
# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版
|
||||
|
||||
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
|
||||
|
||||
## 0. AI 交互核心约束 (元规则)
|
||||
|
||||
1. **语言**:必须使用**中文**回复与注释。
|
||||
2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。
|
||||
3. **环境感知**:
|
||||
- PowerShell 读取文件命令必须带 `-Encoding UTF8`。
|
||||
- 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。
|
||||
4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。
|
||||
5. **推送优先级**:提交后推送时,优先使用 SSH 远程(`git@github.com:...`),避免 HTTPS TLS 问题。
|
||||
6. **推送默认**:提交后自动执行 `git push`,无需再次提醒或确认。
|
||||
7. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。
|
||||
|
||||
## 1. 技术栈详细版本
|
||||
|
||||
| 组件 | 版本/选型 | 用途说明 |
|
||||
| :-- | :-- | :-- |
|
||||
| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 |
|
||||
| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) |
|
||||
| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) |
|
||||
| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
|
||||
| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 |
|
||||
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
|
||||
| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 |
|
||||
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
|
||||
| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 |
|
||||
|
||||
## 2. 目录与分层(Strict Mapping)
|
||||
|
||||
**生成的代码必须严格归类到以下目录:**
|
||||
|
||||
- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。
|
||||
- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。
|
||||
- `src/config/`:全局配置、常量(如主题、布局)。
|
||||
- `src/directives/`:自定义指令,按功能拆文件。
|
||||
- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。
|
||||
- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。
|
||||
- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。
|
||||
- `src/mock/`:本地 mock 数据/接口。
|
||||
- `src/plugins/`:Vue 插件注册。
|
||||
- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。
|
||||
- `src/store/`:Pinia store,模块化放 `modules/`。
|
||||
- `src/types/`:公共类型定义(如 `types/common/response.ts`)。
|
||||
- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。
|
||||
- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。
|
||||
|
||||
## 3. 命名与代码风格
|
||||
|
||||
- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。
|
||||
- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。
|
||||
- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。
|
||||
- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。
|
||||
- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。
|
||||
- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。
|
||||
|
||||
## 4. 接口与 HTTP 规范 (含 .NET 兼容)
|
||||
|
||||
1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。
|
||||
2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。
|
||||
3. **Snowflake ID 精度处理 (关键)**:
|
||||
- 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。
|
||||
- **接收时**:确保后端 DTO 已序列化为 String。
|
||||
- **发送时**:前端保持 String 传输,由后端反序列化。
|
||||
4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。
|
||||
5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。
|
||||
6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。
|
||||
|
||||
## 5. 组件/页面开发规范
|
||||
|
||||
- **组织**:页面放 `views/`,复用组件抽到 `components/`。
|
||||
- **脚本语法**:全线使用 `<script setup lang="ts">`,禁止 Options API。
|
||||
- **Vue 3.5 新特性**:使用 Props 解构 (`const { count = 0 } = defineProps<{...}>`)。
|
||||
- **表单交互**:使用 Element Plus 表单校验;避免直接操作 DOM。
|
||||
- **Loading**:所有修改类操作必须绑定 `loading` 状态。
|
||||
|
||||
## 6. 状态管理规范 (Pinia)
|
||||
|
||||
- **定义**:Store 命名 `useXxxStore`,文件 `modules/xxx.ts`,使用 **Setup Store** 写法。
|
||||
- **持久化 (核心)**:
|
||||
- 默认通过 `pinia-plugin-persistedstate`。
|
||||
- **强制**:Storage Key **必须**由 `src/utils/StorageKeyManager` 生成,**严禁**硬编码字符串 Key。
|
||||
- **职责**:Store 保持纯逻辑,UI 只负责触发。
|
||||
|
||||
## 7. 路由与导航守卫
|
||||
|
||||
- **中心化鉴权**:动态路由加载、Token 校验、登出逻辑均在守卫中处理。
|
||||
- **回退机制**:401 登出后,守卫应记录 `redirect` 参数,登录后自动跳回。
|
||||
- **禁止硬跳**:禁止 `window.location`,统一用 router 实例。
|
||||
|
||||
## 8. 类型与可维护性
|
||||
|
||||
- **TypeScript 强制**:**严禁 Any**。接口/类型定义放 `types/` 或同级 `types.ts`。
|
||||
- **类型收窄**:必要时使用 `unknown` + 类型断言/守卫。
|
||||
- **API 类型**:请求必须泛型声明返回类型 `api.get<UserDto>(...)`。
|
||||
- **导入**:优先使用 `import type`。
|
||||
|
||||
## 9. 国际化与文案
|
||||
|
||||
- **全量覆盖**:所有可见文本走 `$t('key')`。
|
||||
- **同步维护**:新增 Key 时,必须同时更新 `zh-CN` 和 `en` (或其他语言) 文件。
|
||||
- **动态优先**:确认错误/成功提示复用后端返回 `message` 优先。
|
||||
|
||||
## 10. 样式与设计约束 (Tailwind First)
|
||||
|
||||
- **优先级**:
|
||||
1. **Tailwind CSS 4** (原子类) —— **首选**。
|
||||
2. Element Plus 变量 (`var(--el-color-primary)`)。
|
||||
3. Scoped SCSS (仅用于复杂定制)。
|
||||
- **主题配置**:禁止硬编码 HEX 色值,必须引用 `config` 或 CSS 变量。
|
||||
- **布局安全**:禁止内联 `style` 操作布局。
|
||||
|
||||
## 11. 工程化与脚本
|
||||
|
||||
- **常用脚本**:`pnpm dev`、`pnpm build`、`pnpm lint`、`pnpm commit`。
|
||||
- **Lint 要求**:每次修改代码后必须执行 `pnpm lint`,确保无错误和警告。
|
||||
- **提交规范**:Husky + lint-staged 已启用。
|
||||
- **Commitizen**:提交信息必须遵循 Conventional Commits,格式为 `<type>: <中文说明>`(类型英文,说明中文)。
|
||||
|
||||
## 12. 性能与可用性
|
||||
|
||||
- **虚拟滚动**:长列表(>100条)使用 `vue-draggable-plus` 或 Virtual Table。
|
||||
- **按需加载**:路由组件使用 `() => import(...)`;ECharts 按需引入。
|
||||
- **资源优化**:导出/加密等计算密集型任务,若卡顿则考虑 Web Worker。
|
||||
- **反馈**:长链路操作补充进度条 (`nprogress`) 或 Loading。
|
||||
|
||||
## 13. 安全与合规
|
||||
|
||||
- **零信任**:前端**严禁**硬编码 Token、密钥、内网地址。
|
||||
- **XSS 防御**:使用 `v-html` 必须经过 `DOMPurify` 清洗。
|
||||
- **文件处理**:下载/导出使用 `file-saver`,避免在主线程进行大数据解析。
|
||||
|
||||
## 14. 绝对禁止事项 (AI 自检清单)
|
||||
|
||||
**生成代码前,请自查是否违反以下红线:**
|
||||
|
||||
1. [ ] **裸连 API**:是否直接使用 `axios` 或绕过 `utils/http`?
|
||||
2. [ ] **雪花算法灾难**:是否直接把后端的 `long` ID 当数字处理(导致精度丢失)?
|
||||
3. [ ] **持久化隐患**:是否手写了 Storage Key 而非调用 `StorageKeyManager`?
|
||||
4. [ ] **401 冗余**:是否在组件里重复处理了登出跳转?
|
||||
5. [ ] **文案写死**:是否在 Template 中直接写了中文?
|
||||
6. [ ] **路径地狱**:是否使用了 `../../` 而非 `@` 别名?
|
||||
7. [ ] **类型偷懒**:API 返回值是否标记为 `any`?
|
||||
8. [ ] **逻辑混乱**:是否在 `views` 下堆砌了本该在 `components` 的通用组件?
|
||||
9. [ ] **配置硬编码**:是否忽略了 `.env` 中的 `VITE_API_URL`?
|
||||
|
||||
---
|
||||
|
||||
# Working agreements
|
||||
|
||||
- 严格遵循上述 14 条规范与目录职责。
|
||||
- 保持代码可读、可测、可维护。
|
||||
- 你的目标是协助我构建一个企业级、健壮的 `TakeoutAdmin` 后台管理系统前端。
|
||||
157
CLAUDE.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Repository expectations
|
||||
|
||||
# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版
|
||||
|
||||
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
|
||||
|
||||
## 0. AI 交互核心约束 (元规则)
|
||||
|
||||
1. **语言**:必须使用**中文**回复与注释。
|
||||
2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。
|
||||
3. **环境感知**:
|
||||
- PowerShell 读取文件命令必须带 `-Encoding UTF8`。
|
||||
- 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。
|
||||
4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。
|
||||
5. **推送优先级**:提交后推送时,优先使用 SSH 远程(`git@github.com:...`),避免 HTTPS TLS 问题。
|
||||
6. **推送默认**:提交后自动执行 `git push`,无需再次提醒或确认。
|
||||
7. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。
|
||||
|
||||
## 1. 技术栈详细版本
|
||||
|
||||
| 组件 | 版本/选型 | 用途说明 |
|
||||
| :-- | :-- | :-- |
|
||||
| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 |
|
||||
| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) |
|
||||
| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) |
|
||||
| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
|
||||
| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 |
|
||||
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
|
||||
| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 |
|
||||
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
|
||||
| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 |
|
||||
|
||||
## 2. 目录与分层(Strict Mapping)
|
||||
|
||||
**生成的代码必须严格归类到以下目录:**
|
||||
|
||||
- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。
|
||||
- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。
|
||||
- `src/config/`:全局配置、常量(如主题、布局)。
|
||||
- `src/directives/`:自定义指令,按功能拆文件。
|
||||
- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。
|
||||
- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。
|
||||
- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。
|
||||
- `src/mock/`:本地 mock 数据/接口。
|
||||
- `src/plugins/`:Vue 插件注册。
|
||||
- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。
|
||||
- `src/store/`:Pinia store,模块化放 `modules/`。
|
||||
- `src/types/`:公共类型定义(如 `types/common/response.ts`)。
|
||||
- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。
|
||||
- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。
|
||||
|
||||
## 3. 命名与代码风格
|
||||
|
||||
- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。
|
||||
- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。
|
||||
- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。
|
||||
- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。
|
||||
- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。
|
||||
- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。
|
||||
|
||||
## 4. 接口与 HTTP 规范 (含 .NET 兼容)
|
||||
|
||||
1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。
|
||||
2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。
|
||||
3. **Snowflake ID 精度处理 (关键)**:
|
||||
- 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。
|
||||
- **接收时**:确保后端 DTO 已序列化为 String。
|
||||
- **发送时**:前端保持 String 传输,由后端反序列化。
|
||||
4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。
|
||||
5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。
|
||||
6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。
|
||||
|
||||
## 5. 组件/页面开发规范
|
||||
|
||||
- **组织**:页面放 `views/`,复用组件抽到 `components/`。
|
||||
- **脚本语法**:全线使用 `<script setup lang="ts">`,禁止 Options API。
|
||||
- **Vue 3.5 新特性**:使用 Props 解构 (`const { count = 0 } = defineProps<{...}>`)。
|
||||
- **表单交互**:使用 Element Plus 表单校验;避免直接操作 DOM。
|
||||
- **Loading**:所有修改类操作必须绑定 `loading` 状态。
|
||||
|
||||
## 6. 状态管理规范 (Pinia)
|
||||
|
||||
- **定义**:Store 命名 `useXxxStore`,文件 `modules/xxx.ts`,使用 **Setup Store** 写法。
|
||||
- **持久化 (核心)**:
|
||||
- 默认通过 `pinia-plugin-persistedstate`。
|
||||
- **强制**:Storage Key **必须**由 `src/utils/StorageKeyManager` 生成,**严禁**硬编码字符串 Key。
|
||||
- **职责**:Store 保持纯逻辑,UI 只负责触发。
|
||||
|
||||
## 7. 路由与导航守卫
|
||||
|
||||
- **中心化鉴权**:动态路由加载、Token 校验、登出逻辑均在守卫中处理。
|
||||
- **回退机制**:401 登出后,守卫应记录 `redirect` 参数,登录后自动跳回。
|
||||
- **禁止硬跳**:禁止 `window.location`,统一用 router 实例。
|
||||
|
||||
## 8. 类型与可维护性
|
||||
|
||||
- **TypeScript 强制**:**严禁 Any**。接口/类型定义放 `types/` 或同级 `types.ts`。
|
||||
- **类型收窄**:必要时使用 `unknown` + 类型断言/守卫。
|
||||
- **API 类型**:请求必须泛型声明返回类型 `api.get<UserDto>(...)`。
|
||||
- **导入**:优先使用 `import type`。
|
||||
|
||||
## 9. 国际化与文案
|
||||
|
||||
- **全量覆盖**:所有可见文本走 `$t('key')`。
|
||||
- **同步维护**:新增 Key 时,必须同时更新 `zh-CN` 和 `en` (或其他语言) 文件。
|
||||
- **动态优先**:确认错误/成功提示复用后端返回 `message` 优先。
|
||||
|
||||
## 10. 样式与设计约束 (Tailwind First)
|
||||
|
||||
- **优先级**:
|
||||
1. **Tailwind CSS 4** (原子类) —— **首选**。
|
||||
2. Element Plus 变量 (`var(--el-color-primary)`)。
|
||||
3. Scoped SCSS (仅用于复杂定制)。
|
||||
- **主题配置**:禁止硬编码 HEX 色值,必须引用 `config` 或 CSS 变量。
|
||||
- **布局安全**:禁止内联 `style` 操作布局。
|
||||
|
||||
## 11. 工程化与脚本
|
||||
|
||||
- **常用脚本**:`pnpm dev`、`pnpm build`、`pnpm lint`、`pnpm commit`。
|
||||
- **Lint 要求**:每次修改代码后必须执行 `pnpm lint`,确保无错误和警告。
|
||||
- **提交规范**:Husky + lint-staged 已启用。
|
||||
- **Commitizen**:提交信息必须遵循 Conventional Commits,格式为 `<type>: <中文说明>`(类型英文,说明中文)。
|
||||
|
||||
## 12. 性能与可用性
|
||||
|
||||
- **虚拟滚动**:长列表(>100条)使用 `vue-draggable-plus` 或 Virtual Table。
|
||||
- **按需加载**:路由组件使用 `() => import(...)`;ECharts 按需引入。
|
||||
- **资源优化**:导出/加密等计算密集型任务,若卡顿则考虑 Web Worker。
|
||||
- **反馈**:长链路操作补充进度条 (`nprogress`) 或 Loading。
|
||||
|
||||
## 13. 安全与合规
|
||||
|
||||
- **零信任**:前端**严禁**硬编码 Token、密钥、内网地址。
|
||||
- **XSS 防御**:使用 `v-html` 必须经过 `DOMPurify` 清洗。
|
||||
- **文件处理**:下载/导出使用 `file-saver`,避免在主线程进行大数据解析。
|
||||
|
||||
## 14. 绝对禁止事项 (AI 自检清单)
|
||||
|
||||
**生成代码前,请自查是否违反以下红线:**
|
||||
|
||||
1. [ ] **裸连 API**:是否直接使用 `axios` 或绕过 `utils/http`?
|
||||
2. [ ] **雪花算法灾难**:是否直接把后端的 `long` ID 当数字处理(导致精度丢失)?
|
||||
3. [ ] **持久化隐患**:是否手写了 Storage Key 而非调用 `StorageKeyManager`?
|
||||
4. [ ] **401 冗余**:是否在组件里重复处理了登出跳转?
|
||||
5. [ ] **文案写死**:是否在 Template 中直接写了中文?
|
||||
6. [ ] **路径地狱**:是否使用了 `../../` 而非 `@` 别名?
|
||||
7. [ ] **类型偷懒**:API 返回值是否标记为 `any`?
|
||||
8. [ ] **逻辑混乱**:是否在 `views` 下堆砌了本该在 `components` 的通用组件?
|
||||
9. [ ] **配置硬编码**:是否忽略了 `.env` 中的 `VITE_API_URL`?
|
||||
|
||||
---
|
||||
|
||||
# Working agreements
|
||||
|
||||
- 严格遵循上述 14 条规范与目录职责。
|
||||
- 保持代码可读、可测、可维护。
|
||||
- 你的目标是协助我构建一个企业级、健壮的 `TakeoutAdmin` 后台管理系统前端。
|
||||
160
GEMINI.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Repository expectations
|
||||
|
||||
# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版
|
||||
|
||||
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
|
||||
|
||||
## 0. AI 交互核心约束 (元规则)
|
||||
|
||||
1. **语言**:必须使用**中文**回复与注释。
|
||||
2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。
|
||||
3. **环境感知**:
|
||||
- PowerShell 读取文件命令必须带 `-Encoding UTF8`。
|
||||
- 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。
|
||||
4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。
|
||||
5. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。
|
||||
|
||||
## 1. 技术栈详细版本
|
||||
|
||||
| 组件 | 版本/选型 | 用途说明 |
|
||||
| :-- | :-- | :-- |
|
||||
| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 |
|
||||
| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) |
|
||||
| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) |
|
||||
| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
|
||||
| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 |
|
||||
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
|
||||
| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 |
|
||||
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
|
||||
| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 |
|
||||
|
||||
## 2. 目录与分层(Strict Mapping)
|
||||
|
||||
**生成的代码必须严格归类到以下目录:**
|
||||
|
||||
- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。
|
||||
- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。
|
||||
- `src/config/`:全局配置、常量(如主题、布局)。
|
||||
- `src/directives/`:自定义指令,按功能拆文件。
|
||||
- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。
|
||||
- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。
|
||||
- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。
|
||||
- `src/mock/`:本地 mock 数据/接口。
|
||||
- `src/plugins/`:Vue 插件注册。
|
||||
- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。
|
||||
- `src/store/`:Pinia store,模块化放 `modules/`。
|
||||
- `src/types/`:公共类型定义(如 `types/common/response.ts`)。
|
||||
- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。
|
||||
- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。
|
||||
|
||||
## 3. 命名与代码风格
|
||||
|
||||
- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。
|
||||
- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。
|
||||
- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。
|
||||
- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。
|
||||
- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。
|
||||
- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。
|
||||
|
||||
## 4. 接口与 HTTP 规范 (含 .NET 兼容)
|
||||
|
||||
1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。
|
||||
2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。
|
||||
3. **Snowflake ID 精度处理 (关键)**:
|
||||
- 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。
|
||||
- **接收时**:确保后端 DTO 已序列化为 String。
|
||||
- **发送时**:前端保持 String 传输,由后端反序列化。
|
||||
4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。
|
||||
5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。
|
||||
6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。
|
||||
|
||||
## 5. 组件/页面开发规范
|
||||
|
||||
- **组织**:页面放 `views/`,复用组件抽到 `components/`。
|
||||
- **脚本语法**:全线使用 `<script setup lang="ts">`,禁止 Options API。
|
||||
- **Vue 3.5 新特性**:使用 Props 解构 (`const { count = 0 } = defineProps<{...}>`)。
|
||||
- **表单交互**:使用 Element Plus 表单校验;避免直接操作 DOM。
|
||||
- **Loading**:所有修改类操作必须绑定 `loading` 状态。
|
||||
|
||||
## 6. 状态管理规范 (Pinia)
|
||||
|
||||
- **定义**:Store 命名 `useXxxStore`,文件 `modules/xxx.ts`,使用 **Setup Store** 写法。
|
||||
- **持久化 (核心)**:
|
||||
- 默认通过 `pinia-plugin-persistedstate`。
|
||||
- **强制**:Storage Key **必须**由 `src/utils/StorageKeyManager` 生成,**严禁**硬编码字符串 Key。
|
||||
- **职责**:Store 保持纯逻辑,UI 只负责触发。
|
||||
|
||||
## 7. 路由与导航守卫
|
||||
|
||||
- **中心化鉴权**:动态路由加载、Token 校验、登出逻辑均在守卫中处理。
|
||||
- **回退机制**:401 登出后,守卫应记录 `redirect` 参数,登录后自动跳回。
|
||||
- **禁止硬跳**:禁止 `window.location`,统一用 router 实例。
|
||||
|
||||
## 8. 类型与可维护性
|
||||
|
||||
- **TypeScript 强制**:**严禁 Any**。接口/类型定义放 `types/` 或同级 `types.ts`。
|
||||
- **类型收窄**:必要时使用 `unknown` + 类型断言/守卫。
|
||||
- **API 类型**:请求必须泛型声明返回类型 `api.get<UserDto>(...)`。
|
||||
- **导入**:优先使用 `import type`。
|
||||
|
||||
## 9. 国际化与文案
|
||||
|
||||
- **全量覆盖**:所有可见文本走 `$t('key')`。
|
||||
- **同步维护**:新增 Key 时,必须同时更新 `zh-CN` 和 `en` (或其他语言) 文件。
|
||||
- **动态优先**:确认错误/成功提示复用后端返回 `message` 优先。
|
||||
|
||||
## 10. 样式与设计约束 (Tailwind First)
|
||||
|
||||
- **优先级**:
|
||||
1. **Tailwind CSS 4** (原子类) —— **首选**。
|
||||
2. Element Plus 变量 (`var(--el-color-primary)`)。
|
||||
3. Scoped SCSS (仅用于复杂定制)。
|
||||
- **主题配置**:禁止硬编码 HEX 色值,必须引用 `config` 或 CSS 变量。
|
||||
- **布局安全**:禁止内联 `style` 操作布局。
|
||||
|
||||
## 11. 工程化与脚本
|
||||
|
||||
- **常用脚本**:`pnpm dev`、`pnpm build`、`pnpm lint`、`pnpm commit`。
|
||||
- **提交规范**:Husky + lint-staged 已启用。
|
||||
- **Commitizen**:提交信息必须遵循 Conventional Commits (`feat:`, `fix:`, `docs:` 等)。
|
||||
|
||||
## 12. 性能与可用性
|
||||
|
||||
- **虚拟滚动**:长列表(>100条)使用 `vue-draggable-plus` 或 Virtual Table。
|
||||
- **按需加载**:路由组件使用 `() => import(...)`;ECharts 按需引入。
|
||||
- **资源优化**:导出/加密等计算密集型任务,若卡顿则考虑 Web Worker。
|
||||
- **反馈**:长链路操作补充进度条 (`nprogress`) 或 Loading。
|
||||
|
||||
## 13. 安全与合规
|
||||
|
||||
- **零信任**:前端**严禁**硬编码 Token、密钥、内网地址。
|
||||
- **XSS 防御**:使用 `v-html` 必须经过 `DOMPurify` 清洗。
|
||||
- **文件处理**:下载/导出使用 `file-saver`,避免在主线程进行大数据解析。
|
||||
|
||||
## 14. 绝对禁止事项 (AI 自检清单)
|
||||
|
||||
**生成代码前,请自查是否违反以下红线:**
|
||||
|
||||
1. [ ] **裸连 API**:是否直接使用 `axios` 或绕过 `utils/http`?
|
||||
2. [ ] **雪花算法灾难**:是否直接把后端的 `long` ID 当数字处理(导致精度丢失)?
|
||||
3. [ ] **持久化隐患**:是否手写了 Storage Key 而非调用 `StorageKeyManager`?
|
||||
4. [ ] **401 冗余**:是否在组件里重复处理了登出跳转?
|
||||
5. [ ] **文案写死**:是否在 Template 中直接写了中文?
|
||||
6. [ ] **路径地狱**:是否使用了 `../../` 而非 `@` 别名?
|
||||
7. [ ] **类型偷懒**:API 返回值是否标记为 `any`?
|
||||
8. [ ] **逻辑混乱**:是否在 `views` 下堆砌了本该在 `components` 的通用组件?
|
||||
9. [ ] **配置硬编码**:是否忽略了 `.env` 中的 `VITE_API_URL`?
|
||||
|
||||
---
|
||||
|
||||
# Working agreements
|
||||
|
||||
- 严格遵循上述 14 条规范与目录职责。
|
||||
- 保持代码可读、可测、可维护。
|
||||
- 你的目标是协助我构建一个企业级、健壮的 `TakeoutAdmin` 后台管理系统前端。
|
||||
|
||||
## 15. Swagger 更新要求(新增)
|
||||
|
||||
- 每次进行任何开发动作前,必须先从 `http://127.0.0.1:7801/swagger/v1/swagger.json` 拉取最新 Swagger。
|
||||
- 拉取后的文件统一存放到 `document/swagger/` 目录,命名规范:`swaggerYYYYMMDDHHmmss.json`(当前年月日时分秒)。
|
||||
- 开发接口时必须以最新拉取的该 JSON 为对照源进行字段校对与更新。
|
||||
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# TakeoutSaaS AdminUI(平台管理员)
|
||||
|
||||
本仓库用于 **平台管理员** 的后台管理界面(Platform Admin)。
|
||||
|
||||
## 功能范围(当前)
|
||||
|
||||
- 租户管理(租户列表/审核/冻结/配额等)
|
||||
- 订阅与账单(批量延期/提醒、账单创建/收款确认/导出等)
|
||||
- 平台风控(门店审核、资质预警)
|
||||
- 平台公告 / 应用端公告(只读)
|
||||
- 系统管理(用户/权限/菜单/角色模板、系统字典等)
|
||||
|
||||
> 租户端(Tenant Admin)将迁移到独立仓库 `TakeoutSaaS.TenantUI`,本仓库后续会逐步移除/下线租户端页面入口,避免两端职责混淆。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
- `TakeoutSaaS.AdminUI/.env.development`:本地开发 API 地址与代理
|
||||
- `VITE_API_URL`
|
||||
- `VITE_API_PROXY_URL`
|
||||
- `TakeoutSaaS.AdminUI/.env.production`:生产环境 API 地址
|
||||
- `VITE_API_URL`
|
||||
|
||||
97
commitlint.config.cjs
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* commitlint 配置文件
|
||||
* 文档
|
||||
* https://commitlint.js.org/#/reference-rules
|
||||
* https://cz-git.qbb.sh/zh/guide/
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// 继承的规则
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
// 提交类型枚举,git提交type必须是以下类型
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat', // 新增功能
|
||||
'fix', // 修复缺陷
|
||||
'docs', // 文档变更
|
||||
'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
|
||||
'refactor', // 代码重构(不包括 bug 修复、功能新增)
|
||||
'perf', // 性能优化
|
||||
'test', // 添加疏漏测试或已有测试改动
|
||||
'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
|
||||
'ci', // 修改 CI 配置、脚本
|
||||
'revert', // 回滚 commit
|
||||
'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
|
||||
'wip' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
|
||||
]
|
||||
],
|
||||
'subject-case': [0] // subject大小写不做校验
|
||||
},
|
||||
|
||||
prompt: {
|
||||
messages: {
|
||||
type: '选择你要提交的类型 :',
|
||||
scope: '选择一个提交范围(可选):',
|
||||
customScope: '请输入自定义的提交范围 :',
|
||||
subject: '填写简短精炼的变更描述 :\n',
|
||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
|
||||
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
|
||||
footerPrefixesSelect: '选择关联issue前缀(可选):',
|
||||
customFooterPrefix: '输入自定义issue前缀 :',
|
||||
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
|
||||
generatingByAI: '正在通过 AI 生成你的提交简短描述...',
|
||||
generatedSelectByAI: '选择一个 AI 生成的简短描述:',
|
||||
confirmCommit: '是否提交或修改commit ?'
|
||||
},
|
||||
// prettier-ignore
|
||||
types: [
|
||||
{ value: "feat", name: "feat: 新增功能" },
|
||||
{ value: "fix", name: "fix: 修复缺陷" },
|
||||
{ value: "docs", name: "docs: 文档变更" },
|
||||
{ value: "style", name: "style: 代码格式(不影响功能,例如空格、分号等格式修正)" },
|
||||
{ value: "refactor", name: "refactor: 代码重构(不包括 bug 修复、功能新增)" },
|
||||
{ value: "perf", name: "perf: 性能优化" },
|
||||
{ value: "test", name: "test: 添加疏漏测试或已有测试改动" },
|
||||
{ value: "build", name: "build: 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)" },
|
||||
{ value: "ci", name: "ci: 修改 CI 配置、脚本" },
|
||||
{ value: "revert", name: "revert: 回滚 commit" },
|
||||
{ value: "chore", name: "chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)" },
|
||||
],
|
||||
useEmoji: true,
|
||||
emojiAlign: 'center',
|
||||
useAI: false,
|
||||
aiNumber: 1,
|
||||
themeColorCode: '',
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
customScopesAlign: 'bottom',
|
||||
customScopesAlias: 'custom',
|
||||
emptyScopesAlias: 'empty',
|
||||
upperCaseSubject: false,
|
||||
markBreakingChangeMode: false,
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
breaklineNumber: 100,
|
||||
breaklineChar: '|',
|
||||
skipQuestions: ['breaking', 'footerPrefix', 'footer'], // 跳过的步骤
|
||||
issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }],
|
||||
customIssuePrefixAlign: 'top',
|
||||
emptyIssuePrefixAlias: 'skip',
|
||||
customIssuePrefixAlias: 'custom',
|
||||
allowCustomIssuePrefix: true,
|
||||
allowEmptyIssuePrefix: true,
|
||||
confirmColorize: true,
|
||||
maxHeaderLength: Infinity,
|
||||
maxSubjectLength: Infinity,
|
||||
minSubjectLength: 0,
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: '',
|
||||
defaultIssues: '',
|
||||
defaultScope: '',
|
||||
defaultSubject: ''
|
||||
}
|
||||
}
|
||||
190
document/01项目结构.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# 项目结构
|
||||
|
||||
```text
|
||||
├── src
|
||||
│ ├── api # API 接口相关代码
|
||||
│ │ ├── auth.ts # 认证相关的 API 接口定义(如登录、注册、用户信息)
|
||||
│ │ └── system-manage.ts # 系统管理相关的 API 接口定义(如菜单、用户、角色管理)
|
||||
│ ├── App.vue # Vue 根组件,定义应用的全局结构和入口
|
||||
│ ├── assets # 静态资源目录
|
||||
│ │ ├── images # 图片资源目录
|
||||
│ │ ├── styles # 全局样式文件
|
||||
│ │ │ ├── core # 核心样式(系统级样式)
|
||||
│ │ │ ├── custom # 自定义样式(业务级样式)
|
||||
│ │ │ └── index.scss # 样式入口文件
|
||||
│ │ └── svg # SVG 相关资源
|
||||
│ │ └── loading.ts # 加载动画 SVG 定义
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── business # 业务组件(业务相关的自定义组件)
|
||||
│ │ │ └── comment-widget # 评论组件
|
||||
│ │ └── core # 核心组件(系统级通用组件库)
|
||||
│ │ ├── banners # 横幅组件
|
||||
│ │ ├── base # 基础组件
|
||||
│ │ ├── cards # 卡片组件
|
||||
│ │ ├── charts # 图表组件
|
||||
│ │ ├── forms # 表单组件
|
||||
│ │ ├── layouts # 布局组件
|
||||
│ │ ├── media # 媒体组件
|
||||
│ │ ├── others # 其他组件
|
||||
│ │ ├── tables # 表格组件
|
||||
│ │ ├── text-effect # 文本特效组件
|
||||
│ │ ├── theme # 主题相关组件
|
||||
│ │ ├── views # 视图组件
|
||||
│ │ └── widget # 小部件组件
|
||||
│ ├── config # 项目配置目录
|
||||
│ │ ├── assets # 静态资源配置
|
||||
│ │ │ └── images.ts # 图片资源路径配置
|
||||
│ │ ├── modules # 模块化配置
|
||||
│ │ │ ├── component.ts # 组件配置
|
||||
│ │ │ ├── fastEnter.ts # 快捷入口配置
|
||||
│ │ │ ├── festival.ts # 节日/活动配置
|
||||
│ │ │ └── headerBar.ts # 顶部栏配置
|
||||
│ │ ├── index.ts # 配置入口文件
|
||||
│ │ └── setting.ts # 系统设置配置
|
||||
│ ├── directives # Vue 自定义指令
|
||||
│ │ ├── business # 业务指令
|
||||
│ │ │ ├── highlight.ts # 高亮指令
|
||||
│ │ │ └── ripple.ts # 波纹效果指令
|
||||
│ │ ├── core # 核心指令
|
||||
│ │ │ ├── auth.ts # 认证指令
|
||||
│ │ │ └── roles.ts # 角色权限指令
|
||||
│ │ └── index.ts # 指令入口文件
|
||||
│ ├── enums # 枚举定义
|
||||
│ │ ├── appEnum.ts # 应用级枚举(如主题类型、语言类型)
|
||||
│ │ └── formEnum.ts # 表单相关枚举(如表单状态、验证规则)
|
||||
│ ├── env.d.ts # TypeScript 环境声明文件
|
||||
│ ├── hooks # Vue 3 Composable 函数(可复用逻辑)
|
||||
│ │ ├── core # 核心 Hooks
|
||||
│ │ │ ├── useAppMode.ts # 应用模式相关逻辑
|
||||
│ │ │ ├── useAuth.ts # 认证相关逻辑
|
||||
│ │ │ ├── useCeremony.ts # 节日/仪式相关逻辑
|
||||
│ │ │ ├── useChart.ts # 图表相关逻辑
|
||||
│ │ │ ├── useCommon.ts # 通用逻辑
|
||||
│ │ │ ├── useFastEnter.ts # 快捷入口逻辑
|
||||
│ │ │ ├── useHeaderBar.ts # 顶部栏逻辑
|
||||
│ │ │ ├── useLayoutHeight.ts # 布局高度计算逻辑
|
||||
│ │ │ ├── useTable.ts # 表格逻辑
|
||||
│ │ │ ├── useTableColumns.ts # 表格列配置逻辑
|
||||
│ │ │ ├── useTableHeight.ts # 表格高度计算逻辑
|
||||
│ │ │ └── useTheme.ts # 主题切换逻辑
|
||||
│ │ └── index.ts # Hooks 入口文件
|
||||
│ ├── locales # 国际化(i18n)资源
|
||||
│ │ ├── index.ts # 国际化入口文件
|
||||
│ │ └── langs # 多语言文件
|
||||
│ │ ├── en.json # 英文语言包
|
||||
│ │ └── zh.json # 中文语言包
|
||||
│ ├── main.ts # 项目主入口文件
|
||||
│ ├── mock # Mock 数据目录
|
||||
│ │ ├── json # JSON 格式的 Mock 数据
|
||||
│ │ │ └── chinaMap.json # 中国地图数据
|
||||
│ │ ├── temp # 临时 Mock 数据
|
||||
│ │ │ ├── articleList.ts # 文章列表数据
|
||||
│ │ │ ├── commentDetail.ts # 评论详情数据
|
||||
│ │ │ ├── commentList.ts # 评论列表数据
|
||||
│ │ │ └── formData.ts # 表单数据
|
||||
│ │ └── upgrade # 更新日志数据
|
||||
│ │ └── changeLog.ts # 变更日志数据
|
||||
│ ├── plugins # 插件配置
|
||||
│ │ ├── echarts.ts # ECharts 图表库配置
|
||||
│ │ └── index.ts # 插件入口文件
|
||||
│ ├── router # Vue Router 路由相关代码
|
||||
│ │ ├── core # 路由核心功能
|
||||
│ │ │ ├── ComponentLoader.ts # 组件加载器
|
||||
│ │ │ ├── IframeRouteManager.ts # Iframe 路由管理器
|
||||
│ │ │ ├── MenuProcessor.ts # 菜单处理器
|
||||
│ │ │ ├── RouteRegistry.ts # 路由注册器
|
||||
│ │ │ ├── RouteTransformer.ts # 路由转换器
|
||||
│ │ │ ├── RouteValidator.ts # 路由验证器
|
||||
│ │ │ └── index.ts # 核心功能入口
|
||||
│ │ ├── guards # 路由守卫
|
||||
│ │ │ ├── afterEach.ts # 全局后置守卫
|
||||
│ │ │ └── beforeEach.ts # 全局前置守卫
|
||||
│ │ ├── modules # 路由模块定义
|
||||
│ │ │ ├── article.ts # 文章模块路由
|
||||
│ │ │ ├── dashboard.ts # 仪表盘路由
|
||||
│ │ │ ├── examples.ts # 示例页面路由
|
||||
│ │ │ ├── exception.ts # 异常页面路由
|
||||
│ │ │ ├── help.ts # 帮助页面路由
|
||||
│ │ │ ├── index.ts # 路由模块入口
|
||||
│ │ │ ├── result.ts # 结果页面路由
|
||||
│ │ │ ├── safeguard.ts # 安全防护路由
|
||||
│ │ │ ├── system.ts # 系统管理路由
|
||||
│ │ │ ├── template.ts # 模板页面路由
|
||||
│ │ │ └── widgets.ts # 小组件路由
|
||||
│ │ ├── routes # 路由配置
|
||||
│ │ │ ├── asyncRoutes.ts # 异步路由(动态路由)
|
||||
│ │ │ └── staticRoutes.ts # 静态路由(固定路由)
|
||||
│ │ ├── index.ts # 路由主入口
|
||||
│ │ └── routesAlias.ts # 路由别名定义
|
||||
│ ├── store # Pinia 状态管理
|
||||
│ │ ├── modules # 状态管理模块
|
||||
│ │ │ ├── menu.ts # 菜单状态管理
|
||||
│ │ │ ├── setting.ts # 设置状态管理
|
||||
│ │ │ ├── table.ts # 表格状态管理
|
||||
│ │ │ ├── user.ts # 用户状态管理
|
||||
│ │ │ └── worktab.ts # 工作标签页状态管理
|
||||
│ │ └── index.ts # Pinia 入口文件
|
||||
│ ├── types # TypeScript 类型定义
|
||||
│ │ ├── api # API 相关类型
|
||||
│ │ │ └── api.d.ts # API 接口类型定义
|
||||
│ │ ├── common # 通用类型定义
|
||||
│ │ │ ├── index.ts # 通用类型入口
|
||||
│ │ │ └── response.ts # 响应类型定义
|
||||
│ │ ├── component # 组件相关类型
|
||||
│ │ │ ├── chart.ts # 图表组件类型
|
||||
│ │ │ └── index.ts # 组件类型入口
|
||||
│ │ ├── config # 配置相关类型
|
||||
│ │ │ └── index.ts # 配置类型定义
|
||||
│ │ ├── import # 自动导入类型声明
|
||||
│ │ │ ├── auto-imports.d.ts # 自动导入的函数类型
|
||||
│ │ │ └── components.d.ts # 自动导入的组件类型
|
||||
│ │ ├── router # 路由相关类型
|
||||
│ │ │ └── index.ts # 路由类型定义
|
||||
│ │ ├── store # 状态管理相关类型
|
||||
│ │ │ └── index.ts # Store 类型定义
|
||||
│ │ └── index.ts # 类型定义总入口
|
||||
│ ├── utils # 工具函数目录
|
||||
│ │ ├── constants # 常量定义
|
||||
│ │ │ ├── index.ts # 常量入口
|
||||
│ │ │ └── links.ts # 链接常量
|
||||
│ │ ├── form # 表单相关工具
|
||||
│ │ │ ├── index.ts # 表单工具入口
|
||||
│ │ │ ├── responsive.ts # 响应式表单工具
|
||||
│ │ │ └── validator.ts # 表单验证工具
|
||||
│ │ ├── http # HTTP 请求工具
|
||||
│ │ │ ├── error.ts # 错误处理
|
||||
│ │ │ ├── index.ts # HTTP 工具入口
|
||||
│ │ │ └── status.ts # 状态码处理
|
||||
│ │ ├── navigation # 导航相关工具
|
||||
│ │ │ ├── index.ts # 导航工具入口
|
||||
│ │ │ ├── jump.ts # 页面跳转工具
|
||||
│ │ │ ├── route.ts # 路由工具
|
||||
│ │ │ └── worktab.ts # 工作标签页工具
|
||||
│ │ ├── storage # 存储相关工具
|
||||
│ │ │ ├── index.ts # 存储工具入口
|
||||
│ │ │ ├── storage-config.ts # 存储配置
|
||||
│ │ │ ├── storage-key-manager.ts # 存储键管理
|
||||
│ │ │ └── storage.ts # 存储工具实现
|
||||
│ │ ├── sys # 系统相关工具
|
||||
│ │ │ ├── console.ts # 控制台工具
|
||||
│ │ │ ├── error-handle.ts # 错误处理
|
||||
│ │ │ ├── index.ts # 系统工具入口
|
||||
│ │ │ ├── mittBus.ts # 事件总线
|
||||
│ │ │ └── upgrade.ts # 升级相关工具
|
||||
│ │ ├── table # 表格相关工具
|
||||
│ │ │ ├── tableCache.ts # 表格缓存
|
||||
│ │ │ ├── tableConfig.ts # 表格配置
|
||||
│ │ │ └── tableUtils.ts # 表格工具函数
|
||||
│ │ ├── ui # UI 相关工具
|
||||
│ │ │ ├── animation.ts # 动画工具
|
||||
│ │ │ ├── colors.ts # 颜色工具
|
||||
│ │ │ ├── emojo.ts # 表情工具
|
||||
│ │ │ ├── index.ts # UI 工具入口
|
||||
│ │ │ ├── loading.ts # 加载动画工具
|
||||
│ │ │ └── tabs.ts # 标签页工具
|
||||
│ │ ├── index.ts # 工具函数总入口
|
||||
│ │ └── router.ts # 路由工具函数
|
||||
│ └── views # 页面组件目录
|
||||
├── tsconfig.json # TypeScript 配置文件
|
||||
└── vite.config.ts # Vite 配置文件
|
||||
```
|
||||
138
document/99页面ToDo.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 页面ToDo
|
||||
|
||||
## 背景与接口基线
|
||||
|
||||
- 后端架构:Clean Architecture(Api/Application/Domain/Infrastructure),Admin API 路由前缀 `api/admin/v{version}/`,Mini API 路由前缀 `api/mini/v{version}/`,鉴权为 JWT + 权限码(`PermissionAuthorize`),长整型 ID 以字符串返回,前端需保持大数解析(可用 `json-bigint`)。
|
||||
- Admin 端已交付核心 API(Phase 1):
|
||||
- 租户:注册/分页/详情、实名认证提交与审核、套餐订阅/续费/升降配、审核日志、公告/通知、账单、配额占用校验。
|
||||
- 商户:入驻创建、证照上传、合同管理、审核流程、审核日志、类目列表。
|
||||
- 套餐与配额:TenantPackage CRUD、订阅历史、配额占用校验。
|
||||
- RBAC 模板:角色模板 CRUD、复制、租户初始化,权限码种子齐全。
|
||||
- 门店:门店 CRUD、营业时间/配送区/节假日、能力开关(预约/排队)。
|
||||
- 桌码:区域与桌码 CRUD、批量生成、二维码 ZIP 导出。
|
||||
- 员工排班:员工 CRUD、门店角色绑定、排班 CRUD(默认未来 7 天)。
|
||||
- 菜品:分类/SPU/SKU/规格/加料/媒资/定价 CRUD,上/下架;Mini 端可拉取门店菜单。
|
||||
- 库存:SKU 库存与批次、调整/锁定/释放/售罄管理(表已具备,接口可用)。
|
||||
- 自提档期:自提设置与时间窗 CRUD;Mini 端按日期查询档期。
|
||||
- Mini 端基础:桌码扫码上下文(门店+桌台+公告)。
|
||||
- 尚未交付的模块(前端需占位但标记阻塞):购物车、完整下单/支付/优惠、桌台账单、自配送与第三方配送抽象、预购自提核销、业务指标大盘。
|
||||
- 业务关系(前端路由/权限需体现上下游约束):
|
||||
- 租户 ↔ 商户:商户属于租户,租户未通过实名认证/订阅过期时,商户入驻审核需提示或限制;租户停用后商户与门店入口应灰显。
|
||||
- 租户套餐/配额 ↔ 门店/员工/商品:套餐与配额决定可创建的门店数、员工数、商品数/库存操作等,前端在创建/批量操作时需展示剩余额度(调用配额校验接口),超额时阻止提交。
|
||||
- RBAC ↔ 模块入口:权限码控制菜单与按钮显隐;角色模板可复制到租户,租户停用/配额不足时仍可查看权限但部分操作需禁用;商户/门店/商品等操作按钮需绑权限码,防止“看得见点不了”的体验分裂。
|
||||
|
||||
## 搭建顺序(推荐)
|
||||
|
||||
1. [ ] 环境与基础设施:`pnpm i`、配置 `.env` 的 API 基地址;axios 实例 + 拦截器(加 Authorization、全局错误/超时提示、TraceId 透传)。
|
||||
2. [ ] 全局框架:登录页、基础布局(侧边导航/头部/标签页)、动态路由/菜单与权限控制(基于角色模板权限码)。
|
||||
3. [ ] 通用组件:表格查询模板、表单/抽屉、批量操作、上传(对接文件上传 API/COS)、二维码下载、富文本/备注输入。
|
||||
4. [ ] 模块页面按优先级迭代(见下表);每个模块先做列表 → 详情 → 创建/编辑 → 审核/状态流转 → 导出/下载。
|
||||
5. [ ] 联调与观测:与 Swagger 一致的请求/响应模型,统一错误提示,必要时对长 ID 使用字符串展示;完成一模块后补充 E2E/组件级用例。
|
||||
|
||||
## 页面清单与依赖
|
||||
|
||||
| 模块 | 页面 | 关键功能 | 依赖 API | 优先级 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 认证/首页 | 登录/登出 | 账号登录、Token 缓存/续期、租户/角色选择 | Admin 鉴权接口(Swagger),权限码获取 | P0 |
|
||||
| | 控制台总览 | 展示租户数/门店数/商品数、待办项入口 | 后续可接统计接口(暂用静态/Mock) | P2 |
|
||||
| 系统权限 | 角色模板列表/详情 | 查看模板、权限码树、变更记录 | `api/admin/v1/role-templates`\*(根据 RolesController 路由) | P1 |
|
||||
| | 角色模板新建/编辑 | 复制模板、勾选权限、保存 | 同上 | P1 |
|
||||
| | 租户初始化权限 | 选择模板批量初始化租户权限 | 角色模板复制/初始化接口 | P2 |
|
||||
| 租户管理 | 租户列表/搜索 | 分页、状态筛选、关键字搜索 | `api/admin/v1/tenants` GET | P0 |
|
||||
| | 租户详情 | 基本信息、当前套餐/订阅、实名状态、公告/通知概览 | `tenants/{id}` GET | P0 |
|
||||
| | 注册租户 | 提交注册信息并初始化套餐 | `tenants` POST | P1 |
|
||||
| | 实名提交/审核 | 上传资料、查看审核记录、通过/驳回 | `tenants/{id}/verification` POST / `tenants/{id}/review` POST 等 | P1 |
|
||||
| | 订阅与配额 | 订阅创建/续费/升降配、配额占用校验历史 | `tenants/{id}/subscriptions`、`tenants/{id}/quotas/check` | P1 |
|
||||
| | 账单与通知 | 账单列表/详情/标记支付,公告/通知列表+已读 | `tenants/{id}/billings`、`announcements`、`notifications` | P2 |
|
||||
| | 审核日志 | 查看租户审核日志 | `tenants/{id}/audits` | P2 |
|
||||
| 商户入驻 | 商户列表/详情 | 状态筛选、合同/证照查看、审核轨迹 | `api/admin/v1/merchants` GET | P0 |
|
||||
| | 商户创建/编辑 | 录入基本信息、证照上传、合同录入 | `merchants` POST/PUT、`merchants/{id}/documents`、`contracts` | P1 |
|
||||
| | 商户审核 | 状态流转(待审/驳回/通过)、驳回原因 | `merchants/{id}/review` | P1 |
|
||||
| | 类目管理 | 类目列表/选择 | `merchant-categories` 列表接口 | P2 |
|
||||
| 门店 | 门店列表/详情 | 基本信息、营业时间、配送区、能力开关 | `stores` CRUD | P0 |
|
||||
| | 门店编辑 | 基础信息维护、配送区 GeoJSON、节假日 | `stores` PUT、`stores/{id}/delivery-zones` | P1 |
|
||||
| | 门店公告 | 门店公告/通知管理 | `tenant-announcements` (按租户/门店) | P2 |
|
||||
| 桌码 | 区域与桌台列表 | 区域/桌码 CRUD、状态展示 | `stores/{id}/tables`、`table-areas` | P1 |
|
||||
| | 批量生成与导出 | 按区域/容量生成,下载二维码 ZIP | `stores/{id}/tables:batch`、`/export` | P1 |
|
||||
| 员工与排班 | 员工列表/编辑 | 员工信息、门店角色绑定 | `store-employees` CRUD | P1 |
|
||||
| | 排班日历 | 未来 7 天排班视图、冲突提示、批量排班 | `store-employee-shifts` | P1 |
|
||||
| 菜品与菜单 | 分类管理 | 分类 CRUD、排序 | `product-categories` | P1 |
|
||||
| | 商品列表/详情 | SPU/SKU/规格/加料/媒资、上下架 | `products`、`product-skus`、`publish`/`unpublish` | P0 |
|
||||
| | 商品编辑 | 规格/加料组维护、媒资上传、定价 | 同上 | P1 |
|
||||
| | 菜单预览 | 拉取门店全量菜单 JSON 预览 | Mini 菜单接口 `api/mini/v1/stores/{id}/menu` | P2 |
|
||||
| 库存 | 库存列表 | SKU 库存、锁定/售罄状态 | `inventory-items` | P1 |
|
||||
| | 批次管理 | 批次列表、调整、售罄 | `inventory-batches` | P1 |
|
||||
| | 库存调整 | 增减、预售/限购、锁定/释放记录 | `inventory-adjustments`、`inventory-lock-records` | P2 |
|
||||
| 自提档期 | 自提设置 | 截单时间、容量、开关 | `stores/{id}/pickup-settings` | P2 |
|
||||
| | 档期日历 | 日期选择、可用档期/余量展示 | `stores/{id}/pickup-slots` | P2 |
|
||||
| 运营 | 公告/通知 | 公告列表/发布/上下架、已读状态 | `tenant-announcements`、`tenant-notifications` | P2 |
|
||||
| | 账单/对账 | 账单列表、详情、标记支付 | `tenants/{id}/billings` | P2 |
|
||||
| 待后端的占位 | 购物车、下单/支付、桌台账单、自配送/第三方配送、预购自提核销、业务指标报表 | 只做菜单占位或灰显入口,待后端接口就绪再实现 | 无 | P3 |
|
||||
|
||||
> 说明:表内路由根据当前 Controllers 约定推断,实际联调请以 Swagger `api/admin/swagger/index.html` 暴露的接口为准。
|
||||
|
||||
## 下一步行动(可直接执行)
|
||||
|
||||
1. 初始化 axios/路由/权限骨架,完成登录页与基础布局。
|
||||
2. 先联调租户列表/详情/注册与商户列表/审核,确保 RBAC 与菜单权限生效。
|
||||
3. 依次落地门店→桌码→员工排班→商品→库存→自提档期页面,完成 Phase 1 闭环。
|
||||
4. 为未交付模块预留菜单占位(灰色状态 + “开发中”),避免导航缺口。
|
||||
|
||||
## 迭代执行序列(更细步骤)
|
||||
|
||||
1. 环境与骨架(当天完成)
|
||||
- [ ] 配置 `.env`:Admin API 基地址、超时、Token 存储键名。
|
||||
- [ ] 初始化 axios 拦截器:附带 Authorization、长 ID 使用 `json-bigint` 解析、全局错误提示、TraceId 透传。
|
||||
- [ ] 路由/布局:登录页、基础布局(侧边栏 + 顶栏 + 页签),路由守卫检查 Token 与权限码。
|
||||
2. 权限与菜单(P0)
|
||||
- [ ] 角色模板列表/详情页:展示权限树、变更记录。
|
||||
- [ ] 角色模板创建/编辑:复制模板、新增/删除权限码、保存。
|
||||
- [ ] 菜单与按钮显隐:基于权限码动态生成侧栏,按钮加指令控制。
|
||||
3. 登录与首页(P0)
|
||||
- [ ] 登录页:账号密码登录,Token 缓存与续期。
|
||||
- [ ] 控制台总览:占位展示租户数/门店数/商品数、待办(实名/审核/续费),数据可先用 Mock。
|
||||
4. 租户管理(P0-P1)
|
||||
- [ ] 租户列表:分页、状态/关键词筛选,列出套餐、实名状态、到期时间。
|
||||
- [ ] 租户详情:基本信息、当前订阅、配额剩余、公告/通知概览、审核日志。
|
||||
- [ ] 注册租户:表单提交注册;提交后跳转详情。
|
||||
- [ ] 实名提交流程:资料上传、状态展示、提交/驳回/通过操作;显示审核记录。
|
||||
- [ ] 订阅与配额:创建/续费/升降配、调用配额校验接口并展示剩余额度,超额禁止提交。
|
||||
- [ ] 账单/公告/通知:账单列表/详情/标记支付,公告/通知列表和已读状态。
|
||||
5. 商户入驻(P0-P1)
|
||||
- [ ] 商户列表:分页、状态筛选,显示所属租户、审核状态。
|
||||
- [ ] 商户详情:基本信息、证照、合同、审核轨迹。
|
||||
- [ ] 商户创建/编辑:信息录入、证照上传(COS 上传 API)、合同录入。
|
||||
- [ ] 商户审核:待审/通过/驳回,填写驳回原因。
|
||||
- [ ] 类目管理:类目列表选择,支持搜索。
|
||||
6. 门店管理(P0-P1)
|
||||
- [ ] 门店列表:展示租户/商户、状态、能力开关(预约/排队)。
|
||||
- [ ] 门店详情/编辑:基础信息、营业时间、配送区 GeoJSON、节假日;套餐/配额超额时禁用创建。
|
||||
- [ ] 门店公告:公告列表/发布/上下架(若后端按租户共享则合并视图)。
|
||||
7. 桌码管理(P1)
|
||||
- [ ] 桌码区域与桌台列表:区域 CRUD、桌台 CRUD,显示容量/状态。
|
||||
- [ ] 批量生成:按区域/容量批量生成桌码。
|
||||
- [ ] 二维码导出:ZIP 下载,支持按区域过滤。
|
||||
8. 员工与排班(P1)
|
||||
- [ ] 员工列表/编辑:基础信息、门店角色绑定。
|
||||
- [ ] 排班日历:未来 7 天排班视图,创建/编辑/删除,冲突提示。
|
||||
9. 菜品与菜单(P0-P1)
|
||||
- [ ] 分类管理:列表/排序/CRUD。
|
||||
- [ ] 商品列表:SPU/SKU 列表,支持状态/分类筛选、上下架。
|
||||
- [ ] 商品编辑:规格/加料组、媒资上传、定价策略;保存后刷新 SKU 列表。
|
||||
- [ ] 菜单预览:按门店拉取全量菜单 JSON,供验收与演示。
|
||||
10. 库存管理(P1-P2)
|
||||
- [ ] 库存列表:SKU 库存、锁定/售罄状态,支持搜索/筛选。
|
||||
- [ ] 批次管理:批次列表、批次调整(数量/售罄),展示策略(FIFO/FEFO)。
|
||||
- [ ] 库存调整:增减、锁定/释放记录查看;展示幂等键与状态。
|
||||
11. 自提档期(P2)
|
||||
- [ ] 自提设置:截单时间、容量、开关配置。
|
||||
- [ ] 档期日历:选择日期,查看可用档期与剩余容量,支持新增/编辑/删除。
|
||||
12. 运营与账单(P2)
|
||||
- [ ] 公告/通知:公告发布/上下架,通知列表与已读状态。
|
||||
- [ ] 账单/对账:账单列表/详情、标记支付、导出。
|
||||
13. 占位与后续(P3)
|
||||
- [ ] 购物车、下单/支付、桌台账单、自配送/第三方配送、预购自提核销、业务指标报表:菜单入口置灰或“开发中”,待后端接口完成后再实施。
|
||||
14. 联调与验收(每完成一模块即进行)
|
||||
- [ ] 对照 Swagger 校验请求/响应;异常与权限错误统一提示。
|
||||
- [ ] 大整数字段保持字符串展示;上传类接口校验文件大小/格式。
|
||||
- [ ] 完成后补充基础用例(可用 Cypress/Playwright 或组件测试)覆盖核心流程。
|
||||
83
eslint.config.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
// 从 URL 和路径模块中导入必要的功能
|
||||
import fs from 'fs'
|
||||
import path, { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// 从 ESLint 插件中导入推荐配置
|
||||
import pluginJs from '@eslint/js'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
// 使用 import.meta.url 获取当前模块的路径
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
|
||||
const autoImportConfig = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8')
|
||||
)
|
||||
|
||||
export default [
|
||||
// 指定文件匹配规则
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,vue}']
|
||||
},
|
||||
// 指定全局变量和环境
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
// 扩展配置
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
// 自定义规则
|
||||
{
|
||||
// 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置
|
||||
files: ['**/*.{js,mjs,cjs,ts,vue}'],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
// 合并从 autoImportConfig 中读取的全局变量配置
|
||||
...autoImportConfig.globals,
|
||||
// TypeScript 全局命名空间
|
||||
Api: 'readonly'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
quotes: ['error', 'single'], // 使用单引号
|
||||
semi: ['error', 'never'], // 语句末尾不加分号
|
||||
'no-var': 'error', // 要求使用 let 或 const 而不是 var
|
||||
'@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查
|
||||
'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查
|
||||
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
|
||||
'no-unexpected-multiline': 'error' // 禁止空余的多行
|
||||
}
|
||||
},
|
||||
// vue 规则
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: { parser: tseslint.parser }
|
||||
}
|
||||
},
|
||||
// 忽略文件
|
||||
{
|
||||
ignores: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'public',
|
||||
'.vscode/**',
|
||||
'src/assets/**',
|
||||
'src/utils/console.ts'
|
||||
]
|
||||
},
|
||||
// prettier 配置
|
||||
eslintPluginPrettierRecommended
|
||||
]
|
||||
47
index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Art Design Pro</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Art Design Pro - A modern admin dashboard template built with Vue 3, TypeScript, and Element Plus."
|
||||
/>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
|
||||
|
||||
<style>
|
||||
/* 防止页面刷新时白屏的初始样式 */
|
||||
html {
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background-color: #070707;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 初始化 html class 主题属性
|
||||
;(function () {
|
||||
try {
|
||||
if (typeof Storage === 'undefined' || !window.localStorage) {
|
||||
return
|
||||
}
|
||||
|
||||
const themeType = localStorage.getItem('sys-theme')
|
||||
if (themeType === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to apply initial theme:', e)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
133
package.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"name": "takeout-saas-adminui",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.19.0",
|
||||
"pnpm": ">=8.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint",
|
||||
"fix": "eslint --fix",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
||||
"lint:lint-staged": "lint-staged",
|
||||
"prepare": "husky",
|
||||
"commit": "git-cz",
|
||||
"clean:dev": "tsx scripts/clean-dev.ts",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,mjs,mts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{cjs,json,jsonc}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.vue": [
|
||||
"eslint --fix",
|
||||
"stylelint --fix --allow-empty-input",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{html,htm}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{scss,css,less}": [
|
||||
"stylelint --fix --allow-empty-input",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{md,mdx}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@vue/reactivity": "^3.5.21",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "next",
|
||||
"axios": "^1.12.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"echarts": "^6.0.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.11.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.10.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"ohash": "^2.0.11",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.3.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tlbs-map-vue": "^1.3.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^9.14.0",
|
||||
"vue-router": "^4.5.1",
|
||||
"xgplayer": "^3.0.20",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.4.1",
|
||||
"@commitlint/config-conventional": "^19.4.1",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@pinia/testing": "^0.1.7",
|
||||
"@types/node": "^24.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-git": "^1.11.1",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"globals": "^15.9.0",
|
||||
"husky": "^9.1.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.81.0",
|
||||
"stylelint": "^16.20.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recess-order": "^4.6.0",
|
||||
"stylelint-config-recommended-scss": "^14.1.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"terser": "^5.36.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "~5.6.3",
|
||||
"typescript-eslint": "^8.9.0",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-element-plus": "^0.10.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vitest": "^2.1.5",
|
||||
"vue-demi": "^0.14.9",
|
||||
"vue-img-cutter": "^3.0.5",
|
||||
"vue-tsc": "~2.1.6"
|
||||
}
|
||||
}
|
||||
8680
pnpm-lock.yaml
generated
Normal file
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
- core-js
|
||||
- es5-ext
|
||||
- esbuild
|
||||
- vue-demi
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
838
scripts/clean-dev.ts
Normal file
@@ -0,0 +1,838 @@
|
||||
// scripts/clean-dev.ts
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
// 现代化颜色主题
|
||||
const theme = {
|
||||
// 基础颜色
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
|
||||
// 前景色
|
||||
primary: '\x1b[38;5;75m', // 亮蓝色
|
||||
success: '\x1b[38;5;82m', // 亮绿色
|
||||
warning: '\x1b[38;5;220m', // 亮黄色
|
||||
error: '\x1b[38;5;196m', // 亮红色
|
||||
info: '\x1b[38;5;159m', // 青色
|
||||
purple: '\x1b[38;5;141m', // 紫色
|
||||
orange: '\x1b[38;5;208m', // 橙色
|
||||
gray: '\x1b[38;5;245m', // 灰色
|
||||
white: '\x1b[38;5;255m', // 白色
|
||||
|
||||
// 背景色
|
||||
bgDark: '\x1b[48;5;235m', // 深灰背景
|
||||
bgBlue: '\x1b[48;5;24m', // 蓝色背景
|
||||
bgGreen: '\x1b[48;5;22m', // 绿色背景
|
||||
bgRed: '\x1b[48;5;52m' // 红色背景
|
||||
}
|
||||
|
||||
// 现代化图标集
|
||||
const icons = {
|
||||
rocket: '🚀',
|
||||
fire: '🔥',
|
||||
star: '⭐',
|
||||
gem: '💎',
|
||||
crown: '👑',
|
||||
magic: '✨',
|
||||
warning: '⚠️',
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
info: 'ℹ️',
|
||||
folder: '📁',
|
||||
file: '📄',
|
||||
image: '🖼️',
|
||||
code: '💻',
|
||||
data: '📊',
|
||||
globe: '🌐',
|
||||
map: '🗺️',
|
||||
chat: '💬',
|
||||
bolt: '⚡',
|
||||
shield: '🛡️',
|
||||
key: '🔑',
|
||||
link: '🔗',
|
||||
clean: '🧹',
|
||||
trash: '🗑️',
|
||||
check: '✓',
|
||||
cross: '✗',
|
||||
arrow: '→',
|
||||
loading: '⏳'
|
||||
}
|
||||
|
||||
// 格式化工具
|
||||
const fmt = {
|
||||
title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`,
|
||||
subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`,
|
||||
success: (text: string) => `${theme.success}${text}${theme.reset}`,
|
||||
error: (text: string) => `${theme.error}${text}${theme.reset}`,
|
||||
warning: (text: string) => `${theme.warning}${text}${theme.reset}`,
|
||||
info: (text: string) => `${theme.info}${text}${theme.reset}`,
|
||||
highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`,
|
||||
dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`,
|
||||
orange: (text: string) => `${theme.orange}${text}${theme.reset}`,
|
||||
|
||||
// 带背景的文本
|
||||
badge: (text: string, bg: string = theme.bgBlue) =>
|
||||
`${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`,
|
||||
|
||||
// 渐变效果模拟
|
||||
gradient: (text: string) => {
|
||||
const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m']
|
||||
const chars = text.split('')
|
||||
return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset
|
||||
}
|
||||
}
|
||||
|
||||
// 创建现代化标题横幅
|
||||
function createModernBanner() {
|
||||
console.log()
|
||||
console.log(
|
||||
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ║ ║')
|
||||
)
|
||||
console.log(
|
||||
` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║`
|
||||
)
|
||||
console.log(
|
||||
` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║`
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ║ ║')
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
|
||||
)
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 创建分割线
|
||||
function createDivider(char = '─', color = theme.primary) {
|
||||
console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`)
|
||||
}
|
||||
|
||||
// 创建卡片样式容器
|
||||
function createCard(title: string, content: string[]) {
|
||||
console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`)
|
||||
console.log()
|
||||
content.forEach((line) => {
|
||||
console.log(` ${line}`)
|
||||
})
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 进度条动画
|
||||
function createProgressBar(current: number, total: number, text: string, width = 40) {
|
||||
const percentage = Math.round((current / total) * 100)
|
||||
const filled = Math.round((current / total) * width)
|
||||
const empty = width - filled
|
||||
|
||||
const filledBar = '█'.repeat(filled)
|
||||
const emptyBar = '░'.repeat(empty)
|
||||
|
||||
process.stdout.write(
|
||||
`\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}`
|
||||
)
|
||||
|
||||
if (current === total) {
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
const stats = {
|
||||
deletedFiles: 0,
|
||||
deletedPaths: 0,
|
||||
failedPaths: 0,
|
||||
startTime: Date.now(),
|
||||
totalFiles: 0
|
||||
}
|
||||
|
||||
// 清理目标
|
||||
const targets = [
|
||||
'README.md',
|
||||
'README.zh-CN.md',
|
||||
'CHANGELOG.md',
|
||||
'CHANGELOG.zh-CN.md',
|
||||
'src/views/change',
|
||||
'src/views/safeguard',
|
||||
'src/views/article',
|
||||
'src/views/examples',
|
||||
'src/views/system/nested',
|
||||
'src/views/widgets',
|
||||
'src/views/template',
|
||||
'src/views/dashboard/analysis',
|
||||
'src/views/dashboard/ecommerce',
|
||||
'src/mock/json',
|
||||
'src/mock/temp/articleList.ts',
|
||||
'src/mock/temp/commentDetail.ts',
|
||||
'src/mock/temp/commentList.ts',
|
||||
'src/assets/images/cover',
|
||||
'src/assets/images/safeguard',
|
||||
'src/assets/images/3d',
|
||||
'src/components/core/charts/art-map-chart',
|
||||
'src/components/business/comment-widget'
|
||||
]
|
||||
|
||||
// 递归统计文件数量
|
||||
async function countFiles(targetPath: string): Promise<number> {
|
||||
const fullPath = path.resolve(process.cwd(), targetPath)
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(fullPath)
|
||||
|
||||
if (stat.isFile()) {
|
||||
return 1
|
||||
} else if (stat.isDirectory()) {
|
||||
const entries = await fs.readdir(fullPath)
|
||||
let count = 0
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(targetPath, entry)
|
||||
count += await countFiles(entryPath)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// 统计所有目标的文件数量
|
||||
async function countAllFiles(): Promise<number> {
|
||||
let totalCount = 0
|
||||
|
||||
for (const target of targets) {
|
||||
const count = await countFiles(target)
|
||||
totalCount += count
|
||||
}
|
||||
|
||||
return totalCount
|
||||
}
|
||||
|
||||
// 删除文件和目录
|
||||
async function remove(targetPath: string, index: number) {
|
||||
const fullPath = path.resolve(process.cwd(), targetPath)
|
||||
|
||||
createProgressBar(index + 1, targets.length, targetPath)
|
||||
|
||||
try {
|
||||
const fileCount = await countFiles(targetPath)
|
||||
await fs.rm(fullPath, { recursive: true, force: true })
|
||||
stats.deletedFiles += fileCount
|
||||
stats.deletedPaths++
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
} catch (err) {
|
||||
stats.failedPaths++
|
||||
console.log()
|
||||
console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理路由模块
|
||||
async function cleanRouteModules() {
|
||||
const modulesPath = path.resolve(process.cwd(), 'src/router/modules')
|
||||
|
||||
try {
|
||||
// 删除演示相关的路由模块
|
||||
const modulesToRemove = [
|
||||
'template.ts',
|
||||
'widgets.ts',
|
||||
'examples.ts',
|
||||
'article.ts',
|
||||
'safeguard.ts',
|
||||
'help.ts'
|
||||
]
|
||||
|
||||
for (const module of modulesToRemove) {
|
||||
const modulePath = path.join(modulesPath, module)
|
||||
try {
|
||||
await fs.rm(modulePath, { force: true })
|
||||
} catch {
|
||||
// 文件不存在时忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 重写 dashboard.ts - 只保留 console
|
||||
const dashboardContent = `import { AppRouteRecord } from '@/types/router'
|
||||
|
||||
export const dashboardRoutes: AppRouteRecord = {
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.dashboard.title',
|
||||
icon: 'ri:pie-chart-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'console',
|
||||
name: 'Console',
|
||||
component: '/dashboard/console',
|
||||
meta: {
|
||||
title: 'menus.dashboard.console',
|
||||
keepAlive: false,
|
||||
fixedTab: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8')
|
||||
|
||||
// 重写 system.ts - 移除 nested 嵌套菜单
|
||||
const systemContent = `import { AppRouteRecord } from '@/types/router'
|
||||
|
||||
export const systemRoutes: AppRouteRecord = {
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.system.title',
|
||||
icon: 'ri:user-3-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: '/system/user',
|
||||
meta: {
|
||||
title: 'menus.system.user',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: '/system/role',
|
||||
meta: {
|
||||
title: 'menus.system.role',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user-center',
|
||||
name: 'UserCenter',
|
||||
component: '/system/user-center',
|
||||
meta: {
|
||||
title: 'menus.system.userCenter',
|
||||
isHide: true,
|
||||
keepAlive: true,
|
||||
isHideTab: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'Menus',
|
||||
component: '/system/menu',
|
||||
meta: {
|
||||
title: 'menus.system.menu',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
authList: [
|
||||
{ title: '新增', authMark: 'add' },
|
||||
{ title: '编辑', authMark: 'edit' },
|
||||
{ title: '删除', authMark: 'delete' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8')
|
||||
|
||||
// 重写 index.ts - 只导入保留的模块
|
||||
const indexContent = `import { AppRouteRecord } from '@/types/router'
|
||||
import { dashboardRoutes } from './dashboard'
|
||||
import { systemRoutes } from './system'
|
||||
import { resultRoutes } from './result'
|
||||
import { exceptionRoutes } from './exception'
|
||||
|
||||
/**
|
||||
* 导出所有模块化路由
|
||||
*/
|
||||
export const routeModules: AppRouteRecord[] = [
|
||||
dashboardRoutes,
|
||||
systemRoutes,
|
||||
resultRoutes,
|
||||
exceptionRoutes
|
||||
]
|
||||
`
|
||||
await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8')
|
||||
|
||||
console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理路由别名
|
||||
async function cleanRoutesAlias() {
|
||||
const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts')
|
||||
|
||||
try {
|
||||
const cleanedAlias = `/**
|
||||
* 公共路由别名
|
||||
# 存放系统级公共路由路径,如布局容器、登录页等
|
||||
*/
|
||||
export enum RoutesAlias {
|
||||
Layout = '/index/index', // 布局容器
|
||||
Login = '/auth/login' // 登录页
|
||||
}
|
||||
`
|
||||
|
||||
await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8')
|
||||
console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理变更日志
|
||||
async function cleanChangeLog() {
|
||||
const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts')
|
||||
|
||||
try {
|
||||
const cleanedChangeLog = `import { ref } from 'vue'
|
||||
|
||||
interface UpgradeLog {
|
||||
version: string // 版本号
|
||||
title: string // 更新标题
|
||||
date: string // 更新日期
|
||||
detail?: string[] // 更新内容
|
||||
requireReLogin?: boolean // 是否需要重新登录
|
||||
remark?: string // 备注
|
||||
}
|
||||
|
||||
export const upgradeLogList = ref<UpgradeLog[]>([])
|
||||
`
|
||||
|
||||
await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8')
|
||||
console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理语言文件
|
||||
async function cleanLanguageFiles() {
|
||||
const languageFiles = [
|
||||
{ path: 'src/locales/langs/zh.json', name: '中文语言文件' },
|
||||
{ path: 'src/locales/langs/en.json', name: '英文语言文件' }
|
||||
]
|
||||
|
||||
for (const { path: langPath, name } of languageFiles) {
|
||||
try {
|
||||
const fullPath = path.resolve(process.cwd(), langPath)
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
const langData = JSON.parse(content)
|
||||
|
||||
const menusToRemove = [
|
||||
'widgets',
|
||||
'template',
|
||||
'article',
|
||||
'examples',
|
||||
'safeguard',
|
||||
'plan',
|
||||
'help'
|
||||
]
|
||||
|
||||
if (langData.menus) {
|
||||
menusToRemove.forEach((menuKey) => {
|
||||
if (langData.menus[menuKey]) {
|
||||
delete langData.menus[menuKey]
|
||||
}
|
||||
})
|
||||
|
||||
if (langData.menus.dashboard) {
|
||||
if (langData.menus.dashboard.analysis) {
|
||||
delete langData.menus.dashboard.analysis
|
||||
}
|
||||
if (langData.menus.dashboard.ecommerce) {
|
||||
delete langData.menus.dashboard.ecommerce
|
||||
}
|
||||
}
|
||||
|
||||
if (langData.menus.system) {
|
||||
const systemKeysToRemove = [
|
||||
'nested',
|
||||
'menu1',
|
||||
'menu2',
|
||||
'menu21',
|
||||
'menu3',
|
||||
'menu31',
|
||||
'menu32',
|
||||
'menu321'
|
||||
]
|
||||
systemKeysToRemove.forEach((key) => {
|
||||
if (langData.menus.system[key]) {
|
||||
delete langData.menus.system[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8')
|
||||
console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理快速入口组件
|
||||
async function cleanFastEnterComponent() {
|
||||
const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts')
|
||||
|
||||
try {
|
||||
const cleanedFastEnter = `/**
|
||||
* 快速入口配置
|
||||
* 包含:应用列表、快速链接等配置
|
||||
*/
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
import type { FastEnterConfig } from '@/types/config'
|
||||
|
||||
const fastEnterConfig: FastEnterConfig = {
|
||||
// 显示条件(屏幕宽度)
|
||||
minWidth: 1200,
|
||||
// 应用列表
|
||||
applications: [
|
||||
{
|
||||
name: '工作台',
|
||||
description: '系统概览与数据统计',
|
||||
icon: 'ri:pie-chart-line',
|
||||
iconColor: '#377dff',
|
||||
enabled: true,
|
||||
order: 1,
|
||||
routeName: 'Console'
|
||||
},
|
||||
{
|
||||
name: '官方文档',
|
||||
description: '使用指南与开发文档',
|
||||
icon: 'ri:bill-line',
|
||||
iconColor: '#ffb100',
|
||||
enabled: true,
|
||||
order: 2,
|
||||
link: WEB_LINKS.DOCS
|
||||
},
|
||||
{
|
||||
name: '技术支持',
|
||||
description: '技术支持与问题反馈',
|
||||
icon: 'ri:user-location-line',
|
||||
iconColor: '#ff6b6b',
|
||||
enabled: true,
|
||||
order: 3,
|
||||
link: WEB_LINKS.COMMUNITY
|
||||
},
|
||||
{
|
||||
name: '哔哩哔哩',
|
||||
description: '技术分享与交流',
|
||||
icon: 'ri:bilibili-line',
|
||||
iconColor: '#FB7299',
|
||||
enabled: true,
|
||||
order: 4,
|
||||
link: WEB_LINKS.BILIBILI
|
||||
}
|
||||
],
|
||||
// 快速链接
|
||||
quickLinks: [
|
||||
{
|
||||
name: '登录',
|
||||
enabled: true,
|
||||
order: 1,
|
||||
routeName: 'Login'
|
||||
},
|
||||
{
|
||||
name: '注册',
|
||||
enabled: true,
|
||||
order: 2,
|
||||
routeName: 'Register'
|
||||
},
|
||||
{
|
||||
name: '忘记密码',
|
||||
enabled: true,
|
||||
order: 3,
|
||||
routeName: 'ForgetPassword'
|
||||
},
|
||||
{
|
||||
name: '个人中心',
|
||||
enabled: true,
|
||||
order: 4,
|
||||
routeName: 'UserCenter'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default Object.freeze(fastEnterConfig)
|
||||
`
|
||||
|
||||
await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8')
|
||||
console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新菜单接口
|
||||
async function updateMenuApi() {
|
||||
const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts')
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(apiPath, 'utf-8')
|
||||
const updatedContent = content.replace(
|
||||
"url: '/api/v3/system/menus'",
|
||||
"url: '/api/v3/system/menus/simple'"
|
||||
)
|
||||
|
||||
await fs.writeFile(apiPath, updatedContent, 'utf-8')
|
||||
console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`)
|
||||
} catch (err) {
|
||||
console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 用户确认函数
|
||||
async function getUserConfirmation(): Promise<boolean> {
|
||||
const { createInterface } = await import('readline')
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
console.log(
|
||||
` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}`
|
||||
)
|
||||
console.log()
|
||||
process.stdout.write(` ${icons.arrow} `)
|
||||
|
||||
rl.question('', (answer: string) => {
|
||||
rl.close()
|
||||
resolve(answer.toLowerCase().trim() === 'yes')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 显示清理警告
|
||||
async function showCleanupWarning() {
|
||||
createCard('安全警告', [
|
||||
`${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`,
|
||||
`${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}`
|
||||
])
|
||||
|
||||
const cleanupItems = [
|
||||
{
|
||||
icon: icons.image,
|
||||
name: '图片资源',
|
||||
desc: '演示用的封面图片、3D图片、运维图片等',
|
||||
color: theme.orange
|
||||
},
|
||||
{
|
||||
icon: icons.file,
|
||||
name: '演示页面',
|
||||
desc: 'widgets、template、article、examples、safeguard等页面',
|
||||
color: theme.purple
|
||||
},
|
||||
{
|
||||
icon: icons.code,
|
||||
name: '路由模块文件',
|
||||
desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)',
|
||||
color: theme.primary
|
||||
},
|
||||
{
|
||||
icon: icons.link,
|
||||
name: '路由别名',
|
||||
desc: '重写routesAlias.ts,移除演示路由别名',
|
||||
color: theme.info
|
||||
},
|
||||
{
|
||||
icon: icons.data,
|
||||
name: 'Mock数据',
|
||||
desc: '演示用的JSON数据、文章列表、评论数据等',
|
||||
color: theme.success
|
||||
},
|
||||
{
|
||||
icon: icons.globe,
|
||||
name: '多语言文件',
|
||||
desc: '清理中英文语言包中的演示菜单项',
|
||||
color: theme.warning
|
||||
},
|
||||
{ icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error },
|
||||
{ icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange },
|
||||
{
|
||||
icon: icons.bolt,
|
||||
name: '快速入口',
|
||||
desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目',
|
||||
color: theme.purple
|
||||
}
|
||||
]
|
||||
|
||||
console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`)
|
||||
console.log()
|
||||
|
||||
cleanupItems.forEach((item, index) => {
|
||||
console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`)
|
||||
console.log(` ${fmt.dim(item.desc)}`)
|
||||
})
|
||||
|
||||
console.log()
|
||||
console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`)
|
||||
console.log()
|
||||
|
||||
const preservedModules = [
|
||||
{ name: 'Dashboard', desc: '工作台页面' },
|
||||
{ name: 'System', desc: '系统管理模块' },
|
||||
{ name: 'Result', desc: '结果页面' },
|
||||
{ name: 'Exception', desc: '异常页面' },
|
||||
{ name: 'Auth', desc: '登录注册功能' },
|
||||
{ name: 'Core Components', desc: '核心组件库' }
|
||||
]
|
||||
|
||||
preservedModules.forEach((module) => {
|
||||
console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`)
|
||||
})
|
||||
|
||||
console.log()
|
||||
createDivider()
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 显示统计信息
|
||||
async function showStats() {
|
||||
const duration = Date.now() - stats.startTime
|
||||
const seconds = (duration / 1000).toFixed(2)
|
||||
|
||||
console.log()
|
||||
createCard('清理统计', [
|
||||
`${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`,
|
||||
`${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`,
|
||||
...(stats.failedPaths > 0
|
||||
? [
|
||||
`${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径`
|
||||
]
|
||||
: []),
|
||||
`${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒`
|
||||
])
|
||||
}
|
||||
|
||||
// 创建成功横幅
|
||||
function createSuccessBanner() {
|
||||
console.log()
|
||||
console.log(
|
||||
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ║ ║')
|
||||
)
|
||||
console.log(
|
||||
` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║`
|
||||
)
|
||||
console.log(
|
||||
` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║`
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ║ ║')
|
||||
)
|
||||
console.log(
|
||||
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
|
||||
)
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
// 清屏并显示横幅
|
||||
console.clear()
|
||||
createModernBanner()
|
||||
|
||||
// 显示清理警告
|
||||
await showCleanupWarning()
|
||||
|
||||
// 统计文件数量
|
||||
console.log(` ${fmt.info('正在统计文件数量...')}`)
|
||||
stats.totalFiles = await countAllFiles()
|
||||
|
||||
console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`)
|
||||
console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`)
|
||||
console.log()
|
||||
|
||||
// 用户确认
|
||||
const confirmed = await getUserConfirmation()
|
||||
|
||||
if (!confirmed) {
|
||||
console.log(` ${fmt.warning('操作已取消,清理中止')}`)
|
||||
console.log()
|
||||
return
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`)
|
||||
console.log()
|
||||
|
||||
// 开始清理过程
|
||||
console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`)
|
||||
console.log()
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
await remove(targets[i], i)
|
||||
}
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`)
|
||||
console.log()
|
||||
await cleanRouteModules()
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`)
|
||||
console.log()
|
||||
await cleanRoutesAlias()
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`)
|
||||
console.log()
|
||||
await cleanChangeLog()
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`)
|
||||
console.log()
|
||||
await cleanLanguageFiles()
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`)
|
||||
console.log()
|
||||
await cleanFastEnterComponent()
|
||||
console.log()
|
||||
|
||||
console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`)
|
||||
console.log()
|
||||
await updateMenuApi()
|
||||
|
||||
// 显示统计信息
|
||||
await showStats()
|
||||
|
||||
// 显示成功横幅
|
||||
createSuccessBanner()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.log()
|
||||
console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`)
|
||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
||||
console.log()
|
||||
process.exit(1)
|
||||
})
|
||||
34
src/App.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
|
||||
<RouterView></RouterView>
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from './store/modules/user'
|
||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import { systemUpgrade } from './utils/sys'
|
||||
import { toggleTransition } from './utils/ui/animation'
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { initializeTheme } from './hooks/core/useTheme'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
|
||||
const locales = {
|
||||
zh: zh,
|
||||
en: en
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
toggleTransition(true)
|
||||
initializeTheme()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
checkStorageCompatibility()
|
||||
toggleTransition(false)
|
||||
systemUpgrade()
|
||||
})
|
||||
</script>
|
||||
254
src/api/announcement.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 文件用途:公告模块 API 封装
|
||||
* 说明:统一对接平台/租户/应用端公告接口
|
||||
* 日期:2025-12-20
|
||||
*/
|
||||
|
||||
import request from '@/utils/http'
|
||||
import type {
|
||||
AnnouncementCommand,
|
||||
AnnouncementFormData,
|
||||
AnnouncementQueryParams,
|
||||
TenantAnnouncementDto
|
||||
} from '@/types/announcement'
|
||||
import type { PagedResult } from '@/types/common/response'
|
||||
|
||||
/** 发布/撤销命令体(仅包含 RowVersion) */
|
||||
type RowVersionCommand = Pick<AnnouncementCommand, 'rowVersion'>
|
||||
|
||||
const withTenantHeader = (tenantId: string) => ({
|
||||
headers: {
|
||||
'X-Tenant-Id': tenantId
|
||||
}
|
||||
})
|
||||
|
||||
const normalizeQueryParams = (params: AnnouncementQueryParams) => {
|
||||
const { dateFrom, dateTo, ...rest } = params
|
||||
return {
|
||||
...rest,
|
||||
effectiveFrom: params.effectiveFrom ?? dateFrom,
|
||||
effectiveTo: params.effectiveTo ?? dateTo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台公告 API
|
||||
* 路由前缀:/api/platform/announcements
|
||||
*/
|
||||
export const platformAnnouncementApi = {
|
||||
/**
|
||||
* 创建平台公告
|
||||
* @param data 创建参数
|
||||
*/
|
||||
create: (data: AnnouncementFormData) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: '/api/platform/announcements',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询平台公告列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
list: (params: AnnouncementQueryParams) => {
|
||||
return request.get<PagedResult<TenantAnnouncementDto>>({
|
||||
url: '/api/platform/announcements',
|
||||
params: normalizeQueryParams(params)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取平台公告详情
|
||||
* @param announcementId 公告ID
|
||||
*/
|
||||
detail: (announcementId: string) => {
|
||||
return request.get<TenantAnnouncementDto>({
|
||||
url: `/api/platform/announcements/${announcementId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新平台公告(仅草稿)
|
||||
* @param announcementId 公告ID
|
||||
* @param data 更新参数
|
||||
*/
|
||||
update: (announcementId: string, data: AnnouncementFormData) => {
|
||||
return request.put<TenantAnnouncementDto>({
|
||||
url: `/api/platform/announcements/${announcementId}`,
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 发布平台公告
|
||||
* @param announcementId 公告ID
|
||||
* @param command 命令体(包含 rowVersion)
|
||||
*/
|
||||
publish: (announcementId: string, command: RowVersionCommand) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/platform/announcements/${announcementId}/publish`,
|
||||
data: command
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 撤销平台公告
|
||||
* @param announcementId 公告ID
|
||||
* @param command 命令体(包含 rowVersion)
|
||||
*/
|
||||
revoke: (announcementId: string, command: RowVersionCommand) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/platform/announcements/${announcementId}/revoke`,
|
||||
data: command
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户公告 API
|
||||
* 路由前缀:/api/admin/v1/tenants/{tenantId}/announcements
|
||||
*/
|
||||
export const tenantAnnouncementApi = {
|
||||
/**
|
||||
* 分页查询租户公告
|
||||
* @param tenantId 租户ID
|
||||
* @param params 查询参数
|
||||
*/
|
||||
list: (tenantId: string, params: AnnouncementQueryParams) => {
|
||||
return request.get<PagedResult<TenantAnnouncementDto>>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements`,
|
||||
params: normalizeQueryParams(params),
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取租户公告详情
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
*/
|
||||
detail: (tenantId: string, announcementId: string) => {
|
||||
return request.get<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建租户公告
|
||||
* @param tenantId 租户ID
|
||||
* @param data 创建参数
|
||||
*/
|
||||
create: (tenantId: string, data: AnnouncementFormData) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements`,
|
||||
data,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新租户公告(仅草稿)
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
* @param data 更新参数
|
||||
*/
|
||||
update: (tenantId: string, announcementId: string, data: AnnouncementFormData) => {
|
||||
return request.put<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`,
|
||||
data,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 发布租户公告
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
* @param command 命令体(包含 rowVersion)
|
||||
*/
|
||||
publish: (tenantId: string, announcementId: string, command: RowVersionCommand) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/publish`,
|
||||
data: command,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 撤销租户公告
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
* @param command 命令体(包含 rowVersion)
|
||||
*/
|
||||
revoke: (tenantId: string, announcementId: string, command: RowVersionCommand) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/revoke`,
|
||||
data: command,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除租户公告
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
*/
|
||||
delete: (tenantId: string, announcementId: string) => {
|
||||
return request.del<boolean>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记公告已读(兼容旧路径)
|
||||
* @param tenantId 租户ID
|
||||
* @param announcementId 公告ID
|
||||
*/
|
||||
markRead: (tenantId: string, announcementId: string) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/read`,
|
||||
...withTenantHeader(tenantId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用端公告 API
|
||||
* 路由前缀:/api/app/announcements
|
||||
*/
|
||||
export const appAnnouncementApi = {
|
||||
/**
|
||||
* 获取可见公告列表(已发布且有效期内)
|
||||
* @param params 查询参数
|
||||
*/
|
||||
list: (params: AnnouncementQueryParams) => {
|
||||
return request.get<PagedResult<TenantAnnouncementDto>>({
|
||||
url: '/api/app/announcements',
|
||||
params: normalizeQueryParams(params)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取未读公告列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
unread: (params: AnnouncementQueryParams) => {
|
||||
return request.get<PagedResult<TenantAnnouncementDto>>({
|
||||
url: '/api/app/announcements/unread',
|
||||
params: normalizeQueryParams(params)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记公告已读
|
||||
* @param announcementId 公告ID
|
||||
*/
|
||||
markRead: (announcementId: string) => {
|
||||
return request.post<TenantAnnouncementDto>({
|
||||
url: `/api/app/announcements/${announcementId}/mark-read`
|
||||
})
|
||||
}
|
||||
}
|
||||
71
src/api/auth.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param params 登录参数
|
||||
* @returns 登录响应
|
||||
*/
|
||||
export function fetchLogin(params: Api.Auth.LoginParams) {
|
||||
return request.post<Api.Auth.LoginResponse>({
|
||||
url: '/api/admin/v1/auth/login',
|
||||
params
|
||||
// showSuccessMessage: true // 显示成功消息
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 免租户号登录(仅账号+密码)
|
||||
*/
|
||||
export function fetchLoginSimple(params: Api.Auth.LoginParams) {
|
||||
return request.post<Api.Auth.LoginResponse>({
|
||||
url: '/api/admin/v1/auth/login/simple',
|
||||
params,
|
||||
skipTenantHeader: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export function fetchGetUserInfo() {
|
||||
return request.get<Api.Auth.UserInfo>({
|
||||
url: '/api/admin/v1/auth/profile'
|
||||
// 自定义请求头
|
||||
// headers: {
|
||||
// 'X-Custom-Header': 'your-custom-value'
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户菜单
|
||||
* @returns 菜单列表
|
||||
*/
|
||||
export function fetchGetMenu() {
|
||||
return request.get<Api.Auth.MenuListResponse>({
|
||||
url: '/api/admin/v1/auth/menu'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限
|
||||
* @param userId 用户ID
|
||||
* @returns 用户权限信息
|
||||
*/
|
||||
export function fetchGetUserPermissions(userId: string) {
|
||||
return request.get<Api.Auth.UserPermissionsResponse>({
|
||||
url: `/api/admin/v1/auth/permissions/${userId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过重置链接令牌重置管理员密码
|
||||
*/
|
||||
export function fetchResetAdminPassword(data: Api.Auth.ResetAdminPasswordRequest) {
|
||||
return request.post<void>({
|
||||
url: '/api/admin/v1/auth/reset-password',
|
||||
data
|
||||
})
|
||||
}
|
||||
222
src/api/billing.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 账单接口基础路径
|
||||
*
|
||||
* 说明:后端路由为 /api/admin/v1/billings。
|
||||
*/
|
||||
const BILLING_BASE_URL = '/api/admin/v1/billings'
|
||||
|
||||
// ==================== 查询类 API ====================
|
||||
|
||||
/**
|
||||
* 获取账单列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchBillingList(params?: Partial<Api.Billing.BillingListParams>) {
|
||||
return request.get<Api.Billing.BillingListResponse>({
|
||||
url: BILLING_BASE_URL,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单列表(导出用:自动分页拉取)
|
||||
* @param params 查询参数(不含分页也可)
|
||||
* @param options 导出拉取选项
|
||||
*/
|
||||
export async function fetchBillingListForExport(
|
||||
params?: Partial<Api.Billing.BillingListParams>,
|
||||
options?: {
|
||||
/** 单页拉取条数(越大越快,但后端可能有限制) */
|
||||
pageSize?: number
|
||||
/** 最大导出条数(防止一次性导出过大) */
|
||||
maxRows?: number
|
||||
}
|
||||
) {
|
||||
// 1. 兜底配置
|
||||
const pageSize = Math.min(Math.max(options?.pageSize ?? 200, 50), 2000)
|
||||
const maxRows = Math.min(Math.max(options?.maxRows ?? 10000, 1), 100000)
|
||||
|
||||
// 2. 循环分页拉取(直到 totalPages 或达到 maxRows)
|
||||
const result: Api.Billing.BillingListDto[] = []
|
||||
let pageNumber = 1
|
||||
let totalPages = 1
|
||||
|
||||
while (pageNumber <= totalPages && result.length < maxRows) {
|
||||
const res = await fetchBillingList({
|
||||
...params,
|
||||
PageNumber: pageNumber,
|
||||
PageSize: pageSize
|
||||
})
|
||||
|
||||
const items = res.items || []
|
||||
result.push(...items)
|
||||
|
||||
totalPages = res.totalPages || 1
|
||||
if (items.length === 0) break
|
||||
pageNumber += 1
|
||||
}
|
||||
|
||||
// 3. 截断到 maxRows,避免极端情况内存爆炸
|
||||
return result.slice(0, maxRows)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单详情
|
||||
* @param id 账单 ID
|
||||
*/
|
||||
export function fetchBillingDetail(id: string) {
|
||||
return request.get<Api.Billing.BillingDetailDto>({
|
||||
url: `${BILLING_BASE_URL}/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单支付记录
|
||||
* @param billingId 账单 ID
|
||||
*/
|
||||
export function fetchBillingPayments(billingId: string) {
|
||||
return request.get<Api.Billing.PaymentRecordListResponse>({
|
||||
url: `${BILLING_BASE_URL}/${billingId}/payments`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单统计数据
|
||||
* @param params 统计查询参数
|
||||
*/
|
||||
export function fetchBillingStatistics(params: Api.Billing.BillingStatisticsParams) {
|
||||
return request.get<Api.Billing.BillingStatisticsDto>({
|
||||
url: `${BILLING_BASE_URL}/statistics`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取逾期账单列表
|
||||
* @param params 分页参数
|
||||
*/
|
||||
export function fetchOverdueBillings(params?: Partial<Api.Billing.BillingListParams>) {
|
||||
return request.get<Api.Billing.BillingListResponse>({
|
||||
url: `${BILLING_BASE_URL}/overdue`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 命令类 API ====================
|
||||
|
||||
/**
|
||||
* 创建账单
|
||||
* @param data 创建账单数据
|
||||
*/
|
||||
export function createBilling(data: Api.Billing.CreateBillingCommand) {
|
||||
return request.post<Api.Billing.BillingDetailDto>({
|
||||
url: BILLING_BASE_URL,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新账单状态
|
||||
* @param id 账单 ID
|
||||
* @param data 更新状态数据
|
||||
*/
|
||||
export function updateBillingStatus(id: string, data: Api.Billing.UpdateStatusCommand) {
|
||||
return request.put<Api.Billing.BillingDetailDto>({
|
||||
url: `${BILLING_BASE_URL}/${id}/status`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消账单
|
||||
* @param id 账单 ID
|
||||
* @param data 取消原因
|
||||
*/
|
||||
export function cancelBilling(id: string, data: Api.Billing.CancelBillingCommand) {
|
||||
return request.del<void>({
|
||||
url: `${BILLING_BASE_URL}/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录支付(仅创建待审核支付记录,不会立即更新账单状态)
|
||||
* @param billingId 账单 ID
|
||||
* @param data 支付记录数据
|
||||
*/
|
||||
export function recordPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) {
|
||||
return request.post<Api.Billing.PaymentRecordDto>({
|
||||
url: `${BILLING_BASE_URL}/${billingId}/payments`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键确认收款(记录支付 + 立即审核通过 + 同步更新账单状态)
|
||||
* @param billingId 账单 ID
|
||||
* @param data 支付记录数据
|
||||
*/
|
||||
export function confirmPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) {
|
||||
return request.post<Api.Billing.PaymentRecordDto>({
|
||||
url: `${BILLING_BASE_URL}/${billingId}/payments/confirm`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核支付记录
|
||||
* @param paymentId 支付记录 ID
|
||||
* @param data 审核数据
|
||||
*/
|
||||
export function verifyPayment(paymentId: string, data: Api.Billing.VerifyPaymentCommand) {
|
||||
return request.put<Api.Billing.PaymentRecordDto>({
|
||||
url: `${BILLING_BASE_URL}/payments/${paymentId}/verify`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账单状态
|
||||
* @param data 批量更新数据
|
||||
*/
|
||||
export function batchUpdateStatus(data: Api.Billing.BatchUpdateStatusCommand) {
|
||||
return request.post<void>({
|
||||
url: `${BILLING_BASE_URL}/batch/status`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出账单
|
||||
* @param data 导出参数
|
||||
* @returns Blob 对象(用于下载)
|
||||
*/
|
||||
export function exportBillings(data: Api.Billing.ExportParams) {
|
||||
return request.post<Blob>({
|
||||
url: `${BILLING_BASE_URL}/export`,
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 兼容导出(适配现有页面命名) ====================
|
||||
|
||||
/** 获取账单列表(兼容旧命名) */
|
||||
export const fetchGetBillList = fetchBillingList
|
||||
|
||||
/** 获取账单详情(兼容旧命名) */
|
||||
export const fetchGetBillDetail = fetchBillingDetail
|
||||
|
||||
/** 获取账单列表(导出用,兼容旧命名) */
|
||||
export const fetchGetBillListForExport = fetchBillingListForExport
|
||||
|
||||
/** 创建账单(兼容旧命名) */
|
||||
export const fetchCreateBill = createBilling
|
||||
|
||||
/** 更新账单状态(兼容旧命名) */
|
||||
export const fetchUpdateBillStatus = updateBillingStatus
|
||||
|
||||
/** 记录支付(兼容旧命名) */
|
||||
export const fetchRecordPayment = recordPayment
|
||||
70
src/api/dictionary/group.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups'
|
||||
|
||||
export function getGroups(params?: Api.Dictionary.DictionaryGroupQueryParams) {
|
||||
return request.get<Api.Common.PageResult<Api.Dictionary.DictionaryGroupDto>>({
|
||||
url: GROUP_BASE_URL,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getGroupById(groupId: string) {
|
||||
return request.get<Api.Dictionary.DictionaryGroupDto>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}`
|
||||
})
|
||||
}
|
||||
|
||||
export function createGroup(data: Api.Dictionary.CreateDictionaryGroupRequest) {
|
||||
return request.post<Api.Dictionary.DictionaryGroupDto>({
|
||||
url: GROUP_BASE_URL,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateGroup(groupId: string, data: Api.Dictionary.UpdateDictionaryGroupRequest) {
|
||||
return request.put<Api.Dictionary.DictionaryGroupDto>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteGroup(groupId: string) {
|
||||
return request.del<void>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}`
|
||||
})
|
||||
}
|
||||
|
||||
export function exportGroup(
|
||||
groupId: string,
|
||||
format: Api.Dictionary.DictionaryExportRequest['format']
|
||||
) {
|
||||
return request.post<Blob>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/export`,
|
||||
data: { format },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function importGroup(
|
||||
groupId: string,
|
||||
payload: {
|
||||
file: File
|
||||
conflictMode?: Api.Dictionary.ConflictResolutionMode | string
|
||||
format?: 'csv' | 'json'
|
||||
}
|
||||
) {
|
||||
const formData = new FormData()
|
||||
formData.append('File', payload.file)
|
||||
if (payload.conflictMode) {
|
||||
formData.append('ConflictMode', String(payload.conflictMode))
|
||||
}
|
||||
if (payload.format) {
|
||||
formData.append('Format', payload.format)
|
||||
}
|
||||
|
||||
return request.post<Api.Dictionary.DictionaryImportResultDto>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/import`,
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
33
src/api/dictionary/item.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups'
|
||||
|
||||
export function getItems(groupId: string) {
|
||||
return request.get<Api.Dictionary.DictionaryItemDto[]>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/items`
|
||||
})
|
||||
}
|
||||
|
||||
export function createItem(groupId: string, data: Api.Dictionary.CreateDictionaryItemRequest) {
|
||||
return request.post<Api.Dictionary.DictionaryItemDto>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/items`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateItem(
|
||||
groupId: string,
|
||||
itemId: string,
|
||||
data: Api.Dictionary.UpdateDictionaryItemRequest
|
||||
) {
|
||||
return request.put<Api.Dictionary.DictionaryItemDto>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteItem(groupId: string, itemId: string) {
|
||||
return request.del<void>({
|
||||
url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}`
|
||||
})
|
||||
}
|
||||
70
src/api/dictionary/labelOverride.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const LABEL_OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/label-overrides'
|
||||
|
||||
// ==================== 租户端 API ====================
|
||||
|
||||
/**
|
||||
* 获取当前租户的标签覆盖列表
|
||||
*/
|
||||
export function getTenantLabelOverrides() {
|
||||
return request.get<Api.Dictionary.LabelOverrideDto[]>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/tenant`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户创建/更新标签覆盖
|
||||
*/
|
||||
export function upsertTenantLabelOverride(data: Api.Dictionary.UpsertLabelOverrideRequest) {
|
||||
return request.post<Api.Dictionary.LabelOverrideDto>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/tenant`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户删除标签覆盖
|
||||
*/
|
||||
export function deleteTenantLabelOverride(dictionaryItemId: string) {
|
||||
return request.del<void>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/tenant/${dictionaryItemId}`
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 平台端 API ====================
|
||||
|
||||
/**
|
||||
* 获取指定租户的所有标签覆盖(平台管理员用)
|
||||
*/
|
||||
export function getPlatformLabelOverrides(
|
||||
targetTenantId: string,
|
||||
overrideType?: Api.Dictionary.OverrideType
|
||||
) {
|
||||
return request.get<Api.Dictionary.LabelOverrideDto[]>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`,
|
||||
params: { overrideType }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台强制覆盖租户字典项的标签
|
||||
*/
|
||||
export function upsertPlatformLabelOverride(
|
||||
targetTenantId: string,
|
||||
data: Api.Dictionary.UpsertLabelOverrideRequest
|
||||
) {
|
||||
return request.post<Api.Dictionary.LabelOverrideDto>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台删除对租户的强制覆盖
|
||||
*/
|
||||
export function deletePlatformLabelOverride(targetTenantId: string, dictionaryItemId: string) {
|
||||
return request.del<void>({
|
||||
url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}/${dictionaryItemId}`
|
||||
})
|
||||
}
|
||||
35
src/api/dictionary/metrics.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const METRICS_BASE_URL = '/api/admin/v1/dictionary/metrics'
|
||||
|
||||
export function getCacheStats(timeRange: '1h' | '24h' | '7d' = '1h') {
|
||||
return request.get<{
|
||||
totalHits: number
|
||||
totalMisses: number
|
||||
hitRatio: number
|
||||
hitsByLevel: { l1: number; l2: number }
|
||||
missesByLevel: { l1: number; l2: number }
|
||||
averageQueryDurationMs: number
|
||||
topQueriedDictionaries: Array<{ code: string; queryCount: number }>
|
||||
}>({
|
||||
url: `${METRICS_BASE_URL}/cache-stats`,
|
||||
params: { timeRange }
|
||||
})
|
||||
}
|
||||
|
||||
export function getInvalidationEvents(params: {
|
||||
page: number
|
||||
pageSize: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}) {
|
||||
return request.get<Api.Common.PageResult<Api.Dictionary.CacheInvalidationLogDto>>({
|
||||
url: `${METRICS_BASE_URL}/invalidation-events`,
|
||||
params: {
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate
|
||||
}
|
||||
})
|
||||
}
|
||||
49
src/api/dictionary/override.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/overrides'
|
||||
|
||||
export function getOverrides() {
|
||||
return request.get<Api.Dictionary.OverrideConfigDto[]>({
|
||||
url: OVERRIDE_BASE_URL
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverride(groupCode: string) {
|
||||
return request.get<Api.Dictionary.OverrideConfigDto>({
|
||||
url: `${OVERRIDE_BASE_URL}/${groupCode}`
|
||||
})
|
||||
}
|
||||
|
||||
export function enableOverride(groupCode: string) {
|
||||
return request.post<Api.Dictionary.OverrideConfigDto>({
|
||||
url: `${OVERRIDE_BASE_URL}/${groupCode}/enable`,
|
||||
data: {}
|
||||
})
|
||||
}
|
||||
|
||||
export function disableOverride(groupCode: string) {
|
||||
return request.post<void>({
|
||||
url: `${OVERRIDE_BASE_URL}/${groupCode}/disable`,
|
||||
data: {}
|
||||
})
|
||||
}
|
||||
|
||||
export function updateHiddenItems(
|
||||
groupCode: string,
|
||||
hiddenItemIds: Api.Dictionary.DictionaryOverrideHiddenItemsRequest['hiddenItemIds']
|
||||
) {
|
||||
return request.put<Api.Dictionary.OverrideConfigDto>({
|
||||
url: `${OVERRIDE_BASE_URL}/${groupCode}/hidden-items`,
|
||||
data: { hiddenItemIds }
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSortOrder(
|
||||
groupCode: string,
|
||||
sortOrder: Api.Dictionary.DictionaryOverrideSortOrderRequest['sortOrder']
|
||||
) {
|
||||
return request.put<Api.Dictionary.OverrideConfigDto>({
|
||||
url: `${OVERRIDE_BASE_URL}/${groupCode}/sort-order`,
|
||||
data: { sortOrder }
|
||||
})
|
||||
}
|
||||
27
src/api/dictionary/query.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const QUERY_BASE_URL = '/api/admin/v1/dictionaries'
|
||||
|
||||
const normalizeCode = (code: string) => code.trim().toLowerCase()
|
||||
|
||||
export async function getDictionary(code: string) {
|
||||
const result = await batchGetDictionaries([code])
|
||||
return result[code] ?? []
|
||||
}
|
||||
|
||||
export async function batchGetDictionaries(codes: string[]) {
|
||||
if (!codes.length) return {}
|
||||
|
||||
const result = await request.post<Record<string, Api.Dictionary.DictionaryItemDto[]>>({
|
||||
url: `${QUERY_BASE_URL}/batch`,
|
||||
data: { codes }
|
||||
})
|
||||
|
||||
const mapped: Record<string, Api.Dictionary.DictionaryItemDto[]> = {}
|
||||
codes.forEach((code) => {
|
||||
const key = normalizeCode(code)
|
||||
mapped[code] = result[key] ?? result[code] ?? []
|
||||
})
|
||||
|
||||
return mapped
|
||||
}
|
||||
22
src/api/files.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 文件上传 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 上传图片或文件(Admin)
|
||||
* @param file 上传的文件
|
||||
* @param type 上传类型(可选)
|
||||
*/
|
||||
export function fetchUploadFile(file: File, type: Api.Files.UploadFileType = 'other') {
|
||||
const formData = new FormData()
|
||||
formData.append('File', file)
|
||||
formData.append('Type', type)
|
||||
|
||||
return request.post<Api.Files.FileUploadResponse>({
|
||||
url: '/api/admin/v1/files/upload',
|
||||
// 重要:不要手动设置 Content-Type,让浏览器自动带 boundary
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
126
src/api/merchant.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import api from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 获取商户列表
|
||||
*/
|
||||
export function fetchMerchantList(params?: Partial<Api.Merchant.MerchantListParams>) {
|
||||
return api.get<Api.Merchant.MerchantListResponse>({
|
||||
url: '/api/admin/v1/merchants',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户详情
|
||||
*/
|
||||
export function fetchMerchantDetail(merchantId: string, options?: { showErrorMessage?: boolean }) {
|
||||
return api.get<Api.Merchant.MerchantDetail>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}`,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商户信息
|
||||
*/
|
||||
export function fetchUpdateMerchant(
|
||||
merchantId: string,
|
||||
data: Api.Merchant.UpdateMerchantRequest,
|
||||
options?: { showErrorMessage?: boolean }
|
||||
) {
|
||||
return api.put<Api.Merchant.UpdateMerchantResult>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}`,
|
||||
data,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户审核历史
|
||||
*/
|
||||
export function fetchMerchantAuditHistory(merchantId: string) {
|
||||
return api.get<Api.Merchant.MerchantAuditRecord[]>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/audit-history`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户变更历史
|
||||
*/
|
||||
export function fetchMerchantChangeLogs(merchantId: string, params?: { fieldName?: string }) {
|
||||
return api.get<Api.Merchant.MerchantChangeLogRecord[]>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/change-history`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待审核商户列表
|
||||
*/
|
||||
export function fetchPendingReviewList(params?: Partial<Api.Merchant.MerchantListParams>) {
|
||||
return api.get<Api.Merchant.MerchantReviewListResponse>({
|
||||
url: '/api/admin/v1/merchants/pending-review',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核领取信息
|
||||
*/
|
||||
export function fetchMerchantReviewClaim(merchantId: string) {
|
||||
return api.get<Api.Merchant.ClaimInfo | null>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/review/claim`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取审核
|
||||
*/
|
||||
export function fetchClaimMerchantReview(merchantId: string) {
|
||||
return api.post<Api.Merchant.ClaimInfo>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/review/claim`,
|
||||
data: {}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放审核
|
||||
*/
|
||||
export function fetchReleaseMerchantReview(merchantId: string) {
|
||||
return api.del<Api.Merchant.ClaimInfo | null>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/review/claim`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行审核
|
||||
*/
|
||||
export function fetchReviewMerchant(merchantId: string, data: Api.Merchant.ReviewMerchantRequest) {
|
||||
return api.post<void>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/review`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销审核
|
||||
*/
|
||||
export function fetchRevokeMerchantReview(
|
||||
merchantId: string,
|
||||
data: Api.Merchant.RevokeMerchantRequest
|
||||
) {
|
||||
return api.post<void>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/review/revoke`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出商户 PDF
|
||||
*/
|
||||
export function fetchExportMerchantPdf(merchantId: string) {
|
||||
return api.get<Blob>({
|
||||
url: `/api/admin/v1/merchants/${merchantId}/export-pdf`,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
21
src/api/permission.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 获取权限列表(分页)
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchGetPermissions(params?: Api.Permission.PermissionQueryParams) {
|
||||
return request.get<Api.Permission.PermissionPageResult>({
|
||||
url: '/api/admin/v1/permissions',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限树
|
||||
*/
|
||||
export function fetchGetPermissionTree() {
|
||||
return request.get<Api.Permission.PermissionDto[]>({
|
||||
url: '/api/admin/v1/permissions/tree'
|
||||
})
|
||||
}
|
||||
100
src/api/quotaPackage.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 配额包管理 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取配额包列表
|
||||
*/
|
||||
export function fetchQuotaPackageList(params?: {
|
||||
quotaType?: number
|
||||
isActive?: boolean
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}) {
|
||||
return request.get<Api.Common.PageResult<Api.QuotaPackage.QuotaPackageListDto>>({
|
||||
url: '/api/admin/v1/quota-packages',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配额包
|
||||
*/
|
||||
export function fetchCreateQuotaPackage(data: Api.QuotaPackage.CreateQuotaPackageCommand) {
|
||||
return request.post<Api.QuotaPackage.QuotaPackageDto>({
|
||||
url: '/api/admin/v1/quota-packages',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配额包
|
||||
*/
|
||||
export function fetchUpdateQuotaPackage(
|
||||
quotaPackageId: string,
|
||||
data: Api.QuotaPackage.UpdateQuotaPackageCommand
|
||||
) {
|
||||
return request.put<Api.QuotaPackage.QuotaPackageDto>({
|
||||
url: `/api/admin/v1/quota-packages/${quotaPackageId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配额包(软删)
|
||||
*/
|
||||
export function fetchDeleteQuotaPackage(quotaPackageId: string) {
|
||||
return request.del<boolean>({
|
||||
url: `/api/admin/v1/quota-packages/${quotaPackageId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配额包状态(上架/下架)
|
||||
*/
|
||||
export function fetchUpdateQuotaPackageStatus(quotaPackageId: string, isActive: boolean) {
|
||||
return request.put<boolean>({
|
||||
url: `/api/admin/v1/quota-packages/${quotaPackageId}/status`,
|
||||
data: {
|
||||
isActive
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 为租户购买配额包
|
||||
*/
|
||||
export function fetchPurchaseQuotaPackage(
|
||||
tenantId: string,
|
||||
data: Api.QuotaPackage.PurchaseQuotaPackageCommand
|
||||
) {
|
||||
return request.post<Api.QuotaPackage.TenantQuotaPurchaseDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-packages`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户配额使用情况
|
||||
*/
|
||||
export function fetchTenantQuotaUsage(tenantId: string, params?: { quotaType?: number }) {
|
||||
return request.get<Api.QuotaPackage.TenantQuotaUsageDto[]>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-usage`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户配额购买记录
|
||||
*/
|
||||
export function fetchTenantQuotaPurchases(
|
||||
tenantId: string,
|
||||
params?: { page?: number; pageSize?: number }
|
||||
) {
|
||||
return request.get<Api.Common.PageResult<Api.QuotaPackage.TenantQuotaPurchaseDto>>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`,
|
||||
params
|
||||
})
|
||||
}
|
||||
94
src/api/role-template.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 获取角色模板列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchGetRoleTemplates(params?: any) {
|
||||
return request.get<Api.RoleTemplate.RoleTemplateDto[]>({
|
||||
url: '/api/admin/v1/role-templates',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色模板详情
|
||||
* @param templateCode 模板编码
|
||||
*/
|
||||
export function fetchGetRoleTemplateDetail(templateCode: string) {
|
||||
return request.get<Api.RoleTemplate.RoleTemplateDto>({
|
||||
url: `/api/admin/v1/role-templates/${templateCode}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色模板
|
||||
* @param data 创建参数
|
||||
*/
|
||||
export function fetchCreateRoleTemplate(data: Api.RoleTemplate.CreateRoleTemplateCommand) {
|
||||
return request.post<Api.RoleTemplate.RoleTemplateDto>({
|
||||
url: '/api/admin/v1/role-templates',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色模板
|
||||
* @param templateCode 模板编码
|
||||
* @param data 更新参数
|
||||
*/
|
||||
export function fetchUpdateRoleTemplate(
|
||||
templateCode: string,
|
||||
data: Api.RoleTemplate.UpdateRoleTemplateCommand
|
||||
) {
|
||||
return request.put<Api.RoleTemplate.RoleTemplateDto>({
|
||||
url: `/api/admin/v1/role-templates/${templateCode}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色模板
|
||||
* @param templateCode 模板编码
|
||||
*/
|
||||
export function fetchDeleteRoleTemplate(templateCode: string) {
|
||||
return request.del<boolean>({
|
||||
url: `/api/admin/v1/role-templates/${templateCode}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆角色模板
|
||||
* @param templateCode 源模板编码
|
||||
* @param data 克隆参数
|
||||
*/
|
||||
export function fetchCloneRoleTemplate(
|
||||
templateCode: string,
|
||||
data: Api.RoleTemplate.CloneRoleTemplateCommand
|
||||
) {
|
||||
return request.post<Api.RoleTemplate.RoleTemplateDto>({
|
||||
url: `/api/admin/v1/role-templates/${templateCode}/clone`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化角色模板
|
||||
* @param templateCodes 模板编码列表
|
||||
*/
|
||||
export function fetchInitRoleTemplates(templateCodes: string[]) {
|
||||
return request.post<any>({
|
||||
url: '/api/admin/v1/role-templates/init',
|
||||
data: { templateCodes }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色模板权限列表
|
||||
* @param templateCode 模板编码
|
||||
*/
|
||||
export function fetchGetRoleTemplatePermissions(templateCode: string) {
|
||||
return request.get<Api.RoleTemplate.PermissionTemplateDto[]>({
|
||||
url: `/api/admin/v1/role-templates/${templateCode}/permissions`
|
||||
})
|
||||
}
|
||||
47
src/api/statistics.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 统计分析 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取订阅概览统计
|
||||
*/
|
||||
export function fetchSubscriptionOverview() {
|
||||
return request.get<Api.Statistics.SubscriptionOverview>({
|
||||
url: '/api/admin/v1/statistics/subscriptions/overview'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配额使用排行
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchQuotaUsageRanking(params?: Api.Statistics.QuotaUsageRankingParams) {
|
||||
return request.get<Api.Statistics.QuotaUsageRankingResponse>({
|
||||
url: '/api/admin/v1/statistics/quota/ranking',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收入统计
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchRevenueStatistics(params?: Api.Statistics.RevenueStatisticsParams) {
|
||||
return request.get<Api.Statistics.RevenueStatisticsResponse>({
|
||||
url: '/api/admin/v1/statistics/revenue',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取即将到期订阅列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchExpiringSubscriptions(params?: Api.Statistics.ExpiringSubscriptionsParams) {
|
||||
return request.get<Api.Statistics.ExpiringSubscriptionsResponse>({
|
||||
url: '/api/admin/v1/statistics/subscriptions/expiring',
|
||||
params
|
||||
})
|
||||
}
|
||||
309
src/api/store.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import api from '@/utils/http'
|
||||
|
||||
interface StoreRequestOptions {
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
const buildTenantHeader = (options?: StoreRequestOptions) => {
|
||||
// 1. 未提供租户ID时返回空配置
|
||||
if (!options?.tenantId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// 2. 返回携带租户头的请求配置
|
||||
return {
|
||||
headers: {
|
||||
'X-Tenant-Id': String(options.tenantId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店列表
|
||||
*/
|
||||
export function fetchStoreList(params?: Partial<Api.Store.StoreListParams>) {
|
||||
return api.get<Api.Store.StoreListResponse>({
|
||||
url: '/api/admin/v1/stores',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店详情
|
||||
*/
|
||||
export function fetchStoreDetail(storeId: string, options?: { showErrorMessage?: boolean }) {
|
||||
return api.get<Api.Store.StoreDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}`,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建门店
|
||||
*/
|
||||
export function fetchCreateStore(data: Api.Store.CreateStoreRequest) {
|
||||
return api.post<Api.Store.StoreDto>({
|
||||
url: '/api/admin/v1/stores',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新门店
|
||||
*/
|
||||
export function fetchUpdateStore(storeId: string, data: Api.Store.UpdateStoreRequest) {
|
||||
return api.put<Api.Store.StoreDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店
|
||||
*/
|
||||
export function fetchDeleteStore(storeId: string) {
|
||||
return api.del<void>({
|
||||
url: `/api/admin/v1/stores/${storeId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交门店审核
|
||||
*/
|
||||
export function fetchSubmitStoreAudit(storeId: string) {
|
||||
return api.post<boolean>({
|
||||
url: `/api/admin/v1/stores/${storeId}/submit`,
|
||||
data: {}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换门店经营状态
|
||||
*/
|
||||
export function fetchToggleBusinessStatus(
|
||||
storeId: string,
|
||||
data: Api.Store.ToggleBusinessStatusRequest
|
||||
) {
|
||||
return api.post<Api.Store.StoreDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/business-status`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店资质列表
|
||||
*/
|
||||
export function fetchStoreQualifications(storeId: string, options?: StoreRequestOptions) {
|
||||
return api.get<Api.Store.StoreQualificationDto[]>({
|
||||
url: `/api/admin/v1/stores/${storeId}/qualifications`,
|
||||
...buildTenantHeader(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查门店资质完整性
|
||||
*/
|
||||
export function fetchCheckStoreQualifications(storeId: string, options?: StoreRequestOptions) {
|
||||
return api.get<Api.Store.StoreQualificationCheckResultDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/qualifications/check`,
|
||||
...buildTenantHeader(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建门店资质
|
||||
*/
|
||||
export function fetchCreateStoreQualification(
|
||||
storeId: string,
|
||||
data: Api.Store.CreateStoreQualificationRequest,
|
||||
options?: StoreRequestOptions
|
||||
) {
|
||||
return api.post<Api.Store.StoreQualificationDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/qualifications`,
|
||||
data,
|
||||
...buildTenantHeader(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新门店资质
|
||||
*/
|
||||
export function fetchUpdateStoreQualification(
|
||||
storeId: string,
|
||||
qualificationId: string,
|
||||
data: Api.Store.UpdateStoreQualificationRequest,
|
||||
options?: StoreRequestOptions
|
||||
) {
|
||||
return api.put<Api.Store.StoreQualificationDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`,
|
||||
data,
|
||||
...buildTenantHeader(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店资质
|
||||
*/
|
||||
export function fetchDeleteStoreQualification(
|
||||
storeId: string,
|
||||
qualificationId: string,
|
||||
options?: StoreRequestOptions
|
||||
) {
|
||||
return api.del<boolean>({
|
||||
url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`,
|
||||
...buildTenantHeader(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店营业时段
|
||||
*/
|
||||
export function fetchStoreBusinessHours(storeId: string) {
|
||||
return api.get<Api.Store.StoreBusinessHourDto[]>({
|
||||
url: `/api/admin/v1/stores/${storeId}/business-hours`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新门店营业时段
|
||||
*/
|
||||
export function fetchBatchUpdateBusinessHours(
|
||||
storeId: string,
|
||||
data: Api.Store.BatchUpdateBusinessHoursRequest
|
||||
) {
|
||||
return api.put<Api.Store.StoreBusinessHourDto[]>({
|
||||
url: `/api/admin/v1/stores/${storeId}/business-hours/batch`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配送区域
|
||||
*/
|
||||
export function fetchStoreDeliveryZones(storeId: string) {
|
||||
return api.get<Api.Store.StoreDeliveryZoneDto[]>({
|
||||
url: `/api/admin/v1/stores/${storeId}/delivery-zones`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配送区域
|
||||
*/
|
||||
export function fetchCreateStoreDeliveryZone(
|
||||
storeId: string,
|
||||
data: Api.Store.CreateStoreDeliveryZoneRequest
|
||||
) {
|
||||
return api.post<Api.Store.StoreDeliveryZoneDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/delivery-zones`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配送区域
|
||||
*/
|
||||
export function fetchUpdateStoreDeliveryZone(
|
||||
storeId: string,
|
||||
deliveryZoneId: string,
|
||||
data: Api.Store.UpdateStoreDeliveryZoneRequest
|
||||
) {
|
||||
return api.put<Api.Store.StoreDeliveryZoneDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配送区域
|
||||
*/
|
||||
export function fetchDeleteStoreDeliveryZone(storeId: string, deliveryZoneId: string) {
|
||||
return api.del<void>({
|
||||
url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 配送范围检测
|
||||
*/
|
||||
export function fetchDeliveryZoneCheck(storeId: string, data: Api.Store.StoreDeliveryCheckRequest) {
|
||||
return api.post<Api.Store.StoreDeliveryCheckResultDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/delivery-check`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店费用配置
|
||||
*/
|
||||
export function fetchStoreFee(storeId: string) {
|
||||
return api.get<Api.Store.StoreFeeDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/fee`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新门店费用配置
|
||||
*/
|
||||
export function fetchUpdateStoreFee(storeId: string, data: Api.Store.UpdateStoreFeeRequest) {
|
||||
return api.put<Api.Store.StoreFeeDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/fee`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店费用预览
|
||||
*/
|
||||
export function fetchCalculateStoreFee(storeId: string, data: Api.Store.CalculateStoreFeeRequest) {
|
||||
return api.post<Api.Store.StoreFeeCalculationResultDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/fee/calculate`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 临时时段 API ====================
|
||||
|
||||
/**
|
||||
* 获取门店临时时段列表
|
||||
*/
|
||||
export function fetchStoreHolidays(storeId: string) {
|
||||
return api.get<Api.Store.StoreHolidayDto[]>({
|
||||
url: `/api/admin/v1/stores/${storeId}/holidays`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时时段
|
||||
*/
|
||||
export function fetchCreateStoreHoliday(
|
||||
storeId: string,
|
||||
data: Api.Store.CreateStoreHolidayRequest
|
||||
) {
|
||||
return api.post<Api.Store.StoreHolidayDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/holidays`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新临时时段
|
||||
*/
|
||||
export function fetchUpdateStoreHoliday(
|
||||
storeId: string,
|
||||
holidayId: string,
|
||||
data: Api.Store.UpdateStoreHolidayRequest
|
||||
) {
|
||||
return api.put<Api.Store.StoreHolidayDto>({
|
||||
url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除临时时段
|
||||
*/
|
||||
export function fetchDeleteStoreHoliday(storeId: string, holidayId: string) {
|
||||
return api.del<boolean>({
|
||||
url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}`
|
||||
})
|
||||
}
|
||||
103
src/api/storeAudit.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import api from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 获取待审核门店列表
|
||||
*/
|
||||
export function fetchPendingStoreAudits(
|
||||
params?: Partial<Api.StoreAudit.ListPendingStoreAuditsParams>
|
||||
) {
|
||||
return api.get<Api.StoreAudit.PendingStoreAuditResponse>({
|
||||
url: '/api/admin/v1/platform/store-audits/pending',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核统计
|
||||
*/
|
||||
export function fetchStoreAuditStatistics(params?: Api.StoreAudit.StoreAuditStatisticsParams) {
|
||||
return api.get<Api.StoreAudit.StoreAuditStatisticsDto>({
|
||||
url: '/api/admin/v1/platform/store-audits/statistics',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门店审核详情
|
||||
*/
|
||||
export function fetchStoreAuditDetail(storeId: string) {
|
||||
return api.get<Api.StoreAudit.StoreAuditDetailDto>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核记录
|
||||
*/
|
||||
export function fetchStoreAuditRecords(
|
||||
storeId: string,
|
||||
params?: Api.StoreAudit.ListStoreAuditRecordsParams
|
||||
) {
|
||||
return api.get<Api.Common.PageResult<Api.StoreAudit.StoreAuditRecordDto>>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}/records`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过
|
||||
*/
|
||||
export function fetchApproveStoreAudit(
|
||||
storeId: string,
|
||||
data: Api.StoreAudit.ApproveStoreAuditRequest
|
||||
) {
|
||||
return api.post<Api.StoreAudit.StoreAuditActionResultDto>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}/approve`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核驳回
|
||||
*/
|
||||
export function fetchRejectStoreAudit(
|
||||
storeId: string,
|
||||
data: Api.StoreAudit.RejectStoreAuditRequest
|
||||
) {
|
||||
return api.post<Api.StoreAudit.StoreAuditActionResultDto>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}/reject`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制关闭门店
|
||||
*/
|
||||
export function fetchForceCloseStore(storeId: string, data: Api.StoreAudit.ForceCloseStoreRequest) {
|
||||
return api.post<Api.StoreAudit.StoreAuditActionResultDto>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}/force-close`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除强制关闭
|
||||
*/
|
||||
export function fetchReopenStore(storeId: string, data: Api.StoreAudit.ReopenStoreRequest) {
|
||||
return api.post<Api.StoreAudit.StoreAuditActionResultDto>({
|
||||
url: `/api/admin/v1/platform/store-audits/${storeId}/reopen`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资质预警列表(平台)
|
||||
*/
|
||||
export function fetchQualificationAlerts(
|
||||
params?: Partial<Api.Store.StoreQualificationAlertQueryParams>
|
||||
) {
|
||||
return api.get<Api.Store.StoreQualificationAlertResultDto>({
|
||||
url: '/api/admin/v1/platform/store-qualifications/expiring',
|
||||
params
|
||||
})
|
||||
}
|
||||
134
src/api/subscription.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 订阅管理 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取订阅列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function getSubscriptionList(params?: Api.Subscription.SubscriptionListParams) {
|
||||
return request.get<Api.Subscription.SubscriptionListResponse>({
|
||||
url: '/api/admin/v1/subscriptions',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅详情
|
||||
* @param id 订阅ID
|
||||
*/
|
||||
export function getSubscriptionDetail(id: string) {
|
||||
return request.get<Api.Subscription.SubscriptionDetailDto>({
|
||||
url: `/api/admin/v1/subscriptions/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订阅
|
||||
* @param id 订阅ID
|
||||
* @param data 更新数据
|
||||
*/
|
||||
export function updateSubscription(
|
||||
id: string,
|
||||
data: Omit<Api.Subscription.UpdateSubscriptionCommand, 'subscriptionId'>
|
||||
) {
|
||||
return request.put<Api.Subscription.SubscriptionDto>({
|
||||
url: `/api/admin/v1/subscriptions/${id}`,
|
||||
data: {
|
||||
...data,
|
||||
subscriptionId: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 延期订阅
|
||||
* @param id 订阅ID
|
||||
* @param data 延期数据
|
||||
*/
|
||||
export function extendSubscription(
|
||||
id: string,
|
||||
data: Omit<Api.Subscription.ExtendSubscriptionCommand, 'subscriptionId'>
|
||||
) {
|
||||
return request.post<Api.Subscription.SubscriptionDto>({
|
||||
url: `/api/admin/v1/subscriptions/${id}/extend`,
|
||||
data: {
|
||||
...data,
|
||||
subscriptionId: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更套餐
|
||||
* @param id 订阅ID
|
||||
* @param data 变更数据
|
||||
*/
|
||||
export function changeSubscriptionPlan(
|
||||
id: string,
|
||||
data: Omit<Api.Subscription.ChangePlanCommand, 'subscriptionId'>
|
||||
) {
|
||||
return request.post<Api.Subscription.SubscriptionDto>({
|
||||
url: `/api/admin/v1/subscriptions/${id}/change-plan`,
|
||||
data: {
|
||||
...data,
|
||||
subscriptionId: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更订阅状态
|
||||
* @param id 订阅ID
|
||||
* @param data 状态数据
|
||||
*/
|
||||
export function updateSubscriptionStatus(
|
||||
id: string,
|
||||
data: Omit<Api.Subscription.UpdateStatusCommand, 'subscriptionId'>
|
||||
) {
|
||||
return request.post<Api.Subscription.SubscriptionDto>({
|
||||
url: `/api/admin/v1/subscriptions/${id}/status`,
|
||||
data: {
|
||||
...data,
|
||||
subscriptionId: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量延期订阅
|
||||
* @param data 批量延期数据
|
||||
*/
|
||||
export function batchExtendSubscriptions(data: {
|
||||
subscriptionIds: string[]
|
||||
durationDays: number
|
||||
notes?: string
|
||||
}) {
|
||||
return request.post<{
|
||||
successCount: number
|
||||
failureCount: number
|
||||
failures: Array<{ subscriptionId: string; reason: string }>
|
||||
}>({
|
||||
url: '/api/admin/v1/subscriptions/batch-extend',
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送提醒
|
||||
* @param data 批量提醒数据
|
||||
*/
|
||||
export function batchSendReminders(data: { subscriptionIds: string[]; reminderContent: string }) {
|
||||
return request.post<{
|
||||
successCount: number
|
||||
failureCount: number
|
||||
failures: Array<{ subscriptionId: string; reason: string }>
|
||||
}>({
|
||||
url: '/api/admin/v1/subscriptions/batch-remind',
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
86
src/api/system-manage.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import api from '@/utils/http'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
// 1. 获取用户列表
|
||||
export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
|
||||
return api.get<Api.SystemManage.UserListResponse>({
|
||||
url: '/api/admin/v1/users',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 获取用户详情
|
||||
export function fetchGetUserDetail(userId: string, includeDeleted?: boolean) {
|
||||
return api.get<Api.SystemManage.UserDetailDto>({
|
||||
url: `/api/admin/v1/users/${userId}`,
|
||||
params: includeDeleted ? { includeDeleted } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 创建用户
|
||||
export function fetchCreateUser(command: Api.SystemManage.CreateIdentityUserCommand) {
|
||||
return api.post<Api.SystemManage.UserDetailDto>({
|
||||
url: '/api/admin/v1/users',
|
||||
data: command
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 更新用户
|
||||
export function fetchUpdateUser(
|
||||
userId: string,
|
||||
command: Api.SystemManage.UpdateIdentityUserCommand
|
||||
) {
|
||||
return api.put<Api.SystemManage.UserDetailDto>({
|
||||
url: `/api/admin/v1/users/${userId}`,
|
||||
data: command
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 删除用户
|
||||
export function fetchDeleteUser(userId: string) {
|
||||
return api.del<boolean>({
|
||||
url: `/api/admin/v1/users/${userId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 6. 恢复用户
|
||||
export function fetchRestoreUser(userId: string) {
|
||||
return api.post<boolean>({
|
||||
url: `/api/admin/v1/users/${userId}/restore`
|
||||
})
|
||||
}
|
||||
|
||||
// 7. 更新用户状态
|
||||
export function fetchChangeUserStatus(
|
||||
userId: string,
|
||||
command: Api.SystemManage.ChangeIdentityUserStatusCommand
|
||||
) {
|
||||
return api.put<boolean>({
|
||||
url: `/api/admin/v1/users/${userId}/status`,
|
||||
data: command
|
||||
})
|
||||
}
|
||||
|
||||
// 8. 重置用户密码
|
||||
export function fetchResetUserPassword(userId: string) {
|
||||
return api.post<Api.SystemManage.ResetIdentityUserPasswordResult>({
|
||||
url: `/api/admin/v1/users/${userId}/password-reset`
|
||||
})
|
||||
}
|
||||
|
||||
// 9. 批量用户操作
|
||||
export function fetchBatchUserOperation(
|
||||
command: Api.SystemManage.BatchIdentityUserOperationCommand
|
||||
) {
|
||||
return api.post<Api.SystemManage.BatchIdentityUserOperationResult>({
|
||||
url: '/api/admin/v1/users/batch',
|
||||
data: command
|
||||
})
|
||||
}
|
||||
|
||||
// 10. 获取菜单列表
|
||||
export function fetchGetMenuList() {
|
||||
return api.get<AppRouteRecord[]>({
|
||||
url: '/api/v3/system/menus/simple'
|
||||
})
|
||||
}
|
||||
99
src/api/tenant-onboarding.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 自助注册租户
|
||||
* @param data 自助注册参数
|
||||
*/
|
||||
export function fetchSelfRegisterTenant(data: Api.Tenant.SelfRegisterTenantCommand) {
|
||||
return request.post<Api.Tenant.SelfRegisterResult>({
|
||||
url: '/api/public/v1/tenants/self-register',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询租户入驻进度
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
export function fetchTenantProgress(tenantId: string) {
|
||||
return request.get<Api.Tenant.TenantProgress>({
|
||||
url: `/api/public/v1/tenants/${tenantId}/status`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交或更新租户实名信息
|
||||
* @param tenantId 租户ID
|
||||
* @param data 实名资料
|
||||
*/
|
||||
export function submitTenantVerification(
|
||||
tenantId: string,
|
||||
data: Api.Tenant.SubmitTenantVerificationCommand
|
||||
) {
|
||||
return request.post<Api.Tenant.TenantVerificationDto>({
|
||||
url: `/api/public/v1/tenants/${tenantId}/verification`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交或更新租户实名信息(管理端)
|
||||
* @param tenantId 租户ID
|
||||
* @param data 实名资料
|
||||
*/
|
||||
export function submitTenantVerificationAdmin(
|
||||
tenantId: string,
|
||||
data: Api.Tenant.SubmitTenantVerificationCommand
|
||||
) {
|
||||
return request.post<Api.Tenant.TenantVerificationDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/verification`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户订阅
|
||||
* @param tenantId 租户ID
|
||||
* @param data 订阅参数
|
||||
*/
|
||||
export function createTenantSubscription(
|
||||
tenantId: string,
|
||||
data: Api.Tenant.CreateTenantSubscriptionCommand
|
||||
) {
|
||||
return request.post<Api.Tenant.TenantSubscriptionDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/subscriptions`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初次绑定租户订阅(自助入驻)
|
||||
* @param tenantId 租户ID
|
||||
* @param data 初次绑定参数
|
||||
*/
|
||||
export function bindInitialTenantSubscription(
|
||||
tenantId: string,
|
||||
data: Api.Tenant.BindInitialTenantSubscriptionCommand
|
||||
) {
|
||||
return request.post<Api.Tenant.TenantSubscriptionDto>({
|
||||
url: `/api/public/v1/tenants/${tenantId}/subscriptions/initial`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 升降配租户套餐
|
||||
* @param tenantId 租户ID
|
||||
* @param subscriptionId 订阅ID
|
||||
* @param data 升降配参数
|
||||
*/
|
||||
export function changeTenantSubscriptionPlan(
|
||||
tenantId: string,
|
||||
subscriptionId: string,
|
||||
data: Api.Tenant.ChangeTenantSubscriptionPlanCommand
|
||||
) {
|
||||
return request.put<Api.Tenant.TenantSubscriptionDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/subscriptions/${subscriptionId}/plan`,
|
||||
data: { ...data, tenantId, tenantSubscriptionId: subscriptionId }
|
||||
})
|
||||
}
|
||||
103
src/api/tenant-package.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 租户套餐管理 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取租户套餐分页列表
|
||||
*/
|
||||
export function fetchTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) {
|
||||
return request.get<Api.Tenant.TenantPackageListResponse>({
|
||||
url: '/api/admin/v1/tenant-packages',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共租户套餐列表(匿名可访问)
|
||||
*/
|
||||
export function fetchPublicTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) {
|
||||
return request.get<Api.Tenant.PublicTenantPackageListResponse>({
|
||||
url: '/api/public/v1/tenant-packages',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户套餐详情
|
||||
*/
|
||||
export function fetchTenantPackageDetail(tenantPackageId: string) {
|
||||
return request.get<Api.Tenant.TenantPackageDto>({
|
||||
url: `/api/admin/v1/tenant-packages/${tenantPackageId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户套餐
|
||||
*/
|
||||
export function fetchCreateTenantPackage(data: Api.Tenant.CreateTenantPackageCommand) {
|
||||
return request.post<Api.Tenant.TenantPackageDto>({
|
||||
url: '/api/admin/v1/tenant-packages',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户套餐
|
||||
*/
|
||||
export function fetchUpdateTenantPackage(
|
||||
tenantPackageId: string,
|
||||
data: Api.Tenant.UpdateTenantPackageCommand
|
||||
) {
|
||||
return request.put<Api.Tenant.TenantPackageDto>({
|
||||
url: `/api/admin/v1/tenant-packages/${tenantPackageId}`,
|
||||
data: {
|
||||
...data,
|
||||
tenantPackageId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户套餐(软删)
|
||||
*/
|
||||
export function fetchDeleteTenantPackage(tenantPackageId: string) {
|
||||
return request.del<boolean>({
|
||||
url: `/api/admin/v1/tenant-packages/${tenantPackageId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐使用统计(订阅关联数量、使用租户数量)
|
||||
*/
|
||||
export function fetchTenantPackageUsages(tenantPackageIds?: string[]) {
|
||||
const params = new URLSearchParams()
|
||||
if (tenantPackageIds?.length) {
|
||||
tenantPackageIds.forEach((id) => params.append('tenantPackageIds', id))
|
||||
}
|
||||
|
||||
return request.get<Api.Tenant.TenantPackageUsageDto[]>({
|
||||
url: '/api/admin/v1/tenant-packages/usages',
|
||||
params,
|
||||
showErrorMessage: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询套餐当前使用租户列表(按有效订阅口径)
|
||||
*/
|
||||
export function fetchTenantPackageTenants(
|
||||
tenantPackageId: string,
|
||||
params: { keyword?: string; page?: number; pageSize?: number; expiringWithinDays?: number } = {}
|
||||
) {
|
||||
return request.get<Api.Common.PageResult<Api.Tenant.TenantPackageTenantDto>>({
|
||||
url: `/api/admin/v1/tenant-packages/${tenantPackageId}/tenants`,
|
||||
params: {
|
||||
keyword: params.keyword || undefined,
|
||||
expiringWithinDays: params.expiringWithinDays ?? undefined,
|
||||
page: params.page ?? 1,
|
||||
pageSize: params.pageSize ?? 20
|
||||
}
|
||||
})
|
||||
}
|
||||
98
src/api/tenant-role.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 获取租户角色列表
|
||||
* @param tenantId 租户ID
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function fetchGetTenantRoles(
|
||||
tenantId: number | string,
|
||||
params?: Api.TenantRole.RoleQueryParams
|
||||
) {
|
||||
return request.get<Api.TenantRole.RoleDtoPagedResult>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户角色详情
|
||||
* @param tenantId 租户ID
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export function fetchGetTenantRoleDetail(tenantId: number | string, roleId: number | string) {
|
||||
return request.get<Api.TenantRole.RoleDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户角色
|
||||
* @param tenantId 租户ID
|
||||
* @param data 创建参数
|
||||
*/
|
||||
export function fetchCreateTenantRole(
|
||||
tenantId: number | string,
|
||||
data: Api.TenantRole.CreateRoleCommand
|
||||
) {
|
||||
return request.post<Api.TenantRole.RoleDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户角色
|
||||
* @param tenantId 租户ID
|
||||
* @param roleId 角色ID
|
||||
* @param data 更新参数
|
||||
*/
|
||||
export function fetchUpdateTenantRole(
|
||||
tenantId: number | string,
|
||||
roleId: number | string,
|
||||
data: Api.TenantRole.UpdateRoleCommand
|
||||
) {
|
||||
return request.put<Api.TenantRole.RoleDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户角色
|
||||
* @param tenantId 租户ID
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export function fetchDeleteTenantRole(tenantId: number | string, roleId: number | string) {
|
||||
return request.del<boolean>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户角色权限
|
||||
* @param tenantId 租户ID
|
||||
* @param roleId 角色ID
|
||||
*/
|
||||
export function fetchGetTenantRolePermissions(tenantId: number | string, roleId: number | string) {
|
||||
return request.get<string[]>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户角色权限
|
||||
* @param tenantId 租户ID
|
||||
* @param roleId 角色ID
|
||||
* @param permissions 权限代码列表
|
||||
*/
|
||||
export function fetchUpdateTenantRolePermissions(
|
||||
tenantId: number | string,
|
||||
roleId: number | string,
|
||||
permissionIds: string[]
|
||||
) {
|
||||
return request.put<boolean>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions`,
|
||||
data: { permissionIds }
|
||||
})
|
||||
}
|
||||
263
src/api/tenant.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import api from '@/utils/http'
|
||||
import { fetchTenantPackageList } from './tenant-package'
|
||||
import type { QuotaUsageHistoryDto, UpdateTenantCommand } from '@/types/tenant'
|
||||
|
||||
/**
|
||||
* 获取租户列表
|
||||
* @param params 搜索参数
|
||||
*/
|
||||
export function fetchGetTenantList(params?: Partial<Api.Tenant.TenantListParams>) {
|
||||
return api.get<Api.Tenant.TenantListResponse>({
|
||||
url: '/api/admin/v1/tenants',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册租户
|
||||
*/
|
||||
export function fetchRegisterTenant(data: Api.Tenant.RegisterTenantCommand) {
|
||||
return api.post<Api.Tenant.TenantDto>({
|
||||
url: '/api/admin/v1/tenants',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)
|
||||
*/
|
||||
export function fetchCreateTenantManually(data: Api.Tenant.CreateTenantManuallyCommand) {
|
||||
return api.post<Api.Tenant.TenantDetailDto>({
|
||||
url: '/api/admin/v1/tenants/manual',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户详情(包含认证信息、订阅信息)
|
||||
*/
|
||||
export function fetchGetTenantDetail(tenantId: string, options?: { showErrorMessage?: boolean }) {
|
||||
return api.get<Api.Tenant.TenantDetailDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}`,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户配额使用情况
|
||||
*/
|
||||
export function fetchGetTenantQuotaUsage(
|
||||
tenantId: string,
|
||||
options?: { showErrorMessage?: boolean }
|
||||
) {
|
||||
return api.get<Api.QuotaPackage.TenantQuotaUsageDto[]>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-usage`,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户配额使用历史
|
||||
* @param tenantId 租户ID
|
||||
* @param params 分页参数
|
||||
*/
|
||||
export function fetchGetTenantQuotaUsageHistory(
|
||||
tenantId: string,
|
||||
params?: { Page?: number; PageSize?: number }
|
||||
) {
|
||||
return api.get<Api.Common.PageResult<QuotaUsageHistoryDto>>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-usage-history`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅信息(注意:该接口为 POST)
|
||||
*/
|
||||
export function fetchGetTenantSubscriptions(
|
||||
tenantId: string,
|
||||
options?: { showErrorMessage?: boolean }
|
||||
) {
|
||||
return api.post<Api.Tenant.TenantSubscriptionDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/subscriptions`,
|
||||
data: {},
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单信息(分页,最近10条可传 Page=1 PageSize=10)
|
||||
*/
|
||||
export function fetchGetTenantBillings(
|
||||
tenantId: string,
|
||||
params: { Page?: number; PageSize?: number } = {},
|
||||
options?: { showErrorMessage?: boolean }
|
||||
) {
|
||||
return api.get<Api.Common.PageResult<Api.Billing.BillingListDto>>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/billings`,
|
||||
params,
|
||||
showErrorMessage: options?.showErrorMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户信息(TD-001:待后端接口补充)
|
||||
* @param tenantId 租户ID(Snowflake long → string)
|
||||
* @param data 更新数据(tenantId 会以入参为准覆盖)
|
||||
* @returns void(后端 BaseResponse<void> 的 data 字段)
|
||||
*
|
||||
* 后端接口:PUT /api/admin/v1/tenants/{tenantId}
|
||||
* 预期响应:BaseResponse<void>
|
||||
* 预期错误码:400(参数错误)、404(租户不存在)、409(code 冲突)
|
||||
* TODO(TD-001): Swagger 中缺失该端点(当前可能返回 404/405),待后端补充后再联调验证
|
||||
*/
|
||||
export function fetchUpdateTenant(tenantId: string, data: UpdateTenantCommand) {
|
||||
return api.put<void>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}`,
|
||||
data: { ...data, tenantId },
|
||||
showErrorMessage: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户套餐列表(兼容旧调用)
|
||||
*/
|
||||
export const fetchGetTenantPackages = fetchTenantPackageList
|
||||
|
||||
/**
|
||||
* 审核租户
|
||||
*/
|
||||
export function fetchReviewTenant(
|
||||
tenantId: string,
|
||||
data: Omit<Api.Tenant.ReviewTenantCommand, 'tenantId'>
|
||||
) {
|
||||
return api.post<Api.Tenant.TenantDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/review`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户审核领取信息(未领取返回 null)
|
||||
*/
|
||||
export function fetchGetTenantReviewClaim(tenantId: string) {
|
||||
return api.get<Api.Tenant.TenantReviewClaimDto | null>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/review/claim`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取租户审核
|
||||
*/
|
||||
export function fetchClaimTenantReview(tenantId: string) {
|
||||
return api.post<Api.Tenant.TenantReviewClaimDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/review/claim`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制接管租户审核(仅超级管理员)
|
||||
*/
|
||||
export function fetchForceClaimTenantReview(tenantId: string) {
|
||||
return api.post<Api.Tenant.TenantReviewClaimDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/review/force-claim`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放租户审核领取(仅领取人)
|
||||
*/
|
||||
export function fetchReleaseTenantReviewClaim(tenantId: string) {
|
||||
return api.post<Api.Tenant.TenantReviewClaimDto | null>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/review/release`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询租户审核日志
|
||||
*/
|
||||
export function fetchGetTenantAuditLogs(
|
||||
tenantId: string,
|
||||
params: { page?: number; pageSize?: number } = {}
|
||||
) {
|
||||
return api.get<Api.Tenant.TenantAuditLogListResponse>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/audits`,
|
||||
params: {
|
||||
page: params.page ?? 1,
|
||||
pageSize: params.pageSize ?? 20
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 冻结租户(暂停服务)
|
||||
*/
|
||||
export function fetchFreezeTenant(
|
||||
tenantId: string,
|
||||
data: Omit<Api.Tenant.FreezeTenantCommand, 'tenantId'>
|
||||
) {
|
||||
return api.post<Api.Tenant.TenantDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/freeze`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解冻租户(恢复服务)
|
||||
*/
|
||||
export function fetchUnfreezeTenant(
|
||||
tenantId: string,
|
||||
data: Omit<Api.Tenant.UnfreezeTenantCommand, 'tenantId'> = {}
|
||||
) {
|
||||
return api.post<Api.Tenant.TenantDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/unfreeze`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 延期/赠送租户订阅时长(按当前订阅套餐续费)
|
||||
*/
|
||||
export function fetchExtendTenantSubscription(
|
||||
tenantId: string,
|
||||
data: Omit<Api.Tenant.ExtendTenantSubscriptionCommand, 'tenantId'>
|
||||
) {
|
||||
return api.post<Api.Tenant.TenantSubscriptionDto>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/subscriptions/extend`,
|
||||
data: { ...data, tenantId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 伪装登录租户(返回租户主管理员的 Token)
|
||||
*/
|
||||
export function fetchImpersonateTenant(tenantId: string) {
|
||||
return api.post<Api.Auth.TokenResponse>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/impersonate`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成租户主管理员重置密码链接(仅展示一次)
|
||||
*/
|
||||
export function fetchCreateTenantAdminResetLink(tenantId: string) {
|
||||
return api.post<string>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/admin/reset-link`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户配额购买记录(分页)
|
||||
*/
|
||||
export function fetchGetTenantQuotaPurchases(
|
||||
tenantId: string,
|
||||
params: { Page?: number; PageSize?: number } = {}
|
||||
) {
|
||||
return api.get<Api.Common.PageResult<Api.QuotaPackage.TenantQuotaPurchaseDto>>({
|
||||
url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`,
|
||||
params: {
|
||||
Page: params.Page ?? 1,
|
||||
PageSize: params.PageSize ?? 10
|
||||
}
|
||||
})
|
||||
}
|
||||
BIN
src/assets/images/avatar/avatar.webp
Normal file
|
After Width: | Height: | Size: 954 B |
BIN
src/assets/images/avatar/avatar1.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/avatar/avatar10.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/avatar/avatar2.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/avatar/avatar3.webp
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
src/assets/images/avatar/avatar4.webp
Normal file
|
After Width: | Height: | Size: 944 B |
BIN
src/assets/images/avatar/avatar5.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/avatar/avatar6.webp
Normal file
|
After Width: | Height: | Size: 810 B |
BIN
src/assets/images/avatar/avatar7.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/avatar/avatar8.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/avatar/avatar9.webp
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/ceremony/hb.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/ceremony/sd.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/images/ceremony/xc.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/images/ceremony/yd.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/images/common/logo.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/draw/draw1.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/lock/bg_dark.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
src/assets/images/lock/bg_light.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/assets/images/login/lf_icon2.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/images/settings/menu_layouts/dual_column.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
src/assets/images/settings/menu_layouts/horizontal.png
Normal file
|
After Width: | Height: | Size: 409 B |
BIN
src/assets/images/settings/menu_layouts/mixed.png
Normal file
|
After Width: | Height: | Size: 431 B |
BIN
src/assets/images/settings/menu_layouts/vertical.png
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
src/assets/images/settings/menu_styles/dark.png
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
src/assets/images/settings/menu_styles/design.png
Normal file
|
After Width: | Height: | Size: 286 B |
BIN
src/assets/images/settings/menu_styles/light.png
Normal file
|
After Width: | Height: | Size: 293 B |
BIN
src/assets/images/settings/theme_styles/dark.png
Normal file
|
After Width: | Height: | Size: 448 B |
BIN
src/assets/images/settings/theme_styles/light.png
Normal file
|
After Width: | Height: | Size: 416 B |
BIN
src/assets/images/settings/theme_styles/system.png
Normal file
|
After Width: | Height: | Size: 509 B |
1
src/assets/images/svg/403.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="94" y="34" width="212" height="233"><path d="M306 34H94v233h212V34Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M234.427 155.64h38.36V69.6h-38.36v86.04ZM113.326 155.64h121.1V69.6h-121.1v86.04Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.126 155.354h104.2v-72.95h-104.2v72.95ZM236.369 71.05s0 3.3 1.65 5.05c2.33 2.52 7.38-.2 7.38-.2s-1.75 5.15-1.55 10.19c.29 8.24 6.99 9.51 10 4.75 4.56 4.85 8.94-.29 9.52-2.62 4.27 4.76 9.32-.87 9.32-.87v-6.3l-23.99-12.13-12.33 2.13Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M234.429 155.641h-121.1l-15.93 32.11h121.1l15.93-32.11Z" fill="#fff"/><path d="M234.427 69.6h38.46v86.04M113.326 146.52V69.6h121.1M234.429 155.641l-15.93 32.11h-121.1l15.93-32.11h111.39" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M226.37 159.715H116.82l-12.04 23.86H215l11.37-23.86Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="m288.807 187.751-15.92-32.11h-38.46l16.02 32.11h38.36Z" fill="#fff"/><path d="m238.607 163.981 11.84 23.77h38.36l-15.92-32.11h-38.46" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M207.336 223.734c-3.69-13.77-15.44-23.86-29.33-23.86h-8.65s-27.09 14.94-27.09 33.27c0 18.34 25.44 33.18 25.44 33.18h10.4c13.79-.1 25.44-10.19 29.13-23.87 1.75-12.51 0-18.62.1-18.72Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M243.459 240.421c3.98 0 7.28-3.3 7.28-7.27 0-3.98-3.3-7.28-7.28-7.28h-31.08c-3.98 0-7.28 3.3-7.28 7.28 0 3.97 3.3 7.27 7.28 7.27h31.08Z" fill="#C7DEFF"/><path d="M210.342 223.737c-4.08-13.87-16.9-23.96-32.05-23.96H168.972s-29.62 14.94-29.62 33.37 27.87 33.37 27.87 33.37h11.27c15.05-.1 27.77-10.19 31.75-23.96" stroke="#071F4D"/><path d="M212.379 240.421c-3.98 0-7.28-3.3-7.28-7.27m0 0c0-3.98 3.3-7.28 7.28-7.28" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" fill="#006EFF"/><path d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.775 209.38c-13.14 0-23.79 10.64-23.79 23.77 0 13.12 10.65 23.76 23.79 23.76 13.14 0 23.8-10.64 23.8-23.76 0-13.13-10.66-23.77-23.8-23.77Z" fill="#00E4E5"/><path d="M162.174 223.736a17.48 17.48 0 0 1 14.76-8.05M159.455 231.982c.1-1.36.29-2.62.68-3.88" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M173.535 209.87c-1.55-.3-3.11-.49-4.76-.49-13.11 0-23.79 10.67-23.79 23.77 0 13.09 10.68 23.76 23.79 23.76 1.65 0 3.21-.19 4.76-.48-10.88-2.23-19.03-11.84-19.03-23.28 0-11.45 8.15-21.05 19.03-23.28Z" fill="#071F4D"/><path d="M219.957 225.774h23.6c4.08 0 7.38 3.3 7.38 7.37m0 0c0 4.08-3.3 7.37-7.38 7.37h-20.1M212.091 225.774h3.3" stroke="#071F4D"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" fill="#fff"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" stroke="#071F4D"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6l2.04-9.6Z" fill="#fff"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6" stroke="#071F4D"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63l2.04-8.63Z" fill="#fff"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63M147.801 34.485v34.92M121.775 34.485v34.92M102.546 204.724v13.97M102.546 222.379v.87M102.546 197.934v3.49M115.268 206.955v26.29M115.268 239.451v5.34M244.43 197.643v11.93M244.43 213.939v3.49M270.359 201.232v33.76M115.369 47.774h-13.6M94.486 47.774h3.4M241.516 47.774h-84.1M280.168 47.774h25.35" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m282.497 183.575-12.04-23.86h-27.29l11.36 23.86h27.97Z" fill="#00E4E5"/><path d="M234.427 134.88V69.6M234.427 140.412v7.66" stroke="#071F4D"/><path d="M220.831 228.684h16.99M240.934 228.684h2.43" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="m223.842 187.462 21.46-.2-10.97-20.66-10.49 20.86Z" fill="#071F4D"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
1
src/assets/images/svg/404.svg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
5
src/assets/images/svg/500.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg
|
||||
viewBox="0 0 400 300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="38" width="307" height="224"><path d="M353.3 38H47.5v223.8h305.8V38Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M299.2 200.6H61.6v5.1h240.3l-2.7-5.1Z" fill="#C7DEFF"/><path d="m308.9 185.8-6.5 20H183.7M332.3 127.6h10.6l-5 16.7-14.8-.1-7.2 21.1M328.8 127.4l13.6-39.6M307.6 166 337 84.7H180.6l-9.8 26.9h-10.5M296.6 196l4.3-11.8M157.2 149.2l6.4-17.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-34.8 95.8h136.4l34.7-95.8ZM169.9 166.2l5-13.6-5 13.6Z" fill="#fff"/><path d="m169.9 166.2 5-13.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-4 11.7h135.8l4.5-11.7Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M102.6 159.5h38.3l2.7 36.6h-38.4c-10.1 0-20.9-8.2-20.9-18.3 0-10.1 8.2-18.3 18.3-18.3Z" fill="#DEEBFC"/><path fill-rule="evenodd" clip-rule="evenodd" d="M84.3 174.102c2.5 3.4 10 5 17.9 2.8 16.6-6.5 23.8-3.9 23.8-3.9s.5-3.4 1.3-5c-5.8-3-15.4.3-26.1 3.1-10.7 2.8-15.8-2.5-15.8-2.5-.4 0-1.1 2.8-1.1 5.5Z" fill="#fff"/><path d="M96.5 194.2c-7.2-3.3-12.2-10.5-12.2-19m0 0c0-11.5 9.3-20.8 20.8-20.8h29.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8l14.5 19.8Zm-14.5-19.8c0-11.5 9.3-20.8 20.8-20.8l-20.8 20.8Zm20.8-20.8c11.5 0 20.8 9.3 20.8 20.8l-20.8-20.8Zm20.8 20.8c0 8.4-5 15.6-12.1 18.9l12.1-18.9Z" fill="#fff"/><path d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8m0 0c0-11.5 9.3-20.8 20.8-20.8m0 0c11.5 0 20.8 9.3 20.8 20.8m0 0c0 8.4-5 15.6-12.1 18.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.5 177.2c0-7.7-6.3-14-14-14s-14 6.3-14 14c0 5.8 3.5 10.8 8.6 12.9.1 0 5.8 1.6 10.7 0 5.3-1.7 8.7-7.1 8.7-12.9Z" fill="#00E4E5"/><path d="M140.5 190.1c-5.8-2.4-9.9-8.2-9.9-14.9 0-8.9 7.2-16.1 16.1-16.1 8.9 0 16.1 7.2 16.1 16.1 0 6.8-4.2 12.5-10.1 14.9M88.4 170.604c2.9 1.3 7.7 2.6 13.6.3 14.7-5.7 22.3-4.3 24.6-3.5M84.5 174.599s5.9 6.5 19 1.7c9.2-3.4 15.3-3.9 18.8-3.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M340.6 112.3h-55.2l-2.7 6.2H338l2.6-6.2Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M236.8 117.9c-16.13 0-29.2 13.07-29.2 29.2s13.07 29.2 29.2 29.2 29.2-13.07 29.2-29.2-13.07-29.2-29.2-29.2Z" fill="#00E4E5"/><path d="M265 123.3c13.1 13.1 13.1 34.4 0 47.6M306 205.9h19.2M61.7 205.9h32.9M181.2 196.2h115.2M47.5 205.9h10v-9.7h73.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M146.7 179.2c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.5 196.2c3.9 0 7.1 3.2 7.1 7.1 0 3.9-3.2 7.1-7.1 7.1H144c-2.1 0-3.9 1.7-3.9 3.9v1c0 2.1 1.7 3.9 3.9 3.9h48c5.1 0 9.2 4.1 9.2 9.2s-4.1 9.3-9.2 9.2h-33.8c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1h4.2c4.4 0 8 3.6 8 8s-3.6 8-8 8H111c-3.7 0-6.8-3-6.8-6.8 0-3.7 3-6.8 6.8-6.8h.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H79c-4.5 0-8.1-3.6-8.1-8.1s3.6-8.1 8.1-8.1h37.7c2.1 0 3.9-1.7 3.9-3.9 0-2.1-1.7-3.9-3.9-3.9h-7.9c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9h30.4c2.2 0 3.9-1.8 3.9-3.9V187c0-1.9 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5v5.3c0 2.2 1.8 3.9 3.9 3.9h15.5Z" fill="#006EFF"/><path d="m227.8 138.5 18.7 18.7M227.8 157.2l18.7-18.7" stroke="#fff" stroke-width="6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M194.8 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8ZM202.9 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8Z" fill="#fff"/><path d="m291.7 184.3-1.6 4.6h-121M298.1 166.7l22.5-61.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m193 134.1 2.2-5.1h-19.4l-2.3 5.1H193ZM313.2 123.5l2.2-5.1h-24.5l-2.3 5.1h24.6Z" fill="#DEEBFC"/><path d="m164.5 159.2 19.8-54.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M199.6 119.8h-53.2l-4.4 9.3h53.2l4.4-9.3Z" fill="#00E4E5"/><path d="M151.3 129.1H142l4.4-9.3h16.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M353.3 169.4h-67.4l-4.8 12.2h67.3l4.9-12.2Z" fill="#006EFF"/><path d="M332.4 169.4h20.9l-4.9 12.2h-39.7M242.7 235.5v-4.8c0-3.8 3.1-7 7-7h20.2c3.8 0 7 3.1 7 7" stroke="#071F4D"/><path d="M261.1 235.5v-4.8c0-3.8 3.1-7 7-7h13.7c3.8 0 7 3.1 7 7v4.8M242.6 230.7h13.7M235.2 237.7h63.3M224 237.7h6.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.1 141.3H335l3.3-10.7h-10.2l-4 10.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M288.3 230.4c0-3.6-2.9-6.5-6.5-6.5h-14.2c-3.6 0-6.5 2.9-6.5 6.5v5.3h27.2v-5.3Z" fill="#071F4D"/><path d="M80.4 228.5H83M87.7 228.5h19.2M146.3 195.8v2c0 3.6-2.9 6.6-6.6 6.6H138M133.4 204.3h1.5M154 249.9h9.4" stroke="#DEEBFC"/><path d="m299.4 141.9 5.1-13.9" stroke="#071F4D"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
1
src/assets/images/svg/login_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="44" y="42" width="312" height="217"><path d="M355.3 42H44v216.9h311.3V42Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M288.2 248.4h25.1v-30h-25.1v30Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.498 238.199c-1.5-3.9-5.9-15.4-4-21.6-2.9.8-3.3.1-5-.1-1.7-.1 0 10.7 2.2 16.4 1.7 4.5 2.1 11.1 2.1 13.6h5.4c.2-1.9.3-5.5-.7-8.3Z" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6M290.2 214.7h21.4c1 0 1.8.8 1.8 1.8v29" stroke="#071F4D" stroke-width="1.096"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" fill="#fff"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" stroke="#071F4D" stroke-width="1.096"/><path d="M295.402 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3M300.502 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m331 258.4-.3-5.2H88.5l-1.2 5.2H331Z" fill="#C7DEFF"/><path d="M252.9 248.7H331M216.6 258.4H331M47.1 139.3l-2.6 1.5 42.7 117.6h129.2v-6.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" fill="#fff"/><path d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" stroke="#071F4D"/><path d="m203.2 153.2 32.2 88.7H97.8l-32.3-88.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M72.2 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4ZM79.3 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M263.5 171.2h80.3v-63.7h-80.3v63.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M290 143.9h-45.6l12.5 51.3H290v-51.3Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M286 117.4h-29.3v77.8h92.9v-67.6l-55.9.6-7.7-10.8Z" fill="#00E4E5"/><path d="m332.6 127.6-38.9.6-7.7-10.8h-11.7M308.9 195.2h45.9M250.3 195.2h28.5M287.3 195.2h12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 211.4H186v-44h-55.5v44Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M148.7 192.5h-31.6l8.7 35.5h22.9v-35.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M145.9 174.2h-20.2V228h64.1v-46.7l-38.6.4-5.3-7.5Z" fill="#006EFF"/><path d="m179 181.3-27.8.4-5.3-7.5h-7.7M176.2 201.7h19.2M163.2 210.7H195M172.1 228h-54.2M184.8 228h8.1M174.9 228h5.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m293.2 155.7-6.4 6.3 15.3 15.3 22.7-22.6-6.4-6.4-16.3 16.3-8.9-8.9Z" fill="#fff"/><path d="M57.2 258.4h283.6M345.9 258.4h8.1M55.4 258.4h220.5M160.1 118.8l-1.2 2.7M156.7 127c-.3.8-.7 1.8-1.1 2.8M222 68.5c-1 .2-1.9.5-2.9.8M214.1 70.7c-5.8 1.9-11.3 4.4-16.5 7.4M195.4 79.5c-.9.5-1.7 1.1-2.5 1.6M314.2 98.5c-.6-.8-1.3-1.5-2-2.3M308.9 92.8c-4-4-8.3-7.6-13-10.8M293.9 80.7c-.8-.5-1.7-1.1-2.5-1.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.296 71.203c-3.6-1.5-18.5-2.9-21.8-1.9-1 5.8 4.9 13.5 4.9 13.5s6-9.9 16.9-11.6Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.3 42.704c-6.5 6.7-7.8 13-8.8 19.3 24.4-1.1 36.3 13 42.8 20 3.2-9.1 7.8-23 7.2-29-7.1-6.4-20-11.7-41.2-10.3Z" fill="#C7DEFF"/><path d="M230 69.3c36.2-3.8 52 21.1 52 21.1s11.4-28.2 10.5-37.4c-7.3-6.5-23.3-12-45.6-10.1-9 6.3-15.6 18.7-16.9 26.4Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.604 70.7c-6 8.4-9.9 21.9-8.8 33.8 8.4 5.3 32.3 10.5 43.6 11.5 6.1-7.9 15.9-26 15.9-26s-32-4.8-50.7-19.3Z" fill="#C7DEFF"/><path d="M193.103 119.5c4.8-2.7 19.2-29.5 19.2-29.5s-35.8-5.4-53.7-21.8c-9.3 6.1-16.4 24.3-15 40.1 10.6 6.7 45.8 13.3 49.5 11.2Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M189.5 111.6c-3 5.2-5.7 7.2-9.8 6.6 12.2 2.6 13.5 1.2 15.6-1.1 2.2-2.4 4.2-6.6 4.2-6.6s-3.1 2.5-10 1.1Z" fill="#071F4D"/><path d="M331 251.8v6.6M77 165.4l-2.7-6.7h7.8M222.8 228.9l2.8 6.6h-7.9" stroke="#071F4D"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/images/user/avatar.webp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/user/bg.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
71
src/assets/styles/components/_action-btn.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
/**
|
||||
* 统一的操作按钮样式
|
||||
* 用于表格操作列等场景
|
||||
*/
|
||||
|
||||
.art-action-btn {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center; /* 增加居中 */
|
||||
min-width: 64px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
transition: all 0.15s ease; /* 移动 transition 到这里 */
|
||||
|
||||
.art-action-icon {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.97);
|
||||
}
|
||||
|
||||
/* 样式变体 */
|
||||
&.primary {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--el-color-success);
|
||||
background: var(--el-color-success-light-9);
|
||||
border-color: var(--el-color-success-light-8);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--el-color-warning);
|
||||
background: var(--el-color-warning-light-9);
|
||||
border-color: var(--el-color-warning-light-8);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
border-color: var(--el-color-danger-light-8);
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
/* 操作按钮容器 */
|
||||
.action-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
292
src/assets/styles/core/app.scss
Normal file
@@ -0,0 +1,292 @@
|
||||
// 全局样式
|
||||
// 顶部进度条颜色
|
||||
#nprogress .bar {
|
||||
z-index: 2400;
|
||||
background-color: color-mix(in srgb, var(--theme-color) 70%, white);
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow:
|
||||
0 0 10px var(--theme-color),
|
||||
0 0 5px var(--theme-color) !important;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
border-top-color: var(--theme-color) !important;
|
||||
border-left-color: var(--theme-color) !important;
|
||||
}
|
||||
|
||||
// 处理移动端组件兼容性
|
||||
@media screen and (max-width: 640px) {
|
||||
* {
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景滤镜
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
// 色弱模式
|
||||
.color-weak {
|
||||
filter: invert(80%);
|
||||
-webkit-filter: invert(80%);
|
||||
}
|
||||
|
||||
#noop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 语言切换选中样式
|
||||
.langDropDownStyle {
|
||||
// 选中项背景颜色
|
||||
.is-selected {
|
||||
background-color: var(--art-el-active-color) !important;
|
||||
}
|
||||
|
||||
// 语言切换按钮菜单样式优化
|
||||
.lang-btn-item {
|
||||
.el-dropdown-menu__item {
|
||||
padding-left: 13px !important;
|
||||
padding-right: 6px !important;
|
||||
margin-bottom: 3px !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.el-dropdown-menu__item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-txt {
|
||||
min-width: 60px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 盒子默认边框
|
||||
.page-content {
|
||||
border: 1px solid var(--art-card-border) !important;
|
||||
}
|
||||
|
||||
@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) {
|
||||
background: var(--default-box-color);
|
||||
border: 1px solid #{$border-color} !important;
|
||||
border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important;
|
||||
box-shadow: #{$shadow} !important;
|
||||
|
||||
--el-card-border-color: var(--default-border) !important;
|
||||
}
|
||||
|
||||
.art-card,
|
||||
.art-card-sm,
|
||||
.art-card-xs {
|
||||
border: 1px solid var(--art-card-border);
|
||||
}
|
||||
|
||||
// 盒子边框
|
||||
[data-box-mode='border-mode'] {
|
||||
.page-content,
|
||||
.art-table-card {
|
||||
border: 1px solid var(--art-card-border) !important;
|
||||
}
|
||||
|
||||
.art-card {
|
||||
@include art-card-base(var(--art-card-border), none, 4px);
|
||||
}
|
||||
|
||||
.art-card-sm {
|
||||
@include art-card-base(var(--art-card-border), none, 0px);
|
||||
}
|
||||
|
||||
.art-card-xs {
|
||||
@include art-card-base(var(--art-card-border), none, -4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 盒子阴影
|
||||
[data-box-mode='shadow-mode'] {
|
||||
.page-content,
|
||||
.art-table-card {
|
||||
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important;
|
||||
border: 1px solid var(--art-gray-200) !important;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
border-right: 1px solid var(--art-card-border) !important;
|
||||
}
|
||||
|
||||
.art-card {
|
||||
@include art-card-base(
|
||||
var(--art-gray-200),
|
||||
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
|
||||
4px
|
||||
);
|
||||
}
|
||||
|
||||
.art-card-sm {
|
||||
@include art-card-base(
|
||||
var(--art-gray-200),
|
||||
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
|
||||
2px
|
||||
);
|
||||
}
|
||||
|
||||
.art-card-xs {
|
||||
@include art-card-base(
|
||||
var(--art-gray-200),
|
||||
(0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)),
|
||||
-4px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 元素全屏
|
||||
.el-full-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100vw !important;
|
||||
height: 100% !important;
|
||||
z-index: 2300;
|
||||
margin-top: 0;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--default-box-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 表格卡片
|
||||
.art-table-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
.el-card__body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// 容器全高
|
||||
.art-full-height {
|
||||
height: var(--art-full-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 徽章样式
|
||||
.art-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: auto;
|
||||
background: #ff3860;
|
||||
border-radius: 50%;
|
||||
animation: breathe 1.5s ease-in-out infinite;
|
||||
|
||||
&.art-badge-horizontal {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.art-badge-mixed {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.art-badge-dual {
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 文字徽章样式
|
||||
.art-text-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
min-width: 20px;
|
||||
height: 18px;
|
||||
line-height: 17px;
|
||||
padding: 0 5px;
|
||||
margin: auto;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: #fd4e4e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 修复老机型 loading 定位问题
|
||||
.art-loading-fix {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.art-loading-fix .el-loading-spinner {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
// 去除移动端点击背景色
|
||||
@media screen and (max-width: 1180px) {
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
}
|
||||
93
src/assets/styles/core/dark.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 深色主题
|
||||
* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class')
|
||||
*/
|
||||
|
||||
$font-color: rgba(#ffffff, 0.85);
|
||||
|
||||
/* 覆盖element-plus默认深色背景色 */
|
||||
html.dark {
|
||||
// element-plus
|
||||
--el-bg-color: var(--default-box-color);
|
||||
--el-text-color-regular: #{$font-color};
|
||||
|
||||
// 富文本编辑器
|
||||
// 工具栏背景颜色
|
||||
--w-e-toolbar-bg-color: #18191c;
|
||||
// 输入区域背景颜色
|
||||
--w-e-textarea-bg-color: #090909;
|
||||
// 工具栏文字颜色
|
||||
--w-e-toolbar-color: var(--art-gray-600);
|
||||
// 选中菜单颜色
|
||||
--w-e-toolbar-active-bg-color: #25262b;
|
||||
// 弹窗边框颜色
|
||||
--w-e-toolbar-border-color: var(--default-border-dashed);
|
||||
// 分割线颜色
|
||||
--w-e-textarea-border-color: var(--default-border-dashed);
|
||||
// 链接输入框边框颜色
|
||||
--w-e-modal-button-border-color: var(--default-border-dashed);
|
||||
// 表格头颜色
|
||||
--w-e-textarea-slight-bg-color: #090909;
|
||||
// 按钮背景颜色
|
||||
--w-e-modal-button-bg-color: #090909;
|
||||
// hover toolbar 背景颜色
|
||||
--w-e-toolbar-active-color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.dark {
|
||||
.page-content .article-list .item .left .outer > div {
|
||||
border-right-color: var(--dark-border-color) !important;
|
||||
}
|
||||
|
||||
// 富文本编辑器
|
||||
.editor-wrapper {
|
||||
*:not(pre code *) {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
// 分隔线
|
||||
.w-e-bar-divider {
|
||||
background-color: var(--art-gray-300) !important;
|
||||
}
|
||||
|
||||
.w-e-select-list,
|
||||
.w-e-drop-panel,
|
||||
.w-e-bar-item-group .w-e-bar-item-menus-container,
|
||||
.w-e-text-container [data-slate-editor] pre > code {
|
||||
border: 1px solid var(--default-border) !important;
|
||||
}
|
||||
|
||||
// 下拉选择框
|
||||
.w-e-select-list {
|
||||
background-color: var(--default-box-color) !important;
|
||||
}
|
||||
|
||||
/* 下拉选择框 hover 样式调整 */
|
||||
.w-e-select-list ul li:hover,
|
||||
/* 工具栏 hover 按钮背景颜色 */
|
||||
.w-e-bar-item button:hover {
|
||||
background-color: #090909 !important;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.w-e-text-container [data-slate-editor] pre > code {
|
||||
background-color: #25262b !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 引用 */
|
||||
.w-e-text-container [data-slate-editor] blockquote {
|
||||
border-left: 4px solid var(--default-border-dashed) !important;
|
||||
background-color: var(--art-color);
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
|
||||
border-right: 1px solid var(--default-border-dashed) !important;
|
||||
}
|
||||
|
||||
.w-e-modal {
|
||||
background-color: var(--art-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/assets/styles/core/el-dark.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
// 导入暗黑主题
|
||||
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
||||
34
src/assets/styles/core/el-light.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
|
||||
// 自定义Element 亮色主题
|
||||
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||
$colors: (
|
||||
'white': #ffffff,
|
||||
'black': #000000,
|
||||
'success': (
|
||||
'base': #13deb9
|
||||
),
|
||||
'warning': (
|
||||
'base': #ffae1f
|
||||
),
|
||||
'danger': (
|
||||
'base': #ff4d4f
|
||||
),
|
||||
'error': (
|
||||
'base': #fa896b
|
||||
)
|
||||
),
|
||||
$button: (
|
||||
'hover-bg-color': var(--el-color-primary-light-9),
|
||||
'hover-border-color': var(--el-color-primary),
|
||||
'border-color': var(--el-color-primary),
|
||||
'text-color': var(--el-color-primary)
|
||||
),
|
||||
$messagebox: (
|
||||
'border-radius': '12px'
|
||||
),
|
||||
$popover: (
|
||||
'padding': '14px',
|
||||
'border-radius': '10px'
|
||||
)
|
||||
);
|
||||
526
src/assets/styles/core/el-ui.scss
Normal file
@@ -0,0 +1,526 @@
|
||||
// 优化 Element Plus 组件库默认样式
|
||||
|
||||
:root {
|
||||
// 系统主色
|
||||
--main-color: var(--el-color-primary);
|
||||
--el-color-white: white !important;
|
||||
--el-color-black: white !important;
|
||||
// 输入框边框颜色
|
||||
// --el-border-color: #E4E4E7 !important; // DCDFE6
|
||||
// 按钮粗度
|
||||
--el-font-weight-primary: 400 !important;
|
||||
|
||||
--el-component-custom-height: 36px !important;
|
||||
|
||||
--el-component-size: var(--el-component-custom-height) !important;
|
||||
|
||||
// 边框、按钮圆角...
|
||||
--el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important;
|
||||
|
||||
--el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important;
|
||||
--el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
|
||||
--el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
|
||||
|
||||
.region .el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 优化 el-form-item 标签高度
|
||||
.el-form-item__label {
|
||||
height: var(--el-component-custom-height) !important;
|
||||
line-height: var(--el-component-custom-height) !important;
|
||||
}
|
||||
|
||||
// 日期选择器
|
||||
.el-date-range-picker {
|
||||
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
|
||||
}
|
||||
|
||||
// el-card 背景色跟系统背景色保持一致
|
||||
html.dark .el-card {
|
||||
--el-card-bg-color: var(--default-box-color) !important;
|
||||
}
|
||||
|
||||
// 修改 el-pagination 大小
|
||||
.el-pagination--default {
|
||||
& {
|
||||
--el-pagination-button-width: 32px !important;
|
||||
--el-pagination-button-height: var(--el-pagination-button-width) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
& {
|
||||
--el-pagination-button-width: 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select--default .el-select__wrapper {
|
||||
min-height: var(--el-pagination-button-width) !important;
|
||||
}
|
||||
|
||||
.el-pagination__jump .el-input {
|
||||
height: var(--el-pagination-button-width) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pager li {
|
||||
padding: 0 10px !important;
|
||||
// border: 1px solid red !important;
|
||||
}
|
||||
|
||||
// 优化菜单折叠展开动画(提升动画流畅度)
|
||||
.el-menu.el-menu--inline {
|
||||
transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
|
||||
// 优化菜单 item hover 动画(提升鼠标跟手感)
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
transition: background-color 0s !important;
|
||||
}
|
||||
|
||||
// -------------------------------- 修改 el-size=default 组件默认高度 start --------------------------------
|
||||
// 修改 el-button 高度
|
||||
.el-button--default {
|
||||
height: var(--el-component-custom-height) !important;
|
||||
}
|
||||
|
||||
// circle 按钮宽度优化
|
||||
.el-button--default.is-circle {
|
||||
width: var(--el-component-custom-height) !important;
|
||||
}
|
||||
|
||||
// 修改 el-select 高度
|
||||
.el-select--default {
|
||||
.el-select__wrapper {
|
||||
min-height: var(--el-component-custom-height) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 el-checkbox-button 高度
|
||||
.el-checkbox-button--default .el-checkbox-button__inner,
|
||||
// 修改 el-radio-button 高度
|
||||
.el-radio-button--default .el-radio-button__inner {
|
||||
padding: 10px 15px !important;
|
||||
}
|
||||
// -------------------------------- 修改 el-size=default 组件默认高度 end --------------------------------
|
||||
|
||||
.el-pagination.is-background .btn-next,
|
||||
.el-pagination.is-background .btn-prev,
|
||||
.el-pagination.is-background .el-pager li {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-popover {
|
||||
min-width: 80px;
|
||||
border-radius: var(--el-border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 100px !important;
|
||||
border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 25px 0 !important;
|
||||
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
|
||||
}
|
||||
|
||||
.el-dialog.el-dialog-border {
|
||||
.el-dialog__body {
|
||||
// 上边框
|
||||
&::before,
|
||||
// 下边框
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
width: calc(100% + 32px);
|
||||
height: 1px;
|
||||
background-color: var(--art-gray-300);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// el-message 样式优化
|
||||
.el-message {
|
||||
background-color: var(--default-box-color) !important;
|
||||
border: 0 !important;
|
||||
box-shadow:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 el-dropdown 样式
|
||||
.el-dropdown-menu {
|
||||
padding: 6px !important;
|
||||
border-radius: 10px !important;
|
||||
border: none !important;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
padding: 6px 16px !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
color: var(--art-gray-900) !important;
|
||||
background-color: var(--art-el-active-color) !important;
|
||||
}
|
||||
|
||||
&:focus:not(.is-disabled) {
|
||||
color: var(--art-gray-900) !important;
|
||||
background-color: var(--art-gray-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏 select、dropdown 的三角
|
||||
.el-select__popper,
|
||||
.el-dropdown__popper {
|
||||
margin-top: -6px !important;
|
||||
|
||||
.el-popper__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dropdown-selfdefine:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
// 处理移动端组件兼容性
|
||||
@media screen and (max-width: 640px) {
|
||||
.el-message-box,
|
||||
.el-dialog {
|
||||
width: calc(100% - 24px) !important;
|
||||
}
|
||||
|
||||
.el-date-picker.has-sidebar.has-time {
|
||||
width: calc(100% - 24px);
|
||||
left: 12px !important;
|
||||
}
|
||||
|
||||
.el-picker-panel *[slot='sidebar'],
|
||||
.el-picker-panel__sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-picker-panel *[slot='sidebar'] + .el-picker-panel__body,
|
||||
.el-picker-panel__sidebar + .el-picker-panel__body {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改el-button样式
|
||||
.el-button {
|
||||
&.el-button--text {
|
||||
background-color: transparent !important;
|
||||
padding: 0 !important;
|
||||
|
||||
span {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改el-tag样式
|
||||
.el-tag {
|
||||
font-weight: 500;
|
||||
transition: all 0s !important;
|
||||
|
||||
&.el-tag--default {
|
||||
height: 26px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox-group {
|
||||
&.el-table-filter__checkbox-group label.el-checkbox {
|
||||
height: 17px !important;
|
||||
|
||||
.el-checkbox__label {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-radio--default {
|
||||
// 优化单选按钮大小
|
||||
.el-radio__input {
|
||||
.el-radio__inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&::after {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
.el-checkbox__inner {
|
||||
border-radius: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化复选框样式
|
||||
.el-checkbox--default {
|
||||
.el-checkbox__inner {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
border-radius: 4px !important;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 4px !important;
|
||||
top: 5px !important;
|
||||
background-color: #fff !important;
|
||||
transform: scale(0.6) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-checked {
|
||||
.el-checkbox__inner {
|
||||
&::after {
|
||||
width: 3px;
|
||||
height: 8px;
|
||||
margin: auto;
|
||||
border: 2px solid var(--el-checkbox-checked-icon-color);
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important;
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-notification .el-notification__icon {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
// 修改 el-message-box 样式
|
||||
.el-message-box__headerbtn .el-message-box__close,
|
||||
.el-dialog__headerbtn .el-dialog__close {
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-hover-color) !important;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
padding: 25px 20px !important;
|
||||
}
|
||||
|
||||
.el-message-box__title {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.el-table__column-filter-trigger i {
|
||||
color: var(--theme-color) !important;
|
||||
margin: -3px 0 0 2px;
|
||||
}
|
||||
|
||||
// 去除 el-dropdown 鼠标放上去出现的边框
|
||||
.el-tooltip__trigger:focus-visible {
|
||||
outline: unset;
|
||||
}
|
||||
|
||||
// ipad 表单右侧按钮优化
|
||||
@media screen and (max-width: 1180px) {
|
||||
.el-table-fixed-column--right {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-out-dialog {
|
||||
padding: 30px 20px !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
// 修改 dialog 动画
|
||||
.dialog-fade-enter-active {
|
||||
.el-dialog:not(.is-draggable) {
|
||||
animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
|
||||
|
||||
// 修复 el-dialog 动画后宽度不自适应问题
|
||||
.el-select__selected-item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-fade-leave-active {
|
||||
animation: fade-out 0.2s linear;
|
||||
|
||||
.el-dialog:not(.is-draggable) {
|
||||
animation: dialog-close 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-open {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialog-close {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 遮罩层动画
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 el-select 样式
|
||||
.el-select__popper:not(.el-tree-select__popper) {
|
||||
.el-select-dropdown__list {
|
||||
padding: 5px !important;
|
||||
|
||||
.el-select-dropdown__item {
|
||||
height: 34px !important;
|
||||
line-height: 34px !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
&.is-selected {
|
||||
color: var(--art-gray-900) !important;
|
||||
font-weight: 400 !important;
|
||||
background-color: var(--art-el-active-color) !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-hover-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select-dropdown__item:hover ~ .is-selected,
|
||||
.el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 el-tree-select 样式
|
||||
.el-tree-select__popper {
|
||||
.el-select-dropdown__list {
|
||||
padding: 5px !important;
|
||||
|
||||
.el-tree-node {
|
||||
.el-tree-node__content {
|
||||
height: 36px !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-gray-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实现水波纹在文字下面效果
|
||||
.el-button > span {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 优化颜色选择器圆角
|
||||
.el-color-picker__color {
|
||||
border-radius: 2px !important;
|
||||
}
|
||||
|
||||
// 优化日期时间选择器底部圆角
|
||||
.el-picker-panel {
|
||||
.el-picker-panel__footer {
|
||||
border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
// 优化树型菜单样式
|
||||
.el-tree-node__content {
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 1px 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-hover-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
|
||||
background-color: var(--art-gray-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏折叠菜单弹窗 hover 出现的边框
|
||||
.menu-left-popper:focus-within,
|
||||
.horizontal-menu-popper:focus-within {
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
// 数字输入组件右侧按钮高度跟随自定义组件高度
|
||||
.el-input-number--default.is-controls-right {
|
||||
.el-input-number__decrease,
|
||||
.el-input-number__increase {
|
||||
height: calc((var(--el-component-size) / 2)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局输入框文本左对齐(统一表单输入体验)
|
||||
.el-input__inner,
|
||||
.el-textarea__inner,
|
||||
.el-input-number .el-input__inner {
|
||||
text-align: left !important;
|
||||
}
|
||||
1036
src/assets/styles/core/md.scss
Normal file
157
src/assets/styles/core/mixin.scss
Normal file
@@ -0,0 +1,157 @@
|
||||
// sass 混合宏(函数)
|
||||
|
||||
/**
|
||||
* 溢出省略号
|
||||
* @param {Number} 行数
|
||||
*/
|
||||
@mixin ellipsis($rowCount: 1) {
|
||||
@if $rowCount <=1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
} @else {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $rowCount;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制用户能否选中文本
|
||||
* @param {String} 类型
|
||||
*/
|
||||
@mixin userSelect($value: none) {
|
||||
user-select: $value;
|
||||
-moz-user-select: $value;
|
||||
-ms-user-select: $value;
|
||||
-webkit-user-select: $value;
|
||||
}
|
||||
|
||||
// 绝对定位居中
|
||||
@mixin absoluteCenter() {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* css3动画
|
||||
*
|
||||
*/
|
||||
@mixin animation(
|
||||
$from: (
|
||||
width: 0px
|
||||
),
|
||||
$to: (
|
||||
width: 100px
|
||||
),
|
||||
$name: mymove,
|
||||
$animate: mymove 2s 1 linear infinite
|
||||
) {
|
||||
-webkit-animation: $animate;
|
||||
-o-animation: $animate;
|
||||
animation: $animate;
|
||||
|
||||
@keyframes #{$name} {
|
||||
from {
|
||||
@each $key, $value in $from {
|
||||
#{$key}: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
to {
|
||||
@each $key, $value in $to {
|
||||
#{$key}: #{$value};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes #{$name} {
|
||||
from {
|
||||
@each $key, $value in $from {
|
||||
$key: $value;
|
||||
}
|
||||
}
|
||||
|
||||
to {
|
||||
@each $key, $value in $to {
|
||||
$key: $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形盒子
|
||||
@mixin circle($size: 11px, $bg: #fff) {
|
||||
border-radius: 50%;
|
||||
width: $size;
|
||||
height: $size;
|
||||
line-height: $size;
|
||||
text-align: center;
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
// placeholder
|
||||
@mixin placeholder($color: #bbb) {
|
||||
// Firefox
|
||||
&::-moz-placeholder {
|
||||
color: $color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Internet Explorer 10+
|
||||
&:-ms-input-placeholder {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
// Safari and Chrome
|
||||
&::-webkit-input-placeholder {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
//背景透明,文字不透明。兼容IE8
|
||||
@mixin betterTransparentize($color, $alpha) {
|
||||
$c: rgba($color, $alpha);
|
||||
$ie_c: ie_hex_str($c);
|
||||
background: rgba($color, 1);
|
||||
background: $c;
|
||||
background: transparent \9;
|
||||
zoom: 1;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c});
|
||||
-ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})';
|
||||
}
|
||||
|
||||
//添加浏览器前缀
|
||||
@mixin browserPrefix($propertyName, $value) {
|
||||
@each $prefix in -webkit-, -moz-, -ms-, -o-, '' {
|
||||
#{$prefix}#{$propertyName}: $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 边框
|
||||
@mixin border($color: red) {
|
||||
border: 1px solid $color;
|
||||
}
|
||||
|
||||
// 背景滤镜
|
||||
@mixin backdropBlur() {
|
||||
--tw-backdrop-blur: blur(30px);
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
|
||||
var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
|
||||
var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
|
||||
var(--tw-backdrop-sepia);
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
|
||||
var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
|
||||
var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
}
|
||||
41
src/assets/styles/core/reset.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
/*滚动条*/
|
||||
/*滚动条整体部分,必须要设置*/
|
||||
::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/*滚动条的轨道*/
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--art-gray-200);
|
||||
}
|
||||
|
||||
/*滚动条的滑块按钮*/
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: #cccccc !important;
|
||||
transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #b0abab !important;
|
||||
}
|
||||
|
||||
/*滚动条的上下两端的按钮*/
|
||||
::-webkit-scrollbar-button {
|
||||
height: 0px;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--default-bg-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--art-gray-300) !important;
|
||||
}
|
||||
}
|
||||
104
src/assets/styles/core/router-transition.scss
Normal file
@@ -0,0 +1,104 @@
|
||||
@use 'sass:map';
|
||||
|
||||
// === 变量区域 ===
|
||||
$transition: (
|
||||
// 动画持续时间
|
||||
duration: 0.25s,
|
||||
// 滑动动画的移动距离
|
||||
distance: 15px,
|
||||
// 默认缓动函数
|
||||
easing: cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
// 淡入淡出专用的缓动函数
|
||||
fade-easing: cubic-bezier(0.4, 0, 0.6, 1)
|
||||
);
|
||||
|
||||
// 抽取配置值函数,提高可复用性
|
||||
@function transition-config($key) {
|
||||
@return map.get($transition, $key);
|
||||
}
|
||||
|
||||
// 变量简写
|
||||
$duration: transition-config('duration');
|
||||
$distance: transition-config('distance');
|
||||
$easing: transition-config('easing');
|
||||
$fade-easing: transition-config('fade-easing');
|
||||
|
||||
// === 动画类 ===
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity $duration $fade-easing;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-enter-to,
|
||||
&-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动动画通用样式
|
||||
@mixin slide-transition($direction) {
|
||||
$distance-x: 0;
|
||||
$distance-y: 0;
|
||||
|
||||
@if $direction == 'left' {
|
||||
$distance-x: -$distance;
|
||||
} @else if $direction == 'right' {
|
||||
$distance-x: $distance;
|
||||
} @else if $direction == 'top' {
|
||||
$distance-y: -$distance;
|
||||
} @else if $direction == 'bottom' {
|
||||
$distance-y: $distance;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transition:
|
||||
opacity $duration $easing,
|
||||
transform $duration $easing;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition:
|
||||
opacity calc($duration * 0.7) $easing,
|
||||
transform calc($duration * 0.7) $easing;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
&-enter-from {
|
||||
opacity: 0;
|
||||
transform: translate3d($distance-x, $distance-y, 0);
|
||||
}
|
||||
|
||||
&-enter-to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-$distance-x, -$distance-y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动动画方向类
|
||||
.slide-left {
|
||||
@include slide-transition('left');
|
||||
}
|
||||
.slide-right {
|
||||
@include slide-transition('right');
|
||||
}
|
||||
.slide-top {
|
||||
@include slide-transition('top');
|
||||
}
|
||||
.slide-bottom {
|
||||
@include slide-transition('bottom');
|
||||
}
|
||||
208
src/assets/styles/core/tailwind.css
Normal file
@@ -0,0 +1,208 @@
|
||||
@import 'tailwindcss';
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* ==================== Light Mode Variables ==================== */
|
||||
:root {
|
||||
/* Base Colors */
|
||||
--art-color: #ffffff;
|
||||
--theme-color: var(--main-color);
|
||||
|
||||
/* Theme Colors - OKLCH Format */
|
||||
--art-primary: oklch(0.7 0.23 260);
|
||||
--art-secondary: oklch(0.72 0.19 231.6);
|
||||
--art-error: oklch(0.73 0.15 25.3);
|
||||
--art-info: oklch(0.58 0.03 254.1);
|
||||
--art-success: oklch(0.78 0.17 166.1);
|
||||
--art-warning: oklch(0.78 0.14 75.5);
|
||||
--art-danger: oklch(0.68 0.22 25.3);
|
||||
|
||||
/* Gray Scale - Light Mode */
|
||||
--art-gray-100: #f9fafb;
|
||||
--art-gray-200: #f2f4f5;
|
||||
--art-gray-300: #e6eaeb;
|
||||
--art-gray-400: #dbdfe1;
|
||||
--art-gray-500: #949eb7;
|
||||
--art-gray-600: #7987a1;
|
||||
--art-gray-700: #4d5875;
|
||||
--art-gray-800: #383853;
|
||||
--art-gray-900: #323251;
|
||||
|
||||
/* Border Colors */
|
||||
--art-card-border: rgba(0, 0, 0, 0.08);
|
||||
|
||||
--default-border: #e2e8ee;
|
||||
--default-border-dashed: #dbdfe9;
|
||||
|
||||
/* Background Colors */
|
||||
--default-bg-color: #fafbfc;
|
||||
--default-box-color: #ffffff;
|
||||
|
||||
/* Hover Color */
|
||||
--art-hover-color: #edeff0;
|
||||
|
||||
/* Active Color */
|
||||
--art-active-color: #f2f4f5;
|
||||
|
||||
/* Element Component Active Color */
|
||||
--art-el-active-color: #f2f4f5;
|
||||
}
|
||||
|
||||
/* ==================== Dark Mode Variables ==================== */
|
||||
.dark {
|
||||
/* Base Colors */
|
||||
--art-color: #000000;
|
||||
|
||||
/* Gray Scale - Dark Mode */
|
||||
--art-gray-100: #110f0f;
|
||||
--art-gray-200: #17171c;
|
||||
--art-gray-300: #393946;
|
||||
--art-gray-400: #505062;
|
||||
--art-gray-500: #73738c;
|
||||
--art-gray-600: #8f8fa3;
|
||||
--art-gray-700: #ababba;
|
||||
--art-gray-800: #c7c7d1;
|
||||
--art-gray-900: #e3e3e8;
|
||||
|
||||
/* Border Colors */
|
||||
--art-card-border: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--default-border: rgba(255, 255, 255, 0.1);
|
||||
--default-border-dashed: #363843;
|
||||
|
||||
/* Background Colors */
|
||||
--default-bg-color: #070707;
|
||||
--default-box-color: #161618;
|
||||
|
||||
/* Hover Color */
|
||||
--art-hover-color: #252530;
|
||||
|
||||
/* Active Color */
|
||||
--art-active-color: #202226;
|
||||
|
||||
/* Element Component Active Color */
|
||||
--art-el-active-color: #2e2e38;
|
||||
}
|
||||
|
||||
/* ==================== Tailwind Theme Configuration ==================== */
|
||||
@theme {
|
||||
/* Box Color (Light: white / Dark: black) */
|
||||
--color-box: var(--default-box-color);
|
||||
|
||||
/* System Theme Color */
|
||||
--color-theme: var(--theme-color);
|
||||
|
||||
/* Hover Color */
|
||||
--color-hover-color: var(--art-hover-color);
|
||||
|
||||
/* Active Color */
|
||||
--color-active-color: var(--art-active-color);
|
||||
|
||||
/* Active Color */
|
||||
--color-el-active-color: var(--art-active-color);
|
||||
|
||||
/* ElementPlus Theme Colors */
|
||||
--color-primary: var(--art-primary);
|
||||
--color-secondary: var(--art-secondary);
|
||||
--color-error: var(--art-error);
|
||||
--color-info: var(--art-info);
|
||||
--color-success: var(--art-success);
|
||||
--color-warning: var(--art-warning);
|
||||
--color-danger: var(--art-danger);
|
||||
|
||||
/* Gray Scale Colors (Auto-adapts to dark mode) */
|
||||
--color-g-100: var(--art-gray-100);
|
||||
--color-g-200: var(--art-gray-200);
|
||||
--color-g-300: var(--art-gray-300);
|
||||
--color-g-400: var(--art-gray-400);
|
||||
--color-g-500: var(--art-gray-500);
|
||||
--color-g-600: var(--art-gray-600);
|
||||
--color-g-700: var(--art-gray-700);
|
||||
--color-g-800: var(--art-gray-800);
|
||||
--color-g-900: var(--art-gray-900);
|
||||
}
|
||||
|
||||
/* ==================== Custom Border Radius Utilities ==================== */
|
||||
@utility rounded-custom-xs {
|
||||
border-radius: calc(var(--custom-radius) / 2);
|
||||
}
|
||||
|
||||
@utility rounded-custom-sm {
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px);
|
||||
}
|
||||
|
||||
/* ==================== Custom Utility Classes ==================== */
|
||||
@layer utilities {
|
||||
/* Flexbox Layout Utilities */
|
||||
.flex-c {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.flex-b {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
|
||||
.flex-cc {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.flex-cb {
|
||||
@apply flex items-center justify-between;
|
||||
}
|
||||
|
||||
/* Transition Utilities */
|
||||
.tad-200 {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.tad-300 {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
/* Border Utilities */
|
||||
.border-full-d {
|
||||
@apply border border-[var(--default-border)];
|
||||
}
|
||||
|
||||
.border-b-d {
|
||||
@apply border-b border-[var(--default-border)];
|
||||
}
|
||||
|
||||
.border-t-d {
|
||||
@apply border-t border-[var(--default-border)];
|
||||
}
|
||||
|
||||
.border-l-d {
|
||||
@apply border-l border-[var(--default-border)];
|
||||
}
|
||||
|
||||
.border-r-d {
|
||||
@apply border-r border-[var(--default-border)];
|
||||
}
|
||||
|
||||
/* Cursor Utilities */
|
||||
.c-p {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Custom Component Classes ==================== */
|
||||
@layer components {
|
||||
/* Art Card Header Component */
|
||||
.art-card-header {
|
||||
@apply flex justify-between pr-6 pb-1;
|
||||
|
||||
.title {
|
||||
h4 {
|
||||
@apply text-lg font-medium text-g-900;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mt-1 text-sm text-g-600;
|
||||
|
||||
span {
|
||||
@apply ml-2 font-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||