From fac05db39afc818668193f4928f51f70e5b5554f Mon Sep 17 00:00:00 2001 From: luo Date: Mon, 20 Jul 2026 10:50:23 +0800 Subject: [PATCH 01/76] =?UTF-8?q?feat:=20=E6=8D=A2=E8=B4=A7=E6=96=B0?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=BD=92=E5=B1=9E=E7=BB=A7=E6=89=BF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proposal.md | 22 ++ .../specs/exchange-management/spec.md | 88 +++++ .../tasks.md | 22 ++ src/api/modules/asset.ts | 9 +- src/api/modules/exchange.ts | 6 +- .../exchange-management/detail.vue | 11 + .../exchange-management/index.vue | 344 +++++++++++++++++- 7 files changed, 485 insertions(+), 17 deletions(-) create mode 100644 openspec/changes/update-exchange-inherited-shop-hint/proposal.md create mode 100644 openspec/changes/update-exchange-inherited-shop-hint/specs/exchange-management/spec.md create mode 100644 openspec/changes/update-exchange-inherited-shop-hint/tasks.md diff --git a/openspec/changes/update-exchange-inherited-shop-hint/proposal.md b/openspec/changes/update-exchange-inherited-shop-hint/proposal.md new file mode 100644 index 0000000..dd2597d --- /dev/null +++ b/openspec/changes/update-exchange-inherited-shop-hint/proposal.md @@ -0,0 +1,22 @@ +# Change: 换货新资产归属继承提示 + +## Why + +当前换货流程在选择新资产后,页面没有明确告知换货完成后的最终归属店铺,运营人员容易误以为可以手工选择目标店铺,进而对换货结果产生误判。 + +## What Changes + +- 在换货创建入口、发货确认入口、换货详情页补充旧资产所属店铺和新资产继承归属店铺的展示。 +- 保持现有换货创建、发货、确认完成接口和入参语义不变,不新增目标店铺选择控件或目标店铺提交字段。 +- 扩展换货响应数据,返回 `inherited_shop_id` 和 `inherited_shop_name` 供前端统一展示最终归属店铺。 +- 在创建、发货、确认完成流程中补充提交中禁用、防重复提交、后端归属校验失败提示,以及成功后的详情刷新。 + +## Impact + +- Affected specs: `exchange-management` +- Related active changes: `update-exchange-flow-direct-type`(本提案建立在现有换货流程类型改造之上,需合并考虑) +- Affected code: + - `src/api/modules/exchange.ts` + - `src/views/asset-management/exchange-management/index.vue` + - `src/views/asset-management/exchange-management/detail.vue` + - 相关旧资产/新资产搜索结果类型或映射逻辑(如需补充店铺名称展示数据) diff --git a/openspec/changes/update-exchange-inherited-shop-hint/specs/exchange-management/spec.md b/openspec/changes/update-exchange-inherited-shop-hint/specs/exchange-management/spec.md new file mode 100644 index 0000000..05a5e7d --- /dev/null +++ b/openspec/changes/update-exchange-inherited-shop-hint/specs/exchange-management/spec.md @@ -0,0 +1,88 @@ +## ADDED Requirements + +### Requirement: 换货归属继承上下文展示 + +系统 SHALL 在换货创建和发货确认流程中展示旧资产当前所属店铺,并在用户选定新资产后以只读方式提示换货完成后的继承归属店铺,避免用户误认为可以手工指定目标店铺。 + +#### Scenario: 创建入口展示旧资产所属店铺 + +- **GIVEN** 用户进入现有换货创建入口 +- **WHEN** 用户选择旧资产 +- **THEN** 系统 MUST 显示该旧资产当前所属店铺名称 +- **AND** 该所属店铺信息 MUST be read-only + +#### Scenario: 选择新资产后展示继承归属提示 + +- **GIVEN** 用户正在换货创建或发货确认流程中操作 +- **WHEN** 用户选择新资产 +- **THEN** 系统 MUST 显示只读提示 `换货完成后将归属:{店铺名称}` +- **AND** `{店铺名称}` MUST 表示该新资产换货完成后的最终归属店铺 + +#### Scenario: 页面不提供目标店铺选择控件 + +- **WHEN** 用户查看换货创建或发货确认入口 +- **THEN** 系统 MUST NOT 渲染目标店铺选择控件 +- **AND** 用户 MUST NOT 能够手工覆盖继承归属店铺 + +### Requirement: 换货归属继承接口契约 + +系统 SHALL 复用现有换货创建、发货和确认完成接口,并在换货数据模型中返回继承后的店铺标识和店铺名称,供创建结果、发货结果、完成结果和详情页统一展示。 + +#### Scenario: 创建请求保持既有字段语义 + +- **WHEN** 前端调用 `POST /api/admin/exchanges` +- **THEN** 请求体 MUST 继续沿用旧资产、新资产、换货类型和资料迁移字段 +- **AND** 请求体 MUST NOT 新增目标店铺字段 + +#### Scenario: 发货和完成请求不新增目标店铺字段 + +- **WHEN** 前端调用 `POST /api/admin/exchanges/{id}/ship` 或 `POST /api/admin/exchanges/{id}/complete` +- **THEN** 请求体 MUST NOT 新增目标店铺字段 +- **AND** 后端 MUST 继续按现有换货流程处理归属继承 + +#### Scenario: 换货响应返回继承店铺字段 + +- **WHEN** 前端收到换货创建、发货、确认完成结果或读取换货详情 +- **THEN** 返回的 `data` MUST 包含 `inherited_shop_id:int64` 和 `inherited_shop_name:string` +- **AND** `inherited_shop_name` MUST 表示换货完成后新资产最终归属的店铺名称 + +### Requirement: 换货详情继承归属展示 + +系统 SHALL 在换货详情页展示继承后的店铺名称,并在关键操作成功后刷新详情数据,保证页面展示的归属信息与后端一致。 + +#### Scenario: 详情页展示继承后的店铺名称 + +- **GIVEN** 用户进入换货详情页 +- **WHEN** 详情数据加载成功 +- **THEN** 页面 MUST 展示继承后的店铺名称 +- **AND** 页面 MUST NOT 隐藏该归属字段 + +#### Scenario: 发货或确认完成成功后刷新详情 + +- **GIVEN** 用户完成换货发货或确认完成操作 +- **WHEN** 接口返回成功 +- **THEN** 系统 MUST 重新拉取该换货单详情 +- **AND** 刷新后的详情 MUST 展示最新的继承归属店铺信息 + +### Requirement: 换货归属继承错误与提交状态处理 + +系统 SHALL 在新资产归属校验失败时直接展示后端错误,并在创建、发货、确认完成提交期间禁用操作按钮,防止重复提交,同时保证失败后可以重试。 + +#### Scenario: 新资产属于其他店铺时展示后端错误 + +- **GIVEN** 用户选择了不允许用于当前换货流程的新资产 +- **WHEN** 后端返回“新资产属于其他店铺”或等价归属校验错误 +- **THEN** 系统 MUST 直接展示后端错误信息 +- **AND** 系统 MUST NOT 提供手工修改目标店铺的替代入口 + +#### Scenario: 提交期间禁用按钮防止重复提交 + +- **WHEN** 用户提交创建、发货或确认完成操作 +- **THEN** 当前操作按钮 MUST 进入禁用或 loading 状态直到请求结束 +- **AND** 在请求完成前的重复点击 MUST NOT 触发第二次提交 + +#### Scenario: 请求失败后恢复可重试状态 + +- **WHEN** 创建、发货或确认完成请求失败 +- **THEN** 系统 MUST 恢复当前操作按钮可用状态 +- **AND** 页面 MUST 保留当前已展示的资产与归属提示,便于用户修正后重试 diff --git a/openspec/changes/update-exchange-inherited-shop-hint/tasks.md b/openspec/changes/update-exchange-inherited-shop-hint/tasks.md new file mode 100644 index 0000000..b058979 --- /dev/null +++ b/openspec/changes/update-exchange-inherited-shop-hint/tasks.md @@ -0,0 +1,22 @@ +## 1. API Contract + +- [x] 1.1 更新换货数据模型,补充 `inherited_shop_id` 和 `inherited_shop_name` 响应字段,并覆盖创建、发货、确认完成及详情读取场景。 +- [x] 1.2 保持 `POST /api/admin/exchanges`、`POST /api/admin/exchanges/{id}/ship`、`POST /api/admin/exchanges/{id}/complete` 的既有入参语义,不新增目标店铺字段。 + +## 2. Exchange Create And Ship UI + +- [x] 2.1 在换货创建入口中,选择旧资产后展示旧资产所属店铺。 +- [x] 2.2 在换货创建入口和发货确认入口中,选择新资产后展示只读提示 `换货完成后将归属:{店铺名称}`。 +- [x] 2.3 确保上述页面不新增目标店铺选择控件,也不允许手工覆盖继承归属。 + +## 3. Exchange Detail And Submission States + +- [x] 3.1 在换货详情页展示继承后的店铺名称,并在发货或确认完成成功后重新拉取详情。 +- [x] 3.2 在创建、发货、确认完成提交期间禁用操作按钮,防止重复提交。 +- [x] 3.3 在新资产归属校验失败时展示后端错误,并在失败后恢复可重试状态。 + +## 4. Verification + +- [x] 4.1 验证创建、发货确认、详情三个入口均能正确展示旧资产所属店铺和继承归属店铺。 +- [x] 4.2 验证加载中、失败提示、重复提交防护和成功后详情刷新的交互状态。 +- [x] 4.3 运行相关前端校验,并执行 `openspec validate update-exchange-inherited-shop-hint --strict`。 diff --git a/src/api/modules/asset.ts b/src/api/modules/asset.ts index 5e83a46..540e15d 100644 --- a/src/api/modules/asset.ts +++ b/src/api/modules/asset.ts @@ -54,9 +54,14 @@ export class AssetService extends BaseService { */ static resolveAsset( identifier: string, - params?: AssetResolveParams + params?: AssetResolveParams, + config?: Record ): Promise> { - return this.getOne(`/api/admin/assets/resolve/${identifier}`, params) + return this.getOne( + `/api/admin/assets/resolve/${identifier}`, + params, + config + ) } /** diff --git a/src/api/modules/exchange.ts b/src/api/modules/exchange.ts index d6ae11f..5bc8dcb 100644 --- a/src/api/modules/exchange.ts +++ b/src/api/modules/exchange.ts @@ -47,6 +47,8 @@ export interface ExchangeResponse { recipient_address?: string express_company?: string express_no?: string + inherited_shop_id?: number | null + inherited_shop_name?: string | null remark?: string created_at: string updated_at: string @@ -114,8 +116,8 @@ export class ExchangeService extends BaseService { * POST /api/admin/exchanges/{id}/complete * @param id 换货单ID */ - static completeExchange(id: number): Promise { - return this.post(`/api/admin/exchanges/${id}/complete`, {}) + static completeExchange(id: number): Promise> { + return this.post>(`/api/admin/exchanges/${id}/complete`, {}) } /** diff --git a/src/views/asset-management/exchange-management/detail.vue b/src/views/asset-management/exchange-management/detail.vue index c574693..f468540 100644 --- a/src/views/asset-management/exchange-management/detail.vue +++ b/src/views/asset-management/exchange-management/detail.vue @@ -45,6 +45,12 @@ const exchangeDetail = ref(null) const exchangeId = ref(0) + const formatExchangeShopName = (shopName?: string | null, shopId?: number | null) => { + if (shopName) return shopName + if (shopId === null || shopId === 0) return '平台' + return '--' + } + const getStatusType = (status: number): TagProps['type'] => { const types: Record = { 1: 'warning', @@ -96,6 +102,11 @@ label: '新资产标识符', formatter: (value) => value || '--', prop: 'new_asset_identifier' + }, + { + label: '继承归属店铺', + formatter: (_, data) => + formatExchangeShopName(data.inherited_shop_name, data.inherited_shop_id) } ] }) diff --git a/src/views/asset-management/exchange-management/index.vue b/src/views/asset-management/exchange-management/index.vue index a341a31..4adffdc 100644 --- a/src/views/asset-management/exchange-management/index.vue +++ b/src/views/asset-management/exchange-management/index.vue @@ -170,6 +170,9 @@ + + + + +
{{ createDirectInheritedShopHint }}
+
@@ -250,7 +256,9 @@ - + + + + + + + + + - - + + + + + + + + + @@ -189,12 +193,17 @@ import { useI18n } from 'vue-i18n' import { mittBus } from '@/utils/sys' import { useMenuStore } from '@/store/modules/menu' + import { useNotificationStore } from '@/store/modules/notification' + import { useAuth } from '@/composables/useAuth' + import { NOTIFICATION_PERMISSIONS } from '@/config/constants/notification' import AppConfig from '@/config' const isWindows = navigator.userAgent.includes('Windows') const { locale } = useI18n() const settingStore = useSettingStore() const userStore = useUserStore() + const notificationStore = useNotificationStore() + const { hasAuth } = useAuth() const router = useRouter() const { @@ -214,7 +223,6 @@ const { menuList } = storeToRefs(useMenuStore()) const showNotice = ref(false) - const notice = ref(null) const userMenuPopover = ref() const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT) @@ -229,6 +237,11 @@ const { width } = useWindowSize() + const unreadCount = computed(() => notificationStore.unreadCount) + const formattedUnreadCount = computed(() => + unreadCount.value > 99 ? '99+' : String(unreadCount.value) + ) + const menuTopWidth = computed(() => { return width.value * 0.5 }) @@ -251,11 +264,9 @@ onMounted(() => { initLanguage() - document.addEventListener('click', bodyCloseNotice) - }) - - onUnmounted(() => { - document.removeEventListener('click', bodyCloseNotice) + if (hasAuth(NOTIFICATION_PERMISSIONS.page)) { + void notificationStore.refreshUnreadCount().catch(() => undefined) + } }) const { isFullscreen, toggle: toggleFullscreen } = useFullscreen() @@ -334,24 +345,6 @@ mittBus.emit('openSearchDialog') } - const bodyCloseNotice = (e: any) => { - let { className } = e.target - - if (showNotice.value) { - if (typeof className === 'object') { - showNotice.value = false - return - } - if (className.indexOf('notice-btn') === -1) { - showNotice.value = false - } - } - } - - const openChat = () => { - mittBus.emit('openChat') - } - const lockScreen = () => { mittBus.emit('openLockScreen') } diff --git a/src/components/core/layouts/art-header-bar/style.scss b/src/components/core/layouts/art-header-bar/style.scss index 5febcf3..fb75068 100644 --- a/src/components/core/layouts/art-header-bar/style.scss +++ b/src/components/core/layouts/art-header-bar/style.scss @@ -200,6 +200,10 @@ } } + &.notice-button { + position: relative; + } + &.chat-button:hover { i { animation: shake 0.5s ease-in-out; @@ -411,6 +415,21 @@ } } } + + .notice-count { + position: absolute; + top: 2px; + right: 0; + min-width: 16px; + height: 16px; + padding: 0 4px; + font-size: 10px; + line-height: 16px; + color: #fff; + text-align: center; + background: var(--el-color-danger); + border-radius: 10px; + } } @keyframes rotate180 { diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue index c662c5f..78d43fc 100644 --- a/src/components/core/layouts/art-notification/index.vue +++ b/src/components/core/layouts/art-notification/index.vue @@ -1,276 +1,168 @@ From eb1376302034fe899deec3605388fa4c55c29512 Mon Sep 17 00:00:00 2001 From: luo Date: Fri, 24 Jul 2026 17:42:36 +0800 Subject: [PATCH 27/76] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E5=A5=97?= =?UTF-8?q?=E9=A4=90=E5=88=86=E9=85=8D=E7=94=9F=E6=95=88=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- q | 1520 +++++++++++++++++ src/api/modules/shopPackageAllocation.ts | 15 + src/types/api/packageManagement.ts | 20 +- .../package-list/detail.vue | 8 + .../package-management/package-list/index.vue | 7 + .../series-grants/detail.vue | 56 +- .../series-grants/index.vue | 60 +- .../series-grants/packages.vue | 53 +- 8 files changed, 1649 insertions(+), 90 deletions(-) create mode 100644 q diff --git a/q b/q new file mode 100644 index 0000000..22b336c --- /dev/null +++ b/q @@ -0,0 +1,1520 @@ +commit d1ff4d5f6c433c3fbdac9141677294a6c4c2a569 (HEAD -> develop, origin/develop) +Author: luo +Date: Fri Jul 24 17:12:29 2026 +0800 + + feat: 顶部通知铃铛与站内通知中心 + +commit 4c6d871c350e19e3989cc22a97e7c8d293e35893 +Author: luo +Date: Fri Jul 24 16:30:58 2026 +0800 + + feat: 退款企微审批详情与业务处理状态 + +commit 9a84cd0d316276f124e0621767ce9f90e3d2c999 +Author: luo +Date: Fri Jul 24 15:45:55 2026 +0800 + + feat: 批量订购上传支付进度与失败明细 + +commit e043fd86d1437426c64bce036835e924005b0111 +Author: luo +Date: Fri Jul 24 12:03:04 2026 +0800 + + feat: 角色默认信用与店铺实际额度管理 + +commit a0c44ff538baa82de9539bb7f55b9efb14c8caf8 +Author: luo +Date: Fri Jul 24 11:48:02 2026 +0800 + + feat: 七月迭代公共状态与异步任务交互 + +commit 15122e225a968fa49ff35b59d2a8fbae6164c453 +Author: luo +Date: Fri Jul 24 09:31:11 2026 +0800 + + feat: 预计最终到期时间展示新增具体 + +commit 127fdb560bbfca6f065e1997bc5a441910f55027 +Author: luo +Date: Thu Jul 23 17:59:07 2026 +0800 + + fix: 样式优化套餐分配生效条件选择 + +commit 5e15127a8976234388db4d2a9a0d63f32459eda8 +Author: luo +Date: Thu Jul 23 17:06:05 2026 +0800 + + feat: 接入新参数:套餐分配生效条件选择,资产详情前代后代换货标识 + +commit d7c2c146fe7303cbb930c6b7b85813565bb99932 +Author: luo +Date: Thu Jul 23 14:10:48 2026 +0800 + + feat: 角色默认信用与店铺实际额度管理 + +commit d5fd8ac564d7607cd3b932e2c62f00390867a27e +Author: luo +Date: Wed Jul 22 18:37:41 2026 +0800 + + feat: 退款充值换货列表提交人与审批摘要 + +commit 7d3352038a5725a31cc9b134170b70a4e569002c +Author: luo +Date: Wed Jul 22 16:57:17 2026 +0800 + + feat: 换货新旧资产展示与独立搜索 + +commit 8faa2ea2efbdb1ba1c1963f094713f9ad57842ed +Author: luo +Date: Wed Jul 22 16:31:01 2026 +0800 + + feat: 预计最终到期时间展示 + +commit 8de4339505f68851ac29d7375f2822c3e83d0f2d +Author: luo +Date: Wed Jul 22 15:44:48 2026 +0800 + + feat: 卡和设备实名状态筛选 + +commit 68aa03b53833092807381a57129e00559d455d8e +Author: luo +Date: Wed Jul 22 14:21:15 2026 +0800 + + feat: 套餐分配生效条件选择 + +commit fbfdd01eec1c2b25077e0418f5217f5630134fd5 +Author: luo +Date: Wed Jul 22 10:14:13 2026 +0800 + + feat: 换货退款拦截错误展示 + +commit 5079f973262c7301f3bffb7da0e133ab7238f3fc +Author: luo +Date: Tue Jul 21 18:20:35 2026 +0800 + + feat: 店铺列表新增条件 + +commit 830476d49d92064819444ad3843bd57852f1bb69 +Author: luo +Date: Tue Jul 21 16:13:40 2026 +0800 + + H5实名购买顺序与后台批量配置 + +commit 47a2d88c9d2507ddfaeae8a7eee140985737507d +Author: luo +Date: Tue Jul 21 14:30:30 2026 +0800 + + 复机实名校验提示适配 + +commit 6febc66eca7e2bf2f22590d9fddf50e8a1af8c13 +Author: luo +Date: Tue Jul 21 11:50:47 2026 +0800 + + feat: 资产同步状态与同步轨迹入口-新 + +commit 45d255cabfa268e37ac05f13d7aa4ded2f2772c0 +Author: luo +Date: Tue Jul 21 11:13:20 2026 +0800 + + feat: 资产详情前代后代换货标识 + +commit eddfd157adab656c130c3d27e9928d638fec987c +Author: luo +Date: Tue Jul 21 09:57:14 2026 +0800 + + feat: 资产同步状态与同步轨迹入口 + +commit 09225dd8bcb038aa841f62c042bf85364be24497 +Author: csxj2026 +Date: Mon Jul 20 15:33:35 2026 +0800 + + 更新 Dockerfile + +commit 563804f67f94d21e28650de1344eaf00e9291090 +Author: csxj2026 +Date: Mon Jul 20 14:46:19 2026 +0800 + + 更新 docker-compose.prod.yml + +commit e95dc8e4f52c6e9ed99eeab7d51bfea66d81fea4 +Author: luo +Date: Mon Jul 20 12:17:02 2026 +0800 + + feat:代理现金余额不足100元展示、店铺业务员选择展示与筛选 + +commit ca338dd345f22cff6d3df02baaca1e5610ed29bb +Author: luo +Date: Mon Jul 20 11:02:05 2026 +0800 + + fix: 测试环境显示develop分支 + +commit fac05db39afc818668193f4928f51f70e5b5554f +Author: luo +Date: Mon Jul 20 10:50:23 2026 +0800 + + feat: 换货新资产归属继承提示 + +commit 2c8524485bd0b76fc472f4dd6da4c0e41e779278 +Author: luo +Date: Mon Jul 13 16:40:35 2026 +0800 + + fix: 资产信息字段显示隐藏根据角色 + +commit 4c0207c6e697cafb752502fe551fb2ab7e64ce4b +Author: luo +Date: Fri Jul 10 18:09:19 2026 +0800 + + fix: 将微信配置改成支付配置 + +commit 5b8d6610ab7f35d2111f6fe504d7e18c4b215775 +Author: luo +Date: Thu Jul 9 17:38:58 2026 +0800 + + fix: 将套餐系列的套餐加滚动条 + +commit a036b106412a480138910529e170665fc6e7f634 +Author: luo +Date: Thu Jul 9 15:59:39 2026 +0800 + + fix: 将权限管理里面的分页去掉 + +commit c1a7118d35bafbdf12d422d71298ca47a43523fb +Author: luo +Date: Thu Jul 9 15:20:28 2026 +0800 + + fix: 将导出列表分成三个模块 + +commit f27bbacb1edf2b423bd88b40a70d19f32e37148e +Author: luo +Date: Mon Jul 6 17:52:08 2026 +0800 + + fix: 设备信息新增的两个流量放到同一行 + +commit acda0d82c840995db0d304e863f14e9a17d91db3 +Author: luo +Date: Fri Jul 3 12:03:52 2026 +0800 + + feat: 新增全部已使用流量和全部总流量字段 + +commit 99de03604d920ccfa20a1a368a2d1353819e937a +Author: break +Date: Fri Jun 26 18:14:37 2026 +0800 + + 驳回 + +commit 1baa6d87cfdcb27b368f45aa4e79e607159bceb7 +Author: break +Date: Fri Jun 26 18:14:26 2026 +0800 + + 驳回 + +commit f604791c69216d09a36fb3340a69ba9c5bb6904d +Author: break +Date: Thu Jun 25 10:30:49 2026 +0800 + + 倒计时没有覆盖复机停机的问题 + +commit cfb6b52cc29e022848cab3864c0a5b9b0688ac37 +Author: break +Date: Wed Jun 24 15:06:05 2026 +0800 + + 倒计时限制 + +commit 0c3065f970763d21704c1b4483cf54e601aa712e +Author: break +Date: Tue Jun 23 09:50:12 2026 +0800 + + 让凭证数组与套餐作废任务进入可联调状态 + + Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传 + purpose 只能使用 attachment。 + Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。 + Confidence: high + Scope-risk: broad + Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与 + order_package_invalidate_task:create/detail 按钮权限。 + Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。 + Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览 + +commit ebf9739f06319fb6ddaffd5e51b27d59138272b4 +Author: luo +Date: Mon Jun 22 11:09:50 2026 +0800 + + fix:sort + +commit d3b5f55c64b774fe994519c06cbbd3f138426dda +Author: luo +Date: Mon Jun 22 10:36:33 2026 +0800 + + feat:更换企业授权 + +commit 866a2587b31e2121f99572e8f0b9b54c6526b3de +Author: luo +Date: Thu Jun 18 10:17:10 2026 +0800 + + feat:export + +commit 85d158dfec727cff7838ca383c8f9dd806b03ddf +Author: luo +Date: Wed Jun 17 17:46:04 2026 +0800 + + feat: order-export,generation,status + +commit 2a8f4e40d65930af4506cfc3882fef2c9c200b7d +Author: luo +Date: Tue Jun 16 11:35:45 2026 +0800 + + feat: 新增导出 + +commit 908367ba5df1bd27ea7dffcc44cb72eb85005a9c +Author: sexygoat <1538832180@qq.com> +Date: Mon Jun 15 10:19:35 2026 +0800 + + fix: new + +commit 2ecab976f5c89161d8565c0568991237439e1136 +Author: sexygoat <1538832180@qq.com> +Date: Sat Jun 13 10:41:10 2026 +0800 + + fix: 新增: 修改资产套餐已用量, 修改资产套餐过期时间 + +commit e04a283319b8bb58648ac58a6a88536dde0d3509 +Author: sexygoat <1538832180@qq.com> +Date: Fri Jun 12 10:41:37 2026 +0800 + + fix: 新增代理系列授权-建议售价使用套餐 + +commit d23e896f97fd2ed475a4eb3a2a68c43c3a51013a +Author: sexygoat <1538832180@qq.com> +Date: Sat Jun 6 15:09:31 2026 +0800 + + fix: 退款审批可以是0 + +commit c5e4082c260a006965c13917250bd9bc22a0b531 +Author: sexygoat <1538832180@qq.com> +Date: Fri Jun 5 16:26:17 2026 +0800 + + fix: 退款审批可以是0 + +commit 0c737a30c12046d2fd60cd97e7d46469e09fa713 +Author: sexygoat <1538832180@qq.com> +Date: Thu Jun 4 10:47:43 2026 +0800 + + fix: 换货 + +commit c5dae2993b0a5547e83855458062f33b4bba1555 +Author: sexygoat <1538832180@qq.com> +Date: Wed Jun 3 17:53:19 2026 +0800 + + fix: 换货 + +commit 3e6b3981f41e5efe591220835a967166e758dd2f +Author: sexygoat <1538832180@qq.com> +Date: Mon Jun 1 14:10:09 2026 +0800 + + fix:订单列表详情实付金额代理/企业不可见 + +commit e563b81b6e8ef49ef70bcbc51e82f2b212cca29b +Author: sexygoat <1538832180@qq.com> +Date: Mon Jun 1 11:41:52 2026 +0800 + + fix:操作审计日志-style + +commit 322cd0dca5baf82d7d4eb7b6c4c223d3006476f9 +Author: sexygoat <1538832180@qq.com> +Date: Sat May 30 15:06:41 2026 +0800 + + fix: 订单列表套餐显示销售价 + +commit ad56b82944abc409dd92f45a84a777fe33327951 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 29 15:44:53 2026 +0800 + + feat: 新增代理流水资产标识 + +commit f62a8d98a4b522695ae7aae1fe5cac97322e1151 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 29 12:11:03 2026 +0800 + + fix: 自动更新提示bug + +commit 76a526643eb090a9ad553e0b7ef6918624621ee8 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 29 10:46:01 2026 +0800 + + fix:修改bug: 虚流量关闭的时候还是显示了 + +commit 9ce48f171420f28f73268b67fe3133033c66d227 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 29 09:40:17 2026 +0800 + + fix:本月已用流量和运营商周期月内已用流量 + +commit a162dcc650399aa3e6ab14235ddb44b7ce71ea8c +Author: sexygoat <1538832180@qq.com> +Date: Fri May 29 09:38:30 2026 +0800 + + fix:本月已用流量和运营商周期月内已用流量代理企业不展示,其他账号新增权限 + +commit 868a3352adb90442baa4511fedaa9fe794e864f2 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 28 18:18:42 2026 +0800 + + fix: 新增套餐新增缩长比例,增长比例 + +commit 61e575194d8cc120591625eac31ec3ac3e1fe57f +Author: sexygoat <1538832180@qq.com> +Date: Thu May 28 18:13:08 2026 +0800 + + fix: 新增套餐新增缩长比例,增长比例 + +commit 69b024b5cc2d7e2b19ee718e37b4dccc169c2144 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 28 11:20:33 2026 +0800 + + fix: 虚流量进度条 + +commit 6654972ddb0081ca9f02ff819a273b4b7da1372d +Author: sexygoat <1538832180@qq.com> +Date: Wed May 27 14:58:37 2026 +0800 + + feat: 新增退款凭证 + +commit fe0093972f7ef2a12c7e8ea5af87976aad447382 +Author: sexygoat <1538832180@qq.com> +Date: Tue May 26 18:07:39 2026 +0800 + + feat: 卡新增IMEI + +commit 878d93a8650368cddafc0712f43694292050b371 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 22 16:07:47 2026 +0800 + + fix: 微信配置 + +commit b364fb4aec5aa27d636d9b153603d94b685c2d48 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 21 15:43:36 2026 +0800 + + fix: 在线 + +commit a69f972766f3b3b6a2011d45b981a6533dd1fde0 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 21 14:53:36 2026 +0800 + + fix: 格式化时间 + +commit dfb78006acf14b71eb268ac2e0ed86ce084d894c +Author: sexygoat <1538832180@qq.com> +Date: Thu May 21 14:39:21 2026 +0800 + + fix: 分页器一直显示和资产信息使用单卡 + +commit 893921007a9e9ad815fc6ce4969aec3d31dc0383 +Author: sexygoat <1538832180@qq.com> +Date: Wed May 20 11:24:52 2026 +0800 + + feat: 设备管理新增imei和虚拟号 + +commit 59167d2630caef5ecbdd8861b83de3312c1f7954 +Author: sexygoat <1538832180@qq.com> +Date: Tue May 19 15:14:39 2026 +0800 + + feat: 卡/设备是否有生效套餐 + +commit 14832f5c6f3d51b53cb16d0b172e229985d0486c +Author: sexygoat <1538832180@qq.com> +Date: Mon May 18 13:11:40 2026 +0800 + + fix: 拖拽上传 + +commit 894aea0e74f118fdf56f9a3c65b36c54984c934f +Author: sexygoat <1538832180@qq.com> +Date: Mon May 18 12:23:35 2026 +0800 + + feat: 新增卡视角以及设备视角的实名同步时间,卡状态同步时间,流量同步时间 + +commit 877c741d728a15902fbc3b96201e4e93f95706f8 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 15 16:53:00 2026 +0800 + + fix: 最后同步时间 + +commit aeb08f2cc159c476c28e2714171a2df88922ad8d +Author: sexygoat <1538832180@qq.com> +Date: Fri May 15 16:11:47 2026 +0800 + + fix: 时区去掉使用本地 + +commit c7e2d6fe533d83a65058d4c9f6be5dca32be05a3 +Author: sexygoat <1538832180@qq.com> +Date: Fri May 15 11:16:57 2026 +0800 + + fix: get-list + +commit f3b5444dd8d56dd06352318ab729fa33be065130 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 14 15:52:38 2026 +0800 + + fix: 换货原因改成下拉 + +commit 38cf7d12e90d406ce13c726da8c868d7c552fda9 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 14 10:52:11 2026 +0800 + + fix: 真流量虚流量权限 + +commit 1690252740a9fc014d445ecdf5356f9f32ef07a4 +Author: sexygoat <1538832180@qq.com> +Date: Tue May 12 14:54:47 2026 +0800 + + feat: 新增设备激活时间 + +commit 471d1196b3062b18d5dd05dd67735f04b566c04f +Author: sexygoat <1538832180@qq.com> +Date: Tue May 12 12:11:53 2026 +0800 + + feat: 新增运营商停机原因 + +commit 5d20d7dd3f103e9cdf5cbc14fde46fc5e4427171 +Author: sexygoat <1538832180@qq.com> +Date: Mon May 11 18:01:22 2026 +0800 + + fix: 订单详情支付凭证, 退款 + +commit 1990a3ccbac7e86439806a61fc037b3c3f29b59e +Author: sexygoat <1538832180@qq.com> +Date: Mon May 11 15:51:50 2026 +0800 + + fix: bug + +commit 766497dc316958a9bcac926489d434d946bbaf0b +Author: sexygoat <1538832180@qq.com> +Date: Mon May 11 14:51:13 2026 +0800 + + fix: bug + +commit e59c75bf9e42050e3794f03749c65a4055b70142 +Author: sexygoat <1538832180@qq.com> +Date: Mon May 11 14:47:07 2026 +0800 + + fix: bug + +commit eeeef931b1547327c510fa979ea416942a3425c1 +Author: sexygoat <1538832180@qq.com> +Date: Mon May 11 14:35:38 2026 +0800 + + fix: bug + +commit 73ce9af11a5c12a5b9cb66eb6b557b31da4033f8 +Author: sexygoat <1538832180@qq.com> +Date: Sat May 9 16:13:11 2026 +0800 + + fix: 代理系列授权店铺 + +commit 3c71ae0004e71e0717dbc0c76003f400dfac1daf +Author: sexygoat <1538832180@qq.com> +Date: Thu May 7 17:48:43 2026 +0800 + + fix:name + +commit 43de0e3e9e6416ed00309aba130c34ab1456915b +Author: sexygoat <1538832180@qq.com> +Date: Thu May 7 17:45:05 2026 +0800 + + fix:name + +commit 78fe35da01d0839790fac967b39fb0d25de4a235 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 7 17:34:06 2026 +0800 + + fix:name + +commit 90d415cfb9a15cbc602ff819ff7728a86956ac56 +Author: sexygoat <1538832180@qq.com> +Date: Thu May 7 09:18:03 2026 +0800 + + fix: bug + +commit 304a5de1d6d2b9a5e7c19c02bf1ea1ea0c6ab1e4 +Author: sexygoat <1538832180@qq.com> +Date: Wed May 6 11:13:51 2026 +0800 + + fix: tips + +commit 883215056253417a09eee93a05ba7ab2f4f6b312 +Author: sexygoat <1538832180@qq.com> +Date: Wed May 6 10:54:43 2026 +0800 + + feat: sn + +commit 950520635da9d3c5ab6da97ca2184291d67811b4 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 30 18:04:01 2026 +0800 + + feat: 文件下载 + +commit 81155fd8e442149eac514600ff66a2c24d9966f8 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 30 17:01:00 2026 +0800 + + fix:进度条 + +commit 78a41513bd01913f352119b4aa5045b2d08be346 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 30 15:16:15 2026 +0800 + + fix: 字段显示和订单详情按钮 + +commit 57ddbe5f8585fa0cdc84a706e21e4d2bbcff83ba +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 30 12:04:04 2026 +0800 + + fix: order + +commit 044c07fae697d3a3152c0769ebe995aa1d6b9ba9 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 30 10:39:51 2026 +0800 + + fix: order + +commit 1a4eb00440765b846db8fa8475966edb783f2e3e +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 17:09:23 2026 +0800 + + fix: 新增代理系列授权 + +commit 1141b6c92daf9ca1c0df3a609cb34d844528ea5e +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 16:11:54 2026 +0800 + + fix: 新增代理系列授权 + +commit 9b4d956032fdf335c0953ddcb29e07aa634211ef +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 12:49:30 2026 +0800 + + del: 激活时间 + +commit 175fe55be79b9222c7e06f6247b6a5586e85cf85 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 11:40:38 2026 +0800 + + del: 激活时间 + +commit 3d34040bcc77221796a3016cf9060d3885a495ce +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 11:30:25 2026 +0800 + + fix: 套餐 + +commit 1dc4701540738bd7d026db62fd4128cf12ba2259 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 29 11:29:53 2026 +0800 + + fix: 套餐 + +commit 686bddad796a96550f10f62ee3d032222bb214d3 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 18:52:43 2026 +0800 + + fix: 套餐 + +commit b6625748ac661a23c96462fdf4b56395c590f595 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 18:40:18 2026 +0800 + + fix: 套餐 + +commit 55515410ae9846d0a5517c52777de05137ff466c +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 16:04:13 2026 +0800 + + fix: 设备信息顺序 + +commit c80ff80bf09059349323b82174e6fdb9224a28a9 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 15:32:33 2026 +0800 + + fix: 账号管理 + +commit 317afd2d23805aa28ca5982d8009ecfa796590bc +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 15:20:22 2026 +0800 + + fix: 账号管理 + +commit a48ce0cb5483a57c1500e46606c851daa97a19d8 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 14:48:44 2026 +0800 + + fix: 控制台加权限,登录跳转资产信息 + +commit 1ba662de0a9414aca68cf0dbd27ea8b906c74fec +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 12:03:42 2026 +0800 + + fix: 控制台加权限,登录跳转资产信息 + +commit fb10b727600b30129e49f2af25b24f71e9bda06a +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 11:14:13 2026 +0800 + + fix: 手动触发 + +commit b88f2d185ef93688507942f421caac92db029b79 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 10:18:30 2026 +0800 + + fix: type + +commit 4f1cab614915f7192ea5839b26a638ed82bf7ebd +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 09:56:03 2026 +0800 + + fix: role_type + +commit ffdc6e2bbb1f734ed669268c14ce632918c50259 +Author: sexygoat <1538832180@qq.com> +Date: Tue Apr 28 09:38:52 2026 +0800 + + feat: 添加停机断点 + +commit af3f38aba0a4bdb514f079dba71fe8b253a6b89f +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 18:34:02 2026 +0800 + + fix: 操作 + +commit 030195d609024f58732f1ecd4d664b409855afb2 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 17:46:20 2026 +0800 + + fix: 操作 + +commit d0c1f1622cd26814c23ea3fe6b4638b6a2110103 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 17:39:55 2026 +0800 + + fix: 流量显示 + +commit d5c5e07f77084d5422928c3522d04afc8b8a886b +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 16:49:32 2026 +0800 + + feat:操作日志 + +commit a402f2329a6ab218d252c84b57edd95475e41d6d +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 16:05:24 2026 +0800 + + feat:流量展示 + +commit d52efaadcbf5c3c695308330a187f69db690f8f7 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 15:57:31 2026 +0800 + + feat:流量展示 + +commit a055ff166a64e7601d4b0a6cf448137fea81bd3a +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 15:06:27 2026 +0800 + + feat:log + +commit 853c87b8a2bb61950eb7f2284016855ed3ed608c +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 11:59:20 2026 +0800 + + fix: 隐藏虚比,同步订单号 + +commit 6c9b29c53b63e32b5613af9315c715c41c28fecd +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 11:14:20 2026 +0800 + + fix: 套餐订购,界面优化 + +commit 2a3343400563ac6910b09533003377fbcd79295d +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 27 09:59:01 2026 +0800 + + fix: set-wifi + +commit 819fe9080a6313f1f724cbe0faf1ad0f617a0563 +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 25 17:00:11 2026 +0800 + + fix: ui + +commit f0f054390ab63a00d3de1dae33360c14160840cc +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 25 16:23:17 2026 +0800 + + fix: ui + +commit a770984ef73b9b997cdfbc773dc9be0b38407da5 +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 25 15:51:24 2026 +0800 + + fix: bug + +commit 9247001643bba7d2c2326c39103d9a2fc6f15e4c +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 25 12:01:21 2026 +0800 + + fix: bug + +commit 17c299e4ceddccd6ddc287a2e875dedea5a10b0c +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 24 18:39:39 2026 +0800 + + fix: bug + +commit 2d6b5d79038e6713d4fd17aee0cbfe7778f1dc13 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 24 11:48:52 2026 +0800 + + fix: iccid列适配 + +commit ceecc07ffc1f83d2dd3a4f57542c770cfca174f3 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 24 11:28:04 2026 +0800 + + fix: 批量分配 + +commit e4ed3784e3d3678af532c98342b4dab3ece32435 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 24 11:17:39 2026 +0800 + + fix: pay + +commit 62851342c3c5bfe74e18f97dd74fa55d90f5dd69 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 24 09:55:44 2026 +0800 + + fix: permission + +commit aaa648a86d35ca94cdeface523e632b0d82d9f3e +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 18:35:29 2026 +0800 + + feat: 支付凭证 + +commit cd2ca96873646b070f00dd76c523c7a8b5579791 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 18:13:07 2026 +0800 + + feat: 手动实名和套餐列表退款 + +commit 719db517f2a1b01877b4fc46a93cb7b16ec206e9 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 16:21:12 2026 +0800 + + fix: bug + +commit 2140efbacf576d8f4480611bdfa87f669e4e29ce +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 16:08:07 2026 +0800 + + fix: bug + +commit 8cc86369a23cd01cb6266f72325875e0b747138f +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 16:03:54 2026 +0800 + + fix: bug + +commit 8ca03ed7ac858e803696eb2e1c6f2508788742aa +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 15:52:18 2026 +0800 + + fix: bug + +commit 0bc2efdf0ea54a49ea81cbb895a7d885de3fa453 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 15:32:34 2026 +0800 + + fix: bug + +commit a5e76313cb15682b37f09411d32337c5443c3007 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 23 14:59:05 2026 +0800 + + fix: bug + +commit a9467e8a0cdd2168e4756d391b75e0a9266d48f0 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 22 18:33:52 2026 +0800 + + fix: 修改工单bug + +commit fd22dc7ad41bc733e2b1dad0ef7ec95de9848b76 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 22 17:42:36 2026 +0800 + + fix: 修改工单bug + +commit 8378d2228e08b58b122f97decd2e3a3cedc74086 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 22 15:26:55 2026 +0800 + + feat: 新增按钮授权 + +commit bce9054b829ef75c843cebff428089e97be502a9 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 22 15:00:37 2026 +0800 + + feat: 新增按钮授权 + +commit 936a4f06567faf5622b2fa1113fe9535981e9de6 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 20 17:51:03 2026 +0800 + + fix: req:bug + +commit 11bfcadf6585b0180ffb636a5a9a2182c3c48f48 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 20 16:28:09 2026 +0800 + + fix: wifi 自动填充 + +commit 9b66b78110417f76d50b75ea29214a0697aa984c +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 20 15:58:18 2026 +0800 + + feat: 当前生效套餐 + +commit 3acb626a3462306c6e748ba7f76ebb8fbea7ceb8 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 20 15:10:23 2026 +0800 + + feat: 新增更新资产实名认证策略 + +commit d721c4b8a90eb4bc4a203b46fd463cbad14041fa +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 20 11:45:59 2026 +0800 + + fix: 设备分配代理,虚比例,sim顺序 + +commit c5ee690ccd26bcc15fe661ffc5920efeb63c0421 +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 18 16:40:41 2026 +0800 + + feat: 新增设备切卡模式 + +commit fd583e81e074c588ba58b7ddb528f72a8b08854d +Author: huang +Date: Sat Apr 18 12:24:18 2026 +0800 + + fix: 修复部署后切换页面 JS chunk 404 白屏问题 + + - main.ts 监听 vite:preloadError,chunk 加载失败时自动刷新一次 + - 新增 ChunkErrorBoundary 组件,捕获 Vue 渲染层 chunk 加载异常 + - App.vue 包裹 ChunkErrorBoundary + - docker/nginx.conf 修复缓存策略:index.html 不缓存,/assets/ 长缓存 immutable + +commit b48d6a1894f3ea42976aa8ec96b6492b92b8a1db +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 17 18:40:54 2026 +0800 + + feat: 新增导入字段,显示实名认证策略字段 + +commit 90b13eb5ec0f795409055ba87a9b9b4ddac016a3 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 17 16:37:41 2026 +0800 + + fix: 取消关联,居左,店铺列表 + +commit 5adeade9af5fbbe6fa0e7b9f352b1620e6925ff0 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 17 14:56:53 2026 +0800 + + fix: 移除订单详情ID相关参数,新增iccid/VirtualNo + +commit 0e0c3e18d05cd82ccf5a09737142c6d17c4a8d9b +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 17 10:27:52 2026 +0800 + + feat: 分析页新增权限 + +commit a3bc025636d0aae9915bfb35ca6ea4aa991ae26d +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 16 16:36:27 2026 +0800 + + fix: 卡列表删除一次性佣金和累计充值 + +commit cb3105504ebe4b1398ca3825e4845f6e62b19b49 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 16 10:12:30 2026 +0800 + + fix: 修复排序 + +commit 9617cdd825886b799149e94af01284ef035bd1b7 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 15 17:29:53 2026 +0800 + + fix: 注释停用资产 + +commit 209b5342b92d83f1bca097cd537c94e4942218dc +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 15 17:12:50 2026 +0800 + + feat: ICCID新增设备店铺名称,套餐系列名称 + +commit 1931a096b008a62a4f147d3b3961b99ced2f4087 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 15 10:14:53 2026 +0800 + + feat: ICCID新增设备店铺名称,套餐系列名称 + +commit d373c2e70eef1e0730d4d0066ff7ff5b74efa5da +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 15 09:59:05 2026 +0800 + + feat: ICCID新增复制, 宽度自适应 + +commit b5eb4fdbcec27808a6e32a91b263cf1a5bbc7495 +Author: sexygoat <1538832180@qq.com> +Date: Mon Apr 13 14:35:19 2026 +0800 + + fix: 修复微信配置编辑不传递脱敏 + +commit 1acb9db38e894d99b51636c24e769c8cc88c7b96 +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 11 17:04:50 2026 +0800 + + fix: 关联账号列表单选框 + +commit 4f02d6faf0984584aff68372713039a192ed9c70 +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 11 16:51:54 2026 +0800 + + fix: bug + +commit 33e15314ac504fcfec72e9fba4138eeeffde591a +Author: sexygoat <1538832180@qq.com> +Date: Sat Apr 11 12:19:35 2026 +0800 + + bug + +commit 90ae585651ece1536443f494d751768e38a82e84 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 10 16:54:53 2026 +0800 + + fix(operator): fix operator edit issue + +commit 8b30341d501f79240ca043a67c1ed5bc351066ef +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 10 15:06:28 2026 +0800 + + fix(operator): fix operator edit issue + +commit 2b3119c5497f07aec9d2f4f489be6b8b6fd32683 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 10 14:00:21 2026 +0800 + + fix(operator): fix operator edit issue + +commit 9a6f085cdead297d5b73bc567307db42cfdb20e2 +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 10 09:50:00 2026 +0800 + + fix(operator): fix operator edit issue + +commit c2c1644799ccbd05c26001e34b829f8f7dafd512 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 17:47:32 2026 +0800 + + 修复bug + +commit 472099bf96cc36dd8336226823e8c73a135508d4 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 16:56:30 2026 +0800 + + 修改bug + +commit 6a46aeea158a67b11a463d619c1570c8440590d7 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 15:57:51 2026 +0800 + + 修改bug + +commit b171e7cc0389d94843cdd3063ea2275c8d9338e3 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 15:32:20 2026 +0800 + + 修改bug + +commit 4656d403c21154f970ed80a410828c3fe33ae918 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 14:08:25 2026 +0800 + + 修改bug + +commit cb3ba061060462add086a4eea18121765a34fdd5 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 9 13:48:11 2026 +0800 + + 修改bug + +commit cc9d2a85a2e493785dbe56b5b53011b016020b49 +Merge: d1c6588 6a6d3dd +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 8 19:35:20 2026 +0800 + + chore: merge main branch from remote + +commit d1c6588d8fc79c2acbb872a7b1b648eee89b1f8c +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 8 19:31:22 2026 +0800 + + 删除多余代码 + +commit 6a6d3dd3943791ceca28ce6ecfcb0f47dd3d7c4a +Author: huang +Date: Wed Apr 8 11:20:48 2026 +0800 + + 修复: nginx 禁止 index.html 缓存,防止部署后资源 404 + + Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) + + Co-authored-by: Sisyphus + +commit b510b4539f36c2310afbf977fae07d492de5e016 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 8 10:19:25 2026 +0800 + + 修复bug + +commit ff087abd44837a51a1e173428f02445b54bfec0f +Author: sexygoat <1538832180@qq.com> +Date: Fri Apr 3 11:47:11 2026 +0800 + + 修复前端bug + +commit ea14ae92cae67f2e813dd1e2cd852db2a680b266 +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 2 15:17:55 2026 +0800 + + 绑定卡列表操作卡 + +commit 8df7024a454475d6ec30ede3081920df783db02c +Author: sexygoat <1538832180@qq.com> +Date: Thu Apr 2 13:42:57 2026 +0800 + + modify + +commit 411206e039b88c39fdafdca857fe868bee7c14d0 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 1 11:10:13 2026 +0800 + + feat(network_status):停机/正常 + +commit afeb1e7c891757d08ddfe52c8edd6b6ccc11e726 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 1 10:56:26 2026 +0800 + + style(order): 修改订单管理标题 + +commit 0d3fc5ce7f1c8cdda7cb3ca331c41b34e0a80597 +Author: sexygoat <1538832180@qq.com> +Date: Wed Apr 1 10:41:43 2026 +0800 + + package + +commit 01f81c2178fd95b60cdcbea58ee68985778366c2 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 31 16:55:54 2026 +0800 + + 新增运营商 + +commit 3372be99b8d2dc1661dad39f453e6ff1b7e85b04 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 31 15:13:19 2026 +0800 + + 修复 + +commit 104d62b17d9c1df99a4f614c640b72a3a3cc44e7 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 31 10:15:10 2026 +0800 + + 修复: list->items + +commit 36da079d59fc81dd5407bbe039d45742139fe518 +Author: sexygoat <1538832180@qq.com> +Date: Mon Mar 30 18:16:39 2026 +0800 + + 修复详情404 + +commit a05717c2ed1a507572bd869e96a18b6b5d77342c +Author: sexygoat <1538832180@qq.com> +Date: Mon Mar 30 14:59:56 2026 +0800 + + 新增: 轮询管理 + +commit dbe607020791da8b3ad99ec8f86c328612729ea4 +Author: sexygoat <1538832180@qq.com> +Date: Sat Mar 28 18:31:43 2026 +0800 + + modify-bug + +commit d6c23c8bd1265c5f1a681f16b5a9919ea1b72ef3 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 24 16:51:21 2026 +0800 + + 搜索放到重置前面 + +commit 2af0346d04dc60eb9e35cff2c7ca742d2635f3c2 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 24 16:47:52 2026 +0800 + + 修改资产信息字段排序 + +commit a805c279022814fcbff6073fbf2bd5f0e77706bf +Author: sexygoat <1538832180@qq.com> +Date: Fri Mar 20 14:18:47 2026 +0800 + + 修改资产详情 + +commit 62f828f3142c244a84b7ec493efb592eb86909a2 +Author: sexygoat <1538832180@qq.com> +Date: Fri Mar 20 10:26:57 2026 +0800 + + 修改资产详情 + +commit f06d8c9133dd7d5687826ea3eefe95b63e6e8b73 +Author: sexygoat <1538832180@qq.com> +Date: Thu Mar 19 18:32:02 2026 +0800 + + 新增 + +commit 407287f5386597b053069825277a9144df21b8da +Author: sexygoat <1538832180@qq.com> +Date: Wed Mar 18 15:33:39 2026 +0800 + + 删除资产信息的进度条 + +commit 9fb3367fd7e6fefa213afe22730cc7d4e3a8d5c0 +Author: sexygoat <1538832180@qq.com> +Date: Wed Mar 18 10:47:25 2026 +0800 + + 完善充值代理 + +commit ff67681c29de9f0ea1adf990e6ebe7ac9dc28c5e +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 17 14:27:17 2026 +0800 + + 修改布局 + +commit e975e6af4bc9f1ac891e3192e018047c24da1599 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 17 14:06:38 2026 +0800 + + 新增: 微信配置-代理充值 + +commit f4ccf9ed24bd5d7f9af96e87e1980f3e994c5aa3 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 17 09:31:37 2026 +0800 + + 修改bug + +commit 8f315264998a64d198fe9fe1f269d57db33bb00a +Author: sexygoat <1538832180@qq.com> +Date: Sat Mar 14 15:16:21 2026 +0800 + + fix: bug + +commit d43de4cd06c74b0f4910b43bd53c3cc160e7af28 +Author: sexygoat <1538832180@qq.com> +Date: Wed Mar 11 17:09:35 2026 +0800 + + 修改bug + +commit bd45f7a2246a45aea83a2ba0db5fbefaeaea05b6 +Author: sexygoat <1538832180@qq.com> +Date: Sat Mar 7 11:59:16 2026 +0800 + + 详情添加权限 + +commit e73992d253f82be2abb27ad910fa0b76b35afaf3 +Author: sexygoat <1538832180@qq.com> +Date: Sat Mar 7 11:41:15 2026 +0800 + + 弹窗改为跳转链接 + +commit 8fbc321a5e243db46561f9948d9f5e07e0d9a29e +Author: sexygoat <1538832180@qq.com> +Date: Fri Mar 6 17:51:27 2026 +0800 + + 详情修改 + +commit 1ebc0b8929920a7e966db94fd6139b99fce212c1 +Author: sexygoat <1538832180@qq.com> +Date: Fri Mar 6 16:28:58 2026 +0800 + + 新增代理系列授权 + +commit 4d94f7efa6468a677297d7af4aebaf496365138c +Author: sexygoat <1538832180@qq.com> +Date: Fri Mar 6 14:00:22 2026 +0800 + + 套餐系列:梯度佣金,代理授权 + +commit 08d5043b3fb13e0ee56bfa112d21db72f7b5cd61 +Author: sexygoat <1538832180@qq.com> +Date: Wed Mar 4 17:22:47 2026 +0800 + + 将单套餐分配和套餐系列分配改成代理系列授权 + +commit 237eeed87a9fe7700b2f71212cb3f7e8deaf6f33 +Author: sexygoat <1538832180@qq.com> +Date: Tue Mar 3 16:26:04 2026 +0800 + + 修复:原来的统一去了404, 现在加了403, 和部分bug + +commit 7e9acda1ab7baf8060e59e4c051bce8a8f00583f +Author: sexygoat <1538832180@qq.com> +Date: Sat Feb 28 17:46:42 2026 +0800 + + 修改订单管理 + +commit 7b459b5c8d6c39e94e8195e69128e500fa6a6e52 +Author: sexygoat <1538832180@qq.com> +Date: Sat Feb 28 16:49:28 2026 +0800 + + 修改订单管理 + +commit ce1032c7f9bd3651f9ea67d3e0bdc722feaac42b +Author: sexygoat <1538832180@qq.com> +Date: Sat Feb 28 11:04:32 2026 +0800 + + 完善按钮和详情权限 + +commit 4470a4ef04f43daa82f3e170a7e70975ca2d341d +Author: sexygoat <1538832180@qq.com> +Date: Fri Feb 27 17:40:02 2026 +0800 + + 修改: bug + +commit f1cb1e53c87b7395dbb127933d13f834c1f04ba2 +Author: sexygoat <1538832180@qq.com> +Date: Thu Feb 26 17:34:11 2026 +0800 + + 修改: 权限重复 + +commit 3570b062a1aa898362cd071301a6ab4204484ecd +Author: sexygoat <1538832180@qq.com> +Date: Thu Feb 26 10:06:11 2026 +0800 + + 修改工单: 右键 给角色分配权限 + +commit dccad819cfb23e262e8d5f7a2fee95ac0807bfb9 +Author: sexygoat <1538832180@qq.com> +Date: Wed Feb 25 16:14:38 2026 +0800 + + 修改工单 + +commit ca3184857e6a01a24ca6d583251cb3ca143cefd9 +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 9 10:49:52 2026 +0800 + + fetch(modify):账号列表合并 + +commit e8700c2585a81ea917aaba01600f2acd990ec5ac +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 9 10:22:23 2026 +0800 + + fetch(modify):修改账号列表中的分配角色弹窗 + +commit b94c043a56f6b8413329d13748b46a512a0cd0c4 +Author: sexygoat <1538832180@qq.com> +Date: Thu Feb 5 17:22:41 2026 +0800 + + fetch(modify):修改bug + +commit d97dc5f007b6ec336b675f4e5682d12d22fc9467 +Author: sexygoat <1538832180@qq.com> +Date: Wed Feb 4 18:14:52 2026 +0800 + + fetch(modify):修改套餐接口 + +commit 20e8c13e61c3e5d96cfc60568caa3054142255e2 +Author: sexygoat <1538832180@qq.com> +Date: Tue Feb 3 17:58:58 2026 +0800 + + fetch(modify):修改分为元 + +commit 192c6f1d2612997f18bfdd4e172bfe368cf02776 +Author: sexygoat <1538832180@qq.com> +Date: Tue Feb 3 17:20:50 2026 +0800 + + fetch(modify):完善按钮权限 + +commit de9753f42d111db3b59c176a3d79bc2b1fc7695b +Author: sexygoat <1538832180@qq.com> +Date: Tue Feb 3 14:39:45 2026 +0800 + + fetch(modify):修复BUG + +commit 2c6fe4375b072307282462db96a68ed31dd268e6 +Author: sexygoat <1538832180@qq.com> +Date: Tue Feb 3 10:04:59 2026 +0800 + + fetch(modify):修复API的URL + +commit 06cde977ad8382e44a2a884778af098e94fb4969 +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 17:08:49 2026 +0800 + + fetch(modify):修复角色分配权限 + +commit f62437379d2449d2c0bae361f6138ff8f7649f35 +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 14:35:33 2026 +0800 + + fetch(modify):隐藏换卡申请 + +commit c31fdaa74811f5d8e701aab4b2af9fe5f0ca225c +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 14:30:24 2026 +0800 + + fetch(modify):隐藏我的网卡 + +commit 4d2f38c75b58bed72c6f0cf8d7e11ac189430102 +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 11:51:59 2026 +0800 + + fetch(modify):账号管理 + +commit 78bd9fba8531309d3297471731ef4b1e1950732e +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 10:57:31 2026 +0800 + + fetch(modify):完善ioT卡管理 + +commit e08c962c40cd3dbb99b45d7e4b13e7006b89b14d +Author: sexygoat <1538832180@qq.com> +Date: Mon Feb 2 10:27:03 2026 +0800 + + fetch(modify):完善设备管理 + +commit 882feaf3ff8b0ff1967565dbec1d07d9fe8623b9 +Author: sexygoat <1538832180@qq.com> +Date: Sat Jan 31 18:12:58 2026 +0800 + + fetch(modify):修改bug + +commit ecb79dae432e08d658637ef93242d8308fc87dcb +Author: sexygoat <1538832180@qq.com> +Date: Sat Jan 31 16:33:21 2026 +0800 + + fetch(modify):修改bug + +commit 16d53709efc9b029360867dccae740c9ba0f3170 +Author: sexygoat <1538832180@qq.com> +Date: Sat Jan 31 11:37:11 2026 +0800 + + fetch(modify):修改设备和lot卡下载模板 + +commit 31440b29045412a9db357c09d38e7413f1fd9533 +Author: sexygoat <1538832180@qq.com> +Date: Sat Jan 31 11:18:37 2026 +0800 + + fetch(modify):修改原来的bug + +commit 8a1388608c49f2050be060eb8bfd5c2b175fb455 +Author: sexygoat <1538832180@qq.com> +Date: Fri Jan 30 15:39:19 2026 +0800 + + fetch(add): 新增企业设备授权 + +commit 841cf0442bb021ae58c1dd45c3f97bd025a8a83b +Author: sexygoat <1538832180@qq.com> +Date: Thu Jan 29 15:43:45 2026 +0800 + + fetch(add): 订单管理-企业设备 + +commit 1812b7a6c4aa58a009d3d99084e3e7ecd31641e0 +Author: sexygoat <1538832180@qq.com> +Date: Tue Jan 27 16:47:49 2026 +0800 + + fetch(modify): 中英文以及黑屏 + +commit 6127b21c2c0524d61272363257692e8e8dd6cff9 +Author: sexygoat <1538832180@qq.com> +Date: Tue Jan 27 16:06:48 2026 +0800 + + fetch(add): 运营商管理 + +commit c07e481b5babcdc36eb95070602a82373b835a22 +Author: sexygoat <1538832180@qq.com> +Date: Tue Jan 27 10:48:59 2026 +0800 + + chore: remove unused aws-sdk dependency + + Removed aws-sdk as it was not being used in the codebase. + This resolves the Docker build error related to lockfile mismatch. + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + Co-Authored-By: Claude + +commit 5c6312c4074ff47e62d7ca2847da2adc6d388947 +Author: sexygoat <1538832180@qq.com> +Date: Tue Jan 27 09:18:45 2026 +0800 + + fetch(add): 新增 + +commit 0eed8244e5c94700915baff6d1b97c333be2dfb2 +Author: sexygoat <1538832180@qq.com> +Date: Sat Jan 24 16:18:30 2026 +0800 + + fetch(add): 分配记录,批量分配/回收, 单卡列表, 任务列表, 导入ICCID + +commit c69124a819671c47896cd4152e1551a545e796b5 +Author: sexygoat <1538832180@qq.com> +Date: Fri Jan 23 17:24:07 2026 +0800 + + fetch(add): 账户管理 + +commit b53fea43c66ed5ad5a80b65db464d519d50ae6ef +Author: sexygoat <1538832180@qq.com> +Date: Fri Jan 23 17:18:24 2026 +0800 + + fetch(add): 账户管理 + +commit 339abca4c035511966e8e9f5ecae3b5fc60410f8 +Author: sexygoat <1538832180@qq.com> +Date: Thu Jan 22 18:04:32 2026 +0800 + + feat(login): 删除还没有账号? 注册 + +commit 13bdee2899cd282aa106c34392a193b4a6a51769 +Author: huang +Date: Thu Jan 22 17:06:34 2026 +0800 + + 忽略类型检测 + +commit 251a8f6a839128146913a2900b52bc2675b3adba +Author: huang +Date: Thu Jan 22 17:00:57 2026 +0800 + + 部署 + +commit 099e3ec78bbec91e56064e939e7e720b0e53b0a2 +Author: sexygoat <1538832180@qq.com> +Date: Thu Jan 22 16:46:50 2026 +0800 + + first-commmit + +commit 222e5bb11a358dc836d3b34ab9273a8f45d7a228 +Author: sexygoat <1538832180@qq.com> +Date: Thu Jan 22 16:35:33 2026 +0800 + + Initial commit: One Pipe System + + 完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + Co-Authored-By: Claude diff --git a/src/api/modules/shopPackageAllocation.ts b/src/api/modules/shopPackageAllocation.ts index 3fcbc6b..ebbaae1 100644 --- a/src/api/modules/shopPackageAllocation.ts +++ b/src/api/modules/shopPackageAllocation.ts @@ -5,8 +5,10 @@ import { BaseService } from '../BaseService' import type { BaseResponse, + CreateShopPackageBatchAllocationsRequest, CreateShopPackageAllocationRequest, ShopPackageAllocationResponse, + ShopPackageBatchAllocationsResponse, UpdateShopPackageAllocationExpiryBaseRequest } from '@/types/api' @@ -24,6 +26,19 @@ export class ShopPackageAllocationService extends BaseService { ) } + /** + * 批量创建店铺套餐分配 + * POST /api/admin/shop-package-batch-allocations + */ + static createShopPackageBatchAllocations( + data: CreateShopPackageBatchAllocationsRequest + ): Promise> { + return this.post>( + '/api/admin/shop-package-batch-allocations', + data + ) + } + /** * 更新店铺套餐分配生效条件 * PATCH /api/admin/shop-package-allocations/{id}/expiry-base diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts index 453f02c..43c6dc4 100644 --- a/src/types/api/packageManagement.ts +++ b/src/types/api/packageManagement.ts @@ -135,10 +135,23 @@ export interface ShopPackageAllocationResponse extends PackageAllocationExpiryBa cost_price: number } +// 批量创建店铺套餐分配请求 +export interface CreateShopPackageBatchAllocationsRequest { + shop_id: number + package_ids: number[] + cost_price?: number + expiry_base_override: PackageAllocationExpiryBaseOverride | null +} + +// 批量创建店铺套餐分配响应 +export interface ShopPackageBatchAllocationsResponse { + allocations: ShopPackageAllocationResponse[] +} + /** * 套餐响应 */ -export interface PackageResponse { +export interface PackageResponse extends PackageAllocationExpiryBaseFields { id: number package_code: string package_name: string @@ -323,7 +336,8 @@ export interface CreateShopSeriesGrantRequest { commission_tiers?: CommissionTier[] // 梯度配置列表,梯度模式时必填 enable_force_recharge?: boolean // 是否启用强充 force_recharge_amount?: number // 强充金额(分) - packages?: GrantPackageInfo[] // 套餐列表 + packages?: GrantPackageItem[] // 套餐列表 + expiry_base_override: PackageAllocationExpiryBaseOverride | null } /** @@ -342,6 +356,7 @@ export interface UpdateShopSeriesGrantRequest { export interface GrantPackageItem { package_id?: number // 套餐ID cost_price?: number // 成本价(分) + expiry_base_override?: PackageAllocationExpiryBaseOverride | null remove?: boolean | null // 是否删除该套餐授权(true=删除) } @@ -350,4 +365,5 @@ export interface GrantPackageItem { */ export interface ManageGrantPackagesRequest { packages?: GrantPackageItem[] | null // 套餐操作列表 + expiry_base_override: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件 } diff --git a/src/views/package-management/package-list/detail.vue b/src/views/package-management/package-list/detail.vue index 5510ce7..51eec36 100644 --- a/src/views/package-management/package-list/detail.vue +++ b/src/views/package-management/package-list/detail.vue @@ -168,6 +168,14 @@ return data.expiry_base === 'from_activation' ? '实名激活时起算' : '购买时起算' } }, + { label: '套餐默认生效条件', prop: 'default_expiry_base_name' }, + { + label: '分配覆盖生效条件', + prop: 'expiry_base_override_name', + formatter: (_: unknown, data: PackageResponse) => + data.expiry_base_override_name || '跟随套餐默认' + }, + { label: '最终生效条件', prop: 'effective_expiry_base_name' }, { label: '状态', formatter: (_: unknown, data: PackageResponse) => { diff --git a/src/views/package-management/package-list/index.vue b/src/views/package-management/package-list/index.vue index 10685c2..dc2af0b 100644 --- a/src/views/package-management/package-list/index.vue +++ b/src/views/package-management/package-list/index.vue @@ -625,6 +625,13 @@ return map[row.expiry_base] || row.expiry_base || '-' } }, + { label: '套餐默认生效条件', prop: 'default_expiry_base_name' }, + { + label: '分配覆盖生效条件', + prop: 'expiry_base_override_name', + formatter: (row: PackageResponse) => row.expiry_base_override_name || '跟随套餐默认' + }, + { label: '最终生效条件', prop: 'effective_expiry_base_name' }, ...(canUpdatePackageShelfStatus ? [{ label: '上架状态', prop: 'shelf_status' }] : []), ...(canUpdatePackageStatus ? [{ label: '状态', prop: 'status' }] : []), { label: '创建时间', prop: 'created_at' }, diff --git a/src/views/package-management/series-grants/detail.vue b/src/views/package-management/series-grants/detail.vue index e7ab017..90128c9 100644 --- a/src/views/package-management/series-grants/detail.vue +++ b/src/views/package-management/series-grants/detail.vue @@ -158,7 +158,7 @@ @@ -291,7 +291,7 @@ {{ packageForm.default_expiry_base_name || '-' }} - {{ packageForm.expiry_base_override_name || '-' }} + {{ packageForm.expiry_base_override_name || '跟随套餐默认' }} {{ packageForm.effective_expiry_base_name || '-' }} @@ -593,14 +593,17 @@ submitLoading.value = true try { if (packageDialogType.value === 'add') { - await ShopPackageAllocationService.createShopPackageAllocation({ - shop_id: detailData.value!.shop_id, - package_id: packageForm.value.package_id!, - cost_price: Math.round(packageForm.value.cost_price_yuan * 100), + await ShopSeriesGrantService.manageGrantPackages(grantId, { expiry_base_override: packageForm.value.expiry_base_override === 'default' ? null - : packageForm.value.expiry_base_override + : packageForm.value.expiry_base_override, + packages: [ + { + package_id: packageForm.value.package_id, + cost_price: Math.round(packageForm.value.cost_price_yuan * 100) + } + ] }) } else { const expiryBaseChanged = @@ -617,22 +620,24 @@ cost_price: Math.round(packageForm.value.cost_price_yuan * 100) } ] - await Promise.all([ - ShopSeriesGrantService.manageGrantPackages(grantId, { packages }), - ...(expiryBaseChanged - ? [ - ShopPackageAllocationService.updateShopPackageAllocationExpiryBase( - packageForm.value.allocation_id!, - { - expiry_base_override: - packageForm.value.expiry_base_override === 'default' - ? null - : packageForm.value.expiry_base_override - } - ) - ] - : []) - ]) + if (expiryBaseChanged) { + await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase( + packageForm.value.allocation_id!, + { + expiry_base_override: + packageForm.value.expiry_base_override === 'default' + ? null + : packageForm.value.expiry_base_override + } + ) + } + await ShopSeriesGrantService.manageGrantPackages(grantId, { + expiry_base_override: + packageForm.value.expiry_base_override === 'default' + ? null + : packageForm.value.expiry_base_override, + packages + }) } ElMessage.success(packageDialogType.value === 'add' ? '添加成功' : '更新成功') packageDialogVisible.value = false @@ -667,7 +672,10 @@ } ] - await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages }) + await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { + expiry_base_override: row.expiry_base_override ?? null, + packages + }) ElMessage.success('删除成功') // 刷新详情数据 diff --git a/src/views/package-management/series-grants/index.vue b/src/views/package-management/series-grants/index.vue index f406955..e033e51 100644 --- a/src/views/package-management/series-grants/index.vue +++ b/src/views/package-management/series-grants/index.vue @@ -171,18 +171,6 @@ {{ formatSuggestedRetailPrice(pkg.suggested_retail_price) }})
-
-
生效条件
- - - - - -
删除 @@ -191,7 +179,16 @@
设置每个套餐的成本价(单位:元),不能低于成本价,不能高于建议售价的50%溢价
-
生效条件仅影响后续新订单,不影响已购买套餐
+
+ + + + + + +
本次所有授权套餐共用此选择,仅影响后续新订单,不影响已购买套餐
@@ -560,7 +557,6 @@ import { useRouter } from 'vue-router' import { ShopSeriesGrantService, - ShopPackageAllocationService, PackageSeriesService, ShopService, PackageManageService @@ -787,12 +783,12 @@ force_recharge_amount: undefined, force_recharge_locked: false, // 强充是否被套餐系列锁定 series_force_recharge_amount: 0, // 系列强充金额(元) + expiry_base_override: 'default' as PackageAllocationExpiryBaseOverride | 'default', packages: [] as Array<{ package_id: number cost_price: number original_cost_price?: number suggested_retail_price?: number - expiry_base_override: PackageAllocationExpiryBaseOverride | 'default' }> // 套餐配置 }) @@ -1153,8 +1149,7 @@ suggested_retail_price: pkg?.suggested_retail_price !== undefined && pkg?.suggested_retail_price !== null ? pkg.suggested_retail_price / 100 - : undefined, - expiry_base_override: 'default' + : undefined }) } }) @@ -1581,8 +1576,7 @@ pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== null ? pkgOption.suggested_retail_price / 100 - : undefined, - expiry_base_override: pkg.expiry_base_override ?? 'default' + : undefined } }) selectedPackageIds.value = detail.packages.map((pkg) => pkg.package_id) @@ -1616,6 +1610,7 @@ form.force_recharge_amount = undefined form.force_recharge_locked = false form.packages = [] + form.expiry_base_override = 'default' selectedPackageIds.value = [] packageOptions.value = [] // 清空套餐选项 } @@ -1639,6 +1634,7 @@ form.series_name = '' form.shop_name = '' form.allocator_shop_name = '' + form.expiry_base_override = 'default' form.enable_one_time_commission = false form.commission_type = 'fixed' form.one_time_commission_amount = undefined @@ -1649,6 +1645,7 @@ form.force_recharge_amount = undefined form.force_recharge_locked = false form.packages = [] + form.expiry_base_override = 'default' selectedPackageIds.value = [] } @@ -1730,15 +1727,12 @@ data.force_recharge_amount = 0 } - // 新增系列授权后,套餐通过独立分配接口创建,确保显式传递生效条件覆盖值。 - const packageAllocations = form.packages.map((pkg: any) => ({ + const packageItems = form.packages.map((pkg: any) => ({ package_id: pkg.package_id, - cost_price: Math.round((pkg.cost_price ?? 0) * 100), - expiry_base_override: - pkg.expiry_base_override === 'default' ? null : pkg.expiry_base_override + cost_price: Math.round((pkg.cost_price ?? 0) * 100) })) - if (packageAllocations.length > 0) { + if (packageItems.length > 0) { // 验证是否包含赠送套餐 for (const pkg of form.packages) { const pkgInfo = packageOptions.value.find((p) => p.id === pkg.package_id) @@ -1776,20 +1770,10 @@ // 新增时需要必填字段 data.series_id = form.series_id data.shop_id = form.shop_id + data.expiry_base_override = + form.expiry_base_override === 'default' ? null : form.expiry_base_override + data.packages = packageItems await ShopSeriesGrantService.createShopSeriesGrant(data) - await Promise.all( - packageAllocations.map( - (pkg: { - package_id: number - cost_price: number - expiry_base_override: PackageAllocationExpiryBaseOverride | null - }) => - ShopPackageAllocationService.createShopPackageAllocation({ - shop_id: data.shop_id, - ...pkg - }) - ) - ) ElMessage.success('新增成功') } else { // 编辑时调用更新接口 diff --git a/src/views/package-management/series-grants/packages.vue b/src/views/package-management/series-grants/packages.vue index e09ad19..7c8afae 100644 --- a/src/views/package-management/series-grants/packages.vue +++ b/src/views/package-management/series-grants/packages.vue @@ -47,7 +47,7 @@ @@ -144,7 +144,7 @@ {{ packageForm.default_expiry_base_name || '-' }} - {{ packageForm.expiry_base_override_name || '-' }} + {{ packageForm.expiry_base_override_name || '跟随套餐默认' }} {{ packageForm.effective_expiry_base_name || '-' }} @@ -211,7 +211,6 @@ const grantId = computed(() => Number(route.params.id)) const seriesId = ref(0) - const shopId = ref(0) const grantName = ref('') const packageList = ref([]) const availablePackages = ref([]) @@ -325,7 +324,6 @@ if (res.code === 0) { packageList.value = res.data.packages || [] seriesId.value = res.data.series_id - shopId.value = res.data.shop_id grantName.value = `${res.data.series_name} - ${res.data.shop_name}` } } catch (error) { @@ -437,14 +435,12 @@ } const costPrice = Math.round(currentCostPriceYuan * 100) if (packageDialogType.value === 'add') { - await ShopPackageAllocationService.createShopPackageAllocation({ - shop_id: shopId.value, - package_id: packageForm.value.package_id!, - cost_price: costPrice, + await ShopSeriesGrantService.manageGrantPackages(grantId.value, { expiry_base_override: packageForm.value.expiry_base_override === 'default' ? null - : packageForm.value.expiry_base_override + : packageForm.value.expiry_base_override, + packages: [{ package_id: packageForm.value.package_id, cost_price: costPrice }] }) } else { const expiryBaseChanged = @@ -458,22 +454,24 @@ const packages: GrantPackageItem[] = [ { package_id: packageForm.value.package_id, cost_price: costPrice } ] - await Promise.all([ - ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages }), - ...(expiryBaseChanged - ? [ - ShopPackageAllocationService.updateShopPackageAllocationExpiryBase( - packageForm.value.allocation_id!, - { - expiry_base_override: - packageForm.value.expiry_base_override === 'default' - ? null - : packageForm.value.expiry_base_override - } - ) - ] - : []) - ]) + if (expiryBaseChanged) { + await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase( + packageForm.value.allocation_id!, + { + expiry_base_override: + packageForm.value.expiry_base_override === 'default' + ? null + : packageForm.value.expiry_base_override + } + ) + } + await ShopSeriesGrantService.manageGrantPackages(grantId.value, { + expiry_base_override: + packageForm.value.expiry_base_override === 'default' + ? null + : packageForm.value.expiry_base_override, + packages + }) } packageDialogVisible.value = false await fetchPackageList() @@ -501,7 +499,10 @@ remove: true } ] - await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages }) + await ShopSeriesGrantService.manageGrantPackages(grantId.value, { + expiry_base_override: row.expiry_base_override ?? null, + packages + }) await fetchPackageList() } catch (error) { console.error(error) From 9289a6e940ab9ef5f3f088772006f9ebb3f73232 Mon Sep 17 00:00:00 2001 From: luo Date: Sat, 25 Jul 2026 10:20:53 +0800 Subject: [PATCH 28/76] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=8E=A5?= =?UTF-8?q?=E5=8F=A319-=E9=A1=B6=E9=83=A8=E9=80=9A=E7=9F=A5=E9=93=83?= =?UTF-8?q?=E9=93=9B=E4=B8=8E=E7=AB=99=E5=86=85=E9=80=9A=E7=9F=A5=E4=B8=AD?= =?UTF-8?q?=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 35 ++ .../proposal.md | 35 ++ .../specs/admin-expiring-assets/spec.md | 111 ++++++ .../tasks.md | 35 ++ .../design.md | 36 ++ .../proposal.md | 40 ++ .../specs/notification-center/spec.md | 84 ++++ .../tasks.md | 34 ++ .../proposal.md | 5 +- .../specs/shop-management/spec.md | 11 +- .../tasks.md | 2 +- src/api/modules/account.ts | 12 - src/api/modules/asset.ts | 17 +- src/api/modules/notification.ts | 39 +- src/api/modules/shop.ts | 25 +- .../core/layouts/art-header-bar/index.vue | 4 +- .../core/layouts/art-notification/index.vue | 22 +- .../core/layouts/art-notification/style.scss | 6 +- src/router/routes/asyncRoutes.ts | 26 +- src/router/routesAlias.ts | 1 + src/store/modules/notification.ts | 73 ++-- src/types/api/account.ts | 14 - src/types/api/asset.ts | 29 ++ src/types/api/card.ts | 3 + src/types/api/device.ts | 3 + src/types/api/notification.ts | 76 ++-- src/types/api/shop.ts | 19 +- src/utils/business/expiryEstimate.ts | 8 + src/utils/business/notificationNavigation.ts | 40 +- .../components/BasicInfoCard.vue | 25 +- .../asset-management/device-list/index.vue | 23 +- .../iot-card-management/index.vue | 23 +- src/views/dashboard/console/index.vue | 10 +- .../console/widget/ExpiringAssetStats.vue | 102 +++++ src/views/notifications/index.vue | 28 +- .../operations/expiring-assets/index.vue | 364 ++++++++++++++++++ src/views/shop-management/list/index.vue | 70 ++-- 37 files changed, 1264 insertions(+), 226 deletions(-) create mode 100644 openspec/changes/add-admin-expiring-assets-notifications/design.md create mode 100644 openspec/changes/add-admin-expiring-assets-notifications/proposal.md create mode 100644 openspec/changes/add-admin-expiring-assets-notifications/specs/admin-expiring-assets/spec.md create mode 100644 openspec/changes/add-admin-expiring-assets-notifications/tasks.md create mode 100644 openspec/changes/update-admin-notification-center-api/design.md create mode 100644 openspec/changes/update-admin-notification-center-api/proposal.md create mode 100644 openspec/changes/update-admin-notification-center-api/specs/notification-center/spec.md create mode 100644 openspec/changes/update-admin-notification-center-api/tasks.md create mode 100644 src/views/dashboard/console/widget/ExpiringAssetStats.vue create mode 100644 src/views/operations/expiring-assets/index.vue diff --git a/openspec/changes/add-admin-expiring-assets-notifications/design.md b/openspec/changes/add-admin-expiring-assets-notifications/design.md new file mode 100644 index 0000000..0b9e3e9 --- /dev/null +++ b/openspec/changes/add-admin-expiring-assets-notifications/design.md @@ -0,0 +1,35 @@ +## Context + +临期结果由后端根据资产当前套餐及排队套餐计算。管理端需要在独立列表、普通资产列表、代理首页和通知中心保持相同的临期语义。本变更只覆盖管理端,C 端资产和续费能力不纳入实现范围。 + +## Goals / Non-Goals + +- Goals: 统一消费 `estimated_final_expires_at`、`days_until_final_expiry`、`expiry_level`、`expiry_level_name` 和 `can_renew`;提供临期查询、排序、高亮、统计、通知跳转和管理端续费入口。 +- Goals: 由后端负责临期边界、预计到期结果和通知触发,前端只负责展示和路由。 +- Non-Goals: 不计算套餐接续到期时间,不修改历史订单或已购套餐,不实现 C 端入口,不展示企微临期消息。 + +## Decisions + +- Decision: 临期独立列表使用 `GET /api/admin/expiring-assets`,查询参数保持文档约定的 `asset_type`、`keyword`、`shop_id`、`package_id`、`days_min`、`days_max`、`expires_from`、`expires_to`、`page`、`size`。 +- Decision: 临期接口负责跨分页排序,返回结果按 0-3 天优先、其余按 `estimated_final_expires_at` 升序排列;前端只保留接口顺序,不改变普通资产列表原始接口排序。 +- Decision: 颜色只按剩余天数映射:8-15 天粉红、4-7 天紫色、0-3 天红色。已过期和不可预计记录由接口排除,前端不把它们补入临期列表。 +- Decision: 续费入口由 `can_renew` 控制,具体续费动作复用现有管理端续费/充值路由和接口,不新增 C 端逻辑。 +- Decision: 通知中心继续复用现有通知接口和目标跳转协议;临期通知由后端按 15 天、7 天、3 天生成,前端按通知目标跳转到临期列表或对应资产详情。 +- Alternatives considered: 前端从普通资产列表筛选临期记录。Rejected because it无法保证全量、统一排序和后端预计最终到期结果的一致性。 + +## Risks / Trade-offs + +- 风险:代理首页当前概览接口未必已有临期计数字段。Mitigation:在现有首页/概览响应中增加 `expiring_card_count` 和 `expiring_device_count`,避免前端分页统计。 +- 风险:不同资产列表的字段结构不完全一致。Mitigation:在各自 API 类型中复用同一组临期字段语义,并通过统一展示格式处理空值和等级。 +- 风险:续费入口的现有路由可能依赖资产类型。Mitigation:由 `asset_type` 和 `asset_id` 选择对应管理端续费入口,按钮仅在 `can_renew=true` 时展示。 + +## Migration Plan + +1. 先扩展管理端 API 类型和服务,确认列表、首页统计和通知目标字段。 +2. 上线临期列表及普通列表高亮,再接入首页统计和续费入口。 +3. 最后验证通知中心过滤、已读和跳转,不修改 C 端功能。 + +## Open Questions + +- 代理首页现有概览接口的具体路径和响应字段名称,需要以后端接口实现为准确认。 +- 管理端卡/设备续费入口的现有路由是否统一,需实现时复用当前权限和路由定义。 diff --git a/openspec/changes/add-admin-expiring-assets-notifications/proposal.md b/openspec/changes/add-admin-expiring-assets-notifications/proposal.md new file mode 100644 index 0000000..ac19a2e --- /dev/null +++ b/openspec/changes/add-admin-expiring-assets-notifications/proposal.md @@ -0,0 +1,35 @@ +# Change: 增加管理端临期资产高亮、通知与续费入口 + +## Why + +管理端目前缺少统一的临期资产查询入口,运营人员需要在普通资产列表中自行识别临近到期资产,且无法从临期记录快速进入续费操作。需要基于后端统一计算的预计最终到期结果,在管理端集中展示临期资产、标记普通列表并接入站内通知。 + +## What Changes + +- 新增管理端临期资产列表,支持资产类型、关键字、店铺、套餐、剩余天数和预计到期时间筛选。 +- 临期列表按 0-3 天固定置顶,其余按预计最终到期时间升序;已过期和不可预计资产不进入列表。 +- 在卡列表、设备列表和资产相关列表中增加临期颜色高亮,但不改变普通列表原有排序。 +- 在代理首页增加临期卡数量和临期设备数量展示,并可跳转临期列表。 +- 在临期列表提供后端返回 `can_renew` 控制的续费入口,复用现有管理端续费/充值流程。 +- 接入通知中心中的 15 天、7 天、3 天临期提醒及通知跳转;管理端不展示企微临期消息。 +- 所有页面直接使用后端返回的预计最终到期字段和临期等级,不在前端重新计算最终到期时间。 + +## Impact + +- Affected specs: + - `admin-expiring-assets` +- Affected code: + - `src/api/modules` 和 `src/types/api` 中的临期资产接口及类型 + - 管理端临期资产列表页面和路由 + - 网卡列表、设备列表及资产信息展示组件 + - 代理首页统计组件 + - 通知中心跳转处理 +- API contracts: + - `GET /api/admin/expiring-assets` + - 现有代理首页/概览接口增加临期卡、设备数量字段,或提供等价的管理端统计响应 + - 复用现有通知查询、已读和目标跳转接口 +- Out of scope: + - C 端资产页临期展示 + - C 端续费按钮和 C 端续费流程 + - 企微临期消息展示或企微通知改造 + - 前端根据套餐明细自行推导预计最终到期时间 diff --git a/openspec/changes/add-admin-expiring-assets-notifications/specs/admin-expiring-assets/spec.md b/openspec/changes/add-admin-expiring-assets-notifications/specs/admin-expiring-assets/spec.md new file mode 100644 index 0000000..14bc997 --- /dev/null +++ b/openspec/changes/add-admin-expiring-assets-notifications/specs/admin-expiring-assets/spec.md @@ -0,0 +1,111 @@ +## ADDED Requirements + +### Requirement: Admin Expiring Asset Query + +The admin frontend SHALL provide an expiring asset list backed by `GET /api/admin/expiring-assets`. The query SHALL support `asset_type`, `keyword`, `shop_id`, `package_id`, `days_min`, `days_max`, `expires_from`, `expires_to`, `page`, and `size`. + +#### Scenario: Filter expiring assets + +- **WHEN** 管理端用户打开临期资产列表并提交筛选条件 +- **THEN** 前端 MUST send the documented query parameters to `GET /api/admin/expiring-assets` +- **AND** 页面 MUST display the paginated response items + +#### Scenario: Exclude unavailable assets + +- **WHEN** 接口返回临期列表 +- **THEN** 页面 MUST not add expired assets or assets without an estimable final expiry to the list on the client side + +### Requirement: Expiring Asset Fields + +Each expiring asset item SHALL preserve and display the backend fields `asset_type`, `asset_id`, `identifier`, `shop_name`, `package_name`, `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_level`, `expiry_level_name`, and `can_renew`. The frontend MUST use backend-provided expiry results and MUST NOT calculate the estimated final expiry from package details. + +#### Scenario: Display an estimable asset + +- **WHEN** 临期接口返回资产及 `estimated_final_expires_at` +- **THEN** 页面 MUST display the asset identifier, shop, package, estimated final expiry, remaining days, and backend expiry level name + +#### Scenario: Preserve backend result + +- **WHEN** 临期接口返回 `estimated_final_expires_at` 或 `days_until_final_expiry` +- **THEN** 前端 MUST display the returned values +- **AND** 前端 MUST NOT derive or replace them from current or queued package data + +### Requirement: Expiring List Ordering And Highlighting + +The expiring asset API and standalone admin list SHALL return and preserve records with 0-3 remaining days first, then order the remaining records by `estimated_final_expires_at` ascending. The list SHALL apply pink highlighting to 8-15 days, purple highlighting to 4-7 days, and red highlighting to 0-3 days. + +#### Scenario: Order critical assets first + +- **GIVEN** the response contains assets in multiple expiry ranges +- **WHEN** 页面渲染临期列表 +- **THEN** the API response MUST place assets with 0-3 remaining days before assets with more remaining days +- **AND** the API response MUST order records in the remaining ranges by estimated final expiry ascending +- **AND** the frontend MUST preserve that cross-page ordering + +#### Scenario: Highlight by remaining days + +- **WHEN** 页面渲染临期资产 +- **THEN** 8-15 days MUST use the pink visual treatment +- **AND** 4-7 days MUST use the purple visual treatment +- **AND** 0-3 days MUST use the red visual treatment + +### Requirement: Ordinary Admin Asset List Highlighting + +The admin card and device lists SHALL apply the same expiry color treatment to returned expiring asset fields without changing their existing server or client sort order. + +#### Scenario: Highlight ordinary asset rows + +- **WHEN** 卡列表或设备列表返回临期字段 +- **THEN** 页面 MUST apply the matching expiry color treatment +- **AND** 页面 MUST preserve the list's existing ordering + +### Requirement: Admin Expiring Asset Renewal Entry + +The admin expiring asset list SHALL display a renewal action only when the backend item has `can_renew=true`. The action SHALL navigate to the existing management-side renewal or recharge flow for the asset type. + +#### Scenario: Renewable asset + +- **GIVEN** 临期资产项返回 `can_renew=true` +- **WHEN** 用户查看临期列表 +- **THEN** 页面 MUST provide the management-side renewal entry +- **AND** the entry MUST target the corresponding asset and preserve asset type context + +#### Scenario: Non-renewable asset + +- **GIVEN** 临期资产项返回 `can_renew=false` +- **WHEN** 用户查看临期列表 +- **THEN** 页面 MUST NOT display an enabled renewal action + +### Requirement: Agent Dashboard Expiry Counts + +The agent-facing admin home SHALL display the number of expiring cards and expiring devices using backend-provided summary fields and SHALL provide navigation to the standalone expiring asset list. + +#### Scenario: Display expiry counts + +- **WHEN** 代理首页概览数据返回临期卡和设备数量 +- **THEN** 页面 MUST display the expiring card count and expiring device count +- **AND** 页面 MUST not calculate the counts from a paginated asset list + +### Requirement: Admin Expiry Notifications + +The notification center SHALL display backend-generated admin expiry reminders for 15 days, 7 days, and 3 days through the existing notification API and target navigation. The admin frontend SHALL not display WeCom expiry messages. + +#### Scenario: Display expiry reminder + +- **WHEN** 通知接口返回 15 天、7 天或 3 天的临期提醒 +- **THEN** 通知中心 MUST classify it as a 临期提醒 +- **AND** 用户点击通知后 MUST navigate to the returned management-side target + +#### Scenario: Exclude WeCom expiry message + +- **WHEN** 管理端加载通知中心 +- **THEN** 页面 MUST not add or render a separate WeCom expiry message channel + +### Requirement: Admin-Only Scope + +This capability SHALL be limited to the management frontend. It SHALL NOT add or modify C-end asset expiry display, C-end renewal buttons, C-end renewal APIs, or C-end notification behavior. + +#### Scenario: Keep C-end unchanged + +- **WHEN** this change is implemented +- **THEN** C-end asset pages and renewal flows MUST remain outside the change scope diff --git a/openspec/changes/add-admin-expiring-assets-notifications/tasks.md b/openspec/changes/add-admin-expiring-assets-notifications/tasks.md new file mode 100644 index 0000000..17ef23a --- /dev/null +++ b/openspec/changes/add-admin-expiring-assets-notifications/tasks.md @@ -0,0 +1,35 @@ +## 1. API Contract + +- [x] 1.1 增加 `GET /api/admin/expiring-assets` 的查询参数、分页响应和临期资产项类型。 +- [x] 1.2 为卡列表、设备列表、资产信息和首页概览类型补充统一临期字段及临期计数字段。 +- [x] 1.3 增加临期资产 API 服务方法,并确认续费入口复用现有管理端服务和权限。 + +## 2. Expiring Asset List + +- [x] 2.1 增加或接入 `/operations/expiring-assets` 管理端路由和页面。 +- [x] 2.2 实现资产类型、关键字、店铺、套餐、剩余天数和预计到期时间筛选。 +- [x] 2.3 展示资产、店铺、当前套餐、预计最终到期、剩余天数和后端临期等级名称。 +- [x] 2.4 实现 0-3 天置顶、其余按预计到期时间升序,并排除已过期和不可预计记录。 +- [x] 2.5 按 8-15 天粉红、4-7 天紫色、0-3 天红色实现行或到期字段高亮。 +- [x] 2.6 根据 `can_renew` 展示管理端续费入口,并跳转现有续费/充值流程。 + +## 3. Existing Admin Pages + +- [x] 3.1 在网卡普通列表增加临期字段展示和颜色高亮,不改变原排序。 +- [x] 3.2 在设备普通列表增加临期字段展示和颜色高亮,不改变原排序。 +- [x] 3.3 在资产信息详情复用同一临期展示规则,禁止前端重新计算预计最终到期时间。 +- [x] 3.4 在代理首页展示临期卡数量和临期设备数量,并支持跳转临期列表。 + +## 4. Notifications + +- [x] 4.1 在通知中心复用临期分类,展示后端生成的 15 天、7 天、3 天临期提醒。 +- [x] 4.2 使用现有通知目标跳转到临期列表或对应管理端资产详情。 +- [x] 4.3 确认管理端不展示企微临期消息,不新增 C 端通知入口。 + +## 5. Verification + +- [x] 5.1 验证临期字段直接使用后端值,前端不根据套餐明细推导最终到期时间。 +- [x] 5.2 验证 0-3 天置顶、日期升序、颜色映射和普通列表原排序保持不变。 +- [x] 5.3 验证 `can_renew=false` 不展示续费入口,且各资产类型使用正确管理端入口。 +- [x] 5.4 验证通知中心临期分类、已读状态和跳转行为。 +- [x] 5.5 运行类型检查、相关 ESLint/Stylelint、构建和 `openspec validate add-admin-expiring-assets-notifications --strict`。 diff --git a/openspec/changes/update-admin-notification-center-api/design.md b/openspec/changes/update-admin-notification-center-api/design.md new file mode 100644 index 0000000..3de5826 --- /dev/null +++ b/openspec/changes/update-admin-notification-center-api/design.md @@ -0,0 +1,36 @@ +## Context + +后端通知接口已经提供稳定的管理端 REST 合约。前端需要移除对旧字段和旧响应形态的依赖,统一使用真实接口返回的数据,并把通知目标解析限制在前端已注册的内部目标映射中。 + +## Goals / Non-Goals + +- Goals: 顶部铃铛、通知抽屉、通知中心和共享 Pinia 状态使用同一套真实 API 类型。 +- Goals: 保证未读数、分类汇总、列表已读状态在单条已读和全部已读后与后端同步。 +- Goals: 受控目标只能通过 `target_type`、`target_id`、`target_key` 白名单映射到内部页面。 +- Non-Goals: 不实现 C 端通知,不支持任意 URL 跳转,不改变通知产生规则。 + +## Decisions + +- Decision: 通知列表请求使用 `is_read?: boolean`,不再以 `read_status` 字符串代替后端参数。 +- Decision: 通知正文统一使用 `body`;必要时仅在迁移读取阶段兼容旧数据,不再把旧 `content` 作为 API 契约类型。 +- Decision: 顶部徽标优先使用 `unread-count.data.display_count`,数值统计使用 `data.count`;不在前端重新格式化徽标文本。 +- Decision: 未读汇总只保存四个固定分类计数和总数;抽屉最近10条通过 `GET /api/admin/notifications?page=1&page_size=10` 获取,避免把分类汇总误当通知列表。 +- Decision: 全部已读服务方法接收可选 `category`,请求体始终为对象;通知中心的“全部已读”传空对象,分类操作可传具体类别。 +- Decision: `target_type`、`target_key` 由前端白名单映射到已注册路由,`target_id` 仅作为参数;未知、无权限或已失效目标只展示正文,不跳转。 +- Alternatives considered: 继续保留旧字段的宽松兼容类型。Rejected because it会掩盖真实 API 字段错误,并导致筛选和已读状态请求不符合后端契约。 + +## Risks / Trade-offs + +- 风险:后端新增通知类型。Mitigation:类型字段保持 string,页面对未知类型采用原值展示。 +- 风险:受控目标类型新增。Mitigation:未知目标默认不可跳转,不执行后端返回的任意路径。 +- 风险:列表与汇总请求并行时数据短暂不一致。Mitigation:单条已读/全部已读成功后刷新未读数、汇总和当前列表。 + +## Migration Plan + +1. 先更新类型、API service 和通知 store 的真实响应结构。 +2. 再更新顶部抽屉和通知中心字段、筛选及已读交互。 +3. 最后替换目标跳转映射并验证未知目标安全降级。 + +## Open Questions + +- `target_type` 与 `target_key` 的完整白名单值需要以后端实际返回样例补充;未确认值统一按不可跳转处理。 diff --git a/openspec/changes/update-admin-notification-center-api/proposal.md b/openspec/changes/update-admin-notification-center-api/proposal.md new file mode 100644 index 0000000..e6e4fac --- /dev/null +++ b/openspec/changes/update-admin-notification-center-api/proposal.md @@ -0,0 +1,40 @@ +# Change: 按实际接口更新管理端通知中心 + +## Why + +通知中心已有前端链路,但当前类型和页面仍兼容旧字段,例如 `content`、`read_status`、旧的未读摘要结构和可路由目标结构。后端接口文档已更新为实际可用的管理端 API,需要按真实响应字段和受控目标协议修正通知铃铛、抽屉、通知中心及状态同步逻辑。 + +## What Changes + +- 按实际接口更新通知项字段:`body`、`is_read`、`read_at`、`ref_type`、`ref_id`、`ref_key`。 +- 按实际接口更新通知列表筛选参数:使用 `is_read` boolean,支持 `category`、`type`、`severity`、`page`、`page_size`。 +- 接入真实未读数响应 `{ count, display_count }`,顶部徽标直接展示后端 `display_count`。 +- 接入真实未读分类汇总 `{ approval, expiry, sync, system, total }`,抽屉最近通知通过通知列表接口获取,最多展示10条。 +- 全部已读调用 `PUT /api/admin/notifications/read-all`,支持可选 `category` 请求体。 +- 受控目标按 `target_type`、`target_id`、`target_key` 建立前端白名单映射,不接受或执行任意 URL/路由字段。 +- 通知中心展示正文 `body`、读取状态 `is_read`、首次已读时间 `read_at`,并支持余额、审批、临期、同步/系统分类。 +- 本变更只覆盖管理端,明确不接入 C 端 `/api/c/v1/notifications`。 + +## Impact + +- Affected specs: + - `notification-center` +- Affected code: + - `src/types/api/notification.ts` + - `src/api/modules/notification.ts` + - `src/store/modules/notification.ts` + - `src/components/core/layouts/art-header-bar/index.vue` + - `src/components/core/layouts/art-notification/index.vue` + - `src/views/notifications/index.vue` + - `src/utils/business/notificationNavigation.ts` +- API contracts: + - `GET /api/admin/notifications` + - `PUT /api/admin/notifications/{id}/read` + - `PUT /api/admin/notifications/read-all` + - `GET /api/admin/notifications/unread-count` + - `GET /api/admin/notifications/unread-summary` + - `GET /api/admin/notifications/{id}/target` +- Out of scope: + - C 端 `/api/c/v1/notifications` 接口和页面 + - 创建、编辑或删除通知 + - WebSocket、浏览器推送和新的实时推送机制 diff --git a/openspec/changes/update-admin-notification-center-api/specs/notification-center/spec.md b/openspec/changes/update-admin-notification-center-api/specs/notification-center/spec.md new file mode 100644 index 0000000..4c4a506 --- /dev/null +++ b/openspec/changes/update-admin-notification-center-api/specs/notification-center/spec.md @@ -0,0 +1,84 @@ +## ADDED Requirements + +### Requirement: Admin Notification Contract + +The admin notification frontend SHALL use the actual notification contract returned by `GET /api/admin/notifications`. Each item SHALL preserve `id`, `title`, `body`, `category`, `type`, `severity`, `is_read`, `created_at`, `read_at`, `ref_type`, `ref_id`, and `ref_key`. + +#### Scenario: Display actual notification fields + +- **WHEN** 管理端通知列表返回通知项 +- **THEN** 页面 MUST display `title` and `body` +- **AND** 页面 MUST use `is_read` for read state +- **AND** 页面 MUST preserve `read_at` and `ref_key` + +### Requirement: Admin Notification List Filters + +The notification center SHALL query `GET /api/admin/notifications` with `category`, `type`, `severity`, `is_read`, `page`, and `page_size`. The frontend SHALL not send `read_status` as a replacement for `is_read`. + +#### Scenario: Filter unread notifications + +- **WHEN** 用户选择未读筛选 +- **THEN** 前端 MUST send `is_read=false` +- **AND** 页面 MUST render the paginated `data.items` response + +#### Scenario: Filter read notifications + +- **WHEN** 用户选择已读筛选 +- **THEN** 前端 MUST send `is_read=true` + +### Requirement: Unread Count And Summary + +The admin header and notification drawer SHALL use `GET /api/admin/notifications/unread-count` and `GET /api/admin/notifications/unread-summary`. The unread count response SHALL preserve `count` and `display_count`; the summary response SHALL preserve `approval`, `expiry`, `sync`, `system`, and `total`. + +#### Scenario: Display backend unread badge + +- **WHEN** 未读数接口返回 `count` and `display_count` +- **THEN** 顶部铃铛 MUST display the backend `display_count` +- **AND** shared notification state MUST preserve the numeric `count` + +#### Scenario: Load drawer notifications + +- **WHEN** 用户打开顶部通知抽屉 +- **THEN** 前端 MUST use the summary endpoint for category counts +- **AND** 前端 MUST query the notification list endpoint with `page=1` and `page_size=10` for recent notifications + +### Requirement: Read State Operations + +The admin frontend SHALL mark notifications read through `PUT /api/admin/notifications/{id}/read` and mark notifications read in bulk through `PUT /api/admin/notifications/read-all`. The bulk request SHALL support an optional JSON `category` field. + +#### Scenario: Mark one notification read + +- **WHEN** 用户点击通知 +- **THEN** 前端 MUST call the single-read PUT endpoint before target resolution +- **AND** after success MUST refresh the shared unread count and relevant notification state + +#### Scenario: Mark all notifications read + +- **WHEN** 用户点击全部已读 +- **THEN** 前端 MUST call `PUT /api/admin/notifications/read-all` with an object body +- **AND** after success MUST refresh the list, summary and unread count + +### Requirement: Controlled Notification Target + +The admin frontend SHALL resolve notification navigation through `GET /api/admin/notifications/{id}/target`. It SHALL map only recognized `target_type` or `target_key` values to registered internal routes and SHALL use `target_id` as a route parameter when required. + +#### Scenario: Navigate to recognized target + +- **WHEN** 目标接口返回可用的受支持 `target_type`、`target_key` 和 `target_id` +- **THEN** 前端 MUST navigate to the corresponding registered internal route +- **AND** 前端 MUST not execute an arbitrary URL from the response + +#### Scenario: Handle unknown target safely + +- **WHEN** 目标类型未知、目标不可用或当前路由未注册 +- **THEN** 前端 MUST not navigate +- **AND** 页面 MUST display the notification body or an unavailable-target message + +### Requirement: Admin-Only Notification Scope + +This notification capability SHALL apply only to the admin APIs under `/api/admin/notifications` and the admin `/notifications` interface. It SHALL not add or modify C-end `/api/c/v1/notifications` APIs or pages. + +#### Scenario: Exclude C-end notifications + +- **WHEN** this change is implemented +- **THEN** C-end notification APIs and pages MUST remain outside the change scope diff --git a/openspec/changes/update-admin-notification-center-api/tasks.md b/openspec/changes/update-admin-notification-center-api/tasks.md new file mode 100644 index 0000000..a47fb2d --- /dev/null +++ b/openspec/changes/update-admin-notification-center-api/tasks.md @@ -0,0 +1,34 @@ +## 1. Contract And Types + +- [x] 1.1 按实际接口更新通知项、列表响应、未读数、未读汇总和受控目标类型。 +- [x] 1.2 将通知列表筛选参数从 `read_status` 改为 `is_read?: boolean`。 +- [x] 1.3 更新通知 service:未读数、未读汇总、列表、单条已读、全部已读和目标接口的请求/响应类型。 + +## 2. Shared Notification State + +- [x] 2.1 使用 `data.count` 和 `data.display_count` 同步未读数量及顶部徽标。 +- [x] 2.2 使用 `{ approval, expiry, sync, system, total }` 保存分类汇总,并通过通知列表接口加载抽屉最近10条。 +- [x] 2.3 单条已读和全部已读成功后刷新真实未读数、汇总和相关通知状态,避免仅修改本地计数。 + +## 3. Header And Notification Center + +- [x] 3.1 顶部铃铛展示后端 `display_count`,支持0、普通数量和 `99+`。 +- [x] 3.2 抽屉展示最近10条真实通知,支持全部、审批、临期、同步/系统分类和进入通知中心。 +- [x] 3.3 通知中心使用 `body`、`is_read`、`read_at` 展示正文、状态和已读时间。 +- [x] 3.4 通知中心筛选使用 `category`、`type`、`severity`、`is_read`、`page`、`page_size`。 +- [x] 3.5 严重级别支持 `info`、`warning`、`error`、`critical`,未知值稳定展示。 +- [x] 3.6 全部已读和单条已读严格按实际 PUT 接口调用。 + +## 4. Controlled Navigation And Scope + +- [x] 4.1 根据 `target_type`、`target_key` 和 `target_id`实现前端内部目标白名单映射。 +- [x] 4.2 未知、无权限或不可用目标只展示通知正文,不执行任意 URL 或未注册路由跳转。 +- [x] 4.3 保留管理端通知中心路由和顶部入口,不新增 C 端通知接口、页面或逻辑。 + +## 5. Verification + +- [x] 5.1 验证真实通知字段 `body`、`is_read`、`read_at`、`ref_key` 正确展示和保存。 +- [x] 5.2 验证未读徽标、分类汇总、抽屉列表和通知中心状态一致。 +- [x] 5.3 验证 `is_read=true/false` 筛选及全部已读 `category` 请求体。 +- [x] 5.4 验证受控目标白名单和未知目标安全降级。 +- [x] 5.5 运行类型检查、lint、相关测试、构建和 `openspec validate update-admin-notification-center-api --strict`。 diff --git a/openspec/changes/update-shop-salesperson-selection/proposal.md b/openspec/changes/update-shop-salesperson-selection/proposal.md index a6d058e..acee413 100644 --- a/openspec/changes/update-shop-salesperson-selection/proposal.md +++ b/openspec/changes/update-shop-salesperson-selection/proposal.md @@ -7,7 +7,7 @@ ## What Changes - 在店铺新建和编辑表单新增可清空、可搜索的“平台业务员”选择器。 -- 候选业务员复用启用的平台账号查询接口,展示账号名和手机号摘要。 +- 候选业务员使用店铺业务员候选接口,展示账号名和手机号摘要。 - 扩展店铺创建、更新、列表查询、列表返回和详情返回的数据契约,支持 `business_owner_account_id` 及业务员展示字段。 - 在店铺列表新增业务员列和按业务员筛选项,在店铺详情展示业务员名称和手机号摘要。 - 已停用的已绑定业务员仅用于历史列表和详情展示,不能作为新建或编辑表单的候选项。 @@ -26,4 +26,5 @@ - `POST /api/admin/shops` - `PUT /api/admin/shops/{id}` - `GET /api/admin/shops` - - `GET /api/admin/accounts?account_type=platform&status=1` +- `GET /api/admin/shops/{id}` +- `GET /api/admin/shops/business-owner-candidates` diff --git a/openspec/changes/update-shop-salesperson-selection/specs/shop-management/spec.md b/openspec/changes/update-shop-salesperson-selection/specs/shop-management/spec.md index 33ef119..4d780f4 100644 --- a/openspec/changes/update-shop-salesperson-selection/specs/shop-management/spec.md +++ b/openspec/changes/update-shop-salesperson-selection/specs/shop-management/spec.md @@ -27,7 +27,9 @@ The system SHALL allow users to assign or clear an optional platform salesperson #### Scenario: Do not expose disabled accounts as editable candidates - **WHEN** 用户打开或搜索“平台业务员”选择器 -- **THEN** 系统 MUST 仅查询 `account_type=platform` 且 `status=1` 的账号 +- **THEN** 系统 MUST query `GET /api/admin/shops/business-owner-candidates` +- **AND** 请求 MUST support `keyword`、`page` 和 `page_size` +- **AND** 候选项 MUST use `id`、`username` and `phone_summary` - **AND** 已停用平台账号 MUST NOT 作为新建或编辑候选项显示 #### Scenario: No eligible salesperson exists @@ -45,7 +47,7 @@ The shop list SHALL display the assigned salesperson and support filtering by `b - **WHEN** 用户查看店铺列表 - **THEN** 列表 MUST 显示业务员列 -- **AND** 已绑定业务员的行 MUST 展示 `business_owner_name` 和 `business_owner_phone_masked` +- **AND** 已绑定业务员的行 MUST 展示 `business_owner_username` 和 `business_owner_phone_summary` #### Scenario: Filter shops by salesperson @@ -57,7 +59,7 @@ The shop list SHALL display the assigned salesperson and support filtering by `b - **GIVEN** 店铺历史上已绑定的平台业务员后来被停用 - **WHEN** 用户查看店铺列表 -- **THEN** 列表 MUST 继续展示该店铺返回的 `business_owner_name` 和 `business_owner_phone_masked` +- **THEN** 列表 MUST 继续展示该店铺返回的 `business_owner_username` 和 `business_owner_phone_summary` ### Requirement: Shop Detail Salesperson Display @@ -72,7 +74,8 @@ The shop detail SHALL display the assigned salesperson name and masked phone fro - **GIVEN** 店铺绑定的业务员已停用 - **WHEN** 用户查看该店铺详情 -- **THEN** 页面 MUST 继续使用详情响应中的 `business_owner_name` 和 `business_owner_phone_masked` 展示历史业务员信息 +- **THEN** 页面 MUST request `GET /api/admin/shops/{id}` +- **AND** 页面 MUST 继续使用详情响应中的 `business_owner_username` 和 `business_owner_phone_summary` 展示历史业务员信息 #### Scenario: Do not add distribution or commission semantics diff --git a/openspec/changes/update-shop-salesperson-selection/tasks.md b/openspec/changes/update-shop-salesperson-selection/tasks.md index 802e324..15f1c30 100644 --- a/openspec/changes/update-shop-salesperson-selection/tasks.md +++ b/openspec/changes/update-shop-salesperson-selection/tasks.md @@ -3,7 +3,7 @@ - [x] 1.1 更新店铺创建和编辑请求类型,支持可空的 `business_owner_account_id`。 - [x] 1.2 更新店铺列表查询类型,支持 `business_owner_account_id` 筛选参数。 - [x] 1.3 更新店铺列表和详情响应类型,读取业务员 ID、名称和脱敏手机号字段。 -- [x] 1.4 复用启用平台账号查询接口,读取 `account_id`、`account_name`、`phone_masked` 作为候选项。 +- [x] 1.4 接入店铺业务员候选接口,读取 `id`、`username`、`phone_summary` 作为候选项。 ## 2. Shop Form UI diff --git a/src/api/modules/account.ts b/src/api/modules/account.ts index f3256f3..8b8a39f 100644 --- a/src/api/modules/account.ts +++ b/src/api/modules/account.ts @@ -6,8 +6,6 @@ import { BaseService } from '../BaseService' import type { PlatformAccount, AccountQueryParams, - PlatformSalesperson, - PlatformSalespersonQueryParams, CreatePlatformAccountParams, BaseResponse, PaginationResponse @@ -25,16 +23,6 @@ export class AccountService extends BaseService { return this.getPage('/api/admin/accounts', params) } - /** - * 获取启用的平台业务员候选 - * GET /api/admin/accounts?account_type=platform&status=1 - */ - static getPlatformSalespeople( - params: PlatformSalespersonQueryParams - ): Promise> { - return this.getPage('/api/admin/accounts', params) - } - /** * 创建账号 * POST /api/admin/accounts diff --git a/src/api/modules/asset.ts b/src/api/modules/asset.ts index 12c98a9..88a19c7 100644 --- a/src/api/modules/asset.ts +++ b/src/api/modules/asset.ts @@ -31,7 +31,9 @@ import type { AssetOperationLogsParams, AssetPackageUsageRecord, UpdateAssetPackageUsedDataRequest, - UpdateAssetPackageExpiresAtRequest + UpdateAssetPackageExpiresAtRequest, + ExpiringAssetItem, + ExpiringAssetQueryParams } from '@/types/api' const runRateLimitedAssetAction = async ( @@ -45,6 +47,19 @@ const runRateLimitedAssetAction = async ( } export class AssetService extends BaseService { + /** + * 获取管理端临期资产列表 + * GET /api/admin/expiring-assets + */ + static getExpiringAssets( + params?: ExpiringAssetQueryParams + ): Promise< + BaseResponse<{ items: ExpiringAssetItem[]; total: number; page: number; size: number }> + > { + return this.get< + BaseResponse<{ items: ExpiringAssetItem[]; total: number; page: number; size: number }> + >('/api/admin/expiring-assets', params) + } /** * 通过任意标识符查询设备或卡的完整详情 * 支持虚拟号、ICCID、IMEI、SN、MSISDN diff --git a/src/api/modules/notification.ts b/src/api/modules/notification.ts index 4f95965..da75f5f 100644 --- a/src/api/modules/notification.ts +++ b/src/api/modules/notification.ts @@ -1,24 +1,25 @@ import { BaseService } from '../BaseService' import type { BaseResponse, - NotificationItem, NotificationListResponse, NotificationQueryParams, - NotificationSummary, + NotificationReadAllRequest, + NotificationReadAllResponse, + NotificationReadResponse, + NotificationUnreadCount, + NotificationUnreadSummary, NotificationTarget } from '@/types/api' export class NotificationService extends BaseService { - static getUnreadCount(): Promise< - BaseResponse - > { - return this.get>( - '/api/admin/notifications/unread-count' - ) + static getUnreadCount(): Promise> { + return this.get>('/api/admin/notifications/unread-count') } - static getUnreadSummary(): Promise> { - return this.get>('/api/admin/notifications/unread-summary') + static getUnreadSummary(): Promise> { + return this.get>( + '/api/admin/notifications/unread-summary' + ) } static getNotifications( @@ -27,15 +28,23 @@ export class NotificationService extends BaseService { return this.get>('/api/admin/notifications', params) } - static markRead(id: number | string): Promise> { - return this.put>(`/api/admin/notifications/${id}/read`, {}) + static markRead(id: number): Promise> { + return this.put>( + `/api/admin/notifications/${id}/read`, + {} + ) } - static markAllRead(): Promise { - return this.put('/api/admin/notifications/read-all', {}) + static markAllRead( + data: NotificationReadAllRequest = {} + ): Promise> { + return this.put>( + '/api/admin/notifications/read-all', + data + ) } - static getTarget(id: number | string): Promise> { + static getTarget(id: number): Promise> { return this.get>(`/api/admin/notifications/${id}/target`) } } diff --git a/src/api/modules/shop.ts b/src/api/modules/shop.ts index 9ee79ef..3888c21 100644 --- a/src/api/modules/shop.ts +++ b/src/api/modules/shop.ts @@ -12,7 +12,9 @@ import type { AssignShopRolesRequest, UpdateShopCreditLimitRequest, BaseResponse, - PaginationResponse + PaginationResponse, + ShopBusinessOwnerCandidate, + ShopBusinessOwnerCandidateQueryParams } from '@/types/api' export class ShopService extends BaseService { @@ -25,6 +27,27 @@ export class ShopService extends BaseService { return this.getPage('/api/admin/shops', params) } + /** + * 获取店铺业务员候选 + * GET /api/admin/shops/business-owner-candidates + */ + static getBusinessOwnerCandidates( + params?: ShopBusinessOwnerCandidateQueryParams + ): Promise> { + return this.getPage( + '/api/admin/shops/business-owner-candidates', + params + ) + } + + /** + * 获取店铺详情 + * GET /api/admin/shops/{id} + */ + static getShopDetail(id: number): Promise> { + return this.getOne(`/api/admin/shops/${id}`) + } + /** * 店铺级联查询(树结构) * GET /api/admin/shops/cascade diff --git a/src/components/core/layouts/art-header-bar/index.vue b/src/components/core/layouts/art-header-bar/index.vue index ce9059f..5d35a97 100644 --- a/src/components/core/layouts/art-header-bar/index.vue +++ b/src/components/core/layouts/art-header-bar/index.vue @@ -238,9 +238,7 @@ const { width } = useWindowSize() const unreadCount = computed(() => notificationStore.unreadCount) - const formattedUnreadCount = computed(() => - unreadCount.value > 99 ? '99+' : String(unreadCount.value) - ) + const formattedUnreadCount = computed(() => notificationStore.displayCount) const menuTopWidth = computed(() => { return width.value * 0.5 diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue index 78d43fc..6f6d807 100644 --- a/src/components/core/layouts/art-notification/index.vue +++ b/src/components/core/layouts/art-notification/index.vue @@ -39,7 +39,7 @@ {{ item.title }} - {{ item.content }} + {{ item.body }} @@ -85,23 +85,23 @@ return item.category === category } - const isRead = (item: NotificationItem) => - item.read === true || item.is_read === true || item.read_status === 'read' + const isRead = (item: NotificationItem) => item.is_read const filteredItems = computed(() => - notificationStore.summary.items.filter((item) => categoryMatches(item, activeCategory.value)) + notificationStore.recentNotifications.filter((item) => + categoryMatches(item, activeCategory.value) + ) ) const tabs = computed(() => { - const categories = notificationStore.summary.categories || {} return [ - { value: 'all', label: '全部', count: notificationStore.unreadCount }, - { value: 'approval', label: '审批', count: categories.approval || 0 }, - { value: 'expiry', label: '临期', count: categories.expiry || categories.expiring || 0 }, + { value: 'all', label: '全部', count: notificationStore.summary.total }, + { value: 'approval', label: '审批', count: notificationStore.summary.approval }, + { value: 'expiry', label: '临期', count: notificationStore.summary.expiry }, { value: 'system', label: '同步/系统', - count: (categories.sync || 0) + (categories.system || 0) + count: notificationStore.summary.sync + notificationStore.summary.system } ] }) @@ -135,11 +135,11 @@ } const targetResponse = await NotificationService.getTarget(item.id) if (targetResponse.code !== 0 || !targetResponse.data) { - ElMessage.info(item.content || '该通知暂无可跳转目标') + ElMessage.info(item.body || '该通知暂无可跳转目标') return } if (!navigateNotificationTarget(router, targetResponse.data)) { - ElMessage.info(targetResponse.data.message || item.content || '该通知暂无可跳转目标') + ElMessage.info(item.body || '该通知暂无可跳转目标') } else { close() } diff --git a/src/components/core/layouts/art-notification/style.scss b/src/components/core/layouts/art-notification/style.scss index 39f556f..a2e0b7a 100644 --- a/src/components/core/layouts/art-notification/style.scss +++ b/src/components/core/layouts/art-notification/style.scss @@ -108,9 +108,9 @@ background: var(--el-color-danger-light-9); } - &.severity-success { - color: var(--el-color-success); - background: var(--el-color-success-light-9); + &.severity-critical { + color: var(--el-color-danger); + background: var(--el-color-danger-light-9); } } diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index d022275..5b6db3a 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -20,7 +20,8 @@ export const asyncRoutes: AppRouteRecord[] = [ name: 'Notifications', component: RoutesAlias.Home, meta: { - title: '通知中心' + title: '通知中心', + isHide: true }, children: [ { @@ -30,6 +31,29 @@ export const asyncRoutes: AppRouteRecord[] = [ meta: { title: '通知中心', permissions: [NOTIFICATION_PERMISSIONS.page], + keepAlive: true, + isHide: true, + isHideTab: true + } + } + ] + }, + + // 临期资产 + { + path: '/operations', + name: 'Operations', + component: RoutesAlias.Home, + meta: { + title: '运营管理' + }, + children: [ + { + path: 'expiring-assets', + name: 'ExpiringAssets', + component: RoutesAlias.ExpiringAssets, + meta: { + title: '临期资产', keepAlive: true } } diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 8984091..1ae80ab 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -78,6 +78,7 @@ export enum RoutesAlias { // 站内通知 Notifications = '/notifications', // 通知中心 + ExpiringAssets = '/operations/expiring-assets', // 临期资产 // 佣金管理 WithdrawalApproval = '/commission-management/withdrawal-approval', // 提现审批 diff --git a/src/store/modules/notification.ts b/src/store/modules/notification.ts index 5448700..564fe27 100644 --- a/src/store/modules/notification.ts +++ b/src/store/modules/notification.ts @@ -1,22 +1,28 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { NotificationService } from '@/api/modules/notification' -import type { NotificationSummary } from '@/types/api' +import type { NotificationItem, NotificationUnreadSummary } from '@/types/api' -const getCountValue = (data: number | { count?: number; unread_count?: number } | undefined) => { - if (typeof data === 'number') return Math.max(0, data) - return Math.max(0, data?.unread_count ?? data?.count ?? 0) -} +const emptySummary = (): NotificationUnreadSummary => ({ + approval: 0, + expiry: 0, + sync: 0, + system: 0, + total: 0 +}) export const useNotificationStore = defineStore('notificationStore', () => { const unreadCount = ref(0) - const summary = ref({ items: [], categories: {}, total_unread: 0 }) + const displayCount = ref('0') + const summary = ref(emptySummary()) + const recentNotifications = ref([]) const loading = ref(false) const refreshUnreadCount = async () => { const response = await NotificationService.getUnreadCount() - if (response.code === 0) { - unreadCount.value = getCountValue(response.data) + if (response.code === 0 && response.data) { + unreadCount.value = Math.max(0, response.data.count) + displayCount.value = response.data.display_count || '0' } return unreadCount.value } @@ -24,15 +30,15 @@ export const useNotificationStore = defineStore('notificationStore', () => { const refreshSummary = async () => { loading.value = true try { - const response = await NotificationService.getUnreadSummary() - if (response.code === 0 && response.data) { - summary.value = { - ...response.data, - items: (response.data.items || []).slice(0, 10) - } - if (response.data.total_unread !== undefined) { - unreadCount.value = Math.max(0, response.data.total_unread) - } + const [summaryResponse, listResponse] = await Promise.all([ + NotificationService.getUnreadSummary(), + NotificationService.getNotifications({ page: 1, page_size: 10 }) + ]) + if (summaryResponse.code === 0 && summaryResponse.data) { + summary.value = summaryResponse.data + } + if (listResponse.code === 0 && listResponse.data) { + recentNotifications.value = listResponse.data.items.slice(0, 10) } await refreshUnreadCount() return summary.value @@ -41,46 +47,27 @@ export const useNotificationStore = defineStore('notificationStore', () => { } } - const markRead = async (id: number | string) => { + const markRead = async (id: number) => { const response = await NotificationService.markRead(id) if (response.code === 0) { - unreadCount.value = Math.max(0, unreadCount.value - 1) - summary.value = { - ...summary.value, - items: summary.value.items.map((item) => - item.id === id ? { ...item, read: true, is_read: true, read_status: 'read' } : item - ) - } - await refreshUnreadCount() + await refreshSummary() } return response } - const markAllRead = async () => { - const response = await NotificationService.markAllRead() + const markAllRead = async (category?: string) => { + const response = await NotificationService.markAllRead(category ? { category } : {}) if (response.code === 0) { - unreadCount.value = 0 - summary.value = { - ...summary.value, - total_unread: 0, - categories: Object.fromEntries( - Object.keys(summary.value.categories || {}).map((key) => [key, 0]) - ), - items: summary.value.items.map((item) => ({ - ...item, - read: true, - is_read: true, - read_status: 'read' - })) - } - await refreshUnreadCount() + await refreshSummary() } return response } return { unreadCount, + displayCount, summary, + recentNotifications, loading, refreshUnreadCount, refreshSummary, diff --git a/src/types/api/account.ts b/src/types/api/account.ts index 94747b4..108e1a7 100644 --- a/src/types/api/account.ts +++ b/src/types/api/account.ts @@ -107,20 +107,6 @@ export interface AccountQueryParams extends PaginationParams { enterprise_id?: number // 按企业ID筛选 } -// 平台业务员候选账号 -export interface PlatformSalesperson { - account_id: number - account_name: string - phone_masked: string -} - -// 平台业务员候选查询参数 -export interface PlatformSalespersonQueryParams extends PaginationParams { - account_type: 'platform' - status: AccountStatus - account_name?: string -} - // 代理商查询参数 export interface AgentQueryParams extends PaginationParams { keyword?: string diff --git a/src/types/api/asset.ts b/src/types/api/asset.ts index 3914989..adcd028 100644 --- a/src/types/api/asset.ts +++ b/src/types/api/asset.ts @@ -69,6 +69,35 @@ export type AssetRealnamePolicy = 'none' | 'before_order' | 'after_order' // 预计最终到期时间状态 export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | 'none' | 'invalid_data' +// 临期等级由后端返回,前端仅用于展示和筛选结果标识 +export type ExpiryLevel = string + +export interface ExpiringAssetItem { + asset_type: AssetType + asset_id: number + identifier: string + shop_name?: string | null + package_name?: string | null + estimated_final_expires_at: string + days_until_final_expiry: number + expiry_level?: ExpiryLevel | null + expiry_level_name?: string | null + can_renew: boolean +} + +export interface ExpiringAssetQueryParams { + asset_type?: AssetType + keyword?: string + shop_id?: number + package_id?: number + days_min?: number + days_max?: number + expires_from?: string + expires_to?: string + page?: number + size?: number +} + // 批量更新资产实名认证策略请求 export interface BatchUpdateAssetRealnamePolicyRequest { asset_ids: number[] diff --git a/src/types/api/card.ts b/src/types/api/card.ts index 16ee784..e282a94 100644 --- a/src/types/api/card.ts +++ b/src/types/api/card.ts @@ -405,6 +405,9 @@ export interface StandaloneIotCard { expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态 expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称 is_expiring?: boolean // 是否临期,由后端判断 + expiry_level?: string | null // 临期等级 + expiry_level_name?: string | null // 临期等级名称 + can_renew?: boolean // 是否允许续费 last_month_total_mb?: number // 上月流量总量(MB) last_data_check_at?: string | null // 最后流量检查时间 last_real_name_check_at?: string | null // 最后实名检查时间 diff --git a/src/types/api/device.ts b/src/types/api/device.ts index 697fe4c..6d2f941 100644 --- a/src/types/api/device.ts +++ b/src/types/api/device.ts @@ -73,6 +73,9 @@ export interface Device { expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态 expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称 is_expiring?: boolean // 是否临期,由后端判断 + expiry_level?: string | null // 临期等级 + expiry_level_name?: string | null // 临期等级名称 + can_renew?: boolean // 是否允许续费 authorized_enterprise_id?: number | null // 当前有效授权企业ID authorized_enterprise_name?: string | null // 当前有效授权企业名称 } diff --git a/src/types/api/notification.ts b/src/types/api/notification.ts index 6333c4c..5982311 100644 --- a/src/types/api/notification.ts +++ b/src/types/api/notification.ts @@ -1,51 +1,65 @@ -export type NotificationCategory = 'approval' | 'expiry' | 'sync' | 'system' | 'balance' | string -export type NotificationReadStatus = 'read' | 'unread' | string +export type NotificationCategory = 'approval' | 'expiry' | 'sync' | 'system' | string +export type NotificationSeverity = 'info' | 'warning' | 'error' | 'critical' | string export interface NotificationItem { - id: number | string + id: number title: string - content: string - category?: NotificationCategory | null - type?: string | null - severity?: string | null - read?: boolean - is_read?: boolean - read_status?: NotificationReadStatus | null - created_at?: string | null - updated_at?: string | null - ref_type?: string | null - ref_id?: number | string | null - [key: string]: unknown + body: string + category: NotificationCategory + type: string + severity: NotificationSeverity + is_read: boolean + created_at: string + read_at: string | null + ref_type: string | null + ref_id: string | null + ref_key: string | null } export interface NotificationQueryParams { - page?: number - page_size?: number category?: string type?: string severity?: string - read_status?: string + is_read?: boolean + page?: number + page_size?: number } export interface NotificationListResponse { items: NotificationItem[] + page: number + size: number total: number - page?: number - page_size?: number - size?: number } -export interface NotificationSummary { - items: NotificationItem[] - categories?: Record - total_unread?: number +export interface NotificationUnreadCount { + count: number + display_count: string +} + +export interface NotificationUnreadSummary { + approval: number + expiry: number + sync: number + system: number + total: number +} + +export interface NotificationReadResponse { + success: boolean +} + +export interface NotificationReadAllRequest { + category?: string +} + +export interface NotificationReadAllResponse { + updated_count: number } export interface NotificationTarget { - available?: boolean - route_name?: string | null - route_path?: string | null - route_params?: Record | null - query?: Record | null - message?: string | null + available: boolean + target_id: number | null + target_key: string + target_type: string } diff --git a/src/types/api/shop.ts b/src/types/api/shop.ts index 1b03de8..bca6440 100644 --- a/src/types/api/shop.ts +++ b/src/types/api/shop.ts @@ -21,9 +21,10 @@ export interface ShopResponse { contact_name: string // 联系人姓名 contact_phone: string // 联系人电话 status: number // 状态 (0:禁用, 1:启用) - business_owner_account_id?: number | null // 平台业务员账号ID - business_owner_name?: string // 平台业务员名称 - business_owner_phone_masked?: string // 平台业务员脱敏手机号 + business_owner_account_id: number | null // 平台业务员账号ID + business_owner_username: string // 平台业务员账号名 + business_owner_phone_summary: string // 平台业务员手机号摘要 + business_owner_available: boolean // 是否仍可用于通知接收 } // 店铺列表查询参数 @@ -71,6 +72,18 @@ export interface UpdateShopParams { business_owner_account_id?: number | null // 平台业务员账号ID } +// 店铺业务员候选 +export interface ShopBusinessOwnerCandidate { + id: number // 平台业务员账号ID + username: string // 平台业务员账号名 + phone_summary: string // 手机号摘要 +} + +// 店铺业务员候选查询参数 +export interface ShopBusinessOwnerCandidateQueryParams extends PaginationParams { + keyword?: string // 用户名或手机号关键词 +} + // 店铺列表分页响应 export interface ShopPageResult { items: ShopResponse[] | null // 店铺列表 diff --git a/src/utils/business/expiryEstimate.ts b/src/utils/business/expiryEstimate.ts index 5e9560c..1ad6239 100644 --- a/src/utils/business/expiryEstimate.ts +++ b/src/utils/business/expiryEstimate.ts @@ -29,3 +29,11 @@ export const getExpiryEstimateTooltip = (estimate: ExpiryEstimate): string => { if (!estimate.is_expiring || estimate.days_until_final_expiry == null) return '' return `剩余${estimate.days_until_final_expiry}天` } + +export const getExpiryEstimateClass = (estimate: ExpiryEstimate): string => { + if (!estimate.is_expiring || estimate.days_until_final_expiry == null) return '' + if (estimate.days_until_final_expiry <= 3) return 'expiry-estimate--critical' + if (estimate.days_until_final_expiry <= 7) return 'expiry-estimate--warning' + if (estimate.days_until_final_expiry <= 15) return 'expiry-estimate--notice' + return '' +} diff --git a/src/utils/business/notificationNavigation.ts b/src/utils/business/notificationNavigation.ts index 758854f..3d60084 100644 --- a/src/utils/business/notificationNavigation.ts +++ b/src/utils/business/notificationNavigation.ts @@ -1,31 +1,31 @@ import type { Router } from 'vue-router' import type { NotificationTarget } from '@/types/api' +import { RoutesAlias } from '@/router/routesAlias' -export const navigateNotificationTarget = (router: Router, target?: NotificationTarget | null) => { - if (!target || target.available === false) return false - const query = target.query - ? Object.fromEntries(Object.entries(target.query).map(([key, value]) => [key, String(value)])) - : undefined +type TargetRoute = { + path: string + query?: Record +} - if (target.route_name) { - if (!router.hasRoute(target.route_name)) return false - void router.push({ - name: target.route_name, - params: target.route_params || undefined, - query - }) - return true +const resolveTargetRoute = (target: NotificationTarget): TargetRoute | null => { + const targetValues = [target.target_key, target.target_type] + + if (targetValues.includes('expiring_assets') || targetValues.includes('expiring_asset_list')) { + return { path: RoutesAlias.ExpiringAssets } } - const path = target.route_path || '' - if (!path.startsWith('/') || path.startsWith('//') || /^https?:\/\//i.test(path)) return false + if (targetValues.includes('notification_center')) return { path: RoutesAlias.Notifications } - const resolved = router.resolve({ - path, - query - }) + return null +} + +export const navigateNotificationTarget = (router: Router, target?: NotificationTarget | null) => { + if (!target?.available) return false + const route = resolveTargetRoute(target) + if (!route) return false + + const resolved = router.resolve({ path: route.path, query: route.query }) if (!resolved.matched.length || resolved.name === 'Exception404') return false - void router.push(resolved) return true } diff --git a/src/views/asset-management/asset-information/components/BasicInfoCard.vue b/src/views/asset-management/asset-information/components/BasicInfoCard.vue index 2803f76..e5ec523 100644 --- a/src/views/asset-management/asset-information/components/BasicInfoCard.vue +++ b/src/views/asset-management/asset-information/components/BasicInfoCard.vue @@ -88,7 +88,7 @@ {{ getExpiryEstimateText(cardInfo) }} @@ -182,7 +182,7 @@ {{ getExpiryEstimateText(cardInfo) }} @@ -598,7 +598,11 @@ import { useAuth } from '@/composables/useAuth' import { useUserStore } from '@/store/modules/user' import { formatDateTime } from '@/utils/business/format' - import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate' + import { + getExpiryEstimateText, + getExpiryEstimateTooltip, + getExpiryEstimateClass + } from '@/utils/business/expiryEstimate' // Props interface BindingCard { @@ -987,11 +991,24 @@ word-break: break-all; } - .expiry-estimate--expiring { + .expiry-estimate--critical, + .expiry-estimate--warning, + .expiry-estimate--notice { font-weight: 500; + } + + .expiry-estimate--critical { color: var(--el-color-danger); } + .expiry-estimate--warning { + color: #8e44ad; + } + + .expiry-estimate--notice { + color: #e83e8c; + } + .card-header { display: flex; gap: 12px; diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue index 076c6de..6e27391 100644 --- a/src/views/asset-management/device-list/index.vue +++ b/src/views/asset-management/device-list/index.vue @@ -995,7 +995,11 @@ import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useAuth } from '@/composables/useAuth' import { formatDateTime } from '@/utils/business/format' - import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate' + import { + getExpiryEstimateText, + getExpiryEstimateTooltip, + getExpiryEstimateClass + } from '@/utils/business/expiryEstimate' import type { PackageSeriesResponse } from '@/types/api' import type { EnterpriseItem } from '@/types/api/enterprise' import type { @@ -1900,7 +1904,7 @@ h( 'span', { - class: { 'expiry-estimate--expiring': row.is_expiring }, + class: getExpiryEstimateClass(row), title: getExpiryEstimateTooltip(row) }, getExpiryEstimateText(row) @@ -3218,8 +3222,21 @@ cursor: pointer; } - .expiry-estimate--expiring { + .expiry-estimate--critical, + .expiry-estimate--warning, + .expiry-estimate--notice { font-weight: 500; + } + + .expiry-estimate--critical { color: var(--el-color-danger); } + + .expiry-estimate--warning { + color: #8e44ad; + } + + .expiry-estimate--notice { + color: #e83e8c; + } diff --git a/src/views/asset-management/iot-card-management/index.vue b/src/views/asset-management/iot-card-management/index.vue index 60ac789..a10d804 100644 --- a/src/views/asset-management/iot-card-management/index.vue +++ b/src/views/asset-management/iot-card-management/index.vue @@ -1045,7 +1045,11 @@ import { useUserStore } from '@/store/modules/user' import { storeToRefs } from 'pinia' import { formatDateTime } from '@/utils/business/format' - import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate' + import { + getExpiryEstimateText, + getExpiryEstimateTooltip, + getExpiryEstimateClass + } from '@/utils/business/expiryEstimate' import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit' import type { StandaloneIotCard, @@ -2127,7 +2131,7 @@ h( 'span', { - class: { 'expiry-estimate--expiring': row.is_expiring }, + class: getExpiryEstimateClass(row), title: getExpiryEstimateTooltip(row) }, getExpiryEstimateText(row) @@ -3044,8 +3048,21 @@ cursor: pointer; } - .expiry-estimate--expiring { + .expiry-estimate--critical, + .expiry-estimate--warning, + .expiry-estimate--notice { font-weight: 500; + } + + .expiry-estimate--critical { color: var(--el-color-danger); } + + .expiry-estimate--warning { + color: #8e44ad; + } + + .expiry-estimate--notice { + color: #e83e8c; + } diff --git a/src/views/dashboard/console/index.vue b/src/views/dashboard/console/index.vue index d9d44a8..8b60fbe 100644 --- a/src/views/dashboard/console/index.vue +++ b/src/views/dashboard/console/index.vue @@ -1,6 +1,6 @@ + + diff --git a/src/views/notifications/index.vue b/src/views/notifications/index.vue index 36d8034..5ff6705 100644 --- a/src/views/notifications/index.vue +++ b/src/views/notifications/index.vue @@ -20,10 +20,10 @@
- - + + @@ -32,11 +32,11 @@ - + - - - + + + 刷新
@@ -59,7 +59,8 @@
- + + @@ -183,7 +187,10 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElCard, ElEmpty } from 'element-plus' import { RoutesAlias } from '@/router/routesAlias' - import { CardService, DeviceService } from '@/api/modules' + import { CardService, DeviceService } from '@/api/modules' + import { JULY_PERMISSIONS } from '@/config/constants' + import { useAuth } from '@/composables/useAuth' + import { normalizeApiError } from '@/utils/business/apiError' import { formatRemainingTime, FrontendRateLimitError, @@ -204,9 +211,9 @@ SwitchModeDialog, WiFiConfigDialog, PackageRechargeDialog, - DailyRecordsDialog, - OrderHistoryDialog, - SpeedLimitDialog + DailyRecordsDialog, + OrderHistoryDialog, + SpeedLimitDialog } from './components/dialogs' import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue' import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue' @@ -235,6 +242,7 @@ const route = useRoute() const router = useRouter() + const { hasAuth } = useAuth() // ========== 组件引用 ========== const assetSearchCardRef = ref>() @@ -294,6 +302,7 @@ // 绑定卡实名状态更新 const bindingCardRealnameStatusDialogVisible = ref(false) const speedLimitDialogVisible = ref(false) + const speedTierSubmitting = ref(false) const bindingCardRealnameStatusIccid = ref('') const bindingCardRealnameStatusValue = ref(0) @@ -806,12 +815,34 @@ } } const handleSpeedTierConfirm = async (data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }) => { - if (!cardInfo.value?.iccid) return - const response = await CardService.setSpeedTier(cardInfo.value.iccid, data.code) - if (response.code === 0) { + if (!hasAuth(JULY_PERMISSIONS.speedTier.update)) { + ElMessage.error('暂无设置限速权限') + return + } + + const iccid = cardInfo.value?.iccid + if (!iccid) { + ElMessage.error('当前卡缺少 ICCID,无法设置限速') + return + } + if (speedTierSubmitting.value) return + + speedTierSubmitting.value = true + try { + const response = await CardService.setSpeedTier(iccid, data.code) + if (response.code !== 0) { + ElMessage.error(response.msg || '限速设置失败') + return + } + ElMessage.success(`限速设置成功:${response.data.speed_tier_name}`) speedLimitDialogVisible.value = false await handleRefresh() + } catch (error) { + console.error('设置限速失败:', error) + ElMessage.error(normalizeApiError(error).message) + } finally { + speedTierSubmitting.value = false } } diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue index 984cb00..d687e5b 100644 --- a/src/views/asset-management/device-list/index.vue +++ b/src/views/asset-management/device-list/index.vue @@ -17,29 +17,6 @@ @refresh="handleRefresh" > @@ -41,6 +44,14 @@ + + () const createDialogVisible = ref(false) const createLoading = ref(false) + const exportDialogVisible = ref(false) const tableRef = ref() const createFormRef = ref() @@ -438,6 +451,18 @@ created_at_end: '' }) + const exportQuery = computed(() => { + const [startDate, endDate] = searchForm.created_at_range || [] + return { + status: searchForm.status, + flow_type: searchForm.flow_type, + old_asset_keyword: searchForm.old_asset_keyword || undefined, + new_asset_keyword: searchForm.new_asset_keyword || undefined, + created_at_start: startDate || searchForm.created_at_start || undefined, + created_at_end: endDate || searchForm.created_at_end || undefined + } + }) + // 创建换货单表单 const createForm = reactive<{ exchange_reason: string diff --git a/src/views/asset-management/expiring-assets/index.vue b/src/views/asset-management/expiring-assets/index.vue index 9477199..647d8f5 100644 --- a/src/views/asset-management/expiring-assets/index.vue +++ b/src/views/asset-management/expiring-assets/index.vue @@ -5,7 +5,7 @@ v-model:filter="searchForm" :items="searchFormItems" :show-expand="true" - label-width="85" + label-width="100px" @reset="handleReset" @search="handleSearch" /> @@ -17,7 +17,10 @@ @refresh="handleRefresh" > @@ -47,11 +50,12 @@ diff --git a/src/views/asset-management/export-task-management/export-agent-recharge/index.vue b/src/views/asset-management/export-task-management/export-agent-recharge/index.vue new file mode 100644 index 0000000..1628df5 --- /dev/null +++ b/src/views/asset-management/export-task-management/export-agent-recharge/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/views/asset-management/export-task-management/export-agent-wallet-transaction/index.vue b/src/views/asset-management/export-task-management/export-agent-wallet-transaction/index.vue new file mode 100644 index 0000000..ff5c53f --- /dev/null +++ b/src/views/asset-management/export-task-management/export-agent-wallet-transaction/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/views/asset-management/export-task-management/export-exchange/index.vue b/src/views/asset-management/export-task-management/export-exchange/index.vue new file mode 100644 index 0000000..365352e --- /dev/null +++ b/src/views/asset-management/export-task-management/export-exchange/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/views/asset-management/export-task-management/export-package/index.vue b/src/views/asset-management/export-task-management/export-package/index.vue new file mode 100644 index 0000000..c0d2a2b --- /dev/null +++ b/src/views/asset-management/export-task-management/export-package/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/views/asset-management/export-task-management/export-refund/index.vue b/src/views/asset-management/export-task-management/export-refund/index.vue new file mode 100644 index 0000000..17e08ee --- /dev/null +++ b/src/views/asset-management/export-task-management/export-refund/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/views/asset-management/export-task-management/export-task-detail/index.vue b/src/views/asset-management/export-task-management/export-task-detail/index.vue index 747aa21..339b33b 100644 --- a/src/views/asset-management/export-task-management/export-task-detail/index.vue +++ b/src/views/asset-management/export-task-management/export-task-detail/index.vue @@ -14,7 +14,6 @@ 下载文件 - 取消任务 @@ -41,7 +40,7 @@ + + diff --git a/src/views/asset-management/task-management/bulk-purchase/index.vue b/src/views/asset-management/task-management/bulk-purchase/index.vue new file mode 100644 index 0000000..7857893 --- /dev/null +++ b/src/views/asset-management/task-management/bulk-purchase/index.vue @@ -0,0 +1,506 @@ + + + + + diff --git a/src/views/asset-management/task-management/device-batch-allocation/index.vue b/src/views/asset-management/task-management/device-batch-allocation/index.vue new file mode 100644 index 0000000..6ad86bf --- /dev/null +++ b/src/views/asset-management/task-management/device-batch-allocation/index.vue @@ -0,0 +1,506 @@ + + + + + diff --git a/src/views/asset-management/task-management/device-task/index.vue b/src/views/asset-management/task-management/device-task/index.vue index 2ee4c02..e86559a 100644 --- a/src/views/asset-management/task-management/device-task/index.vue +++ b/src/views/asset-management/task-management/device-task/index.vue @@ -1,14 +1,13 @@ diff --git a/src/views/asset-management/task-management/task-detail/index.vue b/src/views/asset-management/task-management/task-detail/index.vue index 57748b0..5138922 100644 --- a/src/views/asset-management/task-management/task-detail/index.vue +++ b/src/views/asset-management/task-management/task-detail/index.vue @@ -2,7 +2,6 @@
-
返回 -

任务详情

+

{{ taskTitle }}

- - -
+
- 失败记录 ({{ taskDetail.fail_count }}) + {{ resultAction }}失败记录 ({{ taskDetail.fail_count }}) - - + + + +
- -
+
- 跳过记录 ({{ taskDetail.skip_count }}) + {{ resultAction }}跳过记录 ({{ taskDetail.skip_count }}) - - + + + + + +
+ +
+ + {{ resultAction }}警告记录 ({{ warningCount }}) + + + + + + +
@@ -47,16 +70,15 @@ @@ -285,8 +355,8 @@ .task-detail-page { .detail-header { display: flex; - gap: 16px; align-items: center; + gap: 16px; padding-bottom: 16px; .detail-title { @@ -297,8 +367,7 @@ } } - .failure-section, - .skipped-section { + .result-section { margin-top: 20px; .section-title { diff --git a/src/views/commission-management/agent-fund-overview/index.vue b/src/views/commission-management/agent-fund-overview/index.vue index 9c6f022..8f82fa5 100644 --- a/src/views/commission-management/agent-fund-overview/index.vue +++ b/src/views/commission-management/agent-fund-overview/index.vue @@ -269,9 +269,22 @@ 搜索 重置 + + 导出 +
+ { + const [startDate, endDate] = mainWalletSearchForm.date_range || [] + return { + shop_id: currentShop.value?.shop_id, + transaction_type: mainWalletSearchForm.transaction_type, + start_date: startDate || undefined, + end_date: endDate || undefined, + asset_identifier: mainWalletSearchForm.asset_identifier.trim() || undefined + } + }) // 列配置 const columnOptions = [ @@ -878,10 +903,6 @@ } ]) - onMounted(() => { - getTableData() - }) - // 获取代理商资金汇总列表 const getTableData = async () => { loading.value = true @@ -905,6 +926,14 @@ } } + const loadInitialData = async () => { + await getTableData() + const shopId = Number(route.query.shop_id) + if (!shopId) return + const targetShop = summaryList.value.find((item) => item.shop_id === shopId) + if (targetShop) showDetail(targetShop) + } + // 搜索 const handleSearch = () => { pagination.page = 1 @@ -953,6 +982,10 @@ loadCommissionRecords() } + onMounted(() => { + void loadInitialData() + }) + // 获取操作按钮 const getActions = (row: ShopFundSummaryItem) => { const actions: any[] = [] diff --git a/src/views/dashboard/console/widget/ExpiringAssetStats.vue b/src/views/dashboard/console/widget/ExpiringAssetStats.vue index 4dc11af..d6fd430 100644 --- a/src/views/dashboard/console/widget/ExpiringAssetStats.vue +++ b/src/views/dashboard/console/widget/ExpiringAssetStats.vue @@ -23,20 +23,24 @@ import { onMounted, reactive } from 'vue' import { useRouter } from 'vue-router' import { AssetService } from '@/api/modules' - import type { AssetType } from '@/types/api' + import type { AssetType, ExpiringAssetType } from '@/types/api' import { RoutesAlias } from '@/router/routesAlias' defineOptions({ name: 'ExpiringAssetStats' }) const router = useRouter() const counts = reactive>({ card: 0, device: 0 }) + const apiAssetTypes: Record = { + card: 'iot_card', + device: 'device' + } const loadCount = async (assetType: AssetType) => { try { const response = await AssetService.getExpiringAssets({ - asset_type: assetType, + asset_type: apiAssetTypes[assetType], page: 1, - size: 1 + page_size: 1 }) if (response.code === 0 && response.data) counts[assetType] = response.data.total || 0 } catch { @@ -47,7 +51,7 @@ const goToList = (assetType?: AssetType) => { router.push({ path: RoutesAlias.ExpiringAssets, - query: assetType ? { asset_type: assetType } : {} + query: assetType ? { asset_type: apiAssetTypes[assetType] } : {} }) } diff --git a/src/views/finance/agent-recharge/agentRechargeActions.ts b/src/views/finance/agent-recharge/agentRechargeActions.ts index 42c581f..c7b8049 100644 --- a/src/views/finance/agent-recharge/agentRechargeActions.ts +++ b/src/views/finance/agent-recharge/agentRechargeActions.ts @@ -19,6 +19,11 @@ export const buildAgentRechargeActions = ( options: BuildAgentRechargeActionsOptions ): AgentRechargeAction[] => { const actions: AgentRechargeAction[] = [] + const hasApprovalRecord = + row.approval_provider === 'wecom' || + row.approval_source === 'wecom' || + row.approval_source === 'legacy' || + (row.approval_instance_id !== undefined && row.approval_instance_id !== null) if ( row.payment_method === 'offline' && @@ -33,6 +38,7 @@ export const buildAgentRechargeActions = ( } if ( + !hasApprovalRecord && row.status === AgentRechargeStatus.PENDING && row.payment_method === 'offline' && options.hasAuth('agent_recharge:confirm_payment') @@ -44,7 +50,11 @@ export const buildAgentRechargeActions = ( }) } - if (row.status === AgentRechargeStatus.PENDING && options.hasAuth('agent_recharge:reject')) { + if ( + !hasApprovalRecord && + row.status === AgentRechargeStatus.PENDING && + options.hasAuth('agent_recharge:reject') + ) { actions.push({ label: '拒绝', handler: () => options.onReject(row), diff --git a/src/views/finance/agent-recharge/detail.vue b/src/views/finance/agent-recharge/detail.vue index eb1c1fb..74ac517 100644 --- a/src/views/finance/agent-recharge/detail.vue +++ b/src/views/finance/agent-recharge/detail.vue @@ -68,26 +68,12 @@ 2: 'success', // 已支付 3: 'success', // 已完成 4: 'info', // 已关闭 - 5: 'danger', // 已退款 - 6: 'danger' // 已驳回 + 5: 'danger', // 已退款 + 6: 'danger' // 已驳回 } return statusMap[status] || 'info' } - // 获取状态文本(优先使用 status_name,否则使用本地映射) - const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => { - if (statusName) return statusName - const statusMap: Record = { - 1: '待支付', - 2: '已支付', - 3: '已完成', - 4: '已关闭', - 5: '已退款', - 6: '已驳回' - } - return statusMap[status] || '-' - } - // 获取支付方式文本 const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => { const methodMap: Record = { @@ -97,6 +83,12 @@ return methodMap[method] || method } + const getApprovalProviderText = (data: AgentRecharge) => { + if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微' + if (data.approval_source === 'legacy') return '历史审批' + return data.approval_provider || data.approval_source || '-' + } + // 详情配置 const detailSections = computed((): DetailSection[] => [ { @@ -104,6 +96,7 @@ fields: [ { label: '充值单号', prop: 'recharge_no' }, { label: '店铺名称', prop: 'shop_name' }, + { label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' }, { label: '充值金额', formatter: (_, data) => formatCurrency(data.amount) @@ -111,9 +104,7 @@ { label: '状态', render: (data) => - h(ElTag, { type: getStatusType(data.status) }, () => - getStatusText(data.status, data.status_name) - ) + h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-') }, { label: '驳回原因', @@ -123,6 +114,30 @@ } ] }, + { + title: '企微审批信息', + fields: [ + { label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) }, + { + label: '审批状态', + formatter: (_, data) => data.approval_status_name || '-' + }, + { + label: '当前审批人摘要', + formatter: (_, data) => data.current_approver_summary || '-', + fullWidth: true + } + ] + }, + { + title: '业务处理结果', + fields: [ + { + label: '处理状态', + formatter: (_, data) => data.processing_status_name || '-' + } + ] + }, { title: '支付信息', fields: [ diff --git a/src/views/finance/agent-recharge/index.vue b/src/views/finance/agent-recharge/index.vue index 958ed36..0aa542e 100644 --- a/src/views/finance/agent-recharge/index.vue +++ b/src/views/finance/agent-recharge/index.vue @@ -24,6 +24,9 @@ v-if="hasAuth('agent_recharge:create')" >创建充值订单 + + 导出 + @@ -66,7 +69,7 @@ placeholder="请输入充值金额(元)" />
- 最小: ¥0.01,最大: 不限制 + 最小: ¥0.01,最大: ¥1,000,000.00
@@ -129,6 +132,15 @@ + + + (null) @@ -336,8 +350,11 @@ placeholder: '请选择状态', options: [ { label: '待支付', value: 1 }, - { label: '已完成', value: 2 }, - { label: '已取消', value: 3 } + { label: '已支付', value: 2 }, + { label: '已完成', value: 3 }, + { label: '已关闭', value: 4 }, + { label: '已退款', value: 5 }, + { label: '已驳回', value: 6 } ], config: { clearable: true @@ -370,16 +387,19 @@ { label: '店铺名称', prop: 'shop_name' }, { label: '充值金额', prop: 'amount' }, { label: '状态', prop: 'status' }, + { label: '审批渠道', prop: 'approval_provider' }, { label: '提交人', prop: 'submitter_name' }, { label: '审批状态', prop: 'approval_status' }, { label: '当前审批人摘要', prop: 'current_approver_summary' }, { label: '业务处理状态', prop: 'processing_status_name' }, { label: '支付方式', prop: 'payment_method' }, { label: '支付通道', prop: 'payment_channel' }, + { label: '运营备注', prop: 'remark' }, { label: '驳回原因', prop: 'rejection_reason' }, { label: '创建时间', prop: 'created_at' }, { label: '支付时间', prop: 'paid_at' }, - { label: '完成时间', prop: 'completed_at' } + { label: '完成时间', prop: 'completed_at' }, + { label: '更新时间', prop: 'updated_at' } ] const createFormRef = ref() @@ -387,6 +407,7 @@ const rejectFormRef = ref() const uploadRef = ref>() const MIN_RECHARGE_AMOUNT = 0.01 + const MAX_RECHARGE_AMOUNT = 1_000_000 const createRules = computed(() => { const rules: FormRules = { @@ -402,6 +423,10 @@ callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`)) return } + if (Number(value) > MAX_RECHARGE_AMOUNT) { + callback(new Error(`充值金额最大为 ¥${MAX_RECHARGE_AMOUNT.toFixed(2)}`)) + return + } callback() }, trigger: 'blur' @@ -464,26 +489,12 @@ 2: 'success', // 已支付 3: 'success', // 已完成 4: 'info', // 已关闭 - 5: 'danger', // 已退款 - 6: 'danger' // 已驳回 + 5: 'danger', // 已退款 + 6: 'danger' // 已驳回 } return statusMap[status] || 'info' } - // 获取状态文本(优先使用 status_name,否则使用本地映射) - const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => { - if (statusName) return statusName - const statusMap: Record = { - 1: '待支付', - 2: '已支付', - 3: '已完成', - 4: '已关闭', - 5: '已退款', - 6: '已驳回' - } - return statusMap[status] || '-' - } - // 获取支付方式文本 const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => { const methodMap: Record = { @@ -530,9 +541,17 @@ label: '状态', width: 100, formatter: (row: AgentRecharge) => { - return h(ElTag, { type: getStatusType(row.status) }, () => - getStatusText(row.status, row.status_name) - ) + return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-') + } + }, + { + prop: 'approval_provider', + label: '审批渠道', + width: 110, + formatter: (row: AgentRecharge) => { + if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微' + if (row.approval_source === 'legacy') return '历史审批' + return row.approval_provider || row.approval_source || '-' } }, { @@ -605,6 +624,12 @@ label: '完成时间', width: 180, formatter: (row: AgentRecharge) => (row.completed_at ? formatDateTime(row.completed_at) : '-') + }, + { + prop: 'updated_at', + label: '更新时间', + width: 180, + formatter: (row: AgentRecharge) => formatDateTime(row.updated_at) } ]) @@ -711,6 +736,13 @@ getTableData() } + const exportQuery = computed(() => ({ + shop_id: searchForm.shop_id, + status: searchForm.status, + start_date: searchForm.start_date || searchForm.dateRange?.[0], + end_date: searchForm.end_date || searchForm.dateRange?.[1] + })) + // 刷新表格 const handleRefresh = () => { getTableData() diff --git a/src/views/finance/refund/detail.vue b/src/views/finance/refund/detail.vue index e83f786..43a2295 100644 --- a/src/views/finance/refund/detail.vue +++ b/src/views/finance/refund/detail.vue @@ -162,6 +162,34 @@ return `¥${(amount / 100).toFixed(2)}` } + const getApprovalProviderText = (item: Refund) => { + const provider = item.approval_provider || item.approval_source || item.approval?.source + if (provider === 'wecom') return '企微' + if (provider === 'legacy') return '历史审批' + if (provider === 'none') return '无' + return provider || '-' + } + + const getRefundApprovalStatusText = (item: Refund) => { + return item.approval_status_name || item.approval?.status_name || item.approval?.status || '-' + } + + const isHiddenIdKey = (key: string) => { + const normalizedKey = key.toLowerCase() + return normalizedKey === 'id' || normalizedKey === 'userid' || normalizedKey.endsWith('_id') + } + + const sanitizeStructuredValue = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map((item) => sanitizeStructuredValue(item)) + if (!value || typeof value !== 'object') return value + + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => !isHiddenIdKey(key)) + .map(([key, item]) => [key, sanitizeStructuredValue(item)]) + ) + } + const formatStructuredValue = (value: unknown) => { if (value === undefined || value === null || value === '') return '-' if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { @@ -171,7 +199,7 @@ } const renderStructuredValue = (value: unknown) => - h('pre', { class: 'structured-value' }, formatStructuredValue(value)) + h('pre', { class: 'structured-value' }, formatStructuredValue(sanitizeStructuredValue(value))) const renderTimeline = (timeline: unknown) => { if (!Array.isArray(timeline) || timeline.length === 0) return h('span', '-') @@ -181,7 +209,7 @@ { class: 'approval-timeline' }, timeline.map((item: any) => { const title = item.status_name || item.status || item.content || '审批节点' - const operator = item.operator_name || item.operator || '' + const operator = item.operator_name || '' const time = item.time || item.timestamp || '' const comment = item.comment || item.content || '' return h('div', { class: 'approval-timeline__item' }, [ @@ -207,6 +235,8 @@ { label: '订单号', prop: 'order_no' }, { label: '资产标识符', prop: 'asset_identifier' }, { label: '资产类型', prop: 'asset_type' }, + { label: '退款状态', prop: 'status_name', formatter: (value) => value || '-' }, + { label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' }, { label: '申请退款金额', formatter: (_, data) => formatCurrency(data.requested_refund_amount) @@ -225,6 +255,12 @@ formatter: (value) => value || '-', fullWidth: true }, + { + label: '拒绝原因', + prop: 'reject_reason', + formatter: (value) => value || '-', + fullWidth: true + }, { label: '备注', prop: 'remark', @@ -258,17 +294,30 @@ formatter: (_, data) => (data.commission_deducted ? '是' : '否') }, { label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) }, + { + label: '审批时间', + prop: 'processed_at', + formatter: (value) => (value ? formatDateTime(value) : '-') + }, { label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) } ] }, { title: '企微审批信息', fields: [ - { label: '审批来源', prop: 'approval.source' }, + { label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) }, + { + label: '审批来源', + formatter: (_, data) => data.approval?.source || data.approval_source || '-' + }, { label: '审批单号', prop: 'approval.sp_no' }, { label: '审批状态', - formatter: (_, data) => data.approval?.status_name || data.approval?.status || '-' + formatter: (_, data) => getRefundApprovalStatusText(data) + }, + { + label: '当前审批人摘要', + formatter: (_, data) => data.current_approver_summary || '-' }, { label: '模板版本', prop: 'approval.template_version' }, { diff --git a/src/views/finance/refund/index.vue b/src/views/finance/refund/index.vue index e378658..9c218fe 100644 --- a/src/views/finance/refund/index.vue +++ b/src/views/finance/refund/index.vue @@ -21,6 +21,9 @@ 创建退款申请 + + 导出 + @@ -48,6 +51,15 @@ + + + >() const createDialogVisible = ref(false) + const exportDialogVisible = ref(false) const resubmitDialogVisible = ref(false) const currentRefund = ref(null) const refundVoucherFileKeys = ref([]) @@ -269,6 +282,7 @@ { label: '实际退款金额', prop: 'approved_refund_amount' }, { label: '实收金额', prop: 'actual_received_amount' }, { label: '状态', prop: 'status' }, + { label: '审批渠道', prop: 'approval_provider' }, { label: '提交人', prop: 'submitter_name' }, { label: '审批状态', prop: 'approval_status' }, { label: '当前审批人摘要', prop: 'current_approver_summary' }, @@ -279,7 +293,8 @@ { label: '资产重置', prop: 'asset_reset' }, { label: '佣金扣除', prop: 'commission_deducted' }, { label: '创建时间', prop: 'created_at' }, - { label: '审批时间', prop: 'processed_at' } + { label: '审批时间', prop: 'processed_at' }, + { label: '更新时间', prop: 'updated_at' } ] const resubmitFormRef = ref() @@ -320,17 +335,6 @@ return statusMap[status] || 'info' } - // 获取状态文本 - const getStatusText = (status: RefundStatus): string => { - const statusMap: Record = { - 1: '待审批', - 2: '已通过', - 3: '已拒绝', - 4: '已退回' - } - return statusMap[status] || '-' - } - // 动态列配置 const { columnChecks, columns } = useCheckedColumns(() => [ { @@ -404,7 +408,17 @@ label: '状态', width: 100, formatter: (row: Refund) => { - return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status)) + return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-') + } + }, + { + prop: 'approval_provider', + label: '审批渠道', + width: 110, + formatter: (row: Refund) => { + if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微' + if (row.approval_source === 'legacy') return '历史审批' + return row.approval_provider || row.approval_source || '-' } }, { @@ -418,7 +432,7 @@ prop: 'approval_status', label: '审批状态', width: 120, - formatter: (row: Refund) => getApprovalStatusText(row) + formatter: (row: Refund) => row.approval_status_name || '-' }, { prop: 'current_approver_summary', @@ -483,6 +497,12 @@ label: '审批时间', width: 180, formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-') + }, + { + prop: 'updated_at', + label: '更新时间', + width: 180, + formatter: (row: Refund) => formatDateTime(row.updated_at) } ]) @@ -564,6 +584,13 @@ } } + const exportQuery = computed(() => ({ + status: searchForm.status, + order_id: searchForm.order_id, + shop_id: searchForm.shop_id, + asset_identifier: searchForm.asset_identifier?.trim() || undefined + })) + // 重置搜索 const handleReset = () => { Object.assign(searchForm, { ...initialSearchState }) diff --git a/src/views/notifications/index.vue b/src/views/notifications/index.vue deleted file mode 100644 index ddc164b..0000000 --- a/src/views/notifications/index.vue +++ /dev/null @@ -1,277 +0,0 @@ - - - - - diff --git a/src/views/order-management/bulk-purchase/index.vue b/src/views/order-management/bulk-purchase/index.vue deleted file mode 100644 index d30e9d5..0000000 --- a/src/views/order-management/bulk-purchase/index.vue +++ /dev/null @@ -1,446 +0,0 @@ - - - - - diff --git a/src/views/package-management/series-grants/detail.vue b/src/views/package-management/series-grants/detail.vue index 8ca51db..c57cb3c 100644 --- a/src/views/package-management/series-grants/detail.vue +++ b/src/views/package-management/series-grants/detail.vue @@ -274,7 +274,7 @@ }}
- + - + - +