This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# Change: Add purchased package names to the order list
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Order list records already include their purchased line items, but operators cannot identify the purchased packages without opening the order detail. Showing these names next to the order number makes multi-package orders immediately understandable.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Place the existing `资产标识符` column immediately after `订单编号`, followed by a `购买套餐名称` column in the order list.
|
||||||
|
- Render `items[].package_name` in the API-returned order, joining multiple item names with the Chinese enumeration separator `、`.
|
||||||
|
- Show a placeholder when an order has no returned items or no usable package name.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `order-list-purchased-package-names`
|
||||||
|
- Affected code: `src/views/order-management/order-list/index.vue`
|
||||||
|
- Dependencies: `GET /api/admin/orders` returns `items` with each item's `package_name`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Order list purchased package name display
|
||||||
|
|
||||||
|
The order list SHALL display `资产标识符` immediately after `订单编号`, followed by a `购买套餐名称` column. The package-name column MUST render package names from the current row's `items` in their API-returned order and MUST join multiple names with `、`.
|
||||||
|
|
||||||
|
#### Scenario: Order contains multiple purchased packages
|
||||||
|
|
||||||
|
- **GIVEN** an order response whose `items` contains package names `正式套餐` and `加油包33`
|
||||||
|
- **WHEN** the order is rendered in the order list
|
||||||
|
- **THEN** `资产标识符` MUST be positioned between `订单编号` and `购买套餐名称`
|
||||||
|
- **AND** the `购买套餐名称` column MUST display `正式套餐、加油包33`
|
||||||
|
- **AND** the order-number column MUST retain its existing navigation behavior
|
||||||
|
|
||||||
|
#### Scenario: Order has no available package names
|
||||||
|
|
||||||
|
- **GIVEN** an order response with `items=null`, an empty item list, or item records without usable names
|
||||||
|
- **WHEN** the order is rendered in the order list
|
||||||
|
- **THEN** the `购买套餐名称` column MUST display the standard placeholder
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## 1. Implementation
|
||||||
|
|
||||||
|
- [x] 1.1 Place `资产标识符` after order number and add the purchased-package-name column immediately after it in the order-list column chooser.
|
||||||
|
- [x] 1.2 Render package names from `Order.items` in API-returned order, separating multiple values with `、`.
|
||||||
|
- [x] 1.3 Render a placeholder for null, empty, or unnamed order items without altering the order-number link behavior.
|
||||||
|
|
||||||
|
## 2. Verification
|
||||||
|
|
||||||
|
- [x] 2.1 Verify a single item renders its package name and multiple items render `套餐A、套餐B`.
|
||||||
|
- [x] 2.2 Run targeted format, type, and lint checks.
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
title="手动更新卡实名状态"
|
title="手动更新卡实名状态"
|
||||||
width="30%"
|
width="30%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
@closed="handleDialogClosed"
|
@closed="handleDialogClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="设置实名认证策略"
|
title="设置实名认证策略"
|
||||||
width="40%"
|
width="40%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
<ElFormItem label="资产标识">
|
<ElFormItem label="资产标识">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="切换SIM卡"
|
title="切换SIM卡"
|
||||||
width="40%"
|
width="40%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<div v-if="loading" style="padding: 40px; text-align: center">
|
<div v-if="loading" style="padding: 40px; text-align: center">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="套餐流量详单"
|
title="套餐流量详单"
|
||||||
width="900px"
|
width="900px"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="往期订单"
|
title="往期订单"
|
||||||
width="60%"
|
width="60%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div v-if="loading" style="padding: 40px 0; text-align: center">
|
<div v-if="loading" style="padding: 40px 0; text-align: center">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="套餐充值"
|
title="套餐充值"
|
||||||
width="40%"
|
width="40%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
@closed="handleDialogClosed"
|
@closed="handleDialogClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
width="500px"
|
width="500px"
|
||||||
:lock-scroll="true"
|
:lock-scroll="true"
|
||||||
:z-index="4000"
|
:z-index="4000"
|
||||||
modal-class="asset-information-dialog speed-limit-modal"
|
modal-class="speed-limit-modal"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<ElFormItem label="固定档位" prop="code">
|
<ElFormItem label="固定档位" prop="code">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="设置切卡模式"
|
title="设置切卡模式"
|
||||||
width="45%"
|
width="45%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
<ElFormItem label="设备标识">
|
<ElFormItem label="设备标识">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="设置WiFi"
|
title="设置WiFi"
|
||||||
width="40%"
|
width="40%"
|
||||||
modal-class="asset-information-dialog"
|
|
||||||
:append-to-body="true"
|
:append-to-body="true"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" autocomplete="off">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" autocomplete="off">
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
|||||||
'last_card_status_check_at'
|
'last_card_status_check_at'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 详情接口是绑定卡实名时间的唯一来源;实时状态接口不应覆盖或保留旧值。
|
||||||
|
const DETAIL_ONLY_BOUND_CARD_FIELDS = new Set(['real_name_at'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化数据大小(MB -> GB/MB)
|
* 格式化数据大小(MB -> GB/MB)
|
||||||
*/
|
*/
|
||||||
@@ -60,10 +63,13 @@ const mergeAssetBoundCards = (
|
|||||||
): AssetBoundCard[] =>
|
): AssetBoundCard[] =>
|
||||||
realtimeCards.map((realtimeCard) => {
|
realtimeCards.map((realtimeCard) => {
|
||||||
const existingCard = existingCards.find((card) => card.iccid === realtimeCard.iccid)
|
const existingCard = existingCards.find((card) => card.iccid === realtimeCard.iccid)
|
||||||
if (!existingCard) return realtimeCard
|
if (!existingCard) {
|
||||||
|
return { ...realtimeCard, real_name_at: null }
|
||||||
|
}
|
||||||
|
|
||||||
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
||||||
for (const [key, value] of Object.entries(realtimeCard)) {
|
for (const [key, value] of Object.entries(realtimeCard)) {
|
||||||
|
if (DETAIL_ONLY_BOUND_CARD_FIELDS.has(key)) continue
|
||||||
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
||||||
mergedCard[key] = value
|
mergedCard[key] = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1653,112 +1653,3 @@
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
/* Dialogs are teleported to body, so their surface needs a page-level hook. */
|
|
||||||
.asset-information-dialog.el-dialog {
|
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #e5eaf1;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: 0 18px 50px rgb(15 23 42 / 18%);
|
|
||||||
|
|
||||||
.el-dialog__header {
|
|
||||||
padding: 20px 24px 16px;
|
|
||||||
margin: 0;
|
|
||||||
border-bottom: 1px solid #e5eaf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 650;
|
|
||||||
color: #172033;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
padding: 22px 24px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: 14px 24px 20px;
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form-item__label {
|
|
||||||
color: #526078;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
min-height: 34px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table {
|
|
||||||
--el-table-header-bg-color: #f8fafc;
|
|
||||||
--el-table-row-hover-bg-color: #f5f8ff;
|
|
||||||
--el-table-border-color: #e5eaf1;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-descriptions__label.el-descriptions__cell.is-bordered-label {
|
|
||||||
color: #526078;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 768px) {
|
|
||||||
.asset-information-dialog.el-dialog {
|
|
||||||
width: calc(100vw - 20px) !important;
|
|
||||||
|
|
||||||
.el-dialog__header {
|
|
||||||
padding: 16px 18px 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
max-height: calc(100vh - 150px);
|
|
||||||
padding: 18px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: 12px 18px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .asset-information-dialog.el-dialog {
|
|
||||||
border-color: #2b3a52;
|
|
||||||
|
|
||||||
.el-dialog__header,
|
|
||||||
.el-dialog__footer {
|
|
||||||
border-color: #2b3a52;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__title,
|
|
||||||
.el-form-item__label {
|
|
||||||
color: #e5edf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table {
|
|
||||||
--el-table-header-bg-color: #202d42;
|
|
||||||
--el-table-row-hover-bg-color: #223451;
|
|
||||||
--el-table-border-color: #2b3a52;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-descriptions__label.el-descriptions__cell.is-bordered-label {
|
|
||||||
color: #afbdd0;
|
|
||||||
background: #202d42;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -507,12 +507,13 @@
|
|||||||
const columnOptions = computed(() => {
|
const columnOptions = computed(() => {
|
||||||
const options = [
|
const options = [
|
||||||
{ label: '订单编号', prop: 'order_no' },
|
{ label: '订单编号', prop: 'order_no' },
|
||||||
|
{ label: '资产标识符', prop: 'asset_identifier' },
|
||||||
|
{ label: '购买套餐名称', prop: 'purchased_package_names' },
|
||||||
{ label: '买家手机号', prop: 'buyer_phone' },
|
{ label: '买家手机号', prop: 'buyer_phone' },
|
||||||
{ label: '买家昵称', prop: 'buyer_nickname' },
|
{ label: '买家昵称', prop: 'buyer_nickname' },
|
||||||
{ label: '订单类型', prop: 'order_type' },
|
{ label: '订单类型', prop: 'order_type' },
|
||||||
{ label: '资产类型', prop: 'asset_type' },
|
{ label: '资产类型', prop: 'asset_type' },
|
||||||
{ label: '买家类型', prop: 'buyer_type' },
|
{ label: '买家类型', prop: 'buyer_type' },
|
||||||
{ label: '资产标识符', prop: 'asset_identifier' },
|
|
||||||
{ label: '订单渠道', prop: 'purchase_role' },
|
{ label: '订单渠道', prop: 'purchase_role' },
|
||||||
{ label: '购买备注', prop: 'purchase_remark' },
|
{ label: '购买备注', prop: 'purchase_remark' },
|
||||||
{ label: '是否代购', prop: 'is_purchase_on_behalf' },
|
{ label: '是否代购', prop: 'is_purchase_on_behalf' },
|
||||||
@@ -727,6 +728,14 @@
|
|||||||
return `¥${(amount / 100).toFixed(2)}`
|
return `¥${(amount / 100).toFixed(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatPurchasedPackageNames = (order: Order): string => {
|
||||||
|
const packageNames = (order.items || [])
|
||||||
|
.map((item) => (item.package_name || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return packageNames.join('、') || '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 获取支付状态标签类型
|
// 获取支付状态标签类型
|
||||||
const getPaymentStatusType = (
|
const getPaymentStatusType = (
|
||||||
status: PaymentStatus
|
status: PaymentStatus
|
||||||
@@ -847,6 +856,20 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'asset_identifier',
|
||||||
|
label: '资产标识符',
|
||||||
|
width: 180,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Order) => row.asset_identifier || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'purchased_package_names',
|
||||||
|
label: '购买套餐名称',
|
||||||
|
minWidth: 180,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Order) => formatPurchasedPackageNames(row)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'buyer_phone',
|
prop: 'buyer_phone',
|
||||||
label: '买家手机号',
|
label: '买家手机号',
|
||||||
@@ -911,13 +934,6 @@
|
|||||||
width: 180,
|
width: 180,
|
||||||
formatter: (row: Order) => (row.expires_at ? formatDateTime(row.expires_at) : '-')
|
formatter: (row: Order) => (row.expires_at ? formatDateTime(row.expires_at) : '-')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'asset_identifier',
|
|
||||||
label: '资产标识符',
|
|
||||||
width: 180,
|
|
||||||
showOverflowTooltip: true,
|
|
||||||
formatter: (row: Order) => row.asset_identifier || '-'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'purchase_role',
|
prop: 'purchase_role',
|
||||||
label: '订单渠道',
|
label: '订单渠道',
|
||||||
|
|||||||
Reference in New Issue
Block a user