diff --git a/openspec/changes/add-carrier-traffic-threshold-hit-query/design.md b/openspec/changes/add-carrier-traffic-threshold-hit-query/design.md new file mode 100644 index 0000000..56cf272 --- /dev/null +++ b/openspec/changes/add-carrier-traffic-threshold-hit-query/design.md @@ -0,0 +1,22 @@ +## Context + +现有前端没有运营商通道阈值命中记录页面或对应 API 类型。后端已提供只读分页查询接口,页面需忠实呈现不可变的命中时快照和周期恢复状态。 + +## Goals / Non-Goals + +- Goals: 提供受限于平台账号的筛选、分页和证据查询;不丢失空值的历史语义。 +- Non-Goals: 修改通道阈值、发起停复机、导出记录、查看卡片详情,或查询上游响应。 + +## Decisions + +- Decision: 在系统管理路径下新增独立隐藏查询页,由运营商管理页提供入口;路由使用 `allowedUserTypes: [1, 2]`,页面自身也在请求前检查平台账号。 +- Rationale: 接口只允许超级管理员和平台账号。前端隐藏/保护页面入口,后端仍是最终授权及卡数据范围过滤边界。 +- Decision: 精确 ID 筛选使用数值输入,计费周期起点使用日期时间选择器,判定时间使用日期时间范围选择器;时间序列化为带时区 RFC3339 秒级字符串。 +- Rationale: 与接口的精确匹配和闭区间语义一致,避免按日期截断或丢失时区。 +- Decision: 页面仅展示响应快照,不再查询通道配置或根据当前阈值重算历史值。 +- Rationale: 命中值冻结于判定时;历史缺失值必须保留接口规定的空值含义。 + +## Risks / Trade-offs + +- 查询页不依赖后端菜单节点;前端入口仅对平台账号显示,页面路由限制用户类型,API 仍执行最终授权和数据范围过滤。 +- 表格包含多列证据字段,采用现有可横向滚动的管理台表格以保持信息完整。 diff --git a/openspec/changes/add-carrier-traffic-threshold-hit-query/proposal.md b/openspec/changes/add-carrier-traffic-threshold-hit-query/proposal.md new file mode 100644 index 0000000..389b15a --- /dev/null +++ b/openspec/changes/add-carrier-traffic-threshold-hit-query/proposal.md @@ -0,0 +1,24 @@ +# Change: 新增运营商通道阈值命中记录查询 + +## Why + +达量停机与周期恢复需要可追溯的数字证据。运营人员需要查询每次停机判定时冻结的累计流量、实际阈值、计费周期、触发来源,以及周期解锁和复机结果;通道阈值后续变更不能影响历史记录。 + +## What Changes + +- 在运营商管理页提供“通道阈值命中记录”入口,打开独立只读查询页并接入 `GET /api/admin/carrier-traffic-threshold-hits`。 +- 支持按运营商通道 ID、IoT 卡 ID、计费周期起点精确匹配,并按判定时间闭区间筛选。 +- 展示命中时的流量和阈值快照、判定/触发信息、锁状态、解锁时间、复机结果及安全失败原因。 +- 仅超级管理员与平台账号可访问;数据范围过滤及越权资源不可区分由后端强制执行,前端不替代后端授权。 +- 对接口定义的空历史值使用明确的“无”或“未处理/历史记录”展示,不发起额外的通道阈值或上游接口查询。 + +## Impact + +- Affected specs: `carrier-traffic-threshold-hits` +- Affected code: + - `src/api/modules/carrierTrafficThresholdHit.ts` + - `src/types/api/carrierTrafficThresholdHit.ts` + - `src/views/system/carrier-traffic-threshold-hits/index.vue` + - `src/router/routesAlias.ts`, `src/router/routes/staticRoutes.ts`, `src/views/system/carrier-management/index.vue` + - `src/locales/langs/zh.json`, `src/locales/langs/en.json` +- Breaking changes: 无;仅新增只读查询接口消费与页面入口。 diff --git a/openspec/changes/add-carrier-traffic-threshold-hit-query/specs/carrier-traffic-threshold-hits/spec.md b/openspec/changes/add-carrier-traffic-threshold-hit-query/specs/carrier-traffic-threshold-hits/spec.md new file mode 100644 index 0000000..075147e --- /dev/null +++ b/openspec/changes/add-carrier-traffic-threshold-hit-query/specs/carrier-traffic-threshold-hits/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: Carrier Traffic Threshold Hit Record Query + +The admin frontend SHALL provide a read-only paginated view of carrier traffic threshold hit records using `GET /api/admin/carrier-traffic-threshold-hits`. + +#### Scenario: Restrict page access to platform accounts + +- **GIVEN** a user is not a super administrator or platform account +- **WHEN** the user navigates to the carrier traffic threshold hit page +- **THEN** the frontend MUST deny access and MUST NOT issue the hit-record query +- **AND** backend authorization and card data-scope filtering MUST remain authoritative + +#### Scenario: Filter hit records + +- **GIVEN** an authorized platform user opens the hit-record page +- **WHEN** the user searches by optional carrier ID, card ID, billing period start, or judged-at time range +- **THEN** the frontend MUST send `carrier_id`, `card_id`, `period_start`, `start_time`, and `end_time` only when provided +- **AND** period and judged-at timestamps MUST be timezone-qualified RFC3339 values with second precision +- **AND** `start_time` and `end_time` MUST represent the API's inclusive time interval + +#### Scenario: Display immutable hit evidence and recovery result + +- **GIVEN** the query returns threshold-hit records +- **WHEN** the frontend renders the table +- **THEN** it MUST display record/card/carrier IDs, hit traffic, frozen threshold value and unit, billing period start, judged time, trigger source, lock status, unlock time, resume result, failure reason, and creation time +- **AND** it MUST NOT recompute hit traffic or threshold from current channel configuration +- **AND** it MUST display nullable historical traffic/threshold values as “无”, blank resume results as “未处理或历史记录”, and missing unlock/failure details as “无” + +#### Scenario: Preserve server ordering and pagination + +- **GIVEN** the user queries hit records +- **WHEN** the frontend loads or changes the page +- **THEN** it MUST send `page` and `page_size` (never exceeding 100) +- **AND** it MUST render the server-provided page, size, total, and item order without client-side re-sorting diff --git a/openspec/changes/add-carrier-traffic-threshold-hit-query/tasks.md b/openspec/changes/add-carrier-traffic-threshold-hit-query/tasks.md new file mode 100644 index 0000000..e493b10 --- /dev/null +++ b/openspec/changes/add-carrier-traffic-threshold-hit-query/tasks.md @@ -0,0 +1,10 @@ +## 1. API Contract + +- [x] 1.1 增加命中记录查询参数、记录项及分页响应类型。 +- [x] 1.2 增加 `GET /api/admin/carrier-traffic-threshold-hits` 服务方法并统一导出。 + +## 2. Admin UI + +- [x] 2.1 新增超级管理员/平台账号可访问的隐藏路由、运营商管理页入口及中英文标题。 +- [x] 2.2 实现筛选、分页和命中/周期恢复快照表格,按接口约定展示空值。 +- [x] 2.3 运行 OpenSpec 校验、ESLint 与生产构建。 diff --git a/openspec/changes/update-order-package-invalidate-template-download/proposal.md b/openspec/changes/update-order-package-invalidate-template-download/proposal.md new file mode 100644 index 0000000..de9bb76 --- /dev/null +++ b/openspec/changes/update-order-package-invalidate-template-download/proposal.md @@ -0,0 +1,18 @@ +# Change: 新建订单套餐作废任务支持下载 CSV 模板 + +## Why + +运营创建订单套餐批量作废任务时,需要按系统要求准备订单号 CSV。当前创建抽屉只有文件上传入口,无法直接取得格式样例,容易因文件列格式不符而返工。 + +## What Changes + +- 在新建订单套餐作废任务抽屉的 CSV 上传项旁提供“下载模板”入口。 +- 下载仓库中已有的静态模板 `src/template/订单批量作废模板.csv`,保留其 `order_no` 表头,不增加模板生成接口。 +- 不改变现有 CSV 上传校验、任务创建、权限或后端接口。 + +## Impact + +- Affected specs: `order-package-invalidate-task` +- Affected code: `src/views/asset-management/task-management/order-package-invalidate-task/index.vue` +- Existing user-provided asset: `src/template/订单批量作废模板.csv`(只读取,不修改) +- Breaking changes: 无。 diff --git a/openspec/changes/update-order-package-invalidate-template-download/specs/order-package-invalidate-task/spec.md b/openspec/changes/update-order-package-invalidate-template-download/specs/order-package-invalidate-task/spec.md new file mode 100644 index 0000000..2d1e681 --- /dev/null +++ b/openspec/changes/update-order-package-invalidate-template-download/specs/order-package-invalidate-task/spec.md @@ -0,0 +1,13 @@ +## ADDED Requirements + +### Requirement: Order Package Invalidation CSV Template Download + +The admin frontend SHALL let users creating an order package invalidation task download the CSV template used by the task's CSV upload field. + +#### Scenario: Download the static CSV template + +- **GIVEN** 用户已打开新建订单套餐作废任务抽屉 +- **WHEN** 用户点击 CSV 上传项旁的模板下载入口 +- **THEN** 页面 MUST 下载静态文件 `订单批量作废模板.csv` +- **AND** 下载内容 MUST match `src/template/订单批量作废模板.csv`, including the `order_no` header +- **AND** 页面 MUST NOT make a template-generation API request diff --git a/openspec/changes/update-order-package-invalidate-template-download/tasks.md b/openspec/changes/update-order-package-invalidate-template-download/tasks.md new file mode 100644 index 0000000..4e2af1a --- /dev/null +++ b/openspec/changes/update-order-package-invalidate-template-download/tasks.md @@ -0,0 +1,4 @@ +## 1. Implementation + +- [x] 1.1 在订单套餐作废任务创建抽屉中添加静态 CSV 模板下载入口。 +- [x] 1.2 验证下载内容、文件名与现有模板一致,并运行相关前端检查。 diff --git a/src/api/modules/carrierTrafficThresholdHit.ts b/src/api/modules/carrierTrafficThresholdHit.ts new file mode 100644 index 0000000..90c884a --- /dev/null +++ b/src/api/modules/carrierTrafficThresholdHit.ts @@ -0,0 +1,24 @@ +/** 运营商通道阈值命中记录查询 API */ + +import { BaseService } from '../BaseService' +import type { BaseResponse } from '@/types/api' +import type { + CarrierTrafficThresholdHitPageResult, + CarrierTrafficThresholdHitQueryParams +} from '@/types/api/carrierTrafficThresholdHit' + +export class CarrierTrafficThresholdHitService extends BaseService { + /** + * 查询通道阈值命中记录 + * GET /api/admin/carrier-traffic-threshold-hits + * 仅超级管理员和平台账号可访问,后端负责卡数据范围过滤。 + */ + static getHits( + params?: CarrierTrafficThresholdHitQueryParams + ): Promise> { + return this.get>( + '/api/admin/carrier-traffic-threshold-hits', + params + ) + } +} diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index 27a1afa..54b5778 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -16,6 +16,7 @@ export { StorageService } from './storage' export { AuthorizationService } from './authorization' export { DeviceService } from './device' export { CarrierService } from './carrier' +export { CarrierTrafficThresholdHitService } from './carrierTrafficThresholdHit' export { PackageSeriesService } from './packageSeries' export { PackageManageService } from './packageManage' export { ShopSeriesGrantService } from './shopSeriesGrant' diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json index ae7972b..8d58f5f 100644 --- a/src/locales/langs/en.json +++ b/src/locales/langs/en.json @@ -356,6 +356,8 @@ "role": "Role Manage", "permission": "Permission Manage", "carrierManagement": "Carrier Management", + "carrierDetail": "Carrier Details", + "carrierTrafficThresholdHits": "Carrier Threshold Hit Records", "userCenter": "User Center", "menu": "Menu Manage", "nested": "Nested Menu", diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json index e0cedb4..4ceb873 100644 --- a/src/locales/langs/zh.json +++ b/src/locales/langs/zh.json @@ -355,6 +355,8 @@ "role": "角色管理", "permission": "权限管理", "carrierManagement": "运营商管理", + "carrierDetail": "运营商详情", + "carrierTrafficThresholdHits": "通道阈值命中记录", "userCenter": "个人中心" }, "packageManagement": { diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts index b91614b..287c210 100644 --- a/src/router/guards/beforeEach.ts +++ b/src/router/guards/beforeEach.ts @@ -238,7 +238,7 @@ async function processFrontendMenu(router: Router): Promise { throw new Error('获取用户角色失败') } - const filteredMenuList = filterMenuByRoles(menuList, roles) + const filteredMenuList = filterMenuByRoles(menuList, roles, Number(userStore.info.user_type)) await new Promise((resolve) => setTimeout(resolve, 300)) await registerAndStoreMenu(router, filteredMenuList, closeLoading) } catch (error) { @@ -264,7 +264,11 @@ async function processBackendMenu(router: Router): Promise { } // 普通用户:使用后端返回的菜单 - const backendMenus = userStore.menus || [] + let backendMenus = userStore.menus || [] + if (Number(userStore.info.user_type) === 2) { + backendMenus = ensureCarrierManagementMenuEntries(backendMenus) + userStore.setMenus(backendMenus) + } const routeMap = buildRouteMap(asyncRoutes) const menuList = backendMenus @@ -279,6 +283,46 @@ async function processBackendMenu(router: Router): Promise { } } +/** + * Backend menu mode may return the existing System menu without the newly added + * carrier detail and threshold-hit entries. Add those as sibling routes under System + * for platform accounts, without introducing a nested RouterView layout. + */ +function ensureCarrierManagementMenuEntries(menus: any[]): any[] { + const requiredEntries = [ + { + url: `${RoutesAlias.CarrierDetail}/:id`, + name: 'menus.system.carrierDetail' + }, + { + url: RoutesAlias.CarrierTrafficThresholdHits, + name: 'menus.system.carrierTrafficThresholdHits' + } + ] + let systemMenuFound = false + + const visit = (items: any[]): any[] => + items.map((item) => { + const children = Array.isArray(item.children) ? visit(item.children) : [] + const itemUrl = item.url || item.path + + if (itemUrl === '/system') { + systemMenuFound = true + const existingUrls = new Set(children.map((child) => child.url || child.path)) + const missingEntries = requiredEntries.filter((entry) => !existingUrls.has(entry.url)) + return { ...item, children: [...children, ...missingEntries] } + } + + return Array.isArray(item.children) ? { ...item, children } : item + }) + + const enrichedMenus = visit(menus) + if (systemMenuFound) return enrichedMenus + + const systemMenu = { url: '/system', name: 'menus.system.title', children: requiredEntries } + return [...enrichedMenus, systemMenu] +} + /** * 合并默认菜单 * 将配置的默认菜单合并到后端返回的菜单中 @@ -448,15 +492,21 @@ function handleMenuError(error: unknown): void { /** * 根据角色过滤菜单 */ -const filterMenuByRoles = (menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] => { +const filterMenuByRoles = ( + menu: AppRouteRecord[], + roles: string[], + userType: number +): AppRouteRecord[] => { return menu.reduce((acc: AppRouteRecord[], item) => { const itemRoles = item.meta?.roles - const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) + const allowedUserTypes = item.meta?.allowedUserTypes as number[] | undefined + const hasRole = !itemRoles || itemRoles.some((role) => roles?.includes(role)) + const hasAllowedUserType = !allowedUserTypes || allowedUserTypes.includes(userType) - if (hasPermission) { + if (hasRole && hasAllowedUserType) { const filteredItem = { ...item } if (filteredItem.children?.length) { - filteredItem.children = filterMenuByRoles(filteredItem.children, roles) + filteredItem.children = filterMenuByRoles(filteredItem.children, roles, userType) } acc.push(filteredItem) } diff --git a/src/router/guards/permission.ts b/src/router/guards/permission.ts index db9f009..34adc3a 100644 --- a/src/router/guards/permission.ts +++ b/src/router/guards/permission.ts @@ -205,4 +205,3 @@ export const buildLoginRedirect = (currentPath: string): string => { } return `/auth/login?redirect=${encodeURIComponent(currentPath)}` } - diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 5fb1427..af6d78c 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -118,6 +118,26 @@ export const asyncRoutes: AppRouteRecord[] = [ keepAlive: true } }, + { + path: 'carrier-management/detail/:id', + name: 'CarrierDetail', + component: RoutesAlias.CarrierDetail, + meta: { + title: 'menus.system.carrierDetail', + isHide: true, + keepAlive: false + } + }, + { + path: 'carrier-traffic-threshold-hits', + name: 'CarrierTrafficThresholdHits', + component: RoutesAlias.CarrierTrafficThresholdHitsPage, + meta: { + title: 'menus.system.carrierTrafficThresholdHits', + keepAlive: true, + allowedUserTypes: [1, 2] + } + }, { path: 'user-center', name: 'UserCenter', diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index a5ef827..44f4f51 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -21,6 +21,9 @@ export enum RoutesAlias { RoleDetail = '/system/role/detail', // 角色详情 Permission = '/system/permission', // 权限管理 CarrierManagement = '/system/carrier-management', // 运营商管理 + CarrierDetail = '/system/carrier-management/detail', // 运营商详情 + CarrierTrafficThresholdHits = '/system/carrier-traffic-threshold-hits', // 通道阈值命中记录 + CarrierTrafficThresholdHitsPage = '/system/carrier-management/traffic-threshold-hits', // 通道阈值命中记录组件 UserCenter = '/system/user-center', // 用户中心 // 审计中心 diff --git a/src/template/批量换绑业务员模板.csv b/src/template/批量换绑业务员模板.csv index 2cc2100..8695ee6 100644 --- a/src/template/批量换绑业务员模板.csv +++ b/src/template/批量换绑业务员模板.csv @@ -1 +1,2 @@ -店铺编码,操作类型,业务员登录账号,备注 +µêÆ̱àÂë,²Ù×÷ÀàÐÍ,ҵÎñԱµÇ¼Õ˺Å,±¸ע +,½ö֧³֡¸»»°󡹻ò¡¸Çå¿ա¹,עÒâ: Çå¿ղ»ҪÌîдҵÎñԱµÇ¼Õ˺Å, diff --git a/src/template/订单批量作废模板.csv b/src/template/订单批量作废模板.csv new file mode 100644 index 0000000..b9beefa --- /dev/null +++ b/src/template/订单批量作废模板.csv @@ -0,0 +1 @@ +order_no diff --git a/src/types/api/carrier.ts b/src/types/api/carrier.ts index 20e7ca4..7cb208f 100644 --- a/src/types/api/carrier.ts +++ b/src/types/api/carrier.ts @@ -12,16 +12,22 @@ export enum CarrierType { CBN = 'CBN' // 中国广电 } +export type CarrierTrafficThresholdEnabled = 0 | 1 +export type CarrierTrafficThresholdUnit = 'MB' | 'GB' + // 运营商响应数据 export interface Carrier { id: number carrier_code: string carrier_name: string carrier_type: CarrierType - description?: string + description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) - realname_link_type?: string // 实名链接类型 - realname_link_template?: string // 实名链接模板 + realname_link_type?: string | null // 实名链接类型 + realname_link_template?: string | null // 实名链接模板 + traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 + traffic_threshold_unit?: CarrierTrafficThresholdUnit | null // 通道流量阈值单位 + traffic_threshold_value?: number | null // 通道流量阈值数值 status: number created_at: string updated_at: string @@ -39,19 +45,25 @@ export interface CreateCarrierParams { carrier_code: string carrier_name: string carrier_type: CarrierType - description?: string + description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) - realname_link_type?: string // 实名链接类型 - realname_link_template?: string // 实名链接模板 + realname_link_type?: string | null // 实名链接类型 + realname_link_template?: string | null // 实名链接模板 + traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 + traffic_threshold_unit?: CarrierTrafficThresholdUnit | null // 通道流量阈值单位 + traffic_threshold_value?: number | null // 通道流量阈值数值 } // 更新运营商请求参数 export interface UpdateCarrierParams { carrier_name?: string - description?: string + description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) - realname_link_type?: string // 实名链接类型 - realname_link_template?: string // 实名链接模板 + realname_link_type?: string | null // 实名链接类型 + realname_link_template?: string | null // 实名链接模板 + traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 + traffic_threshold_unit?: CarrierTrafficThresholdUnit | null // 通道流量阈值单位 + traffic_threshold_value?: number | null // 通道流量阈值数值 } // 更新运营商状态参数 diff --git a/src/types/api/carrierTrafficThresholdHit.ts b/src/types/api/carrierTrafficThresholdHit.ts new file mode 100644 index 0000000..4b45e8a --- /dev/null +++ b/src/types/api/carrierTrafficThresholdHit.ts @@ -0,0 +1,35 @@ +/** 运营商通道阈值命中记录 */ + +export interface CarrierTrafficThresholdHitQueryParams { + page?: number + page_size?: number + carrier_id?: number + card_id?: number + period_start?: string + start_time?: string + end_time?: string +} + +export interface CarrierTrafficThresholdHitItem { + id: number + card_id: number + carrier_id: number + hit_traffic_mb: number | null + hit_threshold_value: number | null + hit_threshold_unit: string + period_start: string + judged_at: string + trigger_source: string + status: string + unlocked_at: string | null + resume_result: string + failure_reason: string + created_at: string +} + +export interface CarrierTrafficThresholdHitPageResult { + items: CarrierTrafficThresholdHitItem[] | null + page: number + size: number + total: number +} diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 50263ac..11cdac5 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -66,6 +66,7 @@ export * from './enterpriseDevice' // 运营商相关 export * from './carrier' +export * from './carrierTrafficThresholdHit' // 订单相关 export * from './order' diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 16b047b..ad2f7d2 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -133,6 +133,7 @@ declare module 'vue' { ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] diff --git a/src/views/asset-management/task-management/order-package-invalidate-task/index.vue b/src/views/asset-management/task-management/order-package-invalidate-task/index.vue index 55be8ce..5027770 100644 --- a/src/views/asset-management/task-management/order-package-invalidate-task/index.vue +++ b/src/views/asset-management/task-management/order-package-invalidate-task/index.vue @@ -56,20 +56,27 @@ > - +
+
+ + 下载模板 + +
+ +
{ + try { + const link = document.createElement('a') + link.href = new URL('@/template/订单批量作废模板.csv', import.meta.url).href + link.download = '订单批量作废模板.csv' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + ElMessage.success('模板下载成功') + } catch (error) { + console.error('下载作废任务模板失败:', error) + ElMessage.error('模板下载失败') + } + } + const handleCsvFilesChange = (files: Array<{ file_name?: string }>) => { csvFileName.value = files[0]?.file_name || '' createForm.file = csvFileName.value @@ -449,6 +471,16 @@ diff --git a/src/views/system/carrier-management/index.vue b/src/views/system/carrier-management/index.vue index 639dc86..bbd4cf6 100644 --- a/src/views/system/carrier-management/index.vue +++ b/src/views/system/carrier-management/index.vue @@ -45,9 +45,15 @@ - + - + + + + + + + + + import { h, watch, nextTick } from 'vue' + import { useRouter } from 'vue-router' import { CarrierService } from '@/api/modules' import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' - import type { Carrier, CarrierType } from '@/types/api' + import type { Carrier, CarrierType, CreateCarrierParams } from '@/types/api' import type { SearchFormItem } from '@/types' import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useAuth } from '@/composables/useAuth' + import { usePermission } from '@/composables/usePermission' + import { RoutesAlias } from '@/router/routesAlias' import { generateCarrierCode } from '@/utils/codeGenerator' import { formatDateTime } from '@/utils/business/format' import { @@ -164,7 +210,10 @@ defineOptions({ name: 'CarrierManagement' }) + const router = useRouter() const { hasAuth } = useAuth() + const { isPlatformAccount } = usePermission() + const canConfigureTrafficThreshold = computed(() => isPlatformAccount.value) const canUpdateCarrierStatus = hasAuth('carrier:update_status') const dialogVisible = ref(false) const loading = ref(false) @@ -235,6 +284,13 @@ { label: '运营商名称', prop: 'carrier_name' }, { label: '运营商类型', prop: 'carrier_type' }, { label: '运营商描述', prop: 'description' }, + ...(canConfigureTrafficThreshold.value + ? [ + { label: '通道阈值状态', prop: 'traffic_threshold_enabled' }, + { label: '通道阈值单位', prop: 'traffic_threshold_unit' }, + { label: '通道阈值数值', prop: 'traffic_threshold_value' } + ] + : []), ...(canUpdateCarrierStatus ? [{ label: '状态', prop: 'status' }] : []), { label: '创建时间', prop: 'created_at' } ] @@ -296,6 +352,9 @@ carrier_type: null, description: '', data_reset_day: 1, + traffic_threshold_mode: 'unconfigured', + traffic_threshold_unit: null, + traffic_threshold_value: null, realname_link_type: 'none', realname_link_template: '' }) @@ -314,7 +373,17 @@ prop: 'carrier_name', label: '运营商名称', minWidth: 150, - showOverflowTooltip: true + showOverflowTooltip: true, + formatter: (row: Carrier) => + h( + ElButton, + { + type: 'primary', + link: true, + onClick: () => void router.push(`${RoutesAlias.CarrierDetail}/${row.id}`) + }, + () => row.carrier_name || '-' + ) }, { prop: 'carrier_type', @@ -335,6 +404,28 @@ minWidth: 200, showOverflowTooltip: true }, + ...(canConfigureTrafficThreshold.value + ? [ + { + prop: 'traffic_threshold_enabled', + label: '通道阈值状态', + width: 130, + formatter: (row: Carrier) => getThresholdEnabledLabel(row.traffic_threshold_enabled) + }, + { + prop: 'traffic_threshold_unit', + label: '通道阈值单位', + width: 120, + formatter: (row: Carrier) => row.traffic_threshold_unit || '未配置' + }, + { + prop: 'traffic_threshold_value', + label: '通道阈值数值', + width: 140, + formatter: (row: Carrier) => formatThresholdValue(row.traffic_threshold_value) + } + ] + : []), ...(canUpdateCarrierStatus ? [ { @@ -482,6 +573,9 @@ form.carrier_type = row.carrier_type form.description = row.description form.data_reset_day = row.data_reset_day || 1 + form.traffic_threshold_mode = getThresholdMode(row.traffic_threshold_enabled) + form.traffic_threshold_unit = row.traffic_threshold_unit ?? null + form.traffic_threshold_value = row.traffic_threshold_value ?? null form.realname_link_type = row.realname_link_type || 'none' form.realname_link_template = row.realname_link_template || '' } else { @@ -491,6 +585,9 @@ form.carrier_type = null form.description = '' form.data_reset_day = 1 + form.traffic_threshold_mode = 'unconfigured' + form.traffic_threshold_unit = null + form.traffic_threshold_value = null form.realname_link_type = 'none' form.realname_link_template = '' } @@ -501,6 +598,49 @@ }) } + const getThresholdEnabledLabel = (enabled?: 0 | 1 | null) => { + if (enabled === 1) return '启用' + if (enabled === 0) return '停用' + return '未配置' + } + + const getThresholdMode = (enabled?: 0 | 1 | null) => { + if (enabled === 1) return 'enabled' + if (enabled === 0) return 'disabled' + return 'unconfigured' + } + + const handleTrafficThresholdModeChange = (mode: string) => { + if (mode === 'unconfigured') { + form.traffic_threshold_unit = null + form.traffic_threshold_value = null + } + } + + const formatThresholdValue = (value?: number | null) => + value === null || value === undefined ? '未配置' : String(value) + + const getTrafficThresholdPayload = () => { + if (!canConfigureTrafficThreshold.value) return {} + if (form.traffic_threshold_mode === 'unconfigured') { + return { + traffic_threshold_enabled: null, + traffic_threshold_unit: null, + traffic_threshold_value: null + } + } + + if (form.traffic_threshold_mode === 'disabled') { + return { traffic_threshold_enabled: 0 as const } + } + + return { + traffic_threshold_enabled: 1 as const, + traffic_threshold_unit: form.traffic_threshold_unit, + traffic_threshold_value: Number(form.traffic_threshold_value) + } + } + // 处理编码生成 const handleGenerateCarrierCode = () => { form.carrier_code = generateCarrierCode() @@ -536,11 +676,22 @@ const handleSubmit = async (formEl: FormInstance | undefined) => { if (!formEl) return + if ( + canConfigureTrafficThreshold.value && + form.traffic_threshold_mode === 'enabled' && + (!form.traffic_threshold_unit || + !Number.isFinite(Number(form.traffic_threshold_value)) || + Number(form.traffic_threshold_value) <= 0) + ) { + ElMessage.warning('启用通道阈值时,请填写阈值单位和大于0的阈值数值') + return + } + await formEl.validate(async (valid) => { if (valid) { submitLoading.value = true try { - const data = { + const data: CreateCarrierParams = { carrier_code: form.carrier_code, carrier_name: form.carrier_name, carrier_type: form.carrier_type as CarrierType, @@ -548,7 +699,8 @@ data_reset_day: form.data_reset_day || null, realname_link_type: form.realname_link_type, realname_link_template: - form.realname_link_type === 'template' ? form.realname_link_template : undefined + form.realname_link_type === 'template' ? form.realname_link_template : undefined, + ...getTrafficThresholdPayload() } if (dialogType.value === 'add') { @@ -561,7 +713,8 @@ data_reset_day: form.data_reset_day || null, realname_link_type: form.realname_link_type, realname_link_template: - form.realname_link_type === 'template' ? form.realname_link_template : undefined + form.realname_link_type === 'template' ? form.realname_link_template : undefined, + ...getTrafficThresholdPayload() } await CarrierService.updateCarrier(form.id, updateData) ElMessage.success('修改成功') @@ -599,4 +752,28 @@ .carrier-page { height: 100%; } + + .carrier-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 24px; + + :deep(.el-form-item) { + margin-right: 0; + } + } + + .carrier-form-span { + grid-column: 1 / -1; + } + + @media (max-width: 640px) { + .carrier-form-grid { + grid-template-columns: minmax(0, 1fr); + } + + .carrier-form-span { + grid-column: auto; + } + } diff --git a/src/views/system/carrier-management/traffic-threshold-hits/index.vue b/src/views/system/carrier-management/traffic-threshold-hits/index.vue new file mode 100644 index 0000000..0427935 --- /dev/null +++ b/src/views/system/carrier-management/traffic-threshold-hits/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/views/system/carrier-traffic-threshold-hits/index.vue b/src/views/system/carrier-traffic-threshold-hits/index.vue new file mode 100644 index 0000000..453babe --- /dev/null +++ b/src/views/system/carrier-traffic-threshold-hits/index.vue @@ -0,0 +1,421 @@ + + + + +