diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9dcd653 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "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)" + ] + } +} diff --git a/10-产品动态时间线_前端API文档.md b/10-产品动态时间线_前端API文档.md deleted file mode 100644 index 834094b..0000000 --- a/10-产品动态时间线_前端API文档.md +++ /dev/null @@ -1,367 +0,0 @@ -# 10-产品动态时间线 前端 API 文档 - -## 0. 文档定位 - -本文档是给前端产品首页“产品动态展示区域”单独使用的接口文档。 - -目标: - -- 明确前端当前应该调用哪个接口 -- 明确左侧筛选项如何映射到后端参数 -- 明确接口返回字段、时间格式、边界规则 -- 避免继续混用设置页最近动态和首页正式时间线 - -说明: - -- 设置页原最近动态接口 `GET /project/product/{id}/activities` 继续保留 -- 产品首页正式动态时间线请统一使用本文档中的新接口 -- 当前首页动态时间线不包含需求池变动,需求池由独立区域承载 - ---- - -## 1. 接口概览 - -### 1.1 接口信息 - -- 接口名称:获取产品动态时间线分页 -- 请求方法:`GET` -- 请求路径:`/project/product/{id}/activities/page` -- 权限码:`project:product:query` -- 适用页面:产品首页动态时间线区域 - -### 1.2 接口用途 - -该接口用于返回产品首页动态展示区域的正式时间线数据,支持: - -- 默认最近 30 天 -- 左侧类型筛选 -- 动作多选筛选 -- 分页查询 -- 创建初始化噪音去除 - -### 1.3 当前纳入首页时间线的事件范围 - -当前只包含以下 5 类: - -- 产品创建 -- 产品状态变更 -- 产品经理变更 -- 成员加入 -- 成员移出 - -当前明确不包含: - -- 需求池变动 -- `update_member` -- 普通产品主数据编辑 `update` -- 删除产品动态 - ---- - -## 2. 请求定义 - -### 2.1 路径参数 - -| 参数名 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `integer(int64)` | 是 | 产品 ID | - -### 2.2 查询参数 - -| 参数名 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `pageNo` | `integer` | 是 | 页码,从 `1` 开始 | -| `pageSize` | `integer` | 是 | 每页条数 | -| `activityType` | `string` | 否 | 分类,可选 `status` / `product` / `member` | -| `actionTypes` | `array` | 否 | 动作编码数组,支持多选 | -| `startTime` | `string` | 否 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` | -| `endTime` | `string` | 否 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` | - -### 2.3 参数规则 - -#### 2.3.1 时间参数规则 - -- `startTime` 和 `endTime` 必须同时传,或者同时不传 -- 都不传时,后端默认查询最近 `30` 天 -- 只传一个时,后端返回参数错误 -- `startTime > endTime` 时,后端返回参数错误 - -#### 2.3.2 筛选参数规则 - -- `activityType` 是分类筛选 -- `actionTypes` 是动作细筛选 -- 两者同时传时,按交集处理 -- 如果前端未来需要做跨类型多选,可以不传 `activityType`,只传 `actionTypes` - -#### 2.3.3 `actionTypes` 传参方式 - -GET 场景请按重复参数方式传递,例如: - -```text -/project/product/1024/activities/page?pageNo=1&pageSize=10&activityType=status&actionTypes=pause&actionTypes=resume&actionTypes=archive&actionTypes=abandon -``` - ---- - -## 3. 左侧筛选映射 - -首页左侧当前 5 个筛选项,前端请按下表映射到请求参数: - -| 前端筛选项 | `activityType` | `actionTypes` | -| --- | --- | --- | -| 产品创建 | `product` | `create` | -| 产品状态变更 | `status` | `pause` / `resume` / `archive` / `abandon` | -| 产品经理变更 | `product` | `change_manager` | -| 成员加入 | `member` | `add_member` | -| 成员移出 | `member` | `remove_member` | - -补充说明: - -- 首页时间线当前不展示需求池变动 -- 需求池的展示由独立模块负责,不要通过本接口混查 - ---- - -## 4. 响应定义 - -### 4.1 响应包装 - -接口统一返回 `CommonResult>`。 - -成功响应结构: - -```json -{ - "code": 0, - "msg": "", - "data": { - "total": 0, - "list": [] - } -} -``` - -### 4.2 `data` 结构 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `total` | `integer(int64)` | 总条数 | -| `list` | `array` | 当前页数据 | - -### 4.3 单条动态结构 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `id` | `string` | 动态唯一标识,格式 `type:sourceId`,例如 `status:11` | -| `type` | `string` | 动态类型,取值 `status` / `product` / `member` | -| `actionType` | `string` | 动作编码 | -| `actionName` | `string` | 动作中文名称 | -| `operatorUserId` | `integer(int64)` | 操作人用户 ID,可为 `null` | -| `operatorName` | `string` | 操作人名称,可为空字符串 | -| `occurredAt` | `integer(int64)` | 动态发生时间,毫秒时间戳 | -| `summary` | `string` | 可直接展示的摘要文案 | -| `reason` | `string` | 原因说明,可为 `null` | -| `fromStatus` | `string` | 原状态编码,可为 `null` | -| `toStatus` | `string` | 目标状态编码,可为 `null` | -| `details` | `string` | 补充明细,当前为 JSON 字符串 | - -### 4.4 时间格式说明 - -这个接口当前有两个时间口径: - -- 请求里的 `startTime`、`endTime`:字符串,格式 `yyyy-MM-dd HH:mm:ss` -- 响应里的 `occurredAt`:毫秒时间戳 `number` - -前端需要按这个真实口径处理,不要把 `occurredAt` 当成格式化字符串读取。 - ---- - -## 5. 字段语义说明 - -### 5.1 `type` 取值说明 - -| 取值 | 说明 | 数据来源 | -| --- | --- | --- | -| `status` | 产品状态变更 | `rdms_product_status_log` | -| `product` | 产品对象动态 | `rdms_biz_audit_log` 中 `bizType=product` | -| `member` | 产品团队动态 | `rdms_biz_audit_log` 中 `bizType=rdms_user_object_role` | - -### 5.2 `actionType` 取值范围 - -当前首页时间线只会出现以下动作: - -| `type` | `actionType` | 说明 | -| --- | --- | --- | -| `product` | `create` | 产品创建 | -| `product` | `change_manager` | 产品经理变更 | -| `status` | `pause` | 暂停 | -| `status` | `resume` | 恢复 | -| `status` | `archive` | 归档 | -| `status` | `abandon` | 废弃 | -| `member` | `add_member` | 成员加入 | -| `member` | `remove_member` | 成员移出 | - -### 5.3 `details` 当前口径 - -`details` 当前不做统一结构化建模,按来源原样返回字符串: - -- `type=status` - - 返回状态日志补充信息 - - 当前包含 `productCodeSnapshot`、`productNameSnapshot` -- `type=product` - - 返回产品审计 `fieldChanges` -- `type=member` - - 返回成员审计 `fieldChanges` - -前端建议: - -- 首版先不依赖 `details` 做复杂渲染 -- 先以 `actionName + summary + operatorName + occurredAt` 跑通展示 - ---- - -## 6. 后端聚合规则 - -为了让前端看到的是“可直接展示的正式时间线”,后端已固定以下规则: - -### 6.1 创建去噪 - -产品创建时通常会伴随初始化动作: - -- 初始化 `change_manager` -- 初始化 `add_member` - -这两类初始化动作不会单独出现在首页时间线里,最终只保留一条 `create`。 - -### 6.2 状态日志优先 - -如果同一状态动作同时存在: - -- 产品审计日志 -- 状态日志 - -则首页时间线只取状态日志,不重复展示产品审计里的同类状态动作。 - -### 6.3 成员调整排除 - -`update_member` 当前不进入首页正式时间线。 - -原因: - -- 首页当前只需要展示“加入”和“移出” -- 角色调整、备注调整等细节先不进入首页主时间线 - ---- - -## 7. 请求示例 - -### 7.1 默认查询首页动态 - -```http -GET /project/product/1024/activities/page?pageNo=1&pageSize=6 -``` - -### 7.2 查询“产品状态变更” - -```http -GET /project/product/1024/activities/page?pageNo=1&pageSize=10&activityType=status&actionTypes=pause&actionTypes=resume&actionTypes=archive&actionTypes=abandon -``` - -### 7.3 查询“成员移出”并限制时间范围 - -```http -GET /project/product/1024/activities/page?pageNo=1&pageSize=10&activityType=member&actionTypes=remove_member&startTime=2026-03-24 00:00:00&endTime=2026-04-23 23:59:59 -``` - ---- - -## 8. 响应示例 - -```json -{ - "code": 0, - "msg": "", - "data": { - "total": 2, - "list": [ - { - "id": "product:22", - "type": "product", - "actionType": "change_manager", - "actionName": "切换产品经理", - "operatorUserId": 10002, - "operatorName": "李四", - "occurredAt": 1776812345000, - "summary": "李四执行了【切换产品经理】", - "reason": null, - "fromStatus": null, - "toStatus": null, - "details": "{\"managerUserId\":{\"before\":10001,\"after\":10002}}" - }, - { - "id": "status:11", - "type": "status", - "actionType": "resume", - "actionName": "恢复", - "operatorUserId": 10001, - "operatorName": "张三", - "occurredAt": 1776812984000, - "summary": "张三执行了【恢复】:可以继续开展", - "reason": "可以继续开展", - "fromStatus": "paused", - "toStatus": "active", - "details": "{\"productCodeSnapshot\":\"CNPD2026001\",\"productNameSnapshot\":\"统一交付平台\"}" - } - ] - } -} -``` - ---- - -## 9. 错误码 - -| `code` | 说明 | -| --- | --- | -| `0` | 成功 | -| `400` | 请求参数错误,例如只传了一侧时间,或开始时间晚于结束时间 | -| `401` | 未登录 | -| `403` | 没有该产品查询权限 | -| `1008001000` | 产品不存在 | - -参数错误示例: - -```json -{ - "code": 400, - "msg": "开始时间和结束时间必须同时传入", - "data": null -} -``` - ---- - -## 10. 前端接入建议 - -首页动态区域首版建议直接消费以下字段: - -- `actionName` -- `summary` -- `operatorName` -- `occurredAt` - -左侧筛选建议直接使用: - -- `activityType` -- `actionTypes` - -当前不建议首版依赖: - -- `details` 的深度结构化解析 -- 需求池事件混入本接口 -- 自行从 `actionType` 反推新的派生事件类型 - -一句话结论: - -- 设置页最近动态继续调 `/activities` -- 产品首页正式动态时间线统一调 `/activities/page` diff --git a/AGENTS.md b/AGENTS.md index a5cb44f..307b299 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ - `build/plugins/router.ts`:elegant-router 配置与路由元信息生成逻辑 - `src/hooks/common/table.ts`:列表页表格 hook 主入口 - `src/hooks/common/form.ts`:表单校验与表单实例 hook +- `src/constants/status-tag.ts`:业务对象状态颜色(ElTag type)集中配置 - `src/styles/scss/element-plus.scss`:当前项目表格、弹层、按钮、表单密度与公共壳样式标准 - `packages/*`:项目内本地共享库 - `docs/`:当前工作上下文的一部分,做架构级、权限级、页面规范级改动前优先查阅 @@ -136,17 +137,18 @@ - 页面组件保持“编排层薄”。页面文件主要负责搜索参数、表格 hook、列定义、弹层开关、接口调用编排,不把大量表单细节和重复交互直接堆在页面根组件里。 - 列表页优先拆出同目录下的 `modules/*` 子组件,例如搜索组件、操作弹层、详情抽屉、资源面板等。 - 系统管理下现有 `user`、`role`、`menu`、`dict` 页面可以作为参考实现,新增同类页面优先沿用它们的拆分方式。 -- 搜索组件优先复用 `src/components/custom/table-search-panel.vue` 作为外壳。搜索模块本身应尽量只接收 `model`,只向外发出 `reset` / `search`,不直接承载列表请求逻辑。 +- 新增或触达列表页搜索组件时,必须按 `docs/table-search-fields-usage.md` 使用 `src/components/custom/table-search-fields.vue` 的 `fields` 声明式配置,不得手写 `ElRow / ElCol / ElFormItem` 搜索区骨架;只有字段存在复杂联动、自定义插槽或 `TableSearchFields` 明确无法承载时,才允许退回 `src/components/custom/table-search-panel.vue`,并需要在实施说明中写明原因。搜索模块本身应尽量只接收 `model` 和必要选项,只向外发出 `reset` / `search`,不直接承载列表请求逻辑。 - 列表能力优先复用 `src/hooks/common/table.ts` 中的 `useUIPaginatedTable`、`useTableOperate`、`defaultTransform`。 - 表单能力优先复用 `src/hooks/common/form.ts` 中的 `useForm`、`useFormRules`。 - 当前项目的真实业务口径是“内网中文优先”。新增业务页不必为了形式强行补全国际化键;但如果是在已有大量 `$t(...)` 的页面或模块内继续开发,优先保持该局部代码风格一致,不要半页中文直写、半页国际化混用。 ## 表格、搜索区与操作列约束 -- 搜索区按钮组保持在最右侧;存在折叠项时,按钮顺序保持为“展开/收起 -> 重置 -> 查询”。 -- 不要在每个页面重新拼一套搜索区骨架,优先延续 `TableSearchPanel` 的结构和交互。 +- 搜索区按钮组必须固定在第一行最后一个位置;存在折叠项时,按钮顺序保持为“展开/收起 -> 重置 -> 查询”。这是 `TableSearchFields` 的布局契约,不允许因为查询条件不足、展开/收起或响应式样式把按钮提前到中间位置或挤到后续行。 +- 不要在每个页面重新拼一套搜索区骨架;常规查询条件必须使用 `TableSearchFields`,通过 `columns` 控制每行格子数和折叠阈值。`columns` 表示首行总格数,其中最后 1 格永远留给按钮区;字段不足 `columns - 1` 时由公共组件补空占位,字段超过时剩余字段进入展开区。类似项目管理入口页这类 4 个查询条件的场景,必须使用 `:columns="4"`,形成“3 个条件 + 按钮区”的首行布局。 - 表格操作列优先复用 `src/components/custom/business-table-action-cell.tsx`。 - 操作数 `<= 2` 时默认直出;操作数 `> 2` 时优先收敛为 `1 个直出主按钮 + 1 个更多按钮`。 +- 新增列表页如果使用 `ElCard` 承载需要撑满剩余高度的 `ElTable height="100%"`,`body-class` 优先使用公共类 `business-table-card-body`,该类由 `src/styles/scss/element-plus.scss` 统一维护;不要再为每个页面新增 `xxx-table-card-body` 私有样式。历史页面已有私有类时不强制专项回改,当前任务触达相关页面再按公共类收敛。 - 表格、按钮、弹层、表单的尺寸和间距标准优先由 `src/styles/scss/element-plus.scss` 和公共组件承接,不在业务页面散落写新的局部尺寸作为事实标准。 ## 表单与弹层约束 @@ -154,10 +156,14 @@ - 新增、编辑能力优先沿用 `ElDialog / ElDrawer / ElForm / ElScrollbar / #footer` 这一套标准组合,不额外创造新的弹层交互模型。 - 轻中量表单优先复用 `src/components/custom/business-form-dialog.vue`;字段较多、需要保留列表上下文或承载重型控件时,再考虑 `src/components/custom/business-form-drawer.vue`。 - 表单分组优先复用 `src/components/custom/business-form-section.vue`。 -- 现有公共壳组件已内置尺寸预设:`dialog` 的 `sm/md/lg` 对应 `520px/640px/720px`,`drawer` 的 `md/lg/xl` 对应 `480px/720px/960px`;优先使用预设值而不是页面内重复硬编码宽度。 -- 常规 CRUD 表单优先使用 `label-position="top"`、`ElRow + ElCol` 双列布局、`gutter=16`;普通字段优先 `span=12`,长文本或重量级字段优先 `span=24`。 +- `dialog` 宽度优先按纯表单字段数分三档:`<= 6` 个字段用 `sm`,默认单列,目标宽度 `520px`;`7 ~ 14` 个字段用 `md`,默认双列,目标宽度 `720px`;`> 14` 个字段用 `lg`,仍以双列为主,目标宽度 `960px`。宽度只做响应式收缩,实际宽度不超过 `calc(100vw - 32px)`;不因为单个 `textarea` 自动升档,也不做列数响应式折叠。 +- 常规 CRUD 表单优先使用 `label-position="top"`、`ElRow + ElCol` 双列布局、`gutter=16`;普通字段优先 `span=12`,长文本或重量级字段优先 `span=24`。如果整体字段数 `<= 6`,默认按单列表单理解。 +- 当纯表单 `dialog` 因字段数 `<= 6` 归入 `sm` 时,不能只改 `preset`;字段布局也要同步落到单列,常规 `ElCol` 应使用 `span=24`,除非该弹框已经被明确判定为复合内容特例。 +- 左右分栏、表单 + 表格、表单 + 树、关系编辑器、时间线、大段说明区这类复合内容 `dialog`,不强行按字段数归类;可按内容复杂度单独评估使用 `md`、`lg` 或更宽值,但只有在无法合理归入“纯表单三档”时才允许特例。 +- 禁止用页面级宽范围样式直接覆盖整页 `.business-form-dialog` 来统一放大弹框;如确实需要特殊宽度,只能精确作用于目标弹框,且不能误伤同页面其他 `dialog`。 - 底部按钮顺序固定为“取消 -> 确认”,并保持右对齐。 - 单选组和开关类字段优先复用仓库既有样式钩子,例如 `business-form-radio-group`、`business-form-switch-field`。 +- 权限控制按钮默认采用“无权限不渲染”口径,不要把纯权限不足的入口做成禁用态再展示给用户;只有业务状态暂时不可操作、但仍需让用户感知入口存在时,才允许保留禁用态。 ## 接口、路由与权限约束 @@ -201,6 +207,14 @@ const directionLabels = getLabels(row.directionCodes, { separator: ',' }); - 当前系统已有页面或接口已经稳定使用某个字典,例如用户所属公司 `company -> system_user_company`。 - 如果以上两种都没有,就先让后端或业务明确 `dictType`,不要前端自己命名。 +## 业务对象状态颜色集中口径 + +- 各业务域(产品、项目、需求、任务、执行、工单等)的 `statusCode -> ElTag type` 集中维护在 `src/constants/status-tag.ts`,不要在各业务页面或模块内散落硬编码同一份映射。 +- 通用入口是 `getStatusTagType(domain, statusCode)`,未匹配的 `statusCode` 默认回退到 `'info'`。 +- 业务模块按域写薄包装暴露给页面调用,例如 `getExecutionStatusTagType(code)` 内部调用 `getStatusTagType('projectExecution', code)`,避免页面直接耦合到 domain 字符串。 +- 新增对象域时同步两处:`StatusDomain` 增加枚举值;`statusTagTypeRegistry` 添加对应 `statusCode -> StatusTagType` 映射。 +- 后端契约:未来若状态字典开始返回颜色字段,调用方应优先使用后端值,缺失时再回退到 `getStatusTagType` 的前端兜底映射,不要直接绕开集中配置另写一份。 + ## 页面资源与菜单目录约束 - 页面组件键、页面资源、菜单目录是三层不同概念,不要把它们当成同一个值。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..46e4803 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,370 @@ +# CLAUDE.md + +本文件是我(Claude)在 `cn-rdms-web` 项目中的个人工作笔记,沉淀团队既有规范(来源:`AGENTS.md`)与协作惯例。每次进入仓库前先读这一份,避免重复踩坑。 + +> 本文件仅本地保留,已加入 `.gitignore`,请勿提交。 + +--- + +## 0. 行为基线(最重要,先记住) + +- **描述现状以代码、配置、文档可直接验证的事实为准**;不引入历史实现/过渡方案/猜测。 +- **默认精简回答**:先给结论 → 改动点 → 验证方式 → 必要风险。**除非用户主动要求详细,否则不要展开**——不复述清单、不列每条改动的小理由、不堆"汇总"段。用户只让分析就停在分析层,不主动跳到实现。 +- **进入实施阶段前,先说目标、涉及模块、预计改动点、验证方式**。 +- **最小改动原则**:只改当前任务必需的范围,不顺手重构无关代码。 +- **不主动执行 git 操作**(status/diff/add/commit/restore/reset/checkout 全部不主动跑),除非用户明确要求。识别用户改动优先用 Read 直接看文件。 +- 工作树脏的时候,**不要回退与当前任务无关的变更**。 +- 静态校验默认只跑 `pnpm typecheck`;UI/交互/样式类任务**默认不补也不跑前端测试**,除非用户明确要求。 + +--- + +## 1. 项目骨架(认知地图) + +| 维度 | 现状 | +|---|---| +| 应用 | RDMS 系统的 Vue 3 后台前端 | +| 包管理 | `pnpm`(>=8.7.0),Node `>=20.19.0` | +| 工具链 | Vite 7、TypeScript、Pinia、Element Plus、UnoCSS | +| 工作区 | `packages/*`,通过 `@sa/*` 引用 | +| 别名 | `@` → `src`;`~` → 仓库根 | +| 端口 | dev 9527 / preview 9725 | +| 环境文件 | `.env`、`.env.dev`、`.env.prod` | + +**已经形成闭环的五条主线,后续改动顺着做,不平行起新的:** + +1. **路由来源统一**:页面文件 + 自定义路由 → `elegant-router` 生成 → `build/plugins/router.ts` 集中补 `meta`。 +2. **权限入口统一**:常量路由 / 权限路由分流;`route store` 负责初始化、菜单生成、缓存路由、面包屑。 +3. **请求入口统一**:所有业务请求走 `src/service/request/index.ts`。 +4. **页面套路统一**:列表页 = 搜索区 + 表格区 + 操作弹层/抽屉 + `modules/*` 子组件。 +5. **衍生资产统一**:页面资源白名单从路由结构生成,不手工维护第二份。 + +--- + +## 2. 关键目录速查 + +| 路径 | 职责 | +|---|---| +| `src/views` | 业务页面(编排层薄) | +| `src/components` | 共享组件 | +| `src/layouts` | 应用壳、头部、侧栏、菜单、标签页、主题抽屉 | +| `src/store/modules` | Pinia 模块:app / auth / route / tab / theme / dict | +| `src/service/api` | 接口封装、参数归一化、查询字符串拼装、返回类型对齐 | +| `src/service/request` | 统一请求实例、鉴权、加密、错误处理、token 刷新 | +| `src/router/routes` | 自定义路由 | +| `src/router/elegant` | **生成产物,不要手改** | +| `src/theme/settings.ts` | 默认主题与布局设置 | +| `build/plugins/router.ts` | elegant-router 配置 + 路由 meta 生成 | +| `src/hooks/common/table.ts` | 列表页表格 hook 主入口 | +| `src/hooks/common/form.ts` | 表单校验与表单实例 hook | +| `src/constants/status-tag.ts` | 业务对象状态颜色(ElTag type)集中配置 | +| `src/styles/scss/element-plus.scss` | 表格/弹层/按钮/表单 密度与公共壳样式 | +| `packages/*` | 项目内本地共享库 | +| `docs/` | 架构/权限/页面规范文档,做相关改动前先查 | + +--- + +## 3. 生成文件(不要手改) + +- `src/router/elegant/imports.ts` +- `src/router/elegant/routes.ts` +- `src/router/elegant/transform.ts` +- `src/typings/elegant-router.d.ts` +- `src/typings/components.d.ts` +- `docs/frontend-page-resource-manifest.json` + +**再生命令:** +- 路由产物过期 → `pnpm gen-route` +- 页面资源清单需同步 → `pnpm gen:page-resource-manifest` + +--- + +## 4. 路由与导航 + +- 新增业务页:通过页面文件 + `build/plugins/router.ts` 补齐,**不要在多个位置重复注册**。 +- `meta.icon` = Iconify 图标;`meta.localIcon` = 本地 SVG。**不要混用字段语义。** +- `meta` 中心落点是 `build/plugins/router.ts`,新页的 `icon`/`order`/`roles`/`keepAlive` 在那里集中维护。 +- `meta.constant = true` → 常量路由;其他默认权限路由。常量路由维护入口是 `build/plugins/router.ts` 和 `src/router/routes/custom-routes.ts`。 +- `i18nKey` 是兼容字段,不是新页必须补齐项。 + +### 4.1 对象上下文业务域(重要陷阱) + +- `product`、`project` 这类业务域,**入口页是设计如此**:先进业务域入口页 → 再选对象建上下文。**不要把"入口页是可点击菜单"误判成 bug。** +- 入口页(如 `product_list -> /product/list -> view.product_list`)可作为左侧一级菜单实际命中页。这 ≠ 已进入对象上下文态。 +- **遇到"点入口页后布局壳消失、只剩内容页"**:先查是否动态权限路由模式 + 后端 `get-user-routes` 是否缺业务域根路由。**不要直接把入口菜单从"菜单"改成"目录"**。 +- 在 `VITE_AUTH_ROUTE_MODE=dynamic` 下,若后端只返回叶子页(如缺 `product -> layout.base`,只返 `product_list`),前端必须在动态路由归一化阶段**补回本地业务域骨架**,不能让入口裸挂为顶层 `view.*`。 +- 对象上下文稳定来源仍是本地路由骨架;动态路由兼容只能"补骨架 + 对齐入口",不能反推。 +- 新增业务域时同步检查:本地静态骨架、`src/constants/object-context.ts` 中的 `domainKey/entryRouteKey/entryRoutePath/fallbackDefaultRouteKey`、动态路由归一化、对象上下文 store、头部菜单切换。 + +--- + +## 5. 分层职责 + +| 层 | 该做 | 不该做 | +|---|---|---| +| `src/views` | 编排状态、表单行为、组合 store/service | 散落 URL 拼接、token 注入、错误提示、权限路由推导 | +| `src/components` | 可复用 UI / 局部业务部件 | 长期堆只服务单页面的复杂流程 | +| `src/service/api` | 接口封装、参数归一化、查询拼装、类型对齐 | 在 views/store/components 重复手写接口地址和序列化 | +| `src/service/request` | 统一鉴权/加密/成功码/token 刷新/错误处理 | 平行引入新的 axios/fetch 链绕开封装 | +| `src/store/modules` | 跨页面共享状态 | 把临时局部状态堆进全局 store | +| `src/router` & `build/plugins/router.ts` | 路由/菜单/权限标识/首页/路由 meta | 在页面里临时写条件分支替代正式配置 | +| `src/layouts` & `src/theme` | 全局布局壳与主题 | 在业务页面复制平行布局/主题状态 | + +--- + +## 6. 业务页面开发风格 + +- **页面组件保持"编排层薄"**:页面文件主管搜索参数、表格 hook、列定义、弹层开关、接口编排。 +- 列表页拆同目录 `modules/*`:搜索组件、操作弹层、详情抽屉、资源面板等。 +- **参考实现**:系统管理下 `user`/`role`/`menu`/`dict`。 +- 列表 hook 优先复用:`src/hooks/common/table.ts` 的 `useUIPaginatedTable`、`useTableOperate`、`defaultTransform`。 +- 表单 hook 优先复用:`src/hooks/common/form.ts` 的 `useForm`、`useFormRules`。 +- **业务口径是"内网中文优先"**:新页不必强行国际化;但已有大量 `$t(...)` 的页面继续开发时,保持局部一致,不要中文/i18n 混用。 + +--- + +## 7. 表格、搜索区、操作列 + +### 7.1 搜索区(强约束) + +- **必须用** `src/components/custom/table-search-fields.vue` 的 `fields` 声明式配置,不得手写 `ElRow/ElCol/ElFormItem` 骨架。 +- 仅当字段存在复杂联动、自定义插槽或 `TableSearchFields` 明确无法承载时,才退回 `src/components/custom/table-search-panel.vue`,并在实施说明中写明原因。 +- **搜索区按钮组固定在第一行最后一格**;存在折叠时按钮顺序固定为 **展开/收起 → 重置 → 查询**。**不允许**因查询条件不足、展开收起或响应式样式把按钮提前或挤到下一行。 +- `columns` 表示首行总格数,**最后 1 格永远留给按钮**;字段不足 `columns - 1` 由组件补空占位;超过则进入展开区。 +- 4 个查询条件的场景必须 `:columns="4"`(3 条件 + 按钮)。 +- 搜索模块只接 `model` 和必要选项,只发 `reset`/`search`,**不直接承载列表请求**。 +- 详细规范见 `docs/table-search-fields-usage.md`。 + +### 7.2 表格 + +- 操作列优先复用 `src/components/custom/business-table-action-cell.tsx`。 +- 操作数 ≤ 2:直出;操作数 > 2:**1 个直出主按钮 + 1 个更多按钮**。 +- `ElCard` 承载 `ElTable height="100%"` 时,`body-class` 优先用公共类 **`business-table-card-body`**(由 `src/styles/scss/element-plus.scss` 维护)。**不要为每页新建 `xxx-table-card-body` 私有样式**。历史私有类不强制专项回改,触达再收敛。 +- 表格/按钮/弹层/表单的尺寸与间距标准走 `element-plus.scss` 和公共组件,**不要在业务页散落写局部尺寸作为事实标准**。 + +--- + +## 8. 表单与弹层(强约束) + +### 8.1 组件选择 + +- 标准组合:`ElDialog / ElDrawer / ElForm / ElScrollbar / #footer`。 +- 轻中量表单:`src/components/custom/business-form-dialog.vue`。 +- 字段较多 / 需保留列表上下文 / 重型控件:`src/components/custom/business-form-drawer.vue`。 +- 表单分组:`src/components/custom/business-form-section.vue`。 + +### 8.2 Dialog 宽度三档(按纯表单字段数) + +| 字段数 | preset | 默认列数 | 目标宽度 | +|---|---|---|---| +| ≤ 6 | `sm` | 单列 | 520px | +| 7 ~ 14 | `md` | 双列 | 720px | +| > 14 | `lg` | 双列为主 | 960px | + +- 实际宽度上限:`calc(100vw - 32px)`。 +- **不因为单个 textarea 自动升档**,不做列数响应式折叠。 +- 归到 `sm` 时不能只改 preset,**字段布局也要落到单列**:常规 `ElCol` 用 `span=24`,除非已判定为复合内容特例。 + +### 8.3 复合内容特例 + +左右分栏 / 表单+表格 / 表单+树 / 关系编辑器 / 时间线 / 大段说明区 → 不强按字段数归类,按内容复杂度评估 `md`/`lg` 或更宽。**只有无法合理归入"纯表单三档"时才允许特例。** + +### 8.4 表单布局 + +- 常规 CRUD:`label-position="top"` + `ElRow + ElCol` 双列 + `gutter=16`。 +- 普通字段 `span=12`;长文本/重量级字段 `span=24`。 +- 字段 ≤ 6 默认按单列理解。 + +### 8.5 其他 + +- **禁止**用页面级宽范围样式覆盖整页 `.business-form-dialog` 来统一放大;如需特殊宽度,必须精确作用于目标弹框,不误伤同页其他 dialog。 +- 底部按钮固定 **取消 → 确认**,右对齐。 +- 单选组/开关字段优先复用既有钩子:`business-form-radio-group`、`business-form-switch-field`。 +- **权限按钮默认"无权限不渲染"**;只有业务状态暂时不可操作但仍需让用户感知入口存在时,才允许保留禁用态。 + +### 8.6 全局反馈(Toast / Message) + +- **全局反馈通道只有一个**:`window.$message`(`src/components/common/app-provider.vue` 注入的 `ElMessage`),全仓 30+ 处都用它。**不要平行引入 `ElNotification` / 自定义 toast**;要求"全局风格切换"则单独立项,不要在小改动里悄悄启动。 +- **type 语义**(4 种 type → 3 类视觉语义): + - `error` → 错误(红):操作失败、明确异常 + - `warning` → 告警(橙):用户即将出错、风险确认 + - `success` → 通知-成功(绿):操作成功 + - `info` → 通知-信息(蓝):信息告知、默认兜底说明 +- **type 选错就丑**:`warning` 是"出错警告",不要拿来表达普通信息(用 `info`);`info` 是"信息告知",不要拿来报错(用 `error`)。 +- **"先做 A 再做 B" 的引导性提示**:用 `ElFormItem :error="msg"` 红字内联(跟校验同款),**不要用 toast**——toast 适合事后反馈、不阻断流程,对引导性提示体验差。 +- **全局视觉**(实色背景 + 白字 + 阴影 + `$radius` 圆角)由 `src/styles/scss/element-plus.scss` 末尾的 `.el-message` 块统一维护,**业务页面禁止覆盖** `.el-message-*` 样式。要调颜色就改 `element-plus.scss`,不要在业务页 scoped 散落。 + +```ts +window.$message?.success('保存成功'); +window.$message?.error('保存失败:xxx'); +window.$message?.warning('当前修改未保存,确认离开?'); +window.$message?.info('未选择计划开始日期,已按今日为基准计算'); +``` + +--- + +## 9. 接口、路由、权限 + +- 默认走 `src/service/request/index.ts`,不另造鉴权/加密/错误处理/token 刷新。 +- 接口前缀、服务常量优先复用 `src/constants/service.ts`。 +- 后端契约变化时同步检查 `src/service/api/*`、`src/typings/api/*`、相关页面、说明文档。 +- 路由/菜单/权限改动时同步检查 `build/plugins/router.ts`、`src/router/routes/*`、`src/store/modules/route/*`、相关文档。 +- 路由产物过期:改源配置 + `pnpm gen-route`,**不要把手工修补生成文件当常规方案**。 + +--- + +## 10. 运行时字典 + +- 由 `src/store/modules/dict/index.ts` 管理,登录后通过 `/system/dict-data/frontend-cache` 初始化。**不要在页面重复直调字典接口。** +- 字典编码常量收敛在 `src/constants/dict.ts`。**不要散落硬编码 `dictType`。** +- **不要猜字典编码**:先从后端接口文档/字段契约/系统字典管理页确认真实 `dictType`,再写入常量。 +- 常量加中文注释:对应业务字段 + 编码确认来源。 +- 后端编码带历史命名痕迹(如 `rdms_product_direction`)时,前端常量名按真实业务语义命名,**不扩散历史误导**。 + +### 字典使用方式 + +| 场景 | 组件/Hook | +|---|---| +| 表单下拉 | `src/components/custom/dict-select.vue` | +| 普通文案回显 | `src/components/custom/dict-text.vue` | +| 标签态回显 | `src/components/custom/dict-tag.vue`(标签颜色业务页自决) | +| script setup / TSX 列格式化 / 复杂判断 | `src/hooks/business/dict.ts` 的 `useDict(dictCode)` | + +`useDict` 常用能力:`dictOptions`、`getItem`、`getLabel`、`getLabels`、`hasValue`。 + +`DictSelect` 默认只展示启用项;需包含禁用项显式 `:only-enabled="false"`。 + +```vue + + + +``` + +```ts +const { getLabel, getLabels } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE); +const directionLabel = getLabel(row.directionCode); +const directionLabels = getLabels(row.directionCodes, { separator: ',' }); +``` + +--- + +## 11. 页面资源 & 菜单目录(三层不同概念) + +- `component` = 渲染哪个页面组件 +- 菜单目录 = 挂在哪个业务目录、最终 URL +- 页面资源 = 白名单中选择并回填组件信息 + +**不要混淆**:组件键 `view.system_dict` ≠ 必须挂 `/system/dict`,同一个组件允许在新业务目录下复用。 + +页面资源白名单的标准路径是**参考路径,不应反向覆盖菜单树已确定的最终 URL**。 + +菜单编辑器/页面资源选择逻辑改动时,保证"组件可解析、资源合法、最终 URL 由菜单树决定",**不要强绑标准路径与父目录前缀**。 + +--- + +## 12. ID 类型铁律(强约束,必须严格执行) + +> 后端主键 ID / 用户 ID / 对象 ID / 雪花 ID / Long ID **一律按 `string` 接收和传递**。 + +**原因**:JS `number` 无法稳定承载 Long 精度;序列化精度丢失;`number/string` 键不一致 → 回显/筛选/映射/路由参数/对象上下文异常。 + +### 落实范围(全部) +`typings`、API 返回类型、表单 model、组件 props/emits、`ElSelect` 的 value、路由参数、查询参数、`Map` 键、筛选条件、store 状态 → **全部 `string` / `string[]`**。 + +### 禁止写法 +- ❌ `Number(id)` / `+id` / `parseInt(id)` / `parseFloat(id)` / `Math.floor(id)` +- ❌ 任何"为了比较/传参/回填/提交而把 ID 转 number" + +### 比较与映射 +- ✅ `id === targetId` +- ✅ `Map` / `Set` +- ❌ 不混用 `number/string` 双口径 + +### 后端契约风险(关键) +- 后端暂返数值型 ID 时,**前端在 `typings` / API 适配层 / 进业务层前转 `string`**,不要按 `number` 扩散。 +- **但如果后端把超 JS 安全整数的 Long 直接作为 JSON 数字返回,前端再 `String(number)` 只能得到"已经丢精度后的错误字符串"**。这种情况必须明确记为接口契约风险,不能误判为"已安全处理"。 +- 最稳妥契约:**后端 Long ID 直接按字符串返回**;前端全链路按字符串。后端未改,前端也不得新增 `number` 口径 ID。 + +### 历史代码原则 +不再新增 `number` 口径;当前任务触达相关链路时**顺手矫正**;不要继续复制历史写法。 + +--- + +## 13. 代码约定 + +- 优先用别名导入(`@/...`、`~/...`),避免长相对路径。 +- 与 TypeScript 严格模式兼容。 +- 沿用 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。 +- UI 沿用 `src/layouts` 和 `src/theme` 现有模式,不平行引入新设计体系。 +- **注释克制**:只在代码本身不直观时补必要中文说明;不删原有有效注释;不写没信息量的注释。 +- 中文内容用 UTF-8,自检显示;**不要用改成英文规避编码问题**。 +- Node ESM 脚本:避免 `__filename`/`__dirname` 这类下划线悬挂命名。 +- 批量异步并发优先 `Promise.all(...)`,不在循环里默认 `await`。 +- 手写 `new Promise(...)` 用 block 写法,不要写成隐式返回的单表达式箭头函数。 +- 函数若同时承担"判断 + 转换 + 组装 + 递归",拆 helper。 + +--- + +## 14. 校验 + +### 14.1 校验口径 + +| 任务类型 | 默认校验 | +|---|---| +| 前端页面/交互/样式 | `pnpm typecheck`,不主动跑测试 | +| 需更严格静态检查 | 加 `pnpm lint` | +| 涉及路由 | 加 `pnpm gen-route` | +| 影响页面资源清单/菜单资源选择/页面白名单 | 加 `pnpm gen:page-resource-manifest` | + +### 14.2 静态校验自查清单 +- 调用链是否闭环?改动是否在正确分层? +- 路由/菜单/权限标识/主题状态/资源注册 是否前后一致? +- 改动范围是否控制在最小集合? +- 文档/类型/接口封装/生成产物 是否需要同步更新? + +--- + +## 15. 提交规范 + +- **`pre-commit` 执行 `pnpm typecheck && pnpm lint && git diff --exit-code`**:能跑 ≠ 能提交。 +- `pnpm lint` 会跑 `eslint . --fix`:提交失败后检查是否有被自动修复但未重新暂存的文件。 +- 推荐提交方式:`pnpm commit:zh`(交互选 type/scope/description)。 +- 手动提交:`git commit -m "type(scope): 描述"`,参考 `docs/前端提交规范与示例.md`。 +- `commit-msg` 钩子校验 Conventional Commits。 + +--- + +## 16. 协作记忆(与本仓库用户共事) + +- 用户语言:**中文**(始终用中文回复)。 +- **不主动跑 git 命令**(用户已强调)。 +- 默认精简、结论先行。 +- 工作树脏时不要回退无关变更。 +- 改架构/权限/页面规范前先翻 `docs/`,避免与现有约定冲突。 +- 改布局/主题时同时检查 `src/layouts/*` 与 `src/store/modules/theme/*`。 +- 改路由/菜单时同时检查 `build/plugins/router.ts` 与 `src/router/routes/*`。 + +--- + +## 17. 常用命令速查 + +```bash +pnpm typecheck # 最小静态校验 +pnpm lint # eslint . --fix +pnpm gen-route # 重新生成路由产物 +pnpm gen:page-resource-manifest # 同步页面资源清单 +pnpm commit:zh # 交互式提交(推荐) +pnpm dev # dev server (9527) +pnpm preview # preview server (9725) +``` + +--- + +## 18. 业务对象状态颜色 + +- 集中文件:`src/constants/status-tag.ts` +- 各业务域 `statusCode → ElTag type` 在此统一维护,**不要在各页面散落硬编码**。 +- 已支持域:`projectExecution`、`projectTask`;预留:`project`、`product`、`requirement`、`workOrder`。 +- helper:`getStatusTagType(domain, statusCode)`,未匹配回退 `'info'`。 +- 业务模块写薄包装,例如 `getExecutionStatusTagType(code) = getStatusTagType('projectExecution', code)`。 +- 新增对象域:在 `StatusDomain` 加枚举 + `statusTagTypeRegistry` 加对应 map;调用方写一个 wrapper 即可。 +- 后端契约:未来若状态字典返颜色字段,调用方优先取后端值,缺失时回退 helper(前端兜底)。 diff --git a/build/plugins/router.ts b/build/plugins/router.ts index 22ce16d..f5543c7 100644 --- a/build/plugins/router.ts +++ b/build/plugins/router.ts @@ -50,6 +50,35 @@ export function setupElegantRouter() { hideInMenu: true, activeMenu: 'product_list' }, + project: { + icon: 'mdi:briefcase-outline', + order: 5 + }, + project_list: { + icon: 'material-symbols:view-list-outline-rounded', + order: 1, + keepAlive: true + }, + project_project: { + hideInMenu: true, + activeMenu: 'project_list' + }, + project_project_overview: { + hideInMenu: true, + activeMenu: 'project_list' + }, + project_project_requirement: { + hideInMenu: true, + activeMenu: 'project_list' + }, + project_project_execution: { + hideInMenu: true, + activeMenu: 'project_list' + }, + project_project_setting: { + hideInMenu: true, + activeMenu: 'project_list' + }, system: { icon: 'carbon:cloud-service-management', order: 9, diff --git a/docs/frontend-page-resource-manifest.json b/docs/frontend-page-resource-manifest.json index 1b2b4fd..16adfcb 100644 --- a/docs/frontend-page-resource-manifest.json +++ b/docs/frontend-page-resource-manifest.json @@ -1,12 +1,12 @@ { - "generatedAt": "2026-04-20T11:27:02.190Z", + "generatedAt": "2026-04-29T08:18:14.397Z", "description": "Frontend visible page resource whitelist for backend route/menu configuration.", "rules": { "directoryComponent": "layout.base", "pageComponentPattern": "view.", "singlePageComponentPattern": "layout.$view." }, - "total": 7, + "total": 8, "items": [ { "name": "product_list", @@ -41,6 +41,39 @@ "pageType": "leaf", "source": "generated" }, + { + "name": "project_list", + "path": "/project/list", + "component": "view.project_list", + "title": "项目列表", + "routeTitle": "project_list", + "i18nKey": "route.project_list", + "icon": "material-symbols:view-list-outline-rounded", + "localIcon": null, + "order": 1, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "项目列表", + "i18nKey": "route.project_list", + "icon": "material-symbols:view-list-outline-rounded", + "localIcon": null, + "order": 1, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "project", + "pageType": "leaf", + "source": "generated" + }, { "name": "system_user", "path": "/system/user", diff --git a/package.json b/package.json index 136cf0c..34fd3dc 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "@visactor/vue-vtable": "1.19.8", "@vueuse/components": "13.9.0", "@vueuse/core": "13.9.0", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.12", "clipboard": "2.0.11", "dayjs": "1.11.18", "defu": "^6.1.4", @@ -77,7 +79,6 @@ "vue-i18n": "11.1.11", "vue-pdf-embed": "2.1.3", "vue-router": "4.5.1", - "wangeditor": "4.7.15", "xgplayer": "3.0.23", "xlsx": "0.18.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1323948..d32abad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,12 @@ importers: '@vueuse/core': specifier: 13.9.0 version: 13.9.0(vue@3.5.20(typescript@5.8.3)) + '@wangeditor/editor': + specifier: ^5.1.23 + version: 5.1.23 + '@wangeditor/editor-for-vue': + specifier: ^5.1.12 + version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.20(typescript@5.8.3)) clipboard: specifier: 2.0.11 version: 2.0.11 @@ -128,9 +134,6 @@ importers: vue-router: specifier: 4.5.1 version: 4.5.1(vue@3.5.20(typescript@5.8.3)) - wangeditor: - specifier: 4.7.15 - version: 4.7.15 xgplayer: specifier: 3.0.23 version: 3.0.23(core-js@3.49.0) @@ -560,10 +563,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime-corejs3@7.29.2': - resolution: {integrity: sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -1399,6 +1398,9 @@ packages: '@sxzz/popperjs-es@2.11.8': resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + '@transloadit/prettier-bytes@0.0.7': + resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} + '@turf/boolean-clockwise@6.5.0': resolution: {integrity: sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==} @@ -1502,6 +1504,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/event-emitter@0.3.5': + resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -1791,6 +1796,23 @@ packages: cpu: [x64] os: [win32] + '@uppy/companion-client@2.2.2': + resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==} + + '@uppy/core@2.3.4': + resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==} + + '@uppy/store-default@2.1.1': + resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==} + + '@uppy/utils@4.1.3': + resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==} + + '@uppy/xhr-upload@2.1.3': + resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==} + peerDependencies: + '@uppy/core': ^2.3.3 + '@visactor/vchart-theme@1.12.2': resolution: {integrity: sha512-r298TUdK+CKbHGVYWgQnNSEB5uqpFvF2/aMNZ/2POQnd2CovAPJOx2nTE6hAcOn8rra2FwJ2xF8AyP1O5OhrTw==} peerDependencies: @@ -2015,6 +2037,93 @@ packages: peerDependencies: vue: ^3.5.0 + '@wangeditor/basic-modules@1.1.7': + resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/code-highlight@1.0.3': + resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/core@1.1.19': + resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==} + peerDependencies: + '@uppy/core': ^2.1.1 + '@uppy/xhr-upload': ^2.0.3 + dom7: ^3.0.0 + is-hotkey: ^0.2.0 + lodash.camelcase: ^4.3.0 + lodash.clonedeep: ^4.5.0 + lodash.debounce: ^4.0.8 + lodash.foreach: ^4.5.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + lodash.toarray: ^4.4.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/editor-for-vue@5.1.12': + resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==} + peerDependencies: + '@wangeditor/editor': '>=5.1.0' + vue: ^3.0.5 + + '@wangeditor/editor@5.1.23': + resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==} + + '@wangeditor/list-module@1.0.5': + resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/table-module@1.1.4': + resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/upload-image-module@1.0.2': + resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==} + peerDependencies: + '@uppy/core': ^2.0.3 + '@uppy/xhr-upload': ^2.0.3 + '@wangeditor/basic-modules': 1.x + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.foreach: ^4.5.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/video-module@1.1.4': + resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==} + peerDependencies: + '@uppy/core': ^2.1.4 + '@uppy/xhr-upload': ^2.0.7 + '@wangeditor/core': 1.x + dom7: ^3.0.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2434,6 +2543,9 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2476,9 +2588,6 @@ packages: core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} - core-js-pure@3.49.0: - resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} - core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} @@ -2800,6 +2909,9 @@ packages: dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom7@3.0.0: + resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==} + domelementtype@1.3.1: resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} @@ -3439,6 +3551,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + html2canvas@1.4.1: resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} engines: {node: '>=8.0.0'} @@ -3450,6 +3565,9 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + i18next@20.6.1: + resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3486,6 +3604,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + immutable@5.1.5: resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} @@ -3606,6 +3727,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -3643,6 +3767,10 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3682,6 +3810,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3874,9 +4005,31 @@ packages: lodash: '*' lodash-es: '*' + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.foreach@4.5.0: + resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.toarray@4.4.0: + resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3953,6 +4106,9 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-match@1.0.2: + resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -4021,6 +4177,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + namespace-emitter@2.0.1: + resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4336,6 +4495,9 @@ packages: resolution: {integrity: sha512-spBB5sgC4cv2YcW03f/IAUN1pgDJWNWD8FzkyY4mArLUMJW+KlQhlmUdKAHQuPfb00Jl5xIfImeOsf6YL8QK7Q==} engines: {node: '>=0.10.0'} + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4366,6 +4528,10 @@ packages: print-js@1.6.0: resolution: {integrity: sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -4547,6 +4713,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + select@1.1.2: resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==} @@ -4647,9 +4816,21 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slate-history@0.66.0: + resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==} + peerDependencies: + slate: '>=0.65.3' + + slate@0.72.8: + resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==} + slice-source@0.4.1: resolution: {integrity: sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==} + snabbdom@3.6.3: + resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==} + engines: {node: '>=12.17.0'} + snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} engines: {node: '>=0.10.0'} @@ -4697,6 +4878,9 @@ packages: resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} engines: {node: '>=0.8'} + ssr-window@3.0.0: + resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -4859,6 +5043,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -5231,9 +5418,6 @@ packages: typescript: optional: true - wangeditor@4.7.15: - resolution: {integrity: sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==} - watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -5276,6 +5460,9 @@ packages: engines: {node: '>= 8'} hasBin: true + wildcard@1.1.2: + resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} + wmf@1.0.2: resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} engines: {node: '>=0.8'} @@ -5742,10 +5929,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime-corejs3@7.29.2': - dependencies: - core-js-pure: 3.49.0 - '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': @@ -6419,6 +6602,8 @@ snapshots: '@sxzz/popperjs-es@2.11.8': {} + '@transloadit/prettier-bytes@0.0.7': {} + '@turf/boolean-clockwise@6.5.0': dependencies: '@turf/helpers': 6.5.0 @@ -6526,6 +6711,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/event-emitter@0.3.5': {} + '@types/geojson@7946.0.16': {} '@types/json-schema@7.0.15': {} @@ -6855,6 +7042,35 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@uppy/companion-client@2.2.2': + dependencies: + '@uppy/utils': 4.1.3 + namespace-emitter: 2.0.1 + + '@uppy/core@2.3.4': + dependencies: + '@transloadit/prettier-bytes': 0.0.7 + '@uppy/store-default': 2.1.1 + '@uppy/utils': 4.1.3 + lodash.throttle: 4.1.1 + mime-match: 1.0.2 + namespace-emitter: 2.0.1 + nanoid: 3.3.11 + preact: 10.29.1 + + '@uppy/store-default@2.1.1': {} + + '@uppy/utils@4.1.3': + dependencies: + lodash.throttle: 4.1.1 + + '@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)': + dependencies: + '@uppy/companion-client': 2.2.2 + '@uppy/core': 2.3.4 + '@uppy/utils': 4.1.3 + nanoid: 3.3.11 + '@visactor/vchart-theme@1.12.2(@visactor/vchart@2.0.4)': dependencies: '@visactor/vchart': 2.0.4 @@ -7284,6 +7500,114 @@ snapshots: dependencies: vue: 3.5.20(typescript@5.8.3) + '@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + is-url: 1.2.4 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + prismjs: 1.30.0 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@types/event-emitter': 0.3.5 + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + dom7: 3.0.0 + event-emitter: 0.3.5 + html-void-elements: 2.0.1 + i18next: 20.6.1 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + scroll-into-view-if-needed: 2.2.31 + slate: 0.72.8 + slate-history: 0.66.0(slate@0.72.8) + snabbdom: 3.6.3 + + '@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.20(typescript@5.8.3))': + dependencies: + '@wangeditor/editor': 5.1.23 + vue: 3.5.20(typescript@5.8.3) + + '@wangeditor/editor@5.1.23': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + lodash.foreach: 4.5.0 + slate: 0.72.8 + snabbdom: 3.6.3 + + '@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3) + dom7: 3.0.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.3 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -7741,6 +8065,8 @@ snapshots: component-emitter@1.3.1: {} + compute-scroll-into-view@1.0.20: {} + concat-map@0.0.1: {} concat-stream@1.4.11: @@ -7778,8 +8104,6 @@ snapshots: dependencies: browserslist: 4.28.1 - core-js-pure@3.49.0: {} - core-js@3.49.0: {} core-util-is@1.0.3: {} @@ -8093,6 +8417,10 @@ snapshots: domhandler: 4.3.1 entities: 2.2.0 + dom7@3.0.0: + dependencies: + ssr-window: 3.0.0 + domelementtype@1.3.1: {} domelementtype@2.3.0: {} @@ -8895,6 +9223,8 @@ snapshots: hookable@5.5.3: {} + html-void-elements@2.0.1: {} + html2canvas@1.4.1: dependencies: css-line-break: 2.1.0 @@ -8911,6 +9241,10 @@ snapshots: human-signals@8.0.1: {} + i18next@20.6.1: + dependencies: + '@babel/runtime': 7.29.2 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -8935,6 +9269,8 @@ snapshots: immediate@3.0.6: {} + immer@9.0.21: {} + immutable@5.1.5: {} import-fresh@3.3.1: @@ -9052,6 +9388,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hotkey@0.2.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -9079,6 +9417,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-plain-object@5.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9115,6 +9455,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -9281,8 +9623,22 @@ snapshots: lodash: 4.17.23 lodash-es: 4.17.23 + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.debounce@4.0.8: {} + + lodash.foreach@4.5.0: {} + + lodash.isequal@4.5.0: {} + lodash.merge@4.6.2: {} + lodash.throttle@4.1.1: {} + + lodash.toarray@4.4.0: {} + lodash@4.17.21: {} lodash@4.17.23: {} @@ -9363,6 +9719,10 @@ snapshots: mime-db@1.52.0: {} + mime-match@1.0.2: + dependencies: + wildcard: 1.1.2 + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -9430,6 +9790,8 @@ snapshots: muggle-string@0.4.1: {} + namespace-emitter@2.0.1: {} + nanoid@3.3.11: {} nanoid@5.1.5: {} @@ -9738,6 +10100,8 @@ snapshots: posthtml-parser: 0.2.1 posthtml-render: 1.4.0 + preact@10.29.1: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -9758,6 +10122,8 @@ snapshots: print-js@1.6.0: {} + prismjs@1.30.0: {} + progress@2.0.3: {} prompts@2.4.2: @@ -9973,6 +10339,10 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + select@1.1.2: {} semver@6.3.1: {} @@ -10094,8 +10464,21 @@ snapshots: sisteransi@1.0.5: {} + slate-history@0.66.0(slate@0.72.8): + dependencies: + is-plain-object: 5.0.0 + slate: 0.72.8 + + slate@0.72.8: + dependencies: + immer: 9.0.21 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + slice-source@0.4.1: {} + snabbdom@3.6.3: {} + snapdragon-node@2.1.1: dependencies: define-property: 1.0.0 @@ -10150,6 +10533,8 @@ snapshots: dependencies: frac: 1.1.2 + ssr-window@3.0.0: {} + stable-hash-x@0.2.0: {} stable@0.1.8: {} @@ -10320,6 +10705,8 @@ snapshots: tiny-invariant@1.3.3: {} + tiny-warning@1.0.3: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -10753,12 +11140,6 @@ snapshots: optionalDependencies: typescript: 5.8.3 - wangeditor@4.7.15: - dependencies: - '@babel/runtime': 7.29.2 - '@babel/runtime-corejs3': 7.29.2 - tslib: 2.8.1 - watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -10845,6 +11226,8 @@ snapshots: dependencies: isexe: 2.0.0 + wildcard@1.1.2: {} + wmf@1.0.2: {} wolfy87-eventemitter@5.2.9: {} diff --git a/src/components/custom/business-form-dialog.vue b/src/components/custom/business-form-dialog.vue index 02d5a9f..8b175ec 100644 --- a/src/components/custom/business-form-dialog.vue +++ b/src/components/custom/business-form-dialog.vue @@ -49,8 +49,8 @@ const visible = defineModel({ const DIALOG_WIDTH_MAP: Record = { sm: '520px', - md: '640px', - lg: '720px' + md: '720px', + lg: '960px' }; const dialogWidth = computed(() => props.width ?? DIALOG_WIDTH_MAP[props.preset]); diff --git a/src/components/custom/business-rich-text-editor.vue b/src/components/custom/business-rich-text-editor.vue new file mode 100644 index 0000000..0316d17 --- /dev/null +++ b/src/components/custom/business-rich-text-editor.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/custom/business-rich-text-view.vue b/src/components/custom/business-rich-text-view.vue new file mode 100644 index 0000000..5a7f9ff --- /dev/null +++ b/src/components/custom/business-rich-text-view.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/components/custom/business-user-select.vue b/src/components/custom/business-user-select.vue new file mode 100644 index 0000000..db8954f --- /dev/null +++ b/src/components/custom/business-user-select.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/components/custom/table-search-fields.vue b/src/components/custom/table-search-fields.vue new file mode 100644 index 0000000..bc44e66 --- /dev/null +++ b/src/components/custom/table-search-fields.vue @@ -0,0 +1,314 @@ + + + + + + diff --git a/src/constants/dict.ts b/src/constants/dict.ts index 7943b4d..87524f3 100644 --- a/src/constants/dict.ts +++ b/src/constants/dict.ts @@ -59,3 +59,19 @@ export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority'; * 来源口径:产品需求文档中定义,标签包括工程需求、用户需求、安全需求、体验优化、功能需求 */ export const RDMS_REQ_CATEGORY_DICT_CODE = 'rdms_req_category'; + +/** + * 项目类型字典编码 + * + * 对应业务字段:项目相关接口和页面中的 projectType + * 来源口径:后端字典 rdms_project_type + */ +export const RDMS_PROJECT_TYPE_DICT_CODE = 'rdms_project_type'; + +/** + * 项目执行类型字典编码 + * + * 对应业务字段:项目任务管理中执行的 executionType + * 来源口径:`rdms-project-boot-执行任务接口API文档.md` 明确 executionType 来自字典 rdms_project_execution_type + */ +export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_type'; diff --git a/src/constants/object-context.ts b/src/constants/object-context.ts index 940fa3d..a1854dd 100644 --- a/src/constants/object-context.ts +++ b/src/constants/object-context.ts @@ -10,11 +10,11 @@ export const objectContextDomainConfigs: App.ObjectContext.DomainConfig[] = [ routePathPrefixes: ['/project'], entryRouteKey: 'project_list', entryRoutePath: '/project/list', - fallbackDefaultRouteKey: 'project_dashboard', - fallbackDefaultRoutePath: '/project/dashboard', - contextApiPath: `${WEB_SERVICE_PREFIX}/project/context`, - contextApiObjectIdParamKey: 'projectId', - contextApiObjectIdPlacement: 'query', + fallbackDefaultRouteKey: 'project_project_overview', + fallbackDefaultRoutePath: '/project/project/overview', + contextApiPath: `${WEB_SERVICE_PREFIX}/project/project/{id}/context`, + contextApiObjectIdParamKey: 'id', + contextApiObjectIdPlacement: 'path', objectIdQueryKey: OBJECT_CONTEXT_QUERY_KEY }, { diff --git a/src/constants/status-tag.ts b/src/constants/status-tag.ts new file mode 100644 index 0000000..f79236e --- /dev/null +++ b/src/constants/status-tag.ts @@ -0,0 +1,59 @@ +/** + * 业务对象状态颜色(ElTag type)集中配置 + * + * 各业务域的 statusCode → ElTag type 在此统一维护,避免散落在各业务模块。 + * 未来若后端状态字典返回颜色字段,可在调用方优先取后端值,缺失时回退此映射。 + */ + +export type StatusTagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'; + +export type StatusDomain = + | 'projectExecution' + | 'projectTask' + | 'executionMember' + | 'project' + | 'product' + | 'requirement' + | 'workOrder'; + +const statusTagTypeRegistry: Record> = { + // 项目-执行 + projectExecution: { + pending: 'info', + active: 'primary', + paused: 'warning', + completed: 'success', + cancelled: 'danger' + }, + // 项目-任务 + projectTask: { + pending: 'info', + active: 'primary', + blocked: 'warning', + completed: 'success', + cancelled: 'danger' + }, + // 执行成员变更事件 + executionMember: { + join: 'success', + inactive: 'danger', + owner_transfer_in: 'warning', + owner_transfer_out: 'warning' + }, + // 项目(待补全) + project: {}, + // 产品(待补全) + product: {}, + // 需求(待补全) + requirement: {}, + // 工单(待补全) + workOrder: {} +}; + +export function getStatusTagType(domain: StatusDomain, statusCode: string | null | undefined): StatusTagType { + if (!statusCode) { + return 'info'; + } + + return statusTagTypeRegistry[domain][statusCode] || 'info'; +} diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 3d99b94..c0d2c99 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -169,12 +169,19 @@ const local: App.I18n.Schema = { function_request: 'Request', 'function_toggle-auth': 'Toggle Auth', 'function_super-page': 'Super Admin Visible', - product: 'Product Management', + product: 'Product', product_list: 'Product List', - product_dashboard: 'Product Dashboard', - product_requirement: 'Requirement Pool', - product_setting: 'Product Settings', - system: 'System Management', + product_dashboard: 'Dashboard', + product_requirement: 'Requirement', + product_setting: 'Settings', + project: 'Project', + project_list: 'Project List', + project_project: 'Project', + project_project_overview: 'Overview', + project_project_requirement: 'Requirement', + project_project_execution: 'Task Management', + project_project_setting: 'Settings', + system: 'System', system_user: 'User Management', 'system_user-detail': 'User Detail', system_role: 'Role Management', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 50482d0..b2866f9 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -174,6 +174,13 @@ const local: App.I18n.Schema = { product_dashboard: '产品仪表盘', product_requirement: '需求池', product_setting: '产品设置', + project: '项目管理', + project_list: '项目列表', + project_project: '项目详情', + project_project_overview: '项目概览', + project_project_requirement: '需求池', + project_project_execution: '任务管理', + project_project_setting: '项目设置', system: '系统管理', system_user: '用户管理', 'system_user-detail': '用户详情', diff --git a/src/router/elegant/imports.ts b/src/router/elegant/imports.ts index 53aef8f..bdc123c 100644 --- a/src/router/elegant/imports.ts +++ b/src/router/elegant/imports.ts @@ -51,6 +51,11 @@ export const views: Record Promise import("@/views/product/list/index.vue"), product_requirement: () => import("@/views/product/requirement/index.vue"), product_setting: () => import("@/views/product/setting/index.vue"), + project_list: () => import("@/views/project/list/index.vue"), + project_project_execution: () => import("@/views/project/project/execution/index.vue"), + project_project_overview: () => import("@/views/project/project/overview/index.vue"), + project_project_requirement: () => import("@/views/project/project/requirement/index.vue"), + project_project_setting: () => import("@/views/project/project/setting/index.vue"), system_dict: () => import("@/views/system/dict/index.vue"), system_menu: () => import("@/views/system/menu/index.vue"), system_post: () => import("@/views/system/post/index.vue"), diff --git a/src/router/elegant/routes.ts b/src/router/elegant/routes.ts index 6cb3374..dfb1692 100644 --- a/src/router/elegant/routes.ts +++ b/src/router/elegant/routes.ts @@ -488,6 +488,87 @@ export const generatedRoutes: GeneratedRoute[] = [ } ] }, + { + name: 'project', + path: '/project', + component: 'layout.base', + meta: { + title: 'project', + i18nKey: 'route.project', + icon: 'mdi:briefcase-outline', + order: 5 + }, + children: [ + { + name: 'project_list', + path: '/project/list', + component: 'view.project_list', + meta: { + title: 'project_list', + i18nKey: 'route.project_list', + icon: 'material-symbols:view-list-outline-rounded', + order: 1, + keepAlive: true + } + }, + { + name: 'project_project', + path: '/project/project', + meta: { + title: 'project_project', + i18nKey: 'route.project_project', + hideInMenu: true, + activeMenu: 'project_list' + }, + children: [ + { + name: 'project_project_execution', + path: '/project/project/execution', + component: 'view.project_project_execution', + meta: { + title: 'project_project_execution', + i18nKey: 'route.project_project_execution', + hideInMenu: true, + activeMenu: 'project_list' + } + }, + { + name: 'project_project_overview', + path: '/project/project/overview', + component: 'view.project_project_overview', + meta: { + title: 'project_project_overview', + i18nKey: 'route.project_project_overview', + hideInMenu: true, + activeMenu: 'project_list' + } + }, + { + name: 'project_project_requirement', + path: '/project/project/requirement', + component: 'view.project_project_requirement', + meta: { + title: 'project_project_requirement', + i18nKey: 'route.project_project_requirement', + hideInMenu: true, + activeMenu: 'project_list' + } + }, + { + name: 'project_project_setting', + path: '/project/project/setting', + component: 'view.project_project_setting', + meta: { + title: 'project_project_setting', + i18nKey: 'route.project_project_setting', + hideInMenu: true, + activeMenu: 'project_list' + } + } + ] + } + ] + }, { name: 'system', path: '/system', diff --git a/src/router/elegant/transform.ts b/src/router/elegant/transform.ts index 1da663d..d9a9345 100644 --- a/src/router/elegant/transform.ts +++ b/src/router/elegant/transform.ts @@ -211,6 +211,13 @@ const routeMap: RouteMap = { "product_list": "/product/list", "product_requirement": "/product/requirement", "product_setting": "/product/setting", + "project": "/project", + "project_list": "/project/list", + "project_project": "/project/project", + "project_project_execution": "/project/project/execution", + "project_project_overview": "/project/project/overview", + "project_project_requirement": "/project/project/requirement", + "project_project_setting": "/project/project/setting", "system": "/system", "system_dict": "/system/dict", "system_menu": "/system/menu", diff --git a/src/service/api/file.ts b/src/service/api/file.ts new file mode 100644 index 0000000..dda2cd9 --- /dev/null +++ b/src/service/api/file.ts @@ -0,0 +1,19 @@ +import { SYSTEM_SERVICE_PREFIX } from '@/constants/service'; +import { request } from '../request'; + +const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`; + +/** 上传文件(模式一:后端中转) */ +export function uploadFile(file: File, directory?: string) { + const formData = new FormData(); + formData.append('file', file); + if (directory) { + formData.append('directory', directory); + } + + return request({ + url: `${FILE_PREFIX}/upload`, + method: 'post', + data: formData + }); +} diff --git a/src/service/api/index.ts b/src/service/api/index.ts index 5eea6a4..b73862c 100644 --- a/src/service/api/index.ts +++ b/src/service/api/index.ts @@ -1,6 +1,9 @@ export * from './auth'; export * from './dict'; +export * from './file'; export * from './object-context'; export * from './product'; +export * from './project'; +export * from './project-shared'; export * from './route'; export * from './system-manage'; diff --git a/src/service/api/object-context-normalize.ts b/src/service/api/object-context-normalize.ts new file mode 100644 index 0000000..0af31b9 --- /dev/null +++ b/src/service/api/object-context-normalize.ts @@ -0,0 +1,199 @@ +import { normalizeNullableStringId, normalizeStringId } from './shared'; + +export interface BackendObjectContextMenuDTO { + key?: string | null; + label?: string | null; + routeKey?: string | null; + routePath?: string | null; + id?: string | number | null; + name?: string | null; + path?: string | null; + icon?: string | null; + sort?: number | null; + children?: BackendObjectContextMenuDTO[] | null; +} + +interface BackendProductContextProductDTO { + id?: string | number | null; + code?: string | null; + directionCode?: string | null; + name?: string | null; + managerUserId?: string | number | null; + statusCode?: string | null; +} + +interface BackendProjectContextProjectDTO { + id?: string | number | null; + projectCode?: string | null; + projectName?: string | null; + projectType?: string | null; + productId?: string | number | null; + managerUserId?: string | number | null; + statusCode?: string | null; +} + +interface BackendObjectContextRoleDTO { + roleId?: string | number | null; + roleCode?: string | null; + roleName?: string | null; + guestFlag?: boolean | null; +} + +export interface BackendObjectContextDTO { + domainKey?: string | null; + objectType?: string | null; + objectId?: string | number | null; + objectName?: string | null; + objectSummary?: Record | null; + menus?: BackendObjectContextMenuDTO[] | null; + contextScopedMenus?: BackendObjectContextMenuDTO[] | null; + buttonCodes?: string[] | null; + currentProduct?: BackendProductContextProductDTO | null; + currentProject?: BackendProjectContextProjectDTO | null; + currentRole?: BackendObjectContextRoleDTO | null; + navs?: BackendObjectContextMenuDTO[] | null; + buttons?: string[] | null; + defaultRouteKey?: string | null; + defaultRoutePath?: string | null; +} + +function normalizeString(value: string | number | null | undefined) { + if (value === null || value === undefined) { + return ''; + } + + return String(value); +} + +function normalizeRoutePath(path: string | null | undefined) { + const normalizedPath = normalizeString(path).trim(); + + if (!normalizedPath) { + return ''; + } + + if (normalizedPath.startsWith('/')) { + return normalizedPath; + } + + return `/${normalizedPath}`; +} + +function normalizeCurrentProduct( + product: BackendProductContextProductDTO +): Record<'id' | 'code' | 'directionCode' | 'name' | 'managerUserId' | 'statusCode', string> { + return { + id: normalizeStringId(product.id || ''), + code: normalizeString(product.code), + directionCode: normalizeString(product.directionCode), + name: normalizeString(product.name), + managerUserId: normalizeNullableStringId(product.managerUserId) ?? '', + statusCode: normalizeString(product.statusCode) + }; +} + +function normalizeCurrentProject(project: BackendProjectContextProjectDTO) { + return { + id: normalizeStringId(project.id || ''), + projectCode: normalizeString(project.projectCode), + projectName: normalizeString(project.projectName), + projectType: normalizeString(project.projectType), + productId: normalizeNullableStringId(project.productId), + managerUserId: normalizeNullableStringId(project.managerUserId) ?? '', + statusCode: normalizeString(project.statusCode) + }; +} + +function normalizeCurrentRole(role: BackendObjectContextRoleDTO) { + return { + roleId: normalizeStringId(role.roleId || ''), + roleCode: normalizeString(role.roleCode), + roleName: normalizeString(role.roleName), + guestFlag: Boolean(role.guestFlag) + }; +} + +function normalizeMenu(menu: BackendObjectContextMenuDTO): App.ObjectContext.Menu { + const routeKey = normalizeString(menu.routeKey); + const routePath = normalizeRoutePath(menu.routePath || menu.path); + const key = normalizeString(menu.key || routeKey || routePath || menu.id); + + return { + key, + label: normalizeString(menu.label || menu.name), + routeKey: routeKey || null, + routePath: routePath || null, + children: menu.children?.map(child => normalizeMenu(child)) || [] + }; +} + +function getFirstNonEmptyMenuSource(data: BackendObjectContextDTO) { + const menuSources = [data.contextScopedMenus, data.menus, data.navs]; + + return menuSources.find(source => Array.isArray(source) && source.length > 0) || []; +} + +function getFirstRoutableMenu(menus: App.ObjectContext.Menu[]): App.ObjectContext.Menu | null { + for (const menu of menus) { + if (menu.routeKey || menu.routePath) { + return menu; + } + + const firstChildMenu = menu.children?.length ? getFirstRoutableMenu(menu.children) : null; + + if (firstChildMenu) { + return firstChildMenu; + } + } + + return null; +} + +function normalizeObjectSummary(data: BackendObjectContextDTO): App.ObjectContext.Summary | null { + if (data.objectSummary) { + return data.objectSummary; + } + + const summary: App.ObjectContext.Summary = {}; + + if (data.currentProduct) { + summary.currentProduct = normalizeCurrentProduct(data.currentProduct); + } + + if (data.currentProject) { + summary.currentProject = normalizeCurrentProject(data.currentProject); + } + + if (data.currentRole !== undefined) { + summary.currentRole = data.currentRole ? normalizeCurrentRole(data.currentRole) : null; + } + + return Object.keys(summary).length ? summary : null; +} + +// 待重构:拆 helper 以降低复杂度,暂以 disable 注释临时放行 +// eslint-disable-next-line complexity +export function normalizeObjectContext( + config: App.ObjectContext.DomainConfig, + objectId: string, + data: BackendObjectContextDTO +): Api.ObjectContext.ContextInfo { + const rawMenus = getFirstNonEmptyMenuSource(data); + const contextScopedMenus = rawMenus.map(menu => normalizeMenu(menu)); + const firstRoutableMenu = getFirstRoutableMenu(contextScopedMenus); + const currentProduct = data.currentProduct ? normalizeCurrentProduct(data.currentProduct) : null; + const currentProject = data.currentProject ? normalizeCurrentProject(data.currentProject) : null; + + return { + domainKey: (data.domainKey || config.domainKey) as App.ObjectContext.DomainKey, + objectType: (data.objectType || config.objectType) as App.ObjectContext.ObjectType, + objectId: normalizeString(data.objectId) || currentProduct?.id || currentProject?.id || objectId, + objectName: normalizeString(data.objectName || currentProduct?.name || currentProject?.projectName), + objectSummary: normalizeObjectSummary(data), + contextScopedMenus, + buttonCodes: data.buttonCodes ?? data.buttons ?? [], + defaultRouteKey: data.defaultRouteKey || firstRoutableMenu?.routeKey || '', + defaultRoutePath: + normalizeRoutePath(data.defaultRoutePath) || firstRoutableMenu?.routePath || config.fallbackDefaultRoutePath + }; +} diff --git a/src/service/api/object-context.ts b/src/service/api/object-context.ts index 83c06ed..4b618b3 100644 --- a/src/service/api/object-context.ts +++ b/src/service/api/object-context.ts @@ -1,145 +1,7 @@ import type { LocationQueryValue } from 'vue-router'; import { request } from '../request'; -import { - type ServiceRequestResult, - normalizeNullableStringId, - normalizeStringId, - safeJsonRequestConfig -} from './shared'; - -interface BackendObjectContextMenuDTO { - key?: string | null; - label?: string | null; - routeKey?: string | null; - routePath?: string | null; - id?: string | number | null; - name?: string | null; - path?: string | null; - children?: BackendObjectContextMenuDTO[] | null; -} - -interface BackendProductContextProductDTO { - id?: string | number | null; - code?: string | null; - directionCode?: string | null; - name?: string | null; - managerUserId?: string | number | null; - statusCode?: string | null; -} - -interface BackendProductContextRoleDTO { - roleId?: string | number | null; - roleCode?: string | null; - roleName?: string | null; -} - -interface BackendObjectContextDTO { - domainKey?: string | null; - objectType?: string | null; - objectId?: string | number | null; - objectName?: string | null; - objectSummary?: Record | null; - menus?: BackendObjectContextMenuDTO[] | null; - contextScopedMenus?: BackendObjectContextMenuDTO[] | null; - buttonCodes?: string[] | null; - currentProduct?: BackendProductContextProductDTO | null; - currentRole?: BackendProductContextRoleDTO | null; - navs?: BackendObjectContextMenuDTO[] | null; - buttons?: string[] | null; - defaultRouteKey?: string | null; - defaultRoutePath?: string | null; -} - -function normalizeString(value: string | number | null | undefined) { - if (value === null || value === undefined) { - return ''; - } - - return String(value); -} - -function normalizeRoutePath(path: string | null | undefined) { - const normalizedPath = normalizeString(path).trim(); - - if (!normalizedPath) { - return ''; - } - - if (normalizedPath.startsWith('/')) { - return normalizedPath; - } - - return `/${normalizedPath}`; -} - -function normalizeCurrentProduct( - product: BackendProductContextProductDTO -): Record<'id' | 'code' | 'directionCode' | 'name' | 'managerUserId' | 'statusCode', string> { - return { - id: normalizeStringId(product.id || ''), - code: normalizeString(product.code), - directionCode: normalizeString(product.directionCode), - name: normalizeString(product.name), - managerUserId: normalizeNullableStringId(product.managerUserId) ?? '', - statusCode: normalizeString(product.statusCode) - }; -} - -function normalizeCurrentRole(role: BackendProductContextRoleDTO) { - return { - roleId: normalizeStringId(role.roleId || ''), - roleCode: normalizeString(role.roleCode), - roleName: normalizeString(role.roleName) - }; -} - -function normalizeMenu(menu: BackendObjectContextMenuDTO): App.ObjectContext.Menu { - const routeKey = normalizeString(menu.routeKey); - const routePath = normalizeRoutePath(menu.routePath || menu.path); - const key = normalizeString(menu.key || routeKey || routePath || menu.id); - - return { - key, - label: normalizeString(menu.label || menu.name), - routeKey: routeKey || null, - routePath: routePath || null, - children: menu.children?.map(child => normalizeMenu(child)) || [] - }; -} - -function getFirstRoutableMenu(menus: App.ObjectContext.Menu[]): App.ObjectContext.Menu | null { - for (const menu of menus) { - if (menu.routeKey || menu.routePath) { - return menu; - } - - const firstChildMenu = menu.children?.length ? getFirstRoutableMenu(menu.children) : null; - - if (firstChildMenu) { - return firstChildMenu; - } - } - - return null; -} - -function normalizeObjectSummary(data: BackendObjectContextDTO): App.ObjectContext.Summary | null { - if (data.objectSummary) { - return data.objectSummary; - } - - const summary: App.ObjectContext.Summary = {}; - - if (data.currentProduct) { - summary.currentProduct = normalizeCurrentProduct(data.currentProduct); - } - - if (data.currentRole !== undefined) { - summary.currentRole = data.currentRole ? normalizeCurrentRole(data.currentRole) : null; - } - - return Object.keys(summary).length ? summary : null; -} +import { type ServiceRequestResult, safeJsonRequestConfig } from './shared'; +import { type BackendObjectContextDTO, normalizeObjectContext } from './object-context-normalize'; function createContextApiUrl(config: App.ObjectContext.DomainConfig, objectId: string) { if (config.contextApiObjectIdPlacement !== 'path') { @@ -151,30 +13,6 @@ function createContextApiUrl(config: App.ObjectContext.DomainConfig, objectId: s return config.contextApiPath.replace(placeholder, encodeURIComponent(objectId)); } -function normalizeObjectContext( - config: App.ObjectContext.DomainConfig, - objectId: string, - data: BackendObjectContextDTO -): Api.ObjectContext.ContextInfo { - const rawMenus = data.contextScopedMenus ?? data.menus ?? data.navs ?? []; - const contextScopedMenus = rawMenus.map(menu => normalizeMenu(menu)); - const firstRoutableMenu = getFirstRoutableMenu(contextScopedMenus); - const currentProduct = data.currentProduct ? normalizeCurrentProduct(data.currentProduct) : null; - - return { - domainKey: (data.domainKey || config.domainKey) as App.ObjectContext.DomainKey, - objectType: (data.objectType || config.objectType) as App.ObjectContext.ObjectType, - objectId: normalizeString(data.objectId) || currentProduct?.id || objectId, - objectName: normalizeString(data.objectName || currentProduct?.name), - objectSummary: normalizeObjectSummary(data), - contextScopedMenus, - buttonCodes: data.buttonCodes ?? data.buttons ?? [], - defaultRouteKey: data.defaultRouteKey || firstRoutableMenu?.routeKey || '', - defaultRoutePath: - normalizeRoutePath(data.defaultRoutePath) || firstRoutableMenu?.routePath || config.fallbackDefaultRoutePath - }; -} - export async function fetchGetObjectContext( config: App.ObjectContext.DomainConfig, objectId: string diff --git a/src/service/api/product.ts b/src/service/api/product.ts index 60c646e..412ca4d 100644 --- a/src/service/api/product.ts +++ b/src/service/api/product.ts @@ -106,6 +106,15 @@ export async function fetchGetProductPage(params?: Api.Product.ProductSearchPara })); } +/** 获取产品入口页概览统计 */ +export function fetchGetProductOverviewSummary() { + return request({ + ...safeJsonRequestConfig, + url: `${PRODUCT_PREFIX}/overview-summary`, + method: 'get' + }); +} + /** 鑾峰彇浜у搧璇︽儏 */ export async function fetchGetProduct(id: string) { const result = await request({ diff --git a/src/service/api/project-shared.ts b/src/service/api/project-shared.ts new file mode 100644 index 0000000..5fb3607 --- /dev/null +++ b/src/service/api/project-shared.ts @@ -0,0 +1,244 @@ +import { normalizeNullableStringId, normalizeStringId } from './shared'; + +type ProjectStatusCode = Api.Project.ProjectStatusCode; +type ProjectStatusActionCode = Exclude; + +type StringIdResponse = string | number; + +export type ProjectLocalDateValue = string | number[] | null; + +export type LifecycleActionResponse = Partial> & { + actionCode: ActionCode; +}; + +export type ProjectExecutionResponse = Omit< + Api.Project.ProjectExecution, + | 'id' + | 'projectId' + | 'projectRequirementId' + | 'ownerId' + | 'availableActions' + | 'plannedStartDate' + | 'plannedEndDate' + | 'actualStartDate' + | 'actualEndDate' + | 'progressRate' +> & { + id: StringIdResponse; + projectId: StringIdResponse; + projectRequirementId?: StringIdResponse | null; + ownerId: StringIdResponse; + availableActions?: LifecycleActionResponse[] | null; + plannedStartDate?: ProjectLocalDateValue; + plannedEndDate?: ProjectLocalDateValue; + actualStartDate?: ProjectLocalDateValue; + actualEndDate?: ProjectLocalDateValue; + progressRate?: number | null; +}; + +export type ExecutionMemberResponse = Omit & { + id: StringIdResponse; + executionId: StringIdResponse; + userId: StringIdResponse; +}; + +export type ExecutionMemberLogResponse = Omit< + Api.Project.ExecutionMemberLog, + 'id' | 'executionId' | 'userId' | 'operatorUserId' +> & { + id: StringIdResponse; + executionId: StringIdResponse; + userId: StringIdResponse; + operatorUserId: StringIdResponse; +}; + +export type ProjectTaskResponse = Omit< + Api.Project.ProjectTask, + | 'id' + | 'projectId' + | 'executionId' + | 'parentTaskId' + | 'ownerId' + | 'availableActions' + | 'plannedStartDate' + | 'plannedEndDate' + | 'actualStartDate' + | 'actualEndDate' + | 'progressRate' +> & { + id: StringIdResponse; + projectId: StringIdResponse; + executionId: StringIdResponse; + parentTaskId?: StringIdResponse | null; + ownerId: StringIdResponse; + availableActions?: LifecycleActionResponse[] | null; + plannedStartDate?: ProjectLocalDateValue; + plannedEndDate?: ProjectLocalDateValue; + actualStartDate?: ProjectLocalDateValue; + actualEndDate?: ProjectLocalDateValue; + progressRate?: number | null; +}; + +export interface ProjectMemberResponse { + id: string | number; + userId: string | number; + userNickname: string; + roleId: string | number; + roleName: string; + roleCode: string; + managerFlag: boolean; + status: 0 | 1; + joinedTime: string; + leftTime?: string | null; + remark?: string | null; +} + +const projectLifecycleActionNameMap: Record = { + pause: '暂停项目', + resume: '恢复项目', + complete: '完成项目', + cancel: '取消项目', + reopen: '重新开启', + archive: '归档项目' +}; + +const projectLifecycleActionReasonRequiredMap: Record = { + pause: true, + resume: false, + complete: true, + cancel: true, + reopen: true, + archive: false +}; + +const projectLifecycleActionMap: Record = { + pending: ['cancel'], + active: ['pause', 'complete', 'cancel'], + paused: ['resume', 'cancel'], + completed: ['reopen', 'archive'], + cancelled: [], + archived: [] +}; + +export function getProjectLifecycleActions(statusCode: ProjectStatusCode): Api.Project.ProjectLifecycleAction[] { + return projectLifecycleActionMap[statusCode].map(actionCode => ({ + actionCode, + actionName: projectLifecycleActionNameMap[actionCode], + needReason: projectLifecycleActionReasonRequiredMap[actionCode] + })); +} + +export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefined) { + if (value === null || value === undefined || value === '') { + return null; + } + + if (Array.isArray(value)) { + const [year, month, day] = value; + + if (!year || !month || !day) { + return null; + } + + return [year, month, day].map(item => String(item).padStart(2, '0')).join('-'); + } + + return String(value); +} + +export function normalizeLifecycleActions( + actions: LifecycleActionResponse[] | null | undefined +): Api.Project.LifecycleAction[] { + return (actions ?? []).map(action => ({ + actionCode: action.actionCode, + actionName: action.actionName ?? '', + needReason: Boolean(action.needReason) + })); +} + +export function normalizeProjectMember(response: ProjectMemberResponse): Api.Project.ProjectMember { + return { + id: normalizeStringId(response.id), + userId: normalizeStringId(response.userId), + userNickname: response.userNickname || '', + roleId: normalizeStringId(response.roleId), + roleName: response.roleName || '', + roleCode: response.roleCode || '', + managerFlag: Boolean(response.managerFlag), + status: response.status, + joinedTime: response.joinedTime, + leftTime: response.leftTime ?? null, + remark: response.remark ?? null + }; +} + +export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution { + return { + ...response, + id: normalizeStringId(response.id), + projectId: normalizeStringId(response.projectId), + projectRequirementId: normalizeNullableStringId(response.projectRequirementId), + ownerId: normalizeStringId(response.ownerId), + ownerNickname: response.ownerNickname ?? null, + statusName: response.statusName ?? null, + terminal: Boolean(response.terminal), + allowEdit: Boolean(response.allowEdit), + availableActions: normalizeLifecycleActions(response.availableActions), + plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), + plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), + actualStartDate: normalizeProjectLocalDate(response.actualStartDate), + actualEndDate: normalizeProjectLocalDate(response.actualEndDate), + progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, + executionDesc: response.executionDesc ?? null, + lastStatusReason: response.lastStatusReason ?? null + }; +} + +export function normalizeExecutionMember(response: ExecutionMemberResponse): Api.Project.ExecutionMember { + return { + ...response, + id: normalizeStringId(response.id), + executionId: normalizeStringId(response.executionId), + userId: normalizeStringId(response.userId), + userNickname: response.userNickname ?? null, + joinedAt: response.joinedAt ?? null, + removedAt: response.removedAt ?? null, + removedReason: response.removedReason ?? null + }; +} + +export function normalizeExecutionMemberLog(response: ExecutionMemberLogResponse): Api.Project.ExecutionMemberLog { + return { + ...response, + id: normalizeStringId(response.id), + executionId: normalizeStringId(response.executionId), + userId: normalizeStringId(response.userId), + operatorUserId: normalizeStringId(response.operatorUserId), + userNicknameSnapshot: response.userNicknameSnapshot ?? null, + operatorNicknameSnapshot: response.operatorNicknameSnapshot ?? null, + reason: response.reason ?? null + }; +} + +export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project.ProjectTask { + return { + ...response, + id: normalizeStringId(response.id), + projectId: normalizeStringId(response.projectId), + executionId: normalizeStringId(response.executionId), + parentTaskId: normalizeNullableStringId(response.parentTaskId), + ownerId: normalizeStringId(response.ownerId), + ownerNickname: response.ownerNickname ?? null, + statusName: response.statusName ?? null, + terminal: Boolean(response.terminal), + allowEdit: Boolean(response.allowEdit), + availableActions: normalizeLifecycleActions(response.availableActions), + progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, + plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), + plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), + actualStartDate: normalizeProjectLocalDate(response.actualStartDate), + actualEndDate: normalizeProjectLocalDate(response.actualEndDate), + taskDesc: response.taskDesc ?? null, + lastStatusReason: response.lastStatusReason ?? null + }; +} diff --git a/src/service/api/project.ts b/src/service/api/project.ts new file mode 100644 index 0000000..76fd0db --- /dev/null +++ b/src/service/api/project.ts @@ -0,0 +1,532 @@ +import { WEB_SERVICE_PREFIX } from '@/constants/service'; +import { request } from '../request'; +import { + type ServiceRequestResult, + mapServiceResult, + normalizeNullableStringId, + normalizeStringId, + safeJsonRequestConfig +} from './shared'; +import { + type ExecutionMemberLogResponse, + type ExecutionMemberResponse, + type ProjectExecutionResponse, + type ProjectLocalDateValue, + type ProjectMemberResponse, + type ProjectTaskResponse, + getProjectLifecycleActions, + normalizeExecutionMember, + normalizeExecutionMemberLog, + normalizeProjectExecution, + normalizeProjectLocalDate, + normalizeProjectMember, + normalizeProjectTask +} from './project-shared'; + +const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`; + +type ProjectResponse = Omit< + Api.Project.Project, + 'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate' +> & { + id: string | number; + managerUserId?: string | number | null; + productId?: string | number | null; + plannedStartDate?: ProjectLocalDateValue; + plannedEndDate?: ProjectLocalDateValue; + actualStartDate?: ProjectLocalDateValue; + actualEndDate?: ProjectLocalDateValue; +}; + +type ProjectPageResponse = Api.Project.PageResult; +type ProjectExecutionPageResponse = Api.Project.PageResult; +type ProjectTaskPageResponse = Api.Project.PageResult; +type StatusBoardResponse = Api.Project.StatusBoard; + +type ProjectContextResponse = Omit & { + currentProject: Omit & { id: string | number }; + navs: Array & { id: string | number }>; +}; + +function getExecutionPrefix(projectId: string) { + return `${PROJECT_PREFIX}/${projectId}/executions`; +} + +function getTaskPrefix(projectId: string, executionId: string) { + return `${getExecutionPrefix(projectId)}/${executionId}/tasks`; +} + +/** 归一化项目数据 */ +function normalizeProject(project: ProjectResponse): Api.Project.Project { + return { + ...project, + id: normalizeStringId(project.id), + managerUserId: normalizeNullableStringId(project.managerUserId) ?? '', + productId: normalizeNullableStringId(project.productId), + plannedStartDate: normalizeProjectLocalDate(project.plannedStartDate), + plannedEndDate: normalizeProjectLocalDate(project.plannedEndDate), + actualStartDate: normalizeProjectLocalDate(project.actualStartDate), + actualEndDate: normalizeProjectLocalDate(project.actualEndDate) + }; +} + +/** 将项目详情组装为设置页数据 */ +function createProjectSettings(project: Api.Project.Project): Api.Project.ProjectSettings { + return { + baseInfo: { + id: project.id, + projectCode: project.projectCode, + projectName: project.projectName, + directionCode: project.directionCode, + projectType: project.projectType, + productId: project.productId, + productName: project.productName ?? null, + managerUserId: project.managerUserId, + managerUserNickname: project.managerUserNickname ?? null, + statusCode: project.statusCode, + plannedStartDate: project.plannedStartDate, + plannedEndDate: project.plannedEndDate, + actualStartDate: project.actualStartDate, + actualEndDate: project.actualEndDate, + projectDesc: project.projectDesc, + lastStatusReason: project.lastStatusReason + }, + lifecycle: { + statusCode: project.statusCode, + lastStatusReason: project.lastStatusReason, + availableActions: getProjectLifecycleActions(project.statusCode) + } + }; +} + +/** 获取项目分页 */ +export async function fetchGetProjectPage(params?: Api.Project.ProjectSearchParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeProject) + })); +} + +/** 获取项目入口页概览统计 */ +export function fetchGetProjectOverviewSummary() { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/overview-summary`, + method: 'get' + }); +} + +/** 获取项目详情 */ +export async function fetchGetProject(id: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/get`, + method: 'get', + params: { id } + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeProject); +} + +/** 创建项目 */ +export async function fetchCreateProject(data: Api.Project.SaveProjectParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/create`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目 */ +export function fetchUpdateProject(data: Api.Project.UpdateProjectParams) { + return request({ + url: `${PROJECT_PREFIX}/update`, + method: 'put', + data + }); +} + +/** 变更项目状态 */ +export function fetchChangeProjectStatus(data: Api.Project.ChangeProjectStatusParams) { + return request({ + url: `${PROJECT_PREFIX}/change-status`, + method: 'post', + data + }); +} + +/** 删除项目 */ +export function fetchDeleteProject(data: Api.Project.DeleteProjectParams) { + return request({ + url: `${PROJECT_PREFIX}/delete`, + method: 'post', + data + }); +} + +/** 获取项目上下文 */ +export async function fetchGetProjectContext(id: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/context`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + currentProject: { + ...data.currentProject, + id: normalizeStringId(data.currentProject.id) + }, + navs: data.navs.map(nav => ({ + ...nav, + id: normalizeStringId(nav.id) + })) + })); +} + +/** 获取项目成员列表 */ +export async function fetchGetProjectMembers(id: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => + data.map(normalizeProjectMember) + ); +} + +/** 创建项目成员 */ +export async function fetchCreateProjectMember(id: string, data: Api.Project.CreateProjectMemberParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目成员 */ +export function fetchUpdateProjectMember(id: string, memberId: string, data: Api.Project.UpdateProjectMemberParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members/${memberId}`, + method: 'put', + data + }); +} + +/** 移出项目成员 */ +export function fetchInactiveProjectMember( + id: string, + memberId: string, + data: Api.Project.InactiveProjectMemberParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/${id}/members/${memberId}/inactive`, + method: 'post', + data + }); +} + +/** 获取项目设置 */ +export async function fetchGetProjectSettings(id: string) { + const result = await fetchGetProject(id); + + if (result.error || !result.data) { + return result as ServiceRequestResult; + } + + return { + ...result, + data: createProjectSettings(result.data) + }; +} + +/** 更新项目设置基础信息 */ +export async function fetchUpdateProjectSettingBaseInfo( + id: string, + data: Api.Project.UpdateProjectSettingBaseInfoParams +) { + const detailResult = await fetchGetProject(id); + + if (detailResult.error || !detailResult.data) { + return detailResult as ServiceRequestResult; + } + + return fetchUpdateProject({ + id, + projectCode: detailResult.data.projectCode, + projectName: data.projectName, + directionCode: data.directionCode, + projectType: data.projectType, + productId: detailResult.data.productId, + managerUserId: detailResult.data.managerUserId, + plannedStartDate: data.plannedStartDate, + plannedEndDate: data.plannedEndDate, + actualStartDate: detailResult.data.actualStartDate, + actualEndDate: detailResult.data.actualEndDate, + projectDesc: data.projectDesc + }); +} + +/** 获取项目执行分页 */ +export async function fetchGetProjectExecutionPage( + projectId: string, + params?: Api.Project.ProjectExecutionSearchParams +) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeProjectExecution) + })); +} + +/** 获取项目执行状态看板 */ +export function fetchGetProjectExecutionStatusBoard( + projectId: string, + params?: Api.Project.ProjectExecutionStatusBoardParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/status-board`, + method: 'get', + params + }); +} + +/** 获取项目执行详情 */ +export async function fetchGetProjectExecution(projectId: string, executionId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeProjectExecution); +} + +/** 创建项目执行 */ +export async function fetchCreateProjectExecution(projectId: string, data: Api.Project.SaveProjectExecutionParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: getExecutionPrefix(projectId), + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目执行 */ +export function fetchUpdateProjectExecution( + projectId: string, + executionId: string, + data: Api.Project.SaveProjectExecutionParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}`, + method: 'put', + data + }); +} + +/** 变更项目执行负责人 */ +export function fetchChangeProjectExecutionOwner( + projectId: string, + executionId: string, + data: Api.Project.ChangeExecutionOwnerParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/change-owner`, + method: 'post', + data + }); +} + +/** 变更项目执行状态 */ +export function fetchChangeProjectExecutionStatus( + projectId: string, + executionId: string, + data: Api.Project.ChangeExecutionStatusParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/change-status`, + method: 'post', + data + }); +} + +/** 获取项目执行成员 */ +export async function fetchGetProjectExecutionMembers(projectId: string, executionId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/members`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => + data.map(normalizeExecutionMember) + ); +} + +/** 创建项目执行成员 */ +export async function fetchCreateProjectExecutionMember( + projectId: string, + executionId: string, + data: Api.Project.CreateExecutionMemberParams +) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/members`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 移除项目执行成员 */ +export function fetchInactiveProjectExecutionMember( + projectId: string, + executionId: string, + payload: { memberId: string; data: Api.Project.InactiveExecutionMemberParams } +) { + return request({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/members/${payload.memberId}/inactive`, + method: 'post', + data: payload.data + }); +} + +/** 获取项目执行成员变更历史分页 */ +export async function fetchGetProjectExecutionMemberLogPage( + projectId: string, + executionId: string, + params?: Api.Project.ExecutionMemberLogSearchParams +) { + const result = await request>({ + ...safeJsonRequestConfig, + url: `${getExecutionPrefix(projectId)}/${executionId}/member-logs`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult>, data => ({ + ...data, + list: data.list.map(normalizeExecutionMemberLog) + })); +} + +/** 获取项目任务分页 */ +export async function fetchGetProjectTaskPage( + projectId: string, + executionId: string, + params?: Api.Project.ProjectTaskSearchParams +) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getTaskPrefix(projectId, executionId)}/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeProjectTask) + })); +} + +/** 获取项目任务状态看板 */ +export function fetchGetProjectTaskStatusBoard( + projectId: string, + executionId: string, + params?: Api.Project.ProjectTaskStatusBoardParams +) { + return request({ + ...safeJsonRequestConfig, + url: `${getTaskPrefix(projectId, executionId)}/status-board`, + method: 'get', + params + }); +} + +/** 获取项目任务详情 */ +export async function fetchGetProjectTask(projectId: string, executionId: string, taskId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${getTaskPrefix(projectId, executionId)}/${taskId}`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeProjectTask); +} + +/** 创建项目任务 */ +export async function fetchCreateProjectTask( + projectId: string, + executionId: string, + data: Api.Project.SaveProjectTaskParams +) { + const result = await request({ + ...safeJsonRequestConfig, + url: getTaskPrefix(projectId, executionId), + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目任务 */ +export function fetchUpdateProjectTask( + projectId: string, + executionId: string, + payload: { taskId: string; data: Api.Project.SaveProjectTaskParams } +) { + return request({ + ...safeJsonRequestConfig, + url: `${getTaskPrefix(projectId, executionId)}/${payload.taskId}`, + method: 'put', + data: payload.data + }); +} + +/** 变更项目任务状态 */ +export function fetchChangeProjectTaskStatus( + projectId: string, + executionId: string, + payload: { taskId: string; data: Api.Project.ChangeTaskStatusParams } +) { + return request({ + ...safeJsonRequestConfig, + url: `${getTaskPrefix(projectId, executionId)}/${payload.taskId}/change-status`, + method: 'post', + data: payload.data + }); +} diff --git a/src/service/api/route.ts b/src/service/api/route.ts index 82ef99b..d1f7f40 100644 --- a/src/service/api/route.ts +++ b/src/service/api/route.ts @@ -95,31 +95,78 @@ function replaceWithStaticObjectContextDomainRoute(routes: Api.Route.MenuRoute[] return; } - const wrappedDomainRoute = cloneStaticRouteAsMenuRoute(staticDomainRoute, `object-context:${config.domainKey}`); - const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id); - const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id)); + // Create a map of backend routes by name for quick lookup + const backendRouteMap = new Map(); + domainTopLevelRoutes.forEach(route => { + if (route.name) { + backendRouteMap.set(String(route.name), route); + } + }); - if (entryRoute.meta) { - const nextMeta: RouteMeta = { - title: wrappedDomainRoute.meta?.title || config.domainKey, - ...(wrappedDomainRoute.meta || {}) + // Clone static route but preserve backend route's meta for children + // 待重构:拆 helper 以降低复杂度,暂以 disable 注释临时放行 + // eslint-disable-next-line complexity + function cloneStaticRoutePreservingBackendMeta(route: ElegantConstRoute, idPrefix: string): Api.Route.MenuRoute { + const backendRoute = route.name ? backendRouteMap.get(String(route.name)) : undefined; + const { children: _children, ...routeWithoutChildren } = route; + const baseRoute: Api.Route.MenuRoute = { + ...routeWithoutChildren, + id: `${idPrefix}:${String(route.name || route.path)}` }; - if (entryRoute.meta.icon) { - nextMeta.icon = entryRoute.meta.icon; + // If there's a backend route, preserve its meta + if (backendRoute?.meta) { + baseRoute.meta = { + ...baseRoute.meta, + title: backendRoute.meta.title || baseRoute.meta?.title || String(route.name || route.path), + icon: backendRoute.meta.icon || baseRoute.meta?.icon, + localIcon: backendRoute.meta.localIcon || baseRoute.meta?.localIcon, + order: + backendRoute.meta.order !== undefined && backendRoute.meta.order !== null + ? backendRoute.meta.order + : baseRoute.meta?.order, + keepAlive: + backendRoute.meta.keepAlive !== undefined && backendRoute.meta.keepAlive !== null + ? backendRoute.meta.keepAlive + : baseRoute.meta?.keepAlive, + i18nKey: backendRoute.meta.i18nKey || baseRoute.meta?.i18nKey + }; } - if (entryRoute.meta.localIcon) { - nextMeta.localIcon = entryRoute.meta.localIcon; + // Recursively process children + if (route.children?.length) { + baseRoute.children = route.children.map(child => cloneStaticRoutePreservingBackendMeta(child, idPrefix)); } - if (entryRoute.meta.order !== undefined) { - nextMeta.order = entryRoute.meta.order; - } - - wrappedDomainRoute.meta = nextMeta; + return baseRoute; } + const wrappedDomainRoute = cloneStaticRoutePreservingBackendMeta( + staticDomainRoute, + `object-context:${config.domainKey}` + ); + + // Merge entry route's meta to domain route + if (entryRoute.meta) { + wrappedDomainRoute.meta = { + ...wrappedDomainRoute.meta, + title: entryRoute.meta.title || wrappedDomainRoute.meta?.title || config.domainKey, + icon: entryRoute.meta.icon || wrappedDomainRoute.meta?.icon, + localIcon: entryRoute.meta.localIcon || wrappedDomainRoute.meta?.localIcon, + order: + entryRoute.meta.order !== undefined && entryRoute.meta.order !== null + ? entryRoute.meta.order + : wrappedDomainRoute.meta?.order, + keepAlive: + entryRoute.meta.keepAlive !== undefined && entryRoute.meta.keepAlive !== null + ? entryRoute.meta.keepAlive + : wrappedDomainRoute.meta?.keepAlive + }; + } + + const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id); + const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id)); + normalizedRoutes = normalizedRoutes.filter(route => !domainRouteIds.has(route.id)); normalizedRoutes.splice(entryRouteIndex < 0 ? normalizedRoutes.length : entryRouteIndex, 0, wrappedDomainRoute); }); diff --git a/src/service/api/system-manage.ts b/src/service/api/system-manage.ts index f2b4fec..66f464f 100644 --- a/src/service/api/system-manage.ts +++ b/src/service/api/system-manage.ts @@ -74,6 +74,7 @@ function createBatchDeleteQuery(ids: Array) { type UserSimpleResponse = Omit & { id: string | number; + deptId?: string | number | null; }; type RoleResponse = Omit & { @@ -120,7 +121,8 @@ type UserManagementRelationTreeResponse = Omit< function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple { return { ...user, - id: normalizeStringId(user.id) + id: normalizeStringId(user.id), + deptId: normalizeNullableStringId(user.deptId) }; } diff --git a/src/store/modules/route/shared.ts b/src/store/modules/route/shared.ts index 48eeae3..45aded0 100644 --- a/src/store/modules/route/shared.ts +++ b/src/store/modules/route/shared.ts @@ -153,7 +153,12 @@ export function getCacheRouteNames(routes: RouteRecordRaw[]) { const cacheNames: LastLevelRouteKey[] = []; routes.forEach(route => { - // only get last two level route, which has component + // Check first-level routes (routes with component but no children) + if (route.component && route.meta?.keepAlive && !route.children?.length) { + cacheNames.push(route.name as LastLevelRouteKey); + } + + // Check second-level routes route.children?.forEach(child => { if (child.component && child.meta?.keepAlive) { cacheNames.push(child.name as LastLevelRouteKey); diff --git a/src/styles/scss/element-plus.scss b/src/styles/scss/element-plus.scss index 0c1ee32..0bd3802 100644 --- a/src/styles/scss/element-plus.scss +++ b/src/styles/scss/element-plus.scss @@ -428,6 +428,18 @@ html .el-collapse { margin-left: 0 !important; } +.business-table-card-body { + display: flex; + height: calc(100% - 56px); + min-height: 0; + flex: 1; + flex-direction: column; + + > .flex-1 { + min-height: 0; + } +} + .el-card { display: flex; flex-direction: column; @@ -484,3 +496,44 @@ html .el-collapse { border-radius: $radius; } } + +.el-message { + min-width: 280px; + padding: 12px 18px; + border: none; + border-radius: $radius; + box-shadow: 0 6px 16px rgb(0 0 0 / 15%); + + .el-message__content { + color: #fff; + font-weight: 500; + } + + .el-icon { + color: #fff; + } + + .el-message__closeBtn { + color: rgb(255 255 255 / 80%); + } + + .el-message__closeBtn:hover { + color: #fff; + } + + &--success { + background-color: var(--el-color-success); + } + + &--info { + background-color: var(--el-color-info); + } + + &--warning { + background-color: var(--el-color-warning); + } + + &--error { + background-color: var(--el-color-danger); + } +} diff --git a/src/typings/api/product.d.ts b/src/typings/api/product.d.ts index 672032d..9d59d22 100644 --- a/src/typings/api/product.d.ts +++ b/src/typings/api/product.d.ts @@ -21,6 +21,12 @@ declare namespace Api { list: T[]; } + /** 产品入口页概览统计 */ + interface ProductOverviewSummary { + /** 产品状态数量映射,key 为后端状态编码 */ + statusCounts: Record; + } + interface Product { /** 产品 ID */ id: string; diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts new file mode 100644 index 0000000..0323a9d --- /dev/null +++ b/src/typings/api/project.d.ts @@ -0,0 +1,450 @@ +declare namespace Api { + /** + * namespace Project + * + * backend api module: "project/project" + */ + namespace Project { + /** 项目状态编码 */ + type ProjectStatusCode = 'pending' | 'active' | 'paused' | 'completed' | 'cancelled' | 'archived'; + + /** 项目状态动作编码 */ + type ProjectStatusActionCode = 'auto_start' | 'pause' | 'resume' | 'complete' | 'cancel' | 'reopen' | 'archive'; + + /** 项目设置基础信息 */ + interface ProjectSettingBaseInfo { + /** 项目 ID */ + id: string; + /** 项目编码 */ + projectCode: string; + /** 项目名称 */ + projectName: string; + /** 项目方向字典值 */ + directionCode: string; + /** 项目类型字典值 */ + projectType: string; + /** 所属产品 ID */ + productId: string | null; + /** 所属产品名称 */ + productName: string | null; + /** 项目负责人用户昵称 */ + managerUserNickname: string | null; + /** 项目负责人用户 ID */ + managerUserId: string | null; + /** 项目状态编码 */ + statusCode: ProjectStatusCode; + /** 计划开始日期 */ + plannedStartDate: string | null; + /** 计划结束日期 */ + plannedEndDate: string | null; + /** 实际开始日期 */ + actualStartDate: string | null; + /** 实际结束日期 */ + actualEndDate: string | null; + /** 项目说明 */ + projectDesc: string | null; + /** 最近一次状态动作原因 */ + lastStatusReason: string | null; + } + + /** 项目生命周期动作 */ + interface ProjectLifecycleAction { + actionCode: Exclude; + actionName: string; + needReason: boolean; + } + + /** 项目生命周期信息 */ + interface ProjectLifecycleInfo { + statusCode: ProjectStatusCode; + lastStatusReason: string | null; + availableActions: ProjectLifecycleAction[]; + } + + /** 执行状态编码 */ + type ProjectExecutionStatusCode = 'pending' | 'active' | 'paused' | 'completed' | 'cancelled'; + + /** 执行动作编码 */ + type ProjectExecutionActionCode = 'start' | 'pause' | 'resume' | 'cancel'; + + /** 任务状态编码 */ + type ProjectTaskStatusCode = 'pending' | 'active' | 'blocked' | 'completed' | 'cancelled'; + + /** 任务动作编码 */ + type ProjectTaskActionCode = 'start' | 'block' | 'resume' | 'complete' | 'cancel'; + + interface LifecycleAction { + actionCode: ActionCode; + actionName: string; + needReason: boolean; + } + + interface StatusBoardItem { + statusCode: string; + statusName: string; + count: number; + sort: number; + terminal?: boolean; + } + + interface StatusBoard { + total: number; + items: StatusBoardItem[]; + } + + interface ProjectExecution { + id: string; + projectId: string; + projectRequirementId: string | null; + executionName: string; + executionType: string | null; + ownerId: string; + ownerNickname?: string | null; + statusCode: ProjectExecutionStatusCode; + statusName: string | null; + terminal: boolean; + allowEdit: boolean; + availableActions: LifecycleAction[]; + plannedStartDate: string | null; + plannedEndDate: string | null; + actualStartDate: string | null; + actualEndDate: string | null; + progressRate: number; + executionDesc: string | null; + lastStatusReason: string | null; + createTime: string; + updateTime: string; + } + + interface ExecutionMember { + id: string; + executionId: string; + userId: string; + userNickname?: string | null; + joinedAt: string | null; + removedAt: string | null; + removedReason: string | null; + } + + /** 执行成员变更事件类型 */ + type ExecutionMemberActionType = 'join' | 'inactive' | 'owner_transfer_in' | 'owner_transfer_out'; + + /** 执行成员变更历史 */ + interface ExecutionMemberLog { + id: string; + executionId: string; + actionType: ExecutionMemberActionType; + userId: string; + userNicknameSnapshot: string | null; + operatorUserId: string; + operatorNicknameSnapshot: string | null; + actionTime: string; + reason: string | null; + } + + type ExecutionMemberLogSearchParams = CommonType.RecordNullable< + Pick & { + actionTypes: ExecutionMemberActionType[]; + userId: string; + startTime: string; + endTime: string; + } + >; + + interface ProjectTask { + id: string; + projectId: string; + executionId: string; + parentTaskId: string | null; + taskTitle: string; + ownerId: string; + ownerNickname?: string | null; + statusCode: ProjectTaskStatusCode; + statusName: string | null; + terminal: boolean; + allowEdit: boolean; + availableActions: LifecycleAction[]; + progressRate: number; + plannedStartDate: string | null; + plannedEndDate: string | null; + actualStartDate: string | null; + actualEndDate: string | null; + taskDesc: string | null; + lastStatusReason: string | null; + createTime: string; + updateTime: string; + } + + type ProjectExecutionSearchParams = CommonType.RecordNullable< + Pick & { + keyword: string; + executionType: string; + ownerId: string; + statusCode: string; + updateTime: string[]; + } + >; + + type ProjectExecutionStatusBoardParams = CommonType.RecordNullable<{ + keyword: string; + executionType: string; + ownerId: string; + updateTime: string[]; + }>; + + interface SaveProjectExecutionParams { + executionName: string; + executionType: string; + ownerId: string; + projectRequirementId: string | null; + plannedStartDate: string | null; + plannedEndDate: string | null; + executionDesc: string | null; + memberUserIds?: string[]; + } + + interface ChangeExecutionOwnerParams { + newOwnerId: string; + reason: string | null; + } + + interface ChangeExecutionStatusParams { + actionCode: ProjectExecutionActionCode; + reason: string | null; + } + + interface CreateExecutionMemberParams { + userId: string; + } + + interface InactiveExecutionMemberParams { + reason: string; + } + + type ProjectTaskSearchParams = CommonType.RecordNullable< + Pick & { + keyword: string; + parentTaskId: string; + ownerId: string; + statusCode: string; + updateTime: string[]; + } + >; + + type ProjectTaskStatusBoardParams = CommonType.RecordNullable<{ + keyword: string; + parentTaskId: string; + ownerId: string; + updateTime: string[]; + }>; + + interface SaveProjectTaskParams { + parentTaskId: string | null; + taskTitle: string; + ownerId: string | null; + progressRate?: number; + plannedStartDate: string | null; + plannedEndDate: string | null; + taskDesc: string | null; + /** 仅创建任务时生效,编辑接口静默忽略;userId 必须是当前有效执行成员且不能等于 ownerId */ + assigneeUserIds?: string[]; + } + + interface ChangeTaskStatusParams { + actionCode: ProjectTaskActionCode; + reason: string | null; + } + + /** 项目设置参数 */ + interface ProjectSettings { + baseInfo: ProjectSettingBaseInfo; + lifecycle: ProjectLifecycleInfo; + } + + /** 项目设置基础信息参数 */ + interface UpdateProjectSettingBaseInfoParams { + projectName: string; + directionCode: string; + projectType: string; + plannedStartDate: string | null; + plannedEndDate: string | null; + projectDesc: string | null; + } + + /** 项目成员状态 */ + type ProjectMemberStatus = 0 | 1; + + interface PageParams { + pageNo: number; + pageSize: number; + } + + interface PageResult { + total: number; + list: T[]; + } + + /** 项目入口页概览统计 */ + interface ProjectOverviewSummary { + /** 项目状态数量映射,key 为后端状态编码 */ + statusCounts: Record; + } + + interface Project { + /** 项目 ID */ + id: string; + /** 项目编码 */ + projectCode: string; + /** 项目名称 */ + projectName: string; + /** 项目方向字典值 */ + directionCode: string; + /** 项目类型字典值 */ + projectType: string; + /** 所属产品 ID */ + productId: string | null; + /** 所属产品名称 */ + productName?: string | null; + /** 项目负责人用户 ID */ + managerUserId: string; + /** 项目负责人用户昵称 */ + managerUserNickname?: string | null; + /** 项目状态编码 */ + statusCode: ProjectStatusCode; + /** 计划开始日期 */ + plannedStartDate: string | null; + /** 计划结束日期 */ + plannedEndDate: string | null; + /** 实际开始日期 */ + actualStartDate: string | null; + /** 实际结束日期 */ + actualEndDate: string | null; + /** 进度百分比 */ + progressRate: number; + /** 项目说明 */ + projectDesc: string | null; + /** 最近一次状态动作原因 */ + lastStatusReason: string | null; + /** 创建时间 */ + createTime: string; + /** 更新时间 */ + updateTime: string; + } + + interface ProjectContext { + currentProject: { + id: string; + projectCode: string; + projectName: string; + projectType: string; + productId: string | null; + managerUserId: string; + statusCode: ProjectStatusCode; + }; + currentRole: { + roleId: string | null; + roleCode: string | null; + roleName: string | null; + guestFlag: boolean; + }; + navs: Array<{ + id: string; + name: string; + path: string; + icon: string; + sort: number; + }>; + buttons: string[]; + } + + interface ProjectMember { + /** 成员关系 ID */ + id: string; + /** 用户 ID */ + userId: string; + /** 用户昵称 */ + userNickname: string; + /** 角色 ID */ + roleId: string; + /** 角色名称 */ + roleName: string; + /** 角色编码 */ + roleCode: string; + /** 是否项目负责人 */ + managerFlag: boolean; + /** 成员状态 */ + status: ProjectMemberStatus; + /** 加入时间 */ + joinedTime: string; + /** 退出时间 */ + leftTime: string | null; + /** 备注 */ + remark: string | null; + } + + /** 项目搜索参数 */ + type ProjectSearchParams = CommonType.RecordNullable< + Pick & { + keyword: string; + directionCode: string; + projectType: string; + productId: string; + managerUserId: string; + statusCode: ProjectStatusCode; + updateTime: string[]; + } + >; + + /** 创建/保存项目参数 */ + type SaveProjectParams = Pick & { + projectCode: string | null; + productId: string | null; + managerUserId: string; + plannedStartDate: string | null; + plannedEndDate: string | null; + actualStartDate?: string | null; + actualEndDate?: string | null; + }; + + /** 更新项目参数 */ + type UpdateProjectParams = { id: string } & SaveProjectParams; + + /** 变更项目状态参数 */ + interface ChangeProjectStatusParams { + id: string; + actionCode: ProjectStatusActionCode; + reason: string | null; + } + + /** 删除项目参数 */ + interface DeleteProjectParams { + id: string; + projectName: string; + confirmText: string; + reason: string; + } + + /** 创建项目成员参数 */ + interface CreateProjectMemberParams { + userId: string; + roleId: string; + remark: string | null; + previousManagerUserId?: string | null; + previousManagerRoleId?: string | null; + } + + /** 更新项目成员参数 */ + interface UpdateProjectMemberParams { + roleId: string; + reason: string | null; + remark: string | null; + previousManagerUserId?: string | null; + previousManagerRoleId?: string | null; + } + + /** 移出项目成员参数 */ + interface InactiveProjectMemberParams { + reason: string | null; + } + } +} diff --git a/src/typings/api/system-manage.d.ts b/src/typings/api/system-manage.d.ts index f3b8dfd..313719b 100644 --- a/src/typings/api/system-manage.d.ts +++ b/src/typings/api/system-manage.d.ts @@ -428,6 +428,12 @@ declare namespace Api { id: string; /** 用户昵称 */ nickname: string; + /** 用户账号 */ + username?: string | null; + /** 部门 ID */ + deptId?: string | null; + /** 部门名称 */ + deptName?: string | null; } } } diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index a7280f6..dbdb227 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -14,6 +14,10 @@ declare module 'vue' { BusinessFormDialog: typeof import('./../components/custom/business-form-dialog.vue')['default'] BusinessFormDrawer: typeof import('./../components/custom/business-form-drawer.vue')['default'] BusinessFormSection: typeof import('./../components/custom/business-form-section.vue')['default'] + BusinessFormSimpleDialog: typeof import('./../components/custom/business-form-simple-dialog.vue')['default'] + BusinessRichTextEditor: typeof import('./../components/custom/business-rich-text-editor.vue')['default'] + BusinessRichTextView: typeof import('./../components/custom/business-rich-text-view.vue')['default'] + BusinessUserSelect: typeof import('./../components/custom/business-user-select.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] CountTo: typeof import('./../components/custom/count-to.vue')['default'] CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default'] @@ -58,6 +62,7 @@ declare module 'vue' { ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] ElPopover: typeof import('element-plus/es')['ElPopover'] + ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] @@ -65,6 +70,7 @@ declare module 'vue' { ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSpace: typeof import('element-plus/es')['ElSpace'] ElStatistic: typeof import('element-plus/es')['ElStatistic'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] @@ -115,6 +121,7 @@ declare module 'vue' { IconLocalLogo: typeof import('~icons/local/logo')['default'] 'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default'] IconMaterialSymbolsLightCheckCircleRounded: typeof import('~icons/material-symbols-light/check-circle-rounded')['default'] + 'IconMdi:paperclip': typeof import('~icons/mdi/paperclip')['default'] 'IconMdi:printer': typeof import('~icons/mdi/printer')['default'] IconMdiAccountTieOutline: typeof import('~icons/mdi/account-tie-outline')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] @@ -124,6 +131,7 @@ declare module 'vue' { IconMdiChevronDoubleUp: typeof import('~icons/mdi/chevron-double-up')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] + IconMdiCloseCircle: typeof import('~icons/mdi/close-circle')['default'] IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] IconMdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default'] IconMdiDrag: typeof import('~icons/mdi/drag')['default'] @@ -154,6 +162,7 @@ declare module 'vue' { SystemLogo: typeof import('./../components/common/system-logo.vue')['default'] TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] + TableSearchFields: typeof import('./../components/custom/table-search-fields.vue')['default'] TableSearchPanel: typeof import('./../components/custom/table-search-panel.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] diff --git a/src/typings/elegant-router.d.ts b/src/typings/elegant-router.d.ts index ceea164..8755cde 100644 --- a/src/typings/elegant-router.d.ts +++ b/src/typings/elegant-router.d.ts @@ -65,6 +65,13 @@ declare module "@elegant-router/types" { "product_list": "/product/list"; "product_requirement": "/product/requirement"; "product_setting": "/product/setting"; + "project": "/project"; + "project_list": "/project/list"; + "project_project": "/project/project"; + "project_project_execution": "/project/project/execution"; + "project_project_overview": "/project/project/overview"; + "project_project_requirement": "/project/project/requirement"; + "project_project_setting": "/project/project/setting"; "system": "/system"; "system_dict": "/system/dict"; "system_menu": "/system/menu"; @@ -117,6 +124,7 @@ declare module "@elegant-router/types" { | "login" | "plugin" | "product" + | "project" | "system" | "user-center" >; @@ -172,6 +180,11 @@ declare module "@elegant-router/types" { | "product_list" | "product_requirement" | "product_setting" + | "project_list" + | "project_project_execution" + | "project_project_overview" + | "project_project_requirement" + | "project_project_setting" | "system_dict" | "system_menu" | "system_post" diff --git a/src/typings/wangeditor.d.ts b/src/typings/wangeditor.d.ts new file mode 100644 index 0000000..f109fb7 --- /dev/null +++ b/src/typings/wangeditor.d.ts @@ -0,0 +1,18 @@ +declare module '@wangeditor/editor-for-vue' { + import type { DefineComponent } from 'vue'; + import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; + + export const Editor: DefineComponent<{ + modelValue?: string | null; + defaultConfig?: Partial; + defaultContent?: unknown[]; + defaultHtml?: string; + mode?: 'default' | 'simple'; + }>; + + export const Toolbar: DefineComponent<{ + editor: IDomEditor | null | undefined; + defaultConfig?: Partial; + mode?: 'default' | 'simple'; + }>; +} diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts new file mode 100644 index 0000000..52aa8af --- /dev/null +++ b/src/utils/sanitize.ts @@ -0,0 +1,64 @@ +import DOMPurify from 'dompurify'; + +const ALLOWED_TAGS = [ + 'a', + 'p', + 'br', + 'span', + 'div', + 'b', + 'i', + 'u', + 's', + 'sub', + 'sup', + 'strong', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ul', + 'ol', + 'li', + 'blockquote', + 'pre', + 'code', + 'hr', + 'img', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td' +]; + +const ALLOWED_ATTR = [ + 'href', + 'target', + 'rel', + 'src', + 'alt', + 'title', + 'class', + 'style', + 'colspan', + 'rowspan', + 'width', + 'height' +]; + +export function sanitizeHtml(html: string | null | undefined): string { + if (!html) { + return ''; + } + + return DOMPurify.sanitize(html, { + ALLOWED_TAGS, + ALLOWED_ATTR, + ADD_ATTR: ['target'] + }); +} diff --git a/src/views/plugin/editor/quill/index.vue b/src/views/plugin/editor/quill/index.vue index 8be98fd..4ce81d3 100644 --- a/src/views/plugin/editor/quill/index.vue +++ b/src/views/plugin/editor/quill/index.vue @@ -1,47 +1,19 @@ - - diff --git a/src/views/product/dashboard/modules/product-activity-timeline-dialog.vue b/src/views/product/dashboard/modules/product-activity-timeline-dialog.vue index 90aeb13..5473de2 100644 --- a/src/views/product/dashboard/modules/product-activity-timeline-dialog.vue +++ b/src/views/product/dashboard/modules/product-activity-timeline-dialog.vue @@ -247,7 +247,12 @@ watch([() => visible.value, () => props.productId], ([currentVisible, productId]

