feat: 套餐分层级

This commit is contained in:
luo
2026-09-08 16:39:29 +08:00
parent 65cc63674e
commit ef966171b4
4 changed files with 85 additions and 24 deletions

View File

@@ -104,7 +104,8 @@ export class AssetService extends BaseService {
} }
/** /**
* 查询该资产所有套餐记录,含虚流量换算字段(分页) * 查询该资产的历史套餐关系组(主套餐—加油包,分页)
* total 仅统计顶层关系组,子项由后端随父项一起返回。
* GET /api/admin/assets/:identifier/packages?page=1&page_size=50 * GET /api/admin/assets/:identifier/packages?page=1&page_size=50
* @param identifier 资产标识符ICCID 或 VirtualNo * @param identifier 资产标识符ICCID 或 VirtualNo
* @param params 查询参数(可选分页参数) * @param params 查询参数(可选分页参数)
@@ -115,12 +116,7 @@ export class AssetService extends BaseService {
): Promise<BaseResponse<AssetPackageListResponse>> { ): Promise<BaseResponse<AssetPackageListResponse>> {
return this.get<BaseResponse<AssetPackageListResponse>>( return this.get<BaseResponse<AssetPackageListResponse>>(
`/api/admin/assets/${identifier}/packages`, `/api/admin/assets/${identifier}/packages`,
params, params
{
requestOptions: {
show404Error: false // 404时不显示错误提示
}
}
) )
} }

View File

@@ -39,6 +39,9 @@ export enum PackageUsageStatus {
// 套餐类型 // 套餐类型
export type PackageType = 'formal' | 'addon' export type PackageType = 'formal' | 'addon'
// 套餐关系状态(历史套餐列表)
export type AssetPackageRelationshipStatus = 'master_missing'
// 套餐使用类型 // 套餐使用类型
export type UsageType = 'single_card' | 'device' export type UsageType = 'single_card' | 'device'
@@ -332,7 +335,7 @@ export type AssetRefreshResponse = AssetRealtimeStatusResponse
/** /**
* 资产套餐使用记录 * 资产套餐使用记录
* 对应接口GET /api/admin/assets/:asset_type/:id/packages * 对应接口GET /api/admin/assets/:identifier/packages
*/ */
export interface AssetPackageUsageRecord { export interface AssetPackageUsageRecord {
id?: number // 兼容前端当前映射字段 id?: number // 兼容前端当前映射字段
@@ -353,31 +356,34 @@ export interface AssetPackageUsageRecord {
virtual_ratio?: number // 虚流量比例real/virtual virtual_ratio?: number // 虚流量比例real/virtual
reduction_pct?: number // 展示增幅比例(小数,如 0.428571 reduction_pct?: number // 展示增幅比例(小数,如 0.428571
enable_virtual_data?: boolean // 是否启用虚流量 enable_virtual_data?: boolean // 是否启用虚流量
paid_amount?: number // 实际支付金额(分) paid_amount?: number | null // 实际支付金额(分,仅平台账号可见
retail_amount?: number // 零售价(分) retail_amount?: number | null // 零售价(分)
package_price?: number // 套餐价格(分) package_price?: number // 套餐价格(分)
start_time?: string // 开始时间(兼容前端现有映射字段) start_time?: string // 开始时间(兼容前端现有映射字段)
expire_time?: string // 到期时间(兼容前端现有映射字段) expire_time?: string // 到期时间(兼容前端现有映射字段)
duration_days?: number // 套餐时长(兼容前端现有映射字段) duration_days?: number // 套餐时长(兼容前端现有映射字段)
activated_at?: string // 激活时间 activated_at?: string // 激活时间
expires_at?: string // 到期时间 expires_at?: string // 到期时间
master_usage_id?: number | null // 主套餐 ID加油包时有值 master_usage_id?: number | null // 主套餐使用记录 ID加油包时有值
relationship_status?: AssetPackageRelationshipStatus // 关系状态master_missing 表示主套餐缺失
relationship_status_name?: string // 关系异常状态名称
expand_by_default?: boolean // 是否默认展开该主套餐下的加油包
children?: AssetPackageUsageRecord[] // 归属于该主套餐的加油包,不单独分页
priority?: number // 优先级 priority?: number // 优先级
created_at?: string // 创建时间 created_at?: string // 创建时间
order_id?: number // 订单ID order_id?: number // 订单ID
refund_id?: number // 退款ID有退款时才有值 refund_id?: number // 退款ID有退款时才有值
refund_no?: string // 退款单号快照
expiry_base?: ExpiryBase | null // 到期计时基准
} }
/** /**
* 资产套餐列表分页响应 * 资产套餐列表分页响应
* 对应接口GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50 * 每个 items 元素为一个顶层主套餐关系组total 只统计顶层组。
* 对应接口GET /api/admin/assets/:identifier/packages?page=1&page_size=50
*/ */
export interface AssetPackageUsageRecord {
expiry_base?: ExpiryBase | null // 到期计时基准
}
export interface AssetPackageListResponse { export interface AssetPackageListResponse {
total: number // 总记录 total: number // 顶层主套餐关系组总数
page: number // 当前页码 page: number // 当前页码
page_size: number // 每页数量 page_size: number // 每页数量
items: AssetPackageUsageRecord[] // 套餐记录列表 items: AssetPackageUsageRecord[] // 套餐记录列表
@@ -389,6 +395,7 @@ export interface AssetPackageListResponse {
export interface AssetPackageParams { export interface AssetPackageParams {
page?: number // 页码默认1 page?: number // 页码默认1
page_size?: number // 每页数量默认50 page_size?: number // 每页数量默认50
status?: PackageUsageStatus | null // 按同一条使用记录筛选
} }
export interface UpdateAssetPackageUsedDataRequest { export interface UpdateAssetPackageUsedDataRequest {

View File

@@ -29,8 +29,19 @@
</template> </template>
</ElEmpty> </ElEmpty>
<div v-else-if="packageList && packageList.length > 0" class="table-scroll-container"> <div v-else-if="packageList && packageList.length > 0" class="table-scroll-container">
<ElTable :data="packageList" class="package-table" border> <ElTable
<ElTableColumn label="订单号" width="240"> :data="packageList"
:expand-row-keys="defaultExpandedPackageUsageIds"
:tree-props="{ children: 'children' }"
row-key="package_usage_id"
class="package-table"
border
>
<ElTableColumn
label="订单号"
:width="orderNoColumnWidth"
class-name="package-order-column"
>
<template #default="scope"> <template #default="scope">
<div class="order-no-cell"> <div class="order-no-cell">
<ElTooltip :content="scope.row.order_no" placement="top" :show-after="300"> <ElTooltip :content="scope.row.order_no" placement="top" :show-after="300">
@@ -50,7 +61,16 @@
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="套餐名称" width="200" show-overflow-tooltip> <ElTableColumn label="套餐名称" width="200" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<span class="package-name-text">{{ scope.row.package_name }}</span> <div class="package-name-cell">
<span class="package-name-text">{{ scope.row.package_name }}</span>
<ElTag
v-if="scope.row.relationship_status === 'master_missing'"
type="warning"
size="small"
>
{{ scope.row.relationship_status_name || '主套餐缺失' }}
</ElTag>
</div>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn v-if="isAdminOrPlatform" label="套餐成本价格" width="120"> <ElTableColumn v-if="isAdminOrPlatform" label="套餐成本价格" width="120">
@@ -659,6 +679,25 @@
total: props.total total: props.total
}) })
const defaultExpandedPackageUsageIds = computed(() =>
props.packageList
.filter((pkg) => pkg.expand_by_default && pkg.children?.length)
.map((pkg) => pkg.package_usage_id ?? pkg.id)
.filter((id): id is number => typeof id === 'number')
.map(String)
)
const getLongestOrderNoLength = (packages: PackageInfo[]): number =>
packages.reduce((longestLength, pkg) => {
const childLongestLength = pkg.children?.length ? getLongestOrderNoLength(pkg.children) : 0
return Math.max(longestLength, pkg.order_no?.length || 0, childLongestLength)
}, 0)
// 为树形展开按钮、层级缩进和单元格留出空间,避免订单号过早省略。
const orderNoColumnWidth = computed(() =>
Math.min(Math.max(240, getLongestOrderNoLength(props.packageList) * 8 + 80), 500)
)
// 监听props变化更新分页 // 监听props变化更新分页
watch( watch(
() => [props.page, props.pageSize, props.total], () => [props.page, props.pageSize, props.total],
@@ -757,6 +796,12 @@
} }
} }
:deep(.package-order-column .cell) {
display: flex;
align-items: center;
min-width: 0;
}
.package-name-text { .package-name-text {
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -765,8 +810,20 @@
white-space: nowrap; white-space: nowrap;
} }
.package-name-cell {
display: flex;
gap: 8px;
align-items: center;
min-width: 0;
.package-name-text {
flex: 1;
min-width: 0;
}
}
.order-no-cell { .order-no-cell {
width: 100%; flex: 1;
min-width: 0; min-width: 0;
:deep(.el-tooltip__trigger) { :deep(.el-tooltip__trigger) {

View File

@@ -220,11 +220,12 @@ export function useAssetInfo() {
page_size: pageSize page_size: pageSize
}) })
if (response.code === 0 && response.data) { if (response.code === 0 && response.data) {
// 使用分页响应中的 items 数组 // items 代表顶层主套餐关系组,必须保留其 children不得展平。
packageList.value = response.data.items || [] const items = response.data.items || []
packageList.value = items
// 返回分页信息供外部使用 // 返回分页信息供外部使用
return { return {
items: response.data.items || [], items,
total: response.data.total || 0, total: response.data.total || 0,
page: response.data.page || 1, page: response.data.page || 1,
pageSize: response.data.page_size || pageSize pageSize: response.data.page_size || pageSize