refactor platform admin from backend contract
This commit is contained in:
@@ -1,68 +1,40 @@
|
||||
# Arco Design Pro Vite
|
||||
# 和气平台总后台
|
||||
|
||||
基于 [Arco Design Pro](https://arco.design/pro/) 的 Vue 3 中后台模板,使用 Vite 8 + Pinia + TypeScript 构建。
|
||||
基于 Vue 3、Vite、TypeScript、Pinia 和 Arco Design 的平台管理端。前端资源、API 路径和操作能力以后端 `resource-contract` 为基准。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 20.19.0
|
||||
- pnpm
|
||||
- Go(同步后端契约时使用)
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
pnpm install # 安装依赖
|
||||
pnpm dev # 开发服务器
|
||||
pnpm build # 生产构建
|
||||
pnpm report # 构建并生成 bundle 分析报告
|
||||
pnpm type:check # TypeScript 检查
|
||||
pnpm lint # Biome 代码检查
|
||||
pnpm lint:fix # 自动修复
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm type:check
|
||||
pnpm lint
|
||||
pnpm contract:sync
|
||||
pnpm contract:check
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
`contract:sync` 从 `backend/api/cmd/cli resource-contract` 生成
|
||||
`src/contracts/platform-resources.json`。修改后端模型或路由后必须重新同步。
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # 接口定义(按业务域)
|
||||
├── assets/ # 静态资源与全局样式
|
||||
├── components/ # 全局 / 布局级组件
|
||||
├── directive/ # 自定义指令
|
||||
├── hooks/ # 组合式函数
|
||||
├── layout/ # 页面布局
|
||||
├── locale/ # i18n 入口与全局文案
|
||||
├── mocks/ # Mock 数据(开发环境)
|
||||
│ ├── handlers/ # 全局 mock 处理器
|
||||
│ └── setup.ts # mock 启用与响应包装
|
||||
├── plugins/ # 应用插件(如 HTTP 拦截器)
|
||||
├── router/ # 路由与守卫
|
||||
├── store/ # Pinia 状态
|
||||
├── types/ # 全局类型
|
||||
├── utils/ # 工具函数
|
||||
└── views/ # 页面(每页可含 components/、locale/、mock.ts)
|
||||
config/
|
||||
└── vite.config.ts # Vite 配置
|
||||
public/ # 静态公共资源
|
||||
```
|
||||
## 实现约定
|
||||
|
||||
## Mock 说明
|
||||
|
||||
仅在开发环境(`import.meta.env.DEV`)下,`main.ts` 会动态加载 `src/mocks/index.ts`;生产构建不会打入 mockjs。
|
||||
|
||||
- 全局 handler 位于 `mocks/handlers/`
|
||||
- 页面级 mock 保留在 `views/**/mock.ts`,由 `import.meta.glob` 自动注册
|
||||
- 后端 API 使用扁平资源路径,例如 `/product_info`、`/wallet_basic`。
|
||||
- 浏览器路由按业务域分组,例如 `/product/product-info`。
|
||||
- 普通资源复用 `views/shared/ResourcePage.vue`。
|
||||
- 产品、钱包、气体配送订单的状态流转通过后端业务动作执行,不直接修改状态字段。
|
||||
- 关联资源统一提交业务 `identity`,不提交数据库自增 ID。
|
||||
- 管理端仅提供中文界面。
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `VITE_API_BASE_URL` | 后端 API 地址(见 `.env.development`) |
|
||||
| `VITE_ERROR_REPORT_URL` | 可选,配置后启用前端错误上报(`utils/error-report.ts`) |
|
||||
|
||||
## i18n 说明
|
||||
|
||||
- 菜单等全局文案:`locale/zh-CN.ts`、`locale/en-US.ts`
|
||||
- 页面文案:`views/**/locale/` 与 `components/**/locale/`,通过 `import.meta.glob` 自动聚合
|
||||
|
||||
## 模板标记
|
||||
|
||||
路由与部分功能块带有 `/** simple */` … `/** simple end */` 注释,表示 Arco Pro「精简版 / 完整版」的可选模块边界。
|
||||
| --- | --- |
|
||||
| `VITE_API_BASE_URL` | 后端 API 地址,默认 `http://localhost:12426/heqi/platform/v1` |
|
||||
| `VITE_ERROR_REPORT_URL` | 可选的前端错误上报地址 |
|
||||
|
||||
@@ -7,13 +7,12 @@ import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { defineConfig, type PluginOption } from 'vite';
|
||||
import compressPlugin from 'vite-plugin-compression';
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
|
||||
const manualChunkGroups: Record<string, string[]> = {
|
||||
arco: ['@arco-design/web-vue'],
|
||||
chart: ['echarts', 'vue-echarts'],
|
||||
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
|
||||
};
|
||||
|
||||
function manualChunks(id: string) {
|
||||
@@ -46,12 +45,6 @@ export default defineConfig(({ command, mode }) => {
|
||||
resolvers: [ArcoResolver()],
|
||||
}),
|
||||
compressPlugin({ ext: '.gz' }),
|
||||
ViteImageOptimizer({
|
||||
png: { quality: 80 },
|
||||
jpeg: { quality: 80 },
|
||||
jpg: { quality: 80 },
|
||||
webp: { quality: 80 },
|
||||
}),
|
||||
);
|
||||
|
||||
if (isReport) {
|
||||
@@ -72,10 +65,6 @@ export default defineConfig(({ command, mode }) => {
|
||||
alias: [
|
||||
{ find: '@', replacement: resolve(__dirname, '../src') },
|
||||
{ find: 'assets', replacement: resolve(__dirname, '../src/assets') },
|
||||
{
|
||||
find: 'vue-i18n',
|
||||
replacement: 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js',
|
||||
},
|
||||
{
|
||||
find: 'vue',
|
||||
replacement: 'vue/dist/vue.esm-bundler.js',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title>
|
||||
<title>和气平台总后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"report": "vite build --config ./config/vite.config.ts --mode report",
|
||||
"preview": "pnpm run build && vite preview --host",
|
||||
"type:check": "vue-tsc -p tsconfig.build.json --noEmit --skipLibCheck",
|
||||
"audit:platform": "node scripts/audit-check.mjs",
|
||||
"contract:sync": "node scripts/sync-backend-contract.mjs",
|
||||
"contract:check": "node scripts/check-backend-contract.mjs",
|
||||
"audit:platform": "node scripts/check-backend-contract.mjs",
|
||||
"lint": "biome lint .",
|
||||
"lint:fix": "biome lint --write .",
|
||||
"format": "biome format --write ."
|
||||
@@ -29,7 +31,6 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -44,12 +45,10 @@
|
||||
"less": "^4.2.2",
|
||||
"mockjs": "^1.1.0",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sharp": "^0.34.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-image-optimizer": "^2.0.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
|
||||
535
frontend/platform_admin/pnpm-lock.yaml
generated
535
frontend/platform_admin/pnpm-lock.yaml
generated
@@ -16,7 +16,7 @@ importers:
|
||||
version: 13.9.0(vue@3.5.38(typescript@5.9.3))
|
||||
axios:
|
||||
specifier: ^1.8.4
|
||||
version: 1.18.0
|
||||
version: 1.18.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.21
|
||||
@@ -44,16 +44,13 @@ importers:
|
||||
vue-echarts:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1(echarts@6.1.0)(vue@3.5.38(typescript@5.9.3))
|
||||
vue-i18n:
|
||||
specifier: ^11.1.2
|
||||
version: 11.4.6(vue@3.5.38(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.5.0
|
||||
version: 4.6.4(vue@3.5.38(typescript@5.9.3))
|
||||
devDependencies:
|
||||
'@arco-plugins/vite-vue':
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
version: 1.4.6(supports-color@7.2.0)
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
@@ -74,7 +71,7 @@ importers:
|
||||
version: 6.0.7(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx':
|
||||
specifier: ^5.0.0
|
||||
version: 5.1.5(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))
|
||||
version: 5.1.5(supports-color@7.2.0)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))
|
||||
less:
|
||||
specifier: ^4.2.2
|
||||
version: 4.6.6
|
||||
@@ -84,27 +81,21 @@ importers:
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.11(rolldown@1.0.3)(rollup@4.62.1)
|
||||
sharp:
|
||||
specifier: ^0.34.1
|
||||
version: 0.34.5
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
unplugin-vue-components:
|
||||
specifier: ^28.8.0
|
||||
version: 28.8.0(@babel/parser@7.29.7)(vue@3.5.38(typescript@5.9.3))
|
||||
version: 28.8.0(@babel/parser@7.29.7)(supports-color@7.2.0)(vue@3.5.38(typescript@5.9.3))
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)
|
||||
vite-plugin-compression:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))
|
||||
vite-plugin-image-optimizer:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.3(sharp@0.34.5)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))
|
||||
version: 0.5.1(supports-color@7.2.0)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))
|
||||
vite-svg-loader:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.1(vue@3.5.38(typescript@5.9.3))
|
||||
version: 5.1.1(supports-color@7.2.0)(vue@3.5.38(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^2.2.8
|
||||
version: 2.2.12(typescript@5.9.3)
|
||||
@@ -302,181 +293,9 @@ packages:
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||
|
||||
'@emnapi/wasi-threads@1.2.1':
|
||||
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@intlify/core-base@11.4.6':
|
||||
resolution: {integrity: sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/devtools-types@11.4.6':
|
||||
resolution: {integrity: sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/message-compiler@11.4.6':
|
||||
resolution: {integrity: sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/shared@11.4.6':
|
||||
resolution: {integrity: sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -884,10 +703,6 @@ packages:
|
||||
alien-signals@1.0.13:
|
||||
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1560,15 +1375,6 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.4:
|
||||
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||
|
||||
@@ -1673,19 +1479,6 @@ packages:
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
|
||||
vite-plugin-image-optimizer@2.0.3:
|
||||
resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
peerDependencies:
|
||||
sharp: '>=0.34.0'
|
||||
svgo: '>=4'
|
||||
vite: '>=5'
|
||||
peerDependenciesMeta:
|
||||
sharp:
|
||||
optional: true
|
||||
svgo:
|
||||
optional: true
|
||||
|
||||
vite-svg-loader@5.1.1:
|
||||
resolution: {integrity: sha512-RPzcXA/EpKJA0585x58DBgs7my2VfeJ+j2j1EoHY4Zh82Y7hV4cR1fElgy2aZi85+QSrcLLoTStQ5uZjD68u+Q==}
|
||||
peerDependencies:
|
||||
@@ -1743,12 +1536,6 @@ packages:
|
||||
echarts: ^6.0.0
|
||||
vue: ^3.3.0
|
||||
|
||||
vue-i18n@11.4.6:
|
||||
resolution: {integrity: sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==}
|
||||
engines: {node: '>= 22'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vue-router@4.6.4:
|
||||
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
|
||||
peerDependencies:
|
||||
@@ -1816,12 +1603,12 @@ snapshots:
|
||||
scroll-into-view-if-needed: 2.2.31
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
|
||||
'@arco-plugins/vite-vue@1.4.6':
|
||||
'@arco-plugins/vite-vue@1.4.6(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
'@types/node': 16.18.126
|
||||
transitivePeerDependencies:
|
||||
@@ -1835,20 +1622,20 @@ snapshots:
|
||||
|
||||
'@babel/compat-data@7.29.7': {}
|
||||
|
||||
'@babel/core@7.29.7':
|
||||
'@babel/core@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/helper-compilation-targets': 7.29.7
|
||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@babel/helpers': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@@ -1875,41 +1662,41 @@ snapshots:
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-annotate-as-pure': 7.29.7
|
||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-optimise-call-expression': 7.29.7
|
||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-globals@7.29.7': {}
|
||||
|
||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||
'@babel/helper-member-expression-to-functions@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-imports@7.29.7':
|
||||
'@babel/helper-module-imports@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -1919,18 +1706,18 @@ snapshots:
|
||||
|
||||
'@babel/helper-plugin-utils@7.29.7': {}
|
||||
|
||||
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-optimise-call-expression': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -1950,24 +1737,24 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
|
||||
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
|
||||
'@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)':
|
||||
'@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-annotate-as-pure': 7.29.7
|
||||
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -1977,7 +1764,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/traverse@7.29.7':
|
||||
'@babel/traverse@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
@@ -1985,7 +1772,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2040,130 +1827,11 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.2.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@intlify/core-base@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/devtools-types': 11.4.6
|
||||
'@intlify/message-compiler': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
|
||||
'@intlify/devtools-types@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
|
||||
'@intlify/message-compiler@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.4.6
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@11.4.6': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -2347,13 +2015,13 @@ snapshots:
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(supports-color@7.2.0)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7)
|
||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
vite: 8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
@@ -2379,27 +2047,27 @@ snapshots:
|
||||
|
||||
'@vue/babel-helper-vue-transform-on@2.0.1': {}
|
||||
|
||||
'@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)':
|
||||
'@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.7
|
||||
'@vue/babel-helper-vue-transform-on': 2.0.1
|
||||
'@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7)
|
||||
'@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
'@vue/shared': 3.5.38
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)':
|
||||
'@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-plugin-utils': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@vue/compiler-sfc': 3.5.38
|
||||
@@ -2513,16 +2181,14 @@ snapshots:
|
||||
|
||||
acorn@8.17.0: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
agent-base@6.0.2(supports-color@7.2.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
alien-signals@1.0.13: {}
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
@@ -2536,11 +2202,11 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.18.0:
|
||||
axios@1.18.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
form-data: 4.0.6
|
||||
https-proxy-agent: 5.0.1
|
||||
https-proxy-agent: 5.0.1(supports-color@7.2.0)
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -2682,9 +2348,11 @@ snapshots:
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
debug@4.4.3:
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
define-lazy-prop@2.0.0: {}
|
||||
|
||||
@@ -2763,7 +2431,9 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
follow-redirects@1.16.0: {}
|
||||
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
|
||||
form-data@4.0.6:
|
||||
dependencies:
|
||||
@@ -2830,10 +2500,10 @@ snapshots:
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
https-proxy-agent@5.0.1(supports-color@7.2.0):
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3
|
||||
agent-base: 6.0.2(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3159,39 +2829,6 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.4: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.4
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.4
|
||||
@@ -3263,10 +2900,10 @@ snapshots:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
|
||||
unplugin-vue-components@28.8.0(@babel/parser@7.29.7)(vue@3.5.38(typescript@5.9.3)):
|
||||
unplugin-vue-components@28.8.0(@babel/parser@7.29.7)(supports-color@7.2.0)(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
local-pkg: 1.2.1
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.2
|
||||
@@ -3292,26 +2929,18 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
vite-plugin-compression@0.5.1(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)):
|
||||
vite-plugin-compression@0.5.1(supports-color@7.2.0)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
fs-extra: 10.1.0
|
||||
vite: 8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)):
|
||||
vite-svg-loader@5.1.1(supports-color@7.2.0)(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
ansi-colors: 4.1.3
|
||||
pathe: 2.0.3
|
||||
vite: 8.0.16(@types/node@26.0.0)(jiti@2.6.1)(less@4.6.6)(yaml@2.9.0)
|
||||
optionalDependencies:
|
||||
sharp: 0.34.5
|
||||
|
||||
vite-svg-loader@5.1.1(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
svgo: 3.3.3
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
@@ -3338,14 +2967,6 @@ snapshots:
|
||||
echarts: 6.1.0
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
|
||||
vue-i18n@11.4.6(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.6
|
||||
'@intlify/devtools-types': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
|
||||
vue-router@4.6.4(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
const files = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? files(path.join(dir, entry.name)) : [path.join(dir, entry.name)]) : [];
|
||||
const sourceFiles = (dir, extensions = ['.ts', '.vue']) => new Map(files(dir).filter((file) => extensions.includes(path.extname(file))).map((file) => [file.replaceAll('\\', '/'), fs.readFileSync(file, 'utf8')]));
|
||||
const mutationActions = (source) => [...source.matchAll(/resourceApi\.(create|update|updateStatus|archive)\b/g)].map((match) => match[1]);
|
||||
|
||||
function requiredBackendRoutes(contract) {
|
||||
if (contract.mode === 'append_only') return [{ method: 'GET', path: contract.path }, { method: 'POST', path: contract.path }];
|
||||
const resource = contract.path;
|
||||
const detail = `${resource}/:identity`;
|
||||
if (contract.mode === 'readonly') return [{ method: 'GET', path: resource }, { method: 'GET', path: detail }];
|
||||
return [
|
||||
{ method: 'GET', path: resource }, { method: 'POST', path: resource }, { method: 'GET', path: detail },
|
||||
{ method: 'PUT', path: detail }, { method: 'PATCH', path: `${detail}/status` }, { method: 'DELETE', path: detail },
|
||||
];
|
||||
}
|
||||
|
||||
function routeCoverage(contract, routeSources, viewSources) {
|
||||
const expectedView = new RegExp(`getResource\\(\\s*['\"]${escapeRegExp(contract.path)}['\"]\\s*\\)`);
|
||||
let hasPage = false;
|
||||
let hasMenu = false;
|
||||
for (const [, source] of sourceEntries(routeSources, 'src/router')) {
|
||||
for (const match of source.matchAll(/component:\s*\(\)\s*=>\s*import\(['\"]@\/views\/([^'\"]+)['\"]\)([\s\S]{0,260}?meta:\s*\{[^}]*\})?/g)) {
|
||||
const view = viewSources.get(`src/views/${match[1]}`);
|
||||
if (!view || !expectedView.test(view)) continue;
|
||||
hasPage = true;
|
||||
if (/locale:\s*['\"]menu\.platform\./.test(match[2] ?? '')) hasMenu = true;
|
||||
}
|
||||
}
|
||||
return { hasPage, hasMenu };
|
||||
}
|
||||
|
||||
function sourceEntries(sources, fallbackDirectory) {
|
||||
if (sources instanceof Map) return [...sources];
|
||||
return sources.map((source, index) => [`${fallbackDirectory}/${index}`, source]);
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/** Scans user-facing API and view sources for auto-increment primary or relation IDs. */
|
||||
export function scanInternalIdLeaks(sources) {
|
||||
const failures = [];
|
||||
for (const [file, source] of sources) {
|
||||
for (const line of source.split(/\r?\n/)) {
|
||||
// Remove only the defensive predicates, then keep scanning the rest of the line.
|
||||
const scanned = line.replace(/key\s*!==\s*['\"]id['\"]/g, '').replace(/key\.endsWith\(\s*['\"]_id['\"]\s*\)/g, '');
|
||||
const relation = scanned.match(/\b([A-Za-z][A-Za-z0-9_]*_id)\b/);
|
||||
if (relation) failures.push(`${file}: internal identifier ${relation[1]}`);
|
||||
else if (/\bdata-index\s*=\s*['\"]id['\"]|\.id\b|[,{]\s*id\s*:/.test(scanned)) failures.push(`${file}: internal identifier id`);
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
/** Evaluates every platform contract against its backend route, UI definition, page and menu route. */
|
||||
export function auditPlatform({ manifest, resources, readOnlyPage, routeSources, viewSources, apiSources, responseShapeVerified = true }) {
|
||||
const failures = [];
|
||||
if (!responseShapeVerified) failures.push('backend list response shape is not identity-only');
|
||||
if (resources.length !== manifest.resources.length) failures.push(`catalogue count: ${resources.length}/${manifest.resources.length}`);
|
||||
for (const contract of manifest.resources) {
|
||||
const resource = resources.find((item) => item.name === contract.name);
|
||||
const pageKind = contract.name === 'ec_category' ? 'tree' : contract.pageKind;
|
||||
const label = `${contract.domain}/${contract.name}`;
|
||||
if (!resource || resource.resource !== contract.path || resource.mode !== contract.mode || resource.pageKind !== pageKind) {
|
||||
failures.push(`${label}: missing frontend resource`);
|
||||
continue;
|
||||
}
|
||||
if (!/^[\u4e00-\u9fff]/.test(resource.title) || resource.fields.length === 0 || resource.fields.some((field) => field.key === 'id' || field.key.endsWith('_id')) || resource.fields.some((field) => !/^[\u4e00-\u9fff]/.test(field.label))) failures.push(`${label}: invalid frontend allowlist`);
|
||||
for (const expected of requiredBackendRoutes(contract)) if (!manifest.routes.some((route) => route.method === expected.method && route.path === expected.path)) failures.push(`${label}: missing backend ${expected.method}`);
|
||||
const coverage = routeCoverage(contract, routeSources, viewSources);
|
||||
if (!coverage.hasPage) failures.push(`${label}: missing page`);
|
||||
if (!coverage.hasMenu) failures.push(`${label}: missing menu route`);
|
||||
if (contract.mode === 'readonly') {
|
||||
const statusCall = new RegExp(`resourceApi\\.updateStatus\\(\\s*['\"]${escapeRegExp(contract.path)}['\"]`);
|
||||
for (const [file, source] of [...sourceEntries(routeSources, 'src/router'), ...sourceEntries(apiSources, 'src/api'), ...sourceEntries(viewSources, 'src/views')]) {
|
||||
if (statusCall.test(source)) failures.push(`${label}: readonly status mutation in ${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(resources.map((item) => item.name).sort()) !== JSON.stringify(manifest.resources.map((item) => item.name).sort())) failures.push('catalogue names differ from backend ExpectedResources');
|
||||
const readonlyMutations = mutationActions(readOnlyPage);
|
||||
if (readonlyMutations.length) failures.push(`readonly: mutation action exposed (${readonlyMutations.join(', ')})`);
|
||||
failures.push(...scanInternalIdLeaks(new Map([...apiSources, ...viewSources])));
|
||||
return failures;
|
||||
}
|
||||
|
||||
function loadResources() {
|
||||
const compiled = ts.transpileModule(fs.readFileSync('src/api/resources.ts', 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
|
||||
const resourceModule = { exports: {} };
|
||||
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
||||
return resourceModule.exports.resources;
|
||||
}
|
||||
|
||||
function runAudit() {
|
||||
const backendDirectory = path.resolve('../..', 'backend/api');
|
||||
const manifest = JSON.parse(execFileSync('go', ['run', './cmd/resource-contract'], { cwd: backendDirectory, encoding: 'utf8' }));
|
||||
let responseShapeVerified = true;
|
||||
try { execFileSync('go', ['test', '-count=1', './internal/logic/platform', '-run', '^TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID$'], { cwd: backendDirectory, stdio: 'pipe' }); } catch { responseShapeVerified = false; }
|
||||
const viewSources = sourceFiles('src/views', ['.vue']);
|
||||
const failures = auditPlatform({
|
||||
manifest,
|
||||
resources: loadResources(),
|
||||
readOnlyPage: fs.readFileSync('src/views/shared/ReadOnlyListPage.vue', 'utf8'),
|
||||
routeSources: sourceFiles('src/router', ['.ts']),
|
||||
viewSources,
|
||||
apiSources: sourceFiles('src/api', ['.ts']),
|
||||
responseShapeVerified,
|
||||
});
|
||||
for (const failure of failures) console.log(failure);
|
||||
if (failures.length) process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]?.replaceAll('\\', '/')}`) runAudit();
|
||||
@@ -1,89 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { auditPlatform, scanInternalIdLeaks } from './audit-check.mjs';
|
||||
|
||||
const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
|
||||
const legacySafetyPrefix = ['sa', 'f_'].join('');
|
||||
const legacyAuditPrefix = ['au', 'd_'].join('');
|
||||
|
||||
test('安全模块资源定义已完全删除', () => {
|
||||
assert.doesNotMatch(resourcesSource, /safe_(?:rule|event|inspection|event_disposal)|\/safety\//);
|
||||
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacyAuditPrefix}(?:operation_log|export_log|approval)',`));
|
||||
assert.doesNotMatch(resourcesSource, /audit_(?:operation_log|export_log|approval)/);
|
||||
});
|
||||
|
||||
test('只读页面将状态变更视为违规写操作', () => {
|
||||
const failures = auditPlatform({
|
||||
manifest: { resources: [], routes: [] },
|
||||
resources: [],
|
||||
readOnlyPage: '<script setup>resourceApi.updateStatus(resource, identity, status)</script>',
|
||||
routeSources: [],
|
||||
viewSources: new Map(),
|
||||
apiSources: new Map(),
|
||||
});
|
||||
|
||||
assert.deepEqual(failures, ['readonly: mutation action exposed (updateStatus)']);
|
||||
});
|
||||
|
||||
test('扫描 API 和页面中用于展示或请求的内部 ID', () => {
|
||||
const failures = scanInternalIdLeaks(new Map([
|
||||
['src/api/leak.ts', "resourceApi.create('/gas/gas_basic', { gas_basic_id: 7 })"],
|
||||
['src/views/leak.vue', '<a-table-column data-index="id" />'],
|
||||
]));
|
||||
|
||||
assert.deepEqual(failures, [
|
||||
'src/api/leak.ts: internal identifier gas_basic_id',
|
||||
'src/views/leak.vue: internal identifier id',
|
||||
]);
|
||||
});
|
||||
|
||||
test('防护表达式不能掩盖同一行的内部 ID 泄漏', () => {
|
||||
const failures = scanInternalIdLeaks(new Map([
|
||||
['src/views/leak.vue', "const visible = row.id; const safe = key !== 'id';"],
|
||||
['src/api/leak.ts', "send({ gas_basic_id: 7 }); const safe = key.endsWith('_id');"],
|
||||
]));
|
||||
|
||||
assert.deepEqual(failures, [
|
||||
'src/views/leak.vue: internal identifier id',
|
||||
'src/api/leak.ts: internal identifier gas_basic_id',
|
||||
]);
|
||||
});
|
||||
|
||||
test('每个资源必须由带菜单元数据的路由实际加载对应页面', () => {
|
||||
const failures = auditPlatform({
|
||||
manifest: { resources: [{ domain: 'gas', name: 'gas_basic', path: '/gas/gas_basic', mode: 'writable', pageKind: 'list' }], routes: [
|
||||
{ method: 'GET', path: '/gas/gas_basic' },
|
||||
{ method: 'POST', path: '/gas/gas_basic' },
|
||||
{ method: 'GET', path: '/gas/gas_basic/:identity' },
|
||||
{ method: 'PUT', path: '/gas/gas_basic/:identity' },
|
||||
{ method: 'PATCH', path: '/gas/gas_basic/:identity/status' },
|
||||
{ method: 'DELETE', path: '/gas/gas_basic/:identity' },
|
||||
] },
|
||||
resources: [{ name: 'gas_basic', resource: '/gas/gas_basic', mode: 'writable', pageKind: 'list', title: '气站管理', fields: [{ key: 'name', label: '名称' }] }],
|
||||
readOnlyPage: '',
|
||||
routeSources: ["{ component: () => import('@/views/gas/gas_basic/ListPage.vue') }"],
|
||||
viewSources: new Map([['src/views/gas/gas_basic/ListPage.vue', "getResource('/gas/gas_basic')"]]),
|
||||
apiSources: new Map(),
|
||||
});
|
||||
|
||||
assert.deepEqual(failures, ['gas/gas_basic: missing menu route']);
|
||||
});
|
||||
|
||||
test('只读资源拒绝 API 或路由中的状态写入', () => {
|
||||
const failures = auditPlatform({
|
||||
manifest: { resources: [{ domain: 'wallet', name: 'wallet', path: '/wallet/wallet', mode: 'readonly', pageKind: 'list' }], routes: [
|
||||
{ method: 'GET', path: '/wallet/wallet' }, { method: 'GET', path: '/wallet/wallet/:identity' },
|
||||
] },
|
||||
resources: [{ name: 'wallet', resource: '/wallet/wallet', mode: 'readonly', pageKind: 'list', title: '钱包', fields: [{ key: 'balance_amount', label: '余额' }] }],
|
||||
readOnlyPage: '',
|
||||
routeSources: ["{ component: () => import('@/views/wallet/wallet/ListPage.vue'), meta: { locale: 'menu.platform.wallet' } }"],
|
||||
viewSources: new Map([['src/views/wallet/wallet/ListPage.vue', "getResource('/wallet/wallet')"]]),
|
||||
apiSources: new Map([['src/api/wallet.ts', "resourceApi.updateStatus('/wallet/wallet', identity, 'disabled')"]]),
|
||||
});
|
||||
|
||||
assert.deepEqual(failures, ['wallet/wallet: readonly status mutation in src/api/wallet.ts']);
|
||||
});
|
||||
35
frontend/platform_admin/scripts/check-backend-contract.mjs
Normal file
35
frontend/platform_admin/scripts/check-backend-contract.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const source = readFileSync(resolve(root, 'src/api/resources.ts'), 'utf8');
|
||||
const routes = readFileSync(resolve(root, 'src/router/routes/modules/platform.ts'), 'utf8');
|
||||
const contract = JSON.parse(
|
||||
readFileSync(resolve(root, 'src/contracts/platform-resources.json'), 'utf8'),
|
||||
);
|
||||
const backend = new Map(contract.resources.map((item) => [item.name, item]));
|
||||
const frontendNames = [...source.matchAll(/define\('([^']+)'/g)].map((match) => match[1]);
|
||||
|
||||
for (const name of frontendNames) {
|
||||
const item = backend.get(name);
|
||||
if (!item) throw new Error(`前端配置了后端不存在的资源:${name}`);
|
||||
if (item.path !== `/${name}`) throw new Error(`资源路径不一致:${name}`);
|
||||
}
|
||||
for (const item of contract.resources) {
|
||||
if (!frontendNames.includes(item.name))
|
||||
throw new Error(`缺少后端资源配置:${item.name}`);
|
||||
}
|
||||
|
||||
const forbidden = [
|
||||
'/platform/platform_', '/gas/gas_', '/delivery/delivery_', '/staff/',
|
||||
'/user/', '/ec/ec_', '/finance/fin_', '/wallet/wallet',
|
||||
'delivery_task', 'delivery_track', 'delivery_track_point',
|
||||
'dev_device_binding', 'dev_smart_cylinder_valve', 'dev_telemetry',
|
||||
'wallet_ledger', 'wallet_recharge', 'wallet_withdrawal',
|
||||
];
|
||||
for (const value of forbidden) {
|
||||
if (source.includes(value) || routes.includes(value))
|
||||
throw new Error(`仍存在旧资源或旧路径:${value}`);
|
||||
}
|
||||
console.log(`契约检查通过:${frontendNames.length} 个资源`);
|
||||
@@ -1,178 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const fromProjectRoot = (...segments) => path.join(projectRoot, ...segments);
|
||||
|
||||
function loadResources() {
|
||||
const compiled = ts.transpileModule(fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const resourceModule = { exports: {} };
|
||||
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
||||
return resourceModule.exports.resources;
|
||||
}
|
||||
|
||||
function loadResourceForm() {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/api/resource-form.ts'), 'utf8');
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const resourceModule = { exports: {} };
|
||||
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
||||
return resourceModule.exports;
|
||||
}
|
||||
|
||||
test('资源字段声明保留数字、布尔、时间与文本类型', () => {
|
||||
const resources = loadResources();
|
||||
const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key);
|
||||
assert.equal(field('ec_product', 'price_amount').type, 'number');
|
||||
assert.equal(field('ec_product_image', 'is_cover').type, 'boolean');
|
||||
assert.equal(field('delivery_track', 'started_at').type, 'datetime');
|
||||
assert.equal(field('dev_telemetry', 'payload').type, 'textarea');
|
||||
});
|
||||
|
||||
test('表单载荷构造器省略空的可选关系并转换字段类型', async () => {
|
||||
const resourceFormPath = fromProjectRoot('src/api/resource-form.ts');
|
||||
assert.ok(fs.existsSync(resourceFormPath), 'resource-form.ts should define the payload boundary');
|
||||
const resourceForm = loadResourceForm();
|
||||
const fields = [
|
||||
{ key: 'gas_basic_identity', label: '气站', type: 'identity' },
|
||||
{ key: 'quantity', label: '数量', type: 'number' },
|
||||
{ key: 'selected', label: '选中', type: 'boolean' },
|
||||
];
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(resourceForm.buildResourcePayload(fields, {
|
||||
gas_basic_identity: '',
|
||||
quantity: '2',
|
||||
selected: false,
|
||||
}))),
|
||||
{ quantity: 2, selected: false },
|
||||
);
|
||||
});
|
||||
|
||||
test('订单明细文本字段允许任意文本提交', () => {
|
||||
const resources = loadResources();
|
||||
const { buildResourcePayload } = loadResourceForm();
|
||||
const fields = (name) => resources.find((item) => item.name === name).fields;
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields('ec_order_item'), {
|
||||
ec_order_identity: 'order-a',
|
||||
ec_product_identity: 'product-a',
|
||||
product_snapshot: 'arbitrary product snapshot text',
|
||||
quantity: 2,
|
||||
sale_amount: 500,
|
||||
}))),
|
||||
{
|
||||
ec_order_identity: 'order-a',
|
||||
ec_product_identity: 'product-a',
|
||||
product_snapshot: 'arbitrary product snapshot text',
|
||||
quantity: 2,
|
||||
sale_amount: 500,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
|
||||
const { buildResourcePayload, isResourceFieldRequired } = loadResourceForm();
|
||||
const fields = [
|
||||
{ key: 'bill_date', label: '账单日期', type: 'date', required: true },
|
||||
{ key: 'started_at', label: '开始时间', type: 'datetime' },
|
||||
{ key: 'username', label: '用户名', type: 'text', required: true },
|
||||
{ key: 'password', label: '密码', type: 'password', required: true },
|
||||
];
|
||||
|
||||
assert.equal(isResourceFieldRequired(fields[3], 'create'), true);
|
||||
assert.equal(isResourceFieldRequired(fields[3], 'edit'), false);
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields, {
|
||||
bill_date: '2026-07-27',
|
||||
started_at: '2026-07-27T10:30:00Z',
|
||||
username: 'operator',
|
||||
password: 'secret',
|
||||
}, 'create'))),
|
||||
{
|
||||
bill_date: '2026-07-27T00:00:00.000Z',
|
||||
started_at: '2026-07-27T10:30:00.000Z',
|
||||
username: 'operator',
|
||||
password: 'secret',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields, {
|
||||
bill_date: '2026-07-27',
|
||||
username: 'operator',
|
||||
password: 'replacement-must-not-be-sent',
|
||||
}, 'edit'))),
|
||||
{
|
||||
bill_date: '2026-07-27T00:00:00.000Z',
|
||||
username: 'operator',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('已删除的审批模块不再保留前端资源或页面', () => {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
|
||||
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
||||
assert.doesNotMatch(resources, /audit_approval|audit_operation_log|audit_export_log/);
|
||||
assert.equal(fs.existsSync(fromProjectRoot('src/views/audit')), false);
|
||||
});
|
||||
|
||||
test('树页面通过资源归档接口归档节点', () => {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/views/shared/TreePage.vue'), 'utf8');
|
||||
assert.match(source, /resourceApi\.archive/);
|
||||
assert.match(source, /Modal\.(warning|confirm)/);
|
||||
});
|
||||
|
||||
test('route access uses authenticated role and assigned menu codes', () => {
|
||||
const routeDir = fromProjectRoot('src/router/routes/modules');
|
||||
const routeSource = fs.readdirSync(routeDir)
|
||||
.filter((name) => name.endsWith('.ts'))
|
||||
.map((name) => fs.readFileSync(path.join(routeDir, name), 'utf8'))
|
||||
.join('\n');
|
||||
const userStore = fs.readFileSync(fromProjectRoot('src/store/modules/user/index.ts'), 'utf8');
|
||||
const permission = fs.readFileSync(fromProjectRoot('src/hooks/permission.ts'), 'utf8');
|
||||
|
||||
assert.doesNotMatch(routeSource, /roles:\s*\[\s*['"]\*['"]\s*\]/);
|
||||
assert.match(routeSource, /menuCode:\s*['"]finance['"]/);
|
||||
assert.match(userStore, /profile\.role_code/);
|
||||
assert.match(userStore, /menuCodes/);
|
||||
assert.match(permission, /menuCode/);
|
||||
});
|
||||
|
||||
test('platform role UI replaces assigned menu identities through the singular contract URL', () => {
|
||||
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
||||
const crudPage = fs.readFileSync(fromProjectRoot('src/views/shared/CrudListPage.vue'), 'utf8');
|
||||
const platformApi = fs.readFileSync(fromProjectRoot('src/api/platform.ts'), 'utf8');
|
||||
|
||||
assert.match(resources, /platform_role[\s\S]*\/platform\/platform_role\/:identity\/menu/);
|
||||
assert.match(resources, /menu-identities/);
|
||||
assert.match(crudPage, /multiple/);
|
||||
assert.match(crudPage, /menu_identities/);
|
||||
assert.match(platformApi, /\/platform\/platform_role\/\$\{identity\}\/menu/);
|
||||
assert.match(platformApi, /method:\s*['"]PUT['"]/);
|
||||
});
|
||||
|
||||
test('platform role UI permits an empty menu selection to revoke every assignment', () => {
|
||||
const resources = loadResources();
|
||||
const { buildResourcePayload, isMissingField } = loadResourceForm();
|
||||
const role = resources.find((item) => item.name === 'platform_role');
|
||||
const menuField = role.detailActions
|
||||
.flatMap((action) => action.fields)
|
||||
.find((field) => field.key === 'menu_identities');
|
||||
|
||||
assert.equal(menuField.required, undefined);
|
||||
assert.equal(isMissingField([]), true);
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload([menuField], {
|
||||
menu_identities: [],
|
||||
}))),
|
||||
{ menu_identities: [] },
|
||||
);
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BASE =
|
||||
'https://raw.githubusercontent.com/arco-design/arco-design-pro-vue/main/arco-design-pro-vite/src';
|
||||
|
||||
const vueFiles = [
|
||||
'views/visualization/multi-dimension-data-analysis/components/content-publishing-source.vue',
|
||||
'views/user/info/components/my-project.vue',
|
||||
'views/user/info/components/my-team.vue',
|
||||
'views/user/setting/components/enterprise-certification.vue',
|
||||
];
|
||||
|
||||
async function download(relPath) {
|
||||
const res = await fetch(`${BASE}/${relPath}`);
|
||||
if (!res.ok) throw new Error(`${relPath}: HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
function patchForProject(content, relPath) {
|
||||
let text = content;
|
||||
|
||||
if (relPath.includes('my-project.vue')) {
|
||||
text = text.replace(
|
||||
"import { queryMyProjectList, MyProjectRecord } from '@/api/user-center';",
|
||||
"import { type MyProjectRecord, queryMyProjectList } from '@/api/user';",
|
||||
);
|
||||
text = text.replace(/\{\{ project\.contributors \}\}\s*/g, '');
|
||||
}
|
||||
|
||||
if (relPath.includes('my-team.vue')) {
|
||||
text = text.replace(
|
||||
"import { queryMyTeamList, MyTeamRecord } from '@/api/user-center';",
|
||||
"import { type MyTeamRecord, queryMyTeamList } from '@/api/user';",
|
||||
);
|
||||
}
|
||||
|
||||
if (relPath.includes('enterprise-certification.vue')) {
|
||||
text = text.replace(
|
||||
"import { EnterpriseCertificationModel } from '@/api/user-center';",
|
||||
"import type { EnterpriseCertificationModel } from '@/api/user';",
|
||||
);
|
||||
text = text.replace(
|
||||
/type: Object as PropType<EnterpriseCertificationModel>/,
|
||||
'type: Object as PropType<EnterpriseCertificationModel>,',
|
||||
);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const relPath of vueFiles) {
|
||||
let content = await download(relPath);
|
||||
content = patchForProject(content, relPath);
|
||||
const fullPath = path.join('src', relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
const hasCn = /[\u4e00-\u9fff]/.test(content);
|
||||
console.log(`OK ${relPath} (cn=${hasCn})`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BASE =
|
||||
'https://raw.githubusercontent.com/arco-design/arco-design-pro-vue/main/arco-design-pro-vite/src';
|
||||
|
||||
const localeFiles = [
|
||||
'locale/zh-CN/settings.ts',
|
||||
'views/login/locale/zh-CN.ts',
|
||||
'views/form/group/locale/zh-CN.ts',
|
||||
'views/form/step/locale/zh-CN.ts',
|
||||
'views/dashboard/workplace/locale/zh-CN.ts',
|
||||
'views/dashboard/monitor/locale/zh-CN.ts',
|
||||
'views/list/card/locale/zh-CN.ts',
|
||||
'views/list/search-table/locale/zh-CN.ts',
|
||||
'views/profile/basic/locale/zh-CN.ts',
|
||||
'views/result/success/locale/zh-CN.ts',
|
||||
'views/result/error/locale/zh-CN.ts',
|
||||
'views/exception/403/locale/zh-CN.ts',
|
||||
'views/exception/404/locale/zh-CN.ts',
|
||||
'views/user/info/locale/zh-CN.ts',
|
||||
'views/user/setting/locale/zh-CN.ts',
|
||||
'views/visualization/data-analysis/locale/zh-CN.ts',
|
||||
'views/visualization/multi-dimension-data-analysis/locale/zh-CN.ts',
|
||||
];
|
||||
|
||||
const rootZhCN = `import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './zh-CN/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.dashboard': '仪表盘',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': '常见问题',
|
||||
'navbar.docs': '文档中心',
|
||||
'navbar.action.locale': '切换为中文',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
`;
|
||||
|
||||
async function download(relPath) {
|
||||
const url = `${BASE}/${relPath}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`${relPath}: HTTP ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const relPath of localeFiles) {
|
||||
const content = await download(relPath);
|
||||
const dest = path.join('src', relPath.replace(/^locale\//, 'locale/'));
|
||||
const fullPath = path.join('src', relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
const hasCn = /[\u4e00-\u9fff]/.test(content);
|
||||
console.log(`OK ${relPath} (cn=${hasCn})`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join('src', 'locale/zh-CN.ts'), rootZhCN, 'utf8');
|
||||
console.log('OK locale/zh-CN.ts (cn=true)');
|
||||
|
||||
// search-table column setting label
|
||||
const stPath = path.join('src', 'views/list/search-table/index.vue');
|
||||
let st = fs.readFileSync(stPath, 'utf8');
|
||||
st = st.replace(
|
||||
"{{ item.title === '#' ? '???' : item.title }}",
|
||||
"{{ item.title === '#' ? '序列号' : item.title }}",
|
||||
);
|
||||
fs.writeFileSync(stPath, st, 'utf8');
|
||||
console.log('OK search-table/index.vue');
|
||||
|
||||
// verify
|
||||
let bad = 0;
|
||||
for (const relPath of ['locale/zh-CN.ts', ...localeFiles]) {
|
||||
const fullPath = path.join('src', relPath);
|
||||
const text = fs.readFileSync(fullPath, 'utf8');
|
||||
if (text.includes("'???'") || text.includes("'??'")) {
|
||||
console.error('STILL BAD:', relPath);
|
||||
bad += 1;
|
||||
}
|
||||
}
|
||||
if (bad) process.exit(1);
|
||||
console.log('All locale files verified');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
16
frontend/platform_admin/scripts/sync-backend-contract.mjs
Normal file
16
frontend/platform_admin/scripts/sync-backend-contract.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const backend = resolve(root, '../../backend/api');
|
||||
const output = resolve(root, 'src/contracts/platform-resources.json');
|
||||
const contract = execFileSync('go', ['run', './cmd/cli', 'resource-contract'], {
|
||||
cwd: backend,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
JSON.parse(contract);
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
writeFileSync(output, `${contract.trim()}\n`);
|
||||
console.log(`已同步后端资源契约:${output}`);
|
||||
@@ -1,113 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile } from './api/platform';
|
||||
|
||||
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'trade' | 'finance' | 'content' | 'track' | 'operation' | 'audit';
|
||||
type CatalogItem = { title: string; description: string; operations: string[] };
|
||||
|
||||
const currentTab = ref<Tab>('dashboard');
|
||||
const loading = ref(false);
|
||||
const loggedIn = ref(platformAPI.hasSession());
|
||||
const errorMessage = ref('');
|
||||
const overview = ref<DashboardOverview>({});
|
||||
const stations = ref<OrgGasStation[]>([]);
|
||||
const deliveryPoints = ref<OrgDeliveryPoint[]>([]);
|
||||
const servicePeople = ref<OrgServicePerson[]>([]);
|
||||
const users = ref<PlatfromAccount[]>([]);
|
||||
const profile = ref<Profile>();
|
||||
const loginForm = reactive({ username: 'root', password: '' });
|
||||
const stationForm = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
|
||||
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: 'dashboard', label: '运营总览' }, { key: 'station', label: '气站管理' }, { key: 'delivery', label: '配送点管理' },
|
||||
{ key: 'person', label: '服务人员' }, { key: 'user', label: '用户管理' },
|
||||
{ key: 'trade', label: '商品交易' }, { key: 'finance', label: '资金结算' }, { key: 'content', label: '内容客服' },
|
||||
{ key: 'track', label: '邀请与轨迹' }, { key: 'operation', label: '全局运营' }, { key: 'audit', label: '审计合规' },
|
||||
];
|
||||
|
||||
const catalog: Partial<Record<Tab, CatalogItem[]>> = {
|
||||
trade: [{ title: '订单中心', description: '统一检索预约、配送、安装维修、安检及售后订单,支持分单、改派、取消和异常升级。', operations: ['订单查询', '异常升级', '退款审核'] }, { title: '商品与价格', description: '维护可燃气体商品、服务项目、区域价目及促销规则。', operations: ['商品上架', '价格审批', '活动配置'] }],
|
||||
finance: [{ title: '结算中心', description: '按气站、配送点、服务人员和订单维度出具结算单并保留审批链路。', operations: ['结算单', '对账差异', '付款审批'] }, { title: '资金风控', description: '识别退款、补贴、佣金和余额异常,重大风险须人工复核。', operations: ['风险规则', '冻结处置', '凭证归档'] }],
|
||||
content: [{ title: '内容运营', description: '管理公告、可燃气体安全知识、消息模板与区域投放策略。', operations: ['内容发布', '模板审核', '投放记录'] }, { title: '客服工单', description: '统一受理用户咨询、投诉、回访和升级闭环。', operations: ['工单分派', '服务质检', '满意度'] }],
|
||||
track: [{ title: '邀请注册二维码', description: '管理气站、配送点邀请二维码的归属、有效期、扫描转化与失效处置。', operations: ['生成二维码', '归属迁移', '转化分析'] }, { title: '订单配送轨迹', description: '按订单回放接单、出库、到达、交付和异常节点,保留不可抵赖审计记录。', operations: ['轨迹查询', '异常告警', '轨迹导出'] }],
|
||||
operation: [{ title: '规则与策略', description: '维护服务范围、配送时段、调度、补贴和风控策略,并按区域灰度发布。', operations: ['规则发布', '灰度控制', '回滚记录'] }, { title: '消息与指标', description: '管理消息触达渠道、运营指标、预警阈值与日报。', operations: ['消息任务', '指标看板', '预警订阅'] }],
|
||||
audit: [{ title: '审计日志', description: '记录组织、人员、用户、设备、订单、结算及权限等关键操作,支持按对象追溯。', operations: ['日志检索', '操作追溯', '导出留档'] }, { title: '合规审批', description: '对高风险安全事件、敏感资料导出、资质过期和重大组织变更实施双人复核。', operations: ['审批队列', '证据附件', '合规报表'] }],
|
||||
};
|
||||
|
||||
const overviewCards = computed(() => [
|
||||
['启用气站', overview.value.gasStationCount ?? 0], ['启用配送点', overview.value.deliveryPointCount ?? 0],
|
||||
['在岗服务人员', overview.value.servicePersonCount ?? 0], ['服务用户', overview.value.userCount ?? 0],
|
||||
]);
|
||||
const activeLabel = computed(() => tabs.find((tab) => tab.key === currentTab.value)?.label ?? '平台总后台');
|
||||
|
||||
async function loadData() {
|
||||
if (!loggedIn.value) return;
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
[profile.value, overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value] = await Promise.all([
|
||||
platformAPI.getProfile(), platformAPI.getDashboard(), platformAPI.listOrgGasStation(), platformAPI.listOrgDeliveryPoint(),
|
||||
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(),
|
||||
]);
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '加载数据失败';
|
||||
if (errorMessage.value.includes('Token') || errorMessage.value.includes('认证')) logout();
|
||||
} finally { loading.value = false; }
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try { await platformAPI.login(loginForm.username, loginForm.password); loggedIn.value = true; loginForm.password = ''; await loadData(); }
|
||||
catch (error) { errorMessage.value = error instanceof Error ? error.message : '登录失败'; }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
|
||||
function logout() { platformAPI.clearSession(); loggedIn.value = false; profile.value = undefined; }
|
||||
|
||||
async function createStation() {
|
||||
if (!stationForm.stationCode || !stationForm.name || !stationForm.principal || !stationForm.serviceArea) { errorMessage.value = '请完整填写气站资料'; return; }
|
||||
loading.value = true;
|
||||
try { await platformAPI.createOrgGasStation({ ...stationForm }); Object.assign(stationForm, { stationCode: '', name: '', principal: '', serviceArea: '' }); await loadData(); }
|
||||
catch (error) { errorMessage.value = error instanceof Error ? error.message : '创建气站失败'; }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="!loggedIn" class="login-page">
|
||||
<form class="login-card" @submit.prevent="login">
|
||||
<span class="brand-mark">阀</span><p class="eyebrow">HEQI PLATFORM</p><h1>可燃气体平台总后台</h1>
|
||||
<p>使用平台管理员账号登录。</p>
|
||||
<input v-model.trim="loginForm.username" autocomplete="username" placeholder="账号" required />
|
||||
<input v-model="loginForm.password" type="password" autocomplete="current-password" placeholder="密码" required />
|
||||
<p v-if="errorMessage" class="error">{{ errorMessage }}</p><button :disabled="loading" type="submit">{{ loading ? '登录中…' : '登录' }}</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<main v-else class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img v-if="profile?.avatar" class="avatar" :src="profile.avatar" :alt="profile.displayName" /><span v-else class="brand-mark">阀</span><div><strong>可燃气体平台</strong><small>平台总后台</small></div></div>
|
||||
<nav><button v-for="tab in tabs" :key="tab.key" :class="{ active: currentTab === tab.key }" @click="currentTab = tab.key">{{ tab.label }}</button></nav>
|
||||
<div class="account"><strong>{{ profile?.displayName || '平台管理员' }}</strong><small>{{ profile?.roleCode || '加载中' }}</small><button class="link-button" @click="logout">退出登录</button></div>
|
||||
</aside>
|
||||
|
||||
<section class="content">
|
||||
<header><div><p class="eyebrow">PLATFORM ADMIN</p><h1>{{ activeLabel }}</h1></div><button class="secondary" :disabled="loading" @click="loadData">{{ loading ? '同步中…' : '刷新数据' }}</button></header>
|
||||
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
|
||||
<template v-if="currentTab === 'dashboard'"><div class="cards"><article v-for="([label, value]) in overviewCards" :key="label"><span>{{ label }}</span><strong>{{ value }}</strong></article></div><article class="panel"><h2>平台职责边界</h2><p>统一管理所有气站、配送点、服务人员和用户,并对可燃气体业务的安全、订单配送轨迹、资金结算、邀请二维码和高风险操作保留审计闭环。</p></article></template>
|
||||
<template v-if="currentTab === 'station'"><article class="panel"><h2>创建气站</h2><form @submit.prevent="createStation"><input v-model.trim="stationForm.stationCode" placeholder="气站编码,例如 GS-1002" /><input v-model.trim="stationForm.name" placeholder="气站名称" /><input v-model.trim="stationForm.principal" placeholder="负责人" /><input v-model.trim="stationForm.serviceArea" placeholder="服务区域" /><button :disabled="loading">提交审核</button></form><p>气站可继续管理其配送点、服务人员、用户及专属邀请注册二维码。</p></article><DataTable :headers="['编码', '名称', '负责人', '服务区域', '状态']" :rows="stations.map((item) => [item.stationCode, item.name, item.principal, item.serviceArea, item.status])" /></template>
|
||||
<template v-if="currentTab === 'delivery'"><article class="panel"><h2>配送点管理边界</h2><p>配送点归属气站,负责管理服务人员、用户、配送班次、订单交付与配送轨迹;平台可跨组织查看、审核、冻结和迁移。</p></article><DataTable :headers="['编码', '配送点', '归属气站', '服务区域', '状态']" :rows="deliveryPoints.map((item) => [item.deliveryCode, item.name, item.gasStationName, item.serviceArea, item.status])" /></template>
|
||||
<template v-if="currentTab === 'person'"><article class="panel"><h2>服务人员生命周期</h2><p>覆盖安装维修、安检、配送角色、资质、登录设备、接单状态、任务绩效及停用留档。</p></article><DataTable :headers="['姓名', '手机号', '角色', '工作状态', '资质状态']" :rows="servicePeople.map((item) => [item.name, item.phoneMasked, item.roles, item.workStatus, item.credentialStatus])" /></template>
|
||||
<template v-if="currentTab === 'user'"><article class="panel"><h2>平台账户管理</h2><p>统一查看平台账户资料、角色、头像、手机号、状态与操作记录;敏感信息按最小化原则展示。</p></article><DataTable :headers="['账号', '名称', '头像', '手机号', '角色', '状态']" :rows="users.map((item) => [item.username, item.displayName, item.avatar || '未设置', item.phoneMasked, item.roleCode, item.status])" /></template>
|
||||
<template v-if="catalog[currentTab]"><div class="catalog"><article v-for="item in catalog[currentTab]" :key="item.title" class="panel"><h2>{{ item.title }}</h2><p>{{ item.description }}</p><div class="tags"><span v-for="operation in item.operations" :key="operation">{{ operation }}</span></div></article></div></template>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
export default defineComponent({ components: { DataTable: defineComponent({ props: { headers: { type: Array as PropType<string[]>, required: true }, rows: { type: Array as PropType<string[][]>, required: true } }, template: `<article class="panel table-panel"><table><thead><tr><th v-for="header in headers" :key="header">{{ header }}</th></tr></thead><tbody><tr v-for="(row, index) in rows" :key="index"><td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td></tr><tr v-if="rows.length === 0"><td :colspan="headers.length">暂无数据</td></tr></tbody></table></article>` }) } });
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { resourceApi } from './resource';
|
||||
|
||||
/** 配送点管理接口。 */
|
||||
export type DeliveryBasic = { identity: string; delivery_code: string; name: string; principal: string; address: string; status: string };
|
||||
export const deliveryApi = { list: () => resourceApi.list<DeliveryBasic>('/delivery/delivery_basic'), create: (data: Record<string, unknown>) => resourceApi.create<DeliveryBasic>('/delivery/delivery_basic', data) };
|
||||
@@ -1,5 +0,0 @@
|
||||
import { resourceApi } from './resource';
|
||||
|
||||
/** 可燃气体站管理接口。 */
|
||||
export type GasBasic = { identity: string; code: string; name: string; principal: string; address: string; status: string };
|
||||
export const gasApi = { list: () => resourceApi.list<GasBasic>('/gas/gas_basic'), create: (data: Record<string, unknown>) => resourceApi.create<GasBasic>('/gas/gas_basic', data) };
|
||||
@@ -7,17 +7,17 @@ export type PlatformMenu = { identity: string; parent_identity?: string; menu_co
|
||||
|
||||
export const platformApi = {
|
||||
overview: () => request<Record<string, number>>('/dashboard/overview'),
|
||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform/platfrom_account'),
|
||||
listRole: () => resourceApi.list<PlatformRole>('/platform/platform_role'),
|
||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform/platform_role', data),
|
||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform/platform_menu'),
|
||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platfrom_account'),
|
||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform_menu'),
|
||||
listRoleMenuIdentities: (identity: string) =>
|
||||
request<{ menu_identities: string[] }>(
|
||||
`/platform/platform_role/${identity}/menu`,
|
||||
`/platform_role/${identity}/menu`,
|
||||
),
|
||||
replaceRoleMenus: (identity: string, menuIdentities: string[]) =>
|
||||
request<{ updated: boolean }>(
|
||||
`/platform/platform_role/${identity}/menu`,
|
||||
`/platform_role/${identity}/menu`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ menu_identities: menuIdentities }),
|
||||
|
||||
@@ -23,7 +23,7 @@ export function buildResourcePayload(
|
||||
for (const field of fields) {
|
||||
if (mode === 'edit' && field.type === 'password') continue;
|
||||
const value = form[field.key];
|
||||
if (field.type === 'menu-identities' && Array.isArray(value)) {
|
||||
if (field.type === 'identity-list' && Array.isArray(value)) {
|
||||
payload[field.key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ export const resourceApi = {
|
||||
update: <T>(resource: string, identity: string, data: Record<string, unknown>) => request<T>(`${resource}/${identity}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
updateStatus: (resource: string, identity: string, status: string) => request<{ updated: boolean }>(`${resource}/${identity}/status`, { method: 'PATCH', body: JSON.stringify({ status }) }),
|
||||
archive: (resource: string, identity: string) => request<{ updated: boolean }>(`${resource}/${identity}`, { method: 'DELETE' }),
|
||||
action: <T>(resource: string, method: 'POST' | 'PUT' | 'PATCH', data: Record<string, unknown>) =>
|
||||
request<T>(resource, { method, body: JSON.stringify(data) }),
|
||||
};
|
||||
|
||||
@@ -1,54 +1,61 @@
|
||||
export type ResourceMode = 'writable' | 'readonly' | 'append_only';
|
||||
export type ResourceMode = 'writable' | 'readonly' | 'append_only' | 'managed';
|
||||
export type ResourcePageKind = 'list' | 'tree';
|
||||
export type ResourceFieldType =
|
||||
| 'text'
|
||||
| 'password'
|
||||
| 'identity'
|
||||
| 'role-code'
|
||||
| 'menu-identities'
|
||||
| 'identity-list'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'textarea';
|
||||
| 'textarea'
|
||||
| 'select';
|
||||
|
||||
export type ResourceField = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: ResourceFieldType;
|
||||
type?: ResourceFieldType;
|
||||
required?: boolean;
|
||||
relation?: string;
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
};
|
||||
|
||||
export type DetailAction = {
|
||||
name: string;
|
||||
resource: string;
|
||||
fields: ResourceField[];
|
||||
payload?: Record<string, unknown>;
|
||||
method?: 'POST' | 'PUT' | 'PATCH';
|
||||
danger?: boolean;
|
||||
fields?: ResourceField[];
|
||||
};
|
||||
|
||||
export type ResourceUiDefinition = {
|
||||
key: string;
|
||||
name: string;
|
||||
resource: string;
|
||||
resource: `/${string}`;
|
||||
title: string;
|
||||
mode: ResourceMode;
|
||||
pageKind: ResourcePageKind;
|
||||
fields: ResourceField[];
|
||||
requiredIdentities: string[];
|
||||
detailActions?: DetailAction[];
|
||||
};
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
const fieldLabels: Record<string, string> = {
|
||||
code: '编码',
|
||||
name: '名称',
|
||||
credit_code: '统一信用代码',
|
||||
principal: '负责人',
|
||||
address: '地址',
|
||||
longitude: '经度',
|
||||
latitude: '纬度',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
display_name: '显示名称',
|
||||
role_code: '角色编码',
|
||||
delivery_code: '配送编码',
|
||||
platform_role_code: '平台角色',
|
||||
credit_code: '统一社会信用代码',
|
||||
principal: '负责人',
|
||||
manager: '库房负责人',
|
||||
phone: '联系电话',
|
||||
address: '地址',
|
||||
longitude: '经度',
|
||||
latitude: '纬度',
|
||||
delivery_code: '配送站编码',
|
||||
avatar: '头像',
|
||||
work_status: '工作状态',
|
||||
credential_type: '资质类型',
|
||||
@@ -56,483 +63,265 @@ const labels: Record<string, string> = {
|
||||
expired_at: '到期时间',
|
||||
real_name: '实名姓名',
|
||||
is_default: '默认地址',
|
||||
device_no: '设备编号',
|
||||
model: '设备型号',
|
||||
online_status: '在线状态',
|
||||
effective_at: '生效时间',
|
||||
reported_at: '上报时间',
|
||||
payload: '遥测数据',
|
||||
version_no: '版本号',
|
||||
params: '产品参数',
|
||||
produced_at: '生产时间',
|
||||
enabled_at: '启用时间',
|
||||
is_enabled: '是否启用',
|
||||
repair_no: '检修单号',
|
||||
repair_type: '检修类型',
|
||||
started_at: '开始时间',
|
||||
completed_at: '完成时间',
|
||||
result: '检修结果',
|
||||
target_status: '目标状态',
|
||||
content: '内容',
|
||||
operator: '操作人员',
|
||||
remark: '备注',
|
||||
action: '动作',
|
||||
occurred_at: '发生时间',
|
||||
reason: '原因',
|
||||
operator_identity: '操作人标识',
|
||||
operator_name: '操作人',
|
||||
owner_type: '归属类型',
|
||||
owner_identity: '归属标识',
|
||||
alipay_id: '支付宝账号',
|
||||
alipay_name: '支付宝账户名',
|
||||
wxpay_id: '微信账号',
|
||||
wxpay_name: '微信账户名',
|
||||
balance: '余额(分)',
|
||||
withdrawal_balance: '可提现余额(分)',
|
||||
card_no_last4: '银行卡末四位',
|
||||
bank_name: '银行名称',
|
||||
card_owner: '持卡人',
|
||||
payment_no: '支付单号',
|
||||
order_no: '订单号',
|
||||
trade_no: '第三方流水号',
|
||||
payment_type: '支付业务类型',
|
||||
pay_channel: '支付渠道',
|
||||
pay_type: '支付类型',
|
||||
amount: '金额(分)',
|
||||
fee: '手续费(分)',
|
||||
args: '支付参数',
|
||||
callback_msg: '回调信息',
|
||||
record_no: '流水号',
|
||||
request_no: '请求流水号',
|
||||
direction: '收支方向',
|
||||
trade_type: '交易类型',
|
||||
balance_after: '变动后余额(分)',
|
||||
withdrawal_balance_after: '变动后可提现余额(分)',
|
||||
refund_no: '退款单号',
|
||||
cash_no: '提现单号',
|
||||
channel: '渠道',
|
||||
review_reason: '审核原因',
|
||||
reviewed_at: '审核时间',
|
||||
ymd: '日期',
|
||||
ym: '月份',
|
||||
contract_no: '合同编号',
|
||||
title: '标题',
|
||||
reason: '处置原因',
|
||||
sort_no: '排序号',
|
||||
terms: '合同条款',
|
||||
file_uri: '附件地址',
|
||||
default_delivery_fee: '默认配送费(分)',
|
||||
signed_at: '签署时间',
|
||||
effective_at: '生效时间',
|
||||
unit_price: '单价(分)',
|
||||
unbound_at: '解绑时间',
|
||||
revision_no: '修订号',
|
||||
creator_type: '创建方类型',
|
||||
creator_identity: '创建方标识',
|
||||
contact_name: '联系人',
|
||||
contact_phone: '联系电话',
|
||||
discount_amount: '优惠金额(分)',
|
||||
total_amount: '总金额(分)',
|
||||
delivery_fee: '配送费(分)',
|
||||
assigned_at: '分配时间',
|
||||
from_status: '原状态',
|
||||
to_status: '新状态',
|
||||
track_no: '轨迹编号',
|
||||
attempt_no: '尝试次数',
|
||||
point_type: '轨迹点类型',
|
||||
confirmed_at: '确认时间',
|
||||
sort_no: '排序',
|
||||
product_code: '商品编码',
|
||||
price_amount: '售价',
|
||||
price_amount: '售价(分)',
|
||||
stock_quantity: '库存',
|
||||
value: '属性值',
|
||||
image_uri: '图片地址',
|
||||
is_cover: '封面图',
|
||||
quantity: '数量',
|
||||
selected: '是否选中',
|
||||
order_no: '订单号',
|
||||
total_amount: '订单金额',
|
||||
product_snapshot: '商品快照',
|
||||
sale_amount: '成交金额',
|
||||
sale_amount: '成交金额(分)',
|
||||
score: '评分',
|
||||
channel: '渠道',
|
||||
amount: '金额',
|
||||
paid_at: '支付时间',
|
||||
settlement_no: '结算单号',
|
||||
subject_type: '结算对象类型',
|
||||
subject_type: '结算主体类型',
|
||||
period_start: '结算开始时间',
|
||||
period_end: '结算结束时间',
|
||||
bill_date: '账单日期',
|
||||
difference_amount: '差异金额',
|
||||
difference_amount: '差异金额(分)',
|
||||
content_type: '内容类型',
|
||||
body: '正文',
|
||||
version_no: '版本号',
|
||||
publish_status: '发布状态',
|
||||
template_code: '模板编码',
|
||||
content: '内容',
|
||||
ticket_no: '工单号',
|
||||
category: '分类',
|
||||
priority: '优先级',
|
||||
platform_role_code: '平台角色',
|
||||
menu_identities: '菜单权限',
|
||||
data_scope: '数据范围',
|
||||
parent_identity: '父级',
|
||||
menu_code: '菜单编码',
|
||||
icon: '图标',
|
||||
path: '路径',
|
||||
balance_amount: '余额',
|
||||
frozen_amount: '冻结金额',
|
||||
balance_after: '变动后余额',
|
||||
report_code: '报表编码',
|
||||
report_type: '报表类型',
|
||||
stat_period: '统计周期',
|
||||
generated_at: '生成时间',
|
||||
dimension: '维度',
|
||||
metric_code: '指标编码',
|
||||
metric_value: '指标值',
|
||||
scope_type: '范围类型',
|
||||
stat_at: '统计时间',
|
||||
applicant_identity: '申请人标识',
|
||||
business_type: '业务类型',
|
||||
business_identity: '业务标识',
|
||||
handler_identity: '处理人标识',
|
||||
opinion: '审批意见',
|
||||
purpose: '用途',
|
||||
field_scope: '字段范围',
|
||||
approved_at: '批准时间',
|
||||
file_uri: '文件地址',
|
||||
operator_identity: '操作人标识',
|
||||
object_identity: '对象标识',
|
||||
resource_type: '资源类型',
|
||||
handled_at: '处理时间',
|
||||
created_at: '创建时间',
|
||||
path: '路由',
|
||||
menu_identities: '菜单权限',
|
||||
status: '状态',
|
||||
};
|
||||
const numberFields = new Set([
|
||||
'version_no',
|
||||
'level',
|
||||
'sort_no',
|
||||
'price_amount',
|
||||
'stock_quantity',
|
||||
'quantity',
|
||||
'total_amount',
|
||||
'sale_amount',
|
||||
'score',
|
||||
'amount',
|
||||
'difference_amount',
|
||||
'balance_amount',
|
||||
'frozen_amount',
|
||||
'balance_after',
|
||||
|
||||
const numbers = new Set([
|
||||
'sort_no', 'unit_price', 'amount', 'fee', 'balance', 'withdrawal_balance',
|
||||
'balance_after', 'withdrawal_balance_after', 'default_delivery_fee',
|
||||
'discount_amount', 'total_amount', 'delivery_fee', 'price_amount',
|
||||
'stock_quantity', 'quantity', 'sale_amount', 'score', 'version_no',
|
||||
'difference_amount', 'attempt_no',
|
||||
]);
|
||||
const booleanFields = new Set(['is_default', 'is_cover', 'selected']);
|
||||
const dateFields = new Set(['bill_date']);
|
||||
const datetimeFields = new Set([
|
||||
'expired_at',
|
||||
'effective_at',
|
||||
'reported_at',
|
||||
'occurred_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'paid_at',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'generated_at',
|
||||
'stat_at',
|
||||
'approved_at',
|
||||
'handled_at',
|
||||
'created_at',
|
||||
const booleans = new Set(['is_default', 'is_enabled', 'is_cover', 'selected', 'withdrawable']);
|
||||
const dates = new Set(['bill_date']);
|
||||
const datetimes = new Set([
|
||||
'expired_at', 'produced_at', 'enabled_at', 'started_at', 'completed_at',
|
||||
'occurred_at', 'reviewed_at', 'signed_at', 'effective_at', 'unbound_at',
|
||||
'assigned_at', 'confirmed_at', 'paid_at', 'period_start', 'period_end',
|
||||
]);
|
||||
// 大文本字段按普通文本处理,不再要求或解析 JSON。
|
||||
const textareaFields = new Set([
|
||||
'body',
|
||||
'content',
|
||||
'reason',
|
||||
'opinion',
|
||||
'payload',
|
||||
'product_snapshot',
|
||||
const textareas = new Set([
|
||||
'params', 'content', 'remark', 'reason', 'terms', 'args', 'callback_msg',
|
||||
'review_reason', 'product_snapshot', 'body',
|
||||
]);
|
||||
const fieldType = (key: string): ResourceFieldType => {
|
||||
if (key === 'platform_role_code') return 'role-code';
|
||||
if (key === 'menu_identities') return 'menu-identities';
|
||||
if (key.endsWith('_identity')) return 'identity';
|
||||
if (key === 'password') return 'password';
|
||||
if (numberFields.has(key)) return 'number';
|
||||
if (booleanFields.has(key)) return 'boolean';
|
||||
if (dateFields.has(key)) return 'date';
|
||||
if (datetimeFields.has(key)) return 'datetime';
|
||||
if (textareaFields.has(key)) return 'textarea';
|
||||
return 'text';
|
||||
};
|
||||
const field = (value: string): ResourceField => {
|
||||
const key = value.replace(/!$/, '');
|
||||
return {
|
||||
key,
|
||||
label:
|
||||
labels[key] ?? (key.endsWith('_identity') ? '关联业务标识' : '业务字段'),
|
||||
type: fieldType(key),
|
||||
required: value.endsWith('!') || undefined,
|
||||
};
|
||||
};
|
||||
const titles: Record<string, string> = {
|
||||
gas_basic: '气站管理',
|
||||
gas_account: '气站账户',
|
||||
delivery_basic: '配送点管理',
|
||||
delivery_account: '配送账户',
|
||||
delivery_task: '配送任务',
|
||||
delivery_track: '配送轨迹',
|
||||
delivery_track_point: '轨迹点',
|
||||
staff_account: '服务人员',
|
||||
staff_credential: '人员资质',
|
||||
user_account: '用户账户',
|
||||
user_address: '用户地址',
|
||||
user_service_relation: '用户服务关系',
|
||||
dev_smart_cylinder_valve: '智能钢瓶阀',
|
||||
dev_device_binding: '设备绑定',
|
||||
dev_telemetry: '设备遥测',
|
||||
ec_category: '商品分类',
|
||||
ec_product: '商品管理',
|
||||
ec_product_attribute: '商品属性',
|
||||
ec_product_image: '商品图片',
|
||||
ec_cart: '购物车',
|
||||
ec_order: '订单管理',
|
||||
ec_order_item: '订单明细',
|
||||
ec_review: '商品评价',
|
||||
fin_payment: '支付记录',
|
||||
fin_settlement: '财务结算',
|
||||
fin_reconciliation: '财务对账',
|
||||
cms_content: '内容管理',
|
||||
cs_ticket: '客服工单',
|
||||
platfrom_account: '平台账户',
|
||||
platform_role: '平台角色',
|
||||
platform_menu: '平台菜单',
|
||||
wallet: '钱包',
|
||||
wallet_ledger: '钱包流水',
|
||||
wallet_recharge: '钱包充值',
|
||||
wallet_withdrawal: '钱包提现',
|
||||
};
|
||||
const define = (
|
||||
|
||||
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
|
||||
const type: ResourceFieldType = key.endsWith('_identity')
|
||||
? 'identity'
|
||||
: numbers.has(key)
|
||||
? 'number'
|
||||
: booleans.has(key)
|
||||
? 'boolean'
|
||||
: dates.has(key)
|
||||
? 'date'
|
||||
: datetimes.has(key)
|
||||
? 'datetime'
|
||||
: textareas.has(key)
|
||||
? 'textarea'
|
||||
: key === 'password'
|
||||
? 'password'
|
||||
: 'text';
|
||||
return { key, label: fieldLabels[key] ?? key, type, ...options };
|
||||
}
|
||||
|
||||
function relation(key: string, resource: string, required = false): ResourceField {
|
||||
return f(key, { type: 'identity', relation: resource, required });
|
||||
}
|
||||
|
||||
function define(
|
||||
name: string,
|
||||
resource: string,
|
||||
title: string,
|
||||
mode: ResourceMode,
|
||||
pageKind: ResourcePageKind,
|
||||
keys: string[],
|
||||
fields: ResourceField[],
|
||||
pageKind: ResourcePageKind = 'list',
|
||||
detailActions?: DetailAction[],
|
||||
): ResourceUiDefinition => {
|
||||
const fields = keys.map(field);
|
||||
): ResourceUiDefinition {
|
||||
return {
|
||||
key: name.replace(/_/g, '-'),
|
||||
name,
|
||||
resource,
|
||||
title: titles[name] ?? '业务资源',
|
||||
resource: `/${name}`,
|
||||
title,
|
||||
mode,
|
||||
pageKind,
|
||||
fields,
|
||||
requiredIdentities: fields
|
||||
.filter((item) => item.required && item.key.endsWith('_identity'))
|
||||
.map((item) => item.key),
|
||||
...(detailActions ? { detailActions } : {}),
|
||||
};
|
||||
};
|
||||
const action = (
|
||||
name: string,
|
||||
resource: string,
|
||||
keys: string[],
|
||||
payload?: Record<string, unknown>,
|
||||
): DetailAction => ({
|
||||
name,
|
||||
resource,
|
||||
fields: keys.map(field),
|
||||
...(payload ? { payload } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const reason = [f('reason', { required: true })];
|
||||
|
||||
/** Exact UI contract for every backend ExpectedResources entry. */
|
||||
export const resources: ResourceUiDefinition[] = [
|
||||
define('gas_basic', '/gas/gas_basic', 'writable', 'list', [
|
||||
'code!',
|
||||
'name!',
|
||||
'credit_code',
|
||||
'principal',
|
||||
'address',
|
||||
'longitude',
|
||||
'latitude',
|
||||
define('gas_basic', '气站', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]),
|
||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||
define('delivery_basic', '配送站', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]),
|
||||
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status')]),
|
||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||
define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]),
|
||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
|
||||
|
||||
define('product_type', '产品类型', 'writable', [f('code', { required: true }), f('name', { required: true })]),
|
||||
define('product_warehouse', '库房', 'writable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone'), f('is_enabled')]),
|
||||
define('product_info', '产品信息', 'managed', [f('code', { required: true }), f('name', { required: true }), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true }), f('is_enabled')], 'list', [
|
||||
{ name: '修改启用状态', resource: '/product_info/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }] })] },
|
||||
]),
|
||||
define('gas_account', '/gas/gas_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'role_code',
|
||||
'gas_basic_identity!',
|
||||
define('product_repair', '产品检修记录', 'writable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result'), f('target_status'), f('content'), f('operator'), f('remark')]),
|
||||
define('product_owner', '产品归属记录', 'append_only', [relation('product_info_identity', '/product_info', true), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true }), f('occurred_at', { required: true }), f('reason'), f('remark')]),
|
||||
|
||||
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason },
|
||||
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason] },
|
||||
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason },
|
||||
]),
|
||||
define('delivery_basic', '/delivery/delivery_basic', 'writable', 'list', [
|
||||
'delivery_code!',
|
||||
'name!',
|
||||
'gas_basic_identity',
|
||||
'principal',
|
||||
'address',
|
||||
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [
|
||||
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
||||
]),
|
||||
define('delivery_account', '/delivery/delivery_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'role_code',
|
||||
'delivery_basic_identity!',
|
||||
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
|
||||
define('gasorder_basic', '气体配送订单', 'managed', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason] },
|
||||
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason },
|
||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason },
|
||||
{ name: '标记异常', resource: '/gasorder_basic/:identity/exception', fields: reason },
|
||||
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason },
|
||||
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason },
|
||||
]),
|
||||
define('delivery_task', '/delivery/delivery_task', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'delivery_basic_identity!',
|
||||
'staff_account_identity',
|
||||
define('gasorder_item', '订单明细', 'readonly', []),
|
||||
define('gasorder_assign', '分配记录', 'readonly', []),
|
||||
define('gasorder_status', '状态记录', 'readonly', []),
|
||||
define('gasorder_track', '运行轨迹', 'readonly', []),
|
||||
define('gasorder_track_point', '轨迹点', 'readonly', []),
|
||||
define('gasorder_confirm', '确认记录', 'readonly', []),
|
||||
define('gasorder_payment', '支付记录', 'readonly', []),
|
||||
|
||||
define('ec_category', '商品分类', 'writable', [relation('parent_identity', '/ec_category'), f('name', { required: true }), f('sort_no')], 'tree'),
|
||||
define('ec_product', '商品', 'writable', [relation('ec_category_identity', '/ec_category', true), f('product_code', { required: true }), f('name', { required: true }), f('price_amount', { required: true }), f('stock_quantity')]),
|
||||
define('ec_product_attribute', '商品属性', 'writable', [relation('ec_product_identity', '/ec_product', true), f('name', { required: true }), f('value', { required: true }), f('sort_no')]),
|
||||
define('ec_product_image', '商品图片', 'writable', [relation('ec_product_identity', '/ec_product', true), f('image_uri', { required: true }), f('sort_no'), f('is_cover')]),
|
||||
define('ec_cart', '购物车', 'writable', [relation('user_account_identity', '/user_account', true), relation('ec_product_identity', '/ec_product', true), f('quantity', { required: true }), f('selected')]),
|
||||
define('ec_order', '商城订单', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('order_no', { required: true }), f('total_amount', { required: true })]),
|
||||
define('ec_order_item', '商城订单明细', 'writable', [relation('ec_order_identity', '/ec_order', true), relation('ec_product_identity', '/ec_product', true), f('product_snapshot', { required: true }), f('quantity', { required: true }), f('sale_amount', { required: true })]),
|
||||
define('ec_review', '商品评价', 'writable', [relation('ec_order_identity', '/ec_order', true), relation('ec_product_identity', '/ec_product', true), relation('user_account_identity', '/user_account', true), f('score', { required: true }), f('content', { required: true })]),
|
||||
|
||||
define('wallet_basic', '钱包', 'managed', [f('owner_type'), f('owner_identity'), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [
|
||||
{ name: '后台充值', resource: '/wallet_basic/:identity/recharge', fields: [f('request_no', { required: true }), f('amount', { required: true }), f('withdrawable'), ...reason, f('remark')] },
|
||||
{ name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }, { label: '冻结', value: 'frozen' }] })] },
|
||||
]),
|
||||
define('delivery_track', '/delivery/delivery_track', 'writable', 'list', [
|
||||
'delivery_task_identity!',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
define('wallet_bank', '银行卡', 'readonly', []),
|
||||
define('wallet_payment', '钱包支付记录', 'readonly', []),
|
||||
define('wallet_record', '钱包流水', 'readonly', []),
|
||||
define('wallet_refund', '退款记录', 'readonly', []),
|
||||
define('wallet_apply_cash', '提现申请', 'managed', [], 'list', [
|
||||
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason },
|
||||
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason },
|
||||
]),
|
||||
define(
|
||||
'delivery_track_point',
|
||||
'/delivery/delivery_track_point',
|
||||
'readonly',
|
||||
'list',
|
||||
[
|
||||
'delivery_track_identity',
|
||||
'point_type',
|
||||
'occurred_at',
|
||||
'longitude',
|
||||
'latitude',
|
||||
],
|
||||
),
|
||||
define('staff_account', '/staff/account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'name!',
|
||||
'phone',
|
||||
'avatar',
|
||||
'role_code',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'work_status',
|
||||
]),
|
||||
define('staff_credential', '/staff/credential', 'writable', 'list', [
|
||||
'staff_account_identity!',
|
||||
'credential_type!',
|
||||
'credential_no',
|
||||
'expired_at',
|
||||
]),
|
||||
define('user_account', '/user/account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'name!',
|
||||
'phone',
|
||||
'avatar',
|
||||
'real_name',
|
||||
]),
|
||||
define('user_address', '/user/address', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'address!',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'is_default',
|
||||
]),
|
||||
define(
|
||||
'user_service_relation',
|
||||
'/user/service_relation',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'user_account_identity!',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'staff_account_identity',
|
||||
],
|
||||
),
|
||||
define(
|
||||
'dev_smart_cylinder_valve',
|
||||
'/device/dev_smart_cylinder_valve',
|
||||
'writable',
|
||||
'list',
|
||||
['device_no!', 'model', 'online_status', 'owner_identity'],
|
||||
),
|
||||
define(
|
||||
'dev_device_binding',
|
||||
'/device/dev_device_binding',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'smart_cylinder_valve_identity!',
|
||||
'user_account_identity!',
|
||||
'effective_at',
|
||||
'expired_at',
|
||||
],
|
||||
),
|
||||
define('dev_telemetry', '/device/dev_telemetry', 'readonly', 'list', [
|
||||
'smart_cylinder_valve_identity',
|
||||
'reported_at',
|
||||
'payload',
|
||||
]),
|
||||
define('ec_category', '/ec/ec_category', 'writable', 'tree', [
|
||||
'parent_identity',
|
||||
'name!',
|
||||
'sort_no',
|
||||
]),
|
||||
define('ec_product', '/ec/ec_product', 'writable', 'list', [
|
||||
'ec_category_identity!',
|
||||
'product_code!',
|
||||
'name!',
|
||||
'price_amount!',
|
||||
'stock_quantity',
|
||||
]),
|
||||
define(
|
||||
'ec_product_attribute',
|
||||
'/ec/ec_product_attribute',
|
||||
'writable',
|
||||
'list',
|
||||
['ec_product_identity!', 'name!', 'value!', 'sort_no'],
|
||||
),
|
||||
define('ec_product_image', '/ec/ec_product_image', 'writable', 'list', [
|
||||
'ec_product_identity!',
|
||||
'image_uri!',
|
||||
'sort_no',
|
||||
'is_cover',
|
||||
]),
|
||||
define('ec_cart', '/ec/ec_cart', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'ec_product_identity!',
|
||||
'quantity!',
|
||||
'selected',
|
||||
]),
|
||||
define('ec_order', '/ec/ec_order', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'gas_basic_identity',
|
||||
'delivery_basic_identity',
|
||||
'order_no!',
|
||||
'total_amount!',
|
||||
]),
|
||||
define('ec_order_item', '/ec/ec_order_item', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'ec_product_identity!',
|
||||
'product_snapshot!',
|
||||
'quantity!',
|
||||
'sale_amount!',
|
||||
]),
|
||||
define('ec_review', '/ec/ec_review', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'ec_product_identity!',
|
||||
'user_account_identity!',
|
||||
'score!',
|
||||
'content!',
|
||||
]),
|
||||
define('fin_payment', '/finance/fin_payment', 'writable', 'list', [
|
||||
'ec_order_identity!',
|
||||
'channel!',
|
||||
'amount!',
|
||||
'paid_at',
|
||||
]),
|
||||
define('fin_settlement', '/finance/fin_settlement', 'writable', 'list', [
|
||||
'settlement_no!',
|
||||
'subject_type!',
|
||||
'subject_identity!',
|
||||
'period_start!',
|
||||
'period_end!',
|
||||
]),
|
||||
define(
|
||||
'fin_reconciliation',
|
||||
'/finance/fin_reconciliation',
|
||||
'writable',
|
||||
'list',
|
||||
['channel!', 'bill_date!', 'difference_amount!'],
|
||||
),
|
||||
define('cms_content', '/content/cms_content', 'writable', 'list', [
|
||||
'content_type!',
|
||||
'title!',
|
||||
'body!',
|
||||
'version_no',
|
||||
'publish_status',
|
||||
]),
|
||||
define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'ticket_no!',
|
||||
'category!',
|
||||
'priority!',
|
||||
]),
|
||||
define('platfrom_account', '/platform/platfrom_account', 'writable', 'list', [
|
||||
'username!',
|
||||
'password!',
|
||||
'display_name',
|
||||
'avatar',
|
||||
'platform_role_code!',
|
||||
'phone',
|
||||
]),
|
||||
define(
|
||||
'platform_role',
|
||||
'/platform/platform_role',
|
||||
'writable',
|
||||
'list',
|
||||
['role_code!', 'name!', 'data_scope!'],
|
||||
[
|
||||
action(
|
||||
'分配菜单',
|
||||
'/platform/platform_role/:identity/menu',
|
||||
['menu_identities'],
|
||||
),
|
||||
],
|
||||
),
|
||||
define('platform_menu', '/platform/platform_menu', 'writable', 'tree', [
|
||||
'parent_identity',
|
||||
'menu_code!',
|
||||
'name!',
|
||||
'icon',
|
||||
'path',
|
||||
'sort_no',
|
||||
]),
|
||||
define('wallet', '/wallet/wallet', 'readonly', 'list', [
|
||||
'owner_identity',
|
||||
'balance_amount',
|
||||
'frozen_amount',
|
||||
'status',
|
||||
]),
|
||||
define('wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'balance_after',
|
||||
]),
|
||||
define('wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'status',
|
||||
]),
|
||||
define('wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list', [
|
||||
'wallet_identity',
|
||||
'amount',
|
||||
'status',
|
||||
|
||||
define('fin_payment', '财务支付记录', 'writable', [relation('ec_order_identity', '/ec_order', true), f('channel', { required: true }), f('amount', { required: true }), f('paid_at')]),
|
||||
define('fin_settlement', '财务结算', 'writable', [f('settlement_no', { required: true }), f('subject_type', { required: true }), f('subject_identity', { required: true }), f('period_start', { required: true }), f('period_end', { required: true })]),
|
||||
define('fin_reconciliation', '财务对账', 'writable', [f('channel', { required: true }), f('bill_date', { required: true }), f('difference_amount', { required: true })]),
|
||||
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
||||
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
||||
define('platfrom_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true }), f('phone')]),
|
||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('data_scope', { required: true })], 'list', [
|
||||
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
||||
]),
|
||||
define('platform_menu', '平台菜单', 'writable', [relation('parent_identity', '/platform_menu'), f('menu_code', { required: true }), f('name', { required: true }), f('icon'), f('path'), f('sort_no')], 'tree'),
|
||||
];
|
||||
|
||||
export const resourceByName = Object.fromEntries(
|
||||
export const resourceByPath = Object.fromEntries(
|
||||
resources.map((definition) => [definition.resource, definition]),
|
||||
) as Record<string, ResourceUiDefinition>;
|
||||
|
||||
export function getResource(resourcePath: string): ResourceUiDefinition {
|
||||
const definition = resourceByName[resourcePath];
|
||||
if (!definition) throw new Error('Unknown resource: ' + resourcePath);
|
||||
const definition = resourceByPath[resourcePath];
|
||||
if (!definition) throw new Error(`未知资源:${resourcePath}`);
|
||||
return definition;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { resourceApi } from './resource';
|
||||
|
||||
/** 服务人员管理接口。 */
|
||||
export type Staff = { identity: string; name: string; phone: string; role_code: string; work_status: string; status: string };
|
||||
export const staffApi = { list: () => resourceApi.list<Staff>('/staff/account'), create: (data: Record<string, unknown>) => resourceApi.create<Staff>('/staff/account', data) };
|
||||
@@ -1,5 +0,0 @@
|
||||
import { resourceApi } from './resource';
|
||||
|
||||
/** 业主客户管理接口。 */
|
||||
export type User = { identity: string; name: string; phone: string; real_name: string; status: string };
|
||||
export const userApi = { list: () => resourceApi.list<User>('/user/account'), create: (data: Record<string, unknown>) => resourceApi.create<User>('/user/account', data) };
|
||||
@@ -1,24 +1,9 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<a-config-provider :locale="zhCN">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||
import { computed } from 'vue';
|
||||
import useLocale from '@/hooks/locale';
|
||||
|
||||
const { currentLocale } = useLocale();
|
||||
const locale = computed(() => {
|
||||
switch (currentLocale.value) {
|
||||
case 'zh-CN':
|
||||
return zhCN;
|
||||
case 'en-US':
|
||||
return enUS;
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,6 @@ import globalComponents from '@/components';
|
||||
import '@/assets/style/global.less';
|
||||
import { setupHttp } from '@/plugins/http';
|
||||
import directive from '@/directive';
|
||||
import i18n from '@/locale';
|
||||
import router from '@/router';
|
||||
import store from '@/store';
|
||||
import setupErrorReport from '@/utils/error-report';
|
||||
@@ -22,7 +21,6 @@ async function bootstrap() {
|
||||
app.use(ArcoVueIcon);
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(i18n);
|
||||
app.use(globalComponents);
|
||||
app.use(directive);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<icon-apps />
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||
{{ $t(item) }}
|
||||
{{ item }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">Arco Pro</a-layout-footer>
|
||||
<a-layout-footer class="footer">和气平台总后台</a-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="tsx">
|
||||
import { compile, computed, defineComponent, h, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
import { type RouteRecordRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
@@ -11,7 +10,6 @@ import useMenuTree from './use-menu-tree';
|
||||
export default defineComponent({
|
||||
emit: ['collapse'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -103,7 +101,7 @@ export default defineComponent({
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
title: () => String(element?.meta?.title ?? ''),
|
||||
}}
|
||||
>
|
||||
{travel(element?.children)}
|
||||
@@ -114,7 +112,7 @@ export default defineComponent({
|
||||
v-slots={{ icon }}
|
||||
onClick={() => goto(element)}
|
||||
>
|
||||
{t(element?.meta?.locale || '')}
|
||||
{String(element?.meta?.title ?? '')}
|
||||
</a-menu-item>
|
||||
);
|
||||
nodes.push(node as never);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||
</template>
|
||||
<a-result v-if="!renderList.length" status="404">
|
||||
<template #subtitle> {{ $t('messageBox.noContent') }} </template>
|
||||
<template #subtitle> 暂无消息 </template>
|
||||
</a-result>
|
||||
<List
|
||||
:render-list="renderList"
|
||||
@@ -16,7 +16,7 @@
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
{{ $t('messageBox.tab.button') }}
|
||||
清空
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { MessageListType, MessageRecord } from './types';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import List from './list.vue';
|
||||
@@ -37,7 +36,6 @@ interface TabItem {
|
||||
}
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const messageType = ref('message');
|
||||
const { t } = useI18n();
|
||||
const messageData = reactive<{
|
||||
renderList: MessageRecord[];
|
||||
messageList: MessageRecord[];
|
||||
@@ -49,15 +47,15 @@ toRefs(messageData);
|
||||
const tabList: TabItem[] = [
|
||||
{
|
||||
key: 'message',
|
||||
title: t('messageBox.tab.title.message'),
|
||||
title: '消息',
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
title: t('messageBox.tab.title.notice'),
|
||||
title: '通知',
|
||||
},
|
||||
{
|
||||
key: 'todo',
|
||||
title: t('messageBox.tab.title.todo'),
|
||||
title: '待办',
|
||||
},
|
||||
];
|
||||
async function fetchSourceData() {
|
||||
|
||||
@@ -1,215 +1,49 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<div class="left-side">
|
||||
<a-space>
|
||||
<img
|
||||
alt="logo"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||
/>
|
||||
<a-typography-title
|
||||
:style="{ margin: 0, fontSize: '18px' }"
|
||||
:heading="5"
|
||||
>
|
||||
Arco Pro
|
||||
</a-typography-title>
|
||||
<icon-menu-fold
|
||||
v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="center-side">
|
||||
<Menu v-if="topMenu" class="center-menu" />
|
||||
<div class="navbar-search">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('settings.searchPlaceholder')"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="navbar-search-btn"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
{{ $t('settings.search') }}
|
||||
<a-space class="brand">
|
||||
<img src="@/assets/logo.svg" alt="和气平台" />
|
||||
<a-typography-title :heading="5" :style="{ margin: 0 }">和气平台总后台</a-typography-title>
|
||||
<icon-menu-fold v-if="!topMenu && appStore.device === 'mobile'" class="menu-trigger" @click="toggleDrawerMenu" />
|
||||
</a-space>
|
||||
<a-space>
|
||||
<a-tooltip :content="theme === 'light' ? '切换深色模式' : '切换浅色模式'">
|
||||
<a-button class="nav-btn" type="outline" shape="circle" @click="() => handleToggleTheme()">
|
||||
<icon-moon-fill v-if="theme === 'dark'" />
|
||||
<icon-sun-fill v-else />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="right-side">
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.language')">
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="setDropDownVisible"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-language />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click" @select="changeLocale as any">
|
||||
<div ref="triggerBtn" class="trigger-btn"></div>
|
||||
<template #content>
|
||||
<a-doption
|
||||
v-for="item in locales"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-check v-show="item.value === currentLocale" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="
|
||||
theme === 'light'
|
||||
? $t('settings.navbar.theme.toDark')
|
||||
: $t('settings.navbar.theme.toLight')
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="handleToggleTheme"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-moon-fill v-if="theme === 'dark'" />
|
||||
<icon-sun-fill v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.navbar.alerts')">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge :count="9" dot>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="setPopoverVisible"
|
||||
>
|
||||
<icon-notification />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:arrow-style="{ display: 'none' }"
|
||||
:content-style="{ padding: 0, minWidth: '400px' }"
|
||||
content-class="message-popover"
|
||||
>
|
||||
<div ref="refBtn" class="ref-btn"></div>
|
||||
<template #content>
|
||||
<message-box />
|
||||
</template>
|
||||
</a-popover>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="
|
||||
isFullscreen
|
||||
? $t('settings.navbar.screen.toExit')
|
||||
: $t('settings.navbar.screen.toFull')
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="toggleFullScreen"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||
<icon-fullscreen v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-dropdown trigger="click">
|
||||
<a-avatar
|
||||
:size="32"
|
||||
:style="{ marginRight: '8px', cursor: 'pointer' }"
|
||||
>
|
||||
<img alt="avatar" :src="avatar" />
|
||||
</a-avatar>
|
||||
<template #content>
|
||||
<a-doption>
|
||||
<a-space @click="switchRoles">
|
||||
<icon-tag />
|
||||
<span>
|
||||
{{ $t('messageBox.switchRoles') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Info' })">
|
||||
<icon-user />
|
||||
<span>
|
||||
{{ $t('messageBox.userCenter') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Setting' })">
|
||||
<icon-settings />
|
||||
<span>
|
||||
{{ $t('messageBox.userSettings') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="handleLogout">
|
||||
<icon-export />
|
||||
<span>
|
||||
{{ $t('messageBox.logout') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</a-tooltip>
|
||||
<a-tooltip :content="isFullscreen ? '退出全屏' : '进入全屏'">
|
||||
<a-button class="nav-btn" type="outline" shape="circle" @click="toggleFullScreen">
|
||||
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||
<icon-fullscreen v-else />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click">
|
||||
<a-space class="account">
|
||||
<a-avatar :size="32"><img alt="头像" :src="avatar" /></a-avatar>
|
||||
<span>{{ userStore.name || '平台管理员' }}</span>
|
||||
</a-space>
|
||||
<template #content>
|
||||
<a-doption @click="handleLogout"><icon-export /> 退出登录</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
<script setup lang="ts">
|
||||
import { useDark, useFullscreen, useToggle } from '@vueuse/core';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import Menu from '@/components/menu/index.vue';
|
||||
import { computed, inject } from 'vue';
|
||||
import { resolveAvatarUrl } from '@/constants/avatar';
|
||||
import useLocale from '@/hooks/locale';
|
||||
import useUser from '@/hooks/user';
|
||||
import { LOCALE_OPTIONS } from '@/locale';
|
||||
import { useAppStore, useUserStore } from '@/store';
|
||||
import MessageBox from '../message-box/index.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { logout } = useUser();
|
||||
const { changeLocale, currentLocale } = useLocale();
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const locales = [...LOCALE_OPTIONS];
|
||||
const searchKeyword = ref('');
|
||||
const avatar = computed(() => resolveAvatarUrl(userStore.avatar));
|
||||
const theme = computed(() => {
|
||||
return appStore.theme;
|
||||
});
|
||||
const theme = computed(() => appStore.theme);
|
||||
const topMenu = computed(() => appStore.topMenu && appStore.menu);
|
||||
const isDark = useDark({
|
||||
selector: 'body',
|
||||
@@ -217,152 +51,24 @@ const isDark = useDark({
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
storageKey: 'arco-theme',
|
||||
onChanged(dark: boolean) {
|
||||
// overridden default behavior
|
||||
appStore.toggleTheme(dark);
|
||||
},
|
||||
onChanged: (dark) => appStore.toggleTheme(dark),
|
||||
});
|
||||
const toggleTheme = useToggle(isDark);
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme();
|
||||
};
|
||||
const handleSearch = () => {
|
||||
const keyword = searchKeyword.value.trim();
|
||||
if (!keyword) {
|
||||
return;
|
||||
}
|
||||
Message.info(`${keyword}`);
|
||||
};
|
||||
const refBtn = ref();
|
||||
const triggerBtn = ref();
|
||||
const setPopoverVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
refBtn.value.dispatchEvent(event);
|
||||
};
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
const setDropDownVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
triggerBtn.value.dispatchEvent(event);
|
||||
};
|
||||
const switchRoles = async () => {
|
||||
const res = await userStore.switchRoles();
|
||||
Message.success(res as string);
|
||||
};
|
||||
const handleToggleTheme = useToggle(isDark);
|
||||
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
|
||||
const handleLogout = () => logout();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.center-side {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.center-menu {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 4px 4px 4px 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:focus-within {
|
||||
border-color: rgb(var(--primary-6));
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.12);
|
||||
}
|
||||
|
||||
:deep(.arco-input-outer) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar-search-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
padding-right: 20px;
|
||||
list-style: none;
|
||||
:deep(.locale-select) {
|
||||
border-radius: 20px;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-btn {
|
||||
border-color: rgb(var(--gray-2));
|
||||
color: rgb(var(--gray-8));
|
||||
font-size: 16px;
|
||||
}
|
||||
.trigger-btn,
|
||||
.ref-btn {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
}
|
||||
.trigger-btn {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.message-popover {
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
background: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.brand img { width: 30px; height: 30px; }
|
||||
.menu-trigger, .account { cursor: pointer; }
|
||||
.nav-btn { border-color: rgb(var(--gray-2)); color: rgb(var(--gray-8)); }
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@click="goto(itemData)"
|
||||
>
|
||||
<span class="tag-link">
|
||||
{{ $t(itemData.title) }}
|
||||
{{ itemData.title }}
|
||||
</span>
|
||||
<span
|
||||
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function useLocale() {
|
||||
const i18 = useI18n();
|
||||
const currentLocale = computed(() => {
|
||||
return i18.locale.value;
|
||||
});
|
||||
const changeLocale = (value: string) => {
|
||||
if (i18.locale.value === value) {
|
||||
return;
|
||||
}
|
||||
i18.locale.value = value;
|
||||
localStorage.setItem('arco-locale', value);
|
||||
Message.success(i18.t('navbar.action.locale'));
|
||||
};
|
||||
return {
|
||||
currentLocale,
|
||||
changeLocale,
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './en-US/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/en-US.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/en-US.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.server.dashboard': 'Dashboard-Server',
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
'menu.list': 'List',
|
||||
'menu.result': 'Result',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.form': 'Form',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.visualization': 'Data Visualization',
|
||||
'menu.user': 'User Center',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': 'FAQ',
|
||||
'navbar.docs': 'Docs',
|
||||
'navbar.action.locale': 'Switch to English',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
'settings.search': 'Search',
|
||||
'settings.searchPlaceholder': 'Search menus, pages, features...',
|
||||
'settings.language': 'Language',
|
||||
'settings.navbar.theme.toLight': 'Click to use light mode',
|
||||
'settings.navbar.theme.toDark': 'Click to use dark mode',
|
||||
'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
|
||||
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
|
||||
'settings.navbar.alerts': 'alerts',
|
||||
'http.error.default': 'Error',
|
||||
'http.error.request': 'Request Error',
|
||||
'http.logout.title': 'Confirm logout',
|
||||
'http.logout.content':
|
||||
'You have been logged out, you can cancel to stay on this page, or log in again',
|
||||
'http.logout.okText': 'Re-Login',
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import en from './en-US';
|
||||
import cn from './zh-CN';
|
||||
|
||||
export const LOCALE_OPTIONS = [
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
];
|
||||
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
legacy: false,
|
||||
messages: {
|
||||
'en-US': en,
|
||||
'zh-CN': cn,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -1,10 +0,0 @@
|
||||
type LocaleModule = { default: Record<string, string> };
|
||||
|
||||
export function mergeLocaleModules(
|
||||
modules: Record<string, LocaleModule>,
|
||||
): Record<string, string> {
|
||||
return Object.values(modules).reduce(
|
||||
(messages, module) => ({ ...messages, ...module.default }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './zh-CN/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.platform.dashboard': '运营概览',
|
||||
'menu.platform.gas': '可燃气体站管理',
|
||||
'menu.platform.gas.basic': '可燃气体站',
|
||||
'menu.platform.delivery': '配送管理',
|
||||
'menu.platform.delivery.basic': '配送点',
|
||||
'menu.platform.staff': '服务人员',
|
||||
'menu.platform.staff.list': '人员档案',
|
||||
'menu.platform.user': '业主客户',
|
||||
'menu.platform.user.list': '客户档案',
|
||||
'menu.platform.config': '平台配置',
|
||||
'menu.platform.config.account': '平台账号',
|
||||
'menu.platform.config.role': '角色管理',
|
||||
'menu.platform.config.menu': '菜单管理',
|
||||
'menu.dashboard': '仪表盘',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': '常见问题',
|
||||
'navbar.docs': '文档中心',
|
||||
'navbar.action.locale': '切换为中文',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
'settings.search': '搜索',
|
||||
'settings.searchPlaceholder': '搜索菜单、页面、功能...',
|
||||
'settings.language': '语言',
|
||||
'settings.navbar.theme.toLight': '点击切换为亮色模式',
|
||||
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
|
||||
'settings.navbar.screen.toFull': '点击切换全屏模式',
|
||||
'settings.navbar.screen.toExit': '点击退出全屏模式',
|
||||
'settings.navbar.alerts': '消息通知',
|
||||
'http.error.default': '错误',
|
||||
'http.error.request': '请求错误',
|
||||
'http.logout.title': '确认登出',
|
||||
'http.logout.content':
|
||||
'您已被登出,您可以取消以停留在此页面,或重新登录',
|
||||
'http.logout.okText': '重新登录',
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import './style.css';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import i18n from '@/locale';
|
||||
import { useUserStore } from '@/store';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
@@ -14,10 +13,6 @@ export interface HttpResponse<T = unknown> {
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function t(key: string) {
|
||||
return i18n.global.t(key);
|
||||
}
|
||||
|
||||
export function setupHttp() {
|
||||
if (initialized) {
|
||||
return;
|
||||
@@ -45,7 +40,7 @@ export function setupHttp() {
|
||||
const res = response.data;
|
||||
if (res.code !== 20000) {
|
||||
Message.error({
|
||||
content: res.msg || t('http.error.default'),
|
||||
content: res.msg || '请求失败',
|
||||
duration: 5 * 1000,
|
||||
});
|
||||
if (
|
||||
@@ -53,9 +48,9 @@ export function setupHttp() {
|
||||
response.config.url !== '/api/user/info'
|
||||
) {
|
||||
Modal.error({
|
||||
title: t('http.logout.title'),
|
||||
content: t('http.logout.content'),
|
||||
okText: t('http.logout.okText'),
|
||||
title: '登录已失效',
|
||||
content: '当前登录状态已失效,请重新登录。',
|
||||
okText: '重新登录',
|
||||
async onOk() {
|
||||
const userStore = useUserStore();
|
||||
await userStore.logout();
|
||||
@@ -63,13 +58,13 @@ export function setupHttp() {
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(res.msg || t('http.error.default')));
|
||||
return Promise.reject(new Error(res.msg || '请求失败'));
|
||||
}
|
||||
return res as unknown as AxiosResponse<HttpResponse>;
|
||||
},
|
||||
(error) => {
|
||||
Message.error({
|
||||
content: error.msg || t('http.error.request'),
|
||||
content: error.msg || '网络请求失败',
|
||||
duration: 5 * 1000,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/content', name: 'content', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.content', requiresAuth: true, icon: 'icon-apps', order: 17 },
|
||||
children: [
|
||||
{ path: 'cms-content', name: 'content-cms-content', component: () => import('@/views/content/cms_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cms_content', requiresAuth: true, menuCode: 'content' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/customer_service', name: 'customer_service', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.customer_service', requiresAuth: true, icon: 'icon-apps', order: 19 },
|
||||
children: [
|
||||
{ path: 'cs-ticket', name: 'customer_service-cs-ticket', component: () => import('@/views/customer_service/cs_ticket/ListPage.vue'), meta: { locale: 'menu.platform.customer_service.cs_ticket', requiresAuth: true, menuCode: 'customer_service' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/delivery', name: 'delivery', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.delivery', requiresAuth: true, icon: 'icon-apps', order: 11 },
|
||||
children: [
|
||||
{ path: 'delivery-basic', name: 'delivery-delivery-basic', component: () => import('@/views/delivery/delivery_basic/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_basic', requiresAuth: true, menuCode: 'delivery' } },
|
||||
{ path: 'delivery-account', name: 'delivery-delivery-account', component: () => import('@/views/delivery/delivery_account/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_account', requiresAuth: true, menuCode: 'delivery' } },
|
||||
{ path: 'delivery-task', name: 'delivery-delivery-task', component: () => import('@/views/delivery/delivery_task/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_task', requiresAuth: true, menuCode: 'delivery' } },
|
||||
{ path: 'delivery-track', name: 'delivery-delivery-track', component: () => import('@/views/delivery/delivery_track/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track', requiresAuth: true, menuCode: 'delivery' } },
|
||||
{ path: 'delivery-track-point', name: 'delivery-delivery-track-point', component: () => import('@/views/delivery/delivery_track_point/ListPage.vue'), meta: { locale: 'menu.platform.delivery.delivery_track_point', requiresAuth: true, menuCode: 'delivery' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/device', name: 'device', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.device', requiresAuth: true, icon: 'icon-apps', order: 14 },
|
||||
children: [
|
||||
{ path: 'dev-smart-cylinder-valve', name: 'device-dev-smart-cylinder-valve', component: () => import('@/views/device/dev_smart_cylinder_valve/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_smart_cylinder_valve', requiresAuth: true, menuCode: 'device' } },
|
||||
{ path: 'dev-device-binding', name: 'device-dev-device-binding', component: () => import('@/views/device/dev_device_binding/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_device_binding', requiresAuth: true, menuCode: 'device' } },
|
||||
{ path: 'dev-telemetry', name: 'device-dev-telemetry', component: () => import('@/views/device/dev_telemetry/ListPage.vue'), meta: { locale: 'menu.platform.device.dev_telemetry', requiresAuth: true, menuCode: 'device' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{ path: '/ec', name: 'ec', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.ec', requiresAuth: true, icon: 'icon-apps', order: 16 }, children: [
|
||||
{ path: 'ec-category', name: 'ec-ec-category', component: () => import('@/views/ec/ec_category/TreePage.vue'), meta: { locale: 'menu.platform.ec.ec_category', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-product', name: 'ec-ec-product', component: () => import('@/views/ec/ec_product/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-product-attribute', name: 'ec-ec-product-attribute', component: () => import('@/views/ec/ec_product_attribute/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_attribute', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-product-image', name: 'ec-ec-product-image', component: () => import('@/views/ec/ec_product_image/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_product_image', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-cart', name: 'ec-ec-cart', component: () => import('@/views/ec/ec_cart/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_cart', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-order', name: 'ec-ec-order', component: () => import('@/views/ec/ec_order/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-order-item', name: 'ec-ec-order-item', component: () => import('@/views/ec/ec_order_item/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_order_item', requiresAuth: true, menuCode: 'ec' } },
|
||||
{ path: 'ec-review', name: 'ec-ec-review', component: () => import('@/views/ec/ec_review/ListPage.vue'), meta: { locale: 'menu.platform.ec.ec_review', requiresAuth: true, menuCode: 'ec' } }
|
||||
] }];
|
||||
export default routes;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/finance', name: 'finance', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.finance', requiresAuth: true, icon: 'icon-apps', order: 16 },
|
||||
children: [
|
||||
{ path: 'fin-payment', name: 'finance-fin-payment', component: () => import('@/views/finance/fin_payment/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_payment', requiresAuth: true, menuCode: 'finance' } },
|
||||
{ path: 'fin-settlement', name: 'finance-fin-settlement', component: () => import('@/views/finance/fin_settlement/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_settlement', requiresAuth: true, menuCode: 'finance' } },
|
||||
{ path: 'fin-reconciliation', name: 'finance-fin-reconciliation', component: () => import('@/views/finance/fin_reconciliation/ListPage.vue'), meta: { locale: 'menu.platform.finance.fin_reconciliation', requiresAuth: true, menuCode: 'finance' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/gas', name: 'gas', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.gas', requiresAuth: true, icon: 'icon-apps', order: 10 },
|
||||
children: [
|
||||
{ path: 'gas-basic', name: 'gas-gas-basic', component: () => import('@/views/gas/gas_basic/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_basic', requiresAuth: true, menuCode: 'gas' } },
|
||||
{ path: 'gas-account', name: 'gas-gas-account', component: () => import('@/views/gas/gas_account/ListPage.vue'), meta: { locale: 'menu.platform.gas.gas_account', requiresAuth: true, menuCode: 'gas' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,21 +1,118 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import gasRoutes from './gas';
|
||||
import deliveryRoutes from './delivery';
|
||||
import staffRoutes from './staff';
|
||||
import userRoutes from './user';
|
||||
import deviceRoutes from './device';
|
||||
import ecRoutes from './ec';
|
||||
import financeRoutes from './finance';
|
||||
import contentRoutes from './content';
|
||||
import customerServiceRoutes from './customer_service';
|
||||
import walletRoutes from './wallet';
|
||||
const platformRoutes: AppRouteRecordRaw[] = [
|
||||
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, meta: { locale: 'menu.dashboard', requiresAuth: true, icon: 'icon-dashboard', order: 0 }, children: [{ path: 'overview', name: 'DashboardOverview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { locale: 'menu.platform.dashboard', requiresAuth: true, menuCode: 'dashboard' } }] },
|
||||
{ path: '/platform', name: 'platform', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.config', requiresAuth: true, icon: 'icon-settings', order: 30 }, children: [
|
||||
{ path: 'platfrom-account', name: 'platform-platfrom-account', component: () => import('@/views/platform/platfrom_account/ListPage.vue'), meta: { locale: 'menu.platform.platform.platfrom_account', requiresAuth: true, menuCode: 'platform' } },
|
||||
{ path: 'platform-role', name: 'platform-platform-role', component: () => import('@/views/platform/platform_role/ListPage.vue'), meta: { locale: 'menu.platform.platform.platform_role', requiresAuth: true, menuCode: 'platform' } },
|
||||
{ path: 'platform-menu', name: 'platform-platform-menu', component: () => import('@/views/platform/platform_menu/TreePage.vue'), meta: { locale: 'menu.platform.platform.platform_menu', requiresAuth: true, menuCode: 'platform' } },
|
||||
] },
|
||||
|
||||
const resourcePage = () => import('@/views/shared/ResourcePage.vue');
|
||||
|
||||
function child(
|
||||
domain: string,
|
||||
path: string,
|
||||
name: string,
|
||||
title: string,
|
||||
resource: string,
|
||||
menuCode = domain,
|
||||
): AppRouteRecordRaw {
|
||||
return {
|
||||
path,
|
||||
name: `${domain}-${name}`,
|
||||
component: resourcePage,
|
||||
meta: { title, resource, requiresAuth: true, menuCode },
|
||||
};
|
||||
}
|
||||
|
||||
function group(
|
||||
path: string,
|
||||
name: string,
|
||||
title: string,
|
||||
icon: string,
|
||||
order: number,
|
||||
children: AppRouteRecordRaw[],
|
||||
menuCode = name,
|
||||
): AppRouteRecordRaw {
|
||||
return {
|
||||
path: `/${path}`,
|
||||
name,
|
||||
component: DEFAULT_LAYOUT,
|
||||
redirect: `/${path}/${children[0].path}`,
|
||||
meta: { title, requiresAuth: true, icon, order, menuCode },
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
const routes: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
redirect: '/dashboard/overview',
|
||||
meta: { title: '首页', requiresAuth: true, icon: 'icon-dashboard', order: 0, menuCode: 'dashboard' },
|
||||
children: [
|
||||
{ path: 'overview', name: 'dashboard-overview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { title: '数据概览', requiresAuth: true, menuCode: 'dashboard' } },
|
||||
{ path: 'reports', name: 'dashboard-reports', component: () => import('@/views/dashboard/ReportPage.vue'), meta: { title: '统计报表', requiresAuth: true, menuCode: 'dashboard' } },
|
||||
],
|
||||
},
|
||||
group('gas', 'gas', '气站管理', 'icon-storage', 10, [
|
||||
child('gas', 'gas-basic', 'basic', '气站', '/gas_basic'),
|
||||
child('gas', 'gas-account', 'account', '气站账户', '/gas_account'),
|
||||
]),
|
||||
group('delivery', 'delivery', '配送站管理', 'icon-send', 20, [
|
||||
child('delivery', 'delivery-basic', 'basic', '配送站', '/delivery_basic'),
|
||||
child('delivery', 'delivery-account', 'account', '配送站账户', '/delivery_account'),
|
||||
]),
|
||||
group('staff', 'staff', '工作人员管理', 'icon-user-group', 30, [
|
||||
child('staff', 'staff-account', 'account', '工作人员', '/staff_account'),
|
||||
child('staff', 'staff-credential', 'credential', '人员资质', '/staff_credential'),
|
||||
]),
|
||||
group('user', 'user', '用户管理', 'icon-user', 40, [
|
||||
child('user', 'user-account', 'account', '用户账户', '/user_account'),
|
||||
child('user', 'user-address', 'address', '用户地址', '/user_address'),
|
||||
child('user', 'service-relation', 'service-relation', '服务关系', '/user_service_relation'),
|
||||
]),
|
||||
group('product', 'product', '产品管理', 'icon-common', 50, [
|
||||
child('product', 'product-type', 'type', '产品类型', '/product_type', 'device'),
|
||||
child('product', 'warehouse', 'warehouse', '库房', '/product_warehouse', 'device'),
|
||||
child('product', 'product-info', 'info', '产品信息', '/product_info', 'device'),
|
||||
child('product', 'repair', 'repair', '检修记录', '/product_repair', 'device'),
|
||||
child('product', 'owner', 'owner', '归属记录', '/product_owner', 'device'),
|
||||
], 'device'),
|
||||
group('gasorder', 'gasorder', '气体配送订单管理', 'icon-list', 60, [
|
||||
child('gasorder', 'contracts', 'contracts', '合同管理', '/gasorder_contract', 'delivery'),
|
||||
child('gasorder', 'orders', 'orders', '配送订单', '/gasorder_basic', 'delivery'),
|
||||
child('gasorder', 'tracks', 'tracks', '运行轨迹', '/gasorder_track', 'delivery'),
|
||||
], 'delivery'),
|
||||
group('ec', 'ec', '商城管理', 'icon-shopping-cart', 70, [
|
||||
child('ec', 'categories', 'categories', '商品分类', '/ec_category'),
|
||||
child('ec', 'products', 'products', '商品', '/ec_product'),
|
||||
child('ec', 'attributes', 'attributes', '商品属性', '/ec_product_attribute'),
|
||||
child('ec', 'images', 'images', '商品图片', '/ec_product_image'),
|
||||
child('ec', 'carts', 'carts', '购物车', '/ec_cart'),
|
||||
child('ec', 'orders', 'orders', '商城订单', '/ec_order'),
|
||||
child('ec', 'order-items', 'order-items', '订单明细', '/ec_order_item'),
|
||||
child('ec', 'reviews', 'reviews', '商品评价', '/ec_review'),
|
||||
]),
|
||||
group('wallet', 'wallet', '钱包管理', 'icon-safe', 80, [
|
||||
child('wallet', 'wallet-basic', 'basic', '钱包', '/wallet_basic'),
|
||||
child('wallet', 'banks', 'banks', '银行卡', '/wallet_bank'),
|
||||
child('wallet', 'payments', 'payments', '支付记录', '/wallet_payment'),
|
||||
child('wallet', 'records', 'records', '钱包流水', '/wallet_record'),
|
||||
child('wallet', 'refunds', 'refunds', '退款记录', '/wallet_refund'),
|
||||
child('wallet', 'apply-cash', 'apply-cash', '提现申请', '/wallet_apply_cash'),
|
||||
]),
|
||||
group('finance', 'finance', '财务管理', 'icon-fund', 90, [
|
||||
child('finance', 'payments', 'payments', '支付记录', '/fin_payment'),
|
||||
child('finance', 'settlements', 'settlements', '财务结算', '/fin_settlement'),
|
||||
child('finance', 'reconciliations', 'reconciliations', '财务对账', '/fin_reconciliation'),
|
||||
]),
|
||||
group('content', 'content', '内容管理', 'icon-file', 100, [
|
||||
child('content', 'contents', 'contents', '内容', '/cms_content'),
|
||||
]),
|
||||
group('customer-service', 'customer_service', '客服管理', 'icon-customer-service', 110, [
|
||||
child('customer_service', 'tickets', 'tickets', '客服工单', '/cs_ticket'),
|
||||
]),
|
||||
group('platform', 'platform', '平台管理', 'icon-settings', 120, [
|
||||
child('platform', 'accounts', 'accounts', '平台账户', '/platfrom_account'),
|
||||
child('platform', 'roles', 'roles', '平台角色', '/platform_role'),
|
||||
child('platform', 'menus', 'menus', '平台菜单', '/platform_menu'),
|
||||
]),
|
||||
];
|
||||
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...customerServiceRoutes, ...walletRoutes];
|
||||
|
||||
export default routes;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/staff', name: 'staff', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.staff', requiresAuth: true, icon: 'icon-apps', order: 12 },
|
||||
children: [
|
||||
{ path: 'account', name: 'staff-staff-account', component: () => import('@/views/staff/staff_account/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_account', requiresAuth: true, menuCode: 'staff' } },
|
||||
{ path: 'credential', name: 'staff-staff-credential', component: () => import('@/views/staff/staff_credential/ListPage.vue'), meta: { locale: 'menu.platform.staff.staff_credential', requiresAuth: true, menuCode: 'staff' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/user', name: 'user', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.user', requiresAuth: true, icon: 'icon-apps', order: 13 },
|
||||
children: [
|
||||
{ path: 'account', name: 'user-user-account', component: () => import('@/views/user/user_account/ListPage.vue'), meta: { locale: 'menu.platform.user.user_account', requiresAuth: true, menuCode: 'user' } },
|
||||
{ path: 'address', name: 'user-user-address', component: () => import('@/views/user/user_address/ListPage.vue'), meta: { locale: 'menu.platform.user.user_address', requiresAuth: true, menuCode: 'user' } },
|
||||
{ path: 'service-relation', name: 'user-user-service-relation', component: () => import('@/views/user/user_service_relation/ListPage.vue'), meta: { locale: 'menu.platform.user.user_service_relation', requiresAuth: true, menuCode: 'user' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/wallet', name: 'wallet', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.wallet', requiresAuth: true, icon: 'icon-apps', order: 20 },
|
||||
children: [
|
||||
{ path: 'wallet', name: 'wallet-wallet', component: () => import('@/views/wallet/wallet/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet', requiresAuth: true, menuCode: 'wallet' } },
|
||||
{ path: 'wallet-ledger', name: 'wallet-wallet-ledger', component: () => import('@/views/wallet/wallet_ledger/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_ledger', requiresAuth: true, menuCode: 'wallet' } },
|
||||
{ path: 'wallet-recharge', name: 'wallet-wallet-recharge', component: () => import('@/views/wallet/wallet_recharge/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_recharge', requiresAuth: true, menuCode: 'wallet' } },
|
||||
{ path: 'wallet-withdrawal', name: 'wallet-wallet-withdrawal', component: () => import('@/views/wallet/wallet_withdrawal/ListPage.vue'), meta: { locale: 'menu.platform.wallet.wallet_withdrawal', requiresAuth: true, menuCode: 'wallet' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,22 +0,0 @@
|
||||
:root { color: #18212f; background: #f4f7fb; font-family: Inter, "Microsoft YaHei", sans-serif; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; }
|
||||
button, input { font: inherit; }
|
||||
.login-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at top right, #d8f3e7, transparent 42%), #f4f7fb; }
|
||||
.login-card { width: min(420px, 100%); display: grid; gap: 14px; padding: 38px; background: #fff; border: 1px solid #e1e9f0; border-radius: 16px; box-shadow: 0 18px 50px rgb(15 42 67 / 12%); }
|
||||
.login-card .brand-mark { margin-bottom: 6px; }.login-card h1 { margin: 0; color: #102a43; }.login-card p { color: #536575; line-height: 1.6; margin: 0; }
|
||||
.shell { min-height: 100vh; display: grid; grid-template-columns: 240px 1fr; }
|
||||
.sidebar { background: #102a43; color: #e8f1fb; padding: 28px 16px; display: flex; flex-direction: column; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; padding: 0 10px 30px; }
|
||||
.brand-mark { background: #33a474; display: grid; place-items: center; border-radius: 10px; height: 38px; width: 38px; font-weight: 800; }
|
||||
.avatar { width: 38px; height: 38px; border-radius: 10px; object-fit: cover; background: #e8f1fb; }
|
||||
.brand strong, .brand small { display: block; }.brand small { color: #a8c2dc; margin-top: 3px; }
|
||||
nav { display: grid; gap: 6px; } nav button { border: 0; border-radius: 8px; background: transparent; color: inherit; text-align: left; padding: 11px 14px; cursor: pointer; } nav button:hover, nav button.active { background: #1e4b70; }
|
||||
.mock-note { font-size: 12px; color: #a8c2dc; line-height: 1.6; margin-top: auto; padding: 12px; background: #173b59; border-radius: 8px; }
|
||||
.account { margin-top: auto; display: grid; gap: 5px; padding: 12px; background: #173b59; border-radius: 8px; }.account small { color: #a8c2dc; }.link-button { padding: 4px 0; text-align: left; background: transparent; color: #cde9dd; font-size: 12px; }
|
||||
.content { padding: 34px; max-width: 1500px; width: 100%; }.content header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 25px; }.eyebrow { color: #33a474; font-size: 12px; font-weight: 700; margin: 0 0 6px; }.content h1, .content h2 { margin: 0; }.content h1 { font-size: 28px; }.content h2 { font-size: 17px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(5, minmax(140px, 1fr)); gap: 16px; margin-bottom: 18px; }.cards article, .panel { border: 1px solid #e1e9f0; background: #fff; border-radius: 12px; box-shadow: 0 4px 12px rgb(15 42 67 / 4%); }.cards article { padding: 20px; }.cards span { color: #66788a; font-size: 13px; }.cards strong { display: block; font-size: 28px; margin-top: 12px; color: #0f3859; }.panel { padding: 22px; margin-bottom: 18px; }.panel p { color: #536575; line-height: 1.7; }
|
||||
form { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)) auto; gap: 10px; margin-top: 16px; } input { min-width: 0; border: 1px solid #cbd7e3; border-radius: 7px; padding: 10px 11px; } button { border: 0; border-radius: 7px; padding: 10px 15px; color: #fff; background: #1677c8; cursor: pointer; }button:disabled { opacity: .65; cursor: wait; }.secondary { background: #fff; color: #1677c8; border: 1px solid #a9c9e7; }.table-panel { padding: 0; overflow-x: auto; } table { border-collapse: collapse; width: 100%; min-width: 650px; } th, td { padding: 14px 18px; border-bottom: 1px solid #edf1f5; text-align: left; font-size: 14px; } th { background: #f8fafc; color: #4c6376; font-weight: 600; } td { color: #233446; }.error { background: #fff0f0; color: #b42318; padding: 11px 14px; border-radius: 8px; }
|
||||
.warning { border-color: #ead4a7; background: #fffdf7; }.password-form { grid-template-columns: repeat(3, minmax(140px, 1fr)) auto; }.catalog { display: grid; grid-template-columns: repeat(2, minmax(280px, 1fr)); gap: 18px; }.catalog .panel { margin: 0; }.tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }.tags span { color: #176e4d; background: #e4f6ee; padding: 5px 9px; border-radius: 20px; font-size: 12px; }
|
||||
@media (max-width: 900px) { .shell { grid-template-columns: 1fr; }.sidebar { min-height: auto; }.mock-note { display: none; } nav { grid-template-columns: repeat(3, 1fr); }.content { padding: 20px; }.cards, .catalog { grid-template-columns: repeat(2, 1fr); } form, .password-form { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 560px) { nav, .cards, .catalog { grid-template-columns: 1fr; }.login-card { padding: 28px 22px; } }
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/content/cms_content');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/customer_service/cs_ticket');
|
||||
</script>
|
||||
41
frontend/platform_admin/src/views/dashboard/ReportPage.vue
Normal file
41
frontend/platform_admin/src/views/dashboard/ReportPage.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a-space direction="vertical" fill :size="16">
|
||||
<a-alert>统计报表仅展示后端当前提供的真实汇总数据。</a-alert>
|
||||
<a-card title="平台资源统计" :bordered="false">
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-grid :cols="{ xs: 1, sm: 2, md: 3, lg: 4 }" :col-gap="16" :row-gap="16">
|
||||
<a-grid-item v-for="[key, value] in metrics" :key="key">
|
||||
<a-statistic :title="labels[key] ?? key" :value="value" show-group-separator />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { platformApi } from '@/api/platform';
|
||||
|
||||
const loading = ref(false);
|
||||
const overview = ref<Record<string, number>>({});
|
||||
const labels: Record<string, string> = {
|
||||
gas_basic_count: '气站数量',
|
||||
delivery_basic_count: '配送站数量',
|
||||
staff_account_count: '工作人员数量',
|
||||
user_account_count: '用户数量',
|
||||
};
|
||||
const metrics = computed(() => Object.entries(overview.value));
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
overview.value = await platformApi.overview();
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<template><CrudListPage title="配送点" resource="/delivery/delivery_basic" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'delivery_code', label: '配送点编码', required: true }, { key: 'name', label: '配送点名称', required: true }, { key: 'principal', label: '负责人' }, { key: 'address', label: '地址' }];</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/delivery/delivery_account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/delivery/delivery_basic');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/delivery/delivery_task');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/delivery/delivery_track');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/delivery/delivery_track_point');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/device/dev_device_binding');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/device/dev_smart_cylinder_valve');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/device/dev_telemetry');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_cart');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><TreePage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import TreePage from '@/views/shared/TreePage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_category');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_order');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_order_item');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_product');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_product_attribute');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_product_image');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/ec/ec_review');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/finance/fin_payment');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/finance/fin_reconciliation');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/finance/fin_settlement');
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<template><CrudListPage title="可燃气体站" resource="/gas/gas_basic" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'code', label: '站点编码', required: true }, { key: 'name', label: '站点名称', required: true }, { key: 'principal', label: '负责人' }, { key: 'address', label: '地址' }];</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/gas/gas_account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/gas/gas_basic');
|
||||
</script>
|
||||
@@ -15,28 +15,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import bannerImage from '@/assets/images/login-banner.png';
|
||||
|
||||
const { t } = useI18n();
|
||||
const carouselItem = computed(() => [
|
||||
const carouselItem = [
|
||||
{
|
||||
slogan: t('login.banner.slogan1'),
|
||||
subSlogan: t('login.banner.subSlogan1'),
|
||||
slogan: '统一运营',
|
||||
subSlogan: '覆盖气站、配送站、人员、用户与产品',
|
||||
image: bannerImage,
|
||||
},
|
||||
{
|
||||
slogan: t('login.banner.slogan2'),
|
||||
subSlogan: t('login.banner.subSlogan2'),
|
||||
slogan: '气体配送闭环',
|
||||
subSlogan: '合同、气瓶、订单、轨迹、确认与支付',
|
||||
image: bannerImage,
|
||||
},
|
||||
{
|
||||
slogan: t('login.banner.slogan3'),
|
||||
subSlogan: t('login.banner.subSlogan3'),
|
||||
slogan: '资金安全',
|
||||
subSlogan: '统一钱包、流水、退款与提现审核',
|
||||
image: bannerImage,
|
||||
},
|
||||
]);
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="login-form-wrapper">
|
||||
<div class="login-form-title">{{ $t('login.form.title') }}</div>
|
||||
<div class="login-form-sub-title">{{ $t('login.form.title') }}</div>
|
||||
<div class="login-form-title">登录和气平台</div>
|
||||
<div class="login-form-sub-title">使用平台管理员账户登录</div>
|
||||
<div class="login-form-error-msg">{{ errorMessage }}</div>
|
||||
<a-form
|
||||
ref="loginForm"
|
||||
@@ -12,13 +12,13 @@
|
||||
>
|
||||
<a-form-item
|
||||
field="username"
|
||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||
:rules="[{ required: true, message: '请输入用户名' }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input
|
||||
v-model="userInfo.username"
|
||||
:placeholder="$t('login.form.userName.placeholder')"
|
||||
placeholder="用户名"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
@@ -27,13 +27,13 @@
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input-password
|
||||
v-model="userInfo.password"
|
||||
:placeholder="$t('login.form.password.placeholder')"
|
||||
placeholder="密码"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -48,15 +48,11 @@
|
||||
:model-value="loginConfig.rememberPassword"
|
||||
@change="setRememberPassword as any"
|
||||
>
|
||||
{{ $t('login.form.rememberPassword') }}
|
||||
记住用户名
|
||||
</a-checkbox>
|
||||
<a-link>{{ $t('login.form.forgetPassword') }}</a-link>
|
||||
</div>
|
||||
<a-button type="primary" html-type="submit" long :loading="loading">
|
||||
{{ $t('login.form.login') }}
|
||||
</a-button>
|
||||
<a-button type="text" long class="login-form-register-btn">
|
||||
{{ $t('login.form.register') }}
|
||||
登录
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
@@ -68,14 +64,12 @@ import { Message } from '@arco-design/web-vue';
|
||||
import type { ValidatedError } from '@arco-design/web-vue/es/form/interface';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { LoginData } from '@/api/auth';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const errorMessage = ref('');
|
||||
const { loading, setLoading } = useLoading();
|
||||
const userStore = useUserStore();
|
||||
@@ -83,7 +77,7 @@ const userStore = useUserStore();
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
password: '',
|
||||
});
|
||||
const userInfo = reactive({
|
||||
username: loginConfig.value.username,
|
||||
@@ -104,17 +98,16 @@ const handleSubmit = async ({
|
||||
await userStore.login(values as LoginData);
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||
router.push({
|
||||
name: (redirect as string) || 'DashboardOverview',
|
||||
name: (redirect as string) || 'dashboard-overview',
|
||||
query: {
|
||||
...othersQuery,
|
||||
},
|
||||
});
|
||||
Message.success(t('login.form.login.success'));
|
||||
Message.success('登录成功');
|
||||
const { rememberPassword } = loginConfig.value;
|
||||
const { username, password } = values;
|
||||
// 实际生产环境需要进行加密存储<E5AD98>? // The actual production environment requires encrypted storage.
|
||||
loginConfig.value.username = rememberPassword ? username : '';
|
||||
loginConfig.value.password = rememberPassword ? password : '';
|
||||
loginConfig.value.password = '';
|
||||
} catch (err) {
|
||||
errorMessage.value = (err as Error).message;
|
||||
} finally {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<img
|
||||
alt="logo"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||
/>
|
||||
<div class="logo-text">Arco Design Pro</div>
|
||||
<img alt="和气平台" src="@/assets/logo.svg" />
|
||||
<div class="logo-text">和气平台总后台</div>
|
||||
</div>
|
||||
<LoginBanner />
|
||||
<div class="content">
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<template><a-card title="平台账号" :bordered="false"><a-table :data="list" :loading="loading" :pagination="false" row-key="identity"><template #columns><a-table-column title="用户名" data-index="username" /><a-table-column title="显示名称" data-index="display_name" /><a-table-column title="角色" data-index="platform_role_code" /><a-table-column title="手机号" data-index="phone_masked" /><a-table-column title="状态" data-index="status" /></template></a-table></a-card></template><script setup lang="ts">import { Message } from '@arco-design/web-vue'; import { onMounted, ref } from 'vue'; import { platformApi } from '@/api/platform'; const list = ref<Record<string, unknown>[]>([]); const loading = ref(false); onMounted(async () => { loading.value = true; try { list.value = (await platformApi.listAccount()).list; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } });</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><TreePage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import TreePage from '@/views/shared/TreePage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/platform/platform_menu');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><TreePage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import TreePage from '@/views/shared/TreePage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/platform/platform_menu');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/platform/platform_role');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/platform/platfrom_account');
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<template><CrudListPage title="平台角色" resource="/platform/platform_role" :fields="fields" /></template><script setup lang="ts">import CrudListPage from '@/views/shared/CrudListPage.vue'; const fields = [{ key: 'role_code', label: '角色编码', required: true }, { key: 'name', label: '角色名称', required: true }, { key: 'data_scope', label: '数据范围', required: true }];</script>
|
||||
@@ -12,6 +12,19 @@
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit">查询</a-button>
|
||||
</a-form>
|
||||
<a-form v-if="definition.name === 'wallet_basic'" :model="walletOwner" layout="inline" class="wallet-owner" @submit.prevent="getOwnerWallet">
|
||||
<a-form-item label="归属类型">
|
||||
<a-select v-model="walletOwner.type" style="width: 140px">
|
||||
<a-option value="user">用户</a-option>
|
||||
<a-option value="staff">工作人员</a-option>
|
||||
<a-option value="delivery">配送站</a-option>
|
||||
<a-option value="gas">气站</a-option>
|
||||
<a-option value="platform">平台</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="归属标识"><a-input v-model="walletOwner.identity" placeholder="请输入 identity" /></a-form-item>
|
||||
<a-button type="primary" html-type="submit">获取或创建钱包</a-button>
|
||||
</a-form>
|
||||
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
|
||||
<template #columns>
|
||||
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
|
||||
@@ -39,7 +52,15 @@
|
||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
||||
<a-select v-else-if="field.type === 'role-code'" v-model="form[field.key]">
|
||||
<a-select v-else-if="field.type === 'select'" v-model="form[field.key]" allow-clear>
|
||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="form[field.key]" :multiple="field.type === 'identity-list'" allow-clear allow-search>
|
||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">
|
||||
{{ optionLabel(option) }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.key === 'platform_role_code'" v-model="form[field.key]">
|
||||
<a-option v-for="role in roleOptions" :key="role.role_code" :value="role.role_code">{{ role.name }}</a-option>
|
||||
</a-select>
|
||||
<a-input v-else v-model="form[field.key]" :placeholder="`请输入${field.label}`" />
|
||||
@@ -49,21 +70,28 @@
|
||||
|
||||
<a-drawer :visible="detailVisible" title="详情" :width="540" @cancel="detailVisible = false">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">{{ value ?? '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item v-for="[key, value] in detailEntries" :key="key" :label="key">
|
||||
<pre v-if="typeof value === 'object'" class="json-value">{{ formatValue(value) }}</pre>
|
||||
<template v-else>{{ value ?? '-' }}</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-space class="detail-actions">
|
||||
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" @click="openDetailAction(action)">{{ action.name }}</a-button>
|
||||
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
|
||||
</a-space>
|
||||
</a-drawer>
|
||||
|
||||
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
|
||||
<a-form :model="actionForm" layout="vertical">
|
||||
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
|
||||
<a-form-item v-for="field in activeAction?.fields ?? []" :key="field.key" :label="field.label" :required="field.required">
|
||||
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
|
||||
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
|
||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
|
||||
<a-select v-else-if="field.type === 'menu-identities'" v-model="actionForm[field.key]" multiple allow-search>
|
||||
<a-option v-for="menu in menuOptions" :key="menu.identity" :value="menu.identity">{{ menu.name }}</a-option>
|
||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="actionForm[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||
<a-select v-else-if="field.type === 'select'" v-model="actionForm[field.key]">
|
||||
<a-option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.type === 'identity' || field.type === 'identity-list'" v-model="actionForm[field.key]" :multiple="field.type === 'identity-list'" allow-search>
|
||||
<a-option v-for="option in relationOptions[field.relation ?? ''] ?? []" :key="String(option.identity)" :value="String(option.identity)">{{ optionLabel(option) }}</a-option>
|
||||
</a-select>
|
||||
<a-input v-else v-model="actionForm[field.key]" />
|
||||
</a-form-item>
|
||||
@@ -75,7 +103,7 @@
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
import { platformApi, type PlatformMenu, type PlatformRole } from '@/api/platform';
|
||||
import { platformApi, type PlatformRole } from '@/api/platform';
|
||||
import {
|
||||
buildResourcePayload,
|
||||
isMissingField,
|
||||
@@ -91,6 +119,7 @@ const pageSize = 20;
|
||||
const total = ref(0);
|
||||
const list = ref<Row[]>([]);
|
||||
const filters = reactive({ keyword: '' });
|
||||
const walletOwner = reactive({ type: 'user', identity: '' });
|
||||
const formVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const editingIdentity = ref('');
|
||||
@@ -99,10 +128,10 @@ const detail = ref<Row>({});
|
||||
const actionVisible = ref(false);
|
||||
const activeAction = ref<DetailAction>();
|
||||
const actionForm = reactive<Record<string, any>>({});
|
||||
const menuOptions = ref<PlatformMenu[]>([]);
|
||||
const roleOptions = ref<PlatformRole[]>([]);
|
||||
const canCreate = computed(() => props.definition.mode !== 'readonly');
|
||||
const canEdit = computed(() => props.definition.mode === 'writable');
|
||||
const relationOptions = reactive<Record<string, Row[]>>({});
|
||||
const canCreate = computed(() => ['writable', 'append_only'].includes(props.definition.mode) || ['product_info', 'gasorder_contract', 'gasorder_basic'].includes(props.definition.name));
|
||||
const canEdit = computed(() => props.definition.mode === 'writable' || ['product_info', 'gasorder_contract'].includes(props.definition.name));
|
||||
const canArchive = computed(() => props.definition.mode === 'writable');
|
||||
const formMode = computed<'create' | 'edit'>(() =>
|
||||
editingIdentity.value ? 'edit' : 'create',
|
||||
@@ -120,6 +149,14 @@ const detailEntries = computed(() =>
|
||||
([key]) => key !== 'id' && !key.endsWith('_id'),
|
||||
),
|
||||
);
|
||||
const currentIdentity = computed(() =>
|
||||
String(
|
||||
detail.value.identity ??
|
||||
(detail.value.order as Row | undefined)?.identity ??
|
||||
(detail.value.contract as Row | undefined)?.identity ??
|
||||
'',
|
||||
),
|
||||
);
|
||||
|
||||
function resetForm(data?: Row) {
|
||||
for (const field of props.definition.fields) {
|
||||
@@ -172,15 +209,15 @@ async function openDetail(row: Row) {
|
||||
|
||||
async function openDetailAction(action: DetailAction) {
|
||||
activeAction.value = action;
|
||||
for (const field of action.fields) actionForm[field.key] = undefined;
|
||||
if (action.fields.some((field) => field.type === 'menu-identities')) {
|
||||
for (const field of action.fields ?? []) actionForm[field.key] = undefined;
|
||||
if (action.resource.includes('/menu')) {
|
||||
try {
|
||||
const identity = String(detail.value.identity);
|
||||
const identity = currentIdentity.value;
|
||||
const [menus, assigned] = await Promise.all([
|
||||
platformApi.listMenu(),
|
||||
platformApi.listRoleMenuIdentities(identity),
|
||||
]);
|
||||
menuOptions.value = menus.list;
|
||||
relationOptions['/platform_menu'] = menus.list;
|
||||
actionForm.menu_identities = assigned.menu_identities;
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
@@ -194,7 +231,7 @@ async function submitDetailAction() {
|
||||
const action = activeAction.value;
|
||||
if (!action) return;
|
||||
if (
|
||||
action.fields.some(
|
||||
(action.fields ?? []).some(
|
||||
(field) => field.required && isMissingField(actionForm[field.key]),
|
||||
)
|
||||
) {
|
||||
@@ -203,19 +240,16 @@ async function submitDetailAction() {
|
||||
}
|
||||
try {
|
||||
const payload = {
|
||||
...action.payload,
|
||||
...buildResourcePayload(action.fields, actionForm),
|
||||
...buildResourcePayload(action.fields ?? [], actionForm),
|
||||
};
|
||||
if (action.fields.some((field) => field.type === 'menu-identities')) {
|
||||
if (action.resource.includes('/menu')) {
|
||||
await platformApi.replaceRoleMenus(
|
||||
String(detail.value.identity),
|
||||
currentIdentity.value,
|
||||
payload.menu_identities as string[],
|
||||
);
|
||||
} else {
|
||||
await resourceApi.create(
|
||||
action.resource.replace(':identity', String(detail.value.identity)),
|
||||
payload,
|
||||
);
|
||||
const path = action.resource.replace(':identity', currentIdentity.value);
|
||||
await resourceApi.action(path, action.method ?? 'POST', payload);
|
||||
}
|
||||
Message.success('操作成功');
|
||||
actionVisible.value = false;
|
||||
@@ -281,15 +315,51 @@ async function changePage(next: number) {
|
||||
await load();
|
||||
}
|
||||
|
||||
async function getOwnerWallet() {
|
||||
if (!walletOwner.identity.trim()) {
|
||||
Message.warning('请输入归属标识');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
detail.value = await resourceApi.detail<Row>(
|
||||
'/wallet_basic/owner',
|
||||
`${walletOwner.type}/${walletOwner.identity.trim()}`,
|
||||
);
|
||||
detailVisible.value = true;
|
||||
await load();
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
if (props.definition.fields.some((field) => field.type === 'role-code')) {
|
||||
const actionFields = props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
|
||||
const relationPaths = new Set(
|
||||
[...props.definition.fields, ...actionFields]
|
||||
.map((field) => field.relation)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
);
|
||||
await Promise.all(
|
||||
[...relationPaths].map(async (resource) => {
|
||||
relationOptions[resource] = (await resourceApi.list<Row>(resource, 1, 200)).list;
|
||||
}),
|
||||
);
|
||||
if (props.definition.fields.some((field) => field.key === 'platform_role_code')) {
|
||||
const result = await platformApi.listRole();
|
||||
roleOptions.value = result.list.filter(
|
||||
(role) => !role.is_system && role.status === 'enabled',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function optionLabel(option: Row) {
|
||||
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -304,4 +374,15 @@ onMounted(async () => {
|
||||
.detail-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.wallet-owner {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
.json-value {
|
||||
max-height: 260px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
15
frontend/platform_admin/src/views/shared/ResourcePage.vue
Normal file
15
frontend/platform_admin/src/views/shared/ResourcePage.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<TreePage v-if="definition.pageKind === 'tree'" :definition="definition" />
|
||||
<CrudListPage v-else :definition="definition" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getResource } from '@/api/resources';
|
||||
import CrudListPage from './CrudListPage.vue';
|
||||
import TreePage from './TreePage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const definition = computed(() => getResource(String(route.meta.resource)));
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/staff/account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/staff/account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/staff/credential');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/user/account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/user/account');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/user/address');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/user/service_relation');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/wallet/wallet');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/wallet/wallet_ledger');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/wallet/wallet_recharge');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/wallet/wallet_withdrawal');
|
||||
</script>
|
||||
Reference in New Issue
Block a user