- {{ item.compactText }} + + + ,状态:{{ item.statusTransition }} ,原因:{{ item.reasonText }}

@@ -497,6 +502,11 @@ watch([() => visible.value, () => props.productId], ([currentVisible, productId] color: var(--el-text-color-primary); } +.product-activity-dialog__subject { + color: var(--el-text-color-primary); + font-weight: 700; +} + .product-activity-dialog__footer-inner { display: flex; align-items: center; diff --git a/src/views/product/dashboard/modules/product-activity-timeline-panel.vue b/src/views/product/dashboard/modules/product-activity-timeline-panel.vue index c38e7ce..5776846 100644 --- a/src/views/product/dashboard/modules/product-activity-timeline-panel.vue +++ b/src/views/product/dashboard/modules/product-activity-timeline-panel.vue @@ -112,7 +112,12 @@ watch(

- {{ item.compactText }} + + + ,状态:{{ item.statusTransition }} ,原因:{{ item.reasonText }}

@@ -262,6 +267,11 @@ watch( color: rgb(15 23 42 / 98%); } +.product-activity-panel__subject { + color: rgb(15 23 42 / 98%); + font-weight: 700; +} + @media (width <= 768px) { .product-activity-panel__body { min-height: auto; diff --git a/src/views/product/dashboard/product-activity.ts b/src/views/product/dashboard/product-activity.ts index ebf4f2c..cadc3a3 100644 --- a/src/views/product/dashboard/product-activity.ts +++ b/src/views/product/dashboard/product-activity.ts @@ -17,13 +17,20 @@ export type ProductActivityFilterType = 'all' | Api.Product.ProductActivityType; export type ProductActivityTone = 'sky' | 'emerald' | 'amber' | 'rose' | 'slate'; +export interface ProductActivityTextPart { + text: string; + strong?: boolean; +} + export interface ProductActivityDisplayItem extends Api.Product.ProductActivityTimelineItem { tagLabel: string; timeText: string; actionText: string; displaySummary: string; compactText: string; + compactTextParts: ProductActivityTextPart[]; operatorText: string; + subjectText: string; reasonText: string; statusTransition: string; tone: ProductActivityTone; @@ -250,6 +257,10 @@ function isGenericActivitySummary(summaryText: string, actionText: string) { return summaryText === actionText || summaryText === actionText.replace('执行了', '执行了'); } +function isMemberActivityAction(actionType: Api.Product.ProductActivityActionType) { + return actionType === 'add_member' || actionType === 'remove_member' || actionType === 'update_member'; +} + function buildMemberChangeSummary( item: Api.Product.ProductActivityTimelineItem, detailsRecord: ActivityDetailRecord | null, @@ -263,9 +274,10 @@ function buildMemberChangeSummary( } const memberDetail = roleName ? `${memberName}(${roleName})` : memberName; - const actionLabel = item.actionType === 'add_member' ? '将成员加入产品' : '将成员移出产品'; - return operatorText === '--' ? `${actionLabel}:${memberDetail}` : `${operatorText}${actionLabel}:${memberDetail}`; + return operatorText === '--' + ? `执行了【${item.actionName}】:${memberDetail}` + : `${operatorText}执行了【${item.actionName}】:${memberDetail}`; } function buildMemberUpdateSummary( @@ -279,8 +291,8 @@ function buildMemberUpdateSummary( const roleText = roleTransitionText ? `,角色:${roleTransitionText}` : ''; return operatorText === '--' - ? `调整成员:${memberText}${roleText}` - : `${operatorText}调整成员:${memberText}${roleText}`; + ? `执行了【${item.actionName}】:${memberText}${roleText}` + : `${operatorText}执行了【${item.actionName}】:${memberText}${roleText}`; } function buildManagerChangeSummary(detailsRecord: ActivityDetailRecord | null, operatorText: string) { @@ -309,15 +321,11 @@ function buildManagerChangeSummary(detailsRecord: ActivityDetailRecord | null, o function resolveDetailedSummary( item: Api.Product.ProductActivityTimelineItem, - operatorText: string, - actionText: string + detailsRecord: ActivityDetailRecord | null, + texts: { operatorText: string; actionText: string } ) { + const { operatorText, actionText } = texts; const summaryText = item.summary?.trim() || ''; - const detailsRecord = parseActivityDetails(item.details); - - if (!isGenericActivitySummary(summaryText, actionText)) { - return summaryText; - } if (item.actionType === 'add_member' || item.actionType === 'remove_member') { return buildMemberChangeSummary(item, detailsRecord, operatorText) || summaryText || actionText; @@ -327,6 +335,10 @@ function resolveDetailedSummary( return buildMemberUpdateSummary(item, detailsRecord, operatorText); } + if (!isGenericActivitySummary(summaryText, actionText)) { + return summaryText; + } + if (item.actionType === 'change_manager') { return buildManagerChangeSummary(detailsRecord, operatorText) || summaryText || actionText; } @@ -334,13 +346,31 @@ function resolveDetailedSummary( return summaryText || actionText; } +function buildProductActivityTextParts(text: string, subjectText: string): ProductActivityTextPart[] { + const normalizedSubject = subjectText.trim(); + const subjectIndex = normalizedSubject ? text.indexOf(normalizedSubject) : -1; + + if (subjectIndex < 0) { + return [{ text }]; + } + + return [ + { text: text.slice(0, subjectIndex) }, + { text: normalizedSubject, strong: true }, + { text: text.slice(subjectIndex + normalizedSubject.length) } + ].filter(part => part.text); +} + export function buildProductActivityDisplayItem( item: Api.Product.ProductActivityTimelineItem ): ProductActivityDisplayItem { const operatorText = item.operatorName?.trim() || '--'; const actionText = operatorText === '--' ? `执行了【${item.actionName}】` : `${operatorText}执行了【${item.actionName}】`; - const displaySummary = item.type === 'status' ? actionText : resolveDetailedSummary(item, operatorText, actionText); + const detailsRecord = parseActivityDetails(item.details); + const subjectText = isMemberActivityAction(item.actionType) ? getActivityTargetUserName(item, detailsRecord) : ''; + const displaySummary = + item.type === 'status' ? actionText : resolveDetailedSummary(item, detailsRecord, { operatorText, actionText }); const compactText = displaySummary; return { @@ -350,7 +380,9 @@ export function buildProductActivityDisplayItem( actionText, displaySummary, compactText, + compactTextParts: buildProductActivityTextParts(compactText, subjectText), operatorText, + subjectText, reasonText: item.reason?.trim() || '', statusTransition: item.type === 'status' && item.fromStatus && item.toStatus diff --git a/src/views/product/list/index.vue b/src/views/product/list/index.vue index 0b9072e..58a4eb2 100644 --- a/src/views/product/list/index.vue +++ b/src/views/product/list/index.vue @@ -6,7 +6,7 @@ import dayjs from 'dayjs'; import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue'; import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict'; import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context'; -import { fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api'; +import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api'; import { useDict } from '@/hooks/business/dict'; import { useRouterPush } from '@/hooks/common/router'; import { useUIPaginatedTable } from '@/hooks/common/table'; @@ -27,7 +27,6 @@ interface StatusNavMeta { type ProductPageResponse = Awaited>; -const PRODUCT_OPTION_PAGE_SIZE = 200; const PRODUCT_ENTRY_ROUTE_PATH = '/product/list'; function getInitSearchParams(): Api.Product.ProductSearchParams { @@ -72,59 +71,6 @@ function formatDateTime(value?: string | null) { return dayjs(value).format('YYYY-MM-DD HH:mm:ss'); } -async function fetchProductTotal(params: Api.Product.ProductSearchParams) { - const { error, data } = await fetchGetProductPage({ - ...params, - pageNo: 1, - pageSize: 1 - }); - - if (error || !data) { - return 0; - } - - return data.total; -} - -async function fetchAllProducts() { - async function collect(pageNo: number, list: Api.Product.Product[]): Promise { - const { error, data } = await fetchGetProductPage({ - pageNo, - pageSize: PRODUCT_OPTION_PAGE_SIZE - }); - - if (error || !data) { - return null; - } - - const nextList = list.concat(data.list); - - if (nextList.length >= data.total || data.list.length === 0) { - return nextList; - } - - return collect(pageNo + 1, nextList); - } - - return collect(1, []); -} - -function createManagerOptions(products: Api.Product.Product[], users: Api.SystemManage.UserSimple[]) { - const managerIdSet = new Set(products.map(item => String(item.managerUserId)).filter(Boolean)); - const userMap = new Map(users.map(item => [String(item.id), item])); - - const options = Array.from(managerIdSet).map(managerUserId => { - return ( - userMap.get(managerUserId) || { - id: managerUserId, - nickname: String(managerUserId) - } - ); - }); - - return sortManagerOptions(options); -} - const statusNavMetas: StatusNavMeta[] = [ { key: 'active', @@ -166,15 +112,13 @@ const { routerPush } = useRouterPush(); const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE); -const statusCounts = ref>({ +const statusCounts = ref>({ active: 0, archived: 0, paused: 0, abandoned: 0 }); -const recentUpdatedCount = ref(0); - const managerLabelMap = computed(() => { return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname])); }); @@ -182,7 +126,7 @@ const managerLabelMap = computed(() => { const statusItems = computed(() => statusNavMetas.map(item => ({ ...item, - count: statusCounts.value[item.key] + count: statusCounts.value[item.key] ?? 0 })) ); @@ -194,7 +138,7 @@ const overviewMetrics = computed(() => [ }, { label: '当前启用', - value: statusCounts.value.active, + value: statusCounts.value.active ?? 0, hint: '正在持续服务和维护的产品' }, { @@ -203,9 +147,9 @@ const overviewMetrics = computed(() => [ hint: '已加载的方向字典项数量' }, { - label: '30天内更新', - value: recentUpdatedCount.value, - hint: '最近 30 天内发生过更新的产品' + label: '废弃产品', + value: statusCounts.value.abandoned ?? 0, + hint: '已明确停止建设的产品' } ]); @@ -312,44 +256,33 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } /> ) } - ] + ], + immediate: false }); async function loadManagerOptions() { - const [allProducts, userSimpleResult] = await Promise.all([fetchAllProducts(), fetchGetUserSimpleList()]); + const { error, data: userList } = await fetchGetUserSimpleList(); - const userSimpleList = - userSimpleResult.error || !userSimpleResult.data ? [] : sortManagerOptions(userSimpleResult.data); - - managerUserOptions.value = userSimpleList; - - if (!allProducts) { + if (error || !userList) { + managerUserOptions.value = []; managerFilterOptions.value = []; return; } - managerFilterOptions.value = createManagerOptions(allProducts, userSimpleList); + const userSimpleList = sortManagerOptions(userList); + managerUserOptions.value = userSimpleList; + managerFilterOptions.value = userSimpleList; } async function loadOverviewData() { - const end = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); - const start = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + const { error, data: overviewSummary } = await fetchGetProductOverviewSummary(); - const [activeTotal, archivedTotal, pausedTotal, abandonedTotal, recentTotal] = await Promise.all([ - fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'active' }), - fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'archived' }), - fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'paused' }), - fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'abandoned' }), - fetchProductTotal({ pageNo: 1, pageSize: 1, updateTime: [start, end] }) - ]); + if (error || !overviewSummary) { + statusCounts.value = {}; + return; + } - statusCounts.value = { - active: activeTotal, - archived: archivedTotal, - paused: pausedTotal, - abandoned: abandonedTotal - }; - recentUpdatedCount.value = recentTotal; + statusCounts.value = overviewSummary.statusCounts || {}; } async function reloadProductTable(page = searchParams.pageNo ?? 1) { diff --git a/src/views/product/list/modules/product-operate-dialog.vue b/src/views/product/list/modules/product-operate-dialog.vue index 0621da6..7bdbd4a 100644 --- a/src/views/product/list/modules/product-operate-dialog.vue +++ b/src/views/product/list/modules/product-operate-dialog.vue @@ -4,6 +4,7 @@ import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict'; import { fetchCreateProduct, fetchGetProduct, fetchUpdateProduct } from '@/service/api'; import { useForm, useFormRules } from '@/hooks/common/form'; import BusinessFormDialog from '@/components/custom/business-form-dialog.vue'; +import BusinessUserSelect from '@/components/custom/business-user-select.vue'; import DictSelect from '@/components/custom/dict-select.vue'; defineOptions({ name: 'ProductOperateDialog' }); @@ -166,14 +167,14 @@ watch(visible, async value => { - + { - + - + { /> - +