Compare commits
63 Commits
5615399a68
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4884035cd | |||
| 570f284230 | |||
| ea6a816d58 | |||
| 3ffdad142d | |||
| b26a9c8a39 | |||
| 10418fea0a | |||
| 632c123112 | |||
| b1d52b852f | |||
| 4a7f54b0ed | |||
| 61fe9ef143 | |||
| 9a5845708d | |||
| cd64cf42cc | |||
| 31344f1d58 | |||
| 1543bf76a9 | |||
| 622d8d5a4d | |||
| 3c1cf6c7fa | |||
| 17690283f6 | |||
| 030dc737fc | |||
| 609a01dc8a | |||
| 80f028bcb9 | |||
| 5061eced32 | |||
| 6896a86130 | |||
| 0652a24c5e | |||
| d53a8dfae5 | |||
| 2e369b23a9 | |||
| b72ad00912 | |||
| 7cc29e0a35 | |||
| 39458386ae | |||
| acef4418d8 | |||
| 9d84b1aae0 | |||
| d3d0830820 | |||
| b2da882b31 | |||
| 4ed4b537ad | |||
| 3988eaf910 | |||
| e9214137c1 | |||
| 13b74cfe97 | |||
|
|
ab882e085b | ||
| 62859bfc38 | |||
| ba328e02bb | |||
|
|
28d597d91e | ||
|
|
fe29fde564 | ||
|
|
7d578ab271 | ||
|
|
71da2d507e | ||
| acd41555f9 | |||
| 2367e03146 | |||
|
|
023490c012 | ||
|
|
29ef03c40f | ||
| 387eb41412 | |||
|
|
480714172e | ||
|
|
0c6ed249ee | ||
| 543d1a59a9 | |||
|
|
3ad30b4f39 | ||
|
|
14e0502d16 | ||
|
|
d43f999b96 | ||
|
|
8b34147868 | ||
| 7a4d831c10 | |||
|
|
3a064eb09f | ||
| 960fe805ec | |||
| 59b73f3dae | |||
| ddd05f8c02 | |||
| f634d21d2a | |||
| e3a456debd | |||
| 60debcda8a |
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(pnpm gen-route *)",
|
|
||||||
"Bash(pnpm typecheck *)",
|
|
||||||
"Bash(pnpm lint *)",
|
|
||||||
"WebFetch(domain:raw.githubusercontent.com)",
|
|
||||||
"Bash(Remove-Item *)",
|
|
||||||
"PowerShell(pnpm typecheck *)",
|
|
||||||
"WebFetch(domain:www.wangeditor.com)",
|
|
||||||
"Bash(node *)",
|
|
||||||
"Bash(dir \"rdms-project-boot-*\")",
|
|
||||||
"Bash(git stash *)",
|
|
||||||
"Bash(pnpm eslint *)",
|
|
||||||
"Bash(Select-String -Pattern \"business-rich-text-editor|task-operate-dialog\")",
|
|
||||||
"Bash(powershell *)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
.env
8
.env
@@ -2,9 +2,9 @@
|
|||||||
# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin"
|
# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin"
|
||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
|
|
||||||
VITE_APP_TITLE=研发内部管理系统
|
VITE_APP_TITLE=研发管理系统
|
||||||
|
|
||||||
VITE_APP_DESC=Frontend application for 灿能研发内部管理系统
|
VITE_APP_DESC=Frontend application for 灿能研发管理系统
|
||||||
|
|
||||||
# 图标名称前缀
|
# 图标名称前缀
|
||||||
VITE_ICON_PREFIX=icon
|
VITE_ICON_PREFIX=icon
|
||||||
@@ -33,7 +33,7 @@ VITE_SERVICE_SUCCESS_CODE=0
|
|||||||
|
|
||||||
# 后端登出状态码;当返回这些 code 时,前端会登出并跳回登录页
|
# 后端登出状态码;当返回这些 code 时,前端会登出并跳回登录页
|
||||||
# 典型场景:token 无效、登录状态失效、账号被踢下线、后端要求强制重新登录
|
# 典型场景:token 无效、登录状态失效、账号被踢下线、后端要求强制重新登录
|
||||||
VITE_SERVICE_LOGOUT_CODES=401,1002023000
|
VITE_SERVICE_LOGOUT_CODES=401
|
||||||
|
|
||||||
# 后端弹窗登出状态码;当返回这些 code 时,前端会先弹窗再登出
|
# 后端弹窗登出状态码;当返回这些 code 时,前端会先弹窗再登出
|
||||||
# 典型场景:账号被禁用、密码已重置、登录安全策略触发、需要用户先确认后再重新登录
|
# 典型场景:账号被禁用、密码已重置、登录安全策略触发、需要用户先确认后再重新登录
|
||||||
@@ -41,7 +41,7 @@ VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
|||||||
|
|
||||||
# token 过期状态码;当返回这些 code 时,前端会尝试刷新 token 并重发请求
|
# token 过期状态码;当返回这些 code 时,前端会尝试刷新 token 并重发请求
|
||||||
# 典型场景:accessToken 过期但 refreshToken 仍有效、短期登录凭证失效但允许无感续期
|
# 典型场景:accessToken 过期但 refreshToken 仍有效、短期登录凭证失效但允许无感续期
|
||||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=1002023001
|
VITE_SERVICE_EXPIRED_TOKEN_CODES=1002023000
|
||||||
|
|
||||||
# 静态路由模式下定义的超级管理员角色
|
# 静态路由模式下定义的超级管理员角色
|
||||||
VITE_STATIC_SUPER_ROLE=R_SUPER
|
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 生产环境的后端服务地址
|
# 生产环境的后端服务地址
|
||||||
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
VITE_SERVICE_BASE_URL=
|
||||||
|
|
||||||
# 生产环境下的其他后端服务地址
|
# 生产环境下的其他后端服务地址
|
||||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -38,5 +38,9 @@ yarn.lock
|
|||||||
/docs/*
|
/docs/*
|
||||||
!/docs/frontend-page-resource-manifest.json
|
!/docs/frontend-page-resource-manifest.json
|
||||||
|
|
||||||
|
# Claude
|
||||||
|
/.claude/*
|
||||||
|
|
||||||
# Temp
|
# Temp
|
||||||
/codeTemp/*
|
/codeTemp/*
|
||||||
|
SKILL.md
|
||||||
|
|||||||
2
.trae/rules/vue-need.md
Normal file
2
.trae/rules/vue-need.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
1. 每次开发新功能、编写代码时都添加好相应的注释。
|
||||||
|
2. 所有的vue文件编码必须是UTF-8的。
|
||||||
@@ -262,6 +262,7 @@ const directionLabels = getLabels(row.directionCodes, { separator: ',' });
|
|||||||
- 如果后端当前接口暂时还返回数值型 ID,前端也必须尽可能在 `typings`、API 适配层或进入业务层前转成 `string`,不要把 ID 按 `number` 扩散到页面、store 和组件里。
|
- 如果后端当前接口暂时还返回数值型 ID,前端也必须尽可能在 `typings`、API 适配层或进入业务层前转成 `string`,不要把 ID 按 `number` 扩散到页面、store 和组件里。
|
||||||
- 但要注意:如果后端把超出 JS 安全整数范围的 Long 直接作为 JSON 数字返回,前端在业务层再 `String(number)` 只能得到“已经丢精度后的错误字符串”。这种情况必须明确记为接口契约风险,不要误判为“前端已安全处理”。
|
- 但要注意:如果后端把超出 JS 安全整数范围的 Long 直接作为 JSON 数字返回,前端在业务层再 `String(number)` 只能得到“已经丢精度后的错误字符串”。这种情况必须明确记为接口契约风险,不要误判为“前端已安全处理”。
|
||||||
- 因此,新增或改造接口时,最稳妥的契约仍然是:后端长整型 ID 直接按字符串返回;前端全链路按字符串接收和传递。若后端暂未改,前端侧也不得新增 `number` 口径 ID 用法。
|
- 因此,新增或改造接口时,最稳妥的契约仍然是:后端长整型 ID 直接按字符串返回;前端全链路按字符串接收和传递。若后端暂未改,前端侧也不得新增 `number` 口径 ID 用法。
|
||||||
|
- API 适配层兜底(实操约束):所有从后端接收的数值型 ID 字段(不论后端实际返回 `string`、`number` 或两者混合),都必须在 `src/service/api/*` 的 normalize 或 map 函数中显式调用 `String(rawId)` 归一一次;前端业务层(`views`、`store`、组件、`Map` 键、路由参数)只接收 `string` 形态,永远不需要自己 `String()`。这条与后端是否做了 Long → String 全局序列化无关——后端做了是双保险,没做且字段取值始终在 JS 安全整数内(例如 `infra_file_config.id` 永远是两位数)也是合理选择,前端 normalize 已经把口径收死,业务层无感。但这条不开按字段取值范围豁免的口子:前端 normalize 是无差别的,任何 ID 都要 `String()`,不要按某个字段当前取值大小决定要不要走 normalize,避免后续逐步污染仓库的 ID 纪律。
|
||||||
- 对仓库中的历史代码,原则是“不再新增 number 口径 ID,当前任务触达相关链路时优先顺手矫正”;不要继续复制历史写法。
|
- 对仓库中的历史代码,原则是“不再新增 number 口径 ID,当前任务触达相关链路时优先顺手矫正”;不要继续复制历史写法。
|
||||||
- 遵循仓库现有的 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。
|
- 遵循仓库现有的 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。
|
||||||
- 修改界面时优先延续 `src/layouts` 和 `src/theme` 中已有的 UI 模式,不要平行引入另一套设计体系。
|
- 修改界面时优先延续 `src/layouts` 和 `src/theme` 中已有的 UI 模式,不要平行引入另一套设计体系。
|
||||||
|
|||||||
23
CLAUDE.md
23
CLAUDE.md
@@ -285,6 +285,15 @@ const directionLabels = getLabels(row.directionCodes, { separator: ',' });
|
|||||||
- **但如果后端把超 JS 安全整数的 Long 直接作为 JSON 数字返回,前端再 `String(number)` 只能得到"已经丢精度后的错误字符串"**。这种情况必须明确记为接口契约风险,不能误判为"已安全处理"。
|
- **但如果后端把超 JS 安全整数的 Long 直接作为 JSON 数字返回,前端再 `String(number)` 只能得到"已经丢精度后的错误字符串"**。这种情况必须明确记为接口契约风险,不能误判为"已安全处理"。
|
||||||
- 最稳妥契约:**后端 Long ID 直接按字符串返回**;前端全链路按字符串。后端未改,前端也不得新增 `number` 口径 ID。
|
- 最稳妥契约:**后端 Long ID 直接按字符串返回**;前端全链路按字符串。后端未改,前端也不得新增 `number` 口径 ID。
|
||||||
|
|
||||||
|
### API 适配层兜底(操作约束)
|
||||||
|
- 所有从后端接收的数值型 ID 字段,**必须**在 `src/service/api/*` 的 normalize/map 函数里显式 `String(rawId)` 一次——**不管后端返回 string、number、还是混合**。
|
||||||
|
- 业务层(views / store / 组件 / `Map` key / 路由参数)**只接收 string**,从不需要自己 `String()`。
|
||||||
|
- 与"后端是否已经全局 Long → String"**无关**:
|
||||||
|
- 后端做了 → 双保险
|
||||||
|
- 后端没做但取值在 JS 安全整数内 → 单层防御也对(实际值不丢精度)
|
||||||
|
- 后端没做且取值超安全整数 → 不安全,必须推后端改
|
||||||
|
- **不开"按取值范围豁免"的口子**:哪怕后端说"这个字段永远是两位数"(如 `infra_file_config.id`),前端照样 `String()`。否则后续会冒出"projectStatus 是 Long 但只有 0-99,也可以保留 number"等连锁例外,铁律字面被掏空。
|
||||||
|
|
||||||
### 历史代码原则
|
### 历史代码原则
|
||||||
不再新增 `number` 口径;当前任务触达相关链路时**顺手矫正**;不要继续复制历史写法。
|
不再新增 `number` 口径;当前任务触达相关链路时**顺手矫正**;不要继续复制历史写法。
|
||||||
|
|
||||||
@@ -406,3 +415,17 @@ pnpm preview # preview server (9725)
|
|||||||
- 新建写接口 → 不用管,默认兜底;只有明确"允许短时间连发"才传 `dedupe: false`
|
- 新建写接口 → 不用管,默认兜底;只有明确"允许短时间连发"才传 `dedupe: false`
|
||||||
- 新建上传 / 下载流程 → FormData / Blob 天然不在去重范围,按原方式写即可
|
- 新建上传 / 下载流程 → FormData / Blob 天然不在去重范围,按原方式写即可
|
||||||
- 用户报"双击双发"复现不出来 → 检查目标按钮是否走了 `business-form-dialog`;若用的是 `ElMessageBox.confirm`,靠第二层就该挡住
|
- 用户报"双击双发"复现不出来 → 检查目标按钮是否走了 `business-form-dialog`;若用的是 `ElMessageBox.confirm`,靠第二层就该挡住
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. 我生成文档的输出格式(强约束)
|
||||||
|
|
||||||
|
- **superpowers 工作流(`docs/superpowers/plans/`、`docs/superpowers/specs/`)下输出的文档继续用 `.md`**——工作流以 markdown 为前提。
|
||||||
|
- **其他**我生成的文档(设计方案、复盘、规约、技术经验沉淀等)**默认用 `.html`**,沿用 `docs/debt/` 现有 HTML 文档(参考 `token-刷新机制对齐分析.html`、`技术负债台账.html`)的样式骨架:
|
||||||
|
- 单文件、内联 CSS
|
||||||
|
- `max-width: 980px` 居中容器、`padding: 32px 28px 80px`
|
||||||
|
- 14px / `line-height: 1.7`、`PingFang SC` / `Microsoft YaHei` 中文字体优先
|
||||||
|
- 模块化区块:`section` + 编号 h2、`card`、`table.cmp`、`pre`、`tag-ok/warn/bad/crit`
|
||||||
|
- 配色用 `--bg / --panel / --border / --text / --primary` 一套 CSS 变量
|
||||||
|
- **`README.md`** 是目录索引约定文件,**保持 `.md`**(不强行 `.html`)。
|
||||||
|
- **已有 `.md` 文档不主动改写**,等用户明确要求再转。
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,35 +0,0 @@
|
|||||||
# cn-rdms-web
|
|
||||||
|
|
||||||
这是当前项目的前端工程仓库。
|
|
||||||
|
|
||||||
原开源模板项目的介绍内容已移除,这个 README 现在只保留当前项目自身所需的信息。
|
|
||||||
|
|
||||||
## 项目说明
|
|
||||||
|
|
||||||
待补充。
|
|
||||||
|
|
||||||
建议后续在这里补充:
|
|
||||||
|
|
||||||
- 项目背景
|
|
||||||
- 技术栈
|
|
||||||
- 目录结构
|
|
||||||
- 本地启动方式
|
|
||||||
- 环境变量说明
|
|
||||||
- 构建与发布流程
|
|
||||||
|
|
||||||
## 本地开发
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常用命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
pnpm build
|
|
||||||
pnpm build:dev
|
|
||||||
pnpm typecheck
|
|
||||||
pnpm lint
|
|
||||||
```
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ProxyOptions } from 'vite';
|
import type { ProxyOptions } from 'vite';
|
||||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||||
import { consola } from 'consola';
|
import { consola } from 'consola';
|
||||||
|
import { WEB_SERVICE_PREFIX } from '../../src/constants/service';
|
||||||
import { createServiceConfig } from '../../src/utils/service';
|
import { createServiceConfig } from '../../src/utils/service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,14 @@ export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
|||||||
Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
|
Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 富文本图片 <img src="/admin-api/system/file/{configId}/get/{path}"> 由浏览器直接发起,
|
||||||
|
// 不经过 axios,没有 baseURL 前缀。这里加一条原样透传,避免被 Vite SPA fallback 兜底成 index.html。
|
||||||
|
// 不带 rewrite —— 原样把 /admin-api/* 转发到后端;不影响现有 /proxy-default 链路。
|
||||||
|
proxy[WEB_SERVICE_PREFIX] = {
|
||||||
|
target: baseURL,
|
||||||
|
changeOrigin: true
|
||||||
|
};
|
||||||
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ export function setupElegantRouter() {
|
|||||||
onRouteMetaGen(routeName) {
|
onRouteMetaGen(routeName) {
|
||||||
const key = routeName as RouteKey;
|
const key = routeName as RouteKey;
|
||||||
|
|
||||||
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
|
const constantRoutes: RouteKey[] = ['login', '403', '404', '500', 'workbench', 'feedback'];
|
||||||
const routeMetaMap: Partial<Record<RouteKey, Partial<RouteMeta>>> = {
|
const routeMetaMap: Partial<Record<RouteKey, Partial<RouteMeta>>> = {
|
||||||
|
workbench: {
|
||||||
|
icon: 'mdi:view-dashboard-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
product: {
|
product: {
|
||||||
icon: 'carbon:product',
|
icon: 'carbon:product',
|
||||||
order: 4
|
order: 4
|
||||||
@@ -79,6 +84,90 @@ export function setupElegantRouter() {
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
activeMenu: 'project_list'
|
activeMenu: 'project_list'
|
||||||
},
|
},
|
||||||
|
ticket: {
|
||||||
|
icon: 'mdi:ticket-confirmation-outline',
|
||||||
|
order: 6
|
||||||
|
},
|
||||||
|
'ticket_my-submitted': {
|
||||||
|
icon: 'mdi:upload-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'ticket_my-pending': {
|
||||||
|
icon: 'mdi:inbox-arrow-down-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
icon: 'mdi:chart-line',
|
||||||
|
order: 7
|
||||||
|
},
|
||||||
|
'metrics_project-progress': {
|
||||||
|
icon: 'mdi:progress-clock',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'metrics_member-efficiency': {
|
||||||
|
icon: 'mdi:account-multiple-check-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
metrics_worktime: {
|
||||||
|
icon: 'mdi:clock-time-five-outline',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center': {
|
||||||
|
icon: 'mdi:account-circle-outline',
|
||||||
|
order: 8
|
||||||
|
},
|
||||||
|
'personal-center_my-profile': {
|
||||||
|
icon: 'mdi:account-box-outline',
|
||||||
|
order: 0,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_my-item': {
|
||||||
|
icon: 'mdi:checkbox-multiple-blank-circle-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_work-report': {
|
||||||
|
icon: 'mdi:file-chart-outline',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_work-report_weekly': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
|
'personal-center_work-report_monthly': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
|
'personal-center_work-report_project': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
|
'personal-center_my-performance': {
|
||||||
|
icon: 'mdi:trophy-outline',
|
||||||
|
order: 4,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_my-application': {
|
||||||
|
icon: 'mdi:file-document-outline',
|
||||||
|
order: 5,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_overtime-application': {
|
||||||
|
icon: 'mdi:clock-plus-outline',
|
||||||
|
order: 6,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'personal-center_pending-approval': {
|
||||||
|
icon: 'mdi:check-decagram-outline',
|
||||||
|
order: 7,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
system: {
|
system: {
|
||||||
icon: 'carbon:cloud-service-management',
|
icon: 'carbon:cloud-service-management',
|
||||||
order: 9,
|
order: 9,
|
||||||
@@ -110,6 +199,46 @@ export function setupElegantRouter() {
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
roles: ['R_ADMIN'],
|
roles: ['R_ADMIN'],
|
||||||
activeMenu: 'system_user'
|
activeMenu: 'system_user'
|
||||||
|
},
|
||||||
|
feedback: {
|
||||||
|
icon: 'mdi:message-alert-outline',
|
||||||
|
order: 10,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
infra: {
|
||||||
|
icon: 'ep:monitor',
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
'infra_state-machine': {
|
||||||
|
icon: 'mdi:state-machine',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'infra_log-management': {
|
||||||
|
icon: 'mdi:text-box-search-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'infra_log-management_login-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_operate-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_api-access-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_api-error-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_rd-code': {
|
||||||
|
icon: 'mdi:identifier',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-04-29T08:18:14.397Z",
|
"generatedAt": "2026-06-27T00:49:28.085Z",
|
||||||
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
||||||
"rules": {
|
"rules": {
|
||||||
"directoryComponent": "layout.base",
|
"directoryComponent": "layout.base",
|
||||||
"pageComponentPattern": "view.<routeName>",
|
"pageComponentPattern": "view.<routeName>",
|
||||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||||
},
|
},
|
||||||
"total": 8,
|
"total": 23,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "product_list",
|
"name": "product_list",
|
||||||
@@ -74,6 +74,402 @@
|
|||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ticket_my-submitted",
|
||||||
|
"path": "/ticket/my-submitted",
|
||||||
|
"component": "view.ticket_my-submitted",
|
||||||
|
"title": "我提交的工单",
|
||||||
|
"routeTitle": "ticket_my-submitted",
|
||||||
|
"i18nKey": "route.ticket_my-submitted",
|
||||||
|
"icon": "mdi:upload-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "我提交的工单",
|
||||||
|
"i18nKey": "route.ticket_my-submitted",
|
||||||
|
"icon": "mdi:upload-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "ticket",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ticket_my-pending",
|
||||||
|
"path": "/ticket/my-pending",
|
||||||
|
"component": "view.ticket_my-pending",
|
||||||
|
"title": "待我处理的工单",
|
||||||
|
"routeTitle": "ticket_my-pending",
|
||||||
|
"i18nKey": "route.ticket_my-pending",
|
||||||
|
"icon": "mdi:inbox-arrow-down-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "待我处理的工单",
|
||||||
|
"i18nKey": "route.ticket_my-pending",
|
||||||
|
"icon": "mdi:inbox-arrow-down-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "ticket",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metrics_project-progress",
|
||||||
|
"path": "/metrics/project-progress",
|
||||||
|
"component": "view.metrics_project-progress",
|
||||||
|
"title": "项目进度",
|
||||||
|
"routeTitle": "metrics_project-progress",
|
||||||
|
"i18nKey": "route.metrics_project-progress",
|
||||||
|
"icon": "mdi:progress-clock",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "项目进度",
|
||||||
|
"i18nKey": "route.metrics_project-progress",
|
||||||
|
"icon": "mdi:progress-clock",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "metrics",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metrics_member-efficiency",
|
||||||
|
"path": "/metrics/member-efficiency",
|
||||||
|
"component": "view.metrics_member-efficiency",
|
||||||
|
"title": "员工能效",
|
||||||
|
"routeTitle": "metrics_member-efficiency",
|
||||||
|
"i18nKey": "route.metrics_member-efficiency",
|
||||||
|
"icon": "mdi:account-multiple-check-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "员工能效",
|
||||||
|
"i18nKey": "route.metrics_member-efficiency",
|
||||||
|
"icon": "mdi:account-multiple-check-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "metrics",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metrics_worktime",
|
||||||
|
"path": "/metrics/worktime",
|
||||||
|
"component": "view.metrics_worktime",
|
||||||
|
"title": "工时统计",
|
||||||
|
"routeTitle": "metrics_worktime",
|
||||||
|
"i18nKey": "route.metrics_worktime",
|
||||||
|
"icon": "mdi:clock-time-five-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "工时统计",
|
||||||
|
"i18nKey": "route.metrics_worktime",
|
||||||
|
"icon": "mdi:clock-time-five-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "metrics",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_my-profile",
|
||||||
|
"path": "/personal-center/my-profile",
|
||||||
|
"component": "view.personal-center_my-profile",
|
||||||
|
"title": "个人信息",
|
||||||
|
"routeTitle": "personal-center_my-profile",
|
||||||
|
"i18nKey": "route.personal-center_my-profile",
|
||||||
|
"icon": "mdi:account-box-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 0,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "个人信息",
|
||||||
|
"i18nKey": "route.personal-center_my-profile",
|
||||||
|
"icon": "mdi:account-box-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 0,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_my-item",
|
||||||
|
"path": "/personal-center/my-item",
|
||||||
|
"component": "view.personal-center_my-item",
|
||||||
|
"title": "我的事项",
|
||||||
|
"routeTitle": "personal-center_my-item",
|
||||||
|
"i18nKey": "route.personal-center_my-item",
|
||||||
|
"icon": "mdi:checkbox-multiple-blank-circle-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "我的事项",
|
||||||
|
"i18nKey": "route.personal-center_my-item",
|
||||||
|
"icon": "mdi:checkbox-multiple-blank-circle-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_work-report",
|
||||||
|
"path": "/personal-center/work-report",
|
||||||
|
"component": "view.personal-center_work-report",
|
||||||
|
"title": "工作报告",
|
||||||
|
"routeTitle": "personal-center_work-report",
|
||||||
|
"i18nKey": "route.personal-center_work-report",
|
||||||
|
"icon": "mdi:file-chart-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "工作报告",
|
||||||
|
"i18nKey": "route.personal-center_work-report",
|
||||||
|
"icon": "mdi:file-chart-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_my-application",
|
||||||
|
"path": "/personal-center/my-application",
|
||||||
|
"component": "view.personal-center_my-application",
|
||||||
|
"title": "我的申请",
|
||||||
|
"routeTitle": "personal-center_my-application",
|
||||||
|
"i18nKey": "route.personal-center_my-application",
|
||||||
|
"icon": "mdi:file-document-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "我的申请",
|
||||||
|
"i18nKey": "route.personal-center_my-application",
|
||||||
|
"icon": "mdi:file-document-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_my-performance",
|
||||||
|
"path": "/personal-center/my-performance",
|
||||||
|
"component": "view.personal-center_my-performance",
|
||||||
|
"title": "我的绩效",
|
||||||
|
"routeTitle": "personal-center_my-performance",
|
||||||
|
"i18nKey": "route.personal-center_my-performance",
|
||||||
|
"icon": "mdi:trophy-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "我的绩效",
|
||||||
|
"i18nKey": "route.personal-center_my-performance",
|
||||||
|
"icon": "mdi:trophy-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_overtime-application",
|
||||||
|
"path": "/personal-center/overtime-application",
|
||||||
|
"component": "view.personal-center_overtime-application",
|
||||||
|
"title": "加班申请",
|
||||||
|
"routeTitle": "personal-center_overtime-application",
|
||||||
|
"i18nKey": "route.personal-center_overtime-application",
|
||||||
|
"icon": "mdi:clock-plus-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 6,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "加班申请",
|
||||||
|
"i18nKey": "route.personal-center_overtime-application",
|
||||||
|
"icon": "mdi:clock-plus-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 6,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_pending-approval",
|
||||||
|
"path": "/personal-center/pending-approval",
|
||||||
|
"component": "view.personal-center_pending-approval",
|
||||||
|
"title": "待我审批",
|
||||||
|
"routeTitle": "personal-center_pending-approval",
|
||||||
|
"i18nKey": "route.personal-center_pending-approval",
|
||||||
|
"icon": "mdi:check-decagram-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 7,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "待我审批",
|
||||||
|
"i18nKey": "route.personal-center_pending-approval",
|
||||||
|
"icon": "mdi:check-decagram-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 7,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "system_user",
|
"name": "system_user",
|
||||||
"path": "/system/user",
|
"path": "/system/user",
|
||||||
@@ -271,6 +667,105 @@
|
|||||||
"parentName": "system",
|
"parentName": "system",
|
||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "infra_state-machine",
|
||||||
|
"path": "/infra/state-machine",
|
||||||
|
"component": "view.infra_state-machine",
|
||||||
|
"title": "状态机管理",
|
||||||
|
"routeTitle": "infra_state-machine",
|
||||||
|
"i18nKey": "route.infra_state-machine",
|
||||||
|
"icon": "mdi:state-machine",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "状态机管理",
|
||||||
|
"i18nKey": "route.infra_state-machine",
|
||||||
|
"icon": "mdi:state-machine",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "infra",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "infra_log-management",
|
||||||
|
"path": "/infra/log-management",
|
||||||
|
"component": "view.infra_log-management",
|
||||||
|
"title": "日志管理",
|
||||||
|
"routeTitle": "infra_log-management",
|
||||||
|
"i18nKey": "route.infra_log-management",
|
||||||
|
"icon": "mdi:text-box-search-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "日志管理",
|
||||||
|
"i18nKey": "route.infra_log-management",
|
||||||
|
"icon": "mdi:text-box-search-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "infra",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "infra_rd-code",
|
||||||
|
"path": "/infra/rd-code",
|
||||||
|
"component": "view.infra_rd-code",
|
||||||
|
"title": "研发令号",
|
||||||
|
"routeTitle": "infra_rd-code",
|
||||||
|
"i18nKey": "route.infra_rd-code",
|
||||||
|
"icon": "mdi:identifier",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "研发令号",
|
||||||
|
"i18nKey": "route.infra_rd-code",
|
||||||
|
"icon": "mdi:identifier",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "infra",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,292 +0,0 @@
|
|||||||
# 产品对象首页改版设计说明
|
|
||||||
|
|
||||||
日期:2026-04-23
|
|
||||||
|
|
||||||
## 1. 目标
|
|
||||||
|
|
||||||
本设计用于收敛 RDMS 产品对象上下文默认首页的改版方向。
|
|
||||||
|
|
||||||
本轮目标不是继续做“说明型占位页”,而是明确把当前 `/product/dashboard?objectId=...` 改成一个真正可用的产品对象首页:
|
|
||||||
|
|
||||||
- 第一眼先让用户知道当前看的是什么产品
|
|
||||||
- 第二眼能快速判断对象最近发生了什么
|
|
||||||
- 第三眼能看出需求池现在的经营状态和最近变化
|
|
||||||
- 底部为后续业务模块保留正式挂载位,而不是临时拼接入口
|
|
||||||
|
|
||||||
## 2. 已确认诉求
|
|
||||||
|
|
||||||
基于本轮对话,已确认以下用户诉求:
|
|
||||||
|
|
||||||
1. 首页顶部必须先展示产品基础概述,而不是先铺统计卡片
|
|
||||||
2. 基础概述至少包含:名称、编号、团队、产品经理等对象基础信息
|
|
||||||
3. 页面需要一块明显的时间线,用于承接产品对象与团队变更动态
|
|
||||||
4. 页面需要承接需求池管理情况,重点看总量、状态、待处理等统计信息
|
|
||||||
5. 需求相关事件不要混入对象时间线,应单独作为需求池最近变化区域
|
|
||||||
6. 快捷入口不要保留
|
|
||||||
7. 底部允许保留后续扩展区,重点预留给里程碑、风险点管理、产品资料等模块
|
|
||||||
8. 能接真实接口就接真实接口,当前没有稳定接口的区域允许先用假数据,但结构必须按正式首页来设计
|
|
||||||
|
|
||||||
## 3. 首页定位结论
|
|
||||||
|
|
||||||
本页定位不是:
|
|
||||||
|
|
||||||
- 纯报表看板
|
|
||||||
- 纯审计日志页
|
|
||||||
- 设置页搬运版
|
|
||||||
- 导航入口集合页
|
|
||||||
|
|
||||||
本页定位应当是:
|
|
||||||
|
|
||||||
- 产品对象首页
|
|
||||||
- 偏统计,也带审计
|
|
||||||
- 但页面主语始终是“当前产品对象”
|
|
||||||
|
|
||||||
换句话说,这个页面要同时回答三个问题:
|
|
||||||
|
|
||||||
1. 我现在看的是什么产品?
|
|
||||||
2. 这个产品对象最近发生了什么?
|
|
||||||
3. 这个产品的需求池现在处于什么状态?
|
|
||||||
|
|
||||||
## 4. 页面结构
|
|
||||||
|
|
||||||
### 4.1 桌面端结构
|
|
||||||
|
|
||||||
桌面端建议采用三层结构:
|
|
||||||
|
|
||||||
1. 顶部 `对象基础概述横幅`
|
|
||||||
2. 中部 `左时间线 + 右需求池双模块`
|
|
||||||
3. 底部 `扩展信息区`
|
|
||||||
|
|
||||||
推荐布局比例:
|
|
||||||
|
|
||||||
- 顶部横幅:`24 / 24`
|
|
||||||
- 中部主区:左 `16 / 24`,右 `8 / 24`
|
|
||||||
- 底部扩展区:`24 / 24`
|
|
||||||
|
|
||||||
中部左侧时间线高度应明显高于右侧任一单模块,形成首页主阅读区。
|
|
||||||
|
|
||||||
### 4.2 移动端结构
|
|
||||||
|
|
||||||
移动端统一退化为单列纵向布局,顺序为:
|
|
||||||
|
|
||||||
1. 对象基础概述横幅
|
|
||||||
2. 对象 / 团队动态时间线
|
|
||||||
3. 需求池管理概览
|
|
||||||
4. 需求池最近变化
|
|
||||||
5. 扩展信息区
|
|
||||||
|
|
||||||
移动端不强撑左右栏并排,不做卡片墙式压缩。
|
|
||||||
|
|
||||||
## 5. 模块设计
|
|
||||||
|
|
||||||
### 5.1 对象基础概述横幅
|
|
||||||
|
|
||||||
顶部采用“档案横幅型”,不采用纯指标卡片型。
|
|
||||||
|
|
||||||
横幅左侧承接对象身份信息:
|
|
||||||
|
|
||||||
- 产品名称
|
|
||||||
- 产品编号
|
|
||||||
- 当前状态标签
|
|
||||||
- 产品经理
|
|
||||||
- 团队规模
|
|
||||||
- 团队角色摘要
|
|
||||||
- 简短描述或备注
|
|
||||||
|
|
||||||
横幅右侧承接 4 个摘要指标:
|
|
||||||
|
|
||||||
- 团队人数
|
|
||||||
- 需求总量
|
|
||||||
- 待处理需求
|
|
||||||
- 最近动态时间
|
|
||||||
|
|
||||||
设计原则:
|
|
||||||
|
|
||||||
- 左侧负责建立对象识别
|
|
||||||
- 右侧负责快速判断当前概况
|
|
||||||
- 右侧指标只保留 4 项,不堆成报表卡片墙
|
|
||||||
|
|
||||||
### 5.2 对象 / 团队动态时间线
|
|
||||||
|
|
||||||
该区域位于中部左侧,是首页的主阅读区。
|
|
||||||
|
|
||||||
这条时间线只承接对象与团队变化,不承接需求事件。
|
|
||||||
|
|
||||||
第一版事件范围收敛为:
|
|
||||||
|
|
||||||
- 产品创建
|
|
||||||
- 产品状态变更
|
|
||||||
- 产品经理变更
|
|
||||||
- 成员加入
|
|
||||||
- 成员移出
|
|
||||||
- 成员角色调整
|
|
||||||
|
|
||||||
每条时间线建议展示:
|
|
||||||
|
|
||||||
- 事件标题
|
|
||||||
- 事件类型标签
|
|
||||||
- 发生时间
|
|
||||||
- 操作摘要
|
|
||||||
- 必要时展示原因或备注
|
|
||||||
|
|
||||||
表达目标是“业务时间线”,不是后台审计表格。
|
|
||||||
|
|
||||||
### 5.3 需求池管理概览
|
|
||||||
|
|
||||||
该区域位于中部右侧上半块,用于表达需求池的经营状态。
|
|
||||||
|
|
||||||
第一版首页需要优先看到的内容:
|
|
||||||
|
|
||||||
- 需求总量
|
|
||||||
- 各状态数量
|
|
||||||
- 待处理数量
|
|
||||||
- 高优先级待处理数量
|
|
||||||
|
|
||||||
展示方式建议为“摘要指标 + 状态分布列表”,不直接在首页展开完整需求表格。
|
|
||||||
|
|
||||||
这一块回答的是:
|
|
||||||
|
|
||||||
- 需求池是否健康
|
|
||||||
- 当前待处理压力大不大
|
|
||||||
- 是否存在需要优先关注的积压
|
|
||||||
|
|
||||||
### 5.4 需求池最近变化
|
|
||||||
|
|
||||||
该区域位于中部右侧下半块,与需求池管理概览上下分层,但属于同一侧栏语义。
|
|
||||||
|
|
||||||
该区域不重复展示总量,而是展示需求池最近发生的变化。
|
|
||||||
|
|
||||||
第一版建议承接:
|
|
||||||
|
|
||||||
- 最近新增需求
|
|
||||||
- 最近状态流转
|
|
||||||
- 最近关闭或完成
|
|
||||||
|
|
||||||
每条记录建议至少展示:
|
|
||||||
|
|
||||||
- 需求标题
|
|
||||||
- 动作类型
|
|
||||||
- 时间
|
|
||||||
- 当前状态或状态变更摘要
|
|
||||||
|
|
||||||
若当前没有真实数据,仍保留正式模块壳,不退化成“待开发”一句话。
|
|
||||||
|
|
||||||
### 5.5 扩展信息区
|
|
||||||
|
|
||||||
底部不再保留快捷入口,改为正式扩展信息区。
|
|
||||||
|
|
||||||
当前优先预留 3 类模块位:
|
|
||||||
|
|
||||||
- 里程碑
|
|
||||||
- 风险点管理
|
|
||||||
- 产品资料
|
|
||||||
|
|
||||||
这一层的作用是:
|
|
||||||
|
|
||||||
- 为后续对象级信息继续扩展留下稳定挂载位
|
|
||||||
- 不把中部主结构挤成信息大杂烩
|
|
||||||
- 避免为了未来模块提前做假导航入口
|
|
||||||
|
|
||||||
如果当前没有稳定接口,可先保留正式卡片结构与空态说明。
|
|
||||||
|
|
||||||
## 6. 数据策略
|
|
||||||
|
|
||||||
### 6.1 真实接口优先
|
|
||||||
|
|
||||||
当前首页优先消费现有真实接口:
|
|
||||||
|
|
||||||
- `fetchGetProduct`
|
|
||||||
- `fetchGetProductSettings`
|
|
||||||
- `fetchGetProductMembers`
|
|
||||||
|
|
||||||
这些接口足以支撑:
|
|
||||||
|
|
||||||
- 对象基础概述中的名称、编号、状态、产品经理、描述
|
|
||||||
- 团队人数与角色摘要
|
|
||||||
- 最近动态中的产品创建、状态变化、成员加入/移出
|
|
||||||
|
|
||||||
### 6.2 假数据使用边界
|
|
||||||
|
|
||||||
当前没有稳定真实接口的区域,允许先用假数据,但边界必须明确:
|
|
||||||
|
|
||||||
- 需求池管理概览
|
|
||||||
- 需求池最近变化
|
|
||||||
- 扩展信息区中的里程碑、风险点管理、产品资料摘要
|
|
||||||
|
|
||||||
假数据的使用原则:
|
|
||||||
|
|
||||||
1. 只补“当前没有稳定接口”的区域
|
|
||||||
2. 不反向污染对象基础信息
|
|
||||||
3. 不把假数据混入对象上下文 store
|
|
||||||
4. 数据源要集中放在概览页自己的 mock 模块中,方便后续替换
|
|
||||||
|
|
||||||
### 6.3 不推荐的做法
|
|
||||||
|
|
||||||
以下做法应避免:
|
|
||||||
|
|
||||||
- 把需求假数据散落写进页面组件
|
|
||||||
- 用对象 demo 数据冒充真实产品详情
|
|
||||||
- 把对象时间线和需求时间线混成一条
|
|
||||||
- 用快捷入口伪装成首页内容
|
|
||||||
|
|
||||||
## 7. 空态规则
|
|
||||||
|
|
||||||
首页至少要区分三种状态:
|
|
||||||
|
|
||||||
1. 能力未接入,只能先显示正式占位信息
|
|
||||||
2. 能力已接入,但当前该产品暂无业务数据
|
|
||||||
3. 当前用户无权限查看该模块
|
|
||||||
|
|
||||||
这三种状态不能共用一套模糊文案。
|
|
||||||
|
|
||||||
对需求池和扩展信息区,当前阶段更推荐“正式空态”而不是“待开发”。
|
|
||||||
|
|
||||||
## 8. 页面边界
|
|
||||||
|
|
||||||
首页明确不承接以下内容:
|
|
||||||
|
|
||||||
- 快捷入口导航区
|
|
||||||
- 完整团队成员表格
|
|
||||||
- 完整需求列表表格
|
|
||||||
- 设置页重表单
|
|
||||||
- 完整审计日志明细页
|
|
||||||
|
|
||||||
首页要做的是概述、判断与阅读,不是重操作页。
|
|
||||||
|
|
||||||
## 9. 实施建议
|
|
||||||
|
|
||||||
第一阶段建议先完成结构性改造:
|
|
||||||
|
|
||||||
1. 重做顶部横幅,建立对象档案感
|
|
||||||
2. 保留中部左高右双块结构
|
|
||||||
3. 用真实接口接通对象概述与对象 / 团队时间线
|
|
||||||
4. 用局部 mock 数据先接通需求池两块和底部扩展区
|
|
||||||
|
|
||||||
第二阶段再逐步替换需求池与扩展区数据源:
|
|
||||||
|
|
||||||
- 接真实需求池统计接口
|
|
||||||
- 接真实需求动态接口
|
|
||||||
- 接里程碑、风险点、产品资料摘要接口
|
|
||||||
|
|
||||||
## 10. 验证标准
|
|
||||||
|
|
||||||
本设计是否成立,可按以下标准判断:
|
|
||||||
|
|
||||||
1. 进入首页后,第一眼能认出当前产品对象
|
|
||||||
2. 用户能自然读到对象 / 团队最近发生了什么
|
|
||||||
3. 右侧能快速判断需求池当前压力与最近变化
|
|
||||||
4. 页面看起来像“对象首页”,而不是“普通后台卡片堆叠页”
|
|
||||||
5. 当前没有真实接口的区域也保留正式结构,不显得像临时占位
|
|
||||||
6. 后续新增里程碑、风险点管理、产品资料等能力时,不需要推翻整页结构
|
|
||||||
|
|
||||||
## 11. 本轮设计结论
|
|
||||||
|
|
||||||
本轮最终设计结论如下:
|
|
||||||
|
|
||||||
- 首页定位为“产品对象首页”,偏统计,也带审计,但不做纯报表页
|
|
||||||
- 顶部采用档案横幅型,先立住对象身份信息
|
|
||||||
- 中部左侧是高权重的对象 / 团队动态时间线
|
|
||||||
- 中部右侧拆为“需求池管理概览 + 需求池最近变化”上下两块
|
|
||||||
- 底部去掉快捷入口,改为正式扩展信息区
|
|
||||||
- 当前有真实接口的模块优先接真实接口
|
|
||||||
- 当前没有稳定接口的区域允许先用假数据,但必须隔离在概览页局部 mock 数据源中
|
|
||||||
28
package.json
28
package.json
@@ -37,9 +37,6 @@
|
|||||||
"update-pkg": "sa update-pkg"
|
"update-pkg": "sa update-pkg"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antv/data-set": "0.11.8",
|
|
||||||
"@antv/g2": "5.4.0",
|
|
||||||
"@antv/g6": "5.0.49",
|
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
"@iconify/vue": "5.0.0",
|
"@iconify/vue": "5.0.0",
|
||||||
"@sa/axios": "workspace:*",
|
"@sa/axios": "workspace:*",
|
||||||
@@ -47,50 +44,35 @@
|
|||||||
"@sa/hooks": "workspace:*",
|
"@sa/hooks": "workspace:*",
|
||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@visactor/vchart": "2.0.4",
|
"@univerjs/preset-sheets-core": "^0.25.0",
|
||||||
"@visactor/vchart-theme": "1.12.2",
|
"@univerjs/presets": "^0.25.0",
|
||||||
"@visactor/vtable-editors": "1.19.8",
|
|
||||||
"@visactor/vtable-gantt": "1.19.8",
|
|
||||||
"@visactor/vue-vtable": "1.19.8",
|
|
||||||
"@vueuse/components": "13.9.0",
|
|
||||||
"@vueuse/core": "13.9.0",
|
"@vueuse/core": "13.9.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"@zwight/luckyexcel": "^1.1.6",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"dayjs": "1.11.18",
|
"dayjs": "1.11.18",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"dhtmlx-gantt": "9.0.14",
|
|
||||||
"dompurify": "3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"echarts": "6.0.0",
|
"echarts": "6.0.0",
|
||||||
"element-plus": "^2.11.1",
|
"element-plus": "^2.11.1",
|
||||||
"jsbarcode": "3.12.1",
|
"grid-layout-plus": "^1.1.1",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "3.0.3",
|
"pinia": "3.0.3",
|
||||||
"pinyin-pro": "3.27.0",
|
|
||||||
"print-js": "1.6.0",
|
|
||||||
"swiper": "11.2.10",
|
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"typeit": "8.8.7",
|
|
||||||
"vditor": "3.11.2",
|
|
||||||
"vue": "3.5.20",
|
"vue": "3.5.20",
|
||||||
"vue-draggable-plus": "0.6.0",
|
"vue-draggable-plus": "0.6.0",
|
||||||
"vue-i18n": "11.1.11",
|
"vue-i18n": "11.1.11",
|
||||||
"vue-pdf-embed": "2.1.3",
|
"vue-router": "4.5.1"
|
||||||
"vue-router": "4.5.1",
|
|
||||||
"xgplayer": "3.0.23",
|
|
||||||
"xlsx": "0.18.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@amap/amap-jsapi-types": "0.0.15",
|
|
||||||
"@elegant-router/vue": "0.3.8",
|
"@elegant-router/vue": "0.3.8",
|
||||||
"@iconify/json": "2.2.380",
|
"@iconify/json": "2.2.380",
|
||||||
"@sa/scripts": "workspace:*",
|
"@sa/scripts": "workspace:*",
|
||||||
"@sa/uno-preset": "workspace:*",
|
"@sa/uno-preset": "workspace:*",
|
||||||
"@soybeanjs/eslint-config": "1.7.1",
|
"@soybeanjs/eslint-config": "1.7.1",
|
||||||
"@types/bmapgl": "0.0.7",
|
|
||||||
"@types/dompurify": "3.2.0",
|
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "24.3.0",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/eslint-config": "66.5.0",
|
"@unocss/eslint-config": "66.5.0",
|
||||||
|
|||||||
5960
pnpm-lock.yaml
generated
5960
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1018
src/components/custom/attendee-user-picker.vue
Normal file
1018
src/components/custom/attendee-user-picker.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||||
import { ArrowDown, Delete, Document, Loading, Picture, QuestionFilled, Upload } from '@element-plus/icons-vue';
|
import { ArrowDown, Delete, Document, Loading, Picture, QuestionFilled, Upload } from '@element-plus/icons-vue';
|
||||||
import { deleteFile, downloadFile, uploadFile } from '@/service/api/file';
|
import { buildFileProxyUrl, deleteFile, downloadFile, uploadFile } from '@/service/api/file';
|
||||||
|
|
||||||
defineOptions({ name: 'BusinessAttachmentUploader' });
|
defineOptions({ name: 'BusinessAttachmentUploader' });
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] });
|
const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] });
|
||||||
|
|
||||||
/** 给用户看的简短分类(hint 行展示) */
|
/** 给用户看的简短分类(hint 行展示) */
|
||||||
const ALLOWED_EXTENSIONS_HINT = '支持 PDF、Word、Excel、PPT、TXT/MD/CSV、图片、ZIP/RAR/7Z、MP3/MP4';
|
const ALLOWED_EXTENSIONS_HINT = '支持 PDF、Word、Excel、PPT、TXT/MD/CSV、图片、ZIP/RAR/7Z、MP3/MP4、SQL/JSON/XML';
|
||||||
|
|
||||||
// 与后端 AttachmentValidator 白/黑名单保持一致(5.16)
|
// 与后端 AttachmentValidator 白/黑名单保持一致(5.16)
|
||||||
const ALLOWED_EXTENSIONS = new Set([
|
const ALLOWED_EXTENSIONS = new Set([
|
||||||
@@ -55,7 +55,10 @@ const ALLOWED_EXTENSIONS = new Set([
|
|||||||
'rar',
|
'rar',
|
||||||
'7z',
|
'7z',
|
||||||
'mp4',
|
'mp4',
|
||||||
'mp3'
|
'mp3',
|
||||||
|
'sql',
|
||||||
|
'xml',
|
||||||
|
'json'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const FORBIDDEN_EXTENSIONS = new Set([
|
const FORBIDDEN_EXTENSIONS = new Set([
|
||||||
@@ -230,7 +233,7 @@ async function uploadOne(file: File) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, url } = result.data;
|
const { id, configId, path, url } = result.data;
|
||||||
|
|
||||||
// 组件已卸载(用户上传过程中关弹层):onBeforeUnmount 已跑过且看不到这个 id,
|
// 组件已卸载(用户上传过程中关弹层):onBeforeUnmount 已跑过且看不到这个 id,
|
||||||
// 这里立刻调删除,避免孤儿文件
|
// 这里立刻调删除,避免孤儿文件
|
||||||
@@ -248,7 +251,9 @@ async function uploadOne(file: File) {
|
|||||||
url,
|
url,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
contentType: file.type || undefined
|
contentType: file.type || undefined,
|
||||||
|
configId,
|
||||||
|
path
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
session.addedIds.add(id);
|
session.addedIds.add(id);
|
||||||
@@ -419,6 +424,16 @@ defineExpose({
|
|||||||
/** 父组件在提交前可读此值判断是否还有 pending 上传 */
|
/** 父组件在提交前可读此值判断是否还有 pending 上传 */
|
||||||
get hasUploading() {
|
get hasUploading() {
|
||||||
return hasUploading.value;
|
return hasUploading.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回当前已选附件的「永久代理 URL」数组(私有/公开桶都不过期)。
|
||||||
|
* 缺 configId/path 时回退 item.url。供反馈等"按 URL 字符串存储"的场景使用。
|
||||||
|
*/
|
||||||
|
getPermanentUrls(): string[] {
|
||||||
|
return model.value.map(item =>
|
||||||
|
item.configId && item.path ? buildFileProxyUrl(item.configId, item.path) : item.url
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -470,7 +485,7 @@ onBeforeUnmount(() => {
|
|||||||
</ElIcon>
|
</ElIcon>
|
||||||
<ElLink
|
<ElLink
|
||||||
type="primary"
|
type="primary"
|
||||||
:underline="false"
|
underline="never"
|
||||||
class="business-attachment-uploader__name"
|
class="business-attachment-uploader__name"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
@click="handleOpen(item)"
|
@click="handleOpen(item)"
|
||||||
@@ -478,7 +493,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</ElLink>
|
</ElLink>
|
||||||
<span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
|
<span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
|
||||||
<ElLink type="primary" :underline="false" @click="handleDownload(item)">下载</ElLink>
|
<ElLink type="primary" underline="never" @click="handleDownload(item)">下载</ElLink>
|
||||||
<ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
|
<ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -509,7 +524,7 @@ onBeforeUnmount(() => {
|
|||||||
</ElIcon>
|
</ElIcon>
|
||||||
<ElLink
|
<ElLink
|
||||||
type="primary"
|
type="primary"
|
||||||
:underline="false"
|
underline="never"
|
||||||
class="business-attachment-uploader__name"
|
class="business-attachment-uploader__name"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
@click="handleOpen(item)"
|
@click="handleOpen(item)"
|
||||||
@@ -517,7 +532,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</ElLink>
|
</ElLink>
|
||||||
<span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
|
<span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
|
||||||
<ElLink type="primary" :underline="false" @click="handleDownload(item)">下载</ElLink>
|
<ElLink type="primary" underline="never" @click="handleDownload(item)">下载</ElLink>
|
||||||
<ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
|
<ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ function handleConfirm() {
|
|||||||
footer-class="business-form-dialog__footer"
|
footer-class="business-form-dialog__footer"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
|
<template #header="{ close, titleId, titleClass }">
|
||||||
|
<slot name="title" :close="close" :title="props.title" :title-id="titleId" :title-class="titleClass">
|
||||||
|
<span :id="titleId" :class="titleClass">{{ props.title }}</span>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<ElScrollbar
|
<ElScrollbar
|
||||||
v-if="props.scrollbar"
|
v-if="props.scrollbar"
|
||||||
:max-height="props.maxBodyHeight"
|
:max-height="props.maxBodyHeight"
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ defineProps<Props>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="business-form-section">
|
<section class="business-form-section">
|
||||||
<h4 class="business-form-section__title">{{ title }}</h4>
|
<h4 class="business-form-section__title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</h4>
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import '@wangeditor/editor/dist/css/style.css';
|
|||||||
import { ElImageViewer } from 'element-plus';
|
import { ElImageViewer } from 'element-plus';
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||||
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
|
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
|
||||||
import { deleteFile, uploadFile } from '@/service/api/file';
|
import { buildFileProxyUrl, deleteFile, uploadFile } from '@/service/api/file';
|
||||||
|
|
||||||
defineOptions({ name: 'BusinessRichTextEditor' });
|
defineOptions({ name: 'BusinessRichTextEditor' });
|
||||||
|
|
||||||
@@ -178,7 +178,9 @@ const toolbarConfig: Partial<IToolbarConfig> = {
|
|||||||
'insertLink',
|
'insertLink',
|
||||||
'editLink',
|
'editLink',
|
||||||
'unLink',
|
'unLink',
|
||||||
'viewLink'
|
'viewLink',
|
||||||
|
// 全屏:弹层内全屏体验割裂,隐藏
|
||||||
|
'fullScreen'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,10 +200,12 @@ const editorConfig: Partial<IEditorConfig> = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, url } = result.data;
|
// 用永久代理路径塞 <img src>,不要用 result.data.url(24h 签名会过期)
|
||||||
|
const { id, configId, path } = result.data;
|
||||||
|
const proxyUrl = buildFileProxyUrl(configId, path);
|
||||||
// 记录 url -> fileId,后续 commit/rollback 才知道删哪个
|
// 记录 url -> fileId,后续 commit/rollback 才知道删哪个
|
||||||
session.uploadedMap.set(url, id);
|
session.uploadedMap.set(proxyUrl, id);
|
||||||
insertFn(url, file.name, url);
|
insertFn(proxyUrl, file.name, proxyUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const isEmpty = computed(() => !safeHtml.value || safeHtml.value.replace(/<[^>]+
|
|||||||
<template>
|
<template>
|
||||||
<div class="business-rich-text-view">
|
<div class="business-rich-text-view">
|
||||||
<span v-if="isEmpty" class="business-rich-text-view__empty">{{ props.emptyText }}</span>
|
<span v-if="isEmpty" class="business-rich-text-view__empty">{{ props.emptyText }}</span>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div v-else class="business-rich-text-view__content" v-html="safeHtml" />
|
<div v-else class="business-rich-text-view__content" v-html="safeHtml" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, h, ref } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { Component, PropType } from 'vue';
|
||||||
import { ElButton, ElPopover } from 'element-plus';
|
import { ElButton, ElPopover, ElTooltip } from 'element-plus';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
import IconMdiDotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||||
|
import IconMdiChevronDown from '~icons/mdi/chevron-down';
|
||||||
|
|
||||||
export type BusinessTableAction = {
|
export type BusinessTableAction = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
icon?: Component;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick: () => void | Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -17,12 +20,20 @@ export default defineComponent({
|
|||||||
actions: {
|
actions: {
|
||||||
type: Array as PropType<BusinessTableAction[]>,
|
type: Array as PropType<BusinessTableAction[]>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<'button' | 'icon'>,
|
||||||
|
default: 'button'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const popoverVisible = ref(false);
|
const popoverVisible = ref(false);
|
||||||
|
|
||||||
const directActions = computed(() => {
|
const directActions = computed(() => {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return props.actions;
|
||||||
|
}
|
||||||
|
|
||||||
if (props.actions.length <= 2) {
|
if (props.actions.length <= 2) {
|
||||||
return props.actions;
|
return props.actions;
|
||||||
}
|
}
|
||||||
@@ -31,6 +42,10 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const moreActions = computed(() => {
|
const moreActions = computed(() => {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (props.actions.length <= 2) {
|
if (props.actions.length <= 2) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -47,21 +62,86 @@ export default defineComponent({
|
|||||||
await action.onClick();
|
await action.onClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => (
|
function renderIcon(action: BusinessTableAction) {
|
||||||
<div class="business-table-action-cell" onClick={event => event.stopPropagation()}>
|
if (!action.icon) return null;
|
||||||
{directActions.value.map(action => (
|
|
||||||
|
return h(action.icon, { class: 'business-table-action-icon' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderButtonAction(action: BusinessTableAction) {
|
||||||
|
return (
|
||||||
|
<ElButton
|
||||||
|
key={action.key}
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-button"
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</ElButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIconAction(action: BusinessTableAction) {
|
||||||
|
return (
|
||||||
|
<ElTooltip key={action.key} content={action.label} placement="top">
|
||||||
<ElButton
|
<ElButton
|
||||||
key={action.key}
|
link
|
||||||
plain
|
|
||||||
size="small"
|
size="small"
|
||||||
type={action.buttonType}
|
type={action.buttonType}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
class="business-table-action-button"
|
class="business-table-action-icon-button"
|
||||||
|
aria-label={action.label}
|
||||||
onClick={() => handleAction(action)}
|
onClick={() => handleAction(action)}
|
||||||
>
|
>
|
||||||
{action.label}
|
{renderIcon(action)}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
))}
|
</ElTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMenuButton(action: BusinessTableAction) {
|
||||||
|
if (props.variant === 'icon') {
|
||||||
|
return (
|
||||||
|
<ElButton
|
||||||
|
key={action.key}
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-menu__link"
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
<span class="business-table-action-menu__item">
|
||||||
|
{renderIcon(action)}
|
||||||
|
<span>{action.label}</span>
|
||||||
|
</span>
|
||||||
|
</ElButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElButton
|
||||||
|
key={action.key}
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
type={action.buttonType}
|
||||||
|
disabled={action.disabled}
|
||||||
|
class="business-table-action-menu__button"
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</ElButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="business-table-action-cell" onClick={event => event.stopPropagation()}>
|
||||||
|
{directActions.value.map(action =>
|
||||||
|
props.variant === 'icon' ? renderIconAction(action) : renderButtonAction(action)
|
||||||
|
)}
|
||||||
|
|
||||||
{moreActions.value.length > 0 && (
|
{moreActions.value.length > 0 && (
|
||||||
<ElPopover
|
<ElPopover
|
||||||
@@ -74,32 +154,28 @@ export default defineComponent({
|
|||||||
{{
|
{{
|
||||||
reference: () => (
|
reference: () => (
|
||||||
<ElButton
|
<ElButton
|
||||||
plain
|
link={props.variant === 'icon'}
|
||||||
|
plain={props.variant !== 'icon'}
|
||||||
size="small"
|
size="small"
|
||||||
class="business-table-action-button"
|
class={
|
||||||
|
props.variant === 'icon' ? 'business-table-action-icon-button' : 'business-table-action-button'
|
||||||
|
}
|
||||||
|
aria-label={$t('common.more')}
|
||||||
onClick={event => event.stopPropagation()}
|
onClick={event => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center gap-4px">
|
{props.variant === 'icon' ? (
|
||||||
{$t('common.more')}
|
<IconMdiDotsHorizontal class="business-table-action-icon" />
|
||||||
<icon-mdi-chevron-down class="text-14px" />
|
) : (
|
||||||
</span>
|
<span class="inline-flex items-center gap-4px">
|
||||||
|
{$t('common.more')}
|
||||||
|
<IconMdiChevronDown class="text-14px" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
),
|
),
|
||||||
default: () => (
|
default: () => (
|
||||||
<div class="business-table-action-menu">
|
<div class="business-table-action-menu">
|
||||||
{moreActions.value.map(action => (
|
{moreActions.value.map(action => renderMenuButton(action))}
|
||||||
<ElButton
|
|
||||||
key={action.key}
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
type={action.buttonType}
|
|
||||||
disabled={action.disabled}
|
|
||||||
class="business-table-action-menu__button"
|
|
||||||
onClick={() => handleAction(action)}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</ElButton>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
938
src/components/custom/business-user-picker.vue
Normal file
938
src/components/custom/business-user-picker.vue
Normal file
@@ -0,0 +1,938 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useFormItem } from 'element-plus';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import { usePickerSelection } from './business-user-picker/composables/use-picker-selection';
|
||||||
|
import { useDeptSource } from './business-user-picker/composables/use-dept-source';
|
||||||
|
import { useChainSource } from './business-user-picker/composables/use-chain-source';
|
||||||
|
import UserPickerTrigger from './business-user-picker/components/user-picker-trigger.vue';
|
||||||
|
import IconEpOfficeBuilding from '~icons/ep/office-building';
|
||||||
|
import IconEpUser from '~icons/ep/user';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BusinessUserPicker' });
|
||||||
|
|
||||||
|
type Source = 'dept' | 'chain' | 'all';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userOptions: Api.SystemManage.UserSimple[];
|
||||||
|
sources?: Source[];
|
||||||
|
multiple?: boolean;
|
||||||
|
disabledUserIds?: readonly string[];
|
||||||
|
excludeUserIds?: readonly string[];
|
||||||
|
disabledLabel?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
title?: string;
|
||||||
|
dialogWidth?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
triggerSize?: 'default' | 'small' | 'large';
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
sources: () => ['dept', 'chain', 'all'],
|
||||||
|
multiple: false,
|
||||||
|
disabledUserIds: () => [],
|
||||||
|
excludeUserIds: () => [],
|
||||||
|
disabledLabel: '',
|
||||||
|
placeholder: '请选择用户',
|
||||||
|
title: '选择用户',
|
||||||
|
dialogWidth: '820px',
|
||||||
|
confirmText: '',
|
||||||
|
triggerSize: 'default',
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'change', value: string | string[] | null): void;
|
||||||
|
(e: 'confirm', payload: { userIds: string[] }): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
}
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const model = defineModel<string | string[] | null>({ default: null });
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const { formItem } = useFormItem();
|
||||||
|
|
||||||
|
const source = ref<Source>(props.sources[0] ?? 'all');
|
||||||
|
const currentNodeId = ref<string | null>(null);
|
||||||
|
const treeSearch = ref('');
|
||||||
|
const userSearch = ref('');
|
||||||
|
const hideAdded = ref(false);
|
||||||
|
|
||||||
|
const disabledUserIdSet = computed(() => new Set(props.disabledUserIds.map(String)));
|
||||||
|
const excludeUserIdSet = computed(() => new Set(props.excludeUserIds.map(String)));
|
||||||
|
|
||||||
|
const selection = usePickerSelection(() => ({ multiple: props.multiple }));
|
||||||
|
const deptSource = useDeptSource(
|
||||||
|
() => props.userOptions,
|
||||||
|
() => new Set(selection.selectedIds.value),
|
||||||
|
() => disabledUserIdSet.value
|
||||||
|
);
|
||||||
|
const chainSource = useChainSource(
|
||||||
|
() => new Set(selection.selectedIds.value),
|
||||||
|
() => disabledUserIdSet.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const showTabs = computed(() => props.sources.length > 1);
|
||||||
|
|
||||||
|
const userByIdMap = computed(() => new Map(props.userOptions.map(u => [String(u.id), u])));
|
||||||
|
|
||||||
|
const committedIds = computed<string[]>(() => {
|
||||||
|
const value = model.value;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(String);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string' && value) {
|
||||||
|
return [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedUsers = computed(() =>
|
||||||
|
committedIds.value.map(id => userByIdMap.value.get(id)).filter((u): u is Api.SystemManage.UserSimple => Boolean(u))
|
||||||
|
);
|
||||||
|
|
||||||
|
const lockedSelectedIds = computed(() => selection.selectedIds.value.filter(id => disabledUserIdSet.value.has(id)));
|
||||||
|
|
||||||
|
const visibleSelectedIds = computed(() => selection.selectedIds.value.slice(0, 4));
|
||||||
|
const overflowSelectedCount = computed(() => Math.max(0, selection.size.value - 4));
|
||||||
|
const overflowSelectedIds = computed(() => selection.selectedIds.value.slice(4));
|
||||||
|
const overflowPopoverVisible = ref(false);
|
||||||
|
const overflowReferenceEl = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
function handleOverflowOutsideClick(e: MouseEvent) {
|
||||||
|
if (!overflowPopoverVisible.value) return;
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
if (target.closest('.user-picker__overflow-popper')) return;
|
||||||
|
if (target.closest('.el-popper')) return;
|
||||||
|
if (overflowReferenceEl.value?.contains(target)) return;
|
||||||
|
overflowPopoverVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('mousedown', handleOverflowOutsideClick, true));
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('mousedown', handleOverflowOutsideClick, true));
|
||||||
|
|
||||||
|
function getUserById(uid: string) {
|
||||||
|
return userByIdMap.value.get(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleUserIds(): string[] {
|
||||||
|
let pool: string[];
|
||||||
|
if (source.value === 'all' || !currentNodeId.value) {
|
||||||
|
pool = props.userOptions.map(u => String(u.id));
|
||||||
|
} else if (source.value === 'dept') {
|
||||||
|
const node = deptSource.findNode(deptSource.tree.value, currentNodeId.value);
|
||||||
|
pool = node ? deptSource.getNodeUserIds(node) : props.userOptions.map(u => String(u.id));
|
||||||
|
} else {
|
||||||
|
const node = chainSource.findNode(chainSource.tree.value, currentNodeId.value);
|
||||||
|
pool = node ? chainSource.getNodeUserIds(node) : props.userOptions.map(u => String(u.id));
|
||||||
|
}
|
||||||
|
return pool.filter(id => !excludeUserIdSet.value.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredUserIds = computed(() => {
|
||||||
|
let ids = visibleUserIds();
|
||||||
|
if (hideAdded.value) ids = ids.filter(id => !disabledUserIdSet.value.has(id));
|
||||||
|
const kw = userSearch.value.trim().toLowerCase();
|
||||||
|
if (kw) {
|
||||||
|
ids = ids.filter(id => {
|
||||||
|
const u = getUserById(id);
|
||||||
|
if (!u) return false;
|
||||||
|
return (
|
||||||
|
u.nickname.toLowerCase().includes(kw) ||
|
||||||
|
(u.username ?? '').toLowerCase().includes(kw) ||
|
||||||
|
(u.deptName ?? '').toLowerCase().includes(kw)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function switchSource(next: Source) {
|
||||||
|
if (source.value === next) return;
|
||||||
|
source.value = next;
|
||||||
|
currentNodeId.value = null;
|
||||||
|
treeSearch.value = '';
|
||||||
|
if (next === 'dept') await deptSource.ensureLoaded();
|
||||||
|
else if (next === 'chain') await chainSource.ensureLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeptNodeClick(data: Api.SystemManage.DeptSimple) {
|
||||||
|
currentNodeId.value = deptSource.nodeKey(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChainNodeClick(data: Api.SystemManage.UserManagementRelationTreeRespVO) {
|
||||||
|
currentNodeId.value = chainSource.nodeKey(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDeptCheck(node: Api.SystemManage.DeptSimple) {
|
||||||
|
if (!props.multiple) return;
|
||||||
|
const ids = deptSource.getNodeUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
|
||||||
|
const state = deptSource.getNodeCheckState(node);
|
||||||
|
if (state === 'all') selection.removeMany(ids);
|
||||||
|
else selection.addMany(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleChainCheck(node: Api.SystemManage.UserManagementRelationTreeRespVO) {
|
||||||
|
if (!props.multiple) return;
|
||||||
|
const ids = chainSource.getNodeUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
|
||||||
|
const state = chainSource.getNodeCheckState(node);
|
||||||
|
if (state === 'all') selection.removeMany(ids);
|
||||||
|
else selection.addMany(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUser(uid: string) {
|
||||||
|
if (disabledUserIdSet.value.has(uid)) return;
|
||||||
|
selection.toggle(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
selection.clear(lockedSelectedIds.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUserFilter() {
|
||||||
|
userSearch.value = '';
|
||||||
|
hideAdded.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDisabled = computed(() => {
|
||||||
|
if (!props.multiple) return !selection.selectedIds.value.length;
|
||||||
|
return selection.size.value === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedConfirmText = computed(() => {
|
||||||
|
if (props.confirmText) return props.confirmText;
|
||||||
|
if (!props.multiple) return '确定';
|
||||||
|
return `确定(${selection.size.value})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
if (confirmDisabled.value) return;
|
||||||
|
const value = selection.commit();
|
||||||
|
model.value = value;
|
||||||
|
emit('change', value);
|
||||||
|
emit('confirm', { userIds: selection.selectedIds.value });
|
||||||
|
visible.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
formItem?.validate?.('change').catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog() {
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(visible, async value => {
|
||||||
|
if (value) {
|
||||||
|
treeSearch.value = '';
|
||||||
|
userSearch.value = '';
|
||||||
|
hideAdded.value = false;
|
||||||
|
currentNodeId.value = null;
|
||||||
|
source.value = props.sources[0] ?? 'all';
|
||||||
|
selection.reset(model.value);
|
||||||
|
if (source.value === 'dept') await deptSource.ensureLoaded();
|
||||||
|
else if (source.value === 'chain') await chainSource.ensureLoaded();
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="business-user-picker">
|
||||||
|
<slot name="trigger" :open="openDialog" :selected-users="selectedUsers" :disabled="disabled">
|
||||||
|
<UserPickerTrigger
|
||||||
|
:selected-users="selectedUsers"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:multiple="multiple"
|
||||||
|
:disabled="disabled"
|
||||||
|
:size="triggerSize"
|
||||||
|
@open="openDialog"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<BusinessFormDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="title"
|
||||||
|
preset="lg"
|
||||||
|
:width="dialogWidth"
|
||||||
|
max-body-height="540px"
|
||||||
|
:confirm-disabled="confirmDisabled"
|
||||||
|
:confirm-text="resolvedConfirmText"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div class="user-picker">
|
||||||
|
<div v-if="showTabs" class="user-picker__tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in sources"
|
||||||
|
:key="tab"
|
||||||
|
class="user-picker__tab"
|
||||||
|
:class="{ 'is-active': source === tab }"
|
||||||
|
type="button"
|
||||||
|
@click="switchSource(tab)"
|
||||||
|
>
|
||||||
|
{{ tab === 'dept' ? '部门' : tab === 'chain' ? '团队' : '全部用户' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-picker__picker" :class="{ 'is-single': source === 'all' }">
|
||||||
|
<div v-if="source !== 'all'" class="user-picker__col user-picker__col--tree">
|
||||||
|
<div class="user-picker__col-head">{{ source === 'dept' ? '部门' : '团队' }}</div>
|
||||||
|
<div class="user-picker__search">
|
||||||
|
<ElInput
|
||||||
|
v-model="treeSearch"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
:placeholder="source === 'dept' ? '搜索部门…' : '搜索成员…'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-loading="source === 'dept' ? deptSource.loading.value : chainSource.loading.value"
|
||||||
|
class="user-picker__col-body"
|
||||||
|
>
|
||||||
|
<ElTree
|
||||||
|
v-if="source === 'dept'"
|
||||||
|
:data="deptSource.filterByKeyword(treeSearch)"
|
||||||
|
:props="deptSource.treeProps.value"
|
||||||
|
node-key="id"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:indent="14"
|
||||||
|
class="user-picker__tree"
|
||||||
|
@node-click="handleDeptNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="user-picker__node" :class="{ 'is-active': currentNodeId === String(data.id) }">
|
||||||
|
<span
|
||||||
|
v-if="multiple"
|
||||||
|
class="user-picker__node-check"
|
||||||
|
:class="{
|
||||||
|
'is-checked': deptSource.getNodeCheckState(data) === 'all',
|
||||||
|
'is-partial': deptSource.getNodeCheckState(data) === 'partial'
|
||||||
|
}"
|
||||||
|
@click.stop="toggleDeptCheck(data)"
|
||||||
|
/>
|
||||||
|
<IconEpOfficeBuilding class="user-picker__node-icon" />
|
||||||
|
<span class="user-picker__node-label">{{ data.name }}</span>
|
||||||
|
<span v-if="deptSource.getMetaText(data)" class="user-picker__node-meta">
|
||||||
|
{{ deptSource.getMetaText(data) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
<ElTree
|
||||||
|
v-else
|
||||||
|
:data="chainSource.filterByKeyword(treeSearch)"
|
||||||
|
:props="chainSource.treeProps.value"
|
||||||
|
node-key="userId"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:indent="14"
|
||||||
|
class="user-picker__tree"
|
||||||
|
@node-click="handleChainNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="user-picker__node" :class="{ 'is-active': currentNodeId === chainSource.nodeKey(data) }">
|
||||||
|
<span
|
||||||
|
v-if="multiple"
|
||||||
|
class="user-picker__node-check"
|
||||||
|
:class="{
|
||||||
|
'is-checked': chainSource.getNodeCheckState(data) === 'all',
|
||||||
|
'is-partial': chainSource.getNodeCheckState(data) === 'partial'
|
||||||
|
}"
|
||||||
|
@click.stop="toggleChainCheck(data)"
|
||||||
|
/>
|
||||||
|
<IconEpUser class="user-picker__node-icon" />
|
||||||
|
<span class="user-picker__node-label">{{ data.userNickname }}</span>
|
||||||
|
<span v-if="chainSource.getMetaText(data)" class="user-picker__node-meta">
|
||||||
|
{{ chainSource.getMetaText(data) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-picker__col user-picker__col--users">
|
||||||
|
<div class="user-picker__col-head user-picker__col-head--user">
|
||||||
|
<span>
|
||||||
|
候选用户(
|
||||||
|
<span>{{ filteredUserIds.length }}</span>
|
||||||
|
人)
|
||||||
|
</span>
|
||||||
|
<label v-if="multiple" class="user-picker__hide-added">
|
||||||
|
<ElCheckbox v-model="hideAdded">隐藏已添加</ElCheckbox>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="user-picker__search">
|
||||||
|
<ElInput
|
||||||
|
v-model="userSearch"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
:placeholder="source === 'all' ? '搜索用户名 / 部门…' : '搜索用户名…'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="user-picker__col-body">
|
||||||
|
<div v-if="!filteredUserIds.length" class="user-picker__empty">
|
||||||
|
该节点下没有匹配用户
|
||||||
|
<button
|
||||||
|
v-if="userSearch || hideAdded"
|
||||||
|
type="button"
|
||||||
|
class="user-picker__link user-picker__empty-action"
|
||||||
|
@click="clearUserFilter"
|
||||||
|
>
|
||||||
|
清除筛选条件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="uid in filteredUserIds"
|
||||||
|
:key="uid"
|
||||||
|
class="user-picker__user-row"
|
||||||
|
:class="{
|
||||||
|
'is-disabled': disabledUserIdSet.has(uid),
|
||||||
|
'is-selected': !multiple && selection.has(uid)
|
||||||
|
}"
|
||||||
|
@click="toggleUser(uid)"
|
||||||
|
>
|
||||||
|
<span v-if="multiple" class="user-picker__node-check" :class="{ 'is-checked': selection.has(uid) }" />
|
||||||
|
<span class="user-picker__user-avatar">{{ (getUserById(uid)?.nickname ?? '?').slice(0, 1) }}</span>
|
||||||
|
<div class="user-picker__user-main">
|
||||||
|
<div class="user-picker__user-name">{{ getUserById(uid)?.nickname }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="disabledUserIdSet.has(uid) && disabledLabel" class="user-picker__user-tag">
|
||||||
|
{{ disabledLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="multiple" class="user-picker__selected">
|
||||||
|
<div class="user-picker__selected-head">
|
||||||
|
<span>
|
||||||
|
已选
|
||||||
|
<strong>{{ selection.size.value }}</strong>
|
||||||
|
人
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="selection.size.value > lockedSelectedIds.length"
|
||||||
|
type="button"
|
||||||
|
class="user-picker__link user-picker__link--danger"
|
||||||
|
@click="clearAll"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="selection.size.value === 0" class="user-picker__selected-empty">从左侧勾选用户后会出现在这里</div>
|
||||||
|
<div v-else class="user-picker__chips">
|
||||||
|
<span v-for="uid in visibleSelectedIds" :key="uid" class="user-picker__chip">
|
||||||
|
<span class="user-picker__chip-name">
|
||||||
|
{{ getUserById(uid)?.nickname }}
|
||||||
|
<ElTooltip v-if="disabledUserIdSet.has(uid) && disabledLabel" :content="disabledLabel" placement="top">
|
||||||
|
<span class="user-picker__chip-lock">·{{ disabledLabel }}</span>
|
||||||
|
</ElTooltip>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="!disabledUserIdSet.has(uid)"
|
||||||
|
type="button"
|
||||||
|
class="user-picker__chip-x"
|
||||||
|
@click="toggleUser(uid)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<ElPopover
|
||||||
|
v-if="overflowSelectedCount > 0"
|
||||||
|
:visible="overflowPopoverVisible"
|
||||||
|
placement="top-end"
|
||||||
|
:width="360"
|
||||||
|
popper-class="user-picker__overflow-popper"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<button
|
||||||
|
ref="overflowReferenceEl"
|
||||||
|
type="button"
|
||||||
|
class="user-picker__chip-more"
|
||||||
|
@click="overflowPopoverVisible = !overflowPopoverVisible"
|
||||||
|
>
|
||||||
|
+{{ overflowSelectedCount }} 更多
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div class="user-picker__overflow-head">
|
||||||
|
<span>
|
||||||
|
另外
|
||||||
|
<strong>{{ overflowSelectedCount }}</strong>
|
||||||
|
人
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-picker__overflow-chips">
|
||||||
|
<span v-for="uid in overflowSelectedIds" :key="uid" class="user-picker__chip">
|
||||||
|
<span class="user-picker__chip-name">
|
||||||
|
{{ getUserById(uid)?.nickname }}
|
||||||
|
<ElTooltip
|
||||||
|
v-if="disabledUserIdSet.has(uid) && disabledLabel"
|
||||||
|
:content="disabledLabel"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span class="user-picker__chip-lock">·{{ disabledLabel }}</span>
|
||||||
|
</ElTooltip>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="!disabledUserIdSet.has(uid)"
|
||||||
|
type="button"
|
||||||
|
class="user-picker__chip-x"
|
||||||
|
@click="toggleUser(uid)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ElPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.business-user-picker {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* picker 内容上下贴满,标准 body padding 显得空——仅在含本组件的 dialog 上收紧 */
|
||||||
|
:deep(.business-form-dialog__body:has(.user-picker)) {
|
||||||
|
padding-top: 8px !important;
|
||||||
|
padding-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tab {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tab.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
border-bottom-color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__picker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
height: min(280px, 44vh);
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__picker.is-single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__col-head {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
background: #fafbfc;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__col-head--user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__col-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__search {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tree {
|
||||||
|
padding: 4px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tree :deep(.el-tree-node__content) {
|
||||||
|
height: 32px;
|
||||||
|
padding-right: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tree :deep(.el-tree-node__content:hover) {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tree :deep(.el-tree-node__expand-icon) {
|
||||||
|
padding: 4px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__tree :deep(.el-tree-node__expand-icon.is-leaf) {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check.is-checked {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check.is-checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 4px;
|
||||||
|
width: 3px;
|
||||||
|
height: 7px;
|
||||||
|
border: solid #fff;
|
||||||
|
border-width: 0 1px 1px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check.is-partial {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-check.is-partial::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 2px;
|
||||||
|
margin: -1px 0 0 -4px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node.is-active .user-picker__node-icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node-meta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__node.is-active .user-picker__node-meta {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 36px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row:hover {
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row.is-disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row.is-disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row.is-selected {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-row.is-selected .user-picker__user-name {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #c7d2fe, #93c5fd);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__user-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--el-color-warning-light-7);
|
||||||
|
color: var(--el-color-warning-dark-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__empty {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__hide-added {
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__empty-action {
|
||||||
|
display: block;
|
||||||
|
margin: 6px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__selected {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__selected-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__selected-head strong {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__selected-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 26px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 4px 2px 8px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--el-border-color-darker);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-lock {
|
||||||
|
color: var(--el-color-warning-dark-2);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-x {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-x:hover {
|
||||||
|
background: var(--el-color-danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px dashed var(--el-border-color-darker);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 11.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__chip-more:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__overflow-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__overflow-head strong {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__overflow-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__link {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11.5px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__link--danger {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker__link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'UserPickerTrigger' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedUsers: Api.SystemManage.UserSimple[];
|
||||||
|
placeholder: string;
|
||||||
|
multiple: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
size: 'default' | 'small' | 'large';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{ (e: 'open'): void }>();
|
||||||
|
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (!props.selectedUsers.length) return '';
|
||||||
|
if (!props.multiple) return props.selectedUsers[0]?.nickname ?? '';
|
||||||
|
const head = props.selectedUsers
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(u => u.nickname)
|
||||||
|
.join('、');
|
||||||
|
const rest = props.selectedUsers.length - 2;
|
||||||
|
return rest > 0 ? `${head} +${rest}` : head;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeClass = computed(() => `is-${props.size}`);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.disabled) return;
|
||||||
|
emit('open');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="user-picker-trigger"
|
||||||
|
:class="[sizeClass, { 'is-disabled': disabled }]"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="handleClick"
|
||||||
|
@keydown.enter.prevent="handleClick"
|
||||||
|
@keydown.space.prevent="handleClick"
|
||||||
|
>
|
||||||
|
<span v-if="displayText" class="user-picker-trigger__text">{{ displayText }}</span>
|
||||||
|
<span v-else class="user-picker-trigger__placeholder">{{ placeholder }}</span>
|
||||||
|
<span class="user-picker-trigger__suffix">
|
||||||
|
<icon-ep:arrow-down />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-picker-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 30px 0 11px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
font-size: var(--el-font-size-base);
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger.is-small {
|
||||||
|
min-height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger.is-large {
|
||||||
|
min-height: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger:hover:not(.is-disabled) {
|
||||||
|
border-color: var(--el-border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger.is-disabled {
|
||||||
|
background: var(--el-disabled-bg-color);
|
||||||
|
color: var(--el-disabled-text-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger__placeholder {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-picker-trigger__suffix {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { fetchGetUserManagementRelationTree } from '@/service/api';
|
||||||
|
import type { TreeCheckState } from './use-dept-source';
|
||||||
|
|
||||||
|
type ChainNode = Api.SystemManage.UserManagementRelationTreeRespVO;
|
||||||
|
|
||||||
|
export function useChainSource(selectedIds: () => Set<string>, disabledUserIdSet: () => Set<string>) {
|
||||||
|
const tree = ref<ChainNode[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
async function ensureLoaded() {
|
||||||
|
if (loaded) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await fetchGetUserManagementRelationTree({ fromUserIndex: false });
|
||||||
|
tree.value = data ?? [];
|
||||||
|
loaded = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeKey(node: ChainNode): string {
|
||||||
|
return node.id ?? `chain_${node.userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeUserIds(node: ChainNode): string[] {
|
||||||
|
const ids = new Set<string>([String(node.userId)]);
|
||||||
|
if (node.children) {
|
||||||
|
for (const c of node.children) {
|
||||||
|
for (const id of getNodeUserIds(c)) ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeCheckState(node: ChainNode): TreeCheckState {
|
||||||
|
const ids = getNodeUserIds(node).filter(id => !disabledUserIdSet().has(id));
|
||||||
|
if (!ids.length) return 'none';
|
||||||
|
const sel = ids.filter(id => selectedIds().has(id)).length;
|
||||||
|
if (sel === 0) return 'none';
|
||||||
|
if (sel === ids.length) return 'all';
|
||||||
|
return 'partial';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNode(list: ChainNode[], key: string): ChainNode | null {
|
||||||
|
for (const n of list) {
|
||||||
|
if (nodeKey(n) === key) return n;
|
||||||
|
if (n.children) {
|
||||||
|
const r = findNode(n.children, key);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchKeyword(node: ChainNode, kw: string): boolean {
|
||||||
|
if (!kw) return true;
|
||||||
|
if (node.userNickname.toLowerCase().includes(kw)) return true;
|
||||||
|
if (node.children) return node.children.some(c => matchKeyword(c, kw));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByKeyword(kw: string) {
|
||||||
|
const lower = kw.trim().toLowerCase();
|
||||||
|
if (!lower) return tree.value;
|
||||||
|
return tree.value.filter(n => matchKeyword(n, lower));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMetaText(node: ChainNode): string {
|
||||||
|
const total = getNodeUserIds(node).length;
|
||||||
|
return total > 1 ? `${total} 人` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeProps = computed(() => ({ children: 'children', label: 'userNickname' }) as const);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tree,
|
||||||
|
loading,
|
||||||
|
treeProps,
|
||||||
|
ensureLoaded,
|
||||||
|
getNodeUserIds,
|
||||||
|
getNodeCheckState,
|
||||||
|
findNode,
|
||||||
|
filterByKeyword,
|
||||||
|
getMetaText,
|
||||||
|
nodeKey
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { fetchGetDeptSimpleList } from '@/service/api';
|
||||||
|
import { buildMenuTree } from '@/views/system/shared/menu-tree';
|
||||||
|
|
||||||
|
export type TreeCheckState = 'none' | 'partial' | 'all';
|
||||||
|
|
||||||
|
export function useDeptSource(
|
||||||
|
userOptions: () => Api.SystemManage.UserSimple[],
|
||||||
|
selectedIds: () => Set<string>,
|
||||||
|
disabledUserIdSet: () => Set<string>
|
||||||
|
) {
|
||||||
|
const tree = ref<Api.SystemManage.DeptSimple[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
async function ensureLoaded() {
|
||||||
|
if (loaded) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await fetchGetDeptSimpleList();
|
||||||
|
tree.value = data ? buildMenuTree(data) : [];
|
||||||
|
loaded = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDeptIds(node: Api.SystemManage.DeptSimple): string[] {
|
||||||
|
const ids: string[] = [String(node.id)];
|
||||||
|
if (node.children) {
|
||||||
|
for (const c of node.children) ids.push(...collectDeptIds(c));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeUserIds(node: Api.SystemManage.DeptSimple): string[] {
|
||||||
|
const deptIds = new Set(collectDeptIds(node));
|
||||||
|
return userOptions()
|
||||||
|
.filter(u => u.deptId !== null && u.deptId !== undefined && deptIds.has(String(u.deptId)))
|
||||||
|
.map(u => String(u.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeCheckState(node: Api.SystemManage.DeptSimple): TreeCheckState {
|
||||||
|
const ids = getNodeUserIds(node).filter(id => !disabledUserIdSet().has(id));
|
||||||
|
if (!ids.length) return 'none';
|
||||||
|
const sel = ids.filter(id => selectedIds().has(id)).length;
|
||||||
|
if (sel === 0) return 'none';
|
||||||
|
if (sel === ids.length) return 'all';
|
||||||
|
return 'partial';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNode(list: Api.SystemManage.DeptSimple[], key: string): Api.SystemManage.DeptSimple | null {
|
||||||
|
for (const n of list) {
|
||||||
|
if (String(n.id) === key) return n;
|
||||||
|
if (n.children) {
|
||||||
|
const r = findNode(n.children, key);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchKeyword(node: Api.SystemManage.DeptSimple, kw: string): boolean {
|
||||||
|
if (!kw) return true;
|
||||||
|
if (node.name.toLowerCase().includes(kw)) return true;
|
||||||
|
if (node.children) return node.children.some(c => matchKeyword(c, kw));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByKeyword(kw: string) {
|
||||||
|
const lower = kw.trim().toLowerCase();
|
||||||
|
if (!lower) return tree.value;
|
||||||
|
return tree.value.filter(n => matchKeyword(n, lower));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMetaText(node: Api.SystemManage.DeptSimple): string {
|
||||||
|
const total = getNodeUserIds(node).length;
|
||||||
|
return total > 0 ? `${total} 人` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeKey(node: Api.SystemManage.DeptSimple): string {
|
||||||
|
return String(node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeProps = computed(() => ({ children: 'children', label: 'name' }) as const);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tree,
|
||||||
|
loading,
|
||||||
|
treeProps,
|
||||||
|
ensureLoaded,
|
||||||
|
getNodeUserIds,
|
||||||
|
getNodeCheckState,
|
||||||
|
findNode,
|
||||||
|
filterByKeyword,
|
||||||
|
getMetaText,
|
||||||
|
nodeKey
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
export interface PickerSelectionOptions {
|
||||||
|
multiple: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePickerSelection(options: () => PickerSelectionOptions) {
|
||||||
|
const multiSet = ref<Set<string>>(new Set());
|
||||||
|
const singleId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const multiple = computed(() => options().multiple);
|
||||||
|
|
||||||
|
function has(userId: string): boolean {
|
||||||
|
if (multiple.value) return multiSet.value.has(userId);
|
||||||
|
return singleId.value === userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(userId: string) {
|
||||||
|
if (multiple.value) {
|
||||||
|
if (multiSet.value.has(userId)) multiSet.value.delete(userId);
|
||||||
|
else multiSet.value.add(userId);
|
||||||
|
multiSet.value = new Set(multiSet.value);
|
||||||
|
} else {
|
||||||
|
singleId.value = singleId.value === userId ? null : userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMany(userIds: readonly string[]) {
|
||||||
|
if (!multiple.value) {
|
||||||
|
singleId.value = userIds[0] ?? singleId.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const id of userIds) multiSet.value.add(id);
|
||||||
|
multiSet.value = new Set(multiSet.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMany(userIds: readonly string[]) {
|
||||||
|
if (!multiple.value) {
|
||||||
|
if (singleId.value && userIds.includes(singleId.value)) singleId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const id of userIds) multiSet.value.delete(id);
|
||||||
|
multiSet.value = new Set(multiSet.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(preserveIds?: readonly string[]) {
|
||||||
|
const keep = new Set((preserveIds ?? []).map(String));
|
||||||
|
if (multiple.value) {
|
||||||
|
const next = new Set<string>();
|
||||||
|
for (const id of multiSet.value) {
|
||||||
|
if (keep.has(id)) next.add(id);
|
||||||
|
}
|
||||||
|
multiSet.value = next;
|
||||||
|
} else if (singleId.value && !keep.has(singleId.value)) singleId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(initial: string | string[] | null | undefined) {
|
||||||
|
if (multiple.value) {
|
||||||
|
const ids = Array.isArray(initial) ? initial.map(String) : [];
|
||||||
|
multiSet.value = new Set(ids);
|
||||||
|
} else {
|
||||||
|
singleId.value = typeof initial === 'string' ? initial : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = computed<string[]>(() => {
|
||||||
|
if (multiple.value) return [...multiSet.value];
|
||||||
|
return singleId.value ? [singleId.value] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const size = computed(() => selectedIds.value.length);
|
||||||
|
|
||||||
|
function commit(): string | string[] | null {
|
||||||
|
if (multiple.value) return [...multiSet.value];
|
||||||
|
return singleId.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedIds,
|
||||||
|
size,
|
||||||
|
has,
|
||||||
|
toggle,
|
||||||
|
addMany,
|
||||||
|
removeMany,
|
||||||
|
clear,
|
||||||
|
reset,
|
||||||
|
commit
|
||||||
|
};
|
||||||
|
}
|
||||||
84
src/components/custom/current-user-role-tags.vue
Normal file
84
src/components/custom/current-user-role-tags.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { ElTag } from 'element-plus';
|
||||||
|
|
||||||
|
defineOptions({ name: 'CurrentUserRoleTags' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 当前登录用户在该对象(产品/项目)的角色;无角色为 []。后端只读计算字段,随登录身份变化 */
|
||||||
|
roles?: Api.Common.CurrentUserRole[] | null;
|
||||||
|
/** 无业务角色时的占位文案 */
|
||||||
|
emptyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { roles: () => [], emptyText: '--' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色族按 roleKey 后缀匹配。
|
||||||
|
*
|
||||||
|
* 后端 roleKey 为域前缀风格:product_manager / project_manager / product_creator /
|
||||||
|
* project_creator / *_observer 等(见 src/constants/business.ts 的经理 code);
|
||||||
|
* 文档示例里的裸 creator / implicit_observer 不是真实 key,故不能按字面量精确匹配。
|
||||||
|
*/
|
||||||
|
function isManagerRole(roleKey: string) {
|
||||||
|
return /manager$/i.test(roleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCreatorRole(roleKey: string) {
|
||||||
|
return /creator$/i.test(roleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统隐式角色:创建者 / 隐式观察者,弱化为淡灰标签 */
|
||||||
|
function isMuted(roleKey: string) {
|
||||||
|
return isCreatorRole(roleKey) || /observer$/i.test(roleKey) || roleKey.includes('implicit');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
const roles = props.roles ?? [];
|
||||||
|
// 当前用户是产品经理 / 项目经理时,隐藏创建者标签(隐式观察者不受影响)
|
||||||
|
const hasManager = roles.some(role => isManagerRole(role.roleKey));
|
||||||
|
const visibleRoles = hasManager ? roles.filter(role => !isCreatorRole(role.roleKey)) : roles;
|
||||||
|
|
||||||
|
return visibleRoles.map((role, index) => ({
|
||||||
|
key: `${role.roleKey}-${index}`,
|
||||||
|
roleName: role.roleName,
|
||||||
|
muted: isMuted(role.roleKey)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="items.length" class="current-user-role-tags">
|
||||||
|
<ElTag
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
round
|
||||||
|
:type="item.muted ? 'info' : 'primary'"
|
||||||
|
:class="{ 'current-user-role-tags__muted': item.muted }"
|
||||||
|
>
|
||||||
|
{{ item.roleName }}
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="current-user-role-tags__empty">{{ emptyText }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.current-user-role-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
// 列设置 align="center" 只影响文本流;flex 容器需显式居中标签
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐式角色(创建者 / 隐式观察者)弱化:在灰色 info 标签基础上再降透明度
|
||||||
|
.current-user-role-tags__muted {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-user-role-tags__empty {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
|
|
||||||
defineOptions({ name: 'DictSelect' });
|
defineOptions({ name: 'DictSelect' });
|
||||||
|
|
||||||
|
const ensuredEmptyDictCodes = new Set<string>();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dictCode: string;
|
dictCode: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -14,6 +17,8 @@ interface Props {
|
|||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
collapseTags?: boolean;
|
collapseTags?: boolean;
|
||||||
collapseTagsTooltip?: boolean;
|
collapseTagsTooltip?: boolean;
|
||||||
|
/** 下拉项右侧追加字典 remark 中文释义(优先级等需要"P0 → 紧急"对照的场景) */
|
||||||
|
showRemark?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -24,29 +29,53 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
onlyEnabled: true,
|
onlyEnabled: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
collapseTags: false,
|
collapseTags: false,
|
||||||
collapseTagsTooltip: false
|
collapseTagsTooltip: false,
|
||||||
|
showRemark: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const model = defineModel<string | number | Array<string | number> | null | undefined>({
|
const model = defineModel<string | number | Array<string | number> | null | undefined>({
|
||||||
default: undefined
|
default: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dictStore = useDictStore();
|
||||||
const { enabledDictData, dictData } = useDict(() => props.dictCode);
|
const { enabledDictData, dictData } = useDict(() => props.dictCode);
|
||||||
|
|
||||||
const dictOptions = computed(() => {
|
const dictOptions = computed(() => {
|
||||||
const source = props.onlyEnabled ? enabledDictData.value : dictData.value;
|
const source = props.onlyEnabled ? enabledDictData.value : dictData.value;
|
||||||
|
|
||||||
return source.map(item => ({
|
return source.map(item => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.value
|
value: item.value,
|
||||||
|
colorType: item.colorType ?? null,
|
||||||
|
remark: item.remark ?? null
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 单选时取当前选中项的 colorType,用于触发器 prefix 色块
|
||||||
|
const selectedColorType = computed<string | null>(() => {
|
||||||
|
if (props.multiple) return null;
|
||||||
|
const value = model.value;
|
||||||
|
if (value === null || value === undefined || value === '') return null;
|
||||||
|
return dictOptions.value.find(opt => opt.value === value)?.colorType ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.dictCode, dictOptions.value.length, dictStore.initialized, dictStore.loading] as const,
|
||||||
|
async ([dictCode, optionCount, initialized, loading]) => {
|
||||||
|
if (!dictCode || optionCount > 0 || !initialized || loading || ensuredEmptyDictCodes.has(dictCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensuredEmptyDictCodes.add(dictCode);
|
||||||
|
await dictStore.ensureDictData(dictCode, true);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="model"
|
v-model="model"
|
||||||
class="w-full"
|
class="dict-select w-full"
|
||||||
:placeholder="props.placeholder"
|
:placeholder="props.placeholder"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:clearable="props.clearable"
|
:clearable="props.clearable"
|
||||||
@@ -55,8 +84,51 @@ const dictOptions = computed(() => {
|
|||||||
:collapse-tags="props.collapseTags"
|
:collapse-tags="props.collapseTags"
|
||||||
:collapse-tags-tooltip="props.collapseTagsTooltip"
|
:collapse-tags-tooltip="props.collapseTagsTooltip"
|
||||||
>
|
>
|
||||||
<ElOption v-for="item in dictOptions" :key="item.value" :label="item.label" :value="item.value" />
|
<template v-if="selectedColorType" #prefix>
|
||||||
|
<span class="dict-select__color-dot" :style="{ background: selectedColorType }" />
|
||||||
|
</template>
|
||||||
|
<ElOption v-for="item in dictOptions" :key="item.value" :label="item.label" :value="item.value">
|
||||||
|
<span class="dict-select__option">
|
||||||
|
<span
|
||||||
|
v-if="item.colorType"
|
||||||
|
class="dict-select__color-dot dict-select__color-dot--option"
|
||||||
|
:style="{ background: item.colorType }"
|
||||||
|
/>
|
||||||
|
<span class="dict-select__option-label">{{ item.label }}</span>
|
||||||
|
<span v-if="props.showRemark && item.remark" class="dict-select__option-remark">{{ item.remark }}</span>
|
||||||
|
</span>
|
||||||
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.dict-select__color-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dict-select__color-dot--option {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dict-select__option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dict-select__option-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dict-select__option-remark {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import DictText from './dict-text.vue';
|
import DictText from './dict-text.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'DictTag' });
|
defineOptions({ name: 'DictTag' });
|
||||||
@@ -14,6 +16,7 @@ interface Props {
|
|||||||
fallback?: string;
|
fallback?: string;
|
||||||
separator?: string;
|
separator?: string;
|
||||||
onlyEnabled?: boolean;
|
onlyEnabled?: boolean;
|
||||||
|
/** 显式传入时优先;不传则按字典 item.colorType 自动取色 */
|
||||||
type?: DictTagType;
|
type?: DictTagType;
|
||||||
effect?: DictTagEffect;
|
effect?: DictTagEffect;
|
||||||
size?: DictTagSize;
|
size?: DictTagSize;
|
||||||
@@ -30,10 +33,54 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
size: 'default',
|
size: 'default',
|
||||||
round: false
|
round: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { getItem } = useDict(() => props.dictCode);
|
||||||
|
|
||||||
|
// 单值才支持自动取色;多值(数组)走默认渲染避免歧义
|
||||||
|
const autoColorType = computed<string | null>(() => {
|
||||||
|
if (Array.isArray(props.value)) return null;
|
||||||
|
if (props.value === null || props.value === undefined || props.value === '') return null;
|
||||||
|
return getItem(props.value, { onlyEnabled: props.onlyEnabled })?.colorType ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// props.type 优先(向后兼容);其次字典 colorType(hex);都没有时回落到原生 ElTag 默认
|
||||||
|
const hexColor = computed(() => (props.type ? null : autoColorType.value));
|
||||||
|
|
||||||
|
const tagStyle = computed<Record<string, string> | null>(() => {
|
||||||
|
if (!hexColor.value) return null;
|
||||||
|
// light 效果:浅底 + 主色字 + 中浅边;plain/dark 同样的色调思路,仅明度差异
|
||||||
|
const fg = hexColor.value;
|
||||||
|
if (props.effect === 'dark') {
|
||||||
|
return {
|
||||||
|
color: '#fff',
|
||||||
|
background: fg,
|
||||||
|
borderColor: fg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (props.effect === 'plain') {
|
||||||
|
return {
|
||||||
|
color: fg,
|
||||||
|
background: 'transparent',
|
||||||
|
borderColor: `color-mix(in srgb, ${fg} 50%, white)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// light(默认)
|
||||||
|
return {
|
||||||
|
color: fg,
|
||||||
|
background: `color-mix(in srgb, ${fg} 12%, white)`,
|
||||||
|
borderColor: `color-mix(in srgb, ${fg} 30%, white)`
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElTag :type="props.type" :effect="props.effect" :size="props.size" :round="props.round">
|
<ElTag
|
||||||
|
:type="props.type"
|
||||||
|
:effect="props.effect"
|
||||||
|
:size="props.size"
|
||||||
|
:round="props.round"
|
||||||
|
:style="tagStyle ?? undefined"
|
||||||
|
>
|
||||||
<DictText
|
<DictText
|
||||||
:dict-code="props.dictCode"
|
:dict-code="props.dictCode"
|
||||||
:value="props.value"
|
:value="props.value"
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'LookForward' });
|
defineOptions({ name: 'LookForward' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -10,7 +17,10 @@ defineOptions({ name: 'LookForward' });
|
|||||||
<SvgIcon local-icon="expectation" />
|
<SvgIcon local-icon="expectation" />
|
||||||
</div>
|
</div>
|
||||||
<slot>
|
<slot>
|
||||||
<h3 class="text-28px text-primary font-500">{{ $t('common.lookForward') }}</h3>
|
<h3 class="text-28px text-primary font-500">{{ title ?? $t('common.lookForward') }}</h3>
|
||||||
|
</slot>
|
||||||
|
<slot name="subtitle">
|
||||||
|
<p v-if="subtitle" class="text-14px text-base-text op-65">{{ subtitle }}</p>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
145
src/components/custom/subordinate-selector.vue
Normal file
145
src/components/custom/subordinate-selector.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SubordinateSelector' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean;
|
||||||
|
data?: Api.SystemManage.MySubordinateTreeNode | null;
|
||||||
|
emptyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
data: null,
|
||||||
|
emptyText: '暂无下属数据'
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedUserId = defineModel<string | null>('selectedUserId', {
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
function sortSubordinateNodes(
|
||||||
|
nodes: Api.SystemManage.MySubordinateTreeNode[] | null | undefined
|
||||||
|
): Api.SystemManage.MySubordinateTreeNode[] | null {
|
||||||
|
if (!nodes?.length) return null;
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
.map((node, index) => ({
|
||||||
|
...node,
|
||||||
|
children: sortSubordinateNodes(node.children),
|
||||||
|
originalIndex: index
|
||||||
|
}))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftHasChildren = (left.children?.length ?? 0) > 0 ? 1 : 0;
|
||||||
|
const rightHasChildren = (right.children?.length ?? 0) > 0 ? 1 : 0;
|
||||||
|
|
||||||
|
if (leftHasChildren !== rightHasChildren) {
|
||||||
|
return rightHasChildren - leftHasChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.originalIndex - right.originalIndex;
|
||||||
|
})
|
||||||
|
.map(({ originalIndex: _ignored, ...node }) => node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = computed<Api.SystemManage.MySubordinateTreeNode[] | null>(() => {
|
||||||
|
if (!props.data) return null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...props.data,
|
||||||
|
children: sortSubordinateNodes(props.data.children)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNodeClick(node: Api.SystemManage.MySubordinateTreeNode) {
|
||||||
|
selectedUserId.value = node.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodeLabel(node: Api.SystemManage.MySubordinateTreeNode) {
|
||||||
|
const label = node.isRoot ? '全部下属' : node.userNickname;
|
||||||
|
return `${label}${node.subordinateCount ? `(${node.subordinateCount})` : ''}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="subordinate-selector" body-class="subordinate-selector__body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<span class="text-14px font-600">团队成员</span>
|
||||||
|
<ElTag v-if="props.data" effect="plain">{{ props.data.subordinateCount }}</ElTag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="props.loading" class="subordinate-selector__content">
|
||||||
|
<ElEmpty v-if="!props.data" :image-size="88" :description="props.emptyText" />
|
||||||
|
<ElTree
|
||||||
|
v-else
|
||||||
|
:data="treeData || []"
|
||||||
|
node-key="userId"
|
||||||
|
:current-node-key="selectedUserId || undefined"
|
||||||
|
:props="{ label: 'userNickname', children: 'children' }"
|
||||||
|
highlight-current
|
||||||
|
:default-expanded-keys="[props.data.userId]"
|
||||||
|
expand-on-click-node
|
||||||
|
class="subordinate-selector__tree"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data: node }">
|
||||||
|
<span class="subordinate-selector__node-label">{{ renderNodeLabel(node) }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.subordinate-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__body) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__tree {
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__node-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node__content) {
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node__content:hover) {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node.is-current > .el-tree-node__content) {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
||||||
import DictSelect from './dict-select.vue';
|
import DictSelect from './dict-select.vue';
|
||||||
|
|
||||||
@@ -17,14 +18,34 @@ export interface SearchField {
|
|||||||
label: string;
|
label: string;
|
||||||
/** 字段类型 */
|
/** 字段类型 */
|
||||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
||||||
|
/** date 字段的日期粒度 */
|
||||||
|
dateType?: 'date' | 'month';
|
||||||
|
/** dateRange 字段的日期范围粒度 */
|
||||||
|
dateRangeType?: 'daterange' | 'monthrange';
|
||||||
|
/** 日期面板展示格式 */
|
||||||
|
format?: string;
|
||||||
|
/** 自定义范围分隔文案 */
|
||||||
|
rangeSeparator?: string;
|
||||||
|
/** 日期字段提交格式 */
|
||||||
|
valueFormat?: string;
|
||||||
/** 占位列数,默认 1 */
|
/** 占位列数,默认 1 */
|
||||||
span?: number;
|
span?: number;
|
||||||
/** select 类型的选项 */
|
/** select 类型的选项 */
|
||||||
options?: Option[];
|
options?: Option[];
|
||||||
/** dict 类型的字典编码 */
|
/** dict 类型的字典编码 */
|
||||||
dictCode?: string;
|
dictCode?: string;
|
||||||
|
/** dict 类型下拉项右侧追加字典 remark 释义(如优先级 "P0 → 紧急") */
|
||||||
|
showRemark?: boolean;
|
||||||
/** 占位提示文本 */
|
/** 占位提示文本 */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
/** select 类型的自定义选项渲染函数 */
|
||||||
|
renderOption?: (option: Option) => VNode | VNode[] | string;
|
||||||
|
/** select/dict 类型是否支持搜索 */
|
||||||
|
filterable?: boolean;
|
||||||
|
/** 值写回模型前的转换函数 */
|
||||||
|
transformValue?: (value: any) => any;
|
||||||
|
/** 从模型值解析展示值 */
|
||||||
|
resolveValue?: (value: any) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -111,6 +132,16 @@ function handleReset() {
|
|||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
emit('search');
|
emit('search');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFieldValue(field: SearchField, value: any) {
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
props.modelValue[field.key] = field.transformValue ? field.transformValue(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldValue(field: SearchField) {
|
||||||
|
const value = props.modelValue[field.key];
|
||||||
|
return field.resolveValue ? field.resolveValue(value) : value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
@@ -128,51 +159,61 @@ function handleSearch() {
|
|||||||
<ElFormItem :label="field.label">
|
<ElFormItem :label="field.label">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-if="field.type === 'input'"
|
v-if="field.type === 'input'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-else-if="field.type === 'select'"
|
v-else-if="field.type === 'select'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
>
|
>
|
||||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||||
|
<template v-if="field.renderOption" #default>
|
||||||
|
<component :is="field.renderOption(opt)" />
|
||||||
|
</template>
|
||||||
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
type="date"
|
:type="field.dateType || 'date'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:format="field.format"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'dateRange'"
|
v-else-if="field.type === 'dateRange'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
type="daterange"
|
:type="field.dateRangeType || 'daterange'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:format="field.format"
|
||||||
start-placeholder="开始日期"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
end-placeholder="结束日期"
|
:range-separator="field.rangeSeparator || '至'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
|
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<DictSelect
|
<DictSelect
|
||||||
v-else-if="field.type === 'dict'"
|
v-else-if="field.type === 'dict'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:dict-code="field.dictCode!"
|
:dict-code="field.dictCode!"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:show-remark="field.showRemark"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
@@ -220,51 +261,61 @@ function handleSearch() {
|
|||||||
<ElFormItem :label="field.label">
|
<ElFormItem :label="field.label">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-if="field.type === 'input'"
|
v-if="field.type === 'input'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-else-if="field.type === 'select'"
|
v-else-if="field.type === 'select'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
>
|
>
|
||||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||||
|
<template v-if="field.renderOption" #default>
|
||||||
|
<component :is="field.renderOption(opt)" />
|
||||||
|
</template>
|
||||||
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
type="date"
|
:type="field.dateType || 'date'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:format="field.format"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'dateRange'"
|
v-else-if="field.type === 'dateRange'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
type="daterange"
|
:type="field.dateRangeType || 'daterange'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:format="field.format"
|
||||||
start-placeholder="开始日期"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
end-placeholder="结束日期"
|
:range-separator="field.rangeSeparator || '至'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
|
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<DictSelect
|
<DictSelect
|
||||||
v-else-if="field.type === 'dict'"
|
v-else-if="field.type === 'dict'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="getFieldValue(field)"
|
||||||
:dict-code="field.dictCode!"
|
:dict-code="field.dictCode!"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
:show-remark="field.showRemark"
|
||||||
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
|||||||
163
src/components/custom/team-context-panel.vue
Normal file
163
src/components/custom/team-context-panel.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { TeamViewMode } from '@/views/personal-center/shared/team-dashboard';
|
||||||
|
|
||||||
|
defineOptions({ name: 'TeamContextPanel' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean;
|
||||||
|
selectedLabel?: string;
|
||||||
|
subordinateCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
selectedLabel: '',
|
||||||
|
subordinateCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = defineModel<TeamViewMode>('mode', {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopeOptions = computed(() => [
|
||||||
|
{ label: '个人视角', value: 'self' satisfies TeamViewMode },
|
||||||
|
{ label: '团队视角', value: 'team' satisfies TeamViewMode }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const contextText = computed(() => {
|
||||||
|
if (mode.value === 'self') {
|
||||||
|
return '当前查看我自己的数据。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectedLabel) {
|
||||||
|
return `当前范围:${props.selectedLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '当前查看团队数据。';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="team-context-panel" body-class="team-context-panel__body">
|
||||||
|
<div v-loading="props.loading" class="team-context-panel__layout">
|
||||||
|
<div class="team-context-panel__controls">
|
||||||
|
<ElSegmented v-model="mode" :options="scopeOptions" class="team-context-panel__segmented" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team-context-panel__info">
|
||||||
|
<div class="team-context-panel__info-main">
|
||||||
|
<div class="team-context-panel__info-item">
|
||||||
|
<span class="team-context-panel__info-label">当前范围</span>
|
||||||
|
<strong class="team-context-panel__info-value">
|
||||||
|
{{ props.selectedLabel || (mode === 'self' ? '我自己' : '--') }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'team'" class="team-context-panel__info-item">
|
||||||
|
<span class="team-context-panel__info-label">下属人数</span>
|
||||||
|
<strong class="team-context-panel__info-value">{{ props.subordinateCount }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="team-context-panel__info-desc">{{ contextText }}</p>
|
||||||
|
<div v-if="$slots.default" class="team-context-panel__summary">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.team-context-panel {
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__body) {
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__segmented) {
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__segmented .el-segmented__item) {
|
||||||
|
min-width: 96px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 22px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-desc {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__summary {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.team-context-panel__layout {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { getPaletteColorByNumber } from '@sa/color';
|
|
||||||
|
|
||||||
defineOptions({ name: 'WaveBg' });
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** Theme color */
|
|
||||||
themeColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
|
|
||||||
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="absolute-lt z-1 size-full overflow-hidden">
|
|
||||||
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
|
|
||||||
<svg height="1337" width="1337">
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
id="path-1"
|
|
||||||
opacity="1"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
|
|
||||||
/>
|
|
||||||
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
|
|
||||||
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
|
|
||||||
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g opacity="1">
|
|
||||||
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
|
|
||||||
<svg height="896" width="967.8852157128662">
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
id="path-2"
|
|
||||||
opacity="1"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
|
||||||
/>
|
|
||||||
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
|
|
||||||
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g opacity="1">
|
|
||||||
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -45,10 +45,14 @@ export const SYSTEM_USER_COMPANY_DICT_CODE = 'system_user_company';
|
|||||||
export const RDMS_REQ_SOURCE_TYPE_DICT_CODE = 'rdms_req_source_type';
|
export const RDMS_REQ_SOURCE_TYPE_DICT_CODE = 'rdms_req_source_type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需求优先级字典编码
|
* 优先级字典编码
|
||||||
*
|
*
|
||||||
* 对应业务字段:需求相关接口和页面中的 priority
|
* 对应业务字段:
|
||||||
* 来源口径:产品需求文档中定义,标签包括紧急、高、中、低
|
* - 需求(产品需求 / 项目需求)的 priority(旧口径:Integer,数字大=高,0=低 / 3=紧急)
|
||||||
|
* - 任务 / 执行的 priority(新口径:String "0"~"3",数字越小优先级越高,"1"=默认 P1)
|
||||||
|
*
|
||||||
|
* 来源口径:后端统一字典 rdms_req_priority,4 档标签 P0/P1/P2/P3。
|
||||||
|
* 数值取值口径不同是已知遗留——前端用本字典的 label / colorType 渲染即可,不要硬编码 P0~P3。
|
||||||
*/
|
*/
|
||||||
export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority';
|
export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority';
|
||||||
|
|
||||||
@@ -76,6 +80,22 @@ export const RDMS_PROJECT_TYPE_DICT_CODE = 'rdms_project_type';
|
|||||||
*/
|
*/
|
||||||
export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_type';
|
export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态机对象类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:状态机管理中的 objectType / 对象类型
|
||||||
|
* 来源口径:用户明确指定对象类型下拉来自运行时字典 object_status_model_object_type
|
||||||
|
*/
|
||||||
|
export const OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE = 'object_status_model_object_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务/个人事项类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:任务、个人事项中的 type
|
||||||
|
* 来源口径:用户明确指定任务/个人事项类型下拉来自运行时字典 rdms_task_item_type
|
||||||
|
*/
|
||||||
|
export const RDMS_TASK_ITEM_TYPE_DICT_CODE = 'rdms_task_item_type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需求允许删除的状态字典编码
|
* 需求允许删除的状态字典编码
|
||||||
*
|
*
|
||||||
@@ -83,3 +103,92 @@ export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_typ
|
|||||||
* 来源口径:用户在系统字典管理页中创建的字典 rdms_req_can_delete_status
|
* 来源口径:用户在系统字典管理页中创建的字典 rdms_req_can_delete_status
|
||||||
*/
|
*/
|
||||||
export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status';
|
export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作日志难度字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:任务/个人事项工作日志中的 difficulty
|
||||||
|
* 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task_item_worklog_difficulty
|
||||||
|
*/
|
||||||
|
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task_item_worklog_difficulty';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班时长快捷选项字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:加班申请中的 overtimeDuration
|
||||||
|
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
||||||
|
*/
|
||||||
|
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站内信消息等级字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:站内信 NotifyMessage.level(1=普通 2=提醒 3=警告 4=严重,数字越大越紧急)
|
||||||
|
* 来源口径:`2026-06-13-站内信消息等级-前端对接.html` 明确等级字典为 notify_message_level,
|
||||||
|
* 显示名与颜色(hex)均走字典,前端按 level 取色不硬编码。
|
||||||
|
*/
|
||||||
|
export const NOTIFY_MESSAGE_LEVEL_DICT_CODE = 'notify_message_level';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 意见反馈分类字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:意见反馈 type(1 缺陷 / 2 体验问题 / 3 功能建议)
|
||||||
|
* 来源口径:`2026-06-25-意见反馈-前端API.html` 明确分类字典为 feedback_type,后端已落库。
|
||||||
|
*/
|
||||||
|
export const FEEDBACK_TYPE_DICT_CODE = 'feedback_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 意见反馈处理状态字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:意见反馈 status(1 待处理 / 2 处理中 / 3 已处理 / 4 已忽略)
|
||||||
|
* 来源口径:同上,后端已落库。提交不传 status,后端强制「待处理」。
|
||||||
|
*/
|
||||||
|
export const FEEDBACK_STATUS_DICT_CODE = 'feedback_status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统用户类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:系统日志中的 userType
|
||||||
|
* 来源口径:后端 DictTypeConstants.USER_TYPE = user_type
|
||||||
|
*/
|
||||||
|
export const SYSTEM_USER_TYPE_DICT_CODE = 'user_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统登录日志类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:登录日志中的 logType
|
||||||
|
* 来源口径:后端 DictTypeConstants.LOGIN_TYPE = system_login_type
|
||||||
|
*/
|
||||||
|
export const SYSTEM_LOGIN_TYPE_DICT_CODE = 'system_login_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统登录结果字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:登录日志中的 result
|
||||||
|
* 来源口径:后端 DictTypeConstants.LOGIN_RESULT = system_login_result
|
||||||
|
*/
|
||||||
|
export const SYSTEM_LOGIN_RESULT_DICT_CODE = 'system_login_result';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础设施操作分类字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:API 访问日志中的 operateType
|
||||||
|
* 来源口径:后端 DictTypeConstants.OPERATE_TYPE = infra_operate_type
|
||||||
|
*/
|
||||||
|
export const INFRA_OPERATE_TYPE_DICT_CODE = 'infra_operate_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 错误日志处理状态字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:API 错误日志中的 processStatus
|
||||||
|
* 来源口径:后端 DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS = infra_api_error_log_process_status
|
||||||
|
*/
|
||||||
|
export const INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE = 'infra_api_error_log_process_status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统请求方式字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:操作日志中的 requestMethod
|
||||||
|
* 来源口径:用户明确指定请求方式下拉来自运行时字典 system_request_method
|
||||||
|
*/
|
||||||
|
export const SYSTEM_REQUEST_METHOD_DICT_CODE = 'system_request_method';
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/** baidu map sdk url */
|
|
||||||
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
|
|
||||||
|
|
||||||
/** Amap sdk url */
|
|
||||||
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
|
|
||||||
|
|
||||||
/** tencent sdk url */
|
|
||||||
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';
|
|
||||||
@@ -14,8 +14,14 @@ export type StatusDomain =
|
|||||||
| 'taskAssigneeMember'
|
| 'taskAssigneeMember'
|
||||||
| 'project'
|
| 'project'
|
||||||
| 'product'
|
| 'product'
|
||||||
| 'requirement'
|
| 'productRequirement'
|
||||||
| 'workOrder';
|
| 'projectRequirement'
|
||||||
|
| 'workOrder'
|
||||||
|
| 'workReport'
|
||||||
|
| 'performanceSheet'
|
||||||
|
| 'personalItem'
|
||||||
|
| 'overtimeApplication'
|
||||||
|
| 'feedback';
|
||||||
|
|
||||||
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
||||||
// 项目-执行
|
// 项目-执行
|
||||||
@@ -50,10 +56,67 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
|||||||
project: {},
|
project: {},
|
||||||
// 产品(待补全)
|
// 产品(待补全)
|
||||||
product: {},
|
product: {},
|
||||||
// 需求(待补全)
|
// 产品需求
|
||||||
requirement: {},
|
productRequirement: {
|
||||||
|
pending_claim: 'info',
|
||||||
|
pending_review: 'info',
|
||||||
|
pending_dispatch: 'primary',
|
||||||
|
reviewed: 'success',
|
||||||
|
review_rejected: 'danger',
|
||||||
|
implementing: 'primary',
|
||||||
|
accepted: 'success',
|
||||||
|
closed: 'danger',
|
||||||
|
rejected: 'danger',
|
||||||
|
cancelled: 'danger'
|
||||||
|
},
|
||||||
|
// 项目需求
|
||||||
|
projectRequirement: {
|
||||||
|
pending_claim: 'info',
|
||||||
|
pending_review: 'info',
|
||||||
|
reviewed: 'success',
|
||||||
|
review_rejected: 'danger',
|
||||||
|
implementing: 'primary',
|
||||||
|
accepted: 'success',
|
||||||
|
closed: 'danger',
|
||||||
|
rejected: 'danger',
|
||||||
|
cancelled: 'danger'
|
||||||
|
},
|
||||||
// 工单(待补全)
|
// 工单(待补全)
|
||||||
workOrder: {}
|
workOrder: {},
|
||||||
|
// 工作报告
|
||||||
|
workReport: {
|
||||||
|
draft: 'info',
|
||||||
|
pending_approval: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
rejected: 'danger'
|
||||||
|
},
|
||||||
|
// 绩效表
|
||||||
|
performanceSheet: {
|
||||||
|
draft: 'info',
|
||||||
|
sent: 'warning',
|
||||||
|
confirmed: 'success',
|
||||||
|
rejected: 'danger'
|
||||||
|
},
|
||||||
|
// 个人事项
|
||||||
|
personalItem: {
|
||||||
|
pending: 'info',
|
||||||
|
active: 'primary',
|
||||||
|
completed: 'success',
|
||||||
|
cancelled: 'danger'
|
||||||
|
},
|
||||||
|
// 加班申请
|
||||||
|
overtimeApplication: {
|
||||||
|
pending: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
rejected: 'danger'
|
||||||
|
},
|
||||||
|
// 意见反馈
|
||||||
|
feedback: {
|
||||||
|
'1': 'warning', // 待处理
|
||||||
|
'2': 'primary', // 处理中
|
||||||
|
'3': 'success', // 已处理
|
||||||
|
'4': 'info' // 已忽略
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getStatusTagType(domain: StatusDomain, statusCode: string | null | undefined): StatusTagType {
|
export function getStatusTagType(domain: StatusDomain, statusCode: string | null | undefined): StatusTagType {
|
||||||
@@ -61,5 +124,16 @@ export function getStatusTagType(domain: StatusDomain, statusCode: string | null
|
|||||||
return 'info';
|
return 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusTagTypeRegistry[domain][statusCode] || 'info';
|
return statusTagTypeRegistry[domain]?.[statusCode] || 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersonalItemStatusTagType(statusCode: string | null | undefined) {
|
||||||
|
return getStatusTagType('personalItem', statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeedbackStatusTagType(statusCode: string | number | null | undefined) {
|
||||||
|
return getStatusTagType(
|
||||||
|
'feedback',
|
||||||
|
statusCode === null || statusCode === undefined ? statusCode : String(statusCode)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export enum SetupStoreId {
|
|||||||
Dict = 'dict-store',
|
Dict = 'dict-store',
|
||||||
Route = 'route-store',
|
Route = 'route-store',
|
||||||
Tab = 'tab-store',
|
Tab = 'tab-store',
|
||||||
ObjectContext = 'object-context-store'
|
ObjectContext = 'object-context-store',
|
||||||
|
Workbench = 'workbench-store'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,12 +131,14 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
* @param callback callback function
|
* @param callback callback function
|
||||||
*/
|
*/
|
||||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||||
|
|
||||||
Object.assign(chartOptions, updatedOpts);
|
Object.assign(chartOptions, updatedOpts);
|
||||||
|
|
||||||
|
// 图表未初始化(容器尺寸未就绪)时只缓存最新 options,待 render() 初始化时一并应用;
|
||||||
|
// 否则数据先于初始化到达会被静默丢弃,首屏永远停留在空数据
|
||||||
|
if (!isRendered()) return;
|
||||||
|
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
chart?.clear();
|
chart?.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import { computed, effectScope, onScopeDispose, ref, watch } from 'vue';
|
|
||||||
import { useElementSize } from '@vueuse/core';
|
|
||||||
import VChart, { registerLiquidChart } from '@visactor/vchart';
|
|
||||||
import type { ISpec, ITheme } from '@visactor/vchart';
|
|
||||||
import light from '@visactor/vchart-theme/public/light.json';
|
|
||||||
import dark from '@visactor/vchart-theme/public/dark.json';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
|
||||||
|
|
||||||
registerLiquidChart();
|
|
||||||
|
|
||||||
// register the theme
|
|
||||||
VChart.ThemeManager.registerTheme('light', light as ITheme);
|
|
||||||
VChart.ThemeManager.registerTheme('dark', dark as ITheme);
|
|
||||||
|
|
||||||
interface ChartHooks {
|
|
||||||
onRender?: (chart: VChart) => void | Promise<void>;
|
|
||||||
onUpdated?: (chart: VChart) => void | Promise<void>;
|
|
||||||
onDestroy?: (chart: VChart) => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVChart<T extends ISpec>(specFactory: () => T, hooks: ChartHooks = {}) {
|
|
||||||
const scope = effectScope();
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
const darkMode = computed(() => themeStore.darkMode);
|
|
||||||
|
|
||||||
const domRef = ref<HTMLElement | null>(null);
|
|
||||||
const initialSize = { width: 0, height: 0 };
|
|
||||||
const { width, height } = useElementSize(domRef, initialSize);
|
|
||||||
|
|
||||||
let chart: VChart | null = null;
|
|
||||||
const spec: T = specFactory();
|
|
||||||
|
|
||||||
const { onRender, onUpdated, onDestroy } = hooks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether can render chart
|
|
||||||
*
|
|
||||||
* when domRef is ready and initialSize is valid
|
|
||||||
*/
|
|
||||||
function canRender() {
|
|
||||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** is chart rendered */
|
|
||||||
function isRendered() {
|
|
||||||
return Boolean(domRef.value && chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update chart spec
|
|
||||||
*
|
|
||||||
* @param callback callback function
|
|
||||||
*/
|
|
||||||
async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) {
|
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(spec, specFactory);
|
|
||||||
|
|
||||||
Object.assign(spec, updatedOpts);
|
|
||||||
|
|
||||||
// if (isRendered()) {
|
|
||||||
// chart?.release();
|
|
||||||
// }
|
|
||||||
|
|
||||||
chart?.updateSpec({ ...updatedOpts }, true);
|
|
||||||
|
|
||||||
await onUpdated?.(chart!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSpec(newSpec: T) {
|
|
||||||
chart?.updateSpec(newSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** render chart */
|
|
||||||
async function render() {
|
|
||||||
if (!isRendered()) {
|
|
||||||
// apply the theme
|
|
||||||
if (darkMode.value) {
|
|
||||||
VChart.ThemeManager.setCurrentTheme('dark');
|
|
||||||
} else {
|
|
||||||
VChart.ThemeManager.setCurrentTheme('light');
|
|
||||||
}
|
|
||||||
|
|
||||||
chart = new VChart(spec, { dom: domRef.value as HTMLElement });
|
|
||||||
chart.renderSync();
|
|
||||||
|
|
||||||
await onRender?.(chart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** resize chart */
|
|
||||||
function resize() {
|
|
||||||
// chart?.resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** destroy chart */
|
|
||||||
async function destroy() {
|
|
||||||
if (!chart) return;
|
|
||||||
|
|
||||||
await onDestroy?.(chart);
|
|
||||||
chart?.release();
|
|
||||||
chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** change chart theme */
|
|
||||||
async function changeTheme() {
|
|
||||||
await destroy();
|
|
||||||
await render();
|
|
||||||
await onUpdated?.(chart!);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* render chart by size
|
|
||||||
*
|
|
||||||
* @param w width
|
|
||||||
* @param h height
|
|
||||||
*/
|
|
||||||
async function renderChartBySize(w: number, h: number) {
|
|
||||||
initialSize.width = w;
|
|
||||||
initialSize.height = h;
|
|
||||||
|
|
||||||
// size is abnormal, destroy chart
|
|
||||||
if (!canRender()) {
|
|
||||||
await destroy();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize chart
|
|
||||||
if (isRendered()) {
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// render chart
|
|
||||||
await render();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.run(() => {
|
|
||||||
watch([width, height], ([newWidth, newHeight]) => {
|
|
||||||
renderChartBySize(newWidth, newHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(darkMode, () => {
|
|
||||||
changeTheme();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onScopeDispose(() => {
|
|
||||||
destroy();
|
|
||||||
scope.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
domRef,
|
|
||||||
updateSpec,
|
|
||||||
setSpec
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
|
||||||
|
import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict';
|
||||||
|
import {
|
||||||
|
fetchGetMyNotifyMessagePage,
|
||||||
|
fetchGetUnreadNotifyCount,
|
||||||
|
fetchUpdateAllNotifyMessageRead,
|
||||||
|
fetchUpdateNotifyMessageRead
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
|
import { formatDateTime, formatRelativeTime } from '@/utils/datetime';
|
||||||
|
|
||||||
|
defineOptions({ name: 'NotificationBell' });
|
||||||
|
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
|
type TabKey = 'unread' | 'read';
|
||||||
|
|
||||||
|
interface MessageListState {
|
||||||
|
items: Api.NotifyMessage.NotifyMessage[];
|
||||||
|
pageNo: number;
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
/** 是否已按当前关键字拉过第一页(tab 懒加载 / 失效重拉用) */
|
||||||
|
loaded: boolean;
|
||||||
|
/** 竞态令牌:重置后递增,过期响应直接丢弃 */
|
||||||
|
token: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createListState(): MessageListState {
|
||||||
|
return { items: [], pageNo: 1, total: 0, loading: false, loaded: false, token: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const listStates = reactive<Record<TabKey, MessageListState>>({
|
||||||
|
unread: createListState(),
|
||||||
|
read: createListState()
|
||||||
|
});
|
||||||
|
|
||||||
|
const unreadCount = ref(0);
|
||||||
|
const badgeLabel = computed(() => (unreadCount.value > 99 ? '99+' : String(unreadCount.value)));
|
||||||
|
|
||||||
|
const drawerOpen = ref(false);
|
||||||
|
const activeTab = ref<TabKey>('unread');
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null);
|
||||||
|
|
||||||
|
function keywordParam() {
|
||||||
|
return searchKeyword.value.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表圆点颜色:跟随消息等级(与等级徽标同一字典色源);取不到时回 undefined,由 CSS 兜底 */
|
||||||
|
function levelDotColor(level: number) {
|
||||||
|
return dictStore.getDictItem(NOTIFY_MESSAGE_LEVEL_DICT_CODE, level)?.colorType ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshUnreadCount() {
|
||||||
|
const { data, error } = await fetchGetUnreadNotifyCount();
|
||||||
|
if (!error && typeof data === 'number') {
|
||||||
|
unreadCount.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetList(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
state.token += 1;
|
||||||
|
state.items = [];
|
||||||
|
state.pageNo = 1;
|
||||||
|
state.total = 0;
|
||||||
|
state.loading = false;
|
||||||
|
state.loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPage(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
if (state.loading) return;
|
||||||
|
|
||||||
|
const token = state.token;
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
const { data, error } = await fetchGetMyNotifyMessagePage({
|
||||||
|
pageNo: state.pageNo,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
readStatus: tab === 'read',
|
||||||
|
keyword: keywordParam()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (token !== state.token) return;
|
||||||
|
|
||||||
|
state.loading = false;
|
||||||
|
state.loaded = true;
|
||||||
|
|
||||||
|
if (error || !data) return;
|
||||||
|
|
||||||
|
state.items.push(...data.list);
|
||||||
|
state.total = data.total;
|
||||||
|
state.pageNo += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMore(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
return state.loaded && state.items.length < state.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureLoaded(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
if (!state.loaded && !state.loading) {
|
||||||
|
loadPage(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyKeywordSearch = useDebounceFn(() => {
|
||||||
|
if (!drawerOpen.value) return;
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
watch(searchKeyword, () => {
|
||||||
|
applyKeywordSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(activeTab, tab => {
|
||||||
|
ensureLoaded(tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
type ScrollbarRefValue = { wrapRef?: HTMLElement } | null;
|
||||||
|
const unreadScrollbar = ref<ScrollbarRefValue>(null);
|
||||||
|
const readScrollbar = ref<ScrollbarRefValue>(null);
|
||||||
|
|
||||||
|
useInfiniteScroll(
|
||||||
|
() => unreadScrollbar.value?.wrapRef,
|
||||||
|
() => {
|
||||||
|
if (drawerOpen.value && hasMore('unread') && !listStates.unread.loading) {
|
||||||
|
loadPage('unread');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ distance: 48 }
|
||||||
|
);
|
||||||
|
|
||||||
|
useInfiniteScroll(
|
||||||
|
() => readScrollbar.value?.wrapRef,
|
||||||
|
() => {
|
||||||
|
if (drawerOpen.value && hasMore('read') && !listStates.read.loading) {
|
||||||
|
loadPage('read');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ distance: 48 }
|
||||||
|
);
|
||||||
|
|
||||||
|
function openDrawer() {
|
||||||
|
drawerOpen.value = true;
|
||||||
|
// 每次打开面板都从第 1 页重拉(与后端对齐的消费口径)
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
refreshUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
drawerOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrawerClosed() {
|
||||||
|
searchKeyword.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markRead(item: Api.NotifyMessage.NotifyMessage) {
|
||||||
|
const { error } = await fetchUpdateNotifyMessageRead([item.id]);
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
// 本地移除、不按原页号回拉,避免未读集合收缩导致的分页漂移
|
||||||
|
const state = listStates.unread;
|
||||||
|
const index = state.items.findIndex(row => row.id === item.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
state.items.splice(index, 1);
|
||||||
|
state.total = Math.max(0, state.total - 1);
|
||||||
|
}
|
||||||
|
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||||
|
|
||||||
|
// 已读列表失效,下次进入已读 tab 时从第 1 页重拉
|
||||||
|
resetList('read');
|
||||||
|
|
||||||
|
// 移除后剩余条目不足一页且还有更多时补拉,防止列表不再触发滚动加载
|
||||||
|
if (state.items.length < PAGE_SIZE && hasMore('unread')) {
|
||||||
|
loadPage('unread');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.NotifyMessage.NotifyMessage) {
|
||||||
|
// 弹框持有该行引用,正文不随未读列表移除而消失
|
||||||
|
detailMessage.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
// 未读消息「打开即已读」:后台静默标记,避免"看一半就跑到已读"
|
||||||
|
if (!row.readStatus) {
|
||||||
|
markRead(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAllRead() {
|
||||||
|
const { error } = await fetchUpdateAllNotifyMessageRead();
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
unreadCount.value = 0;
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 等级徽标颜色/文案走字典:若未在登录缓存内则按编码补拉一次(已缓存时不发请求)
|
||||||
|
dictStore.ensureDictData(NOTIFY_MESSAGE_LEVEL_DICT_CODE);
|
||||||
|
refreshUnreadCount();
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
if (document.hidden) return;
|
||||||
|
refreshUnreadCount();
|
||||||
|
}, UNREAD_COUNT_POLL_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="notification-bell__trigger"
|
||||||
|
type="button"
|
||||||
|
:aria-label="unreadCount > 0 ? `通知,${unreadCount} 条未读` : '通知'"
|
||||||
|
@click="openDrawer"
|
||||||
|
>
|
||||||
|
<SvgIcon icon="mdi:bell-outline" class="notification-bell__icon" />
|
||||||
|
<span v-if="unreadCount > 0" class="notification-bell__badge">{{ badgeLabel }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ElDrawer v-model="drawerOpen" size="480px" @closed="onDrawerClosed">
|
||||||
|
<template #header>
|
||||||
|
<div class="notification-bell__header-main">
|
||||||
|
<span class="notification-bell__title">
|
||||||
|
通知
|
||||||
|
<span v-if="unreadCount > 0" class="notification-bell__title-count">未读 {{ unreadCount }}</span>
|
||||||
|
</span>
|
||||||
|
<ElButton v-if="unreadCount > 0" link size="small" @click="markAllRead">全部已读</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="notification-bell__panel">
|
||||||
|
<div class="notification-bell__search">
|
||||||
|
<ElInput v-model="searchKeyword" placeholder="搜索通知" clearable>
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:magnify" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElTabs v-model="activeTab" class="notification-bell__tabs">
|
||||||
|
<ElTabPane name="unread">
|
||||||
|
<template #label>
|
||||||
|
<span class="notification-bell__tab-label">
|
||||||
|
未读
|
||||||
|
<span class="notification-bell__tab-count">{{ listStates.unread.total }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<ElScrollbar ref="unreadScrollbar" class="notification-bell__scroll">
|
||||||
|
<ul v-if="listStates.unread.items.length > 0" class="notification-bell__list">
|
||||||
|
<li
|
||||||
|
v-for="row in listStates.unread.items"
|
||||||
|
:key="row.id"
|
||||||
|
class="notification-bell__row is-unread"
|
||||||
|
@click="openDetail(row)"
|
||||||
|
>
|
||||||
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
|
<div class="notification-bell__row-body">
|
||||||
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="notification-bell__empty">
|
||||||
|
{{ listStates.unread.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无未读通知' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="listStates.unread.items.length > 0" class="notification-bell__footer-hint">
|
||||||
|
{{ listStates.unread.loading ? '加载中…' : hasMore('unread') ? '滚动加载更多…' : '— 已经到底了 —' }}
|
||||||
|
</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</ElTabPane>
|
||||||
|
|
||||||
|
<ElTabPane name="read">
|
||||||
|
<template #label>
|
||||||
|
<span class="notification-bell__tab-label">已读</span>
|
||||||
|
</template>
|
||||||
|
<ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
|
||||||
|
<ul v-if="listStates.read.items.length > 0" class="notification-bell__list">
|
||||||
|
<li
|
||||||
|
v-for="row in listStates.read.items"
|
||||||
|
:key="row.id"
|
||||||
|
class="notification-bell__row"
|
||||||
|
@click="openDetail(row)"
|
||||||
|
>
|
||||||
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
|
<div class="notification-bell__row-body">
|
||||||
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="notification-bell__empty">
|
||||||
|
{{ listStates.read.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无已读通知' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="listStates.read.items.length > 0" class="notification-bell__footer-hint">
|
||||||
|
{{ listStates.read.loading ? '加载中…' : hasMore('read') ? '滚动加载更多…' : '— 已经到底了 —' }}
|
||||||
|
</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="closeDrawer">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDrawer>
|
||||||
|
|
||||||
|
<ElDialog v-model="detailVisible" width="520px" align-center class="notification-bell__detail">
|
||||||
|
<template #header>
|
||||||
|
<div class="notification-bell__detail-head">
|
||||||
|
<span class="notification-bell__detail-sender">{{ detailMessage?.templateNickname || '系统通知' }}</span>
|
||||||
|
<DictTag
|
||||||
|
v-if="detailMessage"
|
||||||
|
:dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE"
|
||||||
|
:value="detailMessage.level"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="detailMessage" class="notification-bell__detail-body">
|
||||||
|
<div class="notification-bell__detail-content">{{ detailMessage.templateContent }}</div>
|
||||||
|
<div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="detailVisible = false">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-bell__trigger {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 4px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__trigger:hover {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__trigger:focus-visible {
|
||||||
|
outline: 2px solid var(--el-color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-color-danger);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
animation: notification-badge-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扩散波纹:跟随心跳节奏向外晕开,增强未读提醒的醒目度 */
|
||||||
|
.notification-bell__badge::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-color-danger);
|
||||||
|
animation: notification-badge-ping 1.6s ease-out infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-badge-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-badge-ping {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1.9);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__header-main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__title-count {
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-color-danger);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__search {
|
||||||
|
padding: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tabs :deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tabs :deep(.el-tab-pane) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tab-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tab-count {
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-fill-color);
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__tabs :deep(.el-tabs__item.is-active) .notification-bell__tab-count {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__scroll {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 14px minmax(0, 1fr);
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row + .notification-bell__row {
|
||||||
|
border-top: 1px dashed var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row:hover {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: transparent;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row.is-unread .notification-bell__row-dot {
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-title {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row.is-unread .notification-bell__row-title {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-time {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__empty {
|
||||||
|
padding: 48px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__footer-hint {
|
||||||
|
padding: 12px 0 4px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-sender {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-content {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-time {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,7 +18,7 @@ function loginOrRegister() {
|
|||||||
toLogin();
|
toLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropdownKey = 'user-center' | 'logout';
|
type DropdownKey = 'personal-center_my-profile' | 'logout';
|
||||||
|
|
||||||
type DropdownOption = {
|
type DropdownOption = {
|
||||||
key: DropdownKey;
|
key: DropdownKey;
|
||||||
@@ -29,8 +29,8 @@ type DropdownOption = {
|
|||||||
const options = computed(() => {
|
const options = computed(() => {
|
||||||
const opts: DropdownOption[] = [
|
const opts: DropdownOption[] = [
|
||||||
{
|
{
|
||||||
label: $t('common.userCenter'),
|
label: $t('common.myProfile'),
|
||||||
key: 'user-center',
|
key: 'personal-center_my-profile',
|
||||||
icon: SvgIconVNode({ icon: 'ph:user-circle', fontSize: 18 })
|
icon: SvgIconVNode({ icon: 'ph:user-circle', fontSize: 18 })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import GlobalLogo from '../global-logo/index.vue';
|
|||||||
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
||||||
import GlobalSearch from '../global-search/index.vue';
|
import GlobalSearch from '../global-search/index.vue';
|
||||||
import ThemeButton from './components/theme-button.vue';
|
import ThemeButton from './components/theme-button.vue';
|
||||||
|
import NotificationBell from './components/notification-bell.vue';
|
||||||
import UserAvatar from './components/user-avatar.vue';
|
import UserAvatar from './components/user-avatar.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'GlobalHeader' });
|
defineOptions({ name: 'GlobalHeader' });
|
||||||
@@ -40,14 +41,10 @@ const { isFullscreen, toggle } = useFullscreen();
|
|||||||
<div>
|
<div>
|
||||||
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
||||||
</div>
|
</div>
|
||||||
<ThemeSchemaSwitch
|
|
||||||
:theme-schema="themeStore.themeScheme"
|
|
||||||
:is-dark="themeStore.darkMode"
|
|
||||||
@switch="themeStore.toggleThemeScheme"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
</div>
|
</div>
|
||||||
|
<NotificationBell />
|
||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
</div>
|
</div>
|
||||||
</DarkModeContainer>
|
</DarkModeContainer>
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { Search } from '@element-plus/icons-vue';
|
||||||
|
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||||
|
import { fetchGetProductPage, fetchGetProjectPage } from '@/service/api';
|
||||||
|
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ObjectContextSwitcher' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
domainConfig: App.ObjectContext.DomainConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectOption = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const objectContextStore = useObjectContextStore();
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const keyword = ref('');
|
||||||
|
const expanded = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const switchingId = ref('');
|
||||||
|
const options = ref<ObjectOption[]>([]);
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const OBJECT_SWITCHER_PAGE_SIZE = 100;
|
||||||
|
|
||||||
|
const isProductDomain = computed(() => props.domainConfig.domainKey === 'product');
|
||||||
|
const domainLabel = computed(() => (isProductDomain.value ? '产品' : '项目'));
|
||||||
|
const allLabel = computed(() => `全部${domainLabel.value}`);
|
||||||
|
const placeholder = computed(() => `搜索${domainLabel.value}`);
|
||||||
|
const previewOptions = computed(() => options.value.slice(0, 3));
|
||||||
|
const displayOptions = computed(() => {
|
||||||
|
if (keyword.value.trim() || expanded.value) {
|
||||||
|
return options.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previewOptions.value;
|
||||||
|
});
|
||||||
|
const hiddenCount = computed(() => Math.max(options.value.length - previewOptions.value.length, 0));
|
||||||
|
const showAllEntry = computed(() => !keyword.value.trim() && !expanded.value && hiddenCount.value > 0);
|
||||||
|
|
||||||
|
function sortByCreateTimeDesc(list: ObjectOption[]) {
|
||||||
|
return list.slice().sort((left, right) => {
|
||||||
|
const leftTime = left.createTime ? new Date(left.createTime).getTime() : 0;
|
||||||
|
const rightTime = right.createTime ? new Date(right.createTime).getTime() : 0;
|
||||||
|
|
||||||
|
return rightTime - leftTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchObjectOptionsPage(pageNo: number, keywordValue?: string) {
|
||||||
|
const result =
|
||||||
|
props.domainConfig.domainKey === 'product'
|
||||||
|
? await fetchGetProductPage({ pageNo, pageSize: OBJECT_SWITCHER_PAGE_SIZE, keyword: keywordValue })
|
||||||
|
: await fetchGetProjectPage({ pageNo, pageSize: OBJECT_SWITCHER_PAGE_SIZE, keyword: keywordValue });
|
||||||
|
|
||||||
|
if (result.error || !result.data) {
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = result.data.list.map(item => {
|
||||||
|
if (props.domainConfig.domainKey === 'product') {
|
||||||
|
const product = item as Api.Product.Product;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
code: product.code,
|
||||||
|
createTime: product.createTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = item as Api.Project.Project;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: project.id,
|
||||||
|
name: project.projectName,
|
||||||
|
code: project.projectCode,
|
||||||
|
createTime: project.createTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: result.data.total,
|
||||||
|
list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const keywordValue = keyword.value.trim() || undefined;
|
||||||
|
const firstPage = await fetchObjectOptionsPage(1, keywordValue);
|
||||||
|
const pageCount = Math.ceil(firstPage.total / OBJECT_SWITCHER_PAGE_SIZE);
|
||||||
|
const restPages =
|
||||||
|
pageCount > 1
|
||||||
|
? await Promise.all(
|
||||||
|
Array.from({ length: pageCount - 1 }, (_, index) => fetchObjectOptionsPage(index + 2, keywordValue))
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const list = [firstPage, ...restPages].flatMap(page => page.list);
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
options.value = sortByCreateTimeDesc(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibleChange(value: boolean) {
|
||||||
|
visible.value = value;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
expanded.value = false;
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelect(option: ObjectOption) {
|
||||||
|
if (option.id === objectContextStore.objectId) {
|
||||||
|
visible.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchingId.value = option.id;
|
||||||
|
const result = await objectContextStore.switchContext(props.domainConfig, option.id);
|
||||||
|
switchingId.value = '';
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
const query = {
|
||||||
|
...route.query,
|
||||||
|
[OBJECT_CONTEXT_QUERY_KEY]: option.id
|
||||||
|
};
|
||||||
|
const targetLocation = route.name ? { name: route.name, query } : { path: route.path, query };
|
||||||
|
|
||||||
|
await router.push(targetLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => keyword.value,
|
||||||
|
() => {
|
||||||
|
if (!visible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded.value = Boolean(keyword.value.trim());
|
||||||
|
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
loadOptions();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElPopover
|
||||||
|
:visible="visible"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-start"
|
||||||
|
:width="300"
|
||||||
|
popper-class="object-context-switcher__popper"
|
||||||
|
@update:visible="handleVisibleChange"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<button type="button" class="object-context-switcher__trigger" :class="{ 'is-open': visible }">
|
||||||
|
<span class="object-context-switcher__trigger-label">{{ objectContextStore.objectName }}</span>
|
||||||
|
<icon-ep:sort class="object-context-switcher__trigger-icon" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="object-context-switcher__panel">
|
||||||
|
<ElInput v-model="keyword" clearable :placeholder="placeholder" class="object-context-switcher__search">
|
||||||
|
<template #suffix>
|
||||||
|
<ElIcon>
|
||||||
|
<Search />
|
||||||
|
</ElIcon>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="object-context-switcher__list">
|
||||||
|
<button
|
||||||
|
v-for="item in displayOptions"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="object-context-switcher__item"
|
||||||
|
:class="{ 'is-active': item.id === objectContextStore.objectId }"
|
||||||
|
:disabled="switchingId === item.id"
|
||||||
|
@click="handleSelect(item)"
|
||||||
|
>
|
||||||
|
<span class="object-context-switcher__item-icon">
|
||||||
|
<icon-ep:box v-if="isProductDomain" />
|
||||||
|
<icon-ep:folder v-else />
|
||||||
|
</span>
|
||||||
|
<span class="object-context-switcher__item-main">
|
||||||
|
<span class="object-context-switcher__item-name">{{ item.name }}</span>
|
||||||
|
<span v-if="item.code" class="object-context-switcher__item-code">{{ item.code }}</span>
|
||||||
|
</span>
|
||||||
|
<icon-ep:check v-if="item.id === objectContextStore.objectId" class="object-context-switcher__check" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ElEmpty v-if="!loading && !displayOptions.length" :description="`暂无可选${domainLabel}`" :image-size="54" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="showAllEntry" type="button" class="object-context-switcher__all" @click="expanded = true">
|
||||||
|
<span>{{ allLabel }}</span>
|
||||||
|
<span class="object-context-switcher__all-meta">{{ hiddenCount }} 个更多</span>
|
||||||
|
<icon-ep:arrow-right class="object-context-switcher__all-arrow" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ElPopover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.object-context-switcher__trigger {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 16rem;
|
||||||
|
height: 32px;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__trigger:hover,
|
||||||
|
.object-context-switcher__trigger.is-open {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__trigger-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__trigger-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__search {
|
||||||
|
padding: 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__list {
|
||||||
|
min-height: 84px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item:hover,
|
||||||
|
.object-context-switcher__item.is-active {
|
||||||
|
background: rgb(59 130 246 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--el-color-primary-light-8);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item-name,
|
||||||
|
.object-context-switcher__item-code {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__item-code {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__all {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% + 24px);
|
||||||
|
height: 38px;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 -12px -12px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__all:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__all-meta {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-context-switcher__all-arrow {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.object-context-switcher__popper.el-popover) {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgb(226 232 240 / 90%);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow:
|
||||||
|
0 12px 28px rgb(15 23 42 / 10%),
|
||||||
|
0 2px 8px rgb(15 23 42 / 6%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@ const { selectedKeyDummy, handleSelect } = useMenu();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useObjectContextStore } from '@/store/modules/object-context';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
|
import ObjectContextSwitcher from '../components/object-context-switcher.vue';
|
||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../../../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -92,7 +93,8 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<!-- defer:BaseLayout 二次挂载时 GlobalMenu 已缓存为同步挂载,目标 div 还未插入 document,不延迟解析会静默失败且不重试 -->
|
||||||
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
||||||
<button
|
<button
|
||||||
v-if="activeFirstLevelMenu"
|
v-if="activeFirstLevelMenu"
|
||||||
@@ -108,7 +110,7 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"
|
class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"
|
||||||
></div>
|
></div>
|
||||||
<div v-if="showObjectContextInfo" class="context-object-tag h-full flex-y-center">
|
<div v-if="showObjectContextInfo" class="context-object-tag h-full flex-y-center">
|
||||||
<span class="context-object-tag__label">{{ objectContextStore.objectName }}</span>
|
<ObjectContextSwitcher v-if="currentObjectContextDomain" :domain-config="currentObjectContextDomain" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showObjectContextInfo && headerMenus.length"
|
v-if="showObjectContextInfo && headerMenus.length"
|
||||||
@@ -160,7 +162,7 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
@@ -208,28 +210,6 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-object-tag {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-object-tag__label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 14rem;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid rgb(148 163 184 / 26%);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(180deg, rgb(248 250 252 / 95%), rgb(241 245 249 / 92%));
|
|
||||||
color: rgb(15 23 42 / 88%);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-nav-list {
|
.header-nav-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -66,7 +66,7 @@ watch(
|
|||||||
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { themeSchemaRecord } from '@/constants/app';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import SettingItem from '../components/setting-item.vue';
|
import SettingItem from '../components/setting-item.vue';
|
||||||
@@ -9,16 +8,6 @@ defineOptions({ name: 'DarkMode' });
|
|||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const icons: Record<UnionKey.ThemeScheme, string> = {
|
|
||||||
light: 'material-symbols:sunny',
|
|
||||||
dark: 'material-symbols:nightlight-rounded',
|
|
||||||
auto: 'material-symbols:hdr-auto'
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSegmentChange(value: string | number) {
|
|
||||||
themeStore.setThemeScheme(value as UnionKey.ThemeScheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGrayscaleChange(value: boolean) {
|
function handleGrayscaleChange(value: boolean) {
|
||||||
themeStore.setGrayscale(value);
|
themeStore.setGrayscale(value);
|
||||||
}
|
}
|
||||||
@@ -33,15 +22,6 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
|||||||
<template>
|
<template>
|
||||||
<ElDivider>{{ $t('theme.themeSchema.title') }}</ElDivider>
|
<ElDivider>{{ $t('theme.themeSchema.title') }}</ElDivider>
|
||||||
<div class="flex-col-stretch gap-16px">
|
<div class="flex-col-stretch gap-16px">
|
||||||
<div class="i-flex-center">
|
|
||||||
<ElTabs v-model="themeStore.themeScheme" type="border-card" class="segment" @tab-change="handleSegmentChange">
|
|
||||||
<ElTabPane v-for="(_, key) in themeSchemaRecord" :key="key" :name="key">
|
|
||||||
<template #label>
|
|
||||||
<SvgIcon :icon="icons[key]" class="h-23px text-icon-small" />
|
|
||||||
</template>
|
|
||||||
</ElTabPane>
|
|
||||||
</ElTabs>
|
|
||||||
</div>
|
|
||||||
<Transition name="sider-inverted">
|
<Transition name="sider-inverted">
|
||||||
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
||||||
<ElSwitch v-model="themeStore.sider.inverted" />
|
<ElSwitch v-model="themeStore.sider.inverted" />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const local: App.I18n.Schema = {
|
|||||||
trigger: 'Trigger',
|
trigger: 'Trigger',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
updateSuccess: 'Update Success',
|
updateSuccess: 'Update Success',
|
||||||
userCenter: 'User Center',
|
myProfile: 'My Profile',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No'
|
no: 'No'
|
||||||
@@ -158,17 +158,34 @@ const local: App.I18n.Schema = {
|
|||||||
404: 'Page Not Found',
|
404: 'Page Not Found',
|
||||||
500: 'Server Error',
|
500: 'Server Error',
|
||||||
'iframe-page': 'Iframe',
|
'iframe-page': 'Iframe',
|
||||||
'user-center': 'User Center',
|
workbench: 'Workbench',
|
||||||
function: 'System Function',
|
ticket: 'Ticket',
|
||||||
function_tab: 'Tab',
|
'ticket_my-submitted': 'My Submitted',
|
||||||
'function_multi-tab': 'Multi Tab',
|
'ticket_my-pending': 'My Pending',
|
||||||
'function_hide-child': 'Hide Child',
|
metrics: 'Metrics',
|
||||||
'function_hide-child_one': 'Hide Child',
|
'metrics_project-progress': 'Project Progress',
|
||||||
'function_hide-child_two': 'Two',
|
'metrics_member-efficiency': 'Member Efficiency',
|
||||||
'function_hide-child_three': 'Three',
|
metrics_worktime: 'Worktime',
|
||||||
function_request: 'Request',
|
'personal-center': 'Personal Center',
|
||||||
'function_toggle-auth': 'Toggle Auth',
|
'personal-center_my-profile': 'My Profile',
|
||||||
'function_super-page': 'Super Admin Visible',
|
'personal-center_my-item': 'My Items',
|
||||||
|
'personal-center_work-report': 'Work Report',
|
||||||
|
'personal-center_work-report_weekly': 'Weekly Report',
|
||||||
|
'personal-center_work-report_monthly': 'Monthly Report',
|
||||||
|
'personal-center_work-report_project': 'Project Fortnightly Report',
|
||||||
|
'personal-center_my-performance': 'My Performance',
|
||||||
|
'personal-center_my-application': 'My Application',
|
||||||
|
'personal-center_overtime-application': 'Overtime Application',
|
||||||
|
'personal-center_pending-approval': 'Pending Approval',
|
||||||
|
feedback: 'Feedback',
|
||||||
|
infra: 'Infra',
|
||||||
|
'infra_state-machine': 'State Machine',
|
||||||
|
'infra_log-management': 'Log Management',
|
||||||
|
'infra_log-management_login-log': 'Login Log',
|
||||||
|
'infra_log-management_operate-log': 'Operate Log',
|
||||||
|
'infra_log-management_api-access-log': 'API Access Log',
|
||||||
|
'infra_log-management_api-error-log': 'API Error Log',
|
||||||
|
'infra_rd-code': 'R&D Code',
|
||||||
product: 'Product',
|
product: 'Product',
|
||||||
product_list: 'Product List',
|
product_list: 'Product List',
|
||||||
product_dashboard: 'Dashboard',
|
product_dashboard: 'Dashboard',
|
||||||
@@ -192,31 +209,7 @@ const local: App.I18n.Schema = {
|
|||||||
exception: 'Exception',
|
exception: 'Exception',
|
||||||
exception_403: '403',
|
exception_403: '403',
|
||||||
exception_404: '404',
|
exception_404: '404',
|
||||||
exception_500: '500',
|
exception_500: '500'
|
||||||
plugin: 'Plugin',
|
|
||||||
plugin_copy: 'Copy',
|
|
||||||
plugin_charts: 'Charts',
|
|
||||||
plugin_charts_echarts: 'ECharts',
|
|
||||||
plugin_charts_antv: 'AntV',
|
|
||||||
plugin_charts_vchart: 'VChart',
|
|
||||||
plugin_editor: 'Editor',
|
|
||||||
plugin_editor_quill: 'Quill',
|
|
||||||
plugin_editor_markdown: 'Markdown',
|
|
||||||
plugin_icon: 'Icon',
|
|
||||||
plugin_map: 'Map',
|
|
||||||
plugin_print: 'Print',
|
|
||||||
plugin_swiper: 'Swiper',
|
|
||||||
plugin_video: 'Video',
|
|
||||||
plugin_barcode: 'Barcode',
|
|
||||||
plugin_pinyin: 'pinyin',
|
|
||||||
plugin_excel: 'Excel',
|
|
||||||
plugin_pdf: 'PDF preview',
|
|
||||||
plugin_gantt: 'Gantt Chart',
|
|
||||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
|
||||||
plugin_gantt_vtable: 'VTableGantt',
|
|
||||||
plugin_typeit: 'Typeit',
|
|
||||||
plugin_tables: 'Tables',
|
|
||||||
plugin_tables_vtable: 'VTable'
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
@@ -312,45 +305,6 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
creativity: 'Creativity'
|
creativity: 'Creativity'
|
||||||
},
|
},
|
||||||
function: {
|
|
||||||
tab: {
|
|
||||||
tabOperate: {
|
|
||||||
title: 'Tab Operation',
|
|
||||||
addTab: 'Add Tab',
|
|
||||||
addTabDesc: 'To user management page',
|
|
||||||
closeTab: 'Close Tab',
|
|
||||||
closeCurrentTab: 'Close Current Tab',
|
|
||||||
closeAboutTab: 'Close "User Management" Tab',
|
|
||||||
addMultiTab: 'Add Multi Tab',
|
|
||||||
addMultiTabDesc1: 'To MultiTab page',
|
|
||||||
addMultiTabDesc2: 'To MultiTab page(with query params)'
|
|
||||||
},
|
|
||||||
tabTitle: {
|
|
||||||
title: 'Tab Title',
|
|
||||||
changeTitle: 'Change Title',
|
|
||||||
change: 'Change',
|
|
||||||
resetTitle: 'Reset Title',
|
|
||||||
reset: 'Reset'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
multiTab: {
|
|
||||||
routeParam: 'Route Param',
|
|
||||||
backTab: 'Back function_tab'
|
|
||||||
},
|
|
||||||
toggleAuth: {
|
|
||||||
toggleAccount: 'Toggle Account',
|
|
||||||
authHook: 'Auth Hook Function `hasAuth`',
|
|
||||||
superAdminVisible: 'Super Admin Visible',
|
|
||||||
adminVisible: 'Admin Visible',
|
|
||||||
adminOrUserVisible: 'Admin and User Visible'
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
|
|
||||||
repeatedError: 'Repeated Request Error',
|
|
||||||
repeatedErrorMsg1: 'Custom Request Error 1',
|
|
||||||
repeatedErrorMsg2: 'Custom Request Error 2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
system: {
|
system: {
|
||||||
common: {
|
common: {
|
||||||
status: {
|
status: {
|
||||||
@@ -495,6 +449,7 @@ const local: App.I18n.Schema = {
|
|||||||
orgType: {
|
orgType: {
|
||||||
company: 'Company',
|
company: 'Company',
|
||||||
dept: 'Department',
|
dept: 'Department',
|
||||||
|
function: 'Functional Department',
|
||||||
direction: 'Direction',
|
direction: 'Direction',
|
||||||
team: 'Team'
|
team: 'Team'
|
||||||
},
|
},
|
||||||
@@ -692,6 +647,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: 'Dictionary Status',
|
dictStatus: 'Dictionary Status',
|
||||||
dictLabel: 'Dictionary Label',
|
dictLabel: 'Dictionary Label',
|
||||||
dictValue: 'Dictionary Value',
|
dictValue: 'Dictionary Value',
|
||||||
|
colorType: 'Color Type',
|
||||||
sort: 'Sort',
|
sort: 'Sort',
|
||||||
remark: 'Remark',
|
remark: 'Remark',
|
||||||
form: {
|
form: {
|
||||||
@@ -700,6 +656,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: 'Please select dictionary status',
|
dictStatus: 'Please select dictionary status',
|
||||||
dictLabel: 'Please enter dictionary label',
|
dictLabel: 'Please enter dictionary label',
|
||||||
dictValue: 'Please enter dictionary value',
|
dictValue: 'Please enter dictionary value',
|
||||||
|
colorType: 'Please enter color type',
|
||||||
sort: 'Please enter sort',
|
sort: 'Please enter sort',
|
||||||
remark: 'Please enter remark'
|
remark: 'Please enter remark'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const local: App.I18n.Schema = {
|
const local: App.I18n.Schema = {
|
||||||
system: {
|
system: {
|
||||||
title: '研发内部管理系统'
|
title: '研发管理系统'
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
action: '操作',
|
action: '操作',
|
||||||
@@ -40,7 +40,7 @@ const local: App.I18n.Schema = {
|
|||||||
trigger: '触发',
|
trigger: '触发',
|
||||||
update: '更新',
|
update: '更新',
|
||||||
updateSuccess: '更新成功',
|
updateSuccess: '更新成功',
|
||||||
userCenter: '个人中心',
|
myProfile: '个人信息',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: '是',
|
yes: '是',
|
||||||
no: '否'
|
no: '否'
|
||||||
@@ -158,17 +158,34 @@ const local: App.I18n.Schema = {
|
|||||||
404: '页面不存在',
|
404: '页面不存在',
|
||||||
500: '服务器错误',
|
500: '服务器错误',
|
||||||
'iframe-page': '外链页面',
|
'iframe-page': '外链页面',
|
||||||
'user-center': '个人中心',
|
workbench: '工作台',
|
||||||
function: '系统功能',
|
ticket: '工单',
|
||||||
function_tab: '标签页',
|
'ticket_my-submitted': '我提交的工单',
|
||||||
'function_multi-tab': '多标签页',
|
'ticket_my-pending': '待我处理的工单',
|
||||||
'function_hide-child': '隐藏子菜单',
|
metrics: '效能度量',
|
||||||
'function_hide-child_one': '隐藏子菜单',
|
'metrics_project-progress': '项目进度',
|
||||||
'function_hide-child_two': '菜单二',
|
'metrics_member-efficiency': '员工能效',
|
||||||
'function_hide-child_three': '菜单三',
|
metrics_worktime: '工时统计',
|
||||||
function_request: '请求',
|
'personal-center': '个人中心',
|
||||||
'function_toggle-auth': '切换权限',
|
'personal-center_my-profile': '个人信息',
|
||||||
'function_super-page': '超级管理员可见',
|
'personal-center_my-item': '我的事项',
|
||||||
|
'personal-center_work-report': '工作报告',
|
||||||
|
'personal-center_work-report_weekly': '个人周报',
|
||||||
|
'personal-center_work-report_monthly': '个人月报',
|
||||||
|
'personal-center_work-report_project': '项目半月报',
|
||||||
|
'personal-center_my-performance': '我的绩效',
|
||||||
|
'personal-center_my-application': '我的申请',
|
||||||
|
'personal-center_overtime-application': '加班申请',
|
||||||
|
'personal-center_pending-approval': '待我审批',
|
||||||
|
feedback: '意见反馈',
|
||||||
|
infra: '基础设施',
|
||||||
|
'infra_state-machine': '状态机管理',
|
||||||
|
'infra_log-management': '日志管理',
|
||||||
|
'infra_log-management_login-log': '登录日志',
|
||||||
|
'infra_log-management_operate-log': '操作日志',
|
||||||
|
'infra_log-management_api-access-log': 'API访问日志',
|
||||||
|
'infra_log-management_api-error-log': 'API错误日志',
|
||||||
|
'infra_rd-code': '研发令号',
|
||||||
product: '产品管理',
|
product: '产品管理',
|
||||||
product_list: '产品列表',
|
product_list: '产品列表',
|
||||||
product_dashboard: '产品仪表盘',
|
product_dashboard: '产品仪表盘',
|
||||||
@@ -192,31 +209,7 @@ const local: App.I18n.Schema = {
|
|||||||
exception: '异常页',
|
exception: '异常页',
|
||||||
exception_403: '403',
|
exception_403: '403',
|
||||||
exception_404: '404',
|
exception_404: '404',
|
||||||
exception_500: '500',
|
exception_500: '500'
|
||||||
plugin: '插件示例',
|
|
||||||
plugin_copy: '剪贴板',
|
|
||||||
plugin_charts: '图表',
|
|
||||||
plugin_charts_echarts: 'ECharts',
|
|
||||||
plugin_charts_antv: 'AntV',
|
|
||||||
plugin_charts_vchart: 'VChart',
|
|
||||||
plugin_editor: '编辑器',
|
|
||||||
plugin_editor_quill: '富文本编辑器',
|
|
||||||
plugin_editor_markdown: 'MD 编辑器',
|
|
||||||
plugin_icon: '图标',
|
|
||||||
plugin_map: '地图',
|
|
||||||
plugin_print: '打印',
|
|
||||||
plugin_swiper: 'Swiper',
|
|
||||||
plugin_video: '视频',
|
|
||||||
plugin_barcode: '条形码',
|
|
||||||
plugin_pinyin: '拼音',
|
|
||||||
plugin_excel: 'Excel',
|
|
||||||
plugin_pdf: 'PDF 预览',
|
|
||||||
plugin_gantt: '甘特图',
|
|
||||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
|
||||||
plugin_gantt_vtable: 'VTableGantt',
|
|
||||||
plugin_typeit: '打字机',
|
|
||||||
plugin_tables: '表格',
|
|
||||||
plugin_tables_vtable: 'VTable'
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
@@ -268,7 +261,7 @@ const local: App.I18n.Schema = {
|
|||||||
about: {
|
about: {
|
||||||
title: '关于',
|
title: '关于',
|
||||||
introduction:
|
introduction:
|
||||||
'灿能研发内部管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
|
'灿能研发管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
|
||||||
projectInfo: {
|
projectInfo: {
|
||||||
title: '项目信息',
|
title: '项目信息',
|
||||||
version: '版本',
|
version: '版本',
|
||||||
@@ -311,45 +304,6 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
creativity: '创意'
|
creativity: '创意'
|
||||||
},
|
},
|
||||||
function: {
|
|
||||||
tab: {
|
|
||||||
tabOperate: {
|
|
||||||
title: '标签页操作',
|
|
||||||
addTab: '添加标签页',
|
|
||||||
addTabDesc: '跳转到用户管理页面',
|
|
||||||
closeTab: '关闭标签页',
|
|
||||||
closeCurrentTab: '关闭当前标签页',
|
|
||||||
closeAboutTab: '关闭"用户管理"标签页',
|
|
||||||
addMultiTab: '添加多标签页',
|
|
||||||
addMultiTabDesc1: '跳转到多标签页页面',
|
|
||||||
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
|
|
||||||
},
|
|
||||||
tabTitle: {
|
|
||||||
title: '标签页标题',
|
|
||||||
changeTitle: '修改标题',
|
|
||||||
change: '修改',
|
|
||||||
resetTitle: '重置标题',
|
|
||||||
reset: '重置'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
multiTab: {
|
|
||||||
routeParam: '路由参数',
|
|
||||||
backTab: '返回 function_tab'
|
|
||||||
},
|
|
||||||
toggleAuth: {
|
|
||||||
toggleAccount: '切换账号',
|
|
||||||
authHook: '权限钩子函数 `hasAuth`',
|
|
||||||
superAdminVisible: '超级管理员可见',
|
|
||||||
adminVisible: '管理员可见',
|
|
||||||
adminOrUserVisible: '管理员和用户可见'
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
repeatedErrorOccurOnce: '重复请求错误只出现一次',
|
|
||||||
repeatedError: '重复请求错误',
|
|
||||||
repeatedErrorMsg1: '自定义请求错误 1',
|
|
||||||
repeatedErrorMsg2: '自定义请求错误 2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
system: {
|
system: {
|
||||||
common: {
|
common: {
|
||||||
status: {
|
status: {
|
||||||
@@ -491,6 +445,7 @@ const local: App.I18n.Schema = {
|
|||||||
orgType: {
|
orgType: {
|
||||||
company: '公司',
|
company: '公司',
|
||||||
dept: '部门',
|
dept: '部门',
|
||||||
|
function: '职能部门',
|
||||||
direction: '方向',
|
direction: '方向',
|
||||||
team: '团队'
|
team: '团队'
|
||||||
},
|
},
|
||||||
@@ -680,6 +635,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: '字典状态',
|
dictStatus: '字典状态',
|
||||||
dictLabel: '字典标签',
|
dictLabel: '字典标签',
|
||||||
dictValue: '字典键值',
|
dictValue: '字典键值',
|
||||||
|
colorType: '颜色类型',
|
||||||
sort: '排序',
|
sort: '排序',
|
||||||
remark: '备注',
|
remark: '备注',
|
||||||
form: {
|
form: {
|
||||||
@@ -688,6 +644,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: '请选择字典状态',
|
dictStatus: '请选择字典状态',
|
||||||
dictLabel: '请输入字典标签',
|
dictLabel: '请输入字典标签',
|
||||||
dictValue: '请输入字典键值',
|
dictValue: '请输入字典键值',
|
||||||
|
colorType: '请输入颜色类型',
|
||||||
sort: '请输入排序',
|
sort: '请输入排序',
|
||||||
remark: '请输入备注'
|
remark: '请输入备注'
|
||||||
},
|
},
|
||||||
@@ -710,7 +667,7 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
pwd: {
|
pwd: {
|
||||||
required: '请输入密码',
|
required: '请输入密码',
|
||||||
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线'
|
invalid: '密码格式不正确,4-30位字符,包含字母、数字、下划线'
|
||||||
},
|
},
|
||||||
confirmPwd: {
|
confirmPwd: {
|
||||||
required: '请输入确认密码',
|
required: '请输入确认密码',
|
||||||
|
|||||||
@@ -3,6 +3,3 @@ import 'element-plus/dist/index.css';
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
import '../styles/css/global.css';
|
import '../styles/css/global.css';
|
||||||
import 'swiper/css';
|
|
||||||
import 'swiper/css/navigation';
|
|
||||||
import 'swiper/css/pagination';
|
|
||||||
|
|||||||
@@ -20,33 +20,27 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
500: () => import("@/views/_builtin/500/index.vue"),
|
500: () => import("@/views/_builtin/500/index.vue"),
|
||||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
||||||
login: () => import("@/views/_builtin/login/index.vue"),
|
login: () => import("@/views/_builtin/login/index.vue"),
|
||||||
"function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),
|
feedback: () => import("@/views/feedback/index.vue"),
|
||||||
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
|
"infra_log-management_api-access-log": () => import("@/views/infra/log-management/api-access-log/index.vue"),
|
||||||
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
|
"infra_log-management_api-error-log": () => import("@/views/infra/log-management/api-error-log/index.vue"),
|
||||||
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
|
"infra_log-management": () => import("@/views/infra/log-management/index.vue"),
|
||||||
function_request: () => import("@/views/function/request/index.vue"),
|
"infra_log-management_login-log": () => import("@/views/infra/log-management/login-log/index.vue"),
|
||||||
"function_super-page": () => import("@/views/function/super-page/index.vue"),
|
"infra_log-management_operate-log": () => import("@/views/infra/log-management/operate-log/index.vue"),
|
||||||
function_tab: () => import("@/views/function/tab/index.vue"),
|
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
|
||||||
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
|
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
|
||||||
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
|
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
|
||||||
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
"metrics_project-progress": () => import("@/views/metrics/project-progress/index.vue"),
|
||||||
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
|
metrics_worktime: () => import("@/views/metrics/worktime/index.vue"),
|
||||||
plugin_charts_vchart: () => import("@/views/plugin/charts/vchart/index.vue"),
|
"personal-center_my-application": () => import("@/views/personal-center/my-application/index.vue"),
|
||||||
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
|
"personal-center_my-item": () => import("@/views/personal-center/my-item/index.vue"),
|
||||||
plugin_editor_markdown: () => import("@/views/plugin/editor/markdown/index.vue"),
|
"personal-center_my-performance": () => import("@/views/personal-center/my-performance/index.vue"),
|
||||||
plugin_editor_quill: () => import("@/views/plugin/editor/quill/index.vue"),
|
"personal-center_my-profile": () => import("@/views/personal-center/my-profile/index.vue"),
|
||||||
plugin_excel: () => import("@/views/plugin/excel/index.vue"),
|
"personal-center_overtime-application": () => import("@/views/personal-center/overtime-application/index.vue"),
|
||||||
plugin_gantt_dhtmlx: () => import("@/views/plugin/gantt/dhtmlx/index.vue"),
|
"personal-center_pending-approval": () => import("@/views/personal-center/pending-approval/index.vue"),
|
||||||
plugin_gantt_vtable: () => import("@/views/plugin/gantt/vtable/index.vue"),
|
"personal-center_work-report": () => import("@/views/personal-center/work-report/index.vue"),
|
||||||
plugin_icon: () => import("@/views/plugin/icon/index.vue"),
|
"personal-center_work-report_monthly": () => import("@/views/personal-center/work-report/monthly/index.vue"),
|
||||||
plugin_map: () => import("@/views/plugin/map/index.vue"),
|
"personal-center_work-report_project": () => import("@/views/personal-center/work-report/project/index.vue"),
|
||||||
plugin_pdf: () => import("@/views/plugin/pdf/index.vue"),
|
"personal-center_work-report_weekly": () => import("@/views/personal-center/work-report/weekly/index.vue"),
|
||||||
plugin_pinyin: () => import("@/views/plugin/pinyin/index.vue"),
|
|
||||||
plugin_print: () => import("@/views/plugin/print/index.vue"),
|
|
||||||
plugin_swiper: () => import("@/views/plugin/swiper/index.vue"),
|
|
||||||
plugin_tables_vtable: () => import("@/views/plugin/tables/vtable/index.vue"),
|
|
||||||
plugin_typeit: () => import("@/views/plugin/typeit/index.vue"),
|
|
||||||
plugin_video: () => import("@/views/plugin/video/index.vue"),
|
|
||||||
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
|
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
|
||||||
product_list: () => import("@/views/product/list/index.vue"),
|
product_list: () => import("@/views/product/list/index.vue"),
|
||||||
product_requirement: () => import("@/views/product/requirement/index.vue"),
|
product_requirement: () => import("@/views/product/requirement/index.vue"),
|
||||||
@@ -63,5 +57,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
"system_user-detail": () => import("@/views/system/user-detail/[id].vue"),
|
"system_user-detail": () => import("@/views/system/user-detail/[id].vue"),
|
||||||
"system_user-management-relation": () => import("@/views/system/user-management-relation/index.vue"),
|
"system_user-management-relation": () => import("@/views/system/user-management-relation/index.vue"),
|
||||||
system_user: () => import("@/views/system/user/index.vue"),
|
system_user: () => import("@/views/system/user/index.vue"),
|
||||||
"user-center": () => import("@/views/user-center/index.vue"),
|
"ticket_my-pending": () => import("@/views/ticket/my-pending/index.vue"),
|
||||||
|
"ticket_my-submitted": () => import("@/views/ticket/my-submitted/index.vue"),
|
||||||
|
workbench: () => import("@/views/workbench/index.vue"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,122 +40,17 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'function',
|
name: 'feedback',
|
||||||
path: '/function',
|
path: '/feedback',
|
||||||
component: 'layout.base',
|
component: 'layout.base$view.feedback',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'function',
|
title: 'feedback',
|
||||||
i18nKey: 'route.function',
|
i18nKey: 'route.feedback',
|
||||||
icon: 'icon-park-outline:all-application',
|
icon: 'mdi:message-alert-outline',
|
||||||
order: 6
|
order: 10,
|
||||||
},
|
keepAlive: true,
|
||||||
children: [
|
constant: true
|
||||||
{
|
}
|
||||||
name: 'function_hide-child',
|
|
||||||
path: '/function/hide-child',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child',
|
|
||||||
i18nKey: 'route.function_hide-child',
|
|
||||||
icon: 'material-symbols:filter-list-off',
|
|
||||||
order: 2
|
|
||||||
},
|
|
||||||
redirect: '/function/hide-child/one',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_one',
|
|
||||||
path: '/function/hide-child/one',
|
|
||||||
component: 'view.function_hide-child_one',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_one',
|
|
||||||
i18nKey: 'route.function_hide-child_one',
|
|
||||||
icon: 'material-symbols:filter-list-off',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_three',
|
|
||||||
path: '/function/hide-child/three',
|
|
||||||
component: 'view.function_hide-child_three',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_three',
|
|
||||||
i18nKey: 'route.function_hide-child_three',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_two',
|
|
||||||
path: '/function/hide-child/two',
|
|
||||||
component: 'view.function_hide-child_two',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_two',
|
|
||||||
i18nKey: 'route.function_hide-child_two',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_multi-tab',
|
|
||||||
path: '/function/multi-tab',
|
|
||||||
component: 'view.function_multi-tab',
|
|
||||||
meta: {
|
|
||||||
title: 'function_multi-tab',
|
|
||||||
i18nKey: 'route.function_multi-tab',
|
|
||||||
icon: 'ic:round-tab',
|
|
||||||
multiTab: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_tab'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_request',
|
|
||||||
path: '/function/request',
|
|
||||||
component: 'view.function_request',
|
|
||||||
meta: {
|
|
||||||
title: 'function_request',
|
|
||||||
i18nKey: 'route.function_request',
|
|
||||||
icon: 'carbon:network-overlay',
|
|
||||||
order: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_super-page',
|
|
||||||
path: '/function/super-page',
|
|
||||||
component: 'view.function_super-page',
|
|
||||||
meta: {
|
|
||||||
title: 'function_super-page',
|
|
||||||
i18nKey: 'route.function_super-page',
|
|
||||||
icon: 'ic:round-supervisor-account',
|
|
||||||
order: 5,
|
|
||||||
roles: ['R_SUPER']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_tab',
|
|
||||||
path: '/function/tab',
|
|
||||||
component: 'view.function_tab',
|
|
||||||
meta: {
|
|
||||||
title: 'function_tab',
|
|
||||||
i18nKey: 'route.function_tab',
|
|
||||||
icon: 'ic:round-tab',
|
|
||||||
order: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_toggle-auth',
|
|
||||||
path: '/function/toggle-auth',
|
|
||||||
component: 'view.function_toggle-auth',
|
|
||||||
meta: {
|
|
||||||
title: 'function_toggle-auth',
|
|
||||||
i18nKey: 'route.function_toggle-auth',
|
|
||||||
icon: 'ic:round-construction',
|
|
||||||
order: 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'iframe-page',
|
name: 'iframe-page',
|
||||||
@@ -170,6 +65,101 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'infra',
|
||||||
|
path: '/infra',
|
||||||
|
component: 'layout.base',
|
||||||
|
meta: {
|
||||||
|
title: 'infra',
|
||||||
|
i18nKey: 'route.infra',
|
||||||
|
icon: 'ep:monitor',
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'infra_log-management',
|
||||||
|
path: '/infra/log-management',
|
||||||
|
component: 'view.infra_log-management',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management',
|
||||||
|
i18nKey: 'route.infra_log-management',
|
||||||
|
icon: 'mdi:text-box-search-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_api-access-log',
|
||||||
|
path: '/infra/log-management/api-access-log',
|
||||||
|
component: 'view.infra_log-management_api-access-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_api-access-log',
|
||||||
|
i18nKey: 'route.infra_log-management_api-access-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_api-error-log',
|
||||||
|
path: '/infra/log-management/api-error-log',
|
||||||
|
component: 'view.infra_log-management_api-error-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_api-error-log',
|
||||||
|
i18nKey: 'route.infra_log-management_api-error-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_login-log',
|
||||||
|
path: '/infra/log-management/login-log',
|
||||||
|
component: 'view.infra_log-management_login-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_login-log',
|
||||||
|
i18nKey: 'route.infra_log-management_login-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_operate-log',
|
||||||
|
path: '/infra/log-management/operate-log',
|
||||||
|
component: 'view.infra_log-management_operate-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_operate-log',
|
||||||
|
i18nKey: 'route.infra_log-management_operate-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_rd-code',
|
||||||
|
path: '/infra/rd-code',
|
||||||
|
component: 'view.infra_rd-code',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_rd-code',
|
||||||
|
i18nKey: 'route.infra_rd-code',
|
||||||
|
icon: 'mdi:identifier',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_state-machine',
|
||||||
|
path: '/infra/state-machine',
|
||||||
|
component: 'view.infra_state-machine',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_state-machine',
|
||||||
|
i18nKey: 'route.infra_state-machine',
|
||||||
|
icon: 'mdi:state-machine',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'login',
|
name: 'login',
|
||||||
path: '/login/:module(pwd-login|reset-pwd)?',
|
path: '/login/:module(pwd-login|reset-pwd)?',
|
||||||
@@ -183,250 +173,183 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'plugin',
|
name: 'metrics',
|
||||||
path: '/plugin',
|
path: '/metrics',
|
||||||
component: 'layout.base',
|
component: 'layout.base',
|
||||||
meta: {
|
meta: {
|
||||||
title: '插件示例',
|
title: 'metrics',
|
||||||
i18nKey: 'route.plugin',
|
i18nKey: 'route.metrics',
|
||||||
order: 7,
|
icon: 'mdi:chart-line',
|
||||||
icon: 'clarity:plugin-line'
|
order: 7
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'plugin_barcode',
|
name: 'metrics_member-efficiency',
|
||||||
path: '/plugin/barcode',
|
path: '/metrics/member-efficiency',
|
||||||
component: 'view.plugin_barcode',
|
component: 'view.metrics_member-efficiency',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_barcode',
|
title: 'metrics_member-efficiency',
|
||||||
i18nKey: 'route.plugin_barcode',
|
i18nKey: 'route.metrics_member-efficiency',
|
||||||
icon: 'ic:round-barcode'
|
icon: 'mdi:account-multiple-check-outline',
|
||||||
}
|
order: 2,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_charts',
|
|
||||||
path: '/plugin/charts',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts',
|
|
||||||
i18nKey: 'route.plugin_charts',
|
|
||||||
icon: 'mdi:chart-areaspline'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_antv',
|
|
||||||
path: '/plugin/charts/antv',
|
|
||||||
component: 'view.plugin_charts_antv',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_antv',
|
|
||||||
i18nKey: 'route.plugin_charts_antv',
|
|
||||||
icon: 'hugeicons:flow-square'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_echarts',
|
|
||||||
path: '/plugin/charts/echarts',
|
|
||||||
component: 'view.plugin_charts_echarts',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_echarts',
|
|
||||||
i18nKey: 'route.plugin_charts_echarts',
|
|
||||||
icon: 'simple-icons:apacheecharts'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_vchart',
|
|
||||||
path: '/plugin/charts/vchart',
|
|
||||||
component: 'view.plugin_charts_vchart',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_vchart',
|
|
||||||
i18nKey: 'route.plugin_charts_vchart',
|
|
||||||
localIcon: 'visactor'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_copy',
|
|
||||||
path: '/plugin/copy',
|
|
||||||
component: 'view.plugin_copy',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_copy',
|
|
||||||
i18nKey: 'route.plugin_copy',
|
|
||||||
icon: 'mdi:clipboard-outline'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_editor',
|
|
||||||
path: '/plugin/editor',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_editor',
|
|
||||||
i18nKey: 'route.plugin_editor',
|
|
||||||
icon: 'icon-park-outline:editor'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'plugin_editor_markdown',
|
|
||||||
path: '/plugin/editor/markdown',
|
|
||||||
component: 'view.plugin_editor_markdown',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_editor_markdown',
|
|
||||||
i18nKey: 'route.plugin_editor_markdown',
|
|
||||||
icon: 'ri:markdown-line'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_editor_quill',
|
|
||||||
path: '/plugin/editor/quill',
|
|
||||||
component: 'view.plugin_editor_quill',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_editor_quill',
|
|
||||||
i18nKey: 'route.plugin_editor_quill',
|
|
||||||
icon: 'mdi:file-document-edit-outline'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_excel',
|
|
||||||
path: '/plugin/excel',
|
|
||||||
component: 'view.plugin_excel',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_excel',
|
|
||||||
i18nKey: 'route.plugin_excel',
|
|
||||||
icon: 'ri:file-excel-2-line',
|
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'plugin_gantt',
|
name: 'metrics_project-progress',
|
||||||
path: '/plugin/gantt',
|
path: '/metrics/project-progress',
|
||||||
|
component: 'view.metrics_project-progress',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_gantt',
|
title: 'metrics_project-progress',
|
||||||
i18nKey: 'route.plugin_gantt',
|
i18nKey: 'route.metrics_project-progress',
|
||||||
icon: 'ant-design:bar-chart-outlined'
|
icon: 'mdi:progress-clock',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metrics_worktime',
|
||||||
|
path: '/metrics/worktime',
|
||||||
|
component: 'view.metrics_worktime',
|
||||||
|
meta: {
|
||||||
|
title: 'metrics_worktime',
|
||||||
|
i18nKey: 'route.metrics_worktime',
|
||||||
|
icon: 'mdi:clock-time-five-outline',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center',
|
||||||
|
path: '/personal-center',
|
||||||
|
component: 'layout.base',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center',
|
||||||
|
i18nKey: 'route.personal-center',
|
||||||
|
icon: 'mdi:account-circle-outline',
|
||||||
|
order: 8
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'personal-center_my-application',
|
||||||
|
path: '/personal-center/my-application',
|
||||||
|
component: 'view.personal-center_my-application',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_my-application',
|
||||||
|
i18nKey: 'route.personal-center_my-application',
|
||||||
|
icon: 'mdi:file-document-outline',
|
||||||
|
order: 4,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_my-item',
|
||||||
|
path: '/personal-center/my-item',
|
||||||
|
component: 'view.personal-center_my-item',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_my-item',
|
||||||
|
i18nKey: 'route.personal-center_my-item',
|
||||||
|
icon: 'mdi:checkbox-multiple-blank-circle-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_my-performance',
|
||||||
|
path: '/personal-center/my-performance',
|
||||||
|
component: 'view.personal-center_my-performance',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_my-performance',
|
||||||
|
i18nKey: 'route.personal-center_my-performance',
|
||||||
|
icon: 'mdi:trophy-outline',
|
||||||
|
order: 4,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_my-profile',
|
||||||
|
path: '/personal-center/my-profile',
|
||||||
|
component: 'view.personal-center_my-profile',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_my-profile',
|
||||||
|
i18nKey: 'route.personal-center_my-profile',
|
||||||
|
icon: 'mdi:account-box-outline',
|
||||||
|
order: 0,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_overtime-application',
|
||||||
|
path: '/personal-center/overtime-application',
|
||||||
|
component: 'view.personal-center_overtime-application',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_overtime-application',
|
||||||
|
i18nKey: 'route.personal-center_overtime-application',
|
||||||
|
icon: 'mdi:clock-plus-outline',
|
||||||
|
order: 6,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_pending-approval',
|
||||||
|
path: '/personal-center/pending-approval',
|
||||||
|
component: 'view.personal-center_pending-approval',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_pending-approval',
|
||||||
|
i18nKey: 'route.personal-center_pending-approval',
|
||||||
|
icon: 'mdi:check-decagram-outline',
|
||||||
|
order: 7,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'personal-center_work-report',
|
||||||
|
path: '/personal-center/work-report',
|
||||||
|
component: 'view.personal-center_work-report',
|
||||||
|
meta: {
|
||||||
|
title: 'personal-center_work-report',
|
||||||
|
i18nKey: 'route.personal-center_work-report',
|
||||||
|
icon: 'mdi:file-chart-outline',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'plugin_gantt_dhtmlx',
|
name: 'personal-center_work-report_monthly',
|
||||||
path: '/plugin/gantt/dhtmlx',
|
path: '/personal-center/work-report/monthly',
|
||||||
component: 'view.plugin_gantt_dhtmlx',
|
component: 'view.personal-center_work-report_monthly',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_gantt_dhtmlx',
|
title: 'personal-center_work-report_monthly',
|
||||||
i18nKey: 'route.plugin_gantt_dhtmlx',
|
i18nKey: 'route.personal-center_work-report_monthly',
|
||||||
icon: 'gridicons:posts'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'plugin_gantt_vtable',
|
name: 'personal-center_work-report_project',
|
||||||
path: '/plugin/gantt/vtable',
|
path: '/personal-center/work-report/project',
|
||||||
component: 'view.plugin_gantt_vtable',
|
component: 'view.personal-center_work-report_project',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_gantt_vtable',
|
title: 'personal-center_work-report_project',
|
||||||
i18nKey: 'route.plugin_gantt_vtable',
|
i18nKey: 'route.personal-center_work-report_project',
|
||||||
localIcon: 'visactor'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_icon',
|
|
||||||
path: '/plugin/icon',
|
|
||||||
component: 'view.plugin_icon',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_icon',
|
|
||||||
i18nKey: 'route.plugin_icon',
|
|
||||||
localIcon: 'custom-icon'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_map',
|
|
||||||
path: '/plugin/map',
|
|
||||||
component: 'view.plugin_map',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_map',
|
|
||||||
i18nKey: 'route.plugin_map',
|
|
||||||
icon: 'mdi:map'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_pdf',
|
|
||||||
path: '/plugin/pdf',
|
|
||||||
component: 'view.plugin_pdf',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_pdf',
|
|
||||||
i18nKey: 'route.plugin_pdf',
|
|
||||||
icon: 'uiw:file-pdf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_pinyin',
|
|
||||||
path: '/plugin/pinyin',
|
|
||||||
component: 'view.plugin_pinyin',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_pinyin',
|
|
||||||
i18nKey: 'route.plugin_pinyin',
|
|
||||||
icon: 'entypo-social:google-hangouts'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_print',
|
|
||||||
path: '/plugin/print',
|
|
||||||
component: 'view.plugin_print',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_print',
|
|
||||||
i18nKey: 'route.plugin_print',
|
|
||||||
icon: 'mdi:printer'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_swiper',
|
|
||||||
path: '/plugin/swiper',
|
|
||||||
component: 'view.plugin_swiper',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_swiper',
|
|
||||||
i18nKey: 'route.plugin_swiper',
|
|
||||||
icon: 'simple-icons:swiper'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_tables',
|
|
||||||
path: '/plugin/tables',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_tables',
|
|
||||||
i18nKey: 'route.plugin_tables',
|
|
||||||
icon: 'icon-park-outline:table'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
{
|
||||||
name: 'plugin_tables_vtable',
|
name: 'personal-center_work-report_weekly',
|
||||||
path: '/plugin/tables/vtable',
|
path: '/personal-center/work-report/weekly',
|
||||||
component: 'view.plugin_tables_vtable',
|
component: 'view.personal-center_work-report_weekly',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_tables_vtable',
|
title: 'personal-center_work-report_weekly',
|
||||||
i18nKey: 'route.plugin_tables_vtable',
|
i18nKey: 'route.personal-center_work-report_weekly',
|
||||||
localIcon: 'visactor'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_typeit',
|
|
||||||
path: '/plugin/typeit',
|
|
||||||
component: 'view.plugin_typeit',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_typeit',
|
|
||||||
i18nKey: 'route.plugin_typeit',
|
|
||||||
icon: 'mdi:typewriter'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_video',
|
|
||||||
path: '/plugin/video',
|
|
||||||
component: 'view.plugin_video',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_video',
|
|
||||||
i18nKey: 'route.plugin_video',
|
|
||||||
icon: 'mdi:video'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -664,13 +587,53 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-center',
|
name: 'ticket',
|
||||||
path: '/user-center',
|
path: '/ticket',
|
||||||
component: 'layout.base$view.user-center',
|
component: 'layout.base',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'user-center',
|
title: 'ticket',
|
||||||
i18nKey: 'route.user-center',
|
i18nKey: 'route.ticket',
|
||||||
hideInMenu: true
|
icon: 'mdi:ticket-confirmation-outline',
|
||||||
|
order: 6
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'ticket_my-pending',
|
||||||
|
path: '/ticket/my-pending',
|
||||||
|
component: 'view.ticket_my-pending',
|
||||||
|
meta: {
|
||||||
|
title: 'ticket_my-pending',
|
||||||
|
i18nKey: 'route.ticket_my-pending',
|
||||||
|
icon: 'mdi:inbox-arrow-down-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ticket_my-submitted',
|
||||||
|
path: '/ticket/my-submitted',
|
||||||
|
component: 'view.ticket_my-submitted',
|
||||||
|
meta: {
|
||||||
|
title: 'ticket_my-submitted',
|
||||||
|
i18nKey: 'route.ticket_my-submitted',
|
||||||
|
icon: 'mdi:upload-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workbench',
|
||||||
|
path: '/workbench',
|
||||||
|
component: 'layout.base$view.workbench',
|
||||||
|
meta: {
|
||||||
|
title: 'workbench',
|
||||||
|
i18nKey: 'route.workbench',
|
||||||
|
icon: 'mdi:view-dashboard-outline',
|
||||||
|
order: 1,
|
||||||
|
keepAlive: true,
|
||||||
|
constant: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -170,42 +170,32 @@ const routeMap: RouteMap = {
|
|||||||
"403": "/403",
|
"403": "/403",
|
||||||
"404": "/404",
|
"404": "/404",
|
||||||
"500": "/500",
|
"500": "/500",
|
||||||
"function": "/function",
|
"feedback": "/feedback",
|
||||||
"function_hide-child": "/function/hide-child",
|
|
||||||
"function_hide-child_one": "/function/hide-child/one",
|
|
||||||
"function_hide-child_three": "/function/hide-child/three",
|
|
||||||
"function_hide-child_two": "/function/hide-child/two",
|
|
||||||
"function_multi-tab": "/function/multi-tab",
|
|
||||||
"function_request": "/function/request",
|
|
||||||
"function_super-page": "/function/super-page",
|
|
||||||
"function_tab": "/function/tab",
|
|
||||||
"function_toggle-auth": "/function/toggle-auth",
|
|
||||||
"iframe-page": "/iframe-page/:url",
|
"iframe-page": "/iframe-page/:url",
|
||||||
|
"infra": "/infra",
|
||||||
|
"infra_log-management": "/infra/log-management",
|
||||||
|
"infra_log-management_api-access-log": "/infra/log-management/api-access-log",
|
||||||
|
"infra_log-management_api-error-log": "/infra/log-management/api-error-log",
|
||||||
|
"infra_log-management_login-log": "/infra/log-management/login-log",
|
||||||
|
"infra_log-management_operate-log": "/infra/log-management/operate-log",
|
||||||
|
"infra_rd-code": "/infra/rd-code",
|
||||||
|
"infra_state-machine": "/infra/state-machine",
|
||||||
"login": "/login/:module(pwd-login|reset-pwd)?",
|
"login": "/login/:module(pwd-login|reset-pwd)?",
|
||||||
"plugin": "/plugin",
|
"metrics": "/metrics",
|
||||||
"plugin_barcode": "/plugin/barcode",
|
"metrics_member-efficiency": "/metrics/member-efficiency",
|
||||||
"plugin_charts": "/plugin/charts",
|
"metrics_project-progress": "/metrics/project-progress",
|
||||||
"plugin_charts_antv": "/plugin/charts/antv",
|
"metrics_worktime": "/metrics/worktime",
|
||||||
"plugin_charts_echarts": "/plugin/charts/echarts",
|
"personal-center": "/personal-center",
|
||||||
"plugin_charts_vchart": "/plugin/charts/vchart",
|
"personal-center_my-application": "/personal-center/my-application",
|
||||||
"plugin_copy": "/plugin/copy",
|
"personal-center_my-item": "/personal-center/my-item",
|
||||||
"plugin_editor": "/plugin/editor",
|
"personal-center_my-performance": "/personal-center/my-performance",
|
||||||
"plugin_editor_markdown": "/plugin/editor/markdown",
|
"personal-center_my-profile": "/personal-center/my-profile",
|
||||||
"plugin_editor_quill": "/plugin/editor/quill",
|
"personal-center_overtime-application": "/personal-center/overtime-application",
|
||||||
"plugin_excel": "/plugin/excel",
|
"personal-center_pending-approval": "/personal-center/pending-approval",
|
||||||
"plugin_gantt": "/plugin/gantt",
|
"personal-center_work-report": "/personal-center/work-report",
|
||||||
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx",
|
"personal-center_work-report_monthly": "/personal-center/work-report/monthly",
|
||||||
"plugin_gantt_vtable": "/plugin/gantt/vtable",
|
"personal-center_work-report_project": "/personal-center/work-report/project",
|
||||||
"plugin_icon": "/plugin/icon",
|
"personal-center_work-report_weekly": "/personal-center/work-report/weekly",
|
||||||
"plugin_map": "/plugin/map",
|
|
||||||
"plugin_pdf": "/plugin/pdf",
|
|
||||||
"plugin_pinyin": "/plugin/pinyin",
|
|
||||||
"plugin_print": "/plugin/print",
|
|
||||||
"plugin_swiper": "/plugin/swiper",
|
|
||||||
"plugin_tables": "/plugin/tables",
|
|
||||||
"plugin_tables_vtable": "/plugin/tables/vtable",
|
|
||||||
"plugin_typeit": "/plugin/typeit",
|
|
||||||
"plugin_video": "/plugin/video",
|
|
||||||
"product": "/product",
|
"product": "/product",
|
||||||
"product_dashboard": "/product/dashboard",
|
"product_dashboard": "/product/dashboard",
|
||||||
"product_list": "/product/list",
|
"product_list": "/product/list",
|
||||||
@@ -226,7 +216,10 @@ const routeMap: RouteMap = {
|
|||||||
"system_user": "/system/user",
|
"system_user": "/system/user",
|
||||||
"system_user-detail": "/system/user-detail/:id",
|
"system_user-detail": "/system/user-detail/:id",
|
||||||
"system_user-management-relation": "/system/user-management-relation",
|
"system_user-management-relation": "/system/user-management-relation",
|
||||||
"user-center": "/user-center"
|
"ticket": "/ticket",
|
||||||
|
"ticket_my-pending": "/ticket/my-pending",
|
||||||
|
"ticket_my-submitted": "/ticket/my-submitted",
|
||||||
|
"workbench": "/workbench"
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export function createDocumentTitleGuard(router: Router) {
|
|||||||
|
|
||||||
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
||||||
|
|
||||||
useTitle(documentTitle);
|
useTitle(`研发管理系统 - ${documentTitle}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
import { clearUserRouteCache } from './route';
|
import { clearUserRouteCache } from './route';
|
||||||
import type { ServiceRequestResult } from './shared';
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId } from './shared';
|
||||||
|
|
||||||
/** 后端登录返回 */
|
/** 后端登录返回 */
|
||||||
interface BackendLoginToken {
|
interface BackendLoginToken {
|
||||||
@@ -15,10 +15,38 @@ interface BackendUserInfoDTO {
|
|||||||
userId: string | number;
|
userId: string | number;
|
||||||
userName?: string | null;
|
userName?: string | null;
|
||||||
nickname?: string | null;
|
nickname?: string | null;
|
||||||
|
deptId?: string | number | null;
|
||||||
roles?: string[] | null;
|
roles?: string[] | null;
|
||||||
buttons?: string[] | null;
|
buttons?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BackendMyProfileDetailDTO {
|
||||||
|
id?: string | number | null;
|
||||||
|
userId?: string | number | null;
|
||||||
|
username?: string | null;
|
||||||
|
userName?: string | null;
|
||||||
|
nickname?: string | null;
|
||||||
|
company?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
mobile?: string | null;
|
||||||
|
sex?: Api.SystemManage.UserGender | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
loginIp?: string | null;
|
||||||
|
loginDate?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
roles?: Api.SystemManage.RoleSimple[] | null;
|
||||||
|
dept?: Api.SystemManage.DeptSimple | null;
|
||||||
|
position?: Api.SystemManage.PostSimple | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackendFileDTO {
|
||||||
|
id: string | number;
|
||||||
|
configId: string | number;
|
||||||
|
name?: string | null;
|
||||||
|
path: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
let userInfoPromise: Promise<ServiceRequestResult<BackendUserInfoDTO>> | null = null;
|
let userInfoPromise: Promise<ServiceRequestResult<BackendUserInfoDTO>> | null = null;
|
||||||
|
|
||||||
/** 将后端 token 结构转换成前端现有结构 */
|
/** 将后端 token 结构转换成前端现有结构 */
|
||||||
@@ -34,11 +62,48 @@ function mapUserInfo(data: BackendUserInfoDTO): Api.Auth.UserInfo {
|
|||||||
userId: String(data.userId ?? ''),
|
userId: String(data.userId ?? ''),
|
||||||
userName: data.userName ?? '',
|
userName: data.userName ?? '',
|
||||||
nickname: data.nickname ?? '',
|
nickname: data.nickname ?? '',
|
||||||
|
deptId: safeStringId(data.deptId),
|
||||||
roles: data.roles ?? [],
|
roles: data.roles ?? [],
|
||||||
buttons: data.buttons ?? []
|
buttons: data.buttons ?? []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeStringId(value: string | number | null | undefined): string | null {
|
||||||
|
return value === null || value === undefined ? null : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
function mapMyProfileDetail(data: BackendMyProfileDetailDTO, fallbackUserId = ''): Api.Auth.MyProfileDetail {
|
||||||
|
const baseInfo = {
|
||||||
|
userId: String(data.id ?? data.userId ?? fallbackUserId ?? ''),
|
||||||
|
username: data.username ?? data.userName ?? '',
|
||||||
|
nickname: data.nickname ?? '',
|
||||||
|
deptId: safeStringId(data.dept?.id),
|
||||||
|
deptName: data.dept?.name ?? '',
|
||||||
|
positionId: safeStringId(data.position?.id),
|
||||||
|
positionName: data.position?.name ?? ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactInfo = {
|
||||||
|
company: data.company ?? null,
|
||||||
|
email: data.email ?? '',
|
||||||
|
mobile: data.mobile ?? '',
|
||||||
|
sex: data.sex ?? 0,
|
||||||
|
avatar: data.avatar ?? ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const extraInfo = {
|
||||||
|
roles: data.roles ?? [],
|
||||||
|
dept: data.dept ?? null,
|
||||||
|
position: data.position ?? null,
|
||||||
|
loginIp: data.loginIp ?? '',
|
||||||
|
loginDate: data.loginDate ?? null,
|
||||||
|
createTime: data.createTime ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...baseInfo, ...contactInfo, ...extraInfo };
|
||||||
|
}
|
||||||
|
|
||||||
export function clearUserInfoCache() {
|
export function clearUserInfoCache() {
|
||||||
userInfoPromise = null;
|
userInfoPromise = null;
|
||||||
}
|
}
|
||||||
@@ -101,19 +166,88 @@ export async function fetchGetUserInfo(force = false): Promise<ServiceRequestRes
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前登录人资料详情 */
|
||||||
|
export async function fetchGetMyProfileDetail(
|
||||||
|
options: {
|
||||||
|
userId?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<ServiceRequestResult<Api.Auth.MyProfileDetail>> {
|
||||||
|
const result = await request<BackendMyProfileDetailDTO>({
|
||||||
|
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/get`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error || !result.data) {
|
||||||
|
return result as ServiceRequestResult<Api.Auth.MyProfileDetail>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: mapMyProfileDetail(result.data, options.userId ?? '')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新当前登录人基础资料 */
|
||||||
|
export function fetchUpdateMyProfile(data: Api.Auth.UpdateMyProfileParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改当前登录人密码 */
|
||||||
|
export async function fetchUpdateMyAvatar(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const result = await request<BackendFileDTO>({
|
||||||
|
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-avatar`,
|
||||||
|
method: 'put',
|
||||||
|
data: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<BackendFileDTO>, data => ({
|
||||||
|
...data,
|
||||||
|
id: normalizeStringId(data.id),
|
||||||
|
configId: normalizeStringId(data.configId)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateMyPassword(data: Api.Auth.UpdateMyPasswordParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${SYSTEM_SERVICE_PREFIX}/user/profile/update-password`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新 token
|
* 刷新 token
|
||||||
*
|
*
|
||||||
* @param refreshToken 刷新 token
|
* @param refreshToken 刷新 token
|
||||||
*/
|
*/
|
||||||
export function fetchRefreshToken(refreshToken: string) {
|
export async function fetchRefreshToken(refreshToken: string): Promise<ServiceRequestResult<Api.Auth.LoginToken>> {
|
||||||
return request<Api.Auth.LoginToken>({
|
// 后端要求 refreshToken 通过 query 参数传递,且 Content-Type 为 form-urlencoded
|
||||||
|
// skipAuth: 不注入过期 access 头,否则会被网关拦下死循环(网关一律校验 Authorization,不看 PermitAll)
|
||||||
|
const result = await request<BackendLoginToken>({
|
||||||
url: `${SYSTEM_SERVICE_PREFIX}/auth/refresh-token`,
|
url: `${SYSTEM_SERVICE_PREFIX}/auth/refresh-token`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
params: { refreshToken },
|
||||||
refreshToken
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
}
|
skipAuth: true,
|
||||||
|
suppressErrorMessage: true,
|
||||||
|
skipTokenRefresh: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.error || !result.data) {
|
||||||
|
return result as ServiceRequestResult<Api.Auth.LoginToken>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: mapLoginToken(result.data)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult } from './shared';
|
||||||
|
|
||||||
const DICT_TYPE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-type`;
|
const DICT_TYPE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-type`;
|
||||||
const DICT_DATA_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-data`;
|
const DICT_DATA_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-data`;
|
||||||
@@ -15,6 +16,56 @@ function createBatchDeleteQuery(ids: number[]) {
|
|||||||
return query.toString();
|
return query.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
||||||
|
colorType?: string | null;
|
||||||
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
||||||
|
list: DictDataResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
||||||
|
colorType?: string | null;
|
||||||
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
||||||
|
|
||||||
|
function normalizeColorType(value?: string | null) {
|
||||||
|
return value?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
||||||
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
||||||
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSaveDictDataRequest(data: Api.Dict.SaveDictDataParams) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
colorType: normalizeColorType(data.colorType),
|
||||||
|
remark: data.remark?.trim() || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取字典类型分页 */
|
/** 获取字典类型分页 */
|
||||||
export function fetchGetDictTypePage(params?: Api.Dict.DictTypeSearchParams) {
|
export function fetchGetDictTypePage(params?: Api.Dict.DictTypeSearchParams) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictType>>({
|
return request<Api.Dict.PageResult<Api.Dict.DictType>>({
|
||||||
@@ -60,20 +111,40 @@ export function fetchBatchDeleteDictType(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 获取字典数据分页 */
|
/** 获取字典数据分页 */
|
||||||
export function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
export async function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
const result = await request<DictDataPageResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/page`,
|
url: `${DICT_DATA_PREFIX}/page`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.error || !result.data) {
|
||||||
|
return result as unknown as Awaited<ReturnType<typeof request<Api.Dict.PageResult<Api.Dict.DictData>>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: {
|
||||||
|
...result.data,
|
||||||
|
list: result.data.list.map(normalizeDictData)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取前端运行时字典缓存 */
|
/** 获取前端运行时字典缓存 */
|
||||||
export function fetchGetFrontendDictCache() {
|
export async function fetchGetFrontendDictCache() {
|
||||||
return request<Api.Dict.FrontendDictCache>({
|
const result = await request<FrontendDictCacheResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/frontend-cache`,
|
url: `${DICT_DATA_PREFIX}/frontend-cache`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<FrontendDictCacheResponse>,
|
||||||
|
data =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(data).map(([dictType, list]) => [dictType, list.map(normalizeFrontendDictData)])
|
||||||
|
) as Api.Dict.FrontendDictCache
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建字典数据 */
|
/** 创建字典数据 */
|
||||||
@@ -81,7 +152,7 @@ export function fetchCreateDictData(data: Api.Dict.SaveDictDataParams) {
|
|||||||
return request<number>({
|
return request<number>({
|
||||||
url: `${DICT_DATA_PREFIX}/create`,
|
url: `${DICT_DATA_PREFIX}/create`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data: toSaveDictDataRequest(data)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +161,7 @@ export function fetchUpdateDictData(data: { id: number } & Api.Dict.SaveDictData
|
|||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
url: `${DICT_DATA_PREFIX}/update`,
|
url: `${DICT_DATA_PREFIX}/update`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
data: toSaveDictDataRequest(data)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +183,14 @@ export function fetchBatchDeleteDictData(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 通过岗位编码获取该字典的所有字典数据 */
|
/** 通过岗位编码获取该字典的所有字典数据 */
|
||||||
export function fetchGetDictDataByCode(code: string) {
|
export async function fetchGetDictDataByCode(code: string) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
const result = await request<DictDataPageResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/code?code=${code}`,
|
url: `${DICT_DATA_PREFIX}/code?code=${code}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<DictDataPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: (data.list ?? []).map(normalizeDictData)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/service/api/feedback-normalize.ts
Normal file
34
src/service/api/feedback-normalize.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/** 后端统计原始返回(type/status 可能为 number 或 string,count 为整数) */
|
||||||
|
export type FeedbackStatResponse = {
|
||||||
|
total: number;
|
||||||
|
typeCounts?: Array<{ type: string | number; count: number }> | null;
|
||||||
|
statusCounts?: Array<{ status: string | number; count: number }> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 后端统计原始返回 → 业务层(type/status 一律 String 化为 Record 键,count 缺省兜底 0;data 为空全兜底 0) */
|
||||||
|
export function normalizeFeedbackStat(data: FeedbackStatResponse | null | undefined): Api.Feedback.FeedbackStat {
|
||||||
|
const typeCounts: Record<string, number> = {};
|
||||||
|
(data?.typeCounts ?? []).forEach(item => {
|
||||||
|
typeCounts[String(item.type)] = item.count ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusCounts: Record<string, number> = {};
|
||||||
|
(data?.statusCounts ?? []).forEach(item => {
|
||||||
|
statusCounts[String(item.status)] = item.count ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = data?.total ?? 0;
|
||||||
|
|
||||||
|
// 契约约定 total == sum(typeCounts) == sum(statusCounts);dev 下做一致性自检,便于尽早发现后端统计口径错位
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const typeSum = Object.values(typeCounts).reduce((sum, count) => sum + count, 0);
|
||||||
|
const statusSum = Object.values(statusCounts).reduce((sum, count) => sum + count, 0);
|
||||||
|
if (total !== typeSum || total !== statusSum) {
|
||||||
|
console.warn(
|
||||||
|
`[feedback-stat] total=${total} 与分类合计=${typeSum}、状态合计=${statusSum} 不一致,后端统计口径可能有误`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, typeCounts, statusCounts };
|
||||||
|
}
|
||||||
185
src/service/api/feedback.ts
Normal file
185
src/service/api/feedback.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
import { type FeedbackStatResponse, normalizeFeedbackStat } from './feedback-normalize';
|
||||||
|
|
||||||
|
const FEEDBACK_PREFIX = `${SYSTEM_SERVICE_PREFIX}/feedback`;
|
||||||
|
|
||||||
|
type FeedbackResponse = Omit<Api.Feedback.FeedbackItem, 'id' | 'creator' | 'attachments' | 'contentPreview'> & {
|
||||||
|
id: string | number;
|
||||||
|
creator: string | number;
|
||||||
|
/** 后端原始:附件的 JSON 数组字符串(前端约定存完整附件对象,兼容历史纯 URL 数组) */
|
||||||
|
attachmentUrls?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeedbackPageResponse = {
|
||||||
|
total: number;
|
||||||
|
list: FeedbackResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 从代理 URL 末段拆出文件名(兜底历史纯 URL 附件的展示名) */
|
||||||
|
function deriveAttachmentName(url: string): string {
|
||||||
|
try {
|
||||||
|
const path = decodeURI(url.split('?')[0]);
|
||||||
|
const idx = path.lastIndexOf('/');
|
||||||
|
return idx >= 0 ? path.slice(idx + 1) : path;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个附件归一化为业务层 AttachmentItem(ID 铁律:fileId/configId 一律 String)。
|
||||||
|
* - 对象形态(新版存储):按字段对齐;
|
||||||
|
* - 字符串形态(历史仅存 URL):补出 url + 派生 name,fileId 缺省空串(不可下载/清理,仅展示)。
|
||||||
|
*/
|
||||||
|
function normalizeAttachment(raw: unknown): Api.Project.AttachmentItem | null {
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
return { fileId: '', url: raw, name: deriveAttachmentName(raw) };
|
||||||
|
}
|
||||||
|
if (raw && typeof raw === 'object') {
|
||||||
|
const item = raw as Record<string, unknown>;
|
||||||
|
const url = typeof item.url === 'string' ? item.url : '';
|
||||||
|
return {
|
||||||
|
fileId: item.fileId === null || item.fileId === undefined ? '' : String(item.fileId),
|
||||||
|
url,
|
||||||
|
name: typeof item.name === 'string' && item.name ? item.name : deriveAttachmentName(url),
|
||||||
|
size: typeof item.size === 'number' ? item.size : undefined,
|
||||||
|
contentType: typeof item.contentType === 'string' ? item.contentType : undefined,
|
||||||
|
configId: item.configId === null || item.configId === undefined ? undefined : String(item.configId),
|
||||||
|
path: typeof item.path === 'string' ? item.path : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把后端 attachmentUrls(JSON 字符串) 安全解析成附件对象数组;空 / 异常兜底为 [] */
|
||||||
|
function parseAttachments(raw?: string | null): Api.Project.AttachmentItem[] {
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return parsed.map(normalizeAttachment).filter((item): item is Api.Project.AttachmentItem => item !== null);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表「内容」列去富文本标签,取纯文本预览(content 为 HTML,预算一次免每次渲染重扫) */
|
||||||
|
function stripHtml(html: string | null | undefined): string {
|
||||||
|
if (!html) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端记录 → 业务层口径(ID 铁律 String 兜底 + 附件 parse + 内容纯文本预览预算) */
|
||||||
|
function normalizeFeedback(data: FeedbackResponse): Api.Feedback.FeedbackItem {
|
||||||
|
const { attachmentUrls, ...rest } = data;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
id: normalizeStringId(data.id),
|
||||||
|
creator: normalizeStringId(data.creator),
|
||||||
|
attachments: parseAttachments(attachmentUrls),
|
||||||
|
contentPreview: stripHtml(data.content)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件对象数组 → attachmentUrls JSON 字符串(无附件传空串) */
|
||||||
|
function serializeAttachments(attachments: Api.Project.AttachmentItem[]): string {
|
||||||
|
return attachments.length ? JSON.stringify(attachments) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页(全量,不按提交人过滤;默认 createTime 倒序由后端保证) */
|
||||||
|
export async function fetchGetFeedbackPage(params: Api.Feedback.FeedbackSearchParams) {
|
||||||
|
const result = await request<FeedbackPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${FEEDBACK_PREFIX}/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<FeedbackPageResponse>, data => ({
|
||||||
|
total: data.total,
|
||||||
|
list: data.list.map(normalizeFeedback)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计聚合(左侧分面面板;全量口径,无业务筛选入参) */
|
||||||
|
export async function fetchGetFeedbackStat() {
|
||||||
|
const result = await request<FeedbackStatResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${FEEDBACK_PREFIX}/stat`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<FeedbackStatResponse>, normalizeFeedbackStat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情 */
|
||||||
|
export async function fetchGetFeedback(id: string) {
|
||||||
|
const result = await request<FeedbackResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${FEEDBACK_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<FeedbackResponse>, normalizeFeedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交反馈(attachments 对象数组在此 stringify 成 attachmentUrls;无附件传空串;返回新建 id,ID 铁律 String 化) */
|
||||||
|
export async function fetchCreateFeedback(params: Api.Feedback.FeedbackSubmitParams) {
|
||||||
|
const { attachments, ...rest } = params;
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${FEEDBACK_PREFIX}/create`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
attachmentUrls: serializeAttachments(attachments)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新反馈(编辑态;attachments 对象数组在此 stringify 成 attachmentUrls;无附件传空串) */
|
||||||
|
export function fetchUpdateFeedback(params: Api.Feedback.FeedbackUpdateParams) {
|
||||||
|
const { attachments, ...rest } = params;
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${FEEDBACK_PREFIX}/update`,
|
||||||
|
method: 'put',
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
attachmentUrls: serializeAttachments(attachments)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改处理状态(仅超管;状态可任意改,不校验流转) */
|
||||||
|
export function fetchUpdateFeedbackStatus(id: string, status: number) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${FEEDBACK_PREFIX}/${id}/status`,
|
||||||
|
method: 'put',
|
||||||
|
data: { status }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除反馈(软删除;后端按归属二次校验) */
|
||||||
|
export function fetchDeleteFeedback(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${FEEDBACK_PREFIX}/delete`,
|
||||||
|
method: 'delete',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,28 +1,61 @@
|
|||||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult } from './shared';
|
||||||
|
|
||||||
const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`;
|
const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接文件永久代理路径,用于富文本 <img src>。
|
||||||
|
*
|
||||||
|
* 后端 GET 接口匿名访问、Content-Disposition: inline,私有桶下也不会过期。
|
||||||
|
* 调用方拿到上传响应里的 configId + path 后直接调用本函数得到可写入 HTML 的 url。
|
||||||
|
*/
|
||||||
|
export function buildFileProxyUrl(configId: string, path: string) {
|
||||||
|
return `${FILE_PREFIX}/${configId}/get/${encodeURI(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UploadFileResult {
|
export interface UploadFileResult {
|
||||||
/** infra_file.id 的字符串形式(避免 Long 精度丢失) */
|
/** infra_file.id 的字符串形式(避免 Long 精度丢失) */
|
||||||
id: string;
|
id: string;
|
||||||
/** 文件访问 URL:私有桶带签名、公开桶裸 URL */
|
/** 对象存储配置编号(字符串形式),与 path 一起拼接永久代理路径 */
|
||||||
|
configId: string;
|
||||||
|
/** 文件相对路径(含日期目录、文件名),与 configId 一起拼接永久代理路径 */
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* 文件访问 URL:私有桶带签名(24h 过期)、公开桶裸 URL。
|
||||||
|
* ⚠️ 仅供后端调试 / 历史兼容,禁止写进富文本 <img src> —— 会随签名过期导致回显失效。
|
||||||
|
* 富文本图片请用 buildFileProxyUrl(configId, path) 的返回值。
|
||||||
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UploadFileResponse = {
|
||||||
|
id: string | number;
|
||||||
|
configId: string | number;
|
||||||
|
path: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
/** 上传文件(模式一:后端中转) */
|
/** 上传文件(模式一:后端中转) */
|
||||||
export function uploadFile(file: File, directory?: string) {
|
export async function uploadFile(file: File, directory?: string) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
if (directory) {
|
if (directory) {
|
||||||
formData.append('directory', directory);
|
formData.append('directory', directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request<UploadFileResult>({
|
const result = await request<UploadFileResponse>({
|
||||||
url: `${FILE_PREFIX}/upload`,
|
url: `${FILE_PREFIX}/upload`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: formData
|
data: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<UploadFileResponse>, data => ({
|
||||||
|
id: String(data.id),
|
||||||
|
configId: String(data.configId),
|
||||||
|
path: data.path,
|
||||||
|
url: data.url
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './dict';
|
export * from './dict';
|
||||||
|
export * from './feedback';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
|
export * from './infra';
|
||||||
|
export * from './notice';
|
||||||
|
export * from './notify-message';
|
||||||
export * from './object-context';
|
export * from './object-context';
|
||||||
|
export * from './overtime-application';
|
||||||
|
export * from './performance';
|
||||||
|
export * from './personal-item';
|
||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './project';
|
export * from './project';
|
||||||
|
export * from './project-group';
|
||||||
export * from './project-shared';
|
export * from './project-shared';
|
||||||
export * from './route';
|
export * from './route';
|
||||||
export * from './system-manage';
|
export * from './system-manage';
|
||||||
|
export * from './system-log';
|
||||||
|
export * from './work-report';
|
||||||
|
|||||||
208
src/service/api/infra.ts
Normal file
208
src/service/api/infra.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const OBJECT_STATUS_MODEL_PREFIX = `${WEB_SERVICE_PREFIX}/project/status/model`;
|
||||||
|
const OBJECT_STATUS_TRANSITION_PREFIX = `${WEB_SERVICE_PREFIX}/project/status/transition`;
|
||||||
|
|
||||||
|
type ObjectStatusModelResponse = Omit<
|
||||||
|
Api.Infra.ObjectStatusModel,
|
||||||
|
| 'id'
|
||||||
|
| 'initialFlag'
|
||||||
|
| 'terminalFlag'
|
||||||
|
| 'allowEdit'
|
||||||
|
| 'progressExcludedFlag'
|
||||||
|
| 'allowCreateProject'
|
||||||
|
| 'allowCreateRequirement'
|
||||||
|
> & {
|
||||||
|
id: string | number;
|
||||||
|
initialFlag: boolean | number | string | null | undefined;
|
||||||
|
terminalFlag: boolean | number | string | null | undefined;
|
||||||
|
allowEdit: boolean | number | string | null | undefined;
|
||||||
|
progressExcludedFlag: boolean | number | string | null | undefined;
|
||||||
|
allowCreateProject: boolean | number | string | null | undefined;
|
||||||
|
allowCreateRequirement: boolean | number | string | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectStatusTransitionResponse = Omit<Api.Infra.ObjectStatusTransition, 'id' | 'needReason'> & {
|
||||||
|
id: string | number;
|
||||||
|
needReason: boolean | number | string | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectStatusModelPageResponse = Api.Infra.PageResult<ObjectStatusModelResponse>;
|
||||||
|
|
||||||
|
type ObjectStatusTransitionPageResponse = Api.Infra.PageResult<ObjectStatusTransitionResponse>;
|
||||||
|
|
||||||
|
function createBatchDeleteQuery(ids: string[]) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
ids.forEach(id => {
|
||||||
|
query.append('ids', id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!normalized || normalized === '0' || normalized === 'false' || normalized === 'n') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeObjectStatusModel(model: ObjectStatusModelResponse): Api.Infra.ObjectStatusModel {
|
||||||
|
return {
|
||||||
|
...model,
|
||||||
|
id: normalizeStringId(model.id),
|
||||||
|
initialFlag: normalizeBooleanFlag(model.initialFlag),
|
||||||
|
terminalFlag: normalizeBooleanFlag(model.terminalFlag),
|
||||||
|
allowEdit: normalizeBooleanFlag(model.allowEdit),
|
||||||
|
progressExcludedFlag: normalizeBooleanFlag(model.progressExcludedFlag),
|
||||||
|
allowCreateProject: normalizeBooleanFlag(model.allowCreateProject),
|
||||||
|
allowCreateRequirement: normalizeBooleanFlag(model.allowCreateRequirement)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeObjectStatusTransition(transition: ObjectStatusTransitionResponse): Api.Infra.ObjectStatusTransition {
|
||||||
|
return {
|
||||||
|
...transition,
|
||||||
|
id: normalizeStringId(transition.id),
|
||||||
|
needReason: normalizeBooleanFlag(transition.needReason)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetObjectStatusModelPage(params?: Api.Infra.ObjectStatusModelSearchParams) {
|
||||||
|
const result = await request<ObjectStatusModelPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ObjectStatusModelPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeObjectStatusModel)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetObjectStatusModel(id: string) {
|
||||||
|
const result = await request<ObjectStatusModelResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ObjectStatusModelResponse>, normalizeObjectStatusModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateObjectStatusModel(data: Api.Infra.SaveObjectStatusModelParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/create`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateObjectStatusModel(data: { id: string } & Api.Infra.SaveObjectStatusModelParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/update`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteObjectStatusModel(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/delete`,
|
||||||
|
method: 'delete',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchDeleteObjectStatusModel(ids: string[]) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_MODEL_PREFIX}/delete-list?${createBatchDeleteQuery(ids)}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetObjectStatusTransitionPage(params?: Api.Infra.ObjectStatusTransitionSearchParams) {
|
||||||
|
const result = await request<ObjectStatusTransitionPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ObjectStatusTransitionPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeObjectStatusTransition)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetObjectStatusTransition(id: string) {
|
||||||
|
const result = await request<ObjectStatusTransitionResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ObjectStatusTransitionResponse>,
|
||||||
|
normalizeObjectStatusTransition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateObjectStatusTransition(data: Api.Infra.SaveObjectStatusTransitionParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/create`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateObjectStatusTransition(data: { id: string } & Api.Infra.SaveObjectStatusTransitionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/update`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteObjectStatusTransition(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/delete`,
|
||||||
|
method: 'delete',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchDeleteObjectStatusTransition(ids: string[]) {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/delete-list?${createBatchDeleteQuery(ids)}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
28
src/service/api/notice.ts
Normal file
28
src/service/api/notice.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const NOTICE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notice`;
|
||||||
|
|
||||||
|
type NoticeResponse = Omit<Api.Notice.Notice, 'id'> & {
|
||||||
|
id: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeNotice(data: NoticeResponse): Api.Notice.Notice {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: normalizeStringId(data.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取最近公告(status=0,按 id 倒序;登录即可,工作台公告卡片用) */
|
||||||
|
export async function fetchGetRecentNotices(size?: number) {
|
||||||
|
const result = await request<NoticeResponse[]>({
|
||||||
|
url: `${NOTICE_PREFIX}/recent`,
|
||||||
|
method: 'get',
|
||||||
|
params: { size },
|
||||||
|
...safeJsonRequestConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<NoticeResponse[]>, data => data.map(normalizeNotice));
|
||||||
|
}
|
||||||
63
src/service/api/notify-message.ts
Normal file
63
src/service/api/notify-message.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const NOTIFY_MESSAGE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notify-message`;
|
||||||
|
|
||||||
|
type NotifyMessageResponse = Omit<Api.NotifyMessage.NotifyMessage, 'id' | 'level'> & {
|
||||||
|
id: string | number;
|
||||||
|
/** 后端老消息可能不带 level,按可空接收,normalize 时回落普通(1) */
|
||||||
|
level?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MyNotifyMessagePageResponse = Omit<Api.NotifyMessage.PageResult<Api.NotifyMessage.NotifyMessage>, 'list'> & {
|
||||||
|
list: NotifyMessageResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeNotifyMessage(data: NotifyMessageResponse): Api.NotifyMessage.NotifyMessage {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: normalizeStringId(data.id),
|
||||||
|
level: data.level ?? 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前用户未读站内信数量(铃铛红点轮询用) */
|
||||||
|
export function fetchGetUnreadNotifyCount() {
|
||||||
|
return request<number>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/get-unread-count`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页获取我的站内信(消息列表唯一数据源;未读传 readStatus=false、已读传 true) */
|
||||||
|
export async function fetchGetMyNotifyMessagePage(params: Api.NotifyMessage.MyPageParams) {
|
||||||
|
const result = await request<MyNotifyMessagePageResponse>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/my-page`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
...safeJsonRequestConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyNotifyMessagePageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeNotifyMessage)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量标记站内信已读(后端幂等:重复提交、非本人条目均安全) */
|
||||||
|
export function fetchUpdateNotifyMessageRead(ids: string[]) {
|
||||||
|
// 后端约定 ids 逗号分隔
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/update-read?ids=${ids.join(',')}`,
|
||||||
|
method: 'put'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前用户全部站内信标记已读 */
|
||||||
|
export function fetchUpdateAllNotifyMessageRead() {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/update-all-read`,
|
||||||
|
method: 'put'
|
||||||
|
});
|
||||||
|
}
|
||||||
349
src/service/api/overtime-application.ts
Normal file
349
src/service/api/overtime-application.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ProjectLocalDateValue, normalizeProjectLocalDate } from './project-shared';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const OVERTIME_APPLICATION_PREFIX = `${WEB_SERVICE_PREFIX}/project/overtime-applications`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
|
||||||
|
type OvertimeApplicationResponse = Omit<
|
||||||
|
Api.OvertimeApplication.OvertimeApplication,
|
||||||
|
'id' | 'applicantId' | 'approverId' | 'overtimeDate' | 'allowEdit' | 'terminal'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
applicantId: StringIdResponse;
|
||||||
|
approverId: StringIdResponse;
|
||||||
|
overtimeDate: ProjectLocalDateValue;
|
||||||
|
allowEdit?: boolean | number | string | null;
|
||||||
|
terminal?: boolean | number | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OvertimeApplicationPageResponse = Omit<Api.OvertimeApplication.OvertimeApplicationPageResult, 'total' | 'list'> & {
|
||||||
|
total: number | string;
|
||||||
|
list: OvertimeApplicationResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type OvertimeApplicationApprovalRecordResponse = Omit<
|
||||||
|
Api.OvertimeApplication.OvertimeApplicationApprovalRecord,
|
||||||
|
'id' | 'overtimeApplicationId' | 'statusLogId' | 'auditorUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
overtimeApplicationId: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
auditorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamOvertimeSummaryResponse = Omit<
|
||||||
|
Api.OvertimeApplication.TeamOvertimeSummary,
|
||||||
|
'overtimeDateStart' | 'overtimeDateEnd'
|
||||||
|
> & {
|
||||||
|
overtimeDateStart?: unknown;
|
||||||
|
overtimeDateEnd?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeDateText(value: unknown) {
|
||||||
|
if (value === null || value === undefined) return undefined;
|
||||||
|
const text = String(value).trim();
|
||||||
|
const commaDateMatch = text.match(/^(\d{4}),(\d{1,2}),(\d{1,2})$/);
|
||||||
|
|
||||||
|
if (commaDateMatch) {
|
||||||
|
const [, year, month, day] = commaDateMatch;
|
||||||
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
|
||||||
|
return !['', '0', 'false', 'n', 'no'].includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTotal(total: number | string) {
|
||||||
|
const value = Number(total);
|
||||||
|
|
||||||
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOvertimeApplication(
|
||||||
|
response: OvertimeApplicationResponse
|
||||||
|
): Api.OvertimeApplication.OvertimeApplication {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
applicantId: normalizeStringId(response.applicantId),
|
||||||
|
approverId: normalizeStringId(response.approverId),
|
||||||
|
overtimeDate: normalizeProjectLocalDate(response.overtimeDate) ?? '',
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
approvalComment: response.approvalComment ?? null,
|
||||||
|
approvalTime: response.approvalTime ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApprovalRecord(
|
||||||
|
response: OvertimeApplicationApprovalRecordResponse
|
||||||
|
): Api.OvertimeApplication.OvertimeApplicationApprovalRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
overtimeApplicationId: normalizeStringId(response.overtimeApplicationId),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageQuery(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
|
|
||||||
|
if (params.applicantIds !== null && params.applicantIds !== undefined) {
|
||||||
|
if (params.applicantIds.length) {
|
||||||
|
params.applicantIds.forEach(item => {
|
||||||
|
if (item) {
|
||||||
|
query.append('applicantIds', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
query.append('applicantIds', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.keyword) {
|
||||||
|
query.append('keyword', params.keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.applicantName) {
|
||||||
|
query.append('applicantName', params.applicantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.approverId) {
|
||||||
|
query.append('approverId', params.approverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.approverName) {
|
||||||
|
query.append('approverName', params.approverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.statusCode) {
|
||||||
|
query.append('statusCode', params.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.overtimeDate?.forEach(item => {
|
||||||
|
if (item) {
|
||||||
|
query.append('overtimeDate', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
params.createTime?.forEach(item => {
|
||||||
|
if (item) {
|
||||||
|
query.append('createTime', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSaveRequest(data: Api.OvertimeApplication.SaveOvertimeApplicationParams) {
|
||||||
|
return {
|
||||||
|
overtimeDate: data.overtimeDate,
|
||||||
|
overtimeDuration: data.overtimeDuration,
|
||||||
|
overtimeReason: data.overtimeReason.trim(),
|
||||||
|
overtimeContent: data.overtimeContent.trim(),
|
||||||
|
approverId: data.approverId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStatusActionRequest(data: Api.OvertimeApplication.StatusActionParams = {}) {
|
||||||
|
return {
|
||||||
|
reason: data.reason?.trim() || undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationPage(
|
||||||
|
params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}
|
||||||
|
) {
|
||||||
|
const query = createPageQuery(params);
|
||||||
|
|
||||||
|
const result = await request<OvertimeApplicationPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${OVERTIME_APPLICATION_PREFIX}/page?${query}` : `${OVERTIME_APPLICATION_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationPageResponse>, data => ({
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(normalizeOvertimeApplication)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationApprovalPage(
|
||||||
|
params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}
|
||||||
|
) {
|
||||||
|
const query = createPageQuery(params);
|
||||||
|
|
||||||
|
const result = await request<OvertimeApplicationPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query
|
||||||
|
? `${OVERTIME_APPLICATION_PREFIX}/approval-page?${query}`
|
||||||
|
: `${OVERTIME_APPLICATION_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationPageResponse>, data => ({
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(normalizeOvertimeApplication)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationDetail(id: string) {
|
||||||
|
const result = await request<OvertimeApplicationResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationResponse>, normalizeOvertimeApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateOvertimeApplication(data: Api.OvertimeApplication.SaveOvertimeApplicationParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: OVERTIME_APPLICATION_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateRejectedOvertimeApplication(
|
||||||
|
id: string,
|
||||||
|
data: Api.OvertimeApplication.SaveOvertimeApplicationParams
|
||||||
|
) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toSaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchApproveOvertimeApplication(
|
||||||
|
data: Api.OvertimeApplication.OvertimeApplicationBatchActionParams
|
||||||
|
) {
|
||||||
|
return request<Api.OvertimeApplication.OvertimeApplicationBatchActionResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/batch-approve`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchRejectOvertimeApplication(
|
||||||
|
data: Api.OvertimeApplication.OvertimeApplicationBatchActionParams
|
||||||
|
) {
|
||||||
|
return request<Api.OvertimeApplication.OvertimeApplicationBatchActionResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/batch-reject`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteOvertimeApplication(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationApprovalRecords(id: string) {
|
||||||
|
const result = await request<OvertimeApplicationApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationStatusDict() {
|
||||||
|
const result = await request<Api.OvertimeApplication.OvertimeApplicationStatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/status/dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.OvertimeApplication.OvertimeApplicationStatusDict[]>,
|
||||||
|
data => data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetTeamOvertimeSummary(params: Api.OvertimeApplication.TeamOvertimeSummaryParams = {}) {
|
||||||
|
const result = await request<TeamOvertimeSummaryResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/team/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamOvertimeSummaryResponse>, data => {
|
||||||
|
if (!data) return data;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
overtimeDateStart: normalizeDateText(data.overtimeDateStart) || '',
|
||||||
|
overtimeDateEnd: normalizeDateText(data.overtimeDateEnd) || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportOvertimeApplications(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
||||||
|
const query = createPageQuery(params);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${OVERTIME_APPLICATION_PREFIX}/export?${query}` : `${OVERTIME_APPLICATION_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
501
src/service/api/performance.ts
Normal file
501
src/service/api/performance.ts
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
normalizeStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
|
||||||
|
const TEMPLATE_PREFIX = `${WEB_SERVICE_PREFIX}/project/performance-templates`;
|
||||||
|
const SHEET_PREFIX = `${WEB_SERVICE_PREFIX}/project/performance-sheets`;
|
||||||
|
const TEAM_PREFIX = `${SHEET_PREFIX}/team`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
|
||||||
|
type TemplateResponse = Omit<Api.Performance.Template.Template, 'id' | 'fileId' | 'uploadUserId' | 'activeFlag'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
fileId: StringIdResponse;
|
||||||
|
uploadUserId: StringIdResponse;
|
||||||
|
activeFlag?: boolean | number | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TemplatePageResponse = {
|
||||||
|
total: number | string;
|
||||||
|
list: TemplateResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SheetResponse = Omit<
|
||||||
|
Api.Performance.Sheet.Sheet,
|
||||||
|
'id' | 'employeeId' | 'employeeDeptId' | 'managerId' | 'templateId' | 'fileId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
employeeId: StringIdResponse;
|
||||||
|
employeeDeptId: StringIdResponse;
|
||||||
|
managerId: StringIdResponse;
|
||||||
|
templateId: StringIdResponse;
|
||||||
|
fileId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SheetPageResponse = {
|
||||||
|
total: number | string;
|
||||||
|
list: SheetResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusLogResponse = Omit<Api.Performance.Sheet.StatusLog, 'id' | 'sheetId' | 'operatorUserId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
sheetId: StringIdResponse;
|
||||||
|
operatorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseRecordResponse = Omit<
|
||||||
|
Api.Performance.Sheet.ResponseRecord,
|
||||||
|
'id' | 'sheetId' | 'statusLogId' | 'responderUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
sheetId: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
responderUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthlyResultResponse = Omit<Api.Performance.Sheet.MonthlyResult, 'sheetId' | 'employeeId'> & {
|
||||||
|
sheetId?: StringIdResponse | null;
|
||||||
|
employeeId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamSummaryResponse = Omit<
|
||||||
|
Api.Performance.Team.Summary,
|
||||||
|
| 'totalSheetCount'
|
||||||
|
| 'pendingSendCount'
|
||||||
|
| 'pendingConfirmCount'
|
||||||
|
| 'pendingSendUsers'
|
||||||
|
| 'pendingConfirmUsers'
|
||||||
|
| 'deptOrgAverages'
|
||||||
|
> & {
|
||||||
|
totalSheetCount?: number | string | null;
|
||||||
|
pendingSendCount?: number | string | null;
|
||||||
|
pendingConfirmCount?: number | string | null;
|
||||||
|
pendingSendUsers?: Array<
|
||||||
|
Omit<Api.Performance.Team.PendingSendUser, 'userId' | 'managerUserId' | 'sheetId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
managerUserId: StringIdResponse;
|
||||||
|
sheetId?: StringIdResponse | null;
|
||||||
|
}
|
||||||
|
> | null;
|
||||||
|
pendingConfirmUsers?: Array<
|
||||||
|
Omit<Api.Performance.Team.PendingConfirmUser, 'userId' | 'sheetId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
sheetId: StringIdResponse;
|
||||||
|
}
|
||||||
|
> | null;
|
||||||
|
deptOrgAverages?: Array<
|
||||||
|
Omit<Api.Performance.Team.DeptOrgAverage, 'deptId' | 'confirmedCount'> & {
|
||||||
|
deptId: StringIdResponse;
|
||||||
|
confirmedCount?: number | string | null;
|
||||||
|
}
|
||||||
|
> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
if (typeof value === 'number') return value === 1;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
|
||||||
|
return !['', '0', 'false', 'n', 'no'].includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTotal(value: number | string | null | undefined) {
|
||||||
|
const total = Number(value ?? 0);
|
||||||
|
|
||||||
|
return Number.isFinite(total) ? Math.max(0, total) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTemplate(response: TemplateResponse): Api.Performance.Template.Template {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
fileId: normalizeStringId(response.fileId),
|
||||||
|
uploadUserId: normalizeStringId(response.uploadUserId),
|
||||||
|
activeFlag: normalizeBooleanFlag(response.activeFlag),
|
||||||
|
remark: response.remark ?? null,
|
||||||
|
scoreCellMapping: response.scoreCellMapping ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSheet(response: SheetResponse): Api.Performance.Sheet.Sheet {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
employeeId: normalizeStringId(response.employeeId),
|
||||||
|
employeeDeptId: normalizeStringId(response.employeeDeptId),
|
||||||
|
managerId: normalizeStringId(response.managerId),
|
||||||
|
templateId: normalizeStringId(response.templateId),
|
||||||
|
fileId: normalizeNullableStringId(response.fileId),
|
||||||
|
fileName: response.fileName ?? null,
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
actualScoreTotal: response.actualScoreTotal ?? null,
|
||||||
|
baseScoreTotal: response.baseScoreTotal ?? null,
|
||||||
|
extraScoreTotal: response.extraScoreTotal ?? null,
|
||||||
|
sentTime: response.sentTime ?? null,
|
||||||
|
confirmedTime: response.confirmedTime ?? null,
|
||||||
|
rejectedTime: response.rejectedTime ?? null,
|
||||||
|
lastStatusReason: response.lastStatusReason ?? null,
|
||||||
|
createTime: response.createTime ?? null,
|
||||||
|
updateTime: response.updateTime ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStatusLog(response: StatusLogResponse): Api.Performance.Sheet.StatusLog {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
sheetId: normalizeStringId(response.sheetId),
|
||||||
|
operatorUserId: normalizeStringId(response.operatorUserId),
|
||||||
|
reason: response.reason ?? null,
|
||||||
|
remark: response.remark ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResponseRecord(response: ResponseRecordResponse): Api.Performance.Sheet.ResponseRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
sheetId: normalizeStringId(response.sheetId),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
responderUserId: normalizeStringId(response.responderUserId),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMonthlyResult(response: MonthlyResultResponse): Api.Performance.Sheet.MonthlyResult {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
sheetId: normalizeNullableStringId(response.sheetId),
|
||||||
|
employeeId: normalizeStringId(response.employeeId),
|
||||||
|
actualScoreTotal: response.actualScoreTotal ?? null,
|
||||||
|
baseScoreTotal: response.baseScoreTotal ?? null,
|
||||||
|
extraScoreTotal: response.extraScoreTotal ?? null,
|
||||||
|
statusCode: response.statusCode ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTeamSummary(response: TeamSummaryResponse): Api.Performance.Team.Summary {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
totalSheetCount: normalizeTotal(response.totalSheetCount),
|
||||||
|
pendingSendCount: normalizeTotal(response.pendingSendCount),
|
||||||
|
pendingConfirmCount: normalizeTotal(response.pendingConfirmCount),
|
||||||
|
pendingSendUsers: (response.pendingSendUsers || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId),
|
||||||
|
managerUserId: normalizeStringId(item.managerUserId),
|
||||||
|
sheetId: normalizeNullableStringId(item.sheetId),
|
||||||
|
statusCode: item.statusCode ?? null
|
||||||
|
})),
|
||||||
|
pendingConfirmUsers: (response.pendingConfirmUsers || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId),
|
||||||
|
sheetId: normalizeStringId(item.sheetId),
|
||||||
|
sentTime: item.sentTime ?? null
|
||||||
|
})),
|
||||||
|
deptOrgAverages: (response.deptOrgAverages || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
deptId: normalizeStringId(item.deptId),
|
||||||
|
averageScore: item.averageScore ?? null,
|
||||||
|
confirmedCount: normalizeTotal(item.confirmedCount)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (!value.length) {
|
||||||
|
query.append(key, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.forEach(item => appendValue(query, key, item));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatToYYYYMM(value?: string | null) {
|
||||||
|
if (!value) return '';
|
||||||
|
|
||||||
|
const d = dayjs(value);
|
||||||
|
|
||||||
|
return d.isValid() ? d.format('YYYY-MM') : value.slice(0, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSheetQuery(params: Api.Performance.Sheet.SearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
|
appendValue(query, 'employeeIds', params.employeeIds);
|
||||||
|
// 将 periodMonthRange 拆为 periodMonthStart / periodMonthEnd
|
||||||
|
if (params.periodMonthRange?.length === 2) {
|
||||||
|
appendValue(query, 'periodMonthStart', formatToYYYYMM(params.periodMonthRange[0]));
|
||||||
|
appendValue(query, 'periodMonthEnd', formatToYYYYMM(params.periodMonthRange[1]));
|
||||||
|
}
|
||||||
|
// employeeId 单选追加到 employeeIds
|
||||||
|
if (params.employeeId) {
|
||||||
|
query.append('employeeIds', params.employeeId);
|
||||||
|
}
|
||||||
|
appendValue(query, 'employeeDeptId', params.employeeDeptId);
|
||||||
|
appendValue(query, 'managerName', params.managerName);
|
||||||
|
appendValue(query, 'statusCode', params.statusCode);
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTemplateQuery(params: Api.Performance.Template.SearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
|
appendValue(query, 'templateName', params.templateName);
|
||||||
|
appendValue(query, 'activeFlag', params.activeFlag);
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceTemplateCurrent() {
|
||||||
|
const result = await request<TemplateResponse | null>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEMPLATE_PREFIX}/current`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TemplateResponse | null>, data =>
|
||||||
|
data ? normalizeTemplate(data) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceTemplatePage(params: Api.Performance.Template.SearchParams = {}) {
|
||||||
|
const query = createTemplateQuery(params);
|
||||||
|
const result = await request<TemplatePageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${TEMPLATE_PREFIX}/page?${query}` : `${TEMPLATE_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TemplatePageResponse>, data => ({
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(normalizeTemplate)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadPerformanceTemplate(data: Api.Performance.Template.UploadParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEMPLATE_PREFIX}/upload`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activatePerformanceTemplate(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEMPLATE_PREFIX}/${id}/activate`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceSheetPage(params: Api.Performance.Sheet.SearchParams = {}) {
|
||||||
|
const query = createSheetQuery(params);
|
||||||
|
const result = await request<SheetPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${SHEET_PREFIX}/page?${query}` : `${SHEET_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<SheetPageResponse>, data => ({
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(normalizeSheet)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceSheet(id: string) {
|
||||||
|
const result = await request<SheetResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<SheetResponse>, normalizeSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPerformanceSheet(data: Api.Performance.Sheet.CreateParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: SHEET_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePerformanceSheetExcel(id: string, data: Api.Performance.Sheet.ExcelUpdateParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/excel`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePerformanceSheet(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendPerformanceSheet(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/send`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resendPerformanceSheet(id: string) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/resend`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmPerformanceSheet(id: string, data: Api.Performance.Sheet.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/confirm`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rejectPerformanceSheet(id: string, data: Api.Performance.Sheet.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadPerformanceSheet(id: string) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${SHEET_PREFIX}/${id}/download`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchDownloadPerformanceSheets(data: Api.Performance.Sheet.BatchDownloadParams) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/batch-download`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportPerformanceSheets(params: Api.Performance.Sheet.SearchParams = {}) {
|
||||||
|
const query = createSheetQuery(params);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${SHEET_PREFIX}/export?${query}` : `${SHEET_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceSheetStatusLogs(id: string) {
|
||||||
|
const result = await request<StatusLogResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/status-logs`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StatusLogResponse[]>, data => data.map(normalizeStatusLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceSheetResponseRecords(id: string) {
|
||||||
|
const result = await request<ResponseRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/${id}/response-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ResponseRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeResponseRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPerformanceMonthlyResult(employeeId: string, periodMonth: string) {
|
||||||
|
const result = await request<MonthlyResultResponse | null>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/monthly-result`,
|
||||||
|
method: 'get',
|
||||||
|
params: { employeeId, periodMonth }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyResultResponse | null>, data =>
|
||||||
|
data ? normalizeMonthlyResult(data) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPerformanceSheetStatusDict() {
|
||||||
|
return request<Api.Performance.Sheet.StatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/status-dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPerformanceSheetStatusTransitions() {
|
||||||
|
return request<Api.Performance.Sheet.StatusTransition[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${SHEET_PREFIX}/status-transitions`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTeamPerformanceSummary(params: Api.Performance.Team.SummaryParams = {}) {
|
||||||
|
const result = await request<TeamSummaryResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEAM_PREFIX}/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamSummaryResponse>, normalizeTeamSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remindTeamPerformance(data: Api.Performance.Team.RemindParams) {
|
||||||
|
return request<Api.Performance.Team.RemindResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEAM_PREFIX}/remind`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
880
src/service/api/personal-item.ts
Normal file
880
src/service/api/personal-item.ts
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import type { ConfigType } from 'dayjs';
|
||||||
|
import type { FlatResponseData } from '@sa/axios';
|
||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ProjectExecutionResponse,
|
||||||
|
type TaskWorklogResponse,
|
||||||
|
normalizeProjectLocalDate,
|
||||||
|
normalizeTaskWorklog
|
||||||
|
} from './project-shared';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
type PersonalItemRecord = Api.PersonalItem.PersonalItem;
|
||||||
|
type PersonalItemWorklogRecord = Api.Project.TaskWorklog;
|
||||||
|
type PersonalItemResult<T> = Promise<FlatResponseData<any, T>>;
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
type PersonalItemLocalDateValue = string | number[] | null;
|
||||||
|
type AttachmentItemResponse = Omit<Api.Project.AttachmentItem, 'fileId'> & {
|
||||||
|
fileId?: StringIdResponse;
|
||||||
|
id?: StringIdResponse;
|
||||||
|
};
|
||||||
|
type PersonalItemLifecycleActionResponse = Omit<Api.PersonalItem.PersonalItemLifecycleAction, 'needReason'> & {
|
||||||
|
needReason?: boolean | number | string | null;
|
||||||
|
};
|
||||||
|
type PersonalItemResponse = Omit<
|
||||||
|
Api.PersonalItem.PersonalItem,
|
||||||
|
| 'id'
|
||||||
|
| 'ownerId'
|
||||||
|
| 'terminal'
|
||||||
|
| 'allowEdit'
|
||||||
|
| 'availableActions'
|
||||||
|
| 'plannedStartDate'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'actualStartDate'
|
||||||
|
| 'actualEndDate'
|
||||||
|
| 'attachments'
|
||||||
|
| 'totalSpentHours'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
ownerId: StringIdResponse;
|
||||||
|
terminal?: boolean | number | string | null;
|
||||||
|
allowEdit?: boolean | number | string | null;
|
||||||
|
availableActions?: PersonalItemLifecycleActionResponse[] | null;
|
||||||
|
plannedStartDate?: PersonalItemLocalDateValue;
|
||||||
|
plannedEndDate?: PersonalItemLocalDateValue;
|
||||||
|
actualStartDate?: PersonalItemLocalDateValue;
|
||||||
|
actualEndDate?: PersonalItemLocalDateValue;
|
||||||
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
|
progressRate?: number | null;
|
||||||
|
totalSpentHours?: number | string | null;
|
||||||
|
};
|
||||||
|
type PersonalItemPageResponse = Omit<Api.PersonalItem.PersonalItemPageResult, 'total' | 'list'> & {
|
||||||
|
total: number | string;
|
||||||
|
list: PersonalItemResponse[];
|
||||||
|
};
|
||||||
|
type PersonalItemWorklogPageResponse = Api.Project.PageResult<TaskWorklogResponse>;
|
||||||
|
type PersonalItemExecutionOptionResponse = ProjectExecutionResponse & {
|
||||||
|
projectName?: string | null;
|
||||||
|
};
|
||||||
|
type PersonalItemSaveRequest = {
|
||||||
|
executionId?: string;
|
||||||
|
taskTitle: string;
|
||||||
|
type: string;
|
||||||
|
progressRate?: number;
|
||||||
|
plannedStartDate?: string;
|
||||||
|
plannedEndDate?: string;
|
||||||
|
taskDesc?: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
id?: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
contentType?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
type PersonalItemWorklogSaveRequest = {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
durationHours: number;
|
||||||
|
progressRate: number;
|
||||||
|
workContent?: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
id?: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
contentType?: string;
|
||||||
|
}>;
|
||||||
|
difficulty: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PERSONAL_ITEM_PREFIX = `${WEB_SERVICE_PREFIX}/project/personal-items`;
|
||||||
|
|
||||||
|
const CURRENT_USER_ID = 'current-user';
|
||||||
|
const CURRENT_USER_NAME = '当前用户';
|
||||||
|
|
||||||
|
const personalItems: PersonalItemRecord[] = createSeedItems();
|
||||||
|
const personalItemWorklogs: PersonalItemWorklogRecord[] = createSeedWorklogs();
|
||||||
|
const executionOptions: Api.PersonalItem.PersonalItemExecutionOption[] = createExecutionOptions();
|
||||||
|
|
||||||
|
function createSuccessResult<T>(data: T): PersonalItemResult<T> {
|
||||||
|
return Promise.resolve({
|
||||||
|
data,
|
||||||
|
error: null,
|
||||||
|
response: undefined
|
||||||
|
} as unknown as FlatResponseData<any, T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePageTotal(total: number | string) {
|
||||||
|
const value = Number(total);
|
||||||
|
|
||||||
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null {
|
||||||
|
if (!list) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.map(item => {
|
||||||
|
const rawId = item.fileId ?? item.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
fileId: rawId === null || rawId === undefined ? '' : String(rawId)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!normalized || normalized === '0' || normalized === 'false' || normalized === 'n') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLifecycleActions(
|
||||||
|
actions?: PersonalItemLifecycleActionResponse[] | null
|
||||||
|
): Api.PersonalItem.PersonalItemLifecycleAction[] {
|
||||||
|
return (actions ?? []).map(action => ({
|
||||||
|
actionCode: action.actionCode,
|
||||||
|
actionName: action.actionName ?? '',
|
||||||
|
needReason: normalizeBooleanFlag(action.needReason)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePersonalItem(response: PersonalItemResponse): Api.PersonalItem.PersonalItem {
|
||||||
|
return {
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
taskTitle: response.taskTitle ?? '',
|
||||||
|
type: response.type ?? '',
|
||||||
|
ownerId: normalizeStringId(response.ownerId),
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
availableActions: normalizeLifecycleActions(response.availableActions),
|
||||||
|
progressRate:
|
||||||
|
typeof response.progressRate === 'number' ? response.progressRate : Number(response.progressRate ?? 0),
|
||||||
|
totalSpentHours: (() => {
|
||||||
|
if (typeof response.totalSpentHours === 'number') {
|
||||||
|
return response.totalSpentHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.totalSpentHours === null || response.totalSpentHours === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(response.totalSpentHours);
|
||||||
|
})(),
|
||||||
|
plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
||||||
|
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
||||||
|
taskDesc: response.taskDesc ?? null,
|
||||||
|
lastStatusReason: response.lastStatusReason ?? null,
|
||||||
|
attachments: normalizeAttachments(response.attachments),
|
||||||
|
creator: response.creator ?? '',
|
||||||
|
createTime: response.createTime ?? '',
|
||||||
|
updater: response.updater ?? '',
|
||||||
|
updateTime: response.updateTime ?? '',
|
||||||
|
deleted: Boolean(response.deleted),
|
||||||
|
ownerName: response.ownerName ?? null,
|
||||||
|
ownerNickname: response.ownerNickname ?? null,
|
||||||
|
statusName: response.statusName ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePersonalItemExecutionOption(
|
||||||
|
response: PersonalItemExecutionOptionResponse
|
||||||
|
): Api.PersonalItem.PersonalItemExecutionOption {
|
||||||
|
return {
|
||||||
|
executionId: normalizeStringId(response.id),
|
||||||
|
executionName: response.executionName ?? '',
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
projectName: response.projectName ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalItemSaveRequest(data: Api.PersonalItem.SavePersonalItemParams): PersonalItemSaveRequest {
|
||||||
|
return {
|
||||||
|
executionId: data.executionId ?? undefined,
|
||||||
|
taskTitle: data.taskTitle.trim(),
|
||||||
|
type: data.type,
|
||||||
|
progressRate: typeof data.progressRate === 'number' ? data.progressRate : undefined,
|
||||||
|
plannedStartDate: data.plannedStartDate ?? undefined,
|
||||||
|
plannedEndDate: data.plannedEndDate ?? undefined,
|
||||||
|
taskDesc: data.taskDesc ?? undefined,
|
||||||
|
attachments:
|
||||||
|
data.attachments?.map(item => ({
|
||||||
|
id: item.fileId || undefined,
|
||||||
|
url: item.url,
|
||||||
|
name: item.name,
|
||||||
|
size: item.size,
|
||||||
|
contentType: item.contentType
|
||||||
|
})) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalItemWorklogSaveRequest(
|
||||||
|
data: Api.PersonalItem.SavePersonalItemWorklogParams
|
||||||
|
): PersonalItemWorklogSaveRequest {
|
||||||
|
return {
|
||||||
|
startDate: data.startDate,
|
||||||
|
endDate: data.endDate,
|
||||||
|
durationHours: Number(data.durationHours.toFixed(1)),
|
||||||
|
progressRate: Number(data.progressRate.toFixed(2)),
|
||||||
|
workContent: data.workContent ?? undefined,
|
||||||
|
attachments:
|
||||||
|
data.attachments?.map(item => ({
|
||||||
|
id: item.fileId || undefined,
|
||||||
|
url: item.url,
|
||||||
|
name: item.name,
|
||||||
|
size: item.size,
|
||||||
|
contentType: item.contentType
|
||||||
|
})) ?? undefined,
|
||||||
|
difficulty: data.difficulty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPersonalItemPageQuery(params: Api.PersonalItem.PersonalItemSearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
|
|
||||||
|
if (params.keyword) {
|
||||||
|
query.append('keyword', params.keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.ownerId) {
|
||||||
|
query.append('ownerId', params.ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.statusCode) {
|
||||||
|
query.append('statusCode', params.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.updateTime?.forEach(item => {
|
||||||
|
if (item) {
|
||||||
|
query.append('updateTime', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIdsQuery(ids: string[]) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
ids.forEach(id => {
|
||||||
|
if (id) {
|
||||||
|
query.append('ids', id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBindExecutionQuery(payload: Api.PersonalItem.BindPersonalItemExecutionParams) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
payload.ids.forEach(id => {
|
||||||
|
if (id) {
|
||||||
|
query.append('itemIds', id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
query.append('executionId', payload.executionId);
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneAttachment(item: Api.Project.AttachmentItem): Api.Project.AttachmentItem {
|
||||||
|
return { ...item };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneItem(item: PersonalItemRecord): PersonalItemRecord {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
attachments: item.attachments?.map(cloneAttachment) ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneWorklog(item: PersonalItemWorklogRecord): PersonalItemWorklogRecord {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
attachments: item.attachments?.map(cloneAttachment) ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateTime(value?: ConfigType | null) {
|
||||||
|
const target = value ? dayjs(value) : dayjs();
|
||||||
|
return target.isValid() ? target.format('YYYY-MM-DD HH:mm:ss') : dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDate(value?: ConfigType | null) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = dayjs(value);
|
||||||
|
return target.isValid() ? target.format('YYYY-MM-DD') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeedItems(): PersonalItemRecord[] {
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'personal-item-1',
|
||||||
|
taskTitle: '整理供应商沟通纪要',
|
||||||
|
type: 'daily',
|
||||||
|
ownerId: CURRENT_USER_ID,
|
||||||
|
statusCode: 'active',
|
||||||
|
progressRate: 45,
|
||||||
|
plannedStartDate: normalizeDate(now.subtract(3, 'day')),
|
||||||
|
plannedEndDate: normalizeDate(now.add(2, 'day')),
|
||||||
|
actualStartDate: normalizeDate(now.subtract(2, 'day')),
|
||||||
|
actualEndDate: null,
|
||||||
|
taskDesc: '<p>补齐今天会议纪要,沉淀成一页内部记录,便于后续同步。</p>',
|
||||||
|
lastStatusReason: null,
|
||||||
|
attachments: null,
|
||||||
|
creator: CURRENT_USER_NAME,
|
||||||
|
createTime: normalizeDateTime(now.subtract(3, 'day').hour(9).minute(20).second(0)),
|
||||||
|
updater: CURRENT_USER_NAME,
|
||||||
|
updateTime: normalizeDateTime(now.subtract(2, 'hour')),
|
||||||
|
deleted: false,
|
||||||
|
ownerName: CURRENT_USER_NAME,
|
||||||
|
statusName: '进行中'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'personal-item-2',
|
||||||
|
taskTitle: '清理浏览器收藏夹里的项目入口',
|
||||||
|
type: 'daily',
|
||||||
|
ownerId: CURRENT_USER_ID,
|
||||||
|
statusCode: 'pending',
|
||||||
|
progressRate: 0,
|
||||||
|
plannedStartDate: normalizeDate(now.add(1, 'day')),
|
||||||
|
plannedEndDate: normalizeDate(now.add(4, 'day')),
|
||||||
|
actualStartDate: null,
|
||||||
|
actualEndDate: null,
|
||||||
|
taskDesc: '<p>把已经废弃的测试环境、旧文档入口统一清理。</p>',
|
||||||
|
lastStatusReason: null,
|
||||||
|
attachments: null,
|
||||||
|
creator: CURRENT_USER_NAME,
|
||||||
|
createTime: normalizeDateTime(now.subtract(2, 'day').hour(14).minute(10).second(0)),
|
||||||
|
updater: CURRENT_USER_NAME,
|
||||||
|
updateTime: normalizeDateTime(now.subtract(5, 'hour')),
|
||||||
|
deleted: false,
|
||||||
|
ownerName: CURRENT_USER_NAME,
|
||||||
|
statusName: '待处理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'personal-item-3',
|
||||||
|
taskTitle: '补充账号开通说明截图',
|
||||||
|
type: 'support',
|
||||||
|
ownerId: CURRENT_USER_ID,
|
||||||
|
statusCode: 'completed',
|
||||||
|
progressRate: 100,
|
||||||
|
plannedStartDate: normalizeDate(now.subtract(5, 'day')),
|
||||||
|
plannedEndDate: normalizeDate(now.subtract(2, 'day')),
|
||||||
|
actualStartDate: normalizeDate(now.subtract(5, 'day')),
|
||||||
|
actualEndDate: normalizeDate(now.subtract(1, 'day')),
|
||||||
|
taskDesc: '<p>为新同事入职说明补一版截图,后续发在群公告。</p>',
|
||||||
|
lastStatusReason: '已完成并同步团队',
|
||||||
|
attachments: null,
|
||||||
|
creator: CURRENT_USER_NAME,
|
||||||
|
createTime: normalizeDateTime(now.subtract(5, 'day').hour(11).minute(0).second(0)),
|
||||||
|
updater: CURRENT_USER_NAME,
|
||||||
|
updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(30).second(0)),
|
||||||
|
deleted: false,
|
||||||
|
ownerName: CURRENT_USER_NAME,
|
||||||
|
statusName: '已完成'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSeedWorklogs(): PersonalItemWorklogRecord[] {
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'worklog-1',
|
||||||
|
taskId: 'personal-item-1',
|
||||||
|
userId: CURRENT_USER_ID,
|
||||||
|
userNickname: CURRENT_USER_NAME,
|
||||||
|
startDate: normalizeDate(now.subtract(2, 'day'))!,
|
||||||
|
endDate: normalizeDate(now.subtract(2, 'day'))!,
|
||||||
|
durationHours: 2.5,
|
||||||
|
progressRate: 30,
|
||||||
|
difficulty: '2',
|
||||||
|
workContent: '整理会议录音和重点结论,先输出初版纪要。',
|
||||||
|
attachments: null,
|
||||||
|
createTime: normalizeDateTime(now.subtract(2, 'day').hour(19)),
|
||||||
|
updateTime: normalizeDateTime(now.subtract(2, 'day').hour(19))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'worklog-2',
|
||||||
|
taskId: 'personal-item-1',
|
||||||
|
userId: CURRENT_USER_ID,
|
||||||
|
userNickname: CURRENT_USER_NAME,
|
||||||
|
startDate: normalizeDate(now.subtract(1, 'day'))!,
|
||||||
|
endDate: normalizeDate(now.subtract(1, 'day'))!,
|
||||||
|
durationHours: 1.5,
|
||||||
|
progressRate: 45,
|
||||||
|
difficulty: '2',
|
||||||
|
workContent: '补全供应商待确认项并整理后续跟进人。',
|
||||||
|
attachments: null,
|
||||||
|
createTime: normalizeDateTime(now.subtract(1, 'day').hour(18)),
|
||||||
|
updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'worklog-3',
|
||||||
|
taskId: 'personal-item-3',
|
||||||
|
userId: CURRENT_USER_ID,
|
||||||
|
userNickname: CURRENT_USER_NAME,
|
||||||
|
startDate: normalizeDate(now.subtract(5, 'day'))!,
|
||||||
|
endDate: normalizeDate(now.subtract(5, 'day'))!,
|
||||||
|
durationHours: 1,
|
||||||
|
progressRate: 60,
|
||||||
|
difficulty: '1',
|
||||||
|
workContent: '补拍账号开通流程截图。',
|
||||||
|
attachments: null,
|
||||||
|
createTime: normalizeDateTime(now.subtract(5, 'day').hour(15)),
|
||||||
|
updateTime: normalizeDateTime(now.subtract(5, 'day').hour(15))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'worklog-4',
|
||||||
|
taskId: 'personal-item-3',
|
||||||
|
userId: CURRENT_USER_ID,
|
||||||
|
userNickname: CURRENT_USER_NAME,
|
||||||
|
startDate: normalizeDate(now.subtract(1, 'day'))!,
|
||||||
|
endDate: normalizeDate(now.subtract(1, 'day'))!,
|
||||||
|
durationHours: 0.5,
|
||||||
|
progressRate: 100,
|
||||||
|
difficulty: '1',
|
||||||
|
workContent: '校对文案并发到群公告。',
|
||||||
|
attachments: null,
|
||||||
|
createTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(20)),
|
||||||
|
updateTime: normalizeDateTime(now.subtract(1, 'day').hour(18).minute(20))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExecutionOptions(): Api.PersonalItem.PersonalItemExecutionOption[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
executionId: 'execution-1001',
|
||||||
|
executionName: '2026Q2 运营提效',
|
||||||
|
projectId: 'project-1001',
|
||||||
|
projectName: '运营中台优化'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
executionId: 'execution-1002',
|
||||||
|
executionName: '2026Q2 用户支持专项',
|
||||||
|
projectId: 'project-1002',
|
||||||
|
projectName: '基础平台升级'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
executionId: 'execution-1003',
|
||||||
|
executionName: '2026Q3 数据治理',
|
||||||
|
projectId: 'project-1003',
|
||||||
|
projectName: '数据资产规范化'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findItemIndex(id: string) {
|
||||||
|
return personalItems.findIndex(item => item.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemOrThrow(id: string) {
|
||||||
|
const item = personalItems.find(current => current.id === id && !current.deleted);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`personal item not found: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortItems(list: PersonalItemRecord[]) {
|
||||||
|
return [...list].sort((left, right) => dayjs(right.updateTime).valueOf() - dayjs(left.updateTime).valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortWorklogs(list: PersonalItemWorklogRecord[]) {
|
||||||
|
return [...list].sort((left, right) => {
|
||||||
|
const endDiff = dayjs(right.endDate).valueOf() - dayjs(left.endDate).valueOf();
|
||||||
|
if (endDiff !== 0) {
|
||||||
|
return endDiff;
|
||||||
|
}
|
||||||
|
return dayjs(right.updateTime).valueOf() - dayjs(left.updateTime).valueOf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalItemStatusName(statusCode: Api.PersonalItem.PersonalItemStatusCode) {
|
||||||
|
const statusNameMap: Partial<Record<Api.PersonalItem.PersonalItemStatusCode, string>> = {
|
||||||
|
pending: '待处理',
|
||||||
|
active: '进行中',
|
||||||
|
completed: '已完成'
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusNameMap[statusCode] || statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItemsByIds(ids: string[]) {
|
||||||
|
const idSet = new Set(ids);
|
||||||
|
|
||||||
|
for (let i = personalItems.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (idSet.has(personalItems[i].id)) {
|
||||||
|
personalItems.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = personalItemWorklogs.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (idSet.has(personalItemWorklogs[i].taskId)) {
|
||||||
|
personalItemWorklogs.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumWorklogHours(logs: PersonalItemWorklogRecord[]) {
|
||||||
|
return logs.reduce((sum, log) => sum + (log.durationHours ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncItemFromWorklogs(itemId: string) {
|
||||||
|
const item = getItemOrThrow(itemId);
|
||||||
|
const logs = sortWorklogs(personalItemWorklogs.filter(log => log.taskId === itemId));
|
||||||
|
|
||||||
|
item.statusName = getPersonalItemStatusName(item.statusCode);
|
||||||
|
item.totalSpentHours = sumWorklogHours(logs);
|
||||||
|
|
||||||
|
if (logs.length === 0) {
|
||||||
|
if (item.statusCode !== 'completed') {
|
||||||
|
item.progressRate = 0;
|
||||||
|
item.actualStartDate = null;
|
||||||
|
item.actualEndDate = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestLog = logs[0];
|
||||||
|
const chronologicalLogs = [...logs].sort(
|
||||||
|
(left, right) => dayjs(left.startDate).valueOf() - dayjs(right.startDate).valueOf()
|
||||||
|
);
|
||||||
|
|
||||||
|
item.progressRate = latestLog.progressRate ?? item.progressRate;
|
||||||
|
item.actualStartDate = chronologicalLogs[0]?.startDate ?? item.actualStartDate;
|
||||||
|
item.actualEndDate = latestLog.endDate ?? item.actualEndDate;
|
||||||
|
item.updateTime = latestLog.updateTime;
|
||||||
|
item.updater = CURRENT_USER_NAME;
|
||||||
|
|
||||||
|
if (item.statusCode === 'pending') {
|
||||||
|
item.statusCode = 'active';
|
||||||
|
item.statusName = getPersonalItemStatusName(item.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySaveFields(target: PersonalItemRecord, payload: Api.PersonalItem.SavePersonalItemParams) {
|
||||||
|
target.taskTitle = payload.taskTitle.trim();
|
||||||
|
target.type = payload.type;
|
||||||
|
target.ownerId = payload.ownerId || target.ownerId;
|
||||||
|
target.ownerName = CURRENT_USER_NAME;
|
||||||
|
target.plannedStartDate = payload.plannedStartDate;
|
||||||
|
target.plannedEndDate = payload.plannedEndDate;
|
||||||
|
target.taskDesc = payload.taskDesc ?? null;
|
||||||
|
target.attachments = payload.attachments?.map(cloneAttachment) ?? null;
|
||||||
|
target.updater = CURRENT_USER_NAME;
|
||||||
|
target.updateTime = normalizeDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterWorklogs(taskId: string, params?: Api.PersonalItem.PersonalItemWorklogSearchParams) {
|
||||||
|
return sortWorklogs(
|
||||||
|
personalItemWorklogs.filter(item => {
|
||||||
|
if (item.taskId !== taskId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.userId && item.userId !== params.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.startDate && dayjs(item.endDate).isBefore(dayjs(params.startDate), 'day')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.endDate && dayjs(item.startDate).isAfter(dayjs(params.endDate), 'day')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetPersonalItemPage(params: Api.PersonalItem.PersonalItemSearchParams = {}) {
|
||||||
|
const query = createPersonalItemPageQuery(params);
|
||||||
|
|
||||||
|
const result = await request<PersonalItemPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PERSONAL_ITEM_PREFIX}/page?${query}` : `${PERSONAL_ITEM_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PersonalItemPageResponse>, data => ({
|
||||||
|
total: normalizePageTotal(data.total),
|
||||||
|
list: data.list.map(normalizePersonalItem)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetPersonalItemDetail(id: string) {
|
||||||
|
const result = await request<PersonalItemResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PersonalItemResponse>, normalizePersonalItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreatePersonalItem(data: Api.PersonalItem.SavePersonalItemParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: PERSONAL_ITEM_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toPersonalItemSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
|
||||||
|
if (!mapped.error && mapped.data) {
|
||||||
|
const now = normalizeDateTime();
|
||||||
|
const createdItem: PersonalItemRecord = {
|
||||||
|
id: mapped.data,
|
||||||
|
taskTitle: data.taskTitle.trim(),
|
||||||
|
type: data.type,
|
||||||
|
ownerId: data.ownerId || CURRENT_USER_ID,
|
||||||
|
statusCode: 'pending',
|
||||||
|
progressRate: typeof data.progressRate === 'number' ? data.progressRate : 0,
|
||||||
|
plannedStartDate: data.plannedStartDate,
|
||||||
|
plannedEndDate: data.plannedEndDate,
|
||||||
|
actualStartDate: null,
|
||||||
|
actualEndDate: null,
|
||||||
|
taskDesc: data.taskDesc ?? null,
|
||||||
|
lastStatusReason: null,
|
||||||
|
attachments: data.attachments?.map(cloneAttachment) ?? null,
|
||||||
|
creator: CURRENT_USER_NAME,
|
||||||
|
createTime: now,
|
||||||
|
updater: CURRENT_USER_NAME,
|
||||||
|
updateTime: now,
|
||||||
|
deleted: false,
|
||||||
|
ownerName: CURRENT_USER_NAME,
|
||||||
|
statusName: getPersonalItemStatusName('pending')
|
||||||
|
};
|
||||||
|
|
||||||
|
personalItems.unshift(createdItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUpdatePersonalItem(data: Api.PersonalItem.UpdatePersonalItemParams) {
|
||||||
|
const result = await request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${data.id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toPersonalItemSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||||
|
|
||||||
|
if (!mapped.error && mapped.data) {
|
||||||
|
const targetIndex = findItemIndex(data.id);
|
||||||
|
|
||||||
|
if (targetIndex >= 0) {
|
||||||
|
applySaveFields(personalItems[targetIndex], data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchChangePersonalItemStatus(id: string, data: Api.PersonalItem.ChangePersonalItemStatusParams) {
|
||||||
|
const result = await request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${id}/change-status`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
actionCode: data.actionCode,
|
||||||
|
reason: data.reason ?? undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||||
|
|
||||||
|
if (!mapped.error && mapped.data) {
|
||||||
|
const target = personalItems.find(item => item.id === id);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.lastStatusReason = data.reason ?? null;
|
||||||
|
target.updater = CURRENT_USER_NAME;
|
||||||
|
target.updateTime = normalizeDateTime();
|
||||||
|
|
||||||
|
if (data.actionCode === 'start') {
|
||||||
|
target.statusCode = 'active';
|
||||||
|
target.statusName = getPersonalItemStatusName('active');
|
||||||
|
target.actualStartDate ??= normalizeDate(dayjs());
|
||||||
|
target.actualEndDate = null;
|
||||||
|
} else if (data.actionCode === 'complete') {
|
||||||
|
target.statusCode = 'completed';
|
||||||
|
target.statusName = getPersonalItemStatusName('completed');
|
||||||
|
target.progressRate = 100;
|
||||||
|
target.actualStartDate ??= normalizeDate(dayjs());
|
||||||
|
target.actualEndDate = normalizeDate(dayjs());
|
||||||
|
} else if (data.actionCode === 'reopen') {
|
||||||
|
target.statusCode = 'active';
|
||||||
|
target.statusName = getPersonalItemStatusName('active');
|
||||||
|
target.actualStartDate ??= normalizeDate(dayjs());
|
||||||
|
target.actualEndDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDeletePersonalItem(id: string) {
|
||||||
|
const result = await request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/delete`,
|
||||||
|
method: 'delete',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||||
|
|
||||||
|
if (!mapped.error && mapped.data) {
|
||||||
|
removeItemsByIds([id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBatchDeletePersonalItems(payload: Api.PersonalItem.BatchDeletePersonalItemParams) {
|
||||||
|
const query = createIdsQuery(payload.ids);
|
||||||
|
const result = await request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PERSONAL_ITEM_PREFIX}/delete-list?${query}` : `${PERSONAL_ITEM_PREFIX}/delete-list`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped = mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||||
|
|
||||||
|
if (!mapped.error && mapped.data) {
|
||||||
|
removeItemsByIds(payload.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetPersonalItemExecutionOptions() {
|
||||||
|
const result = await request<PersonalItemExecutionOptionResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/owner/all-execution`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PersonalItemExecutionOptionResponse[]>, data =>
|
||||||
|
data.map(normalizePersonalItemExecutionOption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBindPersonalItemsToExecution(payload: Api.PersonalItem.BindPersonalItemExecutionParams) {
|
||||||
|
const query = createBindExecutionQuery(payload);
|
||||||
|
const result = await request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PERSONAL_ITEM_PREFIX}/relate-execution?${query}` : `${PERSONAL_ITEM_PREFIX}/relate-execution`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<boolean>, value => Boolean(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStartPersonalItem(id: string): PersonalItemResult<boolean> {
|
||||||
|
return fetchChangePersonalItemStatus(id, { actionCode: 'start' }) as PersonalItemResult<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCompletePersonalItem(id: string): PersonalItemResult<boolean> {
|
||||||
|
return fetchChangePersonalItemStatus(id, { actionCode: 'complete' }) as PersonalItemResult<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReopenPersonalItem(id: string): PersonalItemResult<boolean> {
|
||||||
|
return fetchChangePersonalItemStatus(id, { actionCode: 'reopen' }) as PersonalItemResult<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetPersonalItemWorklogPage(
|
||||||
|
taskId: string,
|
||||||
|
params: Api.PersonalItem.PersonalItemWorklogSearchParams = {}
|
||||||
|
) {
|
||||||
|
const result = await request<PersonalItemWorklogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PersonalItemWorklogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeTaskWorklog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreatePersonalItemWorklog(
|
||||||
|
taskId: string,
|
||||||
|
data: Api.PersonalItem.SavePersonalItemWorklogParams
|
||||||
|
) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs`,
|
||||||
|
method: 'post',
|
||||||
|
data: toPersonalItemWorklogSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdatePersonalItemWorklog(
|
||||||
|
taskId: string,
|
||||||
|
payload: { worklogId: string; data: Api.PersonalItem.SavePersonalItemWorklogParams }
|
||||||
|
): PersonalItemResult<boolean> {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${payload.worklogId}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toPersonalItemWorklogSaveRequest(payload.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeletePersonalItemWorklog(taskId: string, worklogId: string): PersonalItemResult<boolean> {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PERSONAL_ITEM_PREFIX}/${taskId}/worklogs/${worklogId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -11,9 +11,15 @@ import { normalizeProductMember, normalizeProductSettings } from './product-shar
|
|||||||
|
|
||||||
const PRODUCT_PREFIX = `${WEB_SERVICE_PREFIX}/project/product`;
|
const PRODUCT_PREFIX = `${WEB_SERVICE_PREFIX}/project/product`;
|
||||||
|
|
||||||
type ProductResponse = Omit<Api.Product.Product, 'id' | 'managerUserId'> & {
|
type ProductResponse = Omit<Api.Product.Product, 'id' | 'managerUserId' | 'currentUserRoles'> & {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
managerUserId?: string | number | null;
|
managerUserId?: string | number | null;
|
||||||
|
/** 灰度/兼容期后端可能缺省,适配层兜底为 [] */
|
||||||
|
currentUserRoles?: Api.Common.CurrentUserRole[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProductOptionResponse = Omit<Api.Product.ProductOption, 'id'> & {
|
||||||
|
id: string | number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProductPageResponse = Api.Product.PageResult<ProductResponse>;
|
type ProductPageResponse = Api.Product.PageResult<ProductResponse>;
|
||||||
@@ -39,7 +45,15 @@ function normalizeProduct(product: ProductResponse): Api.Product.Product {
|
|||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
id: normalizeStringId(product.id),
|
id: normalizeStringId(product.id),
|
||||||
managerUserId: normalizeNullableStringId(product.managerUserId) ?? ''
|
managerUserId: normalizeNullableStringId(product.managerUserId) ?? '',
|
||||||
|
currentUserRoles: product.currentUserRoles ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProductOption(option: ProductOptionResponse): Api.Product.ProductOption {
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
id: normalizeStringId(option.id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +105,7 @@ function createProductActivityTimelinePageQuery(params: Api.Product.ProductActiv
|
|||||||
return query.toString();
|
return query.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鑾峰彇浜у搧鍒嗛〉 */
|
/** 获取产品分页 */
|
||||||
export async function fetchGetProductPage(params?: Api.Product.ProductSearchParams) {
|
export async function fetchGetProductPage(params?: Api.Product.ProductSearchParams) {
|
||||||
const result = await request<ProductPageResponse>({
|
const result = await request<ProductPageResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
@@ -106,16 +120,50 @@ export async function fetchGetProductPage(params?: Api.Product.ProductSearchPara
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取可绑定产品下拉选项 */
|
||||||
|
export async function fetchGetProductOptions() {
|
||||||
|
const result = await request<ProductOptionResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PRODUCT_PREFIX}/options`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProductOptionResponse[]>, data =>
|
||||||
|
(data ?? []).map(normalizeProductOption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductOverviewSummaryResponse = Omit<Api.Product.ProductOverviewSummary, 'total' | 'items'> & {
|
||||||
|
/** 后端 overview-summary 升级(total/items)灰度期间可能缺省,适配层兜底 */
|
||||||
|
total?: number | null;
|
||||||
|
items?: Api.Product.OverviewStatusItem[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化产品概览统计:total/items 兜底,保证业务层拿到完整结构 */
|
||||||
|
function normalizeProductOverviewSummary(data: ProductOverviewSummaryResponse): Api.Product.ProductOverviewSummary {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
statusCounts: data.statusCounts ?? {},
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: data.items ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取产品入口页概览统计 */
|
/** 获取产品入口页概览统计 */
|
||||||
export function fetchGetProductOverviewSummary() {
|
export async function fetchGetProductOverviewSummary() {
|
||||||
return request<Api.Product.ProductOverviewSummary>({
|
const result = await request<ProductOverviewSummaryResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${PRODUCT_PREFIX}/overview-summary`,
|
url: `${PRODUCT_PREFIX}/overview-summary`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProductOverviewSummaryResponse>,
|
||||||
|
normalizeProductOverviewSummary
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鑾峰彇浜у搧璇︽儏 */
|
/** 获取产品详情 */
|
||||||
export async function fetchGetProduct(id: string) {
|
export async function fetchGetProduct(id: string) {
|
||||||
const result = await request<ProductResponse>({
|
const result = await request<ProductResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
@@ -127,7 +175,7 @@ export async function fetchGetProduct(id: string) {
|
|||||||
return mapServiceResult(result as ServiceRequestResult<ProductResponse>, normalizeProduct);
|
return mapServiceResult(result as ServiceRequestResult<ProductResponse>, normalizeProduct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鍒涘缓浜у搧 */
|
/** 新增产品 */
|
||||||
export async function fetchCreateProduct(data: Api.Product.SaveProductParams) {
|
export async function fetchCreateProduct(data: Api.Product.SaveProductParams) {
|
||||||
const result = await request<string | number>({
|
const result = await request<string | number>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
@@ -151,7 +199,7 @@ export async function fetchCreateProductWithTeam(data: Api.Product.CreateProduct
|
|||||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鏇存柊浜у搧 */
|
/** 更新产品 */
|
||||||
export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) {
|
export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
url: `${PRODUCT_PREFIX}/update`,
|
url: `${PRODUCT_PREFIX}/update`,
|
||||||
@@ -160,7 +208,7 @@ export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鍙樻洿浜у搧鐘舵€? */
|
/** 改变产品状态 */
|
||||||
export function fetchChangeProductStatus(data: Api.Product.ChangeProductStatusParams) {
|
export function fetchChangeProductStatus(data: Api.Product.ChangeProductStatusParams) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
url: `${PRODUCT_PREFIX}/change-status`,
|
url: `${PRODUCT_PREFIX}/change-status`,
|
||||||
@@ -169,7 +217,7 @@ export function fetchChangeProductStatus(data: Api.Product.ChangeProductStatusPa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 鍒犻櫎浜у搧 */
|
/** 删除产品 */
|
||||||
export function fetchDeleteProduct(data: Api.Product.DeleteProductParams) {
|
export function fetchDeleteProduct(data: Api.Product.DeleteProductParams) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
url: `${PRODUCT_PREFIX}/delete`,
|
url: `${PRODUCT_PREFIX}/delete`,
|
||||||
@@ -183,7 +231,14 @@ const REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/product/requirement`;
|
|||||||
|
|
||||||
type RequirementResponse = Omit<
|
type RequirementResponse = Omit<
|
||||||
Api.Product.Requirement,
|
Api.Product.Requirement,
|
||||||
'id' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'implementProjectId' | 'sourceBizId'
|
| 'id'
|
||||||
|
| 'parentId'
|
||||||
|
| 'moduleId'
|
||||||
|
| 'proposerId'
|
||||||
|
| 'currentHandlerUserId'
|
||||||
|
| 'implementProjectId'
|
||||||
|
| 'sourceBizCode'
|
||||||
|
| 'attachments'
|
||||||
> & {
|
> & {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
parentId: string | number;
|
parentId: string | number;
|
||||||
@@ -192,11 +247,67 @@ type RequirementResponse = Omit<
|
|||||||
currentHandlerUserId?: string | number | null;
|
currentHandlerUserId?: string | number | null;
|
||||||
implementProjectId?: string | number | null;
|
implementProjectId?: string | number | null;
|
||||||
implementProjectName?: string | null;
|
implementProjectName?: string | null;
|
||||||
sourceBizId?: string | number | null;
|
sourceBizCode?: string | null;
|
||||||
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
children?: RequirementResponse[];
|
children?: RequirementResponse[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequirementPageResponse = Api.Product.PageResult<RequirementResponse>;
|
type RequirementPageResponse = Api.Product.PageResult<RequirementResponse>;
|
||||||
|
type RequirementReviewResponse = Omit<
|
||||||
|
Api.Product.RequirementReview,
|
||||||
|
'id' | 'requirementId' | 'operatorId' | 'attendees' | 'attachments'
|
||||||
|
> & {
|
||||||
|
id: string | number;
|
||||||
|
requirementId: string | number;
|
||||||
|
operatorId: string | number;
|
||||||
|
attendees?: Array<{
|
||||||
|
userId: string | number;
|
||||||
|
nickname: string;
|
||||||
|
}>;
|
||||||
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
|
};
|
||||||
|
type ProductRequirementDashboardSummaryResponse = {
|
||||||
|
total?: number | string | null;
|
||||||
|
todo?: number | string | null;
|
||||||
|
pendingClaim?: number | string | null;
|
||||||
|
pendingReview?: number | string | null;
|
||||||
|
pendingDispatch?: number | string | null;
|
||||||
|
completed?: number | string | null;
|
||||||
|
completionRate?: number | string | null;
|
||||||
|
highPriorityTodo?: number | string | null;
|
||||||
|
};
|
||||||
|
type ProductRequirementDashboardRecentChangeResponse = Omit<
|
||||||
|
Api.Product.ProductRequirementDashboardRecentChange,
|
||||||
|
'id' | 'requirementId' | 'operatorUserId'
|
||||||
|
> & {
|
||||||
|
id: string | number;
|
||||||
|
requirementId?: string | number | null;
|
||||||
|
operatorUserId?: string | number | null;
|
||||||
|
};
|
||||||
|
type ProductRequirementDashboardResponse = {
|
||||||
|
summary?: ProductRequirementDashboardSummaryResponse | null;
|
||||||
|
recentChanges?: ProductRequirementDashboardRecentChangeResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttachmentItemResponse = Omit<Api.Project.AttachmentItem, 'fileId'> & {
|
||||||
|
fileId?: string | number;
|
||||||
|
id?: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null {
|
||||||
|
if (!list) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.map(item => {
|
||||||
|
const rawId = item.fileId ?? item.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
fileId: rawId === null || rawId === undefined ? '' : String(rawId)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeRequirement(requirement: RequirementResponse): Api.Product.Requirement {
|
function normalizeRequirement(requirement: RequirementResponse): Api.Product.Requirement {
|
||||||
return {
|
return {
|
||||||
@@ -208,11 +319,57 @@ function normalizeRequirement(requirement: RequirementResponse): Api.Product.Req
|
|||||||
currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId),
|
currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId),
|
||||||
implementProjectId: normalizeNullableStringId(requirement.implementProjectId),
|
implementProjectId: normalizeNullableStringId(requirement.implementProjectId),
|
||||||
implementProjectName: requirement.implementProjectName ?? null,
|
implementProjectName: requirement.implementProjectName ?? null,
|
||||||
sourceBizId: normalizeNullableStringId(requirement.sourceBizId),
|
sourceBizCode: requirement.sourceBizCode ?? null,
|
||||||
|
attachments: normalizeAttachments(requirement.attachments),
|
||||||
children: requirement.children?.map(normalizeRequirement)
|
children: requirement.children?.map(normalizeRequirement)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRequirementReview(review: RequirementReviewResponse): Api.Product.RequirementReview {
|
||||||
|
return {
|
||||||
|
...review,
|
||||||
|
id: normalizeStringId(review.id),
|
||||||
|
requirementId: normalizeStringId(review.requirementId),
|
||||||
|
operatorId: normalizeStringId(review.operatorId),
|
||||||
|
attendees: review.attendees?.map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
})),
|
||||||
|
attachments: normalizeAttachments(review.attachments)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDashboardCount(value: number | string | null | undefined) {
|
||||||
|
const count = Number(value ?? 0);
|
||||||
|
|
||||||
|
return Number.isFinite(count) ? Math.max(0, count) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProductRequirementDashboard(
|
||||||
|
data: ProductRequirementDashboardResponse
|
||||||
|
): Api.Product.ProductRequirementDashboard {
|
||||||
|
const summary = data.summary ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
total: normalizeDashboardCount(summary.total),
|
||||||
|
todo: normalizeDashboardCount(summary.todo),
|
||||||
|
pendingClaim: normalizeDashboardCount(summary.pendingClaim),
|
||||||
|
pendingReview: normalizeDashboardCount(summary.pendingReview),
|
||||||
|
pendingDispatch: normalizeDashboardCount(summary.pendingDispatch),
|
||||||
|
completed: normalizeDashboardCount(summary.completed),
|
||||||
|
completionRate: Math.min(100, normalizeDashboardCount(summary.completionRate)),
|
||||||
|
highPriorityTodo: normalizeDashboardCount(summary.highPriorityTodo)
|
||||||
|
},
|
||||||
|
recentChanges: (data.recentChanges ?? []).map(item => ({
|
||||||
|
...item,
|
||||||
|
id: normalizeStringId(item.id),
|
||||||
|
requirementId: normalizeNullableStringId(item.requirementId),
|
||||||
|
operatorUserId: normalizeNullableStringId(item.operatorUserId)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取需求分页列表 */
|
/** 获取需求分页列表 */
|
||||||
export async function fetchGetRequirementPage(params?: Api.Product.RequirementSearchParams) {
|
export async function fetchGetRequirementPage(params?: Api.Product.RequirementSearchParams) {
|
||||||
const result = await request<RequirementPageResponse>({
|
const result = await request<RequirementPageResponse>({
|
||||||
@@ -308,17 +465,6 @@ export async function fetchSplitRequirement(data: Api.Product.SplitRequirementPa
|
|||||||
|
|
||||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭需求 */
|
|
||||||
export function fetchCloseRequirement(data: Api.Product.CloseRequirementParams) {
|
|
||||||
return request<boolean>({
|
|
||||||
...safeJsonRequestConfig,
|
|
||||||
url: `${REQUIREMENT_PREFIX}/close`,
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取需求可执行的状态动作列表 */
|
/** 获取需求可执行的状态动作列表 */
|
||||||
export async function fetchGetRequirementAllowedTransitions(requirementId: string, productId: string) {
|
export async function fetchGetRequirementAllowedTransitions(requirementId: string, productId: string) {
|
||||||
const result = await request<Api.Product.RequirementLifecycleAction[]>({
|
const result = await request<Api.Product.RequirementLifecycleAction[]>({
|
||||||
@@ -331,16 +477,62 @@ export async function fetchGetRequirementAllowedTransitions(requirementId: strin
|
|||||||
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleAction[]>, data => data);
|
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleAction[]>, data => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取需求生命周期信息 */
|
/** 批量获取需求可执行的状态动作列表 */
|
||||||
export async function fetchGetRequirementLifecycle(requirementId: string, productId: string) {
|
export async function fetchGetRequirementAllowedTransitionsBatch(data: Api.Product.RequirementBatchReqVO) {
|
||||||
const result = await request<Api.Product.RequirementLifecycleInfo>({
|
const result = await request<Api.Product.RequirementAllowedTransitionBatchRespVO[]>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${REQUIREMENT_PREFIX}/lifecycle`,
|
url: `${REQUIREMENT_PREFIX}/allowed-transitions/batch`,
|
||||||
method: 'get',
|
method: 'post',
|
||||||
params: { requirementId, productId }
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleInfo>, data => data);
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.Product.RequirementAllowedTransitionBatchRespVO[]>,
|
||||||
|
data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
transitions: item.transitions
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交产品需求评审 */
|
||||||
|
export async function fetchSubmitProductRequirementReview(data: Api.Product.RequirementReviewSubmitParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/review/submit`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取产品需求评审记录 */
|
||||||
|
export async function fetchGetProductRequirementReview(productId: string, requirementId: string) {
|
||||||
|
const result = await request<RequirementReviewResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/review/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { productId, requirementId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<RequirementReviewResponse>, normalizeRequirementReview);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取产品概览需求池实时看板 */
|
||||||
|
export async function fetchGetProductRequirementDashboard(productId: string) {
|
||||||
|
const result = await request<ProductRequirementDashboardResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/dashboard`,
|
||||||
|
method: 'get',
|
||||||
|
params: { productId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProductRequirementDashboardResponse>,
|
||||||
|
normalizeProductRequirementDashboard
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取需求所有状态字典 */
|
/** 获取需求所有状态字典 */
|
||||||
@@ -354,15 +546,41 @@ export async function fetchGetRequirementStatusDict() {
|
|||||||
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementStatusDict[]>, data => data);
|
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementStatusDict[]>, data => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取需求终止态状态字典 */
|
/** 判断产品需求是否已指派并生成项目需求 */
|
||||||
export async function fetchGetRequirementTerminalStatusDict() {
|
export async function fetchHasDispatchedProjectRequirement(requirementId: string, productId: string) {
|
||||||
const result = await request<Api.Product.RequirementStatusDict[]>({
|
return request<boolean>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${REQUIREMENT_PREFIX}/status/dict/terminal`,
|
url: `${REQUIREMENT_PREFIX}/has-dispatched`,
|
||||||
method: 'get'
|
method: 'get',
|
||||||
|
params: { requirementId, productId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量判断产品需求是否已指派并生成项目需求 */
|
||||||
|
export async function fetchHasDispatchedProjectRequirementBatch(data: Api.Product.RequirementBatchReqVO) {
|
||||||
|
const result = await request<Api.Product.RequirementHasDispatchedBatchRespVO[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/has-dispatched/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementStatusDict[]>, data => data);
|
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementHasDispatchedBatchRespVO[]>, data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
hasDispatched: Boolean(item.hasDispatched)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据当前产品需求id获取对应地,所流转到项目侧的项目需求id */
|
||||||
|
export async function fetchGetDispatchedProjectLink(productRequirementId: string) {
|
||||||
|
return request<{ projectRequirementId: string; projectId: string }>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${REQUIREMENT_PREFIX}/dispatched-project-link`,
|
||||||
|
method: 'get',
|
||||||
|
params: { productRequirementId }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块管理 API ==========
|
// ========== 模块管理 API ==========
|
||||||
@@ -489,6 +707,19 @@ export async function fetchCreateProductMember(id: string, data: Api.Product.Cre
|
|||||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchBatchCreateProductMembers(id: string, data: Api.Product.BatchCreateProductMembersParams) {
|
||||||
|
const result = await request<Array<string | number>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PRODUCT_PREFIX}/${id}/members/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Array<string | number>>, list =>
|
||||||
|
Array.isArray(list) ? list.map(normalizeStringId) : []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchUpdateProductMember(id: string, memberId: string, data: Api.Product.UpdateProductMemberParams) {
|
export function fetchUpdateProductMember(id: string, memberId: string, data: Api.Product.UpdateProductMemberParams) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
@@ -498,6 +729,15 @@ export function fetchUpdateProductMember(id: string, memberId: string, data: Api
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchBatchInactiveProductMembers(id: string, data: Api.Product.BatchInactiveProductMembersParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PRODUCT_PREFIX}/${id}/members/batch/inactive`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchInactiveProductMember(
|
export function fetchInactiveProductMember(
|
||||||
id: string,
|
id: string,
|
||||||
memberId: string,
|
memberId: string,
|
||||||
|
|||||||
62
src/service/api/project-group.ts
Normal file
62
src/service/api/project-group.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
import { type ProjectResponse, normalizeProject } from './project';
|
||||||
|
|
||||||
|
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* group-page 原始响应。
|
||||||
|
* 组级 managerUserId、productId:后端对小数值 Long(如 1001)仍按数字返回,需 String() 归一;
|
||||||
|
* projects 字段与 page 接口项目行完全一致,复用 ProjectResponse / normalizeProject。
|
||||||
|
*/
|
||||||
|
type ProjectGroupResponse = Omit<Api.Project.ProjectGroup, 'productId' | 'managerUserId' | 'projects'> & {
|
||||||
|
productId?: string | number | null;
|
||||||
|
managerUserId?: string | number | null;
|
||||||
|
projects: ProjectResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectGroupPageResponse = Omit<Api.Project.ProjectGroupPageResult, 'list'> & {
|
||||||
|
list: ProjectGroupResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化分组:组级 ID String 化,组内项目复用 normalizeProject(id/managerUserId/productId/日期统一口径) */
|
||||||
|
function normalizeProjectGroup(group: ProjectGroupResponse): Api.Project.ProjectGroup {
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
productId: normalizeNullableStringId(group.productId),
|
||||||
|
managerUserId: normalizeNullableStringId(group.managerUserId),
|
||||||
|
projects: Array.isArray(group.projects) ? group.projects.map(normalizeProject) : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目列表「按产品分组」分页。
|
||||||
|
*
|
||||||
|
* 后端契约见《项目列表产品分组-前端API-2026-06-10》:
|
||||||
|
* - pageNo/pageSize 为产品组维度分页;statusCode 不传 = 「全部」口径(后端从状态机推导,
|
||||||
|
* 当前等价 pending/active/paused/completed,不含 cancelled/archived)。
|
||||||
|
* - 组内 projects 仅返前 topN 条(默认 5),projectTotal 为该口径组内全量计数;
|
||||||
|
* 剩余项目由页面按 productId / orphanOnly + statusCodes 走 page 接口展开拉取。
|
||||||
|
* - typeCounts / hasBaseline 现状恒按「全部」口径统计,不随 statusCode 变化;其中 typeCounts 已提需求
|
||||||
|
* 改为与 projectTotal 同口径(见《2026-06-11-项目分组接口typeCounts口径-后端接口需求》),后端落地后更新本注释;
|
||||||
|
* hasBaseline = 存在非已取消的主线项目(已归档/完成也算占坑),前端直接消费、不自行推导。
|
||||||
|
*/
|
||||||
|
export async function fetchGetProjectGroupPage(params?: Api.Project.ProjectGroupSearchParams) {
|
||||||
|
const result = await request<ProjectGroupPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/group-page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectGroupPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: Array.isArray(data.list) ? data.list.map(normalizeProjectGroup) : []
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
||||||
|
|
||||||
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
||||||
@@ -23,6 +24,8 @@ export type ProjectExecutionResponse = Omit<
|
|||||||
| 'actualStartDate'
|
| 'actualStartDate'
|
||||||
| 'actualEndDate'
|
| 'actualEndDate'
|
||||||
| 'progressRate'
|
| 'progressRate'
|
||||||
|
| 'priority'
|
||||||
|
| 'priorityName'
|
||||||
> & {
|
> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
projectId: StringIdResponse;
|
projectId: StringIdResponse;
|
||||||
@@ -34,6 +37,98 @@ export type ProjectExecutionResponse = Omit<
|
|||||||
actualStartDate?: ProjectLocalDateValue;
|
actualStartDate?: ProjectLocalDateValue;
|
||||||
actualEndDate?: ProjectLocalDateValue;
|
actualEndDate?: ProjectLocalDateValue;
|
||||||
progressRate?: number | null;
|
progressRate?: number | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
priorityName?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyExecutionResponse = Omit<
|
||||||
|
Api.Project.MyExecutionItem,
|
||||||
|
| 'id'
|
||||||
|
| 'projectId'
|
||||||
|
| 'projectRequirementId'
|
||||||
|
| 'priority'
|
||||||
|
| 'progressRate'
|
||||||
|
| 'plannedStartDate'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'actualStartDate'
|
||||||
|
| 'actualEndDate'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
projectRequirementId?: StringIdResponse | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
progressRate?: number | null;
|
||||||
|
plannedStartDate?: ProjectLocalDateValue;
|
||||||
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
|
actualStartDate?: ProjectLocalDateValue;
|
||||||
|
actualEndDate?: ProjectLocalDateValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyParticipatedProjectResponse = Omit<Api.Project.MyParticipatedProjectItem, 'id'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyOwnedProjectMemberResponse = Omit<Api.Project.MyOwnedProjectMember, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyOwnedProjectResponse = Omit<Api.Project.MyOwnedProjectItem, 'id' | 'members'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
members?: MyOwnedProjectMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyTaskResponse = Omit<
|
||||||
|
Api.Project.MyTaskItem,
|
||||||
|
| 'id'
|
||||||
|
| 'projectId'
|
||||||
|
| 'executionId'
|
||||||
|
| 'priority'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'progressRate'
|
||||||
|
| 'createTime'
|
||||||
|
| 'parentTaskId'
|
||||||
|
| 'availableActions'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
executionId?: StringIdResponse | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
|
progressRate?: number | string | null;
|
||||||
|
createTime?: string | number | null;
|
||||||
|
parentTaskId?: StringIdResponse | null;
|
||||||
|
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadDistributionItemResponse = Omit<Api.Project.TeamLoadDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadMemberResponse = Omit<Api.Project.TeamLoadMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: TeamLoadDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadResponse = {
|
||||||
|
members?: TeamLoadMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorklogDistributionItemResponse = Omit<Api.Project.WorklogDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyWorklogWeekResponse = Omit<Api.Project.MyWorklogWeekResult, 'dailyHours' | 'distribution'> & {
|
||||||
|
dailyHours?: number[] | null;
|
||||||
|
distribution?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekMemberResponse = Omit<Api.Project.TeamWorklogWeekMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekResponse = Omit<Api.Project.TeamWorklogWeekResult, 'members'> & {
|
||||||
|
members?: TeamWorklogWeekMemberResponse[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
||||||
@@ -108,6 +203,8 @@ export type ProjectTaskResponse = Omit<
|
|||||||
| 'executionId'
|
| 'executionId'
|
||||||
| 'parentTaskId'
|
| 'parentTaskId'
|
||||||
| 'ownerId'
|
| 'ownerId'
|
||||||
|
| 'executionOwnerId'
|
||||||
|
| 'parentTaskOwnerId'
|
||||||
| 'availableActions'
|
| 'availableActions'
|
||||||
| 'plannedStartDate'
|
| 'plannedStartDate'
|
||||||
| 'plannedEndDate'
|
| 'plannedEndDate'
|
||||||
@@ -116,12 +213,18 @@ export type ProjectTaskResponse = Omit<
|
|||||||
| 'progressRate'
|
| 'progressRate'
|
||||||
| 'assignees'
|
| 'assignees'
|
||||||
| 'attachments'
|
| 'attachments'
|
||||||
|
| 'priority'
|
||||||
|
| 'priorityName'
|
||||||
> & {
|
> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
projectId: StringIdResponse;
|
projectId: StringIdResponse;
|
||||||
executionId: StringIdResponse;
|
executionId: StringIdResponse;
|
||||||
|
executionName?: string | null;
|
||||||
|
executionStatusCode?: Api.Project.ProjectExecutionStatusCode | null;
|
||||||
parentTaskId?: StringIdResponse | null;
|
parentTaskId?: StringIdResponse | null;
|
||||||
ownerId: StringIdResponse;
|
ownerId: StringIdResponse;
|
||||||
|
executionOwnerId?: StringIdResponse | null;
|
||||||
|
parentTaskOwnerId?: StringIdResponse | null;
|
||||||
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
|
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
|
||||||
plannedStartDate?: ProjectLocalDateValue;
|
plannedStartDate?: ProjectLocalDateValue;
|
||||||
plannedEndDate?: ProjectLocalDateValue;
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
@@ -131,13 +234,21 @@ export type ProjectTaskResponse = Omit<
|
|||||||
assignees?: TaskAssigneeRefResponse[] | null;
|
assignees?: TaskAssigneeRefResponse[] | null;
|
||||||
attachments?: AttachmentItemResponse[] | null;
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
totalSpentHours?: number | null;
|
totalSpentHours?: number | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
priorityName?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TaskWorklogResponse = Omit<Api.Project.TaskWorklog, 'id' | 'taskId' | 'userId' | 'attachments'> & {
|
export type TaskWorklogResponse = Omit<
|
||||||
|
Api.Project.TaskWorklog,
|
||||||
|
'id' | 'taskId' | 'userId' | 'difficulty' | 'attachments' | 'startDate' | 'endDate'
|
||||||
|
> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
taskId: StringIdResponse;
|
taskId: StringIdResponse;
|
||||||
userId: StringIdResponse;
|
userId: StringIdResponse;
|
||||||
|
difficulty?: string | null;
|
||||||
attachments?: AttachmentItemResponse[] | null;
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
|
startDate?: ProjectLocalDateValue;
|
||||||
|
endDate?: ProjectLocalDateValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ProjectMemberResponse {
|
export interface ProjectMemberResponse {
|
||||||
@@ -207,6 +318,28 @@ export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefin
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端 LocalDateTime 统一序列化为毫秒时间戳(也可能是数字字符串/格式化字符串),
|
||||||
|
* 归一为 'YYYY-MM-DD HH:mm:ss' 供展示与 dayjs 解析。
|
||||||
|
*/
|
||||||
|
export function normalizeProjectDateTime(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: dayjs.Dayjs;
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
} else if (/^\d+$/.test(value)) {
|
||||||
|
// 字符串形态的毫秒时间戳:dayjs 无法直接解析,先转数值(时间值非 ID,安全整数范围内)
|
||||||
|
parsed = dayjs(Number(value));
|
||||||
|
} else {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeLifecycleActions<ActionCode extends string>(
|
export function normalizeLifecycleActions<ActionCode extends string>(
|
||||||
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
||||||
): Api.Project.LifecycleAction<ActionCode>[] {
|
): Api.Project.LifecycleAction<ActionCode>[] {
|
||||||
@@ -233,12 +366,30 @@ export function normalizeProjectMember(response: ProjectMemberResponse): Api.Pro
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePriority(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProgressRate(value: number | string | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = typeof value === 'number' ? value : Number(value ?? 0);
|
||||||
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
id: normalizeStringId(response.id),
|
id: normalizeStringId(response.id),
|
||||||
projectId: normalizeStringId(response.projectId),
|
projectId: normalizeStringId(response.projectId),
|
||||||
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
|
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
|
||||||
|
projectRequirementName: response.projectRequirementName ?? null,
|
||||||
|
projectRequirementStatusCode: response.projectRequirementStatusCode ?? null,
|
||||||
ownerId: normalizeStringId(response.ownerId),
|
ownerId: normalizeStringId(response.ownerId),
|
||||||
ownerNickname: response.ownerNickname ?? null,
|
ownerNickname: response.ownerNickname ?? null,
|
||||||
statusName: response.statusName ?? null,
|
statusName: response.statusName ?? null,
|
||||||
@@ -250,11 +401,126 @@ export function normalizeProjectExecution(response: ProjectExecutionResponse): A
|
|||||||
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
||||||
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
||||||
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
|
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
priorityName: response.priorityName ?? null,
|
||||||
executionDesc: response.executionDesc ?? null,
|
executionDesc: response.executionDesc ?? null,
|
||||||
lastStatusReason: response.lastStatusReason ?? null
|
lastStatusReason: response.lastStatusReason ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeMyExecution(response: MyExecutionResponse): Api.Project.MyExecutionItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
|
||||||
|
plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
||||||
|
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
||||||
|
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
|
||||||
|
projectRequirementName: response.projectRequirementName ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyParticipatedProject(
|
||||||
|
response: MyParticipatedProjectResponse
|
||||||
|
): Api.Project.MyParticipatedProjectItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
code: response.code ?? null,
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
myRole: response.myRole ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyOwnedProject(response: MyOwnedProjectResponse): Api.Project.MyOwnedProjectItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
code: response.code ?? null,
|
||||||
|
myRole: response.myRole ?? null,
|
||||||
|
plannedEndDate: response.plannedEndDate ?? null,
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
...member,
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userName: member.userName ?? null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyTask(response: MyTaskResponse): Api.Project.MyTaskItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
executionId: normalizeNullableStringId(response.executionId),
|
||||||
|
executionName: response.executionName ?? null,
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
progressRate: normalizeProgressRate(response.progressRate) ?? 0,
|
||||||
|
createTime: normalizeProjectDateTime(response.createTime),
|
||||||
|
parentTaskId: normalizeNullableStringId(response.parentTaskId),
|
||||||
|
terminal: Boolean(response.terminal),
|
||||||
|
allowEdit: Boolean(response.allowEdit),
|
||||||
|
availableActions: normalizeLifecycleActions(response.availableActions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorklogDistributionItem(
|
||||||
|
response: WorklogDistributionItemResponse | TeamLoadDistributionItemResponse
|
||||||
|
): { projectId: string | null; projectName: string | null; kind: 'project' | 'personal' | 'other' } {
|
||||||
|
return {
|
||||||
|
projectId: normalizeNullableStringId(response.projectId),
|
||||||
|
projectName: response.projectName ?? null,
|
||||||
|
kind: response.kind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamLoad(response: TeamLoadResponse): Api.Project.TeamLoadResult {
|
||||||
|
return {
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
count: typeof item.count === 'number' ? item.count : 0
|
||||||
|
})),
|
||||||
|
dueSoonCount: typeof member.dueSoonCount === 'number' ? member.dueSoonCount : 0,
|
||||||
|
overdueCount: typeof member.overdueCount === 'number' ? member.overdueCount : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyWorklogWeek(response: MyWorklogWeekResponse): Api.Project.MyWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
dailyHours: response.dailyHours ?? [0, 0, 0, 0, 0],
|
||||||
|
distribution: (response.distribution ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamWorklogWeek(response: TeamWorklogWeekResponse): Api.Project.TeamWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
@@ -289,9 +555,17 @@ export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project
|
|||||||
id: normalizeStringId(response.id),
|
id: normalizeStringId(response.id),
|
||||||
projectId: normalizeStringId(response.projectId),
|
projectId: normalizeStringId(response.projectId),
|
||||||
executionId: normalizeStringId(response.executionId),
|
executionId: normalizeStringId(response.executionId),
|
||||||
|
executionName: response.executionName ?? null,
|
||||||
|
executionStatusCode: response.executionStatusCode ?? null,
|
||||||
parentTaskId: normalizeNullableStringId(response.parentTaskId),
|
parentTaskId: normalizeNullableStringId(response.parentTaskId),
|
||||||
|
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
|
||||||
|
projectRequirementName: response.projectRequirementName ?? null,
|
||||||
|
projectRequirementStatusCode: response.projectRequirementStatusCode ?? null,
|
||||||
|
type: response.type ?? '',
|
||||||
ownerId: normalizeStringId(response.ownerId),
|
ownerId: normalizeStringId(response.ownerId),
|
||||||
ownerNickname: response.ownerNickname ?? null,
|
ownerNickname: response.ownerNickname ?? null,
|
||||||
|
executionOwnerId: normalizeNullableStringId(response.executionOwnerId),
|
||||||
|
parentTaskOwnerId: normalizeNullableStringId(response.parentTaskOwnerId),
|
||||||
statusName: response.statusName ?? null,
|
statusName: response.statusName ?? null,
|
||||||
terminal: Boolean(response.terminal),
|
terminal: Boolean(response.terminal),
|
||||||
allowEdit: Boolean(response.allowEdit),
|
allowEdit: Boolean(response.allowEdit),
|
||||||
@@ -301,6 +575,8 @@ export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project
|
|||||||
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
||||||
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
priorityName: response.priorityName ?? null,
|
||||||
taskDesc: response.taskDesc ?? null,
|
taskDesc: response.taskDesc ?? null,
|
||||||
lastStatusReason: response.lastStatusReason ?? null,
|
lastStatusReason: response.lastStatusReason ?? null,
|
||||||
assignees:
|
assignees:
|
||||||
@@ -323,7 +599,13 @@ export function normalizeTaskWorklog(response: TaskWorklogResponse): Api.Project
|
|||||||
userNickname: response.userNickname ?? null,
|
userNickname: response.userNickname ?? null,
|
||||||
workContent: response.workContent ?? null,
|
workContent: response.workContent ?? null,
|
||||||
attachments: normalizeAttachments(response.attachments),
|
attachments: normalizeAttachments(response.attachments),
|
||||||
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0
|
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
|
||||||
|
// 后端 LocalDate 默认序列化为 [year, month, day] 数组,必须归一为 'YYYY-MM-DD' 字符串供 ElDatePicker 使用
|
||||||
|
startDate: normalizeProjectLocalDate(response.startDate) ?? '',
|
||||||
|
endDate: normalizeProjectLocalDate(response.endDate) ?? '',
|
||||||
|
// 历史记录或异常缺失时兜底为字典默认档位 "2"
|
||||||
|
difficulty: response.difficulty ?? '2',
|
||||||
|
difficultyName: response.difficultyName ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ExecutionAssigneeLogResponse,
|
type ExecutionAssigneeLogResponse,
|
||||||
type ExecutionAssigneeResponse,
|
type ExecutionAssigneeResponse,
|
||||||
|
type MyExecutionResponse,
|
||||||
|
type MyOwnedProjectResponse,
|
||||||
|
type MyParticipatedProjectResponse,
|
||||||
|
type MyTaskResponse,
|
||||||
|
type MyWorklogWeekResponse,
|
||||||
type ProjectExecutionResponse,
|
type ProjectExecutionResponse,
|
||||||
type ProjectLocalDateValue,
|
type ProjectLocalDateValue,
|
||||||
type ProjectMemberResponse,
|
type ProjectMemberResponse,
|
||||||
@@ -17,23 +22,39 @@ import {
|
|||||||
type TaskAssigneeFromApiResponse,
|
type TaskAssigneeFromApiResponse,
|
||||||
type TaskAssigneeLogResponse,
|
type TaskAssigneeLogResponse,
|
||||||
type TaskWorklogResponse,
|
type TaskWorklogResponse,
|
||||||
|
type TeamLoadResponse,
|
||||||
|
type TeamWorklogWeekResponse,
|
||||||
getProjectLifecycleActions,
|
getProjectLifecycleActions,
|
||||||
normalizeExecutionAssignee,
|
normalizeExecutionAssignee,
|
||||||
normalizeExecutionAssigneeLog,
|
normalizeExecutionAssigneeLog,
|
||||||
|
normalizeMyExecution,
|
||||||
|
normalizeMyOwnedProject,
|
||||||
|
normalizeMyParticipatedProject,
|
||||||
|
normalizeMyTask,
|
||||||
|
normalizeMyWorklogWeek,
|
||||||
normalizeProjectExecution,
|
normalizeProjectExecution,
|
||||||
normalizeProjectLocalDate,
|
normalizeProjectLocalDate,
|
||||||
normalizeProjectMember,
|
normalizeProjectMember,
|
||||||
normalizeProjectTask,
|
normalizeProjectTask,
|
||||||
normalizeTaskAssignee,
|
normalizeTaskAssignee,
|
||||||
normalizeTaskAssigneeLog,
|
normalizeTaskAssigneeLog,
|
||||||
normalizeTaskWorklog
|
normalizeTaskWorklog,
|
||||||
|
normalizeTeamLoad,
|
||||||
|
normalizeTeamWorklogWeek
|
||||||
} from './project-shared';
|
} from './project-shared';
|
||||||
|
|
||||||
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
||||||
|
|
||||||
type ProjectResponse = Omit<
|
export type ProjectResponse = Omit<
|
||||||
Api.Project.Project,
|
Api.Project.Project,
|
||||||
'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate'
|
| 'id'
|
||||||
|
| 'managerUserId'
|
||||||
|
| 'productId'
|
||||||
|
| 'plannedStartDate'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'actualStartDate'
|
||||||
|
| 'actualEndDate'
|
||||||
|
| 'currentUserRoles'
|
||||||
> & {
|
> & {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
managerUserId?: string | number | null;
|
managerUserId?: string | number | null;
|
||||||
@@ -42,12 +63,24 @@ type ProjectResponse = Omit<
|
|||||||
plannedEndDate?: ProjectLocalDateValue;
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
actualStartDate?: ProjectLocalDateValue;
|
actualStartDate?: ProjectLocalDateValue;
|
||||||
actualEndDate?: ProjectLocalDateValue;
|
actualEndDate?: ProjectLocalDateValue;
|
||||||
|
/** 灰度/兼容期后端可能缺省,适配层兜底为 [] */
|
||||||
|
currentUserRoles?: Api.Common.CurrentUserRole[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProjectPageResponse = Api.Project.PageResult<ProjectResponse>;
|
type ProjectPageResponse = Api.Project.PageResult<ProjectResponse>;
|
||||||
type ProjectExecutionPageResponse = Api.Project.PageResult<ProjectExecutionResponse>;
|
type ProjectExecutionPageResponse = Api.Project.PageResult<ProjectExecutionResponse>;
|
||||||
type ProjectTaskPageResponse = Api.Project.PageResult<ProjectTaskResponse>;
|
type ProjectTaskPageResponse = Api.Project.PageResult<ProjectTaskResponse>;
|
||||||
type StatusBoardResponse = Api.Project.StatusBoard;
|
type StatusBoardResponse = Api.Project.StatusBoard;
|
||||||
|
type ProjectTaskBoardPageResponse = {
|
||||||
|
items: Array<{
|
||||||
|
statusCode: string;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
terminal?: boolean;
|
||||||
|
list: ProjectTaskResponse[];
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
type ProjectContextResponse = Omit<Api.Project.ProjectContext, 'currentProject' | 'navs'> & {
|
type ProjectContextResponse = Omit<Api.Project.ProjectContext, 'currentProject' | 'navs'> & {
|
||||||
currentProject: Omit<Api.Project.ProjectContext['currentProject'], 'id'> & { id: string | number };
|
currentProject: Omit<Api.Project.ProjectContext['currentProject'], 'id'> & { id: string | number };
|
||||||
@@ -63,7 +96,7 @@ function getTaskPrefix(projectId: string, executionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 归一化项目数据 */
|
/** 归一化项目数据 */
|
||||||
function normalizeProject(project: ProjectResponse): Api.Project.Project {
|
export function normalizeProject(project: ProjectResponse): Api.Project.Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
id: normalizeStringId(project.id),
|
id: normalizeStringId(project.id),
|
||||||
@@ -72,7 +105,8 @@ function normalizeProject(project: ProjectResponse): Api.Project.Project {
|
|||||||
plannedStartDate: normalizeProjectLocalDate(project.plannedStartDate),
|
plannedStartDate: normalizeProjectLocalDate(project.plannedStartDate),
|
||||||
plannedEndDate: normalizeProjectLocalDate(project.plannedEndDate),
|
plannedEndDate: normalizeProjectLocalDate(project.plannedEndDate),
|
||||||
actualStartDate: normalizeProjectLocalDate(project.actualStartDate),
|
actualStartDate: normalizeProjectLocalDate(project.actualStartDate),
|
||||||
actualEndDate: normalizeProjectLocalDate(project.actualEndDate)
|
actualEndDate: normalizeProjectLocalDate(project.actualEndDate),
|
||||||
|
currentUserRoles: project.currentUserRoles ?? []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,13 +154,34 @@ export async function fetchGetProjectPage(params?: Api.Project.ProjectSearchPara
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectOverviewSummaryResponse = Omit<Api.Project.ProjectOverviewSummary, 'total' | 'items'> & {
|
||||||
|
/** 后端 overview-summary 升级(total/items)灰度期间可能缺省,适配层兜底 */
|
||||||
|
total?: number | null;
|
||||||
|
items?: Api.Project.OverviewStatusItem[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化项目概览统计:total/items 兜底,保证业务层拿到完整结构 */
|
||||||
|
function normalizeProjectOverviewSummary(data: ProjectOverviewSummaryResponse): Api.Project.ProjectOverviewSummary {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
statusCounts: data.statusCounts ?? {},
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: data.items ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目入口页概览统计 */
|
/** 获取项目入口页概览统计 */
|
||||||
export function fetchGetProjectOverviewSummary() {
|
export async function fetchGetProjectOverviewSummary() {
|
||||||
return request<Api.Project.ProjectOverviewSummary>({
|
const result = await request<ProjectOverviewSummaryResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${PROJECT_PREFIX}/overview-summary`,
|
url: `${PROJECT_PREFIX}/overview-summary`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProjectOverviewSummaryResponse>,
|
||||||
|
normalizeProjectOverviewSummary
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取项目详情 */
|
/** 获取项目详情 */
|
||||||
@@ -274,6 +329,28 @@ export function fetchInactiveProjectMember(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchBatchCreateProjectMembers(id: string, data: Api.Project.BatchCreateProjectMembersParams) {
|
||||||
|
const result = await request<Array<string | number>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/members/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Array<string | number>>, list =>
|
||||||
|
Array.isArray(list) ? list.map(normalizeStringId) : []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchInactiveProjectMembers(id: string, data: Api.Project.BatchInactiveProjectMembersParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/members/batch/inactive`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目设置 */
|
/** 获取项目设置 */
|
||||||
export async function fetchGetProjectSettings(id: string) {
|
export async function fetchGetProjectSettings(id: string) {
|
||||||
const result = await fetchGetProject(id);
|
const result = await fetchGetProject(id);
|
||||||
@@ -333,6 +410,105 @@ export async function fetchGetProjectExecutionPage(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我负责的执行」(跨项目聚合,owner 隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyExecutionPage(params?: Api.Project.MyExecutionSearchParams) {
|
||||||
|
type MyExecutionPageResponse = Api.Project.PageResult<MyExecutionResponse>;
|
||||||
|
const result = await request<MyExecutionPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/executions/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyExecutionPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyExecution)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我参与的项目」(成员视角,附我的角色与任务量;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyParticipatedProjectPage(params?: Api.Project.MyProjectSearchParams) {
|
||||||
|
type MyParticipatedProjectPageResponse = Api.Project.PageResult<MyParticipatedProjectResponse>;
|
||||||
|
const result = await request<MyParticipatedProjectPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/participated/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyParticipatedProjectPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyParticipatedProject)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我负责的项目」(项目负责人视角,附聚合统计与成员负载;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyOwnedProjectPage(params?: Api.Project.MyProjectSearchParams) {
|
||||||
|
type MyOwnedProjectPageResponse = Api.Project.PageResult<MyOwnedProjectResponse>;
|
||||||
|
const result = await request<MyOwnedProjectPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/owned/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyOwnedProjectPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyOwnedProject)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的任务」(跨项目聚合,负责人/在岗协办人口径,只返回非终态;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyTaskPage(params?: Api.Project.MyTaskSearchParams) {
|
||||||
|
type MyTaskPageResponse = Api.Project.PageResult<MyTaskResponse>;
|
||||||
|
const result = await request<MyTaskPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/tasks/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyTaskPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyTask)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队负载」(团队 = 当前用户 + 管理链路直接下级,members[0] 恒为当前用户) */
|
||||||
|
export async function fetchGetMyTeamLoad() {
|
||||||
|
const result = await request<TeamLoadResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-load`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamLoadResponse>, normalizeTeamLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的工时周聚合」(weekStart 传任意日期,后端归一到所在周周一;逐日工时为均摊推算值) */
|
||||||
|
export async function fetchGetMyWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<MyWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyWorklogWeekResponse>, normalizeMyWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队工时周聚合」(成员集合与团队负载同口径;周标准工时后端不返回,前端落常量) */
|
||||||
|
export async function fetchGetTeamWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<TeamWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamWorklogWeekResponse>, normalizeTeamWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目执行状态看板 */
|
/** 获取项目执行状态看板 */
|
||||||
export function fetchGetProjectExecutionStatusBoard(
|
export function fetchGetProjectExecutionStatusBoard(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -411,6 +587,14 @@ export function fetchDeleteProjectExecution(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 执行删除预检(spec §2.1:返回是否含下挂数据,用于前端弹层分流) */
|
||||||
|
export function fetchPrecheckDeleteProjectExecution(projectId: string, executionId: string) {
|
||||||
|
return request<Api.Project.ProjectExecutionDeletePrecheck>({
|
||||||
|
url: `${getExecutionPrefix(projectId)}/${executionId}/delete-precheck`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 变更项目执行状态 */
|
/** 变更项目执行状态 */
|
||||||
export function fetchChangeProjectExecutionStatus(
|
export function fetchChangeProjectExecutionStatus(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -523,6 +707,32 @@ export function fetchGetProjectTaskStatusBoard(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务看板按状态分组的分页接口。
|
||||||
|
*
|
||||||
|
* 看板模式专用:一次请求拿到所有列(或指定列)的首屏 + 总数,替代"5 列 5 次 page"的旧方式。
|
||||||
|
* 列内向下滚续页时再传 `statusCode=[X]&pageNo=N+1` 单列查询。
|
||||||
|
*/
|
||||||
|
export async function fetchGetProjectTaskBoardPage(
|
||||||
|
projectId: string,
|
||||||
|
executionId: string,
|
||||||
|
params?: Api.Project.ProjectTaskBoardPageParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectTaskBoardPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${getTaskPrefix(projectId, executionId)}/board-page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectTaskBoardPageResponse>, data => ({
|
||||||
|
items: data.items.map(item => ({
|
||||||
|
...item,
|
||||||
|
list: item.list.map(normalizeProjectTask)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目任务详情 */
|
/** 获取项目任务详情 */
|
||||||
export async function fetchGetProjectTask(projectId: string, executionId: string, taskId: string) {
|
export async function fetchGetProjectTask(projectId: string, executionId: string, taskId: string) {
|
||||||
const result = await request<ProjectTaskResponse>({
|
const result = await request<ProjectTaskResponse>({
|
||||||
@@ -580,6 +790,14 @@ export function fetchDeleteProjectTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 任务删除预检(spec §2.1) */
|
||||||
|
export function fetchPrecheckDeleteProjectTask(projectId: string, executionId: string, taskId: string) {
|
||||||
|
return request<Api.Project.ProjectTaskDeletePrecheck>({
|
||||||
|
url: `${getTaskPrefix(projectId, executionId)}/${taskId}/delete-precheck`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 变更项目任务状态 */
|
/** 变更项目任务状态 */
|
||||||
export function fetchChangeProjectTaskStatus(
|
export function fetchChangeProjectTaskStatus(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -594,6 +812,80 @@ export function fetchChangeProjectTaskStatus(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= 项目级跨执行任务(不带 executionId 路径段) =============
|
||||||
|
// 调试文档:所有接口挂在 /project/project/{projectId}/tasks/* 下;通过 involveUserId / ownerId / executionIds 等
|
||||||
|
// 入参组合表达"我的任务 / 项目全部 / 指定执行"等视角。原有执行级 {eid}/tasks/page 等保留不动。
|
||||||
|
|
||||||
|
function getProjectTasksPrefix(projectId: string) {
|
||||||
|
return `${PROJECT_PREFIX}/${projectId}/tasks`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项目级跨执行任务分页 */
|
||||||
|
export async function fetchGetProjectTaskPageCross(
|
||||||
|
projectId: string,
|
||||||
|
params?: Api.Project.ProjectTaskCrossSearchParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectTaskPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${getProjectTasksPrefix(projectId)}/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectTaskPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeProjectTask)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项目级跨执行任务状态看板 */
|
||||||
|
export function fetchGetProjectTaskStatusBoardCross(
|
||||||
|
projectId: string,
|
||||||
|
params?: Api.Project.ProjectTaskCrossStatusBoardParams
|
||||||
|
) {
|
||||||
|
return request<StatusBoardResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${getProjectTasksPrefix(projectId)}/status-board`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项目级跨执行任务看板分页(每列共用同一组 pageNo / pageSize;列内固定 plannedEndDate ASC, id DESC) */
|
||||||
|
export async function fetchGetProjectTaskBoardPageCross(
|
||||||
|
projectId: string,
|
||||||
|
params?: Api.Project.ProjectTaskCrossBoardPageParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectTaskBoardPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${getProjectTasksPrefix(projectId)}/board-page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectTaskBoardPageResponse>, data => ({
|
||||||
|
items: data.items.map(item => ({
|
||||||
|
...item,
|
||||||
|
list: item.list.map(normalizeProjectTask)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级"今日小条"汇总(4 个数字 + 服务器日期边界)。
|
||||||
|
*
|
||||||
|
* scope=all 必须有 project:task:query 权限,否则 403(PROJECT_OBJECT_PERMISSION_DENIED)。
|
||||||
|
* 前端切到"项目全部"视角前应已基于权限码隐藏入口;如真被 403,UI 应自动切回"我的"。
|
||||||
|
*/
|
||||||
|
export function fetchGetProjectTaskSummary(projectId: string, params?: Api.Project.ProjectTaskSummaryParams) {
|
||||||
|
return request<Api.Project.ProjectTaskSummary>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${getProjectTasksPrefix(projectId)}/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type TaskWorklogPageResponse = Api.Project.PageResult<TaskWorklogResponse>;
|
type TaskWorklogPageResponse = Api.Project.PageResult<TaskWorklogResponse>;
|
||||||
|
|
||||||
function getWorklogPrefix(projectId: string, executionId: string, taskId: string) {
|
function getWorklogPrefix(projectId: string, executionId: string, taskId: string) {
|
||||||
@@ -738,3 +1030,333 @@ export async function fetchGetProjectTaskAssigneeLogPage(
|
|||||||
list: data.list.map(normalizeTaskAssigneeLog)
|
list: data.list.map(normalizeTaskAssigneeLog)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 项目需求 API ==========
|
||||||
|
const PROJECT_REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project/requirement`;
|
||||||
|
|
||||||
|
type ProjectRequirementResponse = Omit<
|
||||||
|
Api.Project.ProjectRequirement,
|
||||||
|
'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizCode' | 'attachments'
|
||||||
|
> & {
|
||||||
|
id: string | number;
|
||||||
|
projectId: string | number;
|
||||||
|
parentId: string | number;
|
||||||
|
moduleId: string | number;
|
||||||
|
proposerId: string | number;
|
||||||
|
currentHandlerUserId?: string | number | null;
|
||||||
|
sourceBizCode?: string | null;
|
||||||
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
|
children?: ProjectRequirementResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectRequirementPageResponse = Api.Project.PageResult<ProjectRequirementResponse>;
|
||||||
|
type ProjectRequirementReviewResponse = Omit<
|
||||||
|
Api.Project.ProjectRequirementReview,
|
||||||
|
'id' | 'requirementId' | 'operatorId' | 'attendees' | 'attachments'
|
||||||
|
> & {
|
||||||
|
id: string | number;
|
||||||
|
requirementId: string | number;
|
||||||
|
operatorId: string | number;
|
||||||
|
attendees?: Array<{
|
||||||
|
userId: string | number;
|
||||||
|
nickname: string;
|
||||||
|
}>;
|
||||||
|
attachments?: AttachmentItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectRequirementModuleResponse = Omit<Api.Project.ProjectRequirementModule, 'id' | 'parentId' | 'projectId'> & {
|
||||||
|
id: string | number;
|
||||||
|
parentId: string | number;
|
||||||
|
projectId: string | number;
|
||||||
|
children?: ProjectRequirementModuleResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttachmentItemResponse = Omit<Api.Project.AttachmentItem, 'fileId'> & {
|
||||||
|
fileId?: string | number;
|
||||||
|
id?: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null {
|
||||||
|
if (!list) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.map(item => {
|
||||||
|
const rawId = item.fileId ?? item.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
fileId: rawId === null || rawId === undefined ? '' : String(rawId)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectRequirement(requirement: ProjectRequirementResponse): Api.Project.ProjectRequirement {
|
||||||
|
return {
|
||||||
|
...requirement,
|
||||||
|
id: normalizeStringId(requirement.id),
|
||||||
|
projectId: normalizeStringId(requirement.projectId),
|
||||||
|
parentId: normalizeStringId(requirement.parentId),
|
||||||
|
moduleId: normalizeStringId(requirement.moduleId),
|
||||||
|
proposerId: normalizeStringId(requirement.proposerId),
|
||||||
|
currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId),
|
||||||
|
sourceBizCode: requirement.sourceBizCode ?? null,
|
||||||
|
attachments: normalizeAttachments(requirement.attachments),
|
||||||
|
progressRate: typeof requirement.progressRate === 'number' ? requirement.progressRate : 0,
|
||||||
|
children: requirement.children?.map(normalizeProjectRequirement)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectRequirementReview(
|
||||||
|
review: ProjectRequirementReviewResponse
|
||||||
|
): Api.Project.ProjectRequirementReview {
|
||||||
|
return {
|
||||||
|
...review,
|
||||||
|
id: normalizeStringId(review.id),
|
||||||
|
requirementId: normalizeStringId(review.requirementId),
|
||||||
|
operatorId: normalizeStringId(review.operatorId),
|
||||||
|
attendees: review.attendees?.map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
})),
|
||||||
|
attachments: normalizeAttachments(review.attachments)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectRequirementModule(
|
||||||
|
module: ProjectRequirementModuleResponse
|
||||||
|
): Api.Project.ProjectRequirementModule {
|
||||||
|
return {
|
||||||
|
...module,
|
||||||
|
id: normalizeStringId(module.id),
|
||||||
|
parentId: normalizeStringId(module.parentId),
|
||||||
|
projectId: normalizeStringId(module.projectId),
|
||||||
|
children: module.children?.map(normalizeProjectRequirementModule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求分页列表 */
|
||||||
|
export async function fetchGetProjectRequirementPage(params?: Api.Project.ProjectRequirementSearchParams) {
|
||||||
|
const result = await request<ProjectRequirementPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectRequirementPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeProjectRequirement)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求树形列表 */
|
||||||
|
export async function fetchGetProjectRequirementTree(params?: Api.Project.ProjectRequirementSearchParams) {
|
||||||
|
const result = await request<ProjectRequirementPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/tree`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectRequirementPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeProjectRequirement)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求详情 */
|
||||||
|
export async function fetchGetProjectRequirement(id: string, projectId: string) {
|
||||||
|
const result = await request<ProjectRequirementResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id, projectId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectRequirementResponse>, normalizeProjectRequirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建项目需求 */
|
||||||
|
export async function fetchCreateProjectRequirement(data: Api.Project.SaveProjectRequirementParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/create`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新项目需求 */
|
||||||
|
export function fetchUpdateProjectRequirement(data: Api.Project.UpdateProjectRequirementParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/update`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 变更项目需求状态 */
|
||||||
|
export function fetchChangeProjectRequirementStatus(data: Api.Project.ChangeProjectRequirementStatusParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/change-status`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除项目需求 */
|
||||||
|
export function fetchDeleteProjectRequirement(data: Api.Project.DeleteProjectRequirementParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/delete`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拆分项目需求 */
|
||||||
|
export async function fetchSplitProjectRequirement(data: Api.Project.SplitProjectRequirementParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/split`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭项目需求 */
|
||||||
|
export function fetchCloseProjectRequirement(data: Api.Project.CloseProjectRequirementParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/close`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求可执行状态动作列表 */
|
||||||
|
export async function fetchGetProjectRequirementAllowedTransitions(requirementId: string, projectId: string) {
|
||||||
|
const result = await request<Api.Project.ProjectRequirementLifecycleAction[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/allowed-transitions`,
|
||||||
|
method: 'get',
|
||||||
|
params: { requirementId, projectId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.Project.ProjectRequirementLifecycleAction[]>,
|
||||||
|
data => data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量获取项目需求可执行状态动作列表 */
|
||||||
|
export async function fetchGetProjectRequirementAllowedTransitionsBatch(
|
||||||
|
data: Api.Project.ProjectRequirementBatchReqVO
|
||||||
|
) {
|
||||||
|
const result = await request<Api.Project.ProjectRequirementAllowedTransitionBatchRespVO[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/allowed-transitions/batch`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.Project.ProjectRequirementAllowedTransitionBatchRespVO[]>,
|
||||||
|
data1 =>
|
||||||
|
data1.map(item => ({
|
||||||
|
requirementId: normalizeStringId(item.requirementId),
|
||||||
|
transitions: item.transitions
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交项目需求评审 */
|
||||||
|
export async function fetchSubmitProjectRequirementReview(data: Api.Project.ProjectRequirementReviewSubmitParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/review/submit`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求评审记录 */
|
||||||
|
export async function fetchGetProjectRequirementReview(projectId: string, requirementId: string) {
|
||||||
|
const result = await request<ProjectRequirementReviewResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/review/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { projectId, requirementId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProjectRequirementReviewResponse>,
|
||||||
|
normalizeProjectRequirementReview
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求状态字典 */
|
||||||
|
export async function fetchGetProjectRequirementStatusDict() {
|
||||||
|
const result = await request<Api.Project.ProjectRequirementStatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/status/dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Api.Project.ProjectRequirementStatusDict[]>, data => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项目需求模块树 */
|
||||||
|
export async function fetchGetProjectRequirementModuleTree(projectId: string) {
|
||||||
|
const result = await request<ProjectRequirementModuleResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/module/tree`,
|
||||||
|
method: 'get',
|
||||||
|
params: { projectId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectRequirementModuleResponse[]>, data =>
|
||||||
|
data.map(normalizeProjectRequirementModule)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建项目需求模块 */
|
||||||
|
export async function fetchCreateProjectRequirementModule(data: Api.Project.SaveProjectRequirementModuleParams) {
|
||||||
|
const result = await request<string | number>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/module/create`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新项目需求模块 */
|
||||||
|
export function fetchUpdateProjectRequirementModule(data: Api.Project.SaveProjectRequirementModuleParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/module/update`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除项目需求模块 */
|
||||||
|
export function fetchDeleteProjectRequirementModule(data: Api.Project.DeleteProjectRequirementModuleParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_REQUIREMENT_PREFIX}/module/delete`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { RouteMeta } from 'vue-router';
|
|
||||||
import type { ElegantConstRoute, LastLevelRouteKey } from '@elegant-router/types';
|
import type { ElegantConstRoute, LastLevelRouteKey } from '@elegant-router/types';
|
||||||
import { objectContextDomainConfigs } from '@/constants/object-context';
|
import { objectContextDomainConfigs } from '@/constants/object-context';
|
||||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
|||||||
260
src/service/api/system-log.ts
Normal file
260
src/service/api/system-log.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
normalizeStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
|
||||||
|
const LOGIN_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/login-log`;
|
||||||
|
const OPERATE_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/operate-log`;
|
||||||
|
const API_ACCESS_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/api-access-log`;
|
||||||
|
const API_ERROR_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/api-error-log`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
|
||||||
|
type LoginLogResponse = Omit<Api.SystemLog.Login.Log, 'id' | 'userId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OperateLogResponse = Omit<Api.SystemLog.Operate.Log, 'id' | 'userId' | 'bizId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
bizId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiAccessLogResponse = Omit<Api.SystemLog.ApiAccess.Log, 'id' | 'userId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiErrorLogResponse = Omit<Api.SystemLog.ApiError.Log, 'id' | 'userId' | 'processUserId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
processUserId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginLogPageResponse = Api.SystemLog.Common.PageResult<LoginLogResponse>;
|
||||||
|
type OperateLogPageResponse = Api.SystemLog.Common.PageResult<OperateLogResponse>;
|
||||||
|
type ApiAccessLogPageResponse = Api.SystemLog.Common.PageResult<ApiAccessLogResponse>;
|
||||||
|
type ApiErrorLogPageResponse = Api.SystemLog.Common.PageResult<ApiErrorLogResponse>;
|
||||||
|
|
||||||
|
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(item => appendValue(query, key, item));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuery(params: Record<string, unknown> = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
appendValue(query, key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLoginLog(log: LoginLogResponse): Api.SystemLog.Login.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeNullableStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
userType: log.userType ?? null,
|
||||||
|
userAgent: log.userAgent ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOperateLog(log: OperateLogResponse): Api.SystemLog.Operate.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
bizId: normalizeNullableStringId(log.bizId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
action: log.action ?? null,
|
||||||
|
extra: log.extra ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApiAccessLog(log: ApiAccessLogResponse): Api.SystemLog.ApiAccess.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
requestParams: log.requestParams ?? null,
|
||||||
|
responseBody: log.responseBody ?? null,
|
||||||
|
operateModule: log.operateModule ?? null,
|
||||||
|
operateName: log.operateName ?? null,
|
||||||
|
operateType: log.operateType ?? null,
|
||||||
|
resultMsg: log.resultMsg ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApiErrorLog(log: ApiErrorLogResponse): Api.SystemLog.ApiError.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
requestParams: log.requestParams ?? null,
|
||||||
|
exceptionRootCauseMessage: log.exceptionRootCauseMessage ?? null,
|
||||||
|
exceptionStackTrace: log.exceptionStackTrace ?? null,
|
||||||
|
exceptionClassName: log.exceptionClassName ?? null,
|
||||||
|
exceptionFileName: log.exceptionFileName ?? null,
|
||||||
|
exceptionMethodName: log.exceptionMethodName ?? null,
|
||||||
|
exceptionLineNumber: log.exceptionLineNumber ?? null,
|
||||||
|
processTime: log.processTime ?? null,
|
||||||
|
processUserId: normalizeNullableStringId(log.processUserId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetLoginLogPage(params?: Api.SystemLog.Login.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<LoginLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${LOGIN_LOG_PREFIX}/page?${query}` : `${LOGIN_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<LoginLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeLoginLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetLoginLog(id: string) {
|
||||||
|
const result = await request<LoginLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${LOGIN_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<LoginLogResponse>, normalizeLoginLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportLoginLog(params: Api.SystemLog.Login.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${LOGIN_LOG_PREFIX}/export-excel?${query}` : `${LOGIN_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOperateLogPage(params?: Api.SystemLog.Operate.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<OperateLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${OPERATE_LOG_PREFIX}/page?${query}` : `${OPERATE_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OperateLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeOperateLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOperateLog(id: string) {
|
||||||
|
const result = await request<OperateLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OPERATE_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OperateLogResponse>, normalizeOperateLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportOperateLog(params: Api.SystemLog.Operate.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${OPERATE_LOG_PREFIX}/export-excel?${query}` : `${OPERATE_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiAccessLogPage(params?: Api.SystemLog.ApiAccess.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<ApiAccessLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${API_ACCESS_LOG_PREFIX}/page?${query}` : `${API_ACCESS_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiAccessLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeApiAccessLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiAccessLog(id: string) {
|
||||||
|
const result = await request<ApiAccessLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${API_ACCESS_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiAccessLogResponse>, normalizeApiAccessLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportApiAccessLog(params: Api.SystemLog.ApiAccess.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${API_ACCESS_LOG_PREFIX}/export-excel?${query}` : `${API_ACCESS_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiErrorLogPage(params?: Api.SystemLog.ApiError.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<ApiErrorLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${API_ERROR_LOG_PREFIX}/page?${query}` : `${API_ERROR_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiErrorLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeApiErrorLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiErrorLog(id: string) {
|
||||||
|
const result = await request<ApiErrorLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${API_ERROR_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiErrorLogResponse>, normalizeApiErrorLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportApiErrorLog(params: Api.SystemLog.ApiError.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${API_ERROR_LOG_PREFIX}/export-excel?${query}` : `${API_ERROR_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -118,6 +118,11 @@ type UserManagementRelationTreeResponse = Omit<
|
|||||||
children?: UserManagementRelationTreeResponse[] | null;
|
children?: UserManagementRelationTreeResponse[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MySubordinateTreeNodeResponse = Omit<Api.SystemManage.MySubordinateTreeNode, 'userId' | 'children'> & {
|
||||||
|
userId: string | number;
|
||||||
|
children?: MySubordinateTreeNodeResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
|
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
@@ -181,6 +186,14 @@ function normalizeUserManagementRelationTree(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMySubordinateTreeNode(node: MySubordinateTreeNodeResponse): Api.SystemManage.MySubordinateTreeNode {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
userId: normalizeStringId(node.userId),
|
||||||
|
children: node.children?.map(normalizeMySubordinateTreeNode) ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取角色分页 */
|
/** 获取角色分页 */
|
||||||
export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) {
|
export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) {
|
||||||
const query = createRolePageQuery(params);
|
const query = createRolePageQuery(params);
|
||||||
@@ -311,6 +324,18 @@ export function fetchGetDeptSimpleList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取部门自身及全部子部门 */
|
||||||
|
export async function fetchGetDeptSelfAndChildren(id: string) {
|
||||||
|
const result = await request<Api.SystemManage.DeptSelfAndChildrenList>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${DEPT_PREFIX}/list-self-and-children`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Api.SystemManage.DeptSelfAndChildrenList>, data => data);
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建部门 */
|
/** 创建部门 */
|
||||||
export function fetchCreateDept(data: Api.SystemManage.SaveDeptParams) {
|
export function fetchCreateDept(data: Api.SystemManage.SaveDeptParams) {
|
||||||
return request<number>({
|
return request<number>({
|
||||||
@@ -445,7 +470,7 @@ export function fetchBatchDeletePost(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 获取用户简单列表(用于用户选择下拉框) */
|
/** 获取用户简单列表(用于用户选择下拉框) */
|
||||||
export function fetchGetUserSimpleList() {
|
export async function fetchGetUserSimpleList() {
|
||||||
return request<UserSimpleResponse[]>({
|
return request<UserSimpleResponse[]>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${USER_PREFIX}/simple-list`,
|
url: `${USER_PREFIX}/simple-list`,
|
||||||
@@ -455,6 +480,19 @@ export function fetchGetUserSimpleList() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前登录人的直属上级 */
|
||||||
|
export async function fetchGetLoginUserDirectManager() {
|
||||||
|
return request<UserSimpleResponse | null>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${USER_PREFIX}/profile/direct-manager`,
|
||||||
|
method: 'get'
|
||||||
|
}).then(result =>
|
||||||
|
mapServiceResult(result as ServiceRequestResult<UserSimpleResponse | null>, data =>
|
||||||
|
data ? normalizeUserSimple(data) : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取用户分页 */
|
/** 获取用户分页 */
|
||||||
export function fetchGetUserPage(params?: Api.SystemManage.UserSearchParams) {
|
export function fetchGetUserPage(params?: Api.SystemManage.UserSearchParams) {
|
||||||
return request<Api.SystemManage.UserList>({
|
return request<Api.SystemManage.UserList>({
|
||||||
@@ -699,6 +737,29 @@ export async function fetchGetUserManagementRelationQuery(query: UserManagementR
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前登录用户下属树 */
|
||||||
|
export async function fetchGetMySubordinateTree() {
|
||||||
|
return request<MySubordinateTreeNodeResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${USER_MANAGEMENT_RELATION_PREFIX}/my-subordinate-tree`,
|
||||||
|
method: 'get'
|
||||||
|
}).then(result =>
|
||||||
|
mapServiceResult(result as ServiceRequestResult<MySubordinateTreeNodeResponse>, normalizeMySubordinateTreeNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取某用户当前生效的直属下级列表 */
|
||||||
|
export async function fetchGetDirectSubordinates(userId: string) {
|
||||||
|
const result = await request<UserSimpleResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${USER_MANAGEMENT_RELATION_PREFIX}/direct-subordinates`,
|
||||||
|
method: 'get',
|
||||||
|
params: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<UserSimpleResponse[]>, data => data.map(normalizeUserSimple));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户管理链路详情
|
* 获取用户管理链路详情
|
||||||
*
|
*
|
||||||
|
|||||||
987
src/service/api/work-report.ts
Normal file
987
src/service/api/work-report.ts
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
normalizeStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
|
||||||
|
const WORK_REPORT_PREFIX = `${WEB_SERVICE_PREFIX}/project/work-reports`;
|
||||||
|
const WEEKLY_PREFIX = `${WORK_REPORT_PREFIX}/weekly`;
|
||||||
|
const MONTHLY_PREFIX = `${WORK_REPORT_PREFIX}/monthly`;
|
||||||
|
const PROJECT_PREFIX = `${WORK_REPORT_PREFIX}/project`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
type MaybeStringIdResponse = string | number | null | undefined;
|
||||||
|
|
||||||
|
type PageResponse<T> = {
|
||||||
|
total: number | string;
|
||||||
|
list: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReviewItemResponse = Omit<Api.WorkReport.Common.PersonalReportReviewItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlanItemResponse = Omit<Api.WorkReport.Common.PersonalReportPlanItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeeklyTravelSegmentResponse = Omit<Api.WorkReport.Weekly.WeeklyReportTravelSegment, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeeklyReportResponse = Omit<
|
||||||
|
Api.WorkReport.Weekly.WeeklyReport,
|
||||||
|
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems' | 'travelSegments'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
reporterId: StringIdResponse;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
reviewItems?: ReviewItemResponse[] | null;
|
||||||
|
planItems?: PlanItemResponse[] | null;
|
||||||
|
travelSegments?: WeeklyTravelSegmentResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthlyReportResponse = Omit<
|
||||||
|
Api.WorkReport.Monthly.MonthlyReport,
|
||||||
|
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
reporterId: StringIdResponse;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
reviewItems?: ReviewItemResponse[] | null;
|
||||||
|
planItems?: PlanItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MemberSnapshotResponse = Omit<Api.WorkReport.Project.WorkReportMemberSnapshot, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectReportItemResponse = Omit<Api.WorkReport.Project.ProjectReportItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectReportResponse = Omit<
|
||||||
|
Api.WorkReport.Project.ProjectReport,
|
||||||
|
'id' | 'projectId' | 'projectOwnerId' | 'projectMemberSnapshot' | 'supervisorUserId' | 'currentItems' | 'nextItems'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
projectOwnerId: StringIdResponse;
|
||||||
|
projectMemberSnapshot?: MemberSnapshotResponse[] | null;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
currentItems?: ProjectReportItemResponse[] | null;
|
||||||
|
nextItems?: ProjectReportItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalRecordResponse = Omit<
|
||||||
|
Api.WorkReport.Common.WorkReportApprovalRecord,
|
||||||
|
'id' | 'statusLogId' | 'auditorUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
auditorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthlyApprovalRecordResponse = Omit<
|
||||||
|
Api.WorkReport.Monthly.MonthlyReportApprovalRecord,
|
||||||
|
'id' | 'statusLogId' | 'auditorUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
auditorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectOptionResponse = Omit<Api.WorkReport.Project.ProjectReportOwnerProjectOption, 'id'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamReportPendingUserResponse = Omit<Api.WorkReport.Common.TeamReportPendingUser, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamReportSummaryResponse = Omit<
|
||||||
|
Api.WorkReport.Common.TeamReportSummary,
|
||||||
|
'unsubmittedUsers' | 'periodStartDate' | 'periodEndDate'
|
||||||
|
> & {
|
||||||
|
unsubmittedUsers?: TeamReportPendingUserResponse[] | null;
|
||||||
|
periodStartDate?: unknown;
|
||||||
|
periodEndDate?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
if (typeof value === 'number') return value === 1;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return !['', '0', 'false', 'n', 'no'].includes(normalized);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApprovalConclusion(value: unknown) {
|
||||||
|
const conclusion = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (conclusion === 'approve') return 'approved';
|
||||||
|
if (conclusion === 'reject') return 'rejected';
|
||||||
|
|
||||||
|
return conclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateText(value: unknown) {
|
||||||
|
if (value === null || value === undefined) return undefined;
|
||||||
|
const text = String(value).trim();
|
||||||
|
const commaDateMatch = text.match(/^(\d{4}),(\d{1,2}),(\d{1,2})$/);
|
||||||
|
|
||||||
|
if (commaDateMatch) {
|
||||||
|
const [, year, month, day] = commaDateMatch;
|
||||||
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTotal(total: number | string) {
|
||||||
|
const value = Number(total);
|
||||||
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumWorkHours(items: Array<{ workHours?: number | string | null }> = []) {
|
||||||
|
return items.reduce((sum, item) => {
|
||||||
|
const value = Number(item.workHours ?? 0);
|
||||||
|
return Number.isFinite(value) ? sum + value : sum;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReportTotalWorkHours(
|
||||||
|
totalWorkHours: number | string | null | undefined,
|
||||||
|
fallbackTotalWorkHours: number
|
||||||
|
) {
|
||||||
|
const normalizedTotal = Number(totalWorkHours ?? 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(totalWorkHours === null ||
|
||||||
|
totalWorkHours === undefined ||
|
||||||
|
totalWorkHours === '' ||
|
||||||
|
(Number.isFinite(normalizedTotal) && normalizedTotal === 0)) &&
|
||||||
|
fallbackTotalWorkHours > 0
|
||||||
|
) {
|
||||||
|
return fallbackTotalWorkHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWorkHours ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendArray(query: URLSearchParams, key: string, values?: Array<string | null | undefined> | null) {
|
||||||
|
values?.forEach(value => appendValue(query, key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendNullableArrayFlag(
|
||||||
|
query: URLSearchParams,
|
||||||
|
key: string,
|
||||||
|
values?: Array<string | null | undefined> | null
|
||||||
|
) {
|
||||||
|
if (values === null || values === undefined) return;
|
||||||
|
|
||||||
|
if (!values.length) {
|
||||||
|
query.append(key, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendArray(query, key, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
appendValue(query, 'pageNo', params.pageNo ?? 1);
|
||||||
|
appendValue(query, 'pageSize', params.pageSize ?? 10);
|
||||||
|
appendValue(query, 'keyword', params.keyword);
|
||||||
|
appendValue(query, 'statusCode', params.statusCode);
|
||||||
|
appendValue(query, 'supervisorName', params.supervisorName);
|
||||||
|
appendArray(query, 'periodStartDate', params.periodStartDate);
|
||||||
|
appendArray(query, 'submitTime', params.submitTime);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'reporterIds', params.reporterIds);
|
||||||
|
appendValue(query, 'isBusinessTrip', params.isBusinessTrip);
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'reporterIds', params.reporterIds);
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'projectOwnerIds', params.projectOwnerIds);
|
||||||
|
appendValue(query, 'projectId', params.projectId);
|
||||||
|
appendValue(query, 'flag', params.flag);
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReviewItem(item: ReviewItemResponse): Api.WorkReport.Common.PersonalReportReviewItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlanItem(item: PlanItemResponse): Api.WorkReport.Common.PersonalReportPlanItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeeklyTravelSegment(
|
||||||
|
item: WeeklyTravelSegmentResponse
|
||||||
|
): Api.WorkReport.Weekly.WeeklyReportTravelSegment {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined,
|
||||||
|
startDate: normalizeDateText(item.startDate),
|
||||||
|
endDate: normalizeDateText(item.endDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeeklyReport(response: WeeklyReportResponse): Api.WorkReport.Weekly.WeeklyReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
reporterId: normalizeStringId(response.reporterId),
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
reporterDeptName: response.reporterDeptName ?? null,
|
||||||
|
reporterPostName: response.reporterPostName ?? null,
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
isBusinessTrip: normalizeBooleanFlag(response.isBusinessTrip),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||||
|
planItems: response.planItems?.map(normalizePlanItem) ?? [],
|
||||||
|
travelSegments: response.travelSegments?.map(normalizeWeeklyTravelSegment) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMonthlyReport(response: MonthlyReportResponse): Api.WorkReport.Monthly.MonthlyReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
reporterId: normalizeStringId(response.reporterId),
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
reporterDeptName: response.reporterDeptName ?? null,
|
||||||
|
reporterPostName: response.reporterPostName ?? null,
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||||
|
planItems: response.planItems?.map(normalizePlanItem) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMemberSnapshot(item: MemberSnapshotResponse): Api.WorkReport.Project.WorkReportMemberSnapshot {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectReportItem(item: ProjectReportItemResponse): Api.WorkReport.Project.ProjectReportItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectReport(response: ProjectReportResponse): Api.WorkReport.Project.ProjectReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.currentItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
projectOwnerId: normalizeStringId(response.projectOwnerId),
|
||||||
|
projectMemberSnapshot: response.projectMemberSnapshot?.map(normalizeMemberSnapshot) ?? [],
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
currentItems: response.currentItems?.map(normalizeProjectReportItem) ?? [],
|
||||||
|
nextItems: response.nextItems?.map(normalizeProjectReportItem) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApprovalRecord(response: ApprovalRecordResponse): Api.WorkReport.Common.WorkReportApprovalRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
|
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMonthlyApprovalRecord(
|
||||||
|
response: MonthlyApprovalRecordResponse
|
||||||
|
): Api.WorkReport.Monthly.MonthlyReportApprovalRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
|
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectOption(
|
||||||
|
response: ProjectOptionResponse
|
||||||
|
): Api.WorkReport.Project.ProjectReportOwnerProjectOption {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTeamReportSummary(response: TeamReportSummaryResponse): Api.WorkReport.Common.TeamReportSummary {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
periodStartDate: normalizeDateText(response.periodStartDate) ?? undefined,
|
||||||
|
periodEndDate: normalizeDateText(response.periodEndDate) ?? undefined,
|
||||||
|
unsubmittedUsers:
|
||||||
|
response.unsubmittedUsers?.map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
})) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPage<TInput, TOutput>(data: PageResponse<TInput>, mapper: (item: TInput) => TOutput) {
|
||||||
|
return {
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(mapper)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStatusActionRequest(data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return {
|
||||||
|
reason: data.reason?.trim() || undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalReviewItems(items: Api.WorkReport.Common.PersonalReportReviewItem[] = []) {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
contentText: item.contentText?.trim() || '',
|
||||||
|
contentJson: item.contentJson ?? null,
|
||||||
|
reflectionText: item.reflectionText?.trim() || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalPlanItems(items: Api.WorkReport.Common.PersonalReportPlanItem[] = []) {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
targetText: item.targetText?.trim() || '',
|
||||||
|
targetJson: item.targetJson ?? null,
|
||||||
|
supportNeed: item.supportNeed?.trim() || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWeeklySaveRequest(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
return {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
isBusinessTrip: data.isBusinessTrip,
|
||||||
|
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||||
|
planItems: toPersonalPlanItems(data.planItems),
|
||||||
|
travelSegments: data.isBusinessTrip
|
||||||
|
? data.travelSegments.map((item, index) => ({
|
||||||
|
sort: item.sort ?? index + 1,
|
||||||
|
startDate: item.startDate || undefined,
|
||||||
|
endDate: item.endDate || undefined,
|
||||||
|
travelDays: item.travelDays ?? 0,
|
||||||
|
location: item.location?.trim() || ''
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMonthlySaveRequest(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
return {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||||
|
planItems: toPersonalPlanItems(data.planItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectItems(items: Api.WorkReport.Project.ProjectReportItem[] = []) {
|
||||||
|
return items.map(item => ({
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
priorityCode: item.priorityCode || undefined,
|
||||||
|
progressRate: item.progressRate ?? 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectSaveRequest(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
return {
|
||||||
|
projectId: data.projectId,
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
flag: data.flag,
|
||||||
|
projectStatusDesc: data.projectStatusDesc?.trim() || '',
|
||||||
|
projectProgressPlan: data.projectProgressPlan?.trim() || '',
|
||||||
|
projectKeyPoints: data.projectKeyPoints?.trim() || '',
|
||||||
|
projectProblems: data.projectProblems?.trim() || '',
|
||||||
|
currentItems: toProjectItems(data.currentItems),
|
||||||
|
nextItems: toProjectItems(data.nextItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWorkReportStatusDict() {
|
||||||
|
const result = await request<Api.WorkReport.Common.WorkReportStatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/status/dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Api.WorkReport.Common.WorkReportStatusDict[]>, data => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetTeamReportSummary(params: Api.WorkReport.Common.TeamReportSummaryParams) {
|
||||||
|
const result = await request<TeamReportSummaryResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/team/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamReportSummaryResponse>, normalizeTeamReportSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemindTeamReport(data: Api.WorkReport.Common.TeamReportRemindParams) {
|
||||||
|
const result = await request<Api.WorkReport.Common.TeamReportRemindResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/team/remind`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
userIds: data.userIds && data.userIds.length ? data.userIds : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.WorkReport.Common.TeamReportRemindResult>,
|
||||||
|
payload => payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/page?${query}` : `${WEEKLY_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeWeeklyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportApprovalPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/approval-page?${query}` : `${WEEKLY_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeWeeklyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportDetail(id: string) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitWeeklyReport() {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/init`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewWeeklyReportDefaultDraft(
|
||||||
|
params: Api.WorkReport.Weekly.WeeklyReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshWeeklyReportDraft(data: Api.WorkReport.Weekly.WeeklyReportRefreshDraftParams) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateWeeklyReport(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: WEEKLY_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateWeeklyReport(id: string, data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitWeeklyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteWeeklyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<ApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportWeeklyReports(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/export?${query}` : `${WEEKLY_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportWeeklyReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Weekly.WeeklyReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${WEEKLY_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/page?${query}` : `${MONTHLY_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeMonthlyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportApprovalPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/approval-page?${query}` : `${MONTHLY_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeMonthlyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportDetail(id: string) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitMonthlyReport() {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/init`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewMonthlyReportDefaultDraft(
|
||||||
|
params: Api.WorkReport.Monthly.MonthlyReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshMonthlyReportDraft(data: Api.WorkReport.Monthly.MonthlyReportRefreshDraftParams) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateMonthlyReport(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: MONTHLY_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitMonthlyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectMonthlyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteMonthlyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<MonthlyApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeMonthlyApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportMonthlyReports(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/export?${query}` : `${MONTHLY_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportMonthlyReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Monthly.MonthlyReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${MONTHLY_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportOwnerProjectOptions() {
|
||||||
|
const result = await request<ProjectOptionResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/owner-project-options`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectOptionResponse[]>, data =>
|
||||||
|
data.map(normalizeProjectOption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PROJECT_PREFIX}/page?${query}` : `${PROJECT_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeProjectReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportApprovalPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PROJECT_PREFIX}/approval-page?${query}` : `${PROJECT_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeProjectReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportDetail(id: string) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitProjectReport(projectId: string) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/init`,
|
||||||
|
method: 'get',
|
||||||
|
params: { projectId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewProjectReportDefaultDraft(
|
||||||
|
projectId: string,
|
||||||
|
params: Api.WorkReport.Project.ProjectReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${projectId}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshProjectReportDraft(
|
||||||
|
projectId: string,
|
||||||
|
data: Api.WorkReport.Project.ProjectReportRefreshDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${projectId}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
flag: data.flag,
|
||||||
|
projectStatusDesc: data.projectStatusDesc?.trim() || '',
|
||||||
|
projectProgressPlan: data.projectProgressPlan?.trim() || '',
|
||||||
|
projectKeyPoints: data.projectKeyPoints?.trim() || '',
|
||||||
|
projectProblems: data.projectProblems?.trim() || '',
|
||||||
|
currentItems: toProjectItems(data.currentItems),
|
||||||
|
nextItems: toProjectItems(data.nextItems)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateProjectReport(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: PROJECT_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toProjectSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateProjectReport(id: string, data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toProjectSaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitProjectReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteProjectReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<ApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportProjectReports(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${PROJECT_PREFIX}/export?${query}` : `${PROJECT_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportProjectReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Project.ProjectReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${PROJECT_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,17 @@ import type { InternalAxiosRequestConfig } from 'axios';
|
|||||||
declare module 'axios' {
|
declare module 'axios' {
|
||||||
interface AxiosRequestConfig {
|
interface AxiosRequestConfig {
|
||||||
dedupe?: boolean;
|
dedupe?: boolean;
|
||||||
|
/**
|
||||||
|
* 跳过 Authorization 注入。
|
||||||
|
*
|
||||||
|
* 用于公开接口(refresh-token / login / register 等 PermitAll 路径),
|
||||||
|
* 避免给它们带上过期 access 头被网关拦截。
|
||||||
|
*/
|
||||||
|
skipAuth?: boolean;
|
||||||
|
/** 请求失败时不走通用错误 toast,由调用方自行收敛提示。 */
|
||||||
|
suppressErrorMessage?: boolean;
|
||||||
|
/** 请求失败命中过期 access code 时,不再触发 refresh-token 流程。 */
|
||||||
|
skipTokenRefresh?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
src/service/request/error-message.ts
Normal file
32
src/service/request/error-message.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export const SESSION_EXPIRED_MESSAGE = '登录已失效,请重新登录';
|
||||||
|
|
||||||
|
export interface ErrorMessageSuppressOptions {
|
||||||
|
backendErrorCode: string;
|
||||||
|
suppressErrorMessage?: boolean;
|
||||||
|
logoutCodes: string[];
|
||||||
|
modalLogoutCodes: string[];
|
||||||
|
expiredTokenCodes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendFailDeferOptions {
|
||||||
|
suppressErrorMessage?: boolean;
|
||||||
|
skipTokenRefresh?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseServiceCodes(codes?: string) {
|
||||||
|
return codes?.split(',').filter(Boolean) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldDeferBackendFailToCaller(options: BackendFailDeferOptions) {
|
||||||
|
return Boolean(options.suppressErrorMessage && options.skipTokenRefresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldSuppressErrorMessage(options: ErrorMessageSuppressOptions) {
|
||||||
|
if (options.suppressErrorMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handledCodes = [...options.logoutCodes, ...options.modalLogoutCodes, ...options.expiredTokenCodes];
|
||||||
|
|
||||||
|
return handledCodes.includes(options.backendErrorCode);
|
||||||
|
}
|
||||||
@@ -5,17 +5,20 @@ import { localStg } from '@/utils/storage';
|
|||||||
import { getServiceBaseURL } from '@/utils/service';
|
import { getServiceBaseURL } from '@/utils/service';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { applyApiEncrypt } from './api-encrypt';
|
import { applyApiEncrypt } from './api-encrypt';
|
||||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
import { parseServiceCodes, shouldDeferBackendFailToCaller, shouldSuppressErrorMessage } from './error-message';
|
||||||
|
import { getAuthorization, handleExpiredRequest, notifySessionExpired, showErrorMsg } from './shared';
|
||||||
import { withDedupe } from './dedupe';
|
import { withDedupe } from './dedupe';
|
||||||
import type { RequestInstanceState } from './type';
|
import type { RequestInstanceState } from './type';
|
||||||
|
|
||||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
const REQUEST_TIMEOUT = 15 * 1000;
|
||||||
|
|
||||||
export const request = withDedupe(
|
export const request = withDedupe(
|
||||||
createFlatRequest(
|
createFlatRequest(
|
||||||
{
|
{
|
||||||
baseURL,
|
baseURL,
|
||||||
|
timeout: REQUEST_TIMEOUT,
|
||||||
headers: {
|
headers: {
|
||||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
||||||
}
|
}
|
||||||
@@ -29,8 +32,12 @@ export const request = withDedupe(
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
async onRequest(config) {
|
async onRequest(config) {
|
||||||
const Authorization = getAuthorization();
|
// skipAuth 为 true 的请求不注入 Authorization——避免给公开接口(如 refresh-token)
|
||||||
Object.assign(config.headers, { Authorization });
|
// 带上过期 access 头被网关拦截(网关只看 Authorization,不区分路由是否 PermitAll)
|
||||||
|
if (!config.skipAuth) {
|
||||||
|
const Authorization = getAuthorization();
|
||||||
|
Object.assign(config.headers, { Authorization });
|
||||||
|
}
|
||||||
applyApiEncrypt(config);
|
applyApiEncrypt(config);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
@@ -44,6 +51,15 @@ export const request = withDedupe(
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const responseCode = String(response.data.code);
|
const responseCode = String(response.data.code);
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldDeferBackendFailToCaller({
|
||||||
|
suppressErrorMessage: response.config.suppressErrorMessage,
|
||||||
|
skipTokenRefresh: response.config.skipTokenRefresh
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
authStore.resetStore();
|
authStore.resetStore();
|
||||||
}
|
}
|
||||||
@@ -55,15 +71,16 @@ export const request = withDedupe(
|
|||||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
|
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当后端返回码命中 `logoutCodes` 时,表示用户需要退出登录并跳转到登录页
|
// 当后端返回码命中 `logoutCodes` 时,表示登录态已失效,需要提示后退出登录
|
||||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
// 走 notifySessionExpired 而不是裸 resetStore:保证并发请求只弹一次 toast、只清一次状态
|
||||||
|
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
|
||||||
if (logoutCodes.includes(responseCode)) {
|
if (logoutCodes.includes(responseCode)) {
|
||||||
handleLogout();
|
notifySessionExpired();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
|
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
|
||||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
|
||||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
||||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||||
|
|
||||||
@@ -87,8 +104,13 @@ export const request = withDedupe(
|
|||||||
|
|
||||||
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
|
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
|
||||||
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
|
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
|
||||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
|
||||||
if (expiredTokenCodes.includes(responseCode)) {
|
if (expiredTokenCodes.includes(responseCode)) {
|
||||||
|
if (response.config.skipTokenRefresh) {
|
||||||
|
notifySessionExpired();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const success = await handleExpiredRequest(request.state);
|
const success = await handleExpiredRequest(request.state);
|
||||||
if (success) {
|
if (success) {
|
||||||
const Authorization = getAuthorization();
|
const Authorization = getAuthorization();
|
||||||
@@ -106,21 +128,29 @@ export const request = withDedupe(
|
|||||||
let message = error.message;
|
let message = error.message;
|
||||||
let backendErrorCode = '';
|
let backendErrorCode = '';
|
||||||
|
|
||||||
|
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||||
|
message = '请求超时,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
// 获取后端错误信息和错误码
|
// 获取后端错误信息和错误码
|
||||||
if (error.code === BACKEND_ERROR_CODE) {
|
if (error.code === BACKEND_ERROR_CODE) {
|
||||||
message = error.response?.data?.msg || message;
|
message = error.response?.data?.msg || message;
|
||||||
backendErrorCode = String(error.response?.data?.code || '');
|
backendErrorCode = String(error.response?.data?.code || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这类错误信息已经通过弹窗展示,不再重复提示
|
const suppressErrorMessage = Boolean(error.config?.suppressErrorMessage);
|
||||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
|
||||||
if (modalLogoutCodes.includes(backendErrorCode)) {
|
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
|
||||||
return;
|
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
|
||||||
}
|
if (
|
||||||
|
shouldSuppressErrorMessage({
|
||||||
// token 过期时会自动刷新并重试请求,这里无需额外提示
|
backendErrorCode,
|
||||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
suppressErrorMessage,
|
||||||
if (expiredTokenCodes.includes(backendErrorCode)) {
|
logoutCodes,
|
||||||
|
modalLogoutCodes,
|
||||||
|
expiredTokenCodes
|
||||||
|
})
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
import { fetchRefreshToken } from '../api';
|
import { fetchRefreshToken } from '../api';
|
||||||
|
import { SESSION_EXPIRED_MESSAGE } from './error-message';
|
||||||
import type { RequestInstanceState } from './type';
|
import type { RequestInstanceState } from './type';
|
||||||
|
|
||||||
export function getAuthorization() {
|
export function getAuthorization() {
|
||||||
@@ -12,8 +13,6 @@ export function getAuthorization() {
|
|||||||
|
|
||||||
/** 刷新 token */
|
/** 刷新 token */
|
||||||
async function handleRefreshToken() {
|
async function handleRefreshToken() {
|
||||||
const { resetStore } = useAuthStore();
|
|
||||||
|
|
||||||
const rToken = localStg.get('refreshToken') || '';
|
const rToken = localStg.get('refreshToken') || '';
|
||||||
const { error, data } = await fetchRefreshToken(rToken);
|
const { error, data } = await fetchRefreshToken(rToken);
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@@ -22,25 +21,48 @@ async function handleRefreshToken() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetStore();
|
notifySessionExpired();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||||
if (!state.refreshTokenFn) {
|
if (!state.refreshTokenPromise) {
|
||||||
state.refreshTokenFn = handleRefreshToken();
|
state.refreshTokenPromise = handleRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await state.refreshTokenFn;
|
const success = await state.refreshTokenPromise;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
state.refreshTokenFn = null;
|
state.refreshTokenPromise = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 会话失效一次性锁:保证 N 个并发请求只弹一次 toast、只 resetStore 一次
|
||||||
|
let sessionExpiredNotified = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知用户会话已失效,弹一次 toast 后清状态、跳登录。
|
||||||
|
*
|
||||||
|
* 多个并发请求触发时只会真正执行一次;登录成功后由 resetSessionExpiredFlag() 复位。
|
||||||
|
*/
|
||||||
|
export function notifySessionExpired() {
|
||||||
|
if (sessionExpiredNotified) return;
|
||||||
|
sessionExpiredNotified = true;
|
||||||
|
|
||||||
|
window.$message?.error(SESSION_EXPIRED_MESSAGE);
|
||||||
|
|
||||||
|
const { resetStore } = useAuthStore();
|
||||||
|
resetStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功后复位一次性锁,让下一次会话失效仍能正常提示 */
|
||||||
|
export function resetSessionExpiredFlag() {
|
||||||
|
sessionExpiredNotified = false;
|
||||||
|
}
|
||||||
|
|
||||||
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
||||||
if (!state.errMsgStack?.length) {
|
if (!state.errMsgStack?.length) {
|
||||||
state.errMsgStack = [];
|
state.errMsgStack = [];
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export interface RequestInstanceState {
|
|||||||
refreshTokenPromise: Promise<boolean> | null;
|
refreshTokenPromise: Promise<boolean> | null;
|
||||||
/** 请求错误信息栈 */
|
/** 请求错误信息栈 */
|
||||||
errMsgStack: string[];
|
errMsgStack: string[];
|
||||||
|
// 索引签名是 @sa/axios 的 defaultState 类型约束(要求 Record<string, unknown>)的硬要求,不能删
|
||||||
|
// 字段名对齐已通过把 shared.ts 里的 refreshTokenFn 全部改成 refreshTokenPromise 来消除隐患
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
|||||||
|
|
||||||
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
||||||
|
|
||||||
useTitle(documentTitle);
|
useTitle(`研发管理系统 - ${documentTitle}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useRoute } from 'vue-router';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useLoading } from '@sa/hooks';
|
import { useLoading } from '@sa/hooks';
|
||||||
import { clearAuthApiCache, fetchGetUserInfo, fetchLogin } from '@/service/api';
|
import { clearAuthApiCache, fetchGetUserInfo, fetchLogin } from '@/service/api';
|
||||||
|
import { resetSessionExpiredFlag } from '@/service/request/shared';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
@@ -50,16 +51,27 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
|
|
||||||
clearAuthStorage();
|
clearAuthStorage();
|
||||||
|
|
||||||
authStore.$reset();
|
// setup store 没有内置 $reset,需要显式重置内部状态,避免 token / userInfo 残留导致 isLogin 误判。
|
||||||
dictStore.resetDictCache();
|
token.value = '';
|
||||||
objectContextStore.$reset();
|
Object.assign(userInfo, {
|
||||||
|
userId: '',
|
||||||
|
userName: '',
|
||||||
|
nickname: '',
|
||||||
|
roles: [],
|
||||||
|
buttons: []
|
||||||
|
});
|
||||||
|
|
||||||
if (!route.meta.constant) {
|
dictStore.resetDictCache();
|
||||||
|
objectContextStore.clearContext();
|
||||||
|
|
||||||
|
// 用路由名判断当前是否已在登录页,避免依赖 route.meta.constant ——
|
||||||
|
// workbench 等首页也是常量路由,原写法会让常量路由上的登出请求不跳转。
|
||||||
|
if (route.name !== 'login') {
|
||||||
await toLogin();
|
await toLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
tabStore.cacheTabs();
|
tabStore.cacheTabs();
|
||||||
routeStore.resetStore();
|
await routeStore.resetStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
|
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
|
||||||
@@ -119,11 +131,17 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
// If the tab needs to be cleared,it means we don't need to redirect.
|
// If the tab needs to be cleared,it means we don't need to redirect.
|
||||||
needRedirect = false;
|
needRedirect = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳首页前先把权限路由建好:菜单/路由/首页 redirect 全部就绪后再导航,
|
||||||
|
// 否则依赖守卫在"跳首页"那次导航里懒加载,会出现首页先以空 menus 渲染、
|
||||||
|
// 之后无新导航补灌、菜单一直空到手动刷新才恢复的竞态。
|
||||||
|
await routeStore.initAuthRoute();
|
||||||
|
|
||||||
await redirectFromLogin(needRedirect);
|
await redirectFromLogin(needRedirect);
|
||||||
|
|
||||||
window.$notification?.success({
|
window.$notification?.success({
|
||||||
title: $t('page.login.common.loginSuccess'),
|
title: $t('page.login.common.loginSuccess'),
|
||||||
message: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
message: $t('page.login.common.welcomeBack', { userName: userInfo.nickname || userInfo.userName }),
|
||||||
duration: 4500
|
duration: 4500
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,6 +167,9 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
|
|
||||||
token.value = loginToken.token;
|
token.value = loginToken.token;
|
||||||
|
|
||||||
|
// 复位会话失效一次性锁,让下一次会话失效仍能正常提示
|
||||||
|
resetSessionExpiredFlag();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +189,18 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshUserInfo() {
|
||||||
|
const { data: info, error } = await fetchGetUserInfo(true);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
Object.assign(userInfo, info);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function initUserInfo() {
|
async function initUserInfo() {
|
||||||
const hasToken = getToken();
|
const hasToken = getToken();
|
||||||
|
|
||||||
@@ -190,6 +223,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
loginLoading,
|
loginLoading,
|
||||||
resetStore,
|
resetStore,
|
||||||
login,
|
login,
|
||||||
initUserInfo
|
initUserInfo,
|
||||||
|
refreshUserInfo
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE } from '@/constants/dict';
|
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE } from '@/constants/dict';
|
||||||
import { fetchGetFrontendDictCache } from '@/service/api';
|
import { fetchGetDictDataByCode, fetchGetFrontendDictCache } from '@/service/api';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
|
|
||||||
type DictValue = string | number | null | undefined;
|
type DictValue = string | number | null | undefined;
|
||||||
@@ -19,6 +19,24 @@ function sortDictData(list: Api.Dict.DictData[]) {
|
|||||||
return list.slice().sort((left, right) => left.sort - right.sort || left.label.localeCompare(right.label, 'zh-CN'));
|
return list.slice().sort((left, right) => left.sort - right.sort || left.label.localeCompare(right.label, 'zh-CN'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hex 色值兜底校验:仅接受 #RRGGBB(6 位);其他格式(含 #RGB 简写 / rgb())一律视为无效回落到默认渲染
|
||||||
|
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/i;
|
||||||
|
|
||||||
|
function normalizeColorType(raw: unknown): string | null {
|
||||||
|
if (typeof raw !== 'string') return null;
|
||||||
|
const trimmed = raw.trim().toLowerCase();
|
||||||
|
return HEX_COLOR_PATTERN.test(trimmed) ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析字典项最终展示色(hex)。
|
||||||
|
* 精确色 cssClass 优先(覆盖 colorType 落到语义色无法区分黄/橙等场景),其次 colorType;
|
||||||
|
* 两者都不是合法 hex 时回落 null(默认渲染)。
|
||||||
|
*/
|
||||||
|
function resolveDisplayColor(colorType: unknown, cssClass: unknown): string | null {
|
||||||
|
return normalizeColorType(cssClass) ?? normalizeColorType(colorType);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictData(
|
function normalizeFrontendDictData(
|
||||||
dictType: string,
|
dictType: string,
|
||||||
list: Api.Dict.FrontendDictData[],
|
list: Api.Dict.FrontendDictData[],
|
||||||
@@ -31,13 +49,25 @@ function normalizeFrontendDictData(
|
|||||||
dictType: item.dictType || dictType,
|
dictType: item.dictType || dictType,
|
||||||
sort: item.sort,
|
sort: item.sort,
|
||||||
status: item.status ?? 0,
|
status: item.status ?? 0,
|
||||||
remark: null,
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
|
remark: item.remark ?? null,
|
||||||
createTime: 0
|
createTime: 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return sortDictData(normalizedList);
|
return sortDictData(normalizedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDictDataItem(item: Api.Dict.DictData, dictType: string): Api.Dict.DictData {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: String(item.value),
|
||||||
|
dictType: item.dictType || dictType,
|
||||||
|
status: item.status ?? 0,
|
||||||
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
|
remark: item.remark ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictCache(cache: Api.Dict.FrontendDictCache) {
|
function normalizeFrontendDictCache(cache: Api.Dict.FrontendDictCache) {
|
||||||
const entries = Object.entries(cache);
|
const entries = Object.entries(cache);
|
||||||
|
|
||||||
@@ -89,6 +119,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
const loadedAt = ref<number | null>(null);
|
const loadedAt = ref<number | null>(null);
|
||||||
|
|
||||||
let initPromise: Promise<boolean> | null = null;
|
let initPromise: Promise<boolean> | null = null;
|
||||||
|
const dictDataLoadPromises = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
function resetDictCache() {
|
function resetDictCache() {
|
||||||
dictTypes.value = [];
|
dictTypes.value = [];
|
||||||
@@ -96,6 +127,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
loadedAt.value = null;
|
loadedAt.value = null;
|
||||||
initialized.value = false;
|
initialized.value = false;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
|
dictDataLoadPromises.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDictCache(force = false) {
|
async function initDictCache(force = false) {
|
||||||
@@ -137,6 +169,51 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
return initPromise;
|
return initPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureDictData(dictType: string, force = false) {
|
||||||
|
if (!dictType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialized.value) {
|
||||||
|
await initDictCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && getDictData(dictType).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = dictDataLoadPromises.get(dictType);
|
||||||
|
if (pending && !force) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const result = await fetchGetDictDataByCode(dictType);
|
||||||
|
|
||||||
|
if (result.error || !result.data?.list?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dictDataMap.value = {
|
||||||
|
...dictDataMap.value,
|
||||||
|
[dictType]: sortDictData(result.data.list.map(item => normalizeDictDataItem(item, dictType)))
|
||||||
|
};
|
||||||
|
dictTypes.value = createRuntimeDictTypes(dictDataMap.value);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
dictDataLoadPromises.set(dictType, promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} finally {
|
||||||
|
if (dictDataLoadPromises.get(dictType) === promise) {
|
||||||
|
dictDataLoadPromises.delete(dictType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getDictData(dictType: string, onlyEnabled = false) {
|
function getDictData(dictType: string, onlyEnabled = false) {
|
||||||
if (!dictType) {
|
if (!dictType) {
|
||||||
return [];
|
return [];
|
||||||
@@ -199,6 +276,7 @@ export const useDictStore = defineStore(SetupStoreId.Dict, () => {
|
|||||||
dictDataMap,
|
dictDataMap,
|
||||||
loadedAt,
|
loadedAt,
|
||||||
initDictCache,
|
initDictCache,
|
||||||
|
ensureDictData,
|
||||||
resetDictCache,
|
resetDictCache,
|
||||||
getDictData,
|
getDictData,
|
||||||
getDictOptions,
|
getDictOptions,
|
||||||
|
|||||||
@@ -149,9 +149,16 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
|
|
||||||
/** 重置 store */
|
/** 重置 store */
|
||||||
async function resetStore() {
|
async function resetStore() {
|
||||||
const routeStore = useRouteStore();
|
// setup store 没有内置 $reset,需要显式重置内部状态。
|
||||||
|
// 否则 isInitConstantRoute / isInitAuthRoute 一直停在 true,导致下面 initConstantRoute 早返,
|
||||||
routeStore.$reset();
|
// 路由被 resetVueRoutes 摘掉后无法重新注册,菜单和导航都会失效。
|
||||||
|
setIsInitConstantRoute(false);
|
||||||
|
setIsInitAuthRoute(false);
|
||||||
|
constantRoutes.value = [];
|
||||||
|
authRoutes.value = [];
|
||||||
|
menus.value = [];
|
||||||
|
cacheRoutes.value = [];
|
||||||
|
excludeCacheRoutes.value = [];
|
||||||
|
|
||||||
resetVueRoutes();
|
resetVueRoutes();
|
||||||
|
|
||||||
@@ -242,7 +249,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
/** 统一处理常量路由和权限路由 */
|
/** 统一处理常量路由和权限路由 */
|
||||||
async function handleConstantAndAuthRoutes() {
|
async function handleConstantAndAuthRoutes() {
|
||||||
const { getAuthVueRoutes } = await loadRouteModule();
|
const { getAuthVueRoutes } = await loadRouteModule();
|
||||||
const allRoutes = [...constantRoutes.value, ...authRoutes.value];
|
// 常量路由优先:动态权限路由中与常量路由 name 重复的项剔除,避免菜单出现重复入口(如 workbench)
|
||||||
|
const constantRouteNames = new Set(constantRoutes.value.map(route => route.name));
|
||||||
|
const dedupedAuthRoutes = authRoutes.value.filter(route => !constantRouteNames.has(route.name));
|
||||||
|
const allRoutes = [...constantRoutes.value, ...dedupedAuthRoutes];
|
||||||
|
|
||||||
const sortRoutes = sortRoutesByOrder(allRoutes);
|
const sortRoutes = sortRoutesByOrder(allRoutes);
|
||||||
|
|
||||||
|
|||||||
11
src/store/modules/workbench/index.ts
Normal file
11
src/store/modules/workbench/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useWorkbenchLayout } from '@/views/workbench/composables/use-workbench-layout';
|
||||||
|
import { SetupStoreId } from '@/enum';
|
||||||
|
import { useAuthStore } from '../auth';
|
||||||
|
|
||||||
|
export const useWorkbenchStore = defineStore(SetupStoreId.Workbench, () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const userId = computed(() => String(authStore.userInfo?.userId ?? 'anonymous'));
|
||||||
|
return useWorkbenchLayout({ userId: userId.value });
|
||||||
|
});
|
||||||
@@ -406,6 +406,7 @@ html .el-collapse {
|
|||||||
.business-table-action-cell {
|
.business-table-action-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@@ -416,6 +417,20 @@ html .el-collapse {
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-table-action-icon-button {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.el-button + .el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-table-action-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.business-table-action-menu {
|
.business-table-action-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -428,6 +443,19 @@ html .el-collapse {
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-table-action-menu__link {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-table-action-menu__item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.business-table-card-body {
|
.business-table-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 56px);
|
height: calc(100% - 56px);
|
||||||
|
|||||||
@@ -89,4 +89,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
*
|
*
|
||||||
* If publish new version, use `overrideThemeSettings` to override certain theme settings
|
* If publish new version, use `overrideThemeSettings` to override certain theme settings
|
||||||
*/
|
*/
|
||||||
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {};
|
// 系统固定亮色主题:切换入口已全部移除,发新版时把老用户缓存的暗色设置刷回亮色
|
||||||
|
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {
|
||||||
|
themeScheme: 'light'
|
||||||
|
};
|
||||||
|
|||||||
35
src/typings/api/auth.d.ts
vendored
35
src/typings/api/auth.d.ts
vendored
@@ -14,8 +14,43 @@ declare namespace Api {
|
|||||||
userId: string;
|
userId: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
|
deptId?: string | null;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
buttons: string[];
|
buttons: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MyProfileDetail {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
nickname?: string | null;
|
||||||
|
deptId?: string | null;
|
||||||
|
deptName?: string | null;
|
||||||
|
positionId?: string | null;
|
||||||
|
positionName?: string | null;
|
||||||
|
company?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
mobile?: string | null;
|
||||||
|
sex?: Api.SystemManage.UserGender | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
roles: Api.SystemManage.RoleSimple[];
|
||||||
|
dept?: Api.SystemManage.DeptSimple | null;
|
||||||
|
position?: Api.SystemManage.PostSimple | null;
|
||||||
|
loginIp?: string | null;
|
||||||
|
loginDate?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateMyProfileParams {
|
||||||
|
nickname?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
mobile?: string | null;
|
||||||
|
sex?: Api.SystemManage.UserGender | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateMyPasswordParams {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/typings/api/common.d.ts
vendored
16
src/typings/api/common.d.ts
vendored
@@ -31,6 +31,22 @@ declare namespace Api {
|
|||||||
*/
|
*/
|
||||||
type EnableStatus = '1' | '2';
|
type EnableStatus = '1' | '2';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表项「当前登录用户在该对象的角色」(产品 / 项目列表共用)。
|
||||||
|
*
|
||||||
|
* 后端只读计算字段,随登录身份变化:同一份列表不同账号看到的内容不同;无角色为 []。
|
||||||
|
* 提交 / 更新接口不需要回传它。
|
||||||
|
*/
|
||||||
|
interface CurrentUserRole {
|
||||||
|
/**
|
||||||
|
* 角色稳定标识(程序判断用,不随中文名变化)。
|
||||||
|
* 例:product_manager / project_manager / developer / tester / watcher / creator / implicit_observer。
|
||||||
|
*/
|
||||||
|
roleKey: string;
|
||||||
|
/** 角色中文名(直接展示) */
|
||||||
|
roleName: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** common record */
|
/** common record */
|
||||||
type CommonRecord<T = any> = {
|
type CommonRecord<T = any> = {
|
||||||
/** record id */
|
/** record id */
|
||||||
|
|||||||
12
src/typings/api/dict.d.ts
vendored
12
src/typings/api/dict.d.ts
vendored
@@ -55,6 +55,10 @@ declare namespace Api {
|
|||||||
sort: number;
|
sort: number;
|
||||||
/** status: 0 enabled, 1 disabled */
|
/** status: 0 enabled, 1 disabled */
|
||||||
status: DictStatus;
|
status: DictStatus;
|
||||||
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType,用于 colorType 落到语义色无法区分的场景 */
|
||||||
|
cssClass?: string | null;
|
||||||
/** remark */
|
/** remark */
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
/** create time */
|
/** create time */
|
||||||
@@ -73,6 +77,12 @@ declare namespace Api {
|
|||||||
dictType?: string;
|
dictType?: string;
|
||||||
/** status: 0 enabled, 1 disabled */
|
/** status: 0 enabled, 1 disabled */
|
||||||
status?: DictStatus;
|
status?: DictStatus;
|
||||||
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType */
|
||||||
|
cssClass?: string | null;
|
||||||
|
/** 备注,可用于下拉中文释义展示 */
|
||||||
|
remark?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** frontend runtime dict cache map */
|
/** frontend runtime dict cache map */
|
||||||
@@ -82,7 +92,7 @@ declare namespace Api {
|
|||||||
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
||||||
|
|
||||||
/** dict data save params */
|
/** dict data save params */
|
||||||
type SaveDictDataParams = Pick<DictData, 'label' | 'value' | 'dictType' | 'sort' | 'status'> & {
|
type SaveDictDataParams = Pick<DictData, 'label' | 'value' | 'dictType' | 'sort' | 'status' | 'colorType'> & {
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/typings/api/feedback.d.ts
vendored
Normal file
82
src/typings/api/feedback.d.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Feedback
|
||||||
|
*
|
||||||
|
* backend api module: "feedback"(用户意见反馈)
|
||||||
|
*/
|
||||||
|
namespace Feedback {
|
||||||
|
/** 反馈分页查询参数(GET query;type/status 传字符串即可,后端按 Integer 解析) */
|
||||||
|
interface FeedbackSearchParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
/** 反馈分类,字典 feedback_type */
|
||||||
|
type?: string | number | null;
|
||||||
|
/** 处理状态,字典 feedback_status */
|
||||||
|
status?: string | number | null;
|
||||||
|
/** 标题关键词,模糊匹配 */
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反馈记录。
|
||||||
|
* - id/creator 在 API 适配层已统一为 string;
|
||||||
|
* - content 为富文本 HTML 字符串(详细描述支持图文);
|
||||||
|
* - attachments 由后端 attachmentUrls(JSON 字符串) 解析为完整附件对象数组,与任务附件同构。
|
||||||
|
*/
|
||||||
|
interface FeedbackItem {
|
||||||
|
id: string;
|
||||||
|
/** 反馈分类,字典 feedback_type */
|
||||||
|
type: number;
|
||||||
|
title: string;
|
||||||
|
/** 详细描述,富文本 HTML */
|
||||||
|
content: string;
|
||||||
|
/** 内容纯文本预览(API 适配层预算,列表列直接取,免每次渲染重跑去标签正则) */
|
||||||
|
contentPreview: string;
|
||||||
|
/** 附件对象列表(含 fileId/url/name 等,支持下载与会话级清理) */
|
||||||
|
attachments: Api.Project.AttachmentItem[];
|
||||||
|
/** 联系方式(选填) */
|
||||||
|
contact?: string;
|
||||||
|
/** 处理状态,字典 feedback_status */
|
||||||
|
status: number;
|
||||||
|
/** 提交人用户 id(字符串) */
|
||||||
|
creator: string;
|
||||||
|
/** 提交人姓名(后端回填,缺失时前端回退展示 creator) */
|
||||||
|
creatorName?: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交反馈参数(页面层传 attachments 对象数组,api 层 stringify 成 attachmentUrls) */
|
||||||
|
interface FeedbackSubmitParams {
|
||||||
|
type: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
attachments: Api.Project.AttachmentItem[];
|
||||||
|
contact?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新反馈参数(编辑态;后端更新接口待提供,见根目录后端诉求文档) */
|
||||||
|
interface FeedbackUpdateParams extends FeedbackSubmitParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页返回(后端 { list, total } 形态) */
|
||||||
|
interface FeedbackListResult {
|
||||||
|
total: number;
|
||||||
|
list: FeedbackItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反馈统计聚合(左侧分面面板用;全量口径,不随筛选变化)。
|
||||||
|
* typeCounts / statusCounts 已在 API 适配层归一化为 Record<码值字符串, 数量>,
|
||||||
|
* 由后端覆盖各自字典的全部码值(无数据为 0)。
|
||||||
|
*/
|
||||||
|
interface FeedbackStat {
|
||||||
|
/** 全部反馈总数 */
|
||||||
|
total: number;
|
||||||
|
/** 各分类计数:key=分类码值字符串(feedback_type) */
|
||||||
|
typeCounts: Record<string, number>;
|
||||||
|
/** 各状态计数:key=状态码值字符串(feedback_status) */
|
||||||
|
statusCounts: Record<string, number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/typings/api/infra.d.ts
vendored
Normal file
101
src/typings/api/infra.d.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Infra
|
||||||
|
*
|
||||||
|
* backend api module: "project/status/*"
|
||||||
|
*/
|
||||||
|
namespace Infra {
|
||||||
|
type CommonStatus = 0 | 1;
|
||||||
|
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T = any> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ObjectStatusModel {
|
||||||
|
id: string;
|
||||||
|
objectType: string;
|
||||||
|
statusCode: string;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
status: CommonStatus;
|
||||||
|
initialFlag: boolean;
|
||||||
|
terminalFlag: boolean;
|
||||||
|
allowEdit: boolean;
|
||||||
|
progressExcludedFlag: boolean;
|
||||||
|
allowCreateProject: boolean;
|
||||||
|
allowCreateRequirement: boolean;
|
||||||
|
remark?: string | null;
|
||||||
|
creator?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
updater?: string | null;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectStatusModelSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
|
Pick<ObjectStatusModel, 'objectType' | 'status' | 'initialFlag' | 'terminalFlag'> & {
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
type SaveObjectStatusModelParams = Pick<
|
||||||
|
ObjectStatusModel,
|
||||||
|
| 'objectType'
|
||||||
|
| 'statusCode'
|
||||||
|
| 'statusName'
|
||||||
|
| 'sort'
|
||||||
|
| 'status'
|
||||||
|
| 'initialFlag'
|
||||||
|
| 'terminalFlag'
|
||||||
|
| 'allowEdit'
|
||||||
|
| 'progressExcludedFlag'
|
||||||
|
| 'allowCreateProject'
|
||||||
|
| 'allowCreateRequirement'
|
||||||
|
> & {
|
||||||
|
remark?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectStatusModelList = PageResult<ObjectStatusModel>;
|
||||||
|
|
||||||
|
interface ObjectStatusTransition {
|
||||||
|
id: string;
|
||||||
|
objectType: string;
|
||||||
|
actionCode: string;
|
||||||
|
actionName: string;
|
||||||
|
fromStatusCode: string;
|
||||||
|
fromStatusName?: string | null;
|
||||||
|
toStatusCode: string;
|
||||||
|
toStatusName?: string | null;
|
||||||
|
needReason: boolean;
|
||||||
|
status: CommonStatus;
|
||||||
|
remark?: string | null;
|
||||||
|
creator?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
updater?: string | null;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectStatusTransitionSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
|
Pick<
|
||||||
|
ObjectStatusTransition,
|
||||||
|
'objectType' | 'fromStatusCode' | 'toStatusCode' | 'status' | 'actionCode' | 'actionName'
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type SaveObjectStatusTransitionParams = Pick<
|
||||||
|
ObjectStatusTransition,
|
||||||
|
'objectType' | 'actionCode' | 'actionName' | 'fromStatusCode' | 'toStatusCode' | 'needReason' | 'status'
|
||||||
|
> & {
|
||||||
|
remark?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectStatusTransitionList = PageResult<ObjectStatusTransition>;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/typings/api/notice.d.ts
vendored
Normal file
24
src/typings/api/notice.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Notice
|
||||||
|
*
|
||||||
|
* backend api module: "notice"(通知公告)
|
||||||
|
*/
|
||||||
|
namespace Notice {
|
||||||
|
/** 公告(ID 在 API 适配层已统一为 string) */
|
||||||
|
interface Notice {
|
||||||
|
/** 公告编号 */
|
||||||
|
id: string;
|
||||||
|
/** 公告标题 */
|
||||||
|
title: string;
|
||||||
|
/** 公告类型,字典 system_notice_type */
|
||||||
|
type: number;
|
||||||
|
/** 公告内容(富文本 / 纯文本,由录入决定) */
|
||||||
|
content: string;
|
||||||
|
/** 状态:0 开启 / 1 关闭 */
|
||||||
|
status: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: string | number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/typings/api/notify-message.d.ts
vendored
Normal file
46
src/typings/api/notify-message.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace NotifyMessage
|
||||||
|
*
|
||||||
|
* backend api module: "notify-message"(站内信 · 我的收件箱)
|
||||||
|
*/
|
||||||
|
namespace NotifyMessage {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T = any> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 站内信(铃铛 / 收件箱展示用;ID 在 API 适配层已统一为 string) */
|
||||||
|
interface NotifyMessage {
|
||||||
|
/** 站内信编号(雪花 Long,按 string 接收) */
|
||||||
|
id: string;
|
||||||
|
/** 发送人名称(模板配置的发件人显示名) */
|
||||||
|
templateNickname: string;
|
||||||
|
/** 最终消息正文(占位符已渲染,直接展示) */
|
||||||
|
templateContent: string;
|
||||||
|
/** 消息类型,字典 system_notify_template_type */
|
||||||
|
templateType: number;
|
||||||
|
/** 消息等级(字典 notify_message_level,1=普通 2=提醒 3=警告 4=严重,数字越大越紧急);老消息缺省为普通(1) */
|
||||||
|
level: number;
|
||||||
|
/** 是否已读 */
|
||||||
|
readStatus: boolean;
|
||||||
|
/** 阅读时间;未读为 null */
|
||||||
|
readTime: string | number | null;
|
||||||
|
/** 收到时间 */
|
||||||
|
createTime: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的站内信分页查询参数 */
|
||||||
|
interface MyPageParams extends PageParams {
|
||||||
|
/** true 只看已读 / false 只看未读 / 不传 = 全部 */
|
||||||
|
readStatus?: boolean;
|
||||||
|
/** 关键字,后端对消息正文模糊匹配;不传或空串 = 不过滤 */
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/typings/api/overtime-application.d.ts
vendored
Normal file
114
src/typings/api/overtime-application.d.ts
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
namespace OvertimeApplication {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected';
|
||||||
|
|
||||||
|
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject';
|
||||||
|
|
||||||
|
interface OvertimeApplication {
|
||||||
|
id: string;
|
||||||
|
applicantId: string;
|
||||||
|
applicantName: string;
|
||||||
|
overtimeDate: string;
|
||||||
|
overtimeDuration: string;
|
||||||
|
overtimeReason: string;
|
||||||
|
overtimeContent: string;
|
||||||
|
approverId: string;
|
||||||
|
approverName: string;
|
||||||
|
statusCode: OvertimeApplicationStatusCode;
|
||||||
|
statusName: string;
|
||||||
|
allowEdit: boolean;
|
||||||
|
terminal: boolean;
|
||||||
|
approvalComment?: string | null;
|
||||||
|
submitTime: string;
|
||||||
|
approvalTime?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OvertimeApplicationSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
applicantIds: string[] | null;
|
||||||
|
keyword: string;
|
||||||
|
applicantName: string;
|
||||||
|
approverId: string;
|
||||||
|
approverName: string;
|
||||||
|
statusCode: OvertimeApplicationStatusCode;
|
||||||
|
overtimeDate: string[];
|
||||||
|
createTime: string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface OvertimeApplicationPageResult {
|
||||||
|
total: number;
|
||||||
|
list: OvertimeApplication[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SaveOvertimeApplicationParams {
|
||||||
|
overtimeDate: string;
|
||||||
|
overtimeDuration: string;
|
||||||
|
overtimeReason: string;
|
||||||
|
overtimeContent: string;
|
||||||
|
approverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusActionParams {
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationBatchActionParams {
|
||||||
|
ids: string[];
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationBatchFailItem {
|
||||||
|
id: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationBatchActionResult {
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
failItems: OvertimeApplicationBatchFailItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationApprovalRecord {
|
||||||
|
id: string;
|
||||||
|
overtimeApplicationId: string;
|
||||||
|
statusLogId: string;
|
||||||
|
approvalRound: number;
|
||||||
|
conclusion: string;
|
||||||
|
opinion?: string | null;
|
||||||
|
auditorUserId: string;
|
||||||
|
auditorName: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationStatusDict {
|
||||||
|
statusCode: string;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
initialFlag: boolean;
|
||||||
|
terminalFlag: boolean;
|
||||||
|
allowEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamOvertimeSummaryParams {
|
||||||
|
overtimeDateStart?: string | null;
|
||||||
|
overtimeDateEnd?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamOvertimeSummary {
|
||||||
|
overtimeDateStart: string;
|
||||||
|
overtimeDateEnd: string;
|
||||||
|
totalApplicationCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
approvedCount: number;
|
||||||
|
rejectedCount: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
231
src/typings/api/performance.d.ts
vendored
Normal file
231
src/typings/api/performance.d.ts
vendored
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
namespace Performance {
|
||||||
|
namespace Common {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type SheetStatusCode = 'draft' | 'sent' | 'confirmed' | 'rejected' | string;
|
||||||
|
type SheetActionCode = 'send' | 'resend' | 'confirm' | 'reject' | string;
|
||||||
|
type RemindType = 'pending_confirm' | 'pending_send';
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Template {
|
||||||
|
interface ScoreCellMapping {
|
||||||
|
actualScoreTotalCell?: string | null;
|
||||||
|
baseScoreTotalCell?: string | null;
|
||||||
|
extraScoreTotalCell?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Template {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
versionNo: number;
|
||||||
|
activeFlag: boolean;
|
||||||
|
uploadUserId: string;
|
||||||
|
uploadUserName: string;
|
||||||
|
uploadTime: string;
|
||||||
|
remark?: string | null;
|
||||||
|
scoreCellMapping?: ScoreCellMapping | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
templateName: string;
|
||||||
|
activeFlag: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface UploadParams {
|
||||||
|
templateName: string;
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
activeFlag?: boolean | null;
|
||||||
|
remark?: string | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Sheet {
|
||||||
|
interface Sheet {
|
||||||
|
id: string;
|
||||||
|
periodMonth: string;
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
employeeDeptId: string;
|
||||||
|
employeeDeptName: string;
|
||||||
|
deptOrgType: string;
|
||||||
|
managerId: string;
|
||||||
|
managerName: string;
|
||||||
|
templateId: string;
|
||||||
|
fileId?: string | null;
|
||||||
|
fileName?: string | null;
|
||||||
|
fileVersion: number;
|
||||||
|
statusCode: Common.SheetStatusCode;
|
||||||
|
statusName: string;
|
||||||
|
actualScoreTotal?: string | number | null;
|
||||||
|
baseScoreTotal?: string | number | null;
|
||||||
|
extraScoreTotal?: string | number | null;
|
||||||
|
sentTime?: string | null;
|
||||||
|
confirmedTime?: string | null;
|
||||||
|
rejectedTime?: string | null;
|
||||||
|
lastStatusReason?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
employeeIds: string[];
|
||||||
|
periodMonthRange: string[];
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
employeeDeptId: string;
|
||||||
|
employeeDeptName: string;
|
||||||
|
managerId: string;
|
||||||
|
managerName: string;
|
||||||
|
statusCode: Common.SheetStatusCode;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface CreateParams {
|
||||||
|
periodMonth: string;
|
||||||
|
employeeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExcelUpdateParams {
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
fileVersion: number;
|
||||||
|
actualScoreTotal: string | number;
|
||||||
|
baseScoreTotal: string | number;
|
||||||
|
extraScoreTotal: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusActionParams {
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDownloadParams {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusDict {
|
||||||
|
statusCode: Common.SheetStatusCode;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
initialFlag: boolean;
|
||||||
|
terminalFlag: boolean;
|
||||||
|
allowEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusTransition {
|
||||||
|
actionCode: Common.SheetActionCode;
|
||||||
|
actionName: string;
|
||||||
|
fromStatusCode: Common.SheetStatusCode;
|
||||||
|
toStatusCode: Common.SheetStatusCode;
|
||||||
|
needReason: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusLog {
|
||||||
|
id: string;
|
||||||
|
sheetId: string;
|
||||||
|
actionType: Common.SheetActionCode;
|
||||||
|
fromStatus?: Common.SheetStatusCode | null;
|
||||||
|
toStatus?: Common.SheetStatusCode | null;
|
||||||
|
reason?: string | null;
|
||||||
|
operatorUserId: string;
|
||||||
|
operatorName: string;
|
||||||
|
periodMonthSnapshot: string;
|
||||||
|
employeeNameSnapshot: string;
|
||||||
|
remark?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseRecord {
|
||||||
|
id: string;
|
||||||
|
sheetId: string;
|
||||||
|
statusLogId: string;
|
||||||
|
roundNo: number;
|
||||||
|
actionType: Common.SheetActionCode;
|
||||||
|
fromStatus: Common.SheetStatusCode;
|
||||||
|
toStatus: Common.SheetStatusCode;
|
||||||
|
opinion?: string | null;
|
||||||
|
responderUserId: string;
|
||||||
|
responderName: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonthlyResult {
|
||||||
|
sheetId?: string | null;
|
||||||
|
periodMonth: string;
|
||||||
|
employeeId: string;
|
||||||
|
actualScoreTotal?: string | number | null;
|
||||||
|
baseScoreTotal?: string | number | null;
|
||||||
|
extraScoreTotal?: string | number | null;
|
||||||
|
statusCode?: Common.SheetStatusCode | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Team {
|
||||||
|
interface SummaryParams {
|
||||||
|
periodMonthStart?: string | null;
|
||||||
|
periodMonthEnd?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingSendUser {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
managerUserId: string;
|
||||||
|
managerName: string;
|
||||||
|
sheetId?: string | null;
|
||||||
|
statusCode?: Common.SheetStatusCode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingConfirmUser {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
sheetId: string;
|
||||||
|
sentTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeptOrgAverage {
|
||||||
|
deptId: string;
|
||||||
|
deptName: string;
|
||||||
|
deptOrgType: string;
|
||||||
|
averageScore?: string | number | null;
|
||||||
|
confirmedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Summary {
|
||||||
|
periodMonthStart: string;
|
||||||
|
periodMonthEnd: string;
|
||||||
|
totalSheetCount: number;
|
||||||
|
pendingSendCount: number;
|
||||||
|
pendingConfirmCount: number;
|
||||||
|
confirmedRate: string | number;
|
||||||
|
pendingSendUsers: PendingSendUser[];
|
||||||
|
pendingConfirmUsers: PendingConfirmUser[];
|
||||||
|
deptOrgAverages: DeptOrgAverage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemindParams {
|
||||||
|
periodMonthStart?: string | null;
|
||||||
|
periodMonthEnd?: string | null;
|
||||||
|
remindType: Common.RemindType;
|
||||||
|
userIds?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemindResult {
|
||||||
|
remindedCount: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/typings/api/personal-item.d.ts
vendored
Normal file
99
src/typings/api/personal-item.d.ts
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
namespace PersonalItem {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalItemStatusCode = 'pending' | 'active' | 'paused' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
|
interface PersonalItemLifecycleAction {
|
||||||
|
actionCode: string;
|
||||||
|
actionName: string;
|
||||||
|
needReason: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonalItem {
|
||||||
|
id: string;
|
||||||
|
taskTitle: string;
|
||||||
|
type: string;
|
||||||
|
ownerId: string;
|
||||||
|
statusCode: PersonalItemStatusCode;
|
||||||
|
terminal?: boolean;
|
||||||
|
allowEdit?: boolean;
|
||||||
|
availableActions?: PersonalItemLifecycleAction[] | null;
|
||||||
|
progressRate: number;
|
||||||
|
totalSpentHours?: number | null;
|
||||||
|
plannedStartDate: string | null;
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
actualStartDate: string | null;
|
||||||
|
actualEndDate: string | null;
|
||||||
|
taskDesc: string | null;
|
||||||
|
lastStatusReason: string | null;
|
||||||
|
attachments: Api.Project.AttachmentItem[] | null;
|
||||||
|
creator: string;
|
||||||
|
createTime: string;
|
||||||
|
updater: string;
|
||||||
|
updateTime: string;
|
||||||
|
deleted: boolean;
|
||||||
|
ownerName?: string | null;
|
||||||
|
ownerNickname?: string | null;
|
||||||
|
statusName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalItemSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
keyword: string;
|
||||||
|
ownerId: string;
|
||||||
|
statusCode: PersonalItemStatusCode;
|
||||||
|
updateTime: string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface PersonalItemPageResult {
|
||||||
|
total: number;
|
||||||
|
list: PersonalItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SavePersonalItemParams {
|
||||||
|
taskTitle: string;
|
||||||
|
type: string;
|
||||||
|
ownerId?: string;
|
||||||
|
executionId?: string | null;
|
||||||
|
progressRate?: number | null;
|
||||||
|
plannedStartDate: string | null;
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
taskDesc: string | null;
|
||||||
|
attachments: Api.Project.AttachmentItem[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdatePersonalItemParams extends SavePersonalItemParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangePersonalItemStatusParams {
|
||||||
|
actionCode: string;
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonalItemExecutionOption {
|
||||||
|
executionId: string;
|
||||||
|
executionName: string;
|
||||||
|
projectId?: string | null;
|
||||||
|
projectName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeletePersonalItemParams {
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BindPersonalItemExecutionParams {
|
||||||
|
ids: string[];
|
||||||
|
executionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalItemWorklog = Api.Project.TaskWorklog;
|
||||||
|
type PersonalItemWorklogSearchParams = Api.Project.TaskWorklogSearchParams;
|
||||||
|
type SavePersonalItemWorklogParams = Api.Project.SaveTaskWorklogParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
197
src/typings/api/product.d.ts
vendored
197
src/typings/api/product.d.ts
vendored
@@ -21,10 +21,27 @@ declare namespace Api {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 入口页概览统计状态看板项(状态机全部启用状态,按 sort 升序,计数为 0 也返回;与项目域契约同构) */
|
||||||
|
interface OverviewStatusItem {
|
||||||
|
statusCode: string;
|
||||||
|
/** 状态展示名(状态机配置中文名,前端直接渲染,不做本地名称映射) */
|
||||||
|
statusName: string;
|
||||||
|
count: number;
|
||||||
|
sort: number;
|
||||||
|
/** 是否终态(状态机 terminal_flag) */
|
||||||
|
terminal: boolean;
|
||||||
|
/** 是否计入"全部";当前口径无排除项恒为 true(产品列表暂无"全部"视图,按同构契约返回) */
|
||||||
|
includeInAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 产品入口页概览统计 */
|
/** 产品入口页概览统计 */
|
||||||
interface ProductOverviewSummary {
|
interface ProductOverviewSummary {
|
||||||
/** 产品状态数量映射,key 为后端状态编码 */
|
/** 产品状态数量映射,key 为后端状态编码(过渡兼容字段,前端迁移完成后由后端删除) */
|
||||||
statusCounts: Record<string, number>;
|
statusCounts: Record<string, number>;
|
||||||
|
/** "全部"口径总数 = items 各状态 count 之和 */
|
||||||
|
total: number;
|
||||||
|
/** 状态看板项,覆盖状态机全部启用状态,按 sort 升序 */
|
||||||
|
items: OverviewStatusItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
@@ -50,6 +67,8 @@ declare namespace Api {
|
|||||||
createTime: string;
|
createTime: string;
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
|
/** 当前登录用户在该产品的角色(后端只读计算字段,随登录身份变化;无角色为 []) */
|
||||||
|
currentUserRoles: Api.Common.CurrentUserRole[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductSettingBaseInfo {
|
interface ProductSettingBaseInfo {
|
||||||
@@ -73,6 +92,17 @@ declare namespace Api {
|
|||||||
lastStatusReason?: string | null;
|
lastStatusReason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProductOption {
|
||||||
|
/** 产品 ID */
|
||||||
|
id: string;
|
||||||
|
/** 产品编码 */
|
||||||
|
code: string;
|
||||||
|
/** 产品名称 */
|
||||||
|
name: string;
|
||||||
|
/** 产品方向字典值 */
|
||||||
|
directionCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ProductLifecycleAction {
|
interface ProductLifecycleAction {
|
||||||
actionCode: ProductStatusActionCode;
|
actionCode: ProductStatusActionCode;
|
||||||
actionName: string;
|
actionName: string;
|
||||||
@@ -172,8 +202,10 @@ declare namespace Api {
|
|||||||
|
|
||||||
type ProductSearchParams = CommonType.RecordNullable<
|
type ProductSearchParams = CommonType.RecordNullable<
|
||||||
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
Pick<Product, 'directionCode' | 'managerUserId' | 'statusCode'> & {
|
Pick<Product, 'directionCode' | 'managerUserId'> & {
|
||||||
keyword: string;
|
keyword: string;
|
||||||
|
/** 状态编码来自状态机(overview-summary items 动态下发),不再用前端字面量联合约束 */
|
||||||
|
statusCode: string;
|
||||||
updateTime: string[];
|
updateTime: string[];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
@@ -195,6 +227,7 @@ declare namespace Api {
|
|||||||
interface DeleteProductParams {
|
interface DeleteProductParams {
|
||||||
id: string;
|
id: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
|
confirmText: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +243,20 @@ declare namespace Api {
|
|||||||
previousManagerRoleId?: string | null;
|
previousManagerRoleId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增产品成员参数
|
||||||
|
*
|
||||||
|
* 刻意不复用 CreateProductMemberParams:批量接口不承担「产品经理交接」语义,
|
||||||
|
* 后端兜底拒绝 roleId 为产品经理角色的项。
|
||||||
|
*/
|
||||||
|
interface BatchCreateProductMembersParams {
|
||||||
|
members: Array<{
|
||||||
|
userId: string;
|
||||||
|
roleId: string;
|
||||||
|
remark?: string | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 产品创建(含初始团队)原子接口参数
|
* 产品创建(含初始团队)原子接口参数
|
||||||
*
|
*
|
||||||
@@ -218,6 +265,8 @@ declare namespace Api {
|
|||||||
interface CreateProductWithTeamParams {
|
interface CreateProductWithTeamParams {
|
||||||
product: SaveProductParams;
|
product: SaveProductParams;
|
||||||
members: CreateProductMemberParams[];
|
members: CreateProductMemberParams[];
|
||||||
|
/** 关注人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||||
|
watcherUserIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProductMemberParams {
|
interface UpdateProductMemberParams {
|
||||||
@@ -232,18 +281,37 @@ declare namespace Api {
|
|||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BatchInactiveProductMembersParams {
|
||||||
|
memberIds: string[];
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 产品需求相关类型定义 ==========
|
// ========== 产品需求相关类型定义 ==========
|
||||||
/** 需求状态编码 */
|
/** 需求状态编码 */
|
||||||
type RequirementStatusCode =
|
type RequirementStatusCode =
|
||||||
| 'pending_confirm'
|
| 'pending_claim'
|
||||||
| 'pending_review'
|
| 'pending_review'
|
||||||
| 'pending_dispatch'
|
| 'pending_dispatch'
|
||||||
|
| 'reviewed'
|
||||||
|
| 'review_rejected'
|
||||||
| 'implementing'
|
| 'implementing'
|
||||||
| 'accepted'
|
| 'accepted'
|
||||||
| 'closed'
|
| 'closed'
|
||||||
| 'rejected'
|
| 'rejected'
|
||||||
| 'cancelled';
|
| 'cancelled';
|
||||||
|
|
||||||
|
/** 需求状态动作编码 */
|
||||||
|
type RequirementStatusActionCode =
|
||||||
|
| 'claim_to_review'
|
||||||
|
| 'claim_to_dispatch'
|
||||||
|
| 'pass_review'
|
||||||
|
| 'reject_review'
|
||||||
|
| 'dispatch'
|
||||||
|
| 'cancel'
|
||||||
|
| 'accept'
|
||||||
|
| 'close'
|
||||||
|
| 'reject';
|
||||||
|
|
||||||
/** 需求来源类型 */
|
/** 需求来源类型 */
|
||||||
type RequirementSourceType = 'manual' | 'work_order';
|
type RequirementSourceType = 'manual' | 'work_order';
|
||||||
|
|
||||||
@@ -270,14 +338,16 @@ declare namespace Api {
|
|||||||
title: string;
|
title: string;
|
||||||
/** 需求内容(富文本) */
|
/** 需求内容(富文本) */
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
/** 附件列表 */
|
||||||
|
attachments?: Api.Project.AttachmentItem[] | null;
|
||||||
/** 需求类型字典值 */
|
/** 需求类型字典值 */
|
||||||
category: string;
|
category: string;
|
||||||
/** 需求类型名称 */
|
/** 需求类型名称 */
|
||||||
categoryName?: string | null;
|
categoryName?: string | null;
|
||||||
/** 需求来源类型 */
|
/** 需求来源类型 */
|
||||||
sourceType: RequirementSourceType;
|
sourceType: RequirementSourceType;
|
||||||
/** 需求来源业务ID */
|
/** 来源业务编号 */
|
||||||
sourceBizId?: string | null;
|
sourceBizCode?: string | null;
|
||||||
/** 优先级(0低 1中 2高 3紧急) */
|
/** 优先级(0低 1中 2高 3紧急) */
|
||||||
priority: RequirementPriority;
|
priority: RequirementPriority;
|
||||||
/** 优先级名称 */
|
/** 优先级名称 */
|
||||||
@@ -296,12 +366,12 @@ declare namespace Api {
|
|||||||
currentHandlerUserId?: string | null;
|
currentHandlerUserId?: string | null;
|
||||||
/** 当前处理人姓名 */
|
/** 当前处理人姓名 */
|
||||||
currentHandlerUserNickname?: string | null;
|
currentHandlerUserNickname?: string | null;
|
||||||
/** 默认实现项目编号 */
|
/** 默认关联项目编号 */
|
||||||
implementProjectId?: string | null;
|
implementProjectId?: string | null;
|
||||||
/** 默认实现项目名称 */
|
/** 默认关联项目名称 */
|
||||||
implementProjectName?: string | null;
|
implementProjectName?: string | null;
|
||||||
/** 所需工时(小时) */
|
/** 预期完成日期 */
|
||||||
workHours: number;
|
expectedTime?: string | null;
|
||||||
/** 排序值 */
|
/** 排序值 */
|
||||||
sort: number;
|
sort: number;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -310,8 +380,6 @@ declare namespace Api {
|
|||||||
updateTime: string;
|
updateTime: string;
|
||||||
/** 子需求列表(树形结构) */
|
/** 子需求列表(树形结构) */
|
||||||
children?: Requirement[];
|
children?: Requirement[];
|
||||||
/** 是否为终态 */
|
|
||||||
terminal?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 需求模块实体 ==========
|
// ========== 需求模块实体 ==========
|
||||||
@@ -348,25 +416,103 @@ declare namespace Api {
|
|||||||
initialFlag: boolean;
|
initialFlag: boolean;
|
||||||
/** 是否终态 */
|
/** 是否终态 */
|
||||||
terminalFlag: boolean;
|
terminalFlag: boolean;
|
||||||
|
/** 是否允许编辑 */
|
||||||
|
allowEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 需求生命周期 ==========
|
|
||||||
|
|
||||||
interface RequirementLifecycleAction {
|
interface RequirementLifecycleAction {
|
||||||
actionCode: string;
|
actionCode: RequirementStatusActionCode;
|
||||||
actionName: string;
|
actionName: string;
|
||||||
toStatusCode: string;
|
toStatusCode: string;
|
||||||
toStatusName: string;
|
toStatusName: string;
|
||||||
needReason: boolean;
|
needReason: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequirementLifecycleInfo {
|
interface RequirementBatchReqVO {
|
||||||
statusCode: RequirementStatusCode;
|
productId: string;
|
||||||
statusName?: string | null;
|
requirementIds: string[];
|
||||||
lastStatusReason?: string | null;
|
}
|
||||||
terminal: boolean;
|
|
||||||
allowEdit: boolean;
|
interface RequirementAllowedTransitionBatchRespVO {
|
||||||
availableActions: RequirementLifecycleAction[];
|
requirementId: string;
|
||||||
|
transitions: RequirementLifecycleAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequirementHasDispatchedBatchRespVO {
|
||||||
|
requirementId: string;
|
||||||
|
hasDispatched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductRequirementDashboardRecentChangeActionType = 'create' | 'delete' | 'status_terminal';
|
||||||
|
|
||||||
|
interface ProductRequirementDashboardSummary {
|
||||||
|
/** 当前产品下所有未删除需求数,包括根需求和子需求 */
|
||||||
|
total: number;
|
||||||
|
/** 待认领、待评审、待指派的需求数 */
|
||||||
|
todo: number;
|
||||||
|
/** 待认领需求数 */
|
||||||
|
pendingClaim: number;
|
||||||
|
/** 待评审需求数 */
|
||||||
|
pendingReview: number;
|
||||||
|
/** 待指派需求数 */
|
||||||
|
pendingDispatch: number;
|
||||||
|
/** 已验收或已关闭需求数 */
|
||||||
|
completed: number;
|
||||||
|
/** 完成率,0-100 */
|
||||||
|
completionRate: number;
|
||||||
|
/** P0/P1 且待处理的需求数 */
|
||||||
|
highPriorityTodo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductRequirementDashboardRecentChange {
|
||||||
|
id: string;
|
||||||
|
requirementId?: string | null;
|
||||||
|
title: string;
|
||||||
|
actionType: ProductRequirementDashboardRecentChangeActionType;
|
||||||
|
actionLabel: string;
|
||||||
|
content: string;
|
||||||
|
occurredAt: string;
|
||||||
|
operatorUserId?: string | null;
|
||||||
|
operatorName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductRequirementDashboard {
|
||||||
|
summary: ProductRequirementDashboardSummary;
|
||||||
|
recentChanges: ProductRequirementDashboardRecentChange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequirementReviewConclusion = 0 | 1;
|
||||||
|
|
||||||
|
interface RequirementReviewAttendeeItem {
|
||||||
|
userId: string;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequirementReview {
|
||||||
|
id: string;
|
||||||
|
objectType: 'product_requirement';
|
||||||
|
requirementId: string;
|
||||||
|
operatorId: string;
|
||||||
|
conclusion: RequirementReviewConclusion;
|
||||||
|
reviewContent?: string | null;
|
||||||
|
requirementEstimatedHours?: number | string | null;
|
||||||
|
attendees?: RequirementReviewAttendeeItem[];
|
||||||
|
attachments?: Api.Project.AttachmentItem[] | null;
|
||||||
|
reviewTime?: string | null;
|
||||||
|
createTime?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequirementReviewSubmitParams {
|
||||||
|
productId: string;
|
||||||
|
requirementId: string;
|
||||||
|
operatorId: string;
|
||||||
|
conclusion: RequirementReviewConclusion;
|
||||||
|
reviewContent?: string | null;
|
||||||
|
requirementEstimatedHours?: number | string | null;
|
||||||
|
attendees?: RequirementReviewAttendeeItem[];
|
||||||
|
attachments?: Api.Project.AttachmentItem[] | null;
|
||||||
|
reviewTime?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 请求参数类型 ==========
|
// ========== 请求参数类型 ==========
|
||||||
@@ -376,7 +522,7 @@ declare namespace Api {
|
|||||||
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
Pick<
|
Pick<
|
||||||
Requirement,
|
Requirement,
|
||||||
'moduleId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceType'
|
'moduleId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceBizCode'
|
||||||
> & {
|
> & {
|
||||||
productId: string;
|
productId: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -391,14 +537,16 @@ declare namespace Api {
|
|||||||
| 'reviewRequired'
|
| 'reviewRequired'
|
||||||
| 'title'
|
| 'title'
|
||||||
| 'description'
|
| 'description'
|
||||||
|
| 'attachments'
|
||||||
| 'category'
|
| 'category'
|
||||||
| 'priority'
|
| 'priority'
|
||||||
|
| 'sourceBizCode'
|
||||||
| 'proposerId'
|
| 'proposerId'
|
||||||
| 'proposerNickname'
|
| 'proposerNickname'
|
||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'implementProjectId'
|
| 'implementProjectId'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -430,13 +578,14 @@ declare namespace Api {
|
|||||||
| 'reviewRequired'
|
| 'reviewRequired'
|
||||||
| 'title'
|
| 'title'
|
||||||
| 'description'
|
| 'description'
|
||||||
|
| 'attachments'
|
||||||
| 'category'
|
| 'category'
|
||||||
| 'priority'
|
| 'priority'
|
||||||
| 'proposerId'
|
| 'proposerId'
|
||||||
| 'proposerNickname'
|
| 'proposerNickname'
|
||||||
| 'currentHandlerUserId'
|
| 'currentHandlerUserId'
|
||||||
| 'currentHandlerUserNickname'
|
| 'currentHandlerUserNickname'
|
||||||
| 'workHours'
|
| 'expectedTime'
|
||||||
| 'sort'
|
| 'sort'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user