chore: 初始化平台管理端

This commit is contained in:
msumshk
2026-01-29 04:21:09 +00:00
commit 914dcc4166
533 changed files with 104838 additions and 0 deletions

25
.env Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
*.html linguist-detectable=false
*.vue linguist-detectable=true

15
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
pnpm dlx commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
pnpm run lint:lint-staged

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
/node_modules/*
/dist/*
/src/main.ts

20
.prettierrc Normal file
View 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
View 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
View 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'
]
}
]
}
}

View File

@@ -0,0 +1,164 @@
---
trigger: always_on
---
# Repository expectations
# 编程规范\_FOR_AITakeoutAdmin 前端) - 终极融合版
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
## 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.5Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
| **UI/样式** | Element Plus 2.11Tailwind CSS 4SCSS | 组件与样式体系 |
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
| **数据可视化/富文本** | ECharts 6xgplayer 3@wangeditor/editor 5 | 图表/播放器/富文本 |
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
| **工程化** | ESLint 9 + `@typescript-eslint`Prettier 3Stylelint 16HuskyCommitizen | 规范、检查、提交流程 |
## 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
View 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
View 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
View File

@@ -0,0 +1,157 @@
# Repository expectations
# 编程规范\_FOR_AITakeoutAdmin 前端) - 终极融合版
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
## 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.5Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
| **UI/样式** | Element Plus 2.11Tailwind CSS 4SCSS | 组件与样式体系 |
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
| **数据可视化/富文本** | ECharts 6xgplayer 3@wangeditor/editor 5 | 图表/播放器/富文本 |
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
| **工程化** | ESLint 9 + `@typescript-eslint`Prettier 3Stylelint 16HuskyCommitizen | 规范、检查、提交流程 |
## 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
View File

@@ -0,0 +1,157 @@
# Repository expectations
# 编程规范\_FOR_AITakeoutAdmin 前端) - 终极融合版
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
## 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.5Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
| **UI/样式** | Element Plus 2.11Tailwind CSS 4SCSS | 组件与样式体系 |
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
| **数据可视化/富文本** | ECharts 6xgplayer 3@wangeditor/editor 5 | 图表/播放器/富文本 |
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
| **工程化** | ESLint 9 + `@typescript-eslint`Prettier 3Stylelint 16HuskyCommitizen | 规范、检查、提交流程 |
## 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
View File

@@ -0,0 +1,160 @@
# Repository expectations
# 编程规范\_FOR_AITakeoutAdmin 前端) - 终极融合版
> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。
## 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.5Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 |
| **UI/样式** | Element Plus 2.11Tailwind CSS 4SCSS | 组件与样式体系 |
| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 |
| **数据可视化/富文本** | ECharts 6xgplayer 3@wangeditor/editor 5 | 图表/播放器/富文本 |
| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 |
| **工程化** | ESLint 9 + `@typescript-eslint`Prettier 3Stylelint 16HuskyCommitizen | 规范、检查、提交流程 |
## 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
View 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
View 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
View 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
View File

@@ -0,0 +1,138 @@
# 页面ToDo
## 背景与接口基线
- 后端架构Clean ArchitectureApi/Application/Domain/InfrastructureAdmin API 路由前缀 `api/admin/v{version}/`Mini API 路由前缀 `api/mini/v{version}/`,鉴权为 JWT + 权限码(`PermissionAuthorize`),长整型 ID 以字符串返回,前端需保持大数解析(可用 `json-bigint`)。
- Admin 端已交付核心 APIPhase 1
- 租户:注册/分页/详情、实名认证提交与审核、套餐订阅/续费/升降配、审核日志、公告/通知、账单、配额占用校验。
- 商户:入驻创建、证照上传、合同管理、审核流程、审核日志、类目列表。
- 套餐与配额TenantPackage CRUD、订阅历史、配额占用校验。
- RBAC 模板:角色模板 CRUD、复制、租户初始化权限码种子齐全。
- 门店:门店 CRUD、营业时间/配送区/节假日、能力开关(预约/排队)。
- 桌码:区域与桌码 CRUD、批量生成、二维码 ZIP 导出。
- 员工排班:员工 CRUD、门店角色绑定、排班 CRUD默认未来 7 天)。
- 菜品:分类/SPU/SKU/规格/加料/媒资/定价 CRUD上/下架Mini 端可拉取门店菜单。
- 库存SKU 库存与批次、调整/锁定/释放/售罄管理(表已具备,接口可用)。
- 自提档期:自提设置与时间窗 CRUDMini 端按日期查询档期。
- 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

7
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,7 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- '@tailwindcss/oxide'
- core-js
- es5-ext
- esbuild
- vue-demi

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

838
scripts/clean-dev.ts Normal file
View 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
View 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
View 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
View 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
View 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

View 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
})
}

View 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}`
})
}

View 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}`
})
}

View 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
}
})
}

View 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 }
})
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'
})
}

View 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
View 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
View 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
View 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 租户IDSnowflake long → string
* @param data 更新数据tenantId 会以入参为准覆盖)
* @returns void后端 BaseResponse<void> 的 data 字段)
*
* 后端接口PUT /api/admin/v1/tenants/{tenantId}
* 预期响应BaseResponse<void>
* 预期错误码400参数错误、404租户不存在、409code 冲突)
* 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
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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;
}

View 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;
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
// 导入暗黑主题
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

View 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'
)
);

View 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;
}

File diff suppressed because it is too large Load Diff

View 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);
}

View 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;
}
}

View 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');
}

View 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;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More