From dd6bceeeb7fc55f6ce4044d05c64b61a635e28fa Mon Sep 17 00:00:00 2001 From: luo Date: Sat, 25 Jul 2026 14:13:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-system-config-management/design.md | 26 ++ .../add-system-config-management/proposal.md | 18 + .../specs/system-config-management/spec.md | 56 +++ .../add-system-config-management/tasks.md | 20 + .../design.md | 36 ++ .../proposal.md | 18 + .../specs/package-series-allocation/spec.md | 61 +++ .../tasks.md | 26 ++ src/api/modules/index.ts | 1 + src/api/modules/systemConfig.ts | 28 ++ src/router/routes/asyncRoutes.ts | 11 + src/router/routesAlias.ts | 1 + src/types/api/index.ts | 3 + src/types/api/packageManagement.ts | 10 +- src/types/api/systemConfig.ts | 37 ++ src/utils/business/seriesGrantPackage.ts | 44 +++ .../series-grants/detail.vue | 28 +- .../series-grants/index.vue | 45 ++- .../series-grants/packages.vue | 26 +- src/views/settings/system-configs/index.vue | 351 ++++++++++++++++++ 20 files changed, 805 insertions(+), 41 deletions(-) create mode 100644 openspec/changes/add-system-config-management/design.md create mode 100644 openspec/changes/add-system-config-management/proposal.md create mode 100644 openspec/changes/add-system-config-management/specs/system-config-management/spec.md create mode 100644 openspec/changes/add-system-config-management/tasks.md create mode 100644 openspec/changes/update-series-grant-package-contract/design.md create mode 100644 openspec/changes/update-series-grant-package-contract/proposal.md create mode 100644 openspec/changes/update-series-grant-package-contract/specs/package-series-allocation/spec.md create mode 100644 openspec/changes/update-series-grant-package-contract/tasks.md create mode 100644 src/api/modules/systemConfig.ts create mode 100644 src/types/api/systemConfig.ts create mode 100644 src/utils/business/seriesGrantPackage.ts create mode 100644 src/views/settings/system-configs/index.vue diff --git a/openspec/changes/add-system-config-management/design.md b/openspec/changes/add-system-config-management/design.md new file mode 100644 index 0000000..724b32b --- /dev/null +++ b/openspec/changes/add-system-config-management/design.md @@ -0,0 +1,26 @@ +## Context + +系统配置由后端注册,前端不能假设固定字段。列表接口返回控件提示、值类型、枚举和范围,更新接口要求配置 Key 与路径一致且值字符串化。 + +## Goals / Non-Goals + +- Goals: + - 在设置管理提供统一的模块筛选、分页和配置编辑入口。 + - 动态遵循后端配置元数据进行展示和校验。 + - 防止误修改只读配置和敏感配置脱敏值。 +- Non-Goals: + - 不在前端维护配置 Key 白名单或复制后端注册规则。 + - 不对 JSON 配置做业务语义解析。 + - 不绕过后端对已注册配置、范围和枚举的最终校验。 + +## Decisions + +- 列表数据读取 `data.list`,分页使用 `page`、`page_size` 和 `total`。 +- 编辑控件优先根据 `control` 渲染,`value_type` 作为值转换和校验兜底。 +- `bool` 以开关展示并提交 `true`/`false` 字符串;`int` 以数字输入展示并提交十进制字符串;其他类型以输入框或文本域提交字符串。 +- `readonly=true` 的配置不显示可编辑操作;`sensitive=true` 的值仅显示接口返回值,只有用户主动输入新值时才提交更新。 + +## Risks / Trade-offs + +- 未知控件提示可能无法映射到专用控件,使用文本输入作为安全兜底,并仍由后端校验。 +- 脱敏敏感值无法判断是否为完整原值,因此禁止把未修改的脱敏值再次提交。 diff --git a/openspec/changes/add-system-config-management/proposal.md b/openspec/changes/add-system-config-management/proposal.md new file mode 100644 index 0000000..11086ae --- /dev/null +++ b/openspec/changes/add-system-config-management/proposal.md @@ -0,0 +1,18 @@ +# Change: 增加系统配置管理 + +## Why + +系统配置接口已经提供受控配置的注册信息、控件提示和校验范围,设置管理需要提供统一入口,避免为每个配置模块单独维护页面和字段。 + +## What Changes + +- 在设置管理下新增系统配置页面和路由。 +- 接入系统配置列表查询和按配置 Key 更新接口。 +- 按后端返回的 `control`、`value_type`、枚举值及范围动态渲染编辑控件。 +- 只读配置不可编辑,更新请求统一提交 `{ key, value }`,其中 `value` 为字符串。 +- 敏感配置按接口脱敏值展示,保存时不覆盖未重新输入的脱敏值。 + +## Impact + +- Affected specs: `system-config-management` +- Affected code: `src/api/modules/systemConfig.ts`, `src/types/api/systemConfig.ts`, `src/views/settings/system-configs/index.vue`, `src/router/routesAlias.ts`, `src/router/routes/asyncRoutes.ts` diff --git a/openspec/changes/add-system-config-management/specs/system-config-management/spec.md b/openspec/changes/add-system-config-management/specs/system-config-management/spec.md new file mode 100644 index 0000000..0787c4d --- /dev/null +++ b/openspec/changes/add-system-config-management/specs/system-config-management/spec.md @@ -0,0 +1,56 @@ +## ADDED Requirements + +### Requirement: 受控系统配置列表 + +系统 SHALL 在设置管理提供受控系统配置列表,支持按 `module` 筛选和分页,并展示后端返回的配置元数据及当前值。 + +#### Scenario: 查询系统配置 + +- **WHEN** 用户进入系统配置页面或执行刷新 +- **THEN** 前端调用 `GET /api/admin/system-configs` +- **AND** 使用响应中的 `data.list`、`data.page`、`data.page_size` 和 `data.total` 渲染列表 +- **AND** 展示配置 Key、模块、说明、值、控件提示、注册状态、只读状态、敏感状态和更新时间 + +#### Scenario: 按模块筛选 + +- **WHEN** 用户选择 `carrier_callback` 或 `c2b.payment` +- **THEN** 查询请求携带对应的 `module` 参数 +- **AND** 页面从第一页展示筛选后的配置 + +### Requirement: 动态配置编辑 + +系统 SHALL 根据配置返回的 `control`、`value_type`、`enum_values`、`min` 和 `max` 元数据渲染编辑控件并进行前端基础校验;更新请求 SHALL 统一提交字符串形式的 `value`。 + +#### Scenario: 更新可编辑配置 + +- **WHEN** 用户修改非只读配置并提交 +- **THEN** 前端调用 `PUT /api/admin/system-configs/{key}` +- **AND** 请求体为 `{ "key": "配置Key", "value": "字符串化配置值" }` +- **AND** 成功后使用接口返回的配置数据更新列表 + +#### Scenario: bool 和 int 配置 + +- **WHEN** 配置类型分别为 `bool` 或 `int` +- **THEN** 页面分别使用开关或数字输入控件 +- **AND** bool 提交 `true`/`false` 字符串,int 提交十进制数字字符串 +- **AND** int 值违反 `min` 或 `max` 时阻止提交 + +#### Scenario: 枚举配置 + +- **WHEN** 配置返回非空 `enum_values` +- **THEN** 页面使用枚举选择控件 +- **AND** 只能提交允许的枚举值 + +### Requirement: 只读和敏感配置保护 + +系统 SHALL 禁止前端编辑只读配置;敏感配置 SHALL 展示接口返回的脱敏值,用户未主动输入新值时不得将该脱敏值再次提交。 + +#### Scenario: 只读配置 + +- **WHEN** 配置的 `readonly` 为 `true` +- **THEN** 页面禁用编辑控件和保存操作 + +#### Scenario: 敏感配置未修改 + +- **WHEN** 敏感配置仅展示脱敏值且用户未输入新值 +- **THEN** 页面不提交该配置的更新请求 diff --git a/openspec/changes/add-system-config-management/tasks.md b/openspec/changes/add-system-config-management/tasks.md new file mode 100644 index 0000000..3c71154 --- /dev/null +++ b/openspec/changes/add-system-config-management/tasks.md @@ -0,0 +1,20 @@ +## 1. API And Types + +- [x] 1.1 新增系统配置字段、查询参数和分页响应类型。 +- [x] 1.2 新增 `GET /api/admin/system-configs` 服务方法,读取 `data.list`。 +- [x] 1.3 新增 `PUT /api/admin/system-configs/{key}` 服务方法,提交 `{ key, value }`。 + +## 2. Settings Management Page + +- [x] 2.1 在设置管理下新增系统配置路由和菜单入口。 +- [x] 2.2 实现模块筛选、分页、刷新和配置列表展示。 +- [x] 2.3 根据配置元数据动态渲染开关、数字、枚举、文本和 JSON 控件。 +- [x] 2.4 实现只读配置禁用编辑、敏感值脱敏保护和字符串化提交。 +- [x] 2.5 展示配置注册状态、更新时间、范围和校验提示。 + +## 3. Verification + +- [x] 3.1 验证两个模块筛选和分页参数正确传递。 +- [x] 3.2 验证 bool、int、enum、string 和 json 配置的展示及提交值。 +- [x] 3.3 验证只读配置不能提交,敏感配置未修改时不会回传脱敏值。 +- [x] 3.4 运行类型检查、lint 和构建。 diff --git a/openspec/changes/update-series-grant-package-contract/design.md b/openspec/changes/update-series-grant-package-contract/design.md new file mode 100644 index 0000000..663d1bc --- /dev/null +++ b/openspec/changes/update-series-grant-package-contract/design.md @@ -0,0 +1,36 @@ +## Context + +代理系列授权详情返回完整的 `packages[]`,套餐列表接口返回可选套餐候选。前端创建授权时需要从候选套餐中选择初始套餐,详情和套餐管理页面则需要同时展示候选套餐信息与已授权状态。 + +## Goals / Non-Goals + +- Goals: + - 以 `package_id` 为唯一键合并套餐候选和授权详情。 + - 在创建前校验初始套餐数量为 1~100 且不重复。 + - 使用 `remove=true` 表达后续删除,保持接口响应结构不变。 +- Non-Goals: + - 不修改后端接口路径或响应字段结构。 + - 不在前端复制后端的成本价、生效条件或套餐状态计算规则。 + - 不新增独立的套餐删除 API 调用。 + +## Decisions + +- 套餐候选状态以 `package_id` 合并:详情返回的套餐标记为已授权,候选列表中不存在于详情的套餐标记为未授权。 +- 创建请求始终携带 `packages`,数量范围为 1~100;每个 `package_id` 只能出现一次。 +- 后续新增或调价沿用 `PUT /api/admin/shop-series-grants/{id}/packages`,删除项只提交 `{ package_id, remove: true }`。 +- 成功响应直接按后端返回的完整授权详情刷新页面,不根据请求内容自行拼装最终响应。 + +## Risks / Trade-offs + +- 套餐候选接口分页或搜索结果可能不包含已授权套餐,合并时必须保留详情中的已授权项,避免误显示为未授权。 +- 用户一次选择超过 100 个套餐时前端应提前阻止提交,同时仍依赖后端校验作为最终约束。 + +## Migration Plan + +1. 更新类型和 API 请求约束。 +2. 更新创建页、授权详情页和套餐列表页的候选合并及套餐操作逻辑。 +3. 使用接口响应重新加载授权详情,验证创建、调价、删除和空候选场景。 + +## Open Questions + +- 套餐候选接口的分页大小是否允许一次返回全部候选;若不允许,页面需要继续使用远程搜索加载候选。 diff --git a/openspec/changes/update-series-grant-package-contract/proposal.md b/openspec/changes/update-series-grant-package-contract/proposal.md new file mode 100644 index 0000000..8335dcd --- /dev/null +++ b/openspec/changes/update-series-grant-package-contract/proposal.md @@ -0,0 +1,18 @@ +# Change: 更新代理系列授权套餐管理契约 + +## Why + +代理系列授权接口已更新为创建时必须一次提交初始套餐,后续通过套餐批量管理接口维护套餐。前端需要将套餐列表作为候选集合、授权详情作为已授权状态来源进行合并,避免新增、编辑和删除时状态不一致。 + +## What Changes + +- 套餐列表页和授权详情页统一按 `package_id` 合并套餐候选数据与已授权数据。 +- 创建代理系列授权时强制提交 1~100 个不重复套餐。 +- 后续套餐管理继续通过 `packages[]` 提交新增、调价和 `remove=true` 删除操作。 +- 保持创建、详情、更新和套餐管理接口的完整授权详情响应结构不变。 +- 更新 API 类型、表单校验和页面状态管理,禁止通过空套餐列表创建授权或使用独立删除接口删除授权套餐。 + +## Impact + +- Affected specs: `package-series-allocation` +- Affected code: `src/api/modules/shopSeriesGrant.ts`, `src/types/api/packageManagement.ts`, `src/views/package-management/series-grants/index.vue`, `src/views/package-management/series-grants/detail.vue`, `src/views/package-management/series-grants/packages.vue` diff --git a/openspec/changes/update-series-grant-package-contract/specs/package-series-allocation/spec.md b/openspec/changes/update-series-grant-package-contract/specs/package-series-allocation/spec.md new file mode 100644 index 0000000..b375763 --- /dev/null +++ b/openspec/changes/update-series-grant-package-contract/specs/package-series-allocation/spec.md @@ -0,0 +1,61 @@ +## MODIFIED Requirements + +### Requirement: 创建代理系列授权时提交初始套餐 + +系统 SHALL 在创建代理系列授权时要求提交 `packages`,且 `packages` 必须包含 1~100 个套餐项;套餐 ID 不得重复。创建成功响应 SHALL 保持完整授权详情结构不变。 + +#### Scenario: 创建授权并提交有效初始套餐 + +- **WHEN** 用户提交 `shop_id`、`series_id` 和包含 1~100 个不重复套餐项的 `packages` +- **THEN** 前端提交 `POST /api/admin/shop-series-grants` +- **AND** 每个套餐项携带必要的 `package_id`,新增或调价时携带 `cost_price` +- **AND** 页面按接口返回的完整授权详情刷新授权信息 + +#### Scenario: 创建授权未选择套餐 + +- **WHEN** 用户未选择任何初始套餐 +- **THEN** 前端阻止提交并提示至少选择 1 个套餐 + +#### Scenario: 创建授权超过套餐数量上限或包含重复套餐 + +- **WHEN** 用户选择超过 100 个套餐或同一 `package_id` 重复出现 +- **THEN** 前端阻止提交并提示套餐数量或重复项错误 + +### Requirement: 合并套餐候选与授权状态 + +系统 SHALL 在套餐列表和授权详情相关页面以 `package_id` 为唯一键合并套餐候选数据与授权详情中的 `packages[]`。已授权套餐即使不在当前候选搜索结果中,也 SHALL 保留并标记为已授权。 + +#### Scenario: 候选套餐与已授权套餐合并 + +- **WHEN** 套餐候选列表和授权详情均加载成功 +- **THEN** 页面按 `package_id` 合并两者 +- **AND** 详情中的套餐显示为已授权并保留其成本价、生效条件和状态字段 +- **AND** 仅出现在候选列表中的套餐显示为未授权候选 + +#### Scenario: 候选搜索结果不包含已授权套餐 + +- **WHEN** 当前候选搜索结果缺少某个已授权套餐 +- **THEN** 页面仍从授权详情保留该套餐 +- **AND** 不将其误判为未授权或从当前编辑状态中删除 + +### Requirement: 后续套餐管理使用批量操作删除 + +系统 SHALL 使用 `PUT /api/admin/shop-series-grants/{id}/packages` 管理既有授权套餐。新增和调价提交套餐项,删除套餐授权 SHALL 提交对应套餐项的 `remove: true`;操作成功后 SHALL 按接口返回的完整授权详情刷新页面。 + +#### Scenario: 新增或调整授权套餐 + +- **WHEN** 用户在已有授权中新增套餐或调整成本价 +- **THEN** 前端提交 `packages[]` 中对应的套餐项及成本价 +- **AND** 不调用独立删除接口 + +#### Scenario: 删除授权套餐 + +- **WHEN** 用户删除已有授权套餐 +- **THEN** 前端提交 `{ package_id, remove: true }` +- **AND** 页面使用接口返回的完整授权详情刷新套餐列表 + +#### Scenario: 套餐管理响应结构保持不变 + +- **WHEN** 套餐批量管理接口成功 +- **THEN** 前端按详情响应读取授权基本信息、佣金配置、强充配置、`packages[]`、状态和时间字段 +- **AND** 不要求后端返回新的响应包装结构 diff --git a/openspec/changes/update-series-grant-package-contract/tasks.md b/openspec/changes/update-series-grant-package-contract/tasks.md new file mode 100644 index 0000000..9f32ee3 --- /dev/null +++ b/openspec/changes/update-series-grant-package-contract/tasks.md @@ -0,0 +1,26 @@ +## 1. API And Types + +- [x] 1.1 按 21 号接口文档核对代理系列授权列表、创建、详情、更新和套餐管理的请求响应类型。 +- [x] 1.2 将创建请求的 `packages` 定义为必填,并补充套餐 1~100 项及套餐 ID 不重复的约束说明。 +- [x] 1.3 明确 `GrantPackageItem.remove` 仅用于后续套餐删除,保留完整授权详情响应类型。 + +## 2. Candidate State Merge + +- [x] 2.1 在创建授权页面按 `package_id` 合并套餐候选列表和当前选择状态。 +- [x] 2.2 在授权详情页面按 `package_id` 合并套餐列表候选和详情中的已授权套餐,已授权套餐必须保留在合并结果中。 +- [x] 2.3 在独立授权套餐列表页面复用同一状态规则,避免候选套餐和已授权套餐重复或丢失。 + +## 3. Create And Manage Packages + +- [x] 3.1 创建授权提交前校验套餐数量为 1~100,且套餐 ID 不重复。 +- [x] 3.2 创建授权始终提交 `packages[]`,禁止空数组或缺失套餐列表提交。 +- [x] 3.3 新增或调价继续提交套餐项和成本价;删除只提交 `remove: true`,不调用独立删除接口。 +- [x] 3.4 套餐管理成功后使用后端返回的完整授权详情刷新页面,不前端重算响应结构。 + +## 4. Verification + +- [x] 4.1 验证创建 1 个套餐成功,创建 0 个、101 个和重复套餐被阻止。 +- [x] 4.2 验证套餐候选与授权详情合并后,已授权状态、成本价和生效条件展示正确。 +- [x] 4.3 验证新增、调价和 `remove=true` 删除均使用套餐管理接口。 +- [x] 4.4 验证接口成功响应仍按完整授权详情结构处理。 +- [x] 4.5 运行类型检查、lint 和构建。 diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index fb05044..2601d94 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -23,6 +23,7 @@ export { OrderService } from './order' export { AssetService } from './asset' export { AgentRechargeService } from './agentRecharge' export { PaymentSettingsService } from './paymentSettings' +export { SystemConfigService } from './systemConfig' export { ExchangeService } from './exchange' export { RefundService } from './refund' export { DataCleanupService } from './dataCleanup' diff --git a/src/api/modules/systemConfig.ts b/src/api/modules/systemConfig.ts new file mode 100644 index 0000000..c22f357 --- /dev/null +++ b/src/api/modules/systemConfig.ts @@ -0,0 +1,28 @@ +import { BaseService } from '../BaseService' +import type { BaseResponse } from '@/types/api' +import type { + SystemConfigItem, + SystemConfigPageResult, + SystemConfigQueryParams, + UpdateSystemConfigRequest +} from '@/types/api/systemConfig' + +const SYSTEM_CONFIG_BASE_URL = '/api/admin/system-configs' + +export class SystemConfigService extends BaseService { + static getSystemConfigs( + params?: SystemConfigQueryParams + ): Promise> { + return this.get>(SYSTEM_CONFIG_BASE_URL, params) + } + + static updateSystemConfig( + key: string, + data: UpdateSystemConfigRequest + ): Promise> { + return this.put>( + `${SYSTEM_CONFIG_BASE_URL}/${encodeURIComponent(key)}`, + data + ) + } +} diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index f18fb6e..88fa16e 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -764,6 +764,17 @@ export const asyncRoutes: AppRouteRecord[] = [ keepAlive: true, roles: ['R_SUPER'] } + }, + // 系统配置 + { + path: 'system-configs', + name: 'SystemConfigs', + component: RoutesAlias.SystemConfigs, + meta: { + title: '系统配置', + keepAlive: true, + roles: ['R_SUPER', 'R_ADMIN'] + } } ] }, diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 27f0a88..3b0f817 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -92,6 +92,7 @@ export enum RoutesAlias { PaymentSettings = '/settings/payment-settings', // 支付设置 PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情 OperationPasswordSettings = '/settings/operation-password', // 操作密码设置 + SystemConfigs = '/settings/system-configs', // 系统配置 // 轮询管理 DataCleanup = '/polling-management/data-cleanup', // 数据清理 diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 708fa5f..31b36ac 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -78,6 +78,9 @@ export * from './agentRecharge' // 支付设置相关 export * from './paymentSettings' +// 系统配置相关 +export * from './systemConfig' + // 退款管理相关 export * from './refund' diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts index 43c6dc4..8561bc3 100644 --- a/src/types/api/packageManagement.ts +++ b/src/types/api/packageManagement.ts @@ -336,8 +336,8 @@ export interface CreateShopSeriesGrantRequest { commission_tiers?: CommissionTier[] // 梯度配置列表,梯度模式时必填 enable_force_recharge?: boolean // 是否启用强充 force_recharge_amount?: number // 强充金额(分) - packages?: GrantPackageItem[] // 套餐列表 - expiry_base_override: PackageAllocationExpiryBaseOverride | null + packages: GrantPackageItem[] // 初始套餐列表,1~100项 + expiry_base_override?: PackageAllocationExpiryBaseOverride | null } /** @@ -354,7 +354,7 @@ export interface UpdateShopSeriesGrantRequest { * 管理套餐请求中的套餐项 */ export interface GrantPackageItem { - package_id?: number // 套餐ID + package_id: number // 套餐ID cost_price?: number // 成本价(分) expiry_base_override?: PackageAllocationExpiryBaseOverride | null remove?: boolean | null // 是否删除该套餐授权(true=删除) @@ -364,6 +364,6 @@ export interface GrantPackageItem { * 管理套餐请求 */ export interface ManageGrantPackagesRequest { - packages?: GrantPackageItem[] | null // 套餐操作列表 - expiry_base_override: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件 + packages: GrantPackageItem[] // 套餐操作列表,1~100项 + expiry_base_override?: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件 } diff --git a/src/types/api/systemConfig.ts b/src/types/api/systemConfig.ts new file mode 100644 index 0000000..2a0b7e9 --- /dev/null +++ b/src/types/api/systemConfig.ts @@ -0,0 +1,37 @@ +import type { PaginationParams } from './common' + +export type SystemConfigModule = 'carrier_callback' | 'c2b.payment' + +export type SystemConfigValueType = 'string' | 'int' | 'bool' | 'json' + +export interface SystemConfigItem { + config_key: string + module: SystemConfigModule + value: string + value_type: SystemConfigValueType + description: string + control: string + enum_values: string[] + min: number | null + max: number | null + readonly: boolean + registered: boolean + sensitive: boolean + updated_at: string | null +} + +export interface SystemConfigQueryParams extends PaginationParams { + module?: SystemConfigModule +} + +export interface SystemConfigPageResult { + list: SystemConfigItem[] + page: number + page_size: number + total: number +} + +export interface UpdateSystemConfigRequest { + key: string + value: string +} diff --git a/src/utils/business/seriesGrantPackage.ts b/src/utils/business/seriesGrantPackage.ts new file mode 100644 index 0000000..7372c40 --- /dev/null +++ b/src/utils/business/seriesGrantPackage.ts @@ -0,0 +1,44 @@ +import type { GrantPackageInfo, PackageResponse } from '@/types/api' + +export interface GrantPackageCandidate { + id: number + package_name?: string + package_code?: string + cost_price?: number + suggested_retail_price?: number | null + is_authorized: boolean + authorized_package?: GrantPackageInfo +} + +export const mergeGrantPackageCandidates = ( + candidates: PackageResponse[], + authorizedPackages: GrantPackageInfo[] +): GrantPackageCandidate[] => { + const merged = new Map() + + candidates.forEach((candidate) => { + merged.set(candidate.id, { + id: candidate.id, + package_name: candidate.package_name, + package_code: candidate.package_code, + cost_price: candidate.cost_price, + suggested_retail_price: candidate.suggested_retail_price, + is_authorized: false + }) + }) + + authorizedPackages.forEach((authorizedPackage) => { + const candidate = merged.get(authorizedPackage.package_id) + merged.set(authorizedPackage.package_id, { + id: authorizedPackage.package_id, + package_name: candidate?.package_name ?? authorizedPackage.package_name, + package_code: candidate?.package_code ?? authorizedPackage.package_code, + cost_price: authorizedPackage.cost_price ?? candidate?.cost_price, + suggested_retail_price: candidate?.suggested_retail_price, + is_authorized: true, + authorized_package: authorizedPackage + }) + }) + + return [...merged.values()] +} diff --git a/src/views/package-management/series-grants/detail.vue b/src/views/package-management/series-grants/detail.vue index 90128c9..13a6a13 100644 --- a/src/views/package-management/series-grants/detail.vue +++ b/src/views/package-management/series-grants/detail.vue @@ -245,7 +245,7 @@ :key="pkg.id" :label="`${pkg.package_name} (${pkg.package_code})`" :value="pkg.id" - :disabled="isPackageAlreadyAdded(pkg.id)" + :disabled="pkg.is_authorized" /> @@ -345,7 +345,6 @@ } from '@/api/modules' import type { ShopSeriesGrantResponse, - PackageResponse, GrantPackageItem, GrantPackageInfo, PackageAllocationExpiryBaseOverride, @@ -353,6 +352,10 @@ } from '@/types/api' import { formatDateTime } from '@/utils/business/format' import { getEnableStatusText } from '@/config/constants' + import { + mergeGrantPackageCandidates, + type GrantPackageCandidate + } from '@/utils/business/seriesGrantPackage' defineOptions({ name: 'SeriesGrantsDetail' }) @@ -366,7 +369,7 @@ const seriesData = ref(null) const packageDialogVisible = ref(false) const packageDialogType = ref<'add' | 'edit'>('add') - const availablePackages = ref([]) + const availablePackages = ref([]) const packageFormRef = ref() type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default' @@ -559,7 +562,10 @@ const res = await PackageManageService.getPackages(params) if (res.code === 0) { - availablePackages.value = res.data.items + availablePackages.value = mergeGrantPackageCandidates( + res.data.items, + detailData.value.packages || [] + ) } } catch (error) { console.error('加载套餐选项失败:', error) @@ -577,11 +583,6 @@ } } - // 检查套餐是否已添加 - const isPackageAlreadyAdded = (packageId: number) => { - return detailData.value?.packages?.some((p) => p.package_id === packageId) || false - } - // 保存套餐 const handleSavePackage = async () => { if (!packageFormRef.value || !detailData.value) return @@ -600,7 +601,7 @@ : packageForm.value.expiry_base_override, packages: [ { - package_id: packageForm.value.package_id, + package_id: packageForm.value.package_id!, cost_price: Math.round(packageForm.value.cost_price_yuan * 100) } ] @@ -616,7 +617,7 @@ const packages: GrantPackageItem[] = [ { - package_id: packageForm.value.package_id, + package_id: packageForm.value.package_id!, cost_price: Math.round(packageForm.value.cost_price_yuan * 100) } ] @@ -672,10 +673,7 @@ } ] - await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { - expiry_base_override: row.expiry_base_override ?? null, - packages - }) + await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages }) ElMessage.success('删除成功') // 刷新详情数据 diff --git a/src/views/package-management/series-grants/index.vue b/src/views/package-management/series-grants/index.vue index e033e51..5ec8074 100644 --- a/src/views/package-management/series-grants/index.vue +++ b/src/views/package-management/series-grants/index.vue @@ -113,17 +113,18 @@
- 套餐配置(可选) + 套餐配置(必选)