Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c261c4f64 | |||
| 34e52ab1d0 | |||
| 9575e5898f |
47
AGENTS.md
47
AGENTS.md
@@ -46,6 +46,28 @@ go test -run TestName ./...
|
||||
|
||||
## 架构
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
smartserve-client/
|
||||
├── main.go # Wails 应用入口
|
||||
├── app.go # App 结构体,暴露给前端的 Go 方法
|
||||
├── go.mod / go.sum
|
||||
├── wails.json # Wails 项目配置
|
||||
└── frontend/
|
||||
└── src/
|
||||
├── App.tsx # 根组件(antd 配置、401 处理器)
|
||||
├── main.tsx # React 入口(MSW 启动)
|
||||
├── api/ # HTTP 请求函数(user、knowledgeCards)
|
||||
├── pages/ # 页面组件(小驼峰命名)
|
||||
├── layouts/ # 布局组件(大驼峰命名)
|
||||
├── router/ # 路由配置与实例
|
||||
├── store/ # Zustand 状态管理
|
||||
├── mock/ # MSW 模拟 API(开发专用)
|
||||
├── types/ # TypeScript 全局类型
|
||||
└── utils/ # 工具函数(request.ts HTTP 客户端)
|
||||
```
|
||||
|
||||
### Go ↔ 前端桥接(Wails 绑定)
|
||||
|
||||
- 在 `main.go` 的 `Bind: []interface{}{app}` 中注册的结构体方法会自动暴露给前端。
|
||||
@@ -61,6 +83,7 @@ go test -run TestName ./...
|
||||
| `app.go` | `App` 结构体,包含暴露给前端的方法;新增 Go→JS 方法在此添加 |
|
||||
| `frontend/src/App.tsx` | 根 React 组件 |
|
||||
| `frontend/src/main.tsx` | React 入口点 |
|
||||
| `frontend/src/utils/request.ts` | HTTP 客户端(Token 注入、401 拦截、超时) |
|
||||
| `wails.json` | 项目配置(前端构建命令、输出文件名、作者信息) |
|
||||
|
||||
### 前端技术栈
|
||||
@@ -68,6 +91,8 @@ go test -run TestName ./...
|
||||
- **React 19** + **TypeScript**(严格模式)
|
||||
- **Ant Design 6**(`antd@6.x` + `@ant-design/icons@6.x`)用于 UI 组件——**编写页面时优先使用 antd 组件**
|
||||
- **react-router-dom v7** 用于路由管理
|
||||
- **Zustand v5** 用于状态管理
|
||||
- **MSW v2** 用于开发模式 API 模拟
|
||||
- **Vite 8** 用于打包
|
||||
- **pnpm** 作为包管理器(不要使用 npm/yarn)
|
||||
|
||||
@@ -93,9 +118,31 @@ go test -run TestName ./...
|
||||
|------|------|
|
||||
| `frontend/src/router/routes.tsx` | 路由配置数据,导出 `RouteConfig` 接口和 `routes` 数组;菜单组件从这里取数据 |
|
||||
| `frontend/src/router/index.tsx` | 基于 `routes` 创建并导出 `router` 实例(`createBrowserRouter`) |
|
||||
| `frontend/src/router/AuthGuard.tsx` | 路由守卫(`AuthGuard` 要求登录,`GuestGuard` 要求未登录) |
|
||||
|
||||
`RouteConfig` 包含 `path`、`label`、`icon`、`element`、`hideInMenu`、`children` 字段,菜单组件过滤 `hideInMenu: true` 的条目即可渲染菜单。
|
||||
|
||||
页面访问流程:
|
||||
```
|
||||
未登录访问任意页 → AuthGuard 重定向至 /login
|
||||
登录成功 → GuestGuard 重定向至 /
|
||||
```
|
||||
|
||||
### 状态管理(Zustand)
|
||||
|
||||
| Store | 文件 | 内容 |
|
||||
|-------|------|------|
|
||||
| `useUserStore` | `store/user.ts` | 用户信息、登录/登出、Token(持久化至 `localStorage` key: `access_token`) |
|
||||
| `useAppStore` | `store/app.ts` | 侧边栏折叠状态 |
|
||||
|
||||
401 响应由 `utils/request.ts` 全局拦截,调用 `App.tsx` 注册的 `setUnauthorizedHandler`,自动执行 `logout()` 并跳转 `/login`。
|
||||
|
||||
### API 层
|
||||
|
||||
- 请求函数位于 `frontend/src/api/`,调用 `utils/request.ts` 封装的 HTTP 客户端。
|
||||
- 统一响应格式(`types/global.d.ts`):`API.Response<T>` 含 `code`、`msg`、`ok`、`data`、`time` 字段。
|
||||
- 开发模式下,`mock/handlers/` 中的 MSW 处理器拦截请求并返回模拟数据;生产构建不包含 MSW。
|
||||
|
||||
### 添加新的 Go 方法
|
||||
|
||||
1. 在 `app.go` 的 `App` 结构体中添加方法(必须是导出/公开的)。
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_API_BASE_URL=
|
||||
VITE_MOCK=true
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=https://api.example.com
|
||||
VITE_MOCK=false
|
||||
|
||||
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -7,6 +7,11 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"msw": "^2.14.6",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.12"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
1723f0aff1690fce405ab7beb6570d82
|
||||
2f1b47a26399ec7c847d08cf81f5dc77
|
||||
389
frontend/pnpm-lock.yaml
generated
389
frontend/pnpm-lock.yaml
generated
@@ -54,6 +54,9 @@ importers:
|
||||
globals:
|
||||
specifier: ^17.6.0
|
||||
version: 17.6.0
|
||||
msw:
|
||||
specifier: ^2.14.6
|
||||
version: 2.14.6(@types/node@24.12.4)(typescript@6.0.3)
|
||||
typescript:
|
||||
specifier: ~6.0.2
|
||||
version: 6.0.3
|
||||
@@ -246,6 +249,41 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@inquirer/ansi@2.0.5':
|
||||
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
|
||||
'@inquirer/confirm@6.0.13':
|
||||
resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@11.1.10':
|
||||
resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@2.0.5':
|
||||
resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
|
||||
'@inquirer/type@4.0.5':
|
||||
resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -262,12 +300,28 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@mswjs/interceptors@0.41.9':
|
||||
resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4':
|
||||
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
|
||||
peerDependencies:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@open-draft/deferred-promise@2.2.0':
|
||||
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
|
||||
|
||||
'@open-draft/deferred-promise@3.0.0':
|
||||
resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==}
|
||||
|
||||
'@open-draft/logger@0.3.0':
|
||||
resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
|
||||
|
||||
'@open-draft/until@2.1.0':
|
||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||
|
||||
'@oxc-project/types@0.130.0':
|
||||
resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==}
|
||||
|
||||
@@ -669,6 +723,12 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/set-cookie-parser@2.4.10':
|
||||
resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==}
|
||||
|
||||
'@types/statuses@2.0.6':
|
||||
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.4':
|
||||
resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -754,6 +814,14 @@ packages:
|
||||
ajv@6.15.0:
|
||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
antd@6.4.3:
|
||||
resolution: {integrity: sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==}
|
||||
peerDependencies:
|
||||
@@ -781,10 +849,25 @@ packages:
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
cli-width@4.1.0:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
compute-scroll-into-view@3.1.1:
|
||||
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||
|
||||
@@ -824,6 +907,9 @@ packages:
|
||||
electron-to-chromium@1.5.359:
|
||||
resolution: {integrity: sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -894,6 +980,15 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-string-truncated-width@3.0.3:
|
||||
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
|
||||
|
||||
fast-string-width@3.0.2:
|
||||
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
||||
|
||||
fast-wrap-ansi@0.2.0:
|
||||
resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -927,6 +1022,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
glob-parent@6.0.2:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -935,6 +1034,13 @@ packages:
|
||||
resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
graphql@16.14.0:
|
||||
resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
headers-polyfill@5.0.1:
|
||||
resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==}
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
@@ -957,6 +1063,10 @@ packages:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -964,6 +1074,9 @@ packages:
|
||||
is-mobile@5.0.0:
|
||||
resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==}
|
||||
|
||||
is-node-process@1.2.0:
|
||||
resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -1087,6 +1200,20 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
msw@2.14.6:
|
||||
resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>= 4.8.x'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
mute-stream@3.0.0:
|
||||
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -1102,6 +1229,9 @@ packages:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
outvariant@1.4.3:
|
||||
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1118,6 +1248,9 @@ packages:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1166,6 +1299,13 @@ packages:
|
||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
rettime@0.11.11:
|
||||
resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==}
|
||||
|
||||
rolldown@1.0.1:
|
||||
resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1189,6 +1329,9 @@ packages:
|
||||
set-cookie-parser@2.7.2:
|
||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||
|
||||
set-cookie-parser@3.1.0:
|
||||
resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1197,16 +1340,39 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
strict-event-emitter@0.5.1:
|
||||
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
|
||||
|
||||
string-convert@0.2.1:
|
||||
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
stylis@4.4.0:
|
||||
resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==}
|
||||
|
||||
tagged-tag@1.0.0:
|
||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
throttle-debounce@5.0.2:
|
||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
||||
engines: {node: '>=12.22'}
|
||||
@@ -1215,6 +1381,17 @@ packages:
|
||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tldts-core@7.0.30:
|
||||
resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
|
||||
|
||||
tldts@7.0.30:
|
||||
resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==}
|
||||
hasBin: true
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
ts-api-utils@2.5.0:
|
||||
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
||||
engines: {node: '>=18.12'}
|
||||
@@ -1228,6 +1405,10 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-fest@5.6.0:
|
||||
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
typescript-eslint@8.59.4:
|
||||
resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1243,6 +1424,9 @@ packages:
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
until-async@3.0.2:
|
||||
resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
@@ -1304,9 +1488,25 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1558,6 +1758,33 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@inquirer/ansi@2.0.5': {}
|
||||
|
||||
'@inquirer/confirm@6.0.13(@types/node@24.12.4)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@24.12.4)
|
||||
'@inquirer/type': 4.0.5(@types/node@24.12.4)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@inquirer/core@11.1.10(@types/node@24.12.4)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.5
|
||||
'@inquirer/figures': 2.0.5
|
||||
'@inquirer/type': 4.0.5(@types/node@24.12.4)
|
||||
cli-width: 4.1.0
|
||||
fast-wrap-ansi: 0.2.0
|
||||
mute-stream: 3.0.0
|
||||
signal-exit: 4.1.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@inquirer/figures@2.0.5': {}
|
||||
|
||||
'@inquirer/type@4.0.5(@types/node@24.12.4)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -1577,6 +1804,15 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@mswjs/interceptors@0.41.9':
|
||||
dependencies:
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/logger': 0.3.0
|
||||
'@open-draft/until': 2.1.0
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
strict-event-emitter: 0.5.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
@@ -1584,6 +1820,17 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.2
|
||||
optional: true
|
||||
|
||||
'@open-draft/deferred-promise@2.2.0': {}
|
||||
|
||||
'@open-draft/deferred-promise@3.0.0': {}
|
||||
|
||||
'@open-draft/logger@0.3.0':
|
||||
dependencies:
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@oxc-project/types@0.130.0': {}
|
||||
|
||||
'@rc-component/async-validator@5.1.0':
|
||||
@@ -1997,6 +2244,12 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/set-cookie-parser@2.4.10':
|
||||
dependencies:
|
||||
'@types/node': 24.12.4
|
||||
|
||||
'@types/statuses@2.0.6': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -2106,6 +2359,12 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
antd@6.4.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
dependencies:
|
||||
'@ant-design/colors': 8.0.1
|
||||
@@ -2180,8 +2439,22 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
compute-scroll-into-view@3.1.1: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@@ -2208,6 +2481,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.359: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@@ -2297,6 +2572,16 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-string-truncated-width@3.0.3: {}
|
||||
|
||||
fast-string-width@3.0.2:
|
||||
dependencies:
|
||||
fast-string-truncated-width: 3.0.3
|
||||
|
||||
fast-wrap-ansi@0.2.0:
|
||||
dependencies:
|
||||
fast-string-width: 3.0.2
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
@@ -2322,12 +2607,21 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
glob-parent@6.0.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
globals@17.6.0: {}
|
||||
|
||||
graphql@16.14.0: {}
|
||||
|
||||
headers-polyfill@5.0.1:
|
||||
dependencies:
|
||||
'@types/set-cookie-parser': 2.4.10
|
||||
set-cookie-parser: 3.1.0
|
||||
|
||||
hermes-estree@0.25.1: {}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
@@ -2342,12 +2636,16 @@ snapshots:
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-mobile@5.0.0: {}
|
||||
|
||||
is-node-process@1.2.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -2438,6 +2736,33 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msw@2.14.6(@types/node@24.12.4)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@inquirer/confirm': 6.0.13(@types/node@24.12.4)
|
||||
'@mswjs/interceptors': 0.41.9
|
||||
'@open-draft/deferred-promise': 3.0.0
|
||||
'@types/statuses': 2.0.6
|
||||
cookie: 1.1.1
|
||||
graphql: 16.14.0
|
||||
headers-polyfill: 5.0.1
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
picocolors: 1.1.1
|
||||
rettime: 0.11.11
|
||||
statuses: 2.0.2
|
||||
strict-event-emitter: 0.5.1
|
||||
tough-cookie: 6.0.1
|
||||
type-fest: 5.6.0
|
||||
until-async: 3.0.2
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
mute-stream@3.0.0: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -2453,6 +2778,8 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
outvariant@1.4.3: {}
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
@@ -2465,6 +2792,8 @@ snapshots:
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
@@ -2502,6 +2831,10 @@ snapshots:
|
||||
|
||||
react@19.2.6: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
rettime@0.11.11: {}
|
||||
|
||||
rolldown@1.0.1:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.130.0
|
||||
@@ -2535,18 +2868,38 @@ snapshots:
|
||||
|
||||
set-cookie-parser@2.7.2: {}
|
||||
|
||||
set-cookie-parser@3.1.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
strict-event-emitter@0.5.1: {}
|
||||
|
||||
string-convert@0.2.1: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
stylis@4.4.0: {}
|
||||
|
||||
tagged-tag@1.0.0: {}
|
||||
|
||||
throttle-debounce@5.0.2: {}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
@@ -2554,6 +2907,16 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tldts-core@7.0.30: {}
|
||||
|
||||
tldts@7.0.30:
|
||||
dependencies:
|
||||
tldts-core: 7.0.30
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
dependencies:
|
||||
tldts: 7.0.30
|
||||
|
||||
ts-api-utils@2.5.0(typescript@6.0.3):
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
@@ -2565,6 +2928,10 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
type-fest@5.6.0:
|
||||
dependencies:
|
||||
tagged-tag: 1.0.0
|
||||
|
||||
typescript-eslint@8.59.4(eslint@10.4.0)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3)
|
||||
@@ -2580,6 +2947,8 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
until-async@3.0.2: {}
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
browserslist: 4.28.2
|
||||
@@ -2607,8 +2976,28 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.4.3):
|
||||
|
||||
349
frontend/public/mockServiceWorker.js
Normal file
349
frontend/public/mockServiceWorker.js
Normal file
@@ -0,0 +1,349 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.14.6'
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
const requestInterceptedAt = Date.now()
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been terminated (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {string} requestId
|
||||
* @param {number} requestInterceptedAt
|
||||
*/
|
||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(
|
||||
event,
|
||||
client,
|
||||
requestId,
|
||||
requestInterceptedAt,
|
||||
)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main client for the given event.
|
||||
* Client that issues a request doesn't necessarily equal the client
|
||||
* that registered the worker. It's with the latter the worker should
|
||||
* communicate with during the response resolving phase.
|
||||
* @param {FetchEvent} event
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @param {number} requestInterceptedAt
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
interceptedAt: requestInterceptedAt,
|
||||
...serializedRequest,
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {any} message
|
||||
* @param {Array<Transferable>} transferrables
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns {Response}
|
||||
*/
|
||||
function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
*/
|
||||
async function serializeRequest(request) {
|
||||
return {
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
}
|
||||
38
frontend/src/api/knowledgeCards.ts
Normal file
38
frontend/src/api/knowledgeCards.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
export type KnowledgeCardType = 'global' | 'product' | 'chat'
|
||||
export type KnowledgeCardAuditStatus = 'pending' | 'approved'
|
||||
|
||||
export interface RelatedProduct {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface RelatedChat {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface KnowledgeCard {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
type: KnowledgeCardType
|
||||
auditStatus: KnowledgeCardAuditStatus
|
||||
author: string
|
||||
updatedAt: string
|
||||
coverColor: string
|
||||
tags: string[]
|
||||
relatedProducts: RelatedProduct[]
|
||||
relatedChats: RelatedChat[]
|
||||
}
|
||||
|
||||
export interface GetKnowledgeCardsParams {
|
||||
type?: KnowledgeCardType
|
||||
auditStatus?: KnowledgeCardAuditStatus
|
||||
}
|
||||
|
||||
/** 获取知识卡片列表 */
|
||||
export function getKnowledgeCards(params?: GetKnowledgeCardsParams) {
|
||||
return get<KnowledgeCard[]>('/knowledge/cards', params as Record<string, string>)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ interface UserInfo {
|
||||
/** 手机号码 */
|
||||
phone: string
|
||||
/** 昵称 */
|
||||
nickname: string
|
||||
nickName: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ interface LoginParams {
|
||||
|
||||
interface LoginResult {
|
||||
token: string
|
||||
user: UserInfo
|
||||
}
|
||||
|
||||
// ---- API 函数 ----
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { Layout, Menu, Button, Popconfirm, Avatar, theme } from 'antd'
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom'
|
||||
import { routes } from '@/router/routes'
|
||||
import { routes, type RouteConfig } from '@/router/routes'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import type { MenuProps } from 'antd'
|
||||
|
||||
const { Sider, Header, Content } = Layout
|
||||
|
||||
// 递归扁平化所有路由,用于查找当前路径对应的标题
|
||||
function flatRouteLabel(list: RouteConfig[], pathname: string): string {
|
||||
for (const r of list) {
|
||||
if (r.path === pathname) return r.label
|
||||
if (r.children) {
|
||||
const found = flatRouteLabel(r.children, pathname)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const SIDER_WIDTH = 220
|
||||
const SIDER_COLLAPSED_WIDTH = 64
|
||||
const HEADER_HEIGHT = 64
|
||||
@@ -102,6 +114,7 @@ function BasicLayout() {
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
defaultOpenKeys={routes.filter(r => r.children).map(r => r.path)}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{ borderInlineEnd: 'none', paddingTop: 8 }}
|
||||
@@ -143,7 +156,7 @@ function BasicLayout() {
|
||||
|
||||
{/* 当前页面标题 */}
|
||||
<span style={{ fontSize: 16, fontWeight: 600, color: token.colorText }}>
|
||||
{routes.find(r => r.path === location.pathname)?.label ?? ''}
|
||||
{flatRouteLabel(routes, location.pathname)}
|
||||
</span>
|
||||
|
||||
{/* 右侧操作区 */}
|
||||
@@ -156,7 +169,7 @@ function BasicLayout() {
|
||||
style={{ background: token.colorPrimary }}
|
||||
/>
|
||||
<span style={{ color: token.colorText, fontSize: 14 }}>
|
||||
{userInfo?.nickname ?? ''}
|
||||
{userInfo?.nickName ?? ''}
|
||||
</span>
|
||||
|
||||
<Popconfirm
|
||||
|
||||
@@ -3,8 +3,21 @@ import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from '@/App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
async function bootstrap() {
|
||||
// 仅开发环境启动 MSW,生产构建时此分支被 tree-shaking 移除
|
||||
if (import.meta.env.VITE_MOCK === 'true') {
|
||||
const { worker } = await import('@/mock/browser')
|
||||
await worker.start({
|
||||
// 未匹配的请求透传到真实网络,不打印警告
|
||||
onUnhandledRequest: 'bypass',
|
||||
})
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
5
frontend/src/mock/browser.ts
Normal file
5
frontend/src/mock/browser.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { setupWorker } from 'msw/browser'
|
||||
import { handlers } from './handlers'
|
||||
|
||||
/** MSW browser worker 实例,仅在开发环境使用 */
|
||||
export const worker = setupWorker(...handlers)
|
||||
8
frontend/src/mock/handlers.ts
Normal file
8
frontend/src/mock/handlers.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { userHandlers } from './handlers/user'
|
||||
import { knowledgeCardsHandlers } from './handlers/knowledgeCards'
|
||||
|
||||
/** 所有 mock handlers,按模块聚合 */
|
||||
export const handlers = [
|
||||
...userHandlers,
|
||||
...knowledgeCardsHandlers,
|
||||
]
|
||||
158
frontend/src/mock/handlers/knowledgeCards.ts
Normal file
158
frontend/src/mock/handlers/knowledgeCards.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
import type { KnowledgeCard } from '@/api/knowledgeCards'
|
||||
|
||||
const mockCards: KnowledgeCard[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '如何处理客户退款申请',
|
||||
content:
|
||||
'当客户提出退款申请时,首先需要核实订单信息,确认是否在退款政策范围内。若符合条件,按照标准流程发起退款,并在24小时内完成审核。对于特殊情况,需上报主管审批。',
|
||||
type: 'global',
|
||||
auditStatus: 'approved',
|
||||
author: '张三',
|
||||
updatedAt: '2025-05-20',
|
||||
coverColor: '#e6f4ff',
|
||||
tags: ['退款', '售后', '客服流程'],
|
||||
relatedProducts: [{ id: 101, name: '夏季连衣裙' }, { id: 102, name: '运动鞋' }],
|
||||
relatedChats: [{ id: 201, name: '退款标准话术' }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '新品发布常见问题解答',
|
||||
content:
|
||||
'本文档汇总了新品发布期间客户最常咨询的问题,包括发货时间、库存查询、预售规则等,客服可直接引用回复客户。',
|
||||
type: 'product',
|
||||
auditStatus: 'pending',
|
||||
author: '李四',
|
||||
updatedAt: '2025-05-22',
|
||||
coverColor: '#fff7e6',
|
||||
tags: ['新品', 'FAQ', '预售'],
|
||||
relatedProducts: [{ id: 103, name: '2025夏季新款T恤' }],
|
||||
relatedChats: [{ id: 202, name: '新品咨询话术' }, { id: 203, name: '预售说明' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '节假日活动话术模板',
|
||||
content:
|
||||
'双十一、春节、618等大促节点的标准欢迎语、活动介绍话术,以及常见异议处理方式。使用时请根据当前活动情况替换活动名称和折扣信息。',
|
||||
type: 'chat',
|
||||
auditStatus: 'approved',
|
||||
author: '王五',
|
||||
updatedAt: '2025-05-18',
|
||||
coverColor: '#f6ffed',
|
||||
tags: ['大促', '话术', '双十一', '618'],
|
||||
relatedProducts: [],
|
||||
relatedChats: [{ id: 204, name: '双十一欢迎语' }, { id: 205, name: '618活动说明' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '商品质量问题处理规范',
|
||||
content:
|
||||
'针对客户反馈商品质量问题的完整处理流程:收集问题描述与凭证、判断责任归属、协商解决方案(补寄/退款/换货)、记录归档。',
|
||||
type: 'product',
|
||||
auditStatus: 'approved',
|
||||
author: '赵六',
|
||||
updatedAt: '2025-05-15',
|
||||
coverColor: '#fff0f6',
|
||||
tags: ['质量问题', '换货', '售后'],
|
||||
relatedProducts: [{ id: 104, name: '皮质背包' }, { id: 105, name: '陶瓷餐具套装' }],
|
||||
relatedChats: [{ id: 206, name: '质量投诉处理话术' }],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '客户服务礼貌用语规范',
|
||||
content:
|
||||
'统一客服沟通风格,包括:开场白、道歉用语、感谢用语、告别语等,禁止使用的不当用语列表,以及如何在情绪激动的客户面前保持专业态度。',
|
||||
type: 'chat',
|
||||
auditStatus: 'pending',
|
||||
author: '孙七',
|
||||
updatedAt: '2025-05-23',
|
||||
coverColor: '#e6f4ff',
|
||||
tags: ['礼貌用语', '规范', '沟通技巧'],
|
||||
relatedProducts: [],
|
||||
relatedChats: [{ id: 207, name: '通用开场白' }, { id: 208, name: '道歉话术' }],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '快递异常处理指南',
|
||||
content:
|
||||
'物流延误、快递丢失、破损到货等场景的处理方式。包括如何查询快递信息、与快递公司协商、赔偿标准,以及如何安抚客户情绪。',
|
||||
type: 'global',
|
||||
auditStatus: 'approved',
|
||||
author: '周八',
|
||||
updatedAt: '2025-05-10',
|
||||
coverColor: '#f9f0ff',
|
||||
tags: ['物流', '快递异常', '赔偿'],
|
||||
relatedProducts: [],
|
||||
relatedChats: [{ id: 209, name: '物流异常安抚话术' }],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '会员积分兑换规则说明',
|
||||
content:
|
||||
'平台会员积分的获取方式(消费、签到、邀请好友)、兑换比例、有效期及使用限制。常见积分问题的排查和处理方式。',
|
||||
type: 'global',
|
||||
auditStatus: 'pending',
|
||||
author: '吴九',
|
||||
updatedAt: '2025-05-21',
|
||||
coverColor: '#fff7e6',
|
||||
tags: ['会员', '积分', '兑换'],
|
||||
relatedProducts: [],
|
||||
relatedChats: [{ id: 210, name: '积分查询话术' }],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: '爆款商品卖点话术',
|
||||
content:
|
||||
'本季度热销TOP10商品的核心卖点提炼、差异化优势描述和场景化推荐话术,帮助客服快速向客户介绍商品价值并促成购买决策。',
|
||||
type: 'product',
|
||||
auditStatus: 'approved',
|
||||
author: '郑十',
|
||||
updatedAt: '2025-05-17',
|
||||
coverColor: '#f6ffed',
|
||||
tags: ['爆款', '卖点', '推荐话术'],
|
||||
relatedProducts: [
|
||||
{ id: 106, name: '无线蓝牙耳机' },
|
||||
{ id: 107, name: '智能手表' },
|
||||
{ id: 108, name: '颈部按摩仪' },
|
||||
],
|
||||
relatedChats: [{ id: 211, name: '爆款推荐话术' }, { id: 212, name: '商品对比话术' }],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: '投诉升级处理流程',
|
||||
content:
|
||||
'当客户投诉进入升级状态(如威胁投诉平台、媒体曝光)时的处置预案:评估风险等级、上报节点、授权范围,以及事后复盘和改进记录要求。',
|
||||
type: 'global',
|
||||
auditStatus: 'pending',
|
||||
author: '张三',
|
||||
updatedAt: '2025-05-24',
|
||||
coverColor: '#fff0f6',
|
||||
tags: ['投诉', '升级处理', '风险'],
|
||||
relatedProducts: [],
|
||||
relatedChats: [{ id: 213, name: '升级投诉安抚话术' }],
|
||||
},
|
||||
]
|
||||
|
||||
export const knowledgeCardsHandlers = [
|
||||
http.get('/knowledge/cards', async ({ request }) => {
|
||||
await delay(300)
|
||||
const url = new URL(request.url)
|
||||
const type = url.searchParams.get('type')
|
||||
const auditStatus = url.searchParams.get('auditStatus')
|
||||
|
||||
const filtered = mockCards.filter(card => {
|
||||
const typeMatch = !type || card.type === type
|
||||
const auditMatch = !auditStatus || card.auditStatus === auditStatus
|
||||
return typeMatch && auditMatch
|
||||
})
|
||||
|
||||
return HttpResponse.json({
|
||||
code: '0',
|
||||
msg: 'ok',
|
||||
time: Date.now(),
|
||||
ok: true,
|
||||
data: filtered,
|
||||
})
|
||||
}),
|
||||
]
|
||||
51
frontend/src/mock/handlers/user.ts
Normal file
51
frontend/src/mock/handlers/user.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
|
||||
// ---- 私有 mock 数据 ----
|
||||
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
phone: '13800138000',
|
||||
nickName: '测试用户',
|
||||
avatar: '',
|
||||
}
|
||||
|
||||
const MOCK_TOKEN = 'mock-token-dev-only'
|
||||
|
||||
// ---- handlers ----
|
||||
|
||||
export const userHandlers = [
|
||||
// 登录
|
||||
http.post('/auth/login', async ({ request }) => {
|
||||
await delay(300)
|
||||
const { phone, password } = await request.json() as { phone: string; password: string }
|
||||
|
||||
if (phone === mockUser.phone && password === '123456') {
|
||||
return HttpResponse.json({
|
||||
code: '0',
|
||||
msg: 'ok',
|
||||
time: Date.now(),
|
||||
ok: true,
|
||||
data: { token: MOCK_TOKEN },
|
||||
})
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
code: '401',
|
||||
msg: '手机号或密码错误',
|
||||
time: Date.now(),
|
||||
ok: false,
|
||||
})
|
||||
}),
|
||||
|
||||
// 获取用户信息
|
||||
http.get('/user/info', async () => {
|
||||
await delay(200)
|
||||
return HttpResponse.json({
|
||||
code: '0',
|
||||
msg: 'ok',
|
||||
time: Date.now(),
|
||||
ok: true,
|
||||
data: mockUser,
|
||||
})
|
||||
}),
|
||||
]
|
||||
563
frontend/src/pages/knowledgeBase/cards/index.tsx
Normal file
563
frontend/src/pages/knowledgeBase/cards/index.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Tag,
|
||||
Typography,
|
||||
Space,
|
||||
Segmented,
|
||||
Flex,
|
||||
Avatar,
|
||||
theme,
|
||||
Drawer,
|
||||
Divider,
|
||||
Spin,
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
GlobalOutlined,
|
||||
ShoppingOutlined,
|
||||
MessageOutlined,
|
||||
CalendarOutlined,
|
||||
UserOutlined,
|
||||
CopyOutlined,
|
||||
ShoppingCartOutlined,
|
||||
CommentOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
getKnowledgeCards,
|
||||
type KnowledgeCard,
|
||||
type KnowledgeCardType,
|
||||
type KnowledgeCardAuditStatus,
|
||||
} from '@/api/knowledgeCards'
|
||||
|
||||
const { Text, Paragraph, Title } = Typography
|
||||
|
||||
type AuditFilter = 'all' | KnowledgeCardAuditStatus
|
||||
type TypeFilter = 'all' | KnowledgeCardType
|
||||
|
||||
const typeConfig = {
|
||||
global: { label: '全域知识', icon: <GlobalOutlined />, color: 'blue' },
|
||||
product: { label: '商品知识', icon: <ShoppingOutlined />, color: 'orange' },
|
||||
chat: { label: '聊天知识', icon: <MessageOutlined />, color: 'green' },
|
||||
}
|
||||
|
||||
const auditConfig = {
|
||||
pending: { label: '未审核', icon: <ClockCircleOutlined />, color: 'warning' as const },
|
||||
approved: { label: '已审核', icon: <CheckCircleOutlined />, color: 'success' as const },
|
||||
}
|
||||
|
||||
// 卡片固定尺寸
|
||||
const CARD_WIDTH = 280
|
||||
const CARD_HEIGHT = 220
|
||||
|
||||
function KnowledgeCardItem({
|
||||
card,
|
||||
onClick,
|
||||
}: {
|
||||
card: KnowledgeCard
|
||||
onClick: (card: KnowledgeCard) => void
|
||||
}) {
|
||||
const { token } = theme.useToken()
|
||||
const type = typeConfig[card.type]
|
||||
const audit = auditConfig[card.auditStatus]
|
||||
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => onClick(card)}
|
||||
style={{
|
||||
borderRadius: token.borderRadiusLG,
|
||||
overflow: 'hidden',
|
||||
width: CARD_WIDTH,
|
||||
height: CARD_HEIGHT,
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
styles={{ body: { padding: 0, height: '100%' } }}
|
||||
>
|
||||
{/* 顶部色块装饰 */}
|
||||
<div
|
||||
style={{
|
||||
height: 6,
|
||||
background:
|
||||
card.type === 'global'
|
||||
? token.colorPrimary
|
||||
: card.type === 'product'
|
||||
? token.colorWarning
|
||||
: token.colorSuccess,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: token.paddingMD,
|
||||
height: `calc(100% - 6px)`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{/* 标题:最多1行,超出省略 */}
|
||||
<Text
|
||||
strong
|
||||
ellipsis
|
||||
style={{
|
||||
fontSize: token.fontSizeLG,
|
||||
display: 'block',
|
||||
marginBottom: token.marginXS,
|
||||
color: token.colorText,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{card.title}
|
||||
</Text>
|
||||
|
||||
{/* 正文:固定行数,超出省略,不可展开 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
marginBottom: token.marginSM,
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
ellipsis={{ rows: 3, expandable: false }}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
fontSize: token.fontSize,
|
||||
}}
|
||||
>
|
||||
{card.content}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 底部作者信息 */}
|
||||
<Flex align="center" justify="space-between" style={{ flexShrink: 0, marginBottom: 6 }}>
|
||||
<Space size={4}>
|
||||
<Avatar
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: token.colorPrimary,
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{card.author[0]}
|
||||
</Avatar>
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
{card.author} · {card.updatedAt}
|
||||
</Text>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{/* 标签行 */}
|
||||
<Flex gap={6} style={{ flexShrink: 0 }} wrap>
|
||||
<Tag
|
||||
icon={audit.icon}
|
||||
color={audit.color}
|
||||
style={{ margin: 0, borderRadius: token.borderRadiusSM }}
|
||||
>
|
||||
{audit.label}
|
||||
</Tag>
|
||||
<Tag
|
||||
icon={type.icon}
|
||||
color={type.color}
|
||||
style={{ margin: 0, borderRadius: token.borderRadiusSM }}
|
||||
>
|
||||
{type.label}
|
||||
</Tag>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeCardDetail({
|
||||
card,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
card: KnowledgeCard | null
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { token } = theme.useToken()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
if (!card) return null
|
||||
|
||||
const type = typeConfig[card.type]
|
||||
const audit = auditConfig[card.auditStatus]
|
||||
const topBarColor =
|
||||
card.type === 'global'
|
||||
? token.colorPrimary
|
||||
: card.type === 'product'
|
||||
? token.colorWarning
|
||||
: token.colorSuccess
|
||||
|
||||
const handleCopyId = () => {
|
||||
navigator.clipboard.writeText(String(card.id)).then(() => {
|
||||
messageApi.success('ID 已复制')
|
||||
})
|
||||
}
|
||||
|
||||
const sidebarBg = token.colorFillQuaternary
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={null}
|
||||
placement="right"
|
||||
width={760}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
styles={{
|
||||
header: { display: 'none' },
|
||||
body: { padding: 0, display: 'flex', flexDirection: 'column' },
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
{/* 顶部色块 */}
|
||||
<div style={{ height: 6, background: topBarColor, flexShrink: 0 }} />
|
||||
|
||||
{/* 左右两栏 */}
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
{/* 左侧:主内容 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: token.paddingLG,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
{/* 标题区 */}
|
||||
<Title level={4} style={{ marginTop: 0, marginBottom: token.marginSM }}>
|
||||
{card.title}
|
||||
</Title>
|
||||
|
||||
{/* 状态标签 */}
|
||||
<Flex gap={8} style={{ marginBottom: token.marginMD }}>
|
||||
<Tag
|
||||
icon={audit.icon}
|
||||
color={audit.color}
|
||||
style={{ borderRadius: token.borderRadiusSM }}
|
||||
>
|
||||
{audit.label}
|
||||
</Tag>
|
||||
<Tag
|
||||
icon={type.icon}
|
||||
color={type.color}
|
||||
style={{ borderRadius: token.borderRadiusSM }}
|
||||
>
|
||||
{type.label}
|
||||
</Tag>
|
||||
</Flex>
|
||||
|
||||
<Divider style={{ marginTop: 0, marginBottom: token.marginMD }} />
|
||||
|
||||
{/* 元信息 */}
|
||||
<Flex gap={token.marginLG} style={{ marginBottom: token.marginLG }}>
|
||||
<Space size={6}>
|
||||
<UserOutlined style={{ color: token.colorTextSecondary }} />
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
{card.author}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<CalendarOutlined style={{ color: token.colorTextSecondary }} />
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
{card.updatedAt}
|
||||
</Text>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{/* 正文完整内容 */}
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: token.fontSize,
|
||||
lineHeight: 1.8,
|
||||
color: token.colorText,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{card.content}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 右侧:属性面板 */}
|
||||
<div
|
||||
style={{
|
||||
width: 220,
|
||||
flexShrink: 0,
|
||||
overflow: 'auto',
|
||||
background: sidebarBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0,
|
||||
}}
|
||||
>
|
||||
{/* 基础信息 */}
|
||||
<div style={{ padding: `${token.paddingMD}px ${token.paddingMD}px ${token.paddingSM}px` }}>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: token.fontSizeSM,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
display: 'block',
|
||||
marginBottom: token.marginSM,
|
||||
}}
|
||||
>
|
||||
基础信息
|
||||
</Text>
|
||||
|
||||
{/* ID 可复制 */}
|
||||
<div style={{ marginBottom: token.marginSM }}>
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM, display: 'block', marginBottom: 4 }}>
|
||||
ID
|
||||
</Text>
|
||||
<Flex
|
||||
align="center"
|
||||
gap={6}
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
padding: `4px ${token.paddingSM}px`,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={handleCopyId}
|
||||
>
|
||||
<Text style={{ fontSize: token.fontSizeSM, flex: 1, fontFamily: 'monospace' }}>
|
||||
{card.id}
|
||||
</Text>
|
||||
<Tooltip title="复制 ID">
|
||||
<CopyOutlined style={{ fontSize: 12, color: token.colorTextSecondary }} />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{/* 多选标签 */}
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM, display: 'block', marginBottom: 4 }}>
|
||||
标签
|
||||
</Text>
|
||||
{card.tags.length > 0 ? (
|
||||
<Flex gap={4} wrap>
|
||||
{card.tags.map(tag => (
|
||||
<Tag
|
||||
key={tag}
|
||||
style={{
|
||||
margin: 0,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
fontSize: token.fontSizeSM,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
暂无标签
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: `${token.marginXS}px 0` }} />
|
||||
|
||||
{/* 扩展信息 */}
|
||||
<div style={{ padding: `${token.paddingSM}px ${token.paddingMD}px ${token.paddingMD}px` }}>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: token.fontSizeSM,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
display: 'block',
|
||||
marginBottom: token.marginSM,
|
||||
}}
|
||||
>
|
||||
扩展信息
|
||||
</Text>
|
||||
|
||||
{/* 关联商品 */}
|
||||
<div style={{ marginBottom: token.marginMD }}>
|
||||
<Space size={4} style={{ marginBottom: 6 }}>
|
||||
<ShoppingCartOutlined style={{ fontSize: token.fontSizeSM, color: token.colorTextSecondary }} />
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
关联商品
|
||||
</Text>
|
||||
</Space>
|
||||
{card.relatedProducts.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{card.relatedProducts.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
padding: `4px ${token.paddingSM}px`,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: token.fontSizeSM }} ellipsis>
|
||||
{p.name}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
暂无关联商品
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 关联聊天 */}
|
||||
<div>
|
||||
<Space size={4} style={{ marginBottom: 6 }}>
|
||||
<CommentOutlined style={{ fontSize: token.fontSizeSM, color: token.colorTextSecondary }} />
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
关联聊天
|
||||
</Text>
|
||||
</Space>
|
||||
{card.relatedChats.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{card.relatedChats.map(c => (
|
||||
<div
|
||||
key={c.id}
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
border: `1px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
padding: `4px ${token.paddingSM}px`,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: token.fontSizeSM }} ellipsis>
|
||||
{c.name}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
|
||||
暂无关联聊天
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeCards() {
|
||||
const { token } = theme.useToken()
|
||||
const [auditFilter, setAuditFilter] = useState<AuditFilter>('all')
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>('all')
|
||||
const [cards, setCards] = useState<KnowledgeCard[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedCard, setSelectedCard] = useState<KnowledgeCard | null>(null)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCards = async () => {
|
||||
setLoading(true)
|
||||
const params: Record<string, string> = {}
|
||||
if (auditFilter !== 'all') params.auditStatus = auditFilter
|
||||
if (typeFilter !== 'all') params.type = typeFilter
|
||||
|
||||
const res = await getKnowledgeCards(
|
||||
params as Parameters<typeof getKnowledgeCards>[0],
|
||||
)
|
||||
if (res.ok && res.data) {
|
||||
setCards(res.data)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchCards()
|
||||
}, [auditFilter, typeFilter])
|
||||
|
||||
const handleCardClick = (card: KnowledgeCard) => {
|
||||
setSelectedCard(card)
|
||||
setDrawerOpen(true)
|
||||
}
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: token.paddingLG }}>
|
||||
{/* 筛选栏 */}
|
||||
<Flex gap={token.marginMD} wrap style={{ marginBottom: token.marginLG }}>
|
||||
<Segmented
|
||||
value={auditFilter}
|
||||
onChange={v => setAuditFilter(v as AuditFilter)}
|
||||
options={[
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '未审核', value: 'pending' },
|
||||
{ label: '已审核', value: 'approved' },
|
||||
]}
|
||||
/>
|
||||
<Segmented
|
||||
value={typeFilter}
|
||||
onChange={v => setTypeFilter(v as TypeFilter)}
|
||||
options={[
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '全域知识', value: 'global' },
|
||||
{ label: '商品知识', value: 'product' },
|
||||
{ label: '聊天知识', value: 'chat' },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* 固定大小卡片网格 */}
|
||||
<Spin spinning={loading}>
|
||||
{cards.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: token.marginMD,
|
||||
}}
|
||||
>
|
||||
{cards.map(card => (
|
||||
<KnowledgeCardItem key={card.id} card={card} onClick={handleCardClick} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
!loading && (
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
style={{ height: 300, color: token.colorTextQuaternary, fontSize: token.fontSizeLG }}
|
||||
>
|
||||
暂无数据
|
||||
</Flex>
|
||||
)
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<KnowledgeCardDetail
|
||||
card={selectedCard}
|
||||
open={drawerOpen}
|
||||
onClose={handleDrawerClose}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KnowledgeCards
|
||||
@@ -1,17 +1,24 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import BasicLayout from '@/layouts/BasicLayout'
|
||||
import LoginPage from '@/pages/login'
|
||||
import NotFound from '@/pages/notFound'
|
||||
import { AuthGuard, GuestGuard } from './AuthGuard'
|
||||
import { routes } from './routes'
|
||||
import { routes, type RouteConfig } from './routes'
|
||||
|
||||
const layoutChildren = routes
|
||||
.filter(r => r.path !== '*')
|
||||
.map(({ path, element }) => ({
|
||||
path,
|
||||
element,
|
||||
...(path === '/' ? { index: true, path: undefined } : {}),
|
||||
}))
|
||||
// 递归展开所有路由(包含子路由),扁平化注册到 layoutChildren
|
||||
function flattenRoutes(list: RouteConfig[]): { path: string; element: ReactNode }[] {
|
||||
return list.flatMap(r => {
|
||||
if (r.path === '*') return []
|
||||
const self = r.path === '/'
|
||||
? [{ path: r.path, element: r.element, index: true }]
|
||||
: [{ path: r.path, element: r.element }]
|
||||
const children = r.children ? flattenRoutes(r.children) : []
|
||||
return [...self, ...children] as { path: string; element: ReactNode }[]
|
||||
})
|
||||
}
|
||||
|
||||
const layoutChildren = flattenRoutes(routes)
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { HomeOutlined } from '@ant-design/icons'
|
||||
import { HomeOutlined, BookOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import Home from '@/pages/home'
|
||||
import NotFound from '@/pages/notFound'
|
||||
import KnowledgeCards from '@/pages/knowledgeBase/cards'
|
||||
|
||||
export interface RouteConfig {
|
||||
/** 路由路径 */
|
||||
@@ -25,6 +26,20 @@ export const routes: RouteConfig[] = [
|
||||
icon: <HomeOutlined />,
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: '/knowledge-base',
|
||||
label: '知识库',
|
||||
icon: <BookOutlined />,
|
||||
element: <></>,
|
||||
children: [
|
||||
{
|
||||
path: '/knowledge-base/cards',
|
||||
label: '知识卡片',
|
||||
icon: <FileTextOutlined />,
|
||||
element: <KnowledgeCards />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
label: '页面不存在',
|
||||
|
||||
@@ -9,7 +9,7 @@ interface UserInfo {
|
||||
/** 手机号码 */
|
||||
phone: string
|
||||
/** 昵称 */
|
||||
nickname: string
|
||||
nickName: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
@@ -47,7 +47,12 @@ export const useUserStore = create<UserState>(set => ({
|
||||
const res = await loginApi(params)
|
||||
if (res.ok && res.data) {
|
||||
setToken(res.data.token)
|
||||
set({ isLoggedIn: true, userInfo: res.data.user })
|
||||
set({ isLoggedIn: true })
|
||||
// 登录成功后立即拉取用户信息
|
||||
const infoRes = await getUserInfo()
|
||||
if (infoRes.ok && infoRes.data) {
|
||||
set({ userInfo: infoRes.data })
|
||||
}
|
||||
return true
|
||||
}
|
||||
return res.msg || '登录失败'
|
||||
|
||||
@@ -59,9 +59,7 @@ async function request<T>(
|
||||
const authHeaders: Record<string, string> = {}
|
||||
if (!skipAuth) {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
authHeaders['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
if (token) authHeaders['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user