feat: 移除mock并接入真实权限控制
This commit is contained in:
+2
-3
@@ -51,7 +51,6 @@ http://localhost:8848/
|
||||
├── .codex/skills/ # Repository-local skill for keeping README.md in sync
|
||||
├── .husky/ # Git hooks for lint-staged and commitlint
|
||||
├── build/ # Vite plugin, CDN, compression, and build helpers
|
||||
├── mock/ # Template mock data for login and async routes
|
||||
├── public/ # Static assets and runtime platform config
|
||||
├── src/
|
||||
│ ├── api/ # HTTP API wrappers; business APIs live in access.ts
|
||||
@@ -65,7 +64,7 @@ http://localhost:8848/
|
||||
│ ├── store/ # Pinia stores
|
||||
│ ├── style/ # Global styles and themes
|
||||
│ ├── utils/ # Auth, HTTP, storage, message, progress, and tree helpers
|
||||
│ └── views/ # Pages; business pages live in employees, roles, stores
|
||||
│ └── views/ # Pages; business pages live in employees, roles, stores, permissions
|
||||
├── types/ # Global TypeScript declarations
|
||||
├── AGENTS.md # Agent entrypoint that delegates to RTK.md
|
||||
├── RTK.md # Repository collaboration rules
|
||||
@@ -96,7 +95,7 @@ Development mode proxies `/api` to `VITE_API_PROXY_TARGET`, defaulting to `http:
|
||||
|
||||
Login uses `POST /api/auth/admin/login`, then the app validates the token through `GET /api/auth/me` and loads menus/actions through `GET /api/permissions/me`. The backend does not expose refresh-token; local `401` or token expiry clears auth state and redirects to `/login`.
|
||||
|
||||
List search, reset, pagination, status changes, deletes, and save refreshes should go through the API layer. Store and role pages re-fetch their list before local narrowing because those endpoints do not define keyword filters; employee list requests send `page`, `pageSize`, `storeId`, `status`, and `keyword` as needed.
|
||||
List search, reset, pagination, status changes, deletes, and save refreshes should go through the API layer. Store, role, and employee list requests send their current filters and pagination to the backend.
|
||||
|
||||
## Documentation Sync Rule
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ http://localhost:8848/
|
||||
├── .husky/ # Git hooks,提交前执行 lint-staged,提交信息走 commitlint
|
||||
├── .vscode/ # VS Code 推荐配置与 Vue 代码片段
|
||||
├── build/ # Vite 插件、CDN、压缩、构建信息与工具函数
|
||||
├── mock/ # pure-admin 模板保留的 mock 登录/路由数据
|
||||
├── public/ # 静态资源与运行时平台配置
|
||||
├── src/ # 前端源码
|
||||
│ ├── api/ # HTTP API 封装,业务接口集中在 access.ts
|
||||
@@ -65,7 +64,7 @@ http://localhost:8848/
|
||||
│ ├── store/ # Pinia store,含用户、权限、标签页、主题等模块
|
||||
│ ├── style/ # 全局样式、主题、暗色模式、侧边栏和过渡样式
|
||||
│ ├── utils/ # 鉴权、HTTP、缓存、消息、进度条、树处理等工具
|
||||
│ └── views/ # 页面视图,当前业务页在 employees、roles、stores
|
||||
│ └── views/ # 页面视图,当前业务页在 employees、roles、stores、permissions
|
||||
├── types/ # 全局类型声明、组件声明、路由声明和 Vue shim
|
||||
├── AGENTS.md # Codex/Agent 入口规则,转到 RTK.md
|
||||
├── RTK.md # 本仓库协作规则与 README 同步要求
|
||||
@@ -91,7 +90,7 @@ http://localhost:8848/
|
||||
| `pnpm preview:build` | 先构建再预览 | 一步完成打包验收 |
|
||||
| `pnpm typecheck` | 执行 `tsc --noEmit` 和 `vue-tsc --noEmit --skipLibCheck` | 类型检查 |
|
||||
| `pnpm lint` | 依次执行 ESLint、Prettier、Stylelint 自动修复 | 提交前整理代码 |
|
||||
| `pnpm lint:eslint` | 对 `src`、`mock`、`build` 下 JS/TS/Vue 做 ESLint 修复 | 脚本或逻辑代码调整后 |
|
||||
| `pnpm lint:eslint` | 对 `src`、`build` 下 JS/TS/Vue 做 ESLint 修复 | 脚本或逻辑代码调整后 |
|
||||
| `pnpm lint:prettier` | 格式化 `src` 下常见源码与文档格式 | 样式统一 |
|
||||
| `pnpm lint:stylelint` | 修复 HTML/Vue/CSS/SCSS 样式规则 | 样式文件调整后 |
|
||||
| `pnpm svgo` | 递归压缩 SVG | SVG 资源变更后 |
|
||||
@@ -102,9 +101,10 @@ http://localhost:8848/
|
||||
- `src/views/stores/index.vue`: 门店管理,筛选、重置、启停、删除后都会重新调用接口,支持新增和编辑。
|
||||
- `src/views/roles/index.vue`: 角色管理,搜索、重置、删除和保存后都会重新调用接口,支持角色编码校验。
|
||||
- `src/views/employees/index.vue`: 员工管理,门店/状态/关键词筛选、重置、分页、启停、删除和保存后都会重新调用接口。
|
||||
- `src/api/access.ts`: 门店、角色、员工接口类型与 HTTP 方法封装。
|
||||
- `src/api/user.ts`: 登录、当前用户、当前权限菜单接口封装。
|
||||
- `src/router/modules/employees.ts`: 权限管理菜单入口,挂载门店、角色、员工三个页面。
|
||||
- `src/views/permissions/index.vue`: 权限策略,支持查看角色权限、按角色勾选权限点并保存到后端。
|
||||
- `src/api/access.ts`: 门店、角色、员工、权限策略和角色权限分配接口类型与 HTTP 方法封装。
|
||||
- `src/api/user.ts`: 登录、当前用户和当前权限菜单接口封装。
|
||||
- `src/router/modules/employees.ts`: 权限管理菜单入口,挂载门店、角色、员工、权限策略四个页面。
|
||||
- `src/store/modules/user.ts`: 保存 JWT 登录态、当前用户、权限码和后端菜单动作权限。
|
||||
|
||||
## 后端对接
|
||||
@@ -119,12 +119,13 @@ http://localhost:8848/
|
||||
- `POST /api/auth/admin/login`
|
||||
- `GET /api/auth/me`
|
||||
- `GET /api/permissions/me`
|
||||
- `GET /api/stores`,管理列表会携带 `includeInactive`,搜索/重置会重新请求接口后按当前条件收敛结果
|
||||
- `GET /api/permissions/policies`
|
||||
- `GET /api/stores`,门店管理列表会携带 `page`、`pageSize`,筛选时会携带 `status`、`keyword`
|
||||
- `GET /api/stores/:id`
|
||||
- `POST /api/stores`
|
||||
- `PATCH /api/stores/:id`
|
||||
- `DELETE /api/stores/:id`
|
||||
- `GET /api/roles`,搜索/重置会重新请求接口后按关键词收敛结果
|
||||
- `GET /api/roles`,角色管理列表会携带 `page`、`pageSize`,筛选时会携带 `keyword`、`isSystem`
|
||||
- `GET /api/roles/:id`
|
||||
- `POST /api/roles`
|
||||
- `PATCH /api/roles/:id`
|
||||
@@ -136,7 +137,7 @@ http://localhost:8848/
|
||||
- `PATCH /api/employees/:id/status`
|
||||
- `DELETE /api/employees/:id`
|
||||
|
||||
接口响应统一在 `src/api/access.ts` 中使用 `ApiResult<T>` 或 `PaginatedData<T>` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。列表搜索、重置、分页和状态变更后的刷新都应通过接口层完成,不直接依赖页面内存里的旧列表。
|
||||
接口响应统一在 `src/api/access.ts` 中使用 `ApiResult<T>` 或 `PaginatedData<T>` 描述,页面层只消费 `result.data`,避免在视图里重复拼接接口路径。门店、角色、员工列表的搜索、重置、分页和状态变更后的刷新都应通过接口层完成,不直接依赖页面内存里的旧列表。
|
||||
|
||||
## 登录与鉴权流程
|
||||
|
||||
@@ -144,7 +145,7 @@ http://localhost:8848/
|
||||
2. 前端保存后端返回的 `data.token`,并按 `expiresIn` 换算本地过期时间。
|
||||
3. 登录成功后立即调用 `GET /api/auth/me` 校验当前账号,再调用 `GET /api/permissions/me` 获取权限码、菜单和动作权限。
|
||||
4. 路由守卫会在刷新页面后重新拉取当前用户和权限,未登录或 token 过期会跳回 `/login`。
|
||||
5. 菜单按路由 `meta.permission` 与后端权限码过滤,按钮按 `store:manage`、`role:manage`、`employee:manage` 等权限码显隐。
|
||||
5. 菜单按路由 `meta.menuKey`、`meta.permission` 与 `/api/permissions/me` 返回的后端菜单过滤;按钮按后端菜单 `actions` 控制新增、编辑、启停和删除显隐。
|
||||
6. 后端当前没有 refresh-token 接口,收到 `401` 或本地 token 过期时直接清理登录态并要求重新登录。
|
||||
|
||||
## 配置说明
|
||||
|
||||
@@ -11,7 +11,6 @@ import removeNoMatch from "vite-plugin-router-warn";
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
import removeConsole from "vite-plugin-remove-console";
|
||||
import { codeInspectorPlugin } from "code-inspector-plugin";
|
||||
import { vitePluginFakeServer } from "vite-plugin-fake-server";
|
||||
|
||||
export function getPluginsList(
|
||||
VITE_CDN: boolean,
|
||||
@@ -40,13 +39,6 @@ export function getPluginsList(
|
||||
* vite-plugin-router-warn只在开发环境下启用,只处理vue-router文件并且只在服务启动或重启时运行一次,性能消耗可忽略不计
|
||||
*/
|
||||
removeNoMatch(),
|
||||
// mock支持
|
||||
vitePluginFakeServer({
|
||||
logger: false,
|
||||
include: "mock",
|
||||
infixName: false,
|
||||
enableProd: true
|
||||
}),
|
||||
// svg组件化支持
|
||||
svgLoader(),
|
||||
// 自动按需加载图标
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// 模拟后端动态生成路由
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
/**
|
||||
* roles:页面级别权限,这里模拟二种 "admin"、"common"
|
||||
* admin:管理员角色
|
||||
* common:普通角色
|
||||
*/
|
||||
const permissionRouter = {
|
||||
path: "/permission",
|
||||
meta: {
|
||||
title: "权限管理",
|
||||
icon: "ep:lollipop",
|
||||
rank: 10
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/permission/page/index",
|
||||
name: "PermissionPage",
|
||||
meta: {
|
||||
title: "页面权限",
|
||||
roles: ["admin", "common"]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permission/button",
|
||||
meta: {
|
||||
title: "按钮权限",
|
||||
roles: ["admin", "common"]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/permission/button/router",
|
||||
component: "permission/button/index",
|
||||
name: "PermissionButtonRouter",
|
||||
meta: {
|
||||
title: "路由返回按钮权限",
|
||||
auths: [
|
||||
"permission:btn:add",
|
||||
"permission:btn:edit",
|
||||
"permission:btn:delete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permission/button/login",
|
||||
component: "permission/button/perms",
|
||||
name: "PermissionButtonLogin",
|
||||
meta: {
|
||||
title: "登录接口返回按钮权限"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: "/get-async-routes",
|
||||
method: "get",
|
||||
response: () => {
|
||||
return {
|
||||
success: true,
|
||||
data: [permissionRouter]
|
||||
};
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -1,42 +0,0 @@
|
||||
// 根据角色动态生成路由
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: "/login",
|
||||
method: "post",
|
||||
response: ({ body }) => {
|
||||
if (body.username === "admin") {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
avatar: "https://avatars.githubusercontent.com/u/44761321",
|
||||
username: "admin",
|
||||
nickname: "小铭",
|
||||
// 一个用户可能有多个角色
|
||||
roles: ["admin"],
|
||||
// 按钮级别权限
|
||||
permissions: ["*:*:*"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
|
||||
expires: "2030/10/30 00:00:00"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
avatar: "https://avatars.githubusercontent.com/u/52823142",
|
||||
username: "common",
|
||||
nickname: "小林",
|
||||
roles: ["common"],
|
||||
permissions: ["permission:btn:add", "permission:btn:edit"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
|
||||
expires: "2030/10/30 00:00:00"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -1,27 +0,0 @@
|
||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||
|
||||
// 模拟刷新token接口
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: "/refresh-token",
|
||||
method: "post",
|
||||
response: ({ body }) => {
|
||||
if (body.refreshToken) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh",
|
||||
// `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。
|
||||
expires: "2030/10/30 23:59:59"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
+1
-3
@@ -14,7 +14,7 @@
|
||||
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
|
||||
"svgo": "svgo -f . -r",
|
||||
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
|
||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
||||
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||
@@ -79,7 +79,6 @@
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@commitlint/types": "^20.0.0",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@iconify/json": "^2.2.400",
|
||||
"@iconify/vue": "4.2.0",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
@@ -122,7 +121,6 @@
|
||||
"vite": "^7.1.12",
|
||||
"vite-plugin-cdn-import": "^1.0.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-fake-server": "^2.2.0",
|
||||
"vite-plugin-remove-console": "^2.2.0",
|
||||
"vite-plugin-router-warn": "^1.0.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
|
||||
Generated
+2
-339
@@ -93,9 +93,6 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: ^9.38.0
|
||||
version: 9.38.0
|
||||
'@faker-js/faker':
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.400
|
||||
version: 2.2.400
|
||||
@@ -222,9 +219,6 @@ importers:
|
||||
vite-plugin-compression:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(vite@7.1.12(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1))
|
||||
vite-plugin-fake-server:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
vite-plugin-remove-console:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
@@ -497,252 +491,126 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.11':
|
||||
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.11':
|
||||
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.11':
|
||||
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.11':
|
||||
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.11':
|
||||
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.11':
|
||||
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.11':
|
||||
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.11':
|
||||
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.11':
|
||||
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.11':
|
||||
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.11':
|
||||
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.11':
|
||||
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -755,48 +623,24 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.11':
|
||||
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.11':
|
||||
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.11':
|
||||
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -841,10 +685,6 @@ packages:
|
||||
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@faker-js/faker@10.1.0':
|
||||
resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
@@ -966,42 +806,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -1102,67 +936,56 @@ packages:
|
||||
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.52.5':
|
||||
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.52.5':
|
||||
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
|
||||
@@ -1230,28 +1053,24 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
|
||||
resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
|
||||
resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
|
||||
resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
|
||||
resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
|
||||
@@ -1629,9 +1448,6 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
bundle-import@0.0.2:
|
||||
resolution: {integrity: sha512-XB3T6xlgqJHThyr2luo3pNAVhfN/Y2qFEsblrzUO5QZLpJtesget8jmGDImSairScy80ZKBDVcRdFzTzWv3v8A==}
|
||||
|
||||
c12@3.3.1:
|
||||
resolution: {integrity: sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ==}
|
||||
peerDependencies:
|
||||
@@ -1987,11 +1803,6 @@ packages:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.24.2:
|
||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.11:
|
||||
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2213,9 +2024,6 @@ packages:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
|
||||
giget@2.0.0:
|
||||
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
||||
hasBin: true
|
||||
@@ -2223,6 +2031,7 @@ packages:
|
||||
git-raw-commits@4.0.0:
|
||||
resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==}
|
||||
engines: {node: '>=16'}
|
||||
deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead.
|
||||
hasBin: true
|
||||
|
||||
glob-parent@5.1.2:
|
||||
@@ -2236,6 +2045,7 @@ packages:
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
engines: {node: 20 || >=22}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
global-directory@4.0.1:
|
||||
@@ -2334,9 +2144,6 @@ packages:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-from-string@0.0.5:
|
||||
resolution: {integrity: sha512-z59WIHImWhnGVswc0JoyI10Qn4A8xQw7OKrCFRQHvzGZhhEixX13OtXP9ud3Xjpn16CUoYfh5mTu3tnNODiSAw==}
|
||||
|
||||
import-meta-resolve@4.2.0:
|
||||
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
|
||||
|
||||
@@ -2530,28 +2337,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@@ -2850,9 +2653,6 @@ packages:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3192,9 +2992,6 @@ packages:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
responsive-storage@2.2.0:
|
||||
resolution: {integrity: sha512-94W5Chr2F5kDBT6J+OCOeJguEkSTDc3jPOUQXYmzNG64DCNl5p7hoBDF7bx7u6EXAEcpUKF64OZR4b7Nn8h/Gg==}
|
||||
|
||||
@@ -3633,9 +3430,6 @@ packages:
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
|
||||
vite-plugin-fake-server@2.2.0:
|
||||
resolution: {integrity: sha512-RP691997Q207nenNMhg7cvYyBXZyjqXwApTBa+a9kHmILgcAU2w4TMRDiAhIU0HPLAAR5MHlWTEpxA9Tbf+v8g==}
|
||||
|
||||
vite-plugin-remove-console@2.2.0:
|
||||
resolution: {integrity: sha512-qgjh5pz75MdE9Kzs8J0kBwaCfifHV0ezRbB9rpGsIOxam+ilcGV7WOk91vFJXquzRmiKrFh3Hxlh0JJWAmXTbQ==}
|
||||
|
||||
@@ -4178,156 +3972,81 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
@@ -4377,8 +4096,6 @@ snapshots:
|
||||
'@eslint/core': 0.16.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@faker-js/faker@10.1.0': {}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
@@ -5187,11 +4904,6 @@ snapshots:
|
||||
node-releases: 2.0.26
|
||||
update-browserslist-db: 1.1.4(browserslist@4.27.0)
|
||||
|
||||
bundle-import@0.0.2:
|
||||
dependencies:
|
||||
get-tsconfig: 4.13.0
|
||||
import-from-string: 0.0.5
|
||||
|
||||
c12@3.3.1:
|
||||
dependencies:
|
||||
chokidar: 4.0.3
|
||||
@@ -5581,34 +5293,6 @@ snapshots:
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
esbuild@0.24.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.24.2
|
||||
'@esbuild/android-arm': 0.24.2
|
||||
'@esbuild/android-arm64': 0.24.2
|
||||
'@esbuild/android-x64': 0.24.2
|
||||
'@esbuild/darwin-arm64': 0.24.2
|
||||
'@esbuild/darwin-x64': 0.24.2
|
||||
'@esbuild/freebsd-arm64': 0.24.2
|
||||
'@esbuild/freebsd-x64': 0.24.2
|
||||
'@esbuild/linux-arm': 0.24.2
|
||||
'@esbuild/linux-arm64': 0.24.2
|
||||
'@esbuild/linux-ia32': 0.24.2
|
||||
'@esbuild/linux-loong64': 0.24.2
|
||||
'@esbuild/linux-mips64el': 0.24.2
|
||||
'@esbuild/linux-ppc64': 0.24.2
|
||||
'@esbuild/linux-riscv64': 0.24.2
|
||||
'@esbuild/linux-s390x': 0.24.2
|
||||
'@esbuild/linux-x64': 0.24.2
|
||||
'@esbuild/netbsd-arm64': 0.24.2
|
||||
'@esbuild/netbsd-x64': 0.24.2
|
||||
'@esbuild/openbsd-arm64': 0.24.2
|
||||
'@esbuild/openbsd-x64': 0.24.2
|
||||
'@esbuild/sunos-x64': 0.24.2
|
||||
'@esbuild/win32-arm64': 0.24.2
|
||||
'@esbuild/win32-ia32': 0.24.2
|
||||
'@esbuild/win32-x64': 0.24.2
|
||||
|
||||
esbuild@0.25.11:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.11
|
||||
@@ -5867,10 +5551,6 @@ snapshots:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
giget@2.0.0:
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
@@ -5986,11 +5666,6 @@ snapshots:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
import-from-string@0.0.5:
|
||||
dependencies:
|
||||
esbuild: 0.24.2
|
||||
import-meta-resolve: 4.2.0
|
||||
|
||||
import-meta-resolve@4.2.0: {}
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
@@ -6418,8 +6093,6 @@ snapshots:
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -6714,8 +6387,6 @@ snapshots:
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
responsive-storage@2.2.0: {}
|
||||
|
||||
restore-cursor@5.1.0:
|
||||
@@ -7238,14 +6909,6 @@ snapshots:
|
||||
magic-string: 0.25.9
|
||||
vite: 7.1.12(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(yaml@2.8.1)
|
||||
|
||||
vite-plugin-fake-server@2.2.0:
|
||||
dependencies:
|
||||
bundle-import: 0.0.2
|
||||
chokidar: 4.0.3
|
||||
path-to-regexp: 8.3.0
|
||||
picocolors: 1.1.1
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
vite-plugin-remove-console@2.2.0: {}
|
||||
|
||||
vite-plugin-router-warn@1.0.0: {}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"ShowLogo": true,
|
||||
"ShowModel": "smart",
|
||||
"MenuArrowIconNoTransition": false,
|
||||
"CachingAsyncRoutes": false,
|
||||
"TooltipEffect": "light",
|
||||
"ResponsiveStorageNameSpace": "responsive-",
|
||||
"MenuSearchHistory": 6
|
||||
|
||||
+117
-7
@@ -58,17 +58,74 @@ export interface Role extends RoleOption {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 员工列表是服务端分页,筛选条件统一通过查询参数传递。 */
|
||||
export interface EmployeeListParams {
|
||||
storeId?: number;
|
||||
status?: EmployeeStatus;
|
||||
keyword?: string;
|
||||
export interface PermissionPolicyMenu {
|
||||
key: string;
|
||||
title: string;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
export interface PermissionPolicy {
|
||||
roleId: number;
|
||||
roleCode: string;
|
||||
roleName: string;
|
||||
roleDescription: string | null;
|
||||
isSystem: boolean;
|
||||
editable: boolean;
|
||||
scope: string;
|
||||
permissions: string[];
|
||||
menus: PermissionPolicyMenu[];
|
||||
}
|
||||
|
||||
export interface PermissionDefinition {
|
||||
code: string;
|
||||
title: string;
|
||||
description: string;
|
||||
groupKey: string;
|
||||
groupTitle: string;
|
||||
}
|
||||
|
||||
export interface PermissionDefinitionGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
permissions: PermissionDefinition[];
|
||||
}
|
||||
|
||||
export interface PermissionDefinitionMenu {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
permission: string;
|
||||
actions: string[];
|
||||
actionLabels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PermissionDefinitions {
|
||||
permissions: PermissionDefinition[];
|
||||
groups: PermissionDefinitionGroup[];
|
||||
menus: PermissionDefinitionMenu[];
|
||||
}
|
||||
|
||||
/** 列表接口是服务端分页,筛选条件统一通过查询参数传递。 */
|
||||
export interface PageParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface StoreListParams {
|
||||
export interface EmployeeListParams extends PageParams {
|
||||
storeId?: number;
|
||||
status?: EmployeeStatus;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface StoreListParams extends PageParams {
|
||||
includeInactive?: boolean;
|
||||
status?: StoreStatus;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface RoleListParams extends PageParams {
|
||||
keyword?: string;
|
||||
isSystem?: boolean;
|
||||
}
|
||||
|
||||
export interface StorePayload {
|
||||
@@ -111,17 +168,42 @@ export interface PaginatedData<T> {
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export type PermissionPolicyResult = ApiResult<PermissionPolicy[]>;
|
||||
export type PermissionDefinitionsResult = ApiResult<PermissionDefinitions>;
|
||||
|
||||
/** 门店接口:管理门店基础资料,并给员工下拉选项提供数据源。 */
|
||||
export const listStores = (params?: StoreListParams) => {
|
||||
export const listStoreOptions = () => {
|
||||
return http.request<ApiResult<StoreOption[]>>("get", `${API_PREFIX}/stores`);
|
||||
};
|
||||
|
||||
export const listAllStores = (
|
||||
params?: Pick<StoreListParams, "includeInactive">
|
||||
) => {
|
||||
return http.request<ApiResult<Store[]>>("get", `${API_PREFIX}/stores`, {
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const listStores = (params: StoreListParams) => {
|
||||
return http.request<ApiResult<PaginatedData<Store>>>(
|
||||
"get",
|
||||
`${API_PREFIX}/stores`,
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
export const listRoles = () => {
|
||||
return http.request<ApiResult<Role[]>>("get", `${API_PREFIX}/roles`);
|
||||
};
|
||||
|
||||
export const listRolePage = (params: RoleListParams) => {
|
||||
return http.request<ApiResult<PaginatedData<Role>>>(
|
||||
"get",
|
||||
`${API_PREFIX}/roles`,
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
export const getStore = (id: number) => {
|
||||
return http.request<ApiResult<Store>>("get", `${API_PREFIX}/stores/${id}`);
|
||||
};
|
||||
@@ -204,3 +286,31 @@ export const updateEmployeeStatus = (id: number, status: EmployeeStatus) => {
|
||||
export const deleteEmployee = (id: number) => {
|
||||
return http.request<void>("delete", `${API_PREFIX}/employees/${id}`);
|
||||
};
|
||||
|
||||
/** 权限接口:读取权限定义、查看角色策略,并把权限点分配给角色。 */
|
||||
export const getPermissionPolicies = () => {
|
||||
return http.request<PermissionPolicyResult>(
|
||||
"get",
|
||||
`${API_PREFIX}/permissions/policies`
|
||||
);
|
||||
};
|
||||
|
||||
export const getPermissionDefinitions = () => {
|
||||
return http.request<PermissionDefinitionsResult>(
|
||||
"get",
|
||||
`${API_PREFIX}/permissions/definitions`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateRolePermissions = (
|
||||
roleId: number,
|
||||
permissions: string[]
|
||||
) => {
|
||||
return http.request<ApiResult<PermissionPolicy>>(
|
||||
"put",
|
||||
`${API_PREFIX}/permissions/roles/${roleId}`,
|
||||
{
|
||||
data: { permissions }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
type Result = {
|
||||
success: boolean;
|
||||
data: Array<any>;
|
||||
};
|
||||
|
||||
export const getAsyncRoutes = () => {
|
||||
return http.request<Result>("get", "/get-async-routes");
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineComponent, Fragment } from "vue";
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Auth",
|
||||
@@ -12,7 +12,7 @@ export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
return () => {
|
||||
if (!slots) return null;
|
||||
return hasAuth(props.value) ? (
|
||||
return hasPerms(props.value) ? (
|
||||
<Fragment>{slots.default?.()}</Fragment>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) {
|
||||
const { value } = binding;
|
||||
if (value) {
|
||||
!hasAuth(value) && el.parentNode?.removeChild(el);
|
||||
!hasPerms(value) && el.parentNode?.removeChild(el);
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
|
||||
"[Directive: auth]: need permissions! Like v-auth=\"['store:manage']\""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -30,6 +30,14 @@ export const routerArrays: Array<RouteConfigs> = [
|
||||
title: "员工管理",
|
||||
icon: "ep/user-filled"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permissions",
|
||||
name: "PermissionPolicies",
|
||||
meta: {
|
||||
title: "权限策略",
|
||||
icon: "ep/key"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -40,7 +48,6 @@ export type routeMetaType = {
|
||||
savedPosition?: boolean;
|
||||
permission?: string | Array<string>;
|
||||
menuKey?: string;
|
||||
auths?: Array<string>;
|
||||
};
|
||||
|
||||
export type RouteConfigs = {
|
||||
|
||||
+7
-1
@@ -173,7 +173,13 @@ router.beforeEach(async (to: ToRouteType, _from, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasRoutePermission(to.meta, useUserStoreHook().permissions ?? [])) {
|
||||
if (
|
||||
!hasRoutePermission(
|
||||
to.meta,
|
||||
useUserStoreHook().permissions ?? [],
|
||||
useUserStoreHook().permissionMenus ?? []
|
||||
)
|
||||
) {
|
||||
next({ path: "/access-denied" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const Layout = () => import("@/layout/index.vue");
|
||||
/**
|
||||
* 权限管理业务菜单。
|
||||
*
|
||||
* 三个子页面都是静态路由,菜单展示顺序由这里的 children 决定;
|
||||
* 子页面都是静态路由,菜单展示顺序由这里的 children 决定;
|
||||
* 默认访问该模块时进入员工管理,保证和登录/登出后的主工作流一致。
|
||||
*/
|
||||
export default {
|
||||
@@ -49,6 +49,17 @@ export default {
|
||||
permission: ["employee:view:all", "employee:view:store"],
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/permissions",
|
||||
name: "PermissionPolicies",
|
||||
component: () => import("@/views/permissions/index.vue"),
|
||||
meta: {
|
||||
title: "权限策略",
|
||||
menuKey: "permissions",
|
||||
permission: "permission:view",
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies RouteConfigsTable;
|
||||
|
||||
+33
-141
@@ -9,25 +9,16 @@ import { router } from "./index";
|
||||
import { isProxy, toRaw } from "vue";
|
||||
import { useTimeoutFn } from "@vueuse/core";
|
||||
import {
|
||||
isString,
|
||||
cloneDeep,
|
||||
isAllEmpty,
|
||||
intersection,
|
||||
storageLocal,
|
||||
isIncludeAllChildren
|
||||
storageLocal
|
||||
} from "@pureadmin/utils";
|
||||
import { getConfig } from "@/config";
|
||||
import { buildHierarchyTree } from "@/utils/tree";
|
||||
import { userKey, type DataInfo } from "@/utils/auth";
|
||||
import { type menuType, routerArrays } from "@/layout/types";
|
||||
import { userKey, type DataInfo, type PermissionMenuInfo } from "@/utils/auth";
|
||||
import type { menuType } from "@/layout/types";
|
||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
const IFrame = () => import("@/layout/frame.vue");
|
||||
// https://cn.vitejs.dev/guide/features.html#glob-import
|
||||
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
|
||||
|
||||
// 动态路由
|
||||
import { getAsyncRoutes } from "@/api/routes";
|
||||
|
||||
function handRank(routeInfo: any) {
|
||||
const { name, path, parentId, meta } = routeInfo;
|
||||
@@ -81,26 +72,49 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
|
||||
: true;
|
||||
}
|
||||
|
||||
function hasRoutePermission(meta: CustomizeRouteMeta, permissions: string[]) {
|
||||
const required = meta?.permission;
|
||||
function hasWildcardPermission(permissions: string[]) {
|
||||
return permissions.includes("*") || permissions.includes("*:*:*");
|
||||
}
|
||||
|
||||
if (!required) return true;
|
||||
if (permissions.includes("*") || permissions.includes("*:*:*")) return true;
|
||||
function hasMenuAccess(
|
||||
meta: CustomizeRouteMeta,
|
||||
permissions: string[],
|
||||
permissionMenus: PermissionMenuInfo[] = []
|
||||
) {
|
||||
if (!meta?.menuKey) return true;
|
||||
if (hasWildcardPermission(permissions)) return true;
|
||||
|
||||
return permissionMenus.some(menu => menu.key === meta.menuKey);
|
||||
}
|
||||
|
||||
function hasRoutePermission(
|
||||
meta: CustomizeRouteMeta,
|
||||
permissions: string[],
|
||||
permissionMenus: PermissionMenuInfo[] = []
|
||||
) {
|
||||
const required = meta?.permission;
|
||||
const hasMenu = hasMenuAccess(meta, permissions, permissionMenus);
|
||||
|
||||
if (!required) return hasMenu;
|
||||
if (hasWildcardPermission(permissions)) return true;
|
||||
|
||||
const requiredList = Array.isArray(required) ? required : [required];
|
||||
|
||||
return requiredList.some(permission => permissions.includes(permission));
|
||||
return (
|
||||
hasMenu && requiredList.some(permission => permissions.includes(permission))
|
||||
);
|
||||
}
|
||||
|
||||
/** 从localStorage里取出当前登录用户权限码,过滤无权限菜单 */
|
||||
/** 从 localStorage 取出后端菜单和权限码,过滤无权限菜单 */
|
||||
function filterNoPermissionTree(data: RouteComponent[]) {
|
||||
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
|
||||
const currentRoles = userInfo?.roles ?? [];
|
||||
const currentPermissions = userInfo?.permissions ?? [];
|
||||
const currentMenus = userInfo?.permissionMenus ?? [];
|
||||
const newTree = cloneDeep(data).filter((v: any) => {
|
||||
return (
|
||||
isOneOfArray(v.meta?.roles, currentRoles) &&
|
||||
hasRoutePermission(v.meta, currentPermissions)
|
||||
hasRoutePermission(v.meta, currentPermissions, currentMenus)
|
||||
);
|
||||
});
|
||||
newTree.forEach(
|
||||
@@ -169,78 +183,6 @@ function addPathMatch() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理动态路由(后端返回的路由) */
|
||||
function handleAsyncRoutes(routeList) {
|
||||
if (routeList.length === 0) {
|
||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||
} else {
|
||||
formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
|
||||
(v: RouteRecordRaw) => {
|
||||
// 防止重复添加路由
|
||||
if (
|
||||
router.options.routes[0].children.findIndex(
|
||||
value => value.path === v.path
|
||||
) !== -1
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
// 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
|
||||
router.options.routes[0].children.push(v);
|
||||
// 最终路由进行升序
|
||||
ascending(router.options.routes[0].children);
|
||||
if (!router.hasRoute(v?.name)) router.addRoute(v);
|
||||
const flattenRouters: any = router
|
||||
.getRoutes()
|
||||
.find(n => n.path === "/");
|
||||
// 保持router.options.routes[0].children与path为"/"的children一致,防止数据不一致导致异常
|
||||
flattenRouters.children = router.options.routes[0].children;
|
||||
router.addRoute(flattenRouters);
|
||||
}
|
||||
}
|
||||
);
|
||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||
}
|
||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||
useMultiTagsStoreHook().handleTags("equal", [
|
||||
...routerArrays,
|
||||
...usePermissionStoreHook().flatteningRoutes.filter(
|
||||
v => v?.meta?.fixedTag
|
||||
)
|
||||
]);
|
||||
}
|
||||
addPathMatch();
|
||||
}
|
||||
|
||||
/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/
|
||||
function initRouter() {
|
||||
if (getConfig()?.CachingAsyncRoutes) {
|
||||
// 开启动态路由缓存本地localStorage
|
||||
const key = "async-routes";
|
||||
const asyncRouteList = storageLocal().getItem(key) as any;
|
||||
if (asyncRouteList && asyncRouteList?.length > 0) {
|
||||
return new Promise(resolve => {
|
||||
handleAsyncRoutes(asyncRouteList);
|
||||
resolve(router);
|
||||
});
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
getAsyncRoutes().then(({ data }) => {
|
||||
handleAsyncRoutes(cloneDeep(data));
|
||||
storageLocal().setItem(key, data);
|
||||
resolve(router);
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return new Promise(resolve => {
|
||||
getAsyncRoutes().then(({ data }) => {
|
||||
handleAsyncRoutes(cloneDeep(data));
|
||||
resolve(router);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多级嵌套路由处理成一维数组
|
||||
* @param routesList 传入路由
|
||||
@@ -320,35 +262,6 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 过滤后端传来的动态路由 重新生成规范路由 */
|
||||
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
||||
if (!arrRoutes || !arrRoutes.length) return;
|
||||
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
||||
arrRoutes.forEach((v: RouteRecordRaw) => {
|
||||
// 将backstage属性加入meta,标识此路由为后端返回路由
|
||||
v.meta.backstage = true;
|
||||
// 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值
|
||||
if (v?.children && v.children.length && !v.redirect)
|
||||
v.redirect = v.children[0].path;
|
||||
// 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复)
|
||||
if (v?.children && v.children.length && !v.name)
|
||||
v.name = (v.children[0].name as string) + "Parent";
|
||||
if (v.meta?.frameSrc) {
|
||||
v.component = IFrame;
|
||||
} else {
|
||||
// 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致)
|
||||
const index = v?.component
|
||||
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
|
||||
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
|
||||
v.component = modulesRoutes[modulesRoutesKeys[index]];
|
||||
}
|
||||
if (v?.children && v.children.length) {
|
||||
addAsyncRoutes(v.children);
|
||||
}
|
||||
});
|
||||
return arrRoutes;
|
||||
}
|
||||
|
||||
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
|
||||
function getHistoryMode(routerHistory): RouterHistory {
|
||||
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
|
||||
@@ -372,23 +285,6 @@ function getHistoryMode(routerHistory): RouterHistory {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前页面按钮级别的权限 */
|
||||
function getAuths(): Array<string> {
|
||||
return router.currentRoute.value.meta.auths as Array<string>;
|
||||
}
|
||||
|
||||
/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/
|
||||
function hasAuth(value: string | Array<string>): boolean {
|
||||
if (!value) return false;
|
||||
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
|
||||
const metaAuths = getAuths();
|
||||
if (!metaAuths) return false;
|
||||
const isAuths = isString(value)
|
||||
? metaAuths.includes(value)
|
||||
: isIncludeAllChildren(value, metaAuths);
|
||||
return isAuths ? true : false;
|
||||
}
|
||||
|
||||
function handleTopMenu(route) {
|
||||
if (route?.children && route.children.length > 1) {
|
||||
if (route.redirect) {
|
||||
@@ -411,17 +307,13 @@ function getTopMenu(tag = false): menuType {
|
||||
}
|
||||
|
||||
export {
|
||||
hasAuth,
|
||||
getAuths,
|
||||
ascending,
|
||||
filterTree,
|
||||
initRouter,
|
||||
getTopMenu,
|
||||
addPathMatch,
|
||||
isOneOfArray,
|
||||
hasRoutePermission,
|
||||
getHistoryMode,
|
||||
addAsyncRoutes,
|
||||
getParentPaths,
|
||||
findRouteByPath,
|
||||
handleAliveRoute,
|
||||
|
||||
@@ -100,6 +100,15 @@ export const hasPerms = (value: string | Array<string>): boolean => {
|
||||
: isIncludeAllChildren(value, permissions);
|
||||
};
|
||||
|
||||
/** 是否拥有后端返回的菜单入口。 */
|
||||
export const hasMenuAccess = (menuKey: string): boolean => {
|
||||
if (hasPerms("*")) return true;
|
||||
|
||||
const menus = getUserInfo()?.permissionMenus ?? [];
|
||||
|
||||
return menus.some(item => item.key === menuKey);
|
||||
};
|
||||
|
||||
/** 是否拥有后端菜单动作权限,用于比权限码更细的按钮显隐。 */
|
||||
export const hasMenuAction = (menuKey: string, action: string): boolean => {
|
||||
if (hasPerms("*")) return true;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { removeToken, setToken, type DataInfo } from "./auth";
|
||||
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||
|
||||
/**
|
||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/employees?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
||||
* 划重点:
|
||||
* 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
|
||||
* 1.清空本地旧信息;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getEmployee,
|
||||
listEmployees,
|
||||
listRoles,
|
||||
listStores,
|
||||
listStoreOptions,
|
||||
updateEmployee,
|
||||
updateEmployeeStatus,
|
||||
type Employee,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
type RoleOption,
|
||||
type StoreOption
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction, hasPerms } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -107,7 +107,12 @@ const inactiveCount = computed(
|
||||
() => employees.value.filter(item => item.status === "INACTIVE").length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑员工" : "新增员工"));
|
||||
const canManageEmployees = computed(() => hasPerms("employee:manage"));
|
||||
const canCreateEmployee = computed(() => hasMenuAction("employees", "create"));
|
||||
const canUpdateEmployee = computed(() => hasMenuAction("employees", "update"));
|
||||
const canDeleteEmployee = computed(() => hasMenuAction("employees", "delete"));
|
||||
const canOperateEmployee = computed(
|
||||
() => canUpdateEmployee.value || canDeleteEmployee.value
|
||||
);
|
||||
const canViewAllEmployees = computed(() => hasPerms("employee:view:all"));
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
@@ -158,15 +163,17 @@ function buildPayload(): EmployeePayload {
|
||||
/** 门店和角色是员工表单的基础字典,打开弹窗前必须先加载。 */
|
||||
async function fetchCatalog() {
|
||||
const shouldLoadStores =
|
||||
canViewAllEmployees.value || canManageEmployees.value;
|
||||
const shouldLoadRoles = canManageEmployees.value;
|
||||
canViewAllEmployees.value ||
|
||||
canCreateEmployee.value ||
|
||||
canUpdateEmployee.value;
|
||||
const shouldLoadRoles = canCreateEmployee.value || canUpdateEmployee.value;
|
||||
|
||||
if (!shouldLoadStores && !shouldLoadRoles) return;
|
||||
|
||||
catalogLoading.value = true;
|
||||
try {
|
||||
const [storeResult, roleResult] = await Promise.all([
|
||||
shouldLoadStores ? listStores() : Promise.resolve({ data: [] }),
|
||||
shouldLoadStores ? listStoreOptions() : Promise.resolve({ data: [] }),
|
||||
shouldLoadRoles ? listRoles() : Promise.resolve({ data: [] })
|
||||
]);
|
||||
stores.value = storeResult.data;
|
||||
@@ -227,14 +234,14 @@ function handleSizeChange(pageSize: number) {
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canCreateEmployee.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
/** 编辑前重新拉详情,确保角色绑定不是来自列表摘要的过期数据。 */
|
||||
async function openEditDialog(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
try {
|
||||
const result = await getEmployee(row.id);
|
||||
const employee = result.data;
|
||||
@@ -255,7 +262,7 @@ async function openEditDialog(row: Employee) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (form.id ? !canUpdateEmployee.value : !canCreateEmployee.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -280,7 +287,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
const nextStatus: EmployeeStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -306,7 +313,7 @@ async function toggleStatus(row: Employee) {
|
||||
}
|
||||
|
||||
async function removeEmployee(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canDeleteEmployee.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后员工「${row.name}」会被软删除并停用,确认继续?`,
|
||||
@@ -345,7 +352,7 @@ onMounted(async () => {
|
||||
<h1>员工管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageEmployees"
|
||||
v-if="canCreateEmployee"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -470,13 +477,14 @@ onMounted(async () => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageEmployees"
|
||||
v-if="canOperateEmployee"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -485,6 +493,7 @@ onMounted(async () => {
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -493,6 +502,7 @@ onMounted(async () => {
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteEmployee"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -618,9 +628,9 @@ onMounted(async () => {
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -733,9 +743,9 @@ onMounted(async () => {
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
@@ -761,8 +771,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -781,8 +791,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasAuth, getAuths } from "@/router/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonRouter"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ getAuths() }}</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Auth value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasAuth([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
const { permissions } = useUserStoreHook();
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonLogin"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ permissions }}</p>
|
||||
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
|
||||
*:*:* 代表拥有全部按钮级别权限
|
||||
</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Perms value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPerms(['permission:btn:edit'])"
|
||||
plain
|
||||
type="primary"
|
||||
>
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasPerms([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-perms="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-perms="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { initRouter } from "@/router/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { type CSSProperties, ref, computed } from "vue";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPage"
|
||||
});
|
||||
|
||||
const elStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: "85vw",
|
||||
justifyContent: "start"
|
||||
};
|
||||
});
|
||||
|
||||
const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "admin",
|
||||
label: "管理员角色"
|
||||
},
|
||||
{
|
||||
value: "common",
|
||||
label: "普通角色"
|
||||
}
|
||||
];
|
||||
|
||||
function onChange() {
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: username.value, password: "admin123" })
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
storageLocal().removeItem("async-routes");
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">
|
||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
||||
</p>
|
||||
<el-card shadow="never" :style="elStyle">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前角色:{{ username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="username" class="w-[160px]!" @change="onChange">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,533 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
getPermissionDefinitions,
|
||||
getPermissionPolicies,
|
||||
updateRolePermissions,
|
||||
type PermissionDefinitionGroup,
|
||||
type PermissionPolicy
|
||||
} from "@/api/access";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Check from "~icons/ep/check";
|
||||
import Edit from "~icons/ep/edit";
|
||||
import Refresh from "~icons/ep/refresh";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPolicies"
|
||||
});
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const saving = ref(false);
|
||||
const drawerVisible = ref(false);
|
||||
const policies = ref<PermissionPolicy[]>([]);
|
||||
const definitionGroups = ref<PermissionDefinitionGroup[]>([]);
|
||||
const editingPolicy = ref<PermissionPolicy | null>(null);
|
||||
const checkedPermissions = ref<string[]>([]);
|
||||
|
||||
const canUpdatePermissions = computed(() =>
|
||||
hasMenuAction("permissions", "update")
|
||||
);
|
||||
const editableRoleCount = computed(
|
||||
() => policies.value.filter(item => item.editable).length
|
||||
);
|
||||
const menuTotal = computed(() =>
|
||||
policies.value.reduce((total, item) => total + item.menus.length, 0)
|
||||
);
|
||||
const permissionTotal = computed(() =>
|
||||
policies.value.reduce(
|
||||
(total, item) =>
|
||||
total + item.permissions.filter(permission => permission !== "*").length,
|
||||
0
|
||||
)
|
||||
);
|
||||
const drawerTitle = computed(() =>
|
||||
editingPolicy.value ? `分配权限:${editingPolicy.value.roleName}` : "分配权限"
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
error as { response?: { data?: { error?: { message?: string } } } }
|
||||
)?.response?.data?.error?.message;
|
||||
|
||||
if (message) return message;
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function formatActions(actions: string[]) {
|
||||
const actionMap: Record<string, string> = {
|
||||
view: "查看",
|
||||
create: "新增",
|
||||
update: "编辑",
|
||||
delete: "删除"
|
||||
};
|
||||
|
||||
return actions.map(action => actionMap[action] ?? action).join(" / ");
|
||||
}
|
||||
|
||||
function formatPermissionTitle(code: string) {
|
||||
for (const group of definitionGroups.value) {
|
||||
const found = group.permissions.find(permission => permission.code === code);
|
||||
if (found) return found.title;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function isGroupChecked(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
return codes.every(code => checkedPermissions.value.includes(code));
|
||||
}
|
||||
|
||||
function isGroupIndeterminate(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
const checkedCount = codes.filter(code =>
|
||||
checkedPermissions.value.includes(code)
|
||||
).length;
|
||||
|
||||
return checkedCount > 0 && checkedCount < codes.length;
|
||||
}
|
||||
|
||||
function toggleGroup(group: PermissionDefinitionGroup, checked: boolean) {
|
||||
const nextPermissions = new Set(checkedPermissions.value);
|
||||
|
||||
for (const permission of group.permissions) {
|
||||
if (checked) {
|
||||
nextPermissions.add(permission.code);
|
||||
} else {
|
||||
nextPermissions.delete(permission.code);
|
||||
}
|
||||
}
|
||||
|
||||
checkedPermissions.value = [...nextPermissions];
|
||||
}
|
||||
|
||||
function openEditor(policy: PermissionPolicy) {
|
||||
if (!policy.editable || !canUpdatePermissions.value) return;
|
||||
|
||||
editingPolicy.value = policy;
|
||||
checkedPermissions.value = policy.permissions.filter(
|
||||
permission => permission !== "*"
|
||||
);
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
async function fetchPermissionData() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const [policyResult, definitionResult] = await Promise.all([
|
||||
getPermissionPolicies(),
|
||||
getPermissionDefinitions()
|
||||
]);
|
||||
|
||||
policies.value = policyResult.data;
|
||||
definitionGroups.value = definitionResult.data.groups;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载权限数据失败"));
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePermissions() {
|
||||
if (!editingPolicy.value) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateRolePermissions(
|
||||
editingPolicy.value.roleId,
|
||||
checkedPermissions.value
|
||||
);
|
||||
await fetchPermissionData();
|
||||
await useUserStoreHook().loadAuthContext(true);
|
||||
drawerVisible.value = false;
|
||||
ElMessage.success("权限已更新");
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "保存权限失败"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchPermissionData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="permission-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">动态权限分配</p>
|
||||
<h1>权限策略</h1>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="fetchPermissionData">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>角色策略</span>
|
||||
<strong>{{ policies.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>可分配角色</span>
|
||||
<strong>{{ editableRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>权限码</span>
|
||||
<strong>{{ permissionTotal }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>菜单授权</span>
|
||||
<strong>{{ menuTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-shell">
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="policies"
|
||||
row-key="roleCode"
|
||||
stripe
|
||||
class="permission-table"
|
||||
>
|
||||
<el-table-column label="角色" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="role-cell">
|
||||
<strong>{{ row.roleName }}</strong>
|
||||
<span>{{ row.roleCode }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scope" label="作用范围" min-width="140" />
|
||||
<el-table-column label="权限码" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="tag-list">
|
||||
<el-tag
|
||||
v-for="permission in row.permissions"
|
||||
:key="permission"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ permission === "*" ? "全部权限" : formatPermissionTitle(permission) }}
|
||||
</el-tag>
|
||||
<span v-if="row.permissions.length === 0" class="muted-text">
|
||||
未分配
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="菜单与动作" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div class="menu-list">
|
||||
<div v-for="menu in row.menus" :key="menu.key" class="menu-item">
|
||||
<strong>{{ menu.title }}</strong>
|
||||
<span>{{ formatActions(menu.actions) }}</span>
|
||||
</div>
|
||||
<span v-if="row.menus.length === 0" class="muted-text">
|
||||
无后台菜单
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="132" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
:content="row.editable ? '分配权限' : '系统最高权限不可编辑'"
|
||||
placement="top"
|
||||
>
|
||||
<span>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
:disabled="!row.editable || !canUpdatePermissions"
|
||||
text
|
||||
type="primary"
|
||||
@click="openEditor(row)"
|
||||
>
|
||||
分配
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
size="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<section
|
||||
v-for="group in definitionGroups"
|
||||
:key="group.key"
|
||||
class="permission-group"
|
||||
>
|
||||
<div class="group-heading">
|
||||
<strong>{{ group.title }}</strong>
|
||||
<el-checkbox
|
||||
:model-value="isGroupChecked(group)"
|
||||
:indeterminate="isGroupIndeterminate(group)"
|
||||
@change="value => toggleGroup(group, Boolean(value))"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="permission-options">
|
||||
<el-checkbox-group v-model="checkedPermissions">
|
||||
<el-checkbox
|
||||
v-for="permission in group.permissions"
|
||||
:key="permission.code"
|
||||
:label="permission.code"
|
||||
border
|
||||
>
|
||||
<span class="permission-option">
|
||||
<strong>{{ permission.title }}</strong>
|
||||
<small>{{ permission.code }}</small>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button
|
||||
:icon="Check"
|
||||
:loading="saving"
|
||||
type="primary"
|
||||
@click="savePermissions"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-page {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-shell {
|
||||
padding: 8px 0 14px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.permission-group:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.permission-group:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.group-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-options {
|
||||
:deep(.el-checkbox-group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox.is-bordered) {
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
small {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.permission-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.permission-options :deep(.el-checkbox-group) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+106
-36
@@ -9,12 +9,12 @@ import {
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
listRoles,
|
||||
listRolePage,
|
||||
updateRole,
|
||||
type Role,
|
||||
type RolePayload
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -40,7 +40,16 @@ const formRef = ref<FormInstance>();
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
isSystem: undefined as boolean | undefined,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 角色管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<RoleFormState>({
|
||||
@@ -72,26 +81,15 @@ const describedCount = computed(
|
||||
() => roles.value.filter(item => Boolean(item.description)).length
|
||||
);
|
||||
const systemRoleCount = computed(
|
||||
() => roles.value.filter(item => item.code === "admin").length
|
||||
() => roles.value.filter(item => item.isSystem).length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
|
||||
const canManageRoles = computed(() => hasPerms("role:manage"));
|
||||
|
||||
function applyRoleQuery(items: Role[]) {
|
||||
const keyword = query.keyword.trim().toLowerCase();
|
||||
|
||||
if (keyword.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter(role => {
|
||||
return (
|
||||
role.code.toLowerCase().includes(keyword) ||
|
||||
role.name.toLowerCase().includes(keyword) ||
|
||||
(role.description ?? "").toLowerCase().includes(keyword)
|
||||
const canCreateRole = computed(() => hasMenuAction("roles", "create"));
|
||||
const canUpdateRole = computed(() => hasMenuAction("roles", "update"));
|
||||
const canDeleteRole = computed(() => hasMenuAction("roles", "delete"));
|
||||
const canOperateRole = computed(
|
||||
() => canUpdateRole.value || canDeleteRole.value
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -134,12 +132,19 @@ function buildPayload(): RolePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 角色查询必须走接口,避免搜索条件只在前端过滤当前缓存。 */
|
||||
/** 角色筛选、分页必须走接口,避免查询条件只在前端过滤当前缓存。 */
|
||||
async function fetchRoles() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listRoles();
|
||||
roles.value = applyRoleQuery(result.data);
|
||||
const result = await listRolePage({
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
isSystem: query.isSystem,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
roles.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
|
||||
} finally {
|
||||
@@ -149,22 +154,37 @@ async function fetchRoles() {
|
||||
|
||||
function handleReset() {
|
||||
query.keyword = "";
|
||||
query.isSystem = undefined;
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (!canCreateRole.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canUpdateRole.value || row.isSystem) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -176,7 +196,7 @@ function openEditDialog(row: Role) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (form.id ? !canUpdateRole.value : !canCreateRole.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -201,7 +221,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function removeRole(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canDeleteRole.value || row.isSystem) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后角色「${row.name}」无法再绑定员工,确认继续?`,
|
||||
@@ -214,6 +234,10 @@ async function removeRole(row: Role) {
|
||||
);
|
||||
await deleteRole(row.id);
|
||||
ElMessage.success("角色已删除");
|
||||
|
||||
if (roles.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -233,7 +257,7 @@ onMounted(fetchRoles);
|
||||
<h1>角色管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageRoles"
|
||||
v-if="canCreateRole"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -245,14 +269,14 @@ onMounted(fetchRoles);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总角色</span>
|
||||
<strong>{{ roles.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>已配置说明</span>
|
||||
<span>当前页有说明</span>
|
||||
<strong>{{ describedCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>系统角色</span>
|
||||
<span>当前页系统角色</span>
|
||||
<strong>{{ systemRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -262,6 +286,16 @@ onMounted(fetchRoles);
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="query.isSystem"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
class="toolbar-control"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="系统内置" :value="true" />
|
||||
<el-option label="自定义角色" :value="false" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
@@ -305,14 +339,14 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageRoles"
|
||||
v-if="canOperateRole"
|
||||
label="操作"
|
||||
width="170"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canUpdateRole && !row.isSystem"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -321,7 +355,7 @@ onMounted(fetchRoles);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canDeleteRole && !row.isSystem"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -333,6 +367,22 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -391,9 +441,9 @@ onMounted(fetchRoles);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -459,6 +509,10 @@ onMounted(fetchRoles);
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.toolbar-control {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -474,6 +528,16 @@ onMounted(fetchRoles);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -510,8 +574,8 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -519,6 +583,7 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.keyword-input,
|
||||
.toolbar-control,
|
||||
.toolbar-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
@@ -528,6 +593,11 @@ onMounted(fetchRoles);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
+88
-34
@@ -15,7 +15,7 @@ import {
|
||||
type StorePayload,
|
||||
type StoreStatus
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -42,7 +42,15 @@ const stores = ref<Store[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
status: undefined as StoreStatus | undefined,
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 门店管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<StoreFormState>({
|
||||
@@ -71,23 +79,12 @@ const inactiveCount = computed(
|
||||
() => stores.value.filter(item => item.status === "INACTIVE").length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑门店" : "新增门店"));
|
||||
const canManageStores = computed(() => hasPerms("store:manage"));
|
||||
|
||||
function applyStoreQuery(items: Store[]) {
|
||||
const keyword = query.keyword.trim().toLowerCase();
|
||||
|
||||
return items.filter(store => {
|
||||
const matchedStatus =
|
||||
query.status === undefined || store.status === query.status;
|
||||
const matchedKeyword =
|
||||
keyword.length === 0 ||
|
||||
store.name.toLowerCase().includes(keyword) ||
|
||||
(store.address ?? "").toLowerCase().includes(keyword) ||
|
||||
(store.phone ?? "").toLowerCase().includes(keyword);
|
||||
|
||||
return matchedStatus && matchedKeyword;
|
||||
});
|
||||
}
|
||||
const canCreateStore = computed(() => hasMenuAction("stores", "create"));
|
||||
const canUpdateStore = computed(() => hasMenuAction("stores", "update"));
|
||||
const canDeleteStore = computed(() => hasMenuAction("stores", "delete"));
|
||||
const canOperateStore = computed(
|
||||
() => canUpdateStore.value || canDeleteStore.value
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -133,14 +130,19 @@ function buildPayload(): StorePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 门店筛选必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
/** 门店筛选、分页必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
async function fetchStores() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listStores({
|
||||
includeInactive: true
|
||||
status: query.status,
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
stores.value = applyStoreQuery(result.data);
|
||||
stores.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
|
||||
} finally {
|
||||
@@ -151,22 +153,36 @@ async function fetchStores() {
|
||||
function handleReset() {
|
||||
query.status = undefined;
|
||||
query.keyword = "";
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canCreateStore.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -179,7 +195,7 @@ function openEditDialog(row: Store) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageStores.value) return;
|
||||
if (form.id ? !canUpdateStore.value : !canCreateStore.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -204,7 +220,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
const nextStatus: StoreStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -230,7 +246,7 @@ async function toggleStatus(row: Store) {
|
||||
}
|
||||
|
||||
async function removeStore(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canDeleteStore.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后门店「${row.name}」不会再出现在员工新增下拉里,确认继续?`,
|
||||
@@ -243,6 +259,10 @@ async function removeStore(row: Store) {
|
||||
);
|
||||
await deleteStore(row.id);
|
||||
ElMessage.success("门店已删除");
|
||||
|
||||
if (stores.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchStores();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -262,7 +282,7 @@ onMounted(fetchStores);
|
||||
<h1>门店管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageStores"
|
||||
v-if="canCreateStore"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -274,14 +294,14 @@ onMounted(fetchStores);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总门店</span>
|
||||
<strong>{{ stores.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>启用门店</span>
|
||||
<span>当前页启用</span>
|
||||
<strong>{{ activeCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>停用门店</span>
|
||||
<span>当前页停用</span>
|
||||
<strong>{{ inactiveCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -356,13 +376,14 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageStores"
|
||||
v-if="canOperateStore"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -371,6 +392,7 @@ onMounted(fetchStores);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -379,6 +401,7 @@ onMounted(fetchStores);
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteStore"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -389,6 +412,22 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -453,9 +492,9 @@ onMounted(fetchStores);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -540,6 +579,16 @@ onMounted(fetchStores);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.primary-text {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
@@ -561,8 +610,8 @@ onMounted(fetchStores);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -580,6 +629,11 @@ onMounted(fetchStores);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"mock/*.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
|
||||
Vendored
+1
-1
@@ -5,7 +5,7 @@ declare module "vue" {
|
||||
export interface ComponentCustomProperties {
|
||||
/** `Loading` 动画加载指令,具体看:https://element-plus.org/zh-CN/component/loading.html#%E6%8C%87%E4%BB%A4 */
|
||||
vLoading: Directive<Element, boolean>;
|
||||
/** 按钮权限指令(根据路由`meta`中的`auths`字段进行判断)*/
|
||||
/** 按钮权限指令(根据后端权限码进行判断)*/
|
||||
vAuth: Directive<HTMLElement, string | Array<string>>;
|
||||
/** 文本复制指令(默认双击复制) */
|
||||
vCopy: Directive<CopyEl, string>;
|
||||
|
||||
Vendored
-1
@@ -105,7 +105,6 @@ declare global {
|
||||
ShowLogo?: boolean;
|
||||
ShowModel?: string;
|
||||
MenuArrowIconNoTransition?: boolean;
|
||||
CachingAsyncRoutes?: boolean;
|
||||
TooltipEffect?: Effect;
|
||||
ResponsiveStorageNameSpace?: string;
|
||||
MenuSearchHistory?: number;
|
||||
|
||||
Vendored
-2
@@ -28,8 +28,6 @@ declare global {
|
||||
permission?: string | Array<string>;
|
||||
/** 后端权限菜单 key,用于和 `/api/permissions/me` 返回菜单对应 */
|
||||
menuKey?: string;
|
||||
/** 按钮级别权限设置 `可选` */
|
||||
auths?: Array<string>;
|
||||
/** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
|
||||
keepAlive?: boolean;
|
||||
/** 内嵌的`iframe`链接 `可选` */
|
||||
|
||||
Reference in New Issue
Block a user