Compare commits

12 Commits

Author SHA1 Message Date
luo
8229ad9248 fix: 退款管理申请退款显示逻辑
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 52m24s
2026-09-12 11:45:42 +08:00
luo
fc01fa2ac9 fix: 超时时间改为90s, 分配店铺查询
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m11s
2026-09-09 16:40:40 +08:00
luo
65cc63674e fix: 格式化时间
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m44s
2026-09-08 14:33:59 +08:00
luo
01cddd7eda fix:bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m46s
2026-09-08 10:59:38 +08:00
luo
a7b006076a fix: 字段位置交换
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m55s
2026-09-04 17:08:35 +08:00
luo
c36c59e268 fix: 推送到main
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 23m29s
2026-08-31 17:27:08 +08:00
luo
e56951d4b7 fix: 资产信息 绑定卡运营商显示逻辑 2026-08-31 17:10:27 +08:00
luo
b5c58d695f fix: ui 2026-08-24 16:44:08 +08:00
luo
6f6ac83b71 fix: ui 2026-08-24 16:37:21 +08:00
luo
48e4adf3ff fix: ui 2026-08-24 16:33:20 +08:00
luo
d335984c20 fix: 店铺用户名 2026-08-24 16:10:24 +08:00
luo
89e6d19d1f fix: other 2026-08-24 10:18:09 +08:00
29 changed files with 383 additions and 274 deletions

View File

@@ -3,7 +3,7 @@ name: 构建并部署前端到测试环境
on: on:
push: push:
branches: branches:
- develop - main
- dev - dev
- test - test
@@ -27,7 +27,7 @@ jobs:
- name: 设置镜像标签 - name: 设置镜像标签
id: tag id: tag
run: | run: |
if [ "${{ github.ref }}" = "refs/heads/develop" ]; then if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tag=latest" >> $GITHUB_OUTPUT echo "tag=latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
echo "tag=dev" >> $GITHUB_OUTPUT echo "tag=dev" >> $GITHUB_OUTPUT
@@ -51,8 +51,8 @@ jobs:
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }} docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: 部署到本地(仅 develop 分支) - name: 部署到本地(仅 main 分支)
if: github.ref == 'refs/heads/develop' if: github.ref == 'refs/heads/main'
run: | run: |
# 确保部署目录存在 # 确保部署目录存在
mkdir -p ${{ env.DEPLOY_DIR }} mkdir -p ${{ env.DEPLOY_DIR }}

View File

@@ -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`.

View File

@@ -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

View File

@@ -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.

View File

@@ -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">

View File

@@ -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="资产标识">

View File

@@ -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">

View File

@@ -236,6 +236,8 @@ export interface AssetBoundCard {
card_id: number // 卡 ID card_id: number // 卡 ID
iccid: string // ICCID iccid: string // ICCID
msisdn: string // 手机号 msisdn: string // 手机号
carrier_type?: string // 运营商类型
carrier_name?: string // 运营商名称
network_status: NetworkStatus // 网络状态 network_status: NetworkStatus // 网络状态
real_name_status: RealNameStatus // 实名状态 real_name_status: RealNameStatus // 实名状态
real_name_at?: string | null // 实名时间 real_name_at?: string | null // 实名时间

View File

@@ -10,6 +10,19 @@ export enum RefundStatus {
RETURNED = 4 // 已退回 RETURNED = 4 // 已退回
} }
// 审批状态
export enum RefundApprovalStatus {
SUBMITTING = 0, // 提交中
APPROVING = 1, // 审批中
APPROVED = 2, // 已通过
REJECTED = 3, // 已拒绝
REVOKED = 4, // 已撤销
REVOKED_AFTER_APPROVED = 5, // 通过后撤销
DELETED = 6, // 已删除
SUBMIT_FAILED = 7, // 提交失败
RESULT_UNKNOWN = 8 // 提交结果未知
}
export interface RefundAttachment { export interface RefundAttachment {
file_key: string file_key: string
file_name?: string file_name?: string
@@ -70,7 +83,7 @@ export interface Refund {
approval_provider?: 'wecom' | null approval_provider?: 'wecom' | null
approval_instance_id?: number | null approval_instance_id?: number | null
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源 approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 审批状态 approval_status?: RefundApprovalStatus | null // 审批状态
approval_status_name?: string | null // 审批状态名称 approval_status_name?: string | null // 审批状态名称
current_approver_summary?: string | null // 当前审批人摘要 current_approver_summary?: string | null // 当前审批人摘要
processing_status?: string | null // 业务处理状态 processing_status?: string | null // 业务处理状态

View File

@@ -7,7 +7,7 @@ import { normalizeApiError } from '@/utils/business/apiError'
import { syncCompatibleIdFields } from '@/utils/business/id' import { syncCompatibleIdFields } from '@/utils/business/id'
const axiosInstance = axios.create({ const axiosInstance = axios.create({
timeout: 15000, // 请求超时时间(毫秒) timeout: 90000, // 请求超时时间(毫秒)
baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理其他模式使用完整URL baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理其他模式使用完整URL
withCredentials: false, // 异步请求携带cookie withCredentials: false, // 异步请求携带cookie
transformRequest: [ transformRequest: [

View File

@@ -118,13 +118,13 @@
label="实名时间" label="实名时间"
label-class-name="asset-label asset-label--realname-time" label-class-name="asset-label asset-label--realname-time"
>{{ >{{
cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-' cardInfo?.real_name_at ? formatAssetInfoTime(cardInfo.real_name_at) : '-'
}}</ElDescriptionsItem }}</ElDescriptionsItem
> >
<ElDescriptionsItem <ElDescriptionsItem
label="实名同步时间" label="实名同步时间"
label-class-name="asset-label asset-label--realname-sync" label-class-name="asset-label asset-label--realname-sync"
>{{ formatDateTime(cardInfo?.last_real_name_check_at) || '-' }}</ElDescriptionsItem >{{ formatAssetInfoTime(cardInfo?.last_real_name_check_at) }}</ElDescriptionsItem
> >
<ElDescriptionsItem <ElDescriptionsItem
@@ -143,12 +143,12 @@
<ElDescriptionsItem <ElDescriptionsItem
label="卡状态同步时间" label="卡状态同步时间"
label-class-name="asset-label asset-label--card-sync" label-class-name="asset-label asset-label--card-sync"
>{{ formatDateTime(cardInfo?.last_card_status_check_at) || '-' }}</ElDescriptionsItem >{{ formatAssetInfoTime(cardInfo?.last_card_status_check_at) }}</ElDescriptionsItem
> >
<ElDescriptionsItem <ElDescriptionsItem
label="流量同步时间" label="流量同步时间"
label-class-name="asset-label asset-label--traffic-sync" label-class-name="asset-label asset-label--traffic-sync"
>{{ formatDateTime(cardInfo?.last_data_check_at) || '-' }}</ElDescriptionsItem >{{ formatAssetInfoTime(cardInfo?.last_data_check_at) }}</ElDescriptionsItem
> >
<ElDescriptionsItem <ElDescriptionsItem
@@ -241,11 +241,10 @@
<span v-else>-</span> <span v-else>-</span>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem <ElDescriptionsItem
label="激活状态" label="虚拟号"
label-class-name="asset-label asset-label--activation-status" label-class-name="asset-label asset-label--virtual-number"
>{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem
> >
{{ cardInfo?.activation_status_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem <ElDescriptionsItem
label="设备型号" label="设备型号"
@@ -257,7 +256,7 @@
label-class-name="asset-label asset-label--realname-time" label-class-name="asset-label asset-label--realname-time"
>{{ >{{
selectedDeviceStatusCard?.real_name_at selectedDeviceStatusCard?.real_name_at
? formatDateTime(selectedDeviceStatusCard.real_name_at) ? formatAssetInfoTime(selectedDeviceStatusCard.real_name_at)
: '-' : '-'
}}</ElDescriptionsItem }}</ElDescriptionsItem
> >
@@ -275,10 +274,11 @@
<span v-else>-</span> <span v-else>-</span>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem <ElDescriptionsItem
label="虚拟号" label="激活状态"
label-class-name="asset-label asset-label--virtual-number" label-class-name="asset-label asset-label--activation-status"
>{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem
> >
{{ cardInfo?.activation_status_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem <ElDescriptionsItem
label="切卡模式" label="切卡模式"
label-class-name="asset-label asset-label--switch-mode" label-class-name="asset-label asset-label--switch-mode"
@@ -434,7 +434,7 @@
</div> </div>
<div class="binding-card-field"> <div class="binding-card-field">
<span class="binding-card-field-label">运营商</span> <span class="binding-card-field-label">运营商</span>
<span class="binding-card-field-value">{{ bindingCard.carrier_name || '-' }}</span> <span class="binding-card-field-value">{{ getCarrierAccountDisplay(bindingCard) }}</span>
</div> </div>
<div class="binding-card-field"> <div class="binding-card-field">
<span class="binding-card-field-label">卡槽位置</span> <span class="binding-card-field-label">卡槽位置</span>
@@ -467,7 +467,11 @@
<div class="binding-card-field"> <div class="binding-card-field">
<span class="binding-card-field-label">实名时间</span> <span class="binding-card-field-label">实名时间</span>
<span class="binding-card-field-value"> <span class="binding-card-field-value">
{{ bindingCard.real_name_at ? formatDateTime(bindingCard.real_name_at) : '-' }} {{
bindingCard.real_name_at
? formatAssetInfoTime(bindingCard.real_name_at)
: '-'
}}
</span> </span>
</div> </div>
<div class="binding-card-field"> <div class="binding-card-field">
@@ -679,6 +683,7 @@
card_id?: number card_id?: number
iccid: string iccid: string
msisdn?: string msisdn?: string
carrier_type?: string
carrier_name?: string carrier_name?: string
slot_position?: number slot_position?: number
is_current: boolean is_current: boolean
@@ -883,12 +888,17 @@
return !isAgentOrEnterpriseAccount.value return !isAgentOrEnterpriseAccount.value
}) })
const carrierAccountDisplay = computed(() => { const getCarrierAccountDisplay = (card?: {
carrier_type?: string
carrier_name?: string
}) => {
if (isAgentOrEnterpriseAccount.value) { if (isAgentOrEnterpriseAccount.value) {
return getCarrierTypeName(props.cardInfo?.carrier_type) return getCarrierTypeName(card?.carrier_type)
} }
return props.cardInfo?.carrier_name || '-' return card?.carrier_name || '-'
}) }
const carrierAccountDisplay = computed(() => getCarrierAccountDisplay(props.cardInfo))
const descriptionsColumn = computed(() => { const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1 if (width.value <= 640) return 1
@@ -989,19 +999,19 @@
{ {
key: 'realname', key: 'realname',
label: '实名同步时间', label: '实名同步时间',
value: formatDateTime(props.cardInfo?.last_real_name_check_at) || '-', value: formatAssetInfoTime(props.cardInfo?.last_real_name_check_at),
icon: 'realname-sync' icon: 'realname-sync'
}, },
{ {
key: 'cardSync', key: 'cardSync',
label: '卡状态同步时间', label: '卡状态同步时间',
value: formatDateTime(props.cardInfo?.last_card_status_check_at) || '-', value: formatAssetInfoTime(props.cardInfo?.last_card_status_check_at),
icon: 'card-sync' icon: 'card-sync'
}, },
{ {
key: 'trafficSync', key: 'trafficSync',
label: '流量同步时间', label: '流量同步时间',
value: formatDateTime(props.cardInfo?.last_data_check_at) || '-', value: formatAssetInfoTime(props.cardInfo?.last_data_check_at),
icon: 'traffic-sync' icon: 'traffic-sync'
}, },
{ {
@@ -1134,6 +1144,34 @@
return formatDataSize(value) return formatDataSize(value)
} }
/**
* 接口中的这几项时间以其原始文本为基准直接加 8 小时;时区后缀不参与换算。
* 例如 2026-08-06T11:54:26.693382+08:00 显示为 2026-08-06 19:54:26。
*/
const formatAssetInfoTime = (value?: string | null) => {
if (!value?.trim()) return '-'
const timeText = value.trim()
const match = timeText.match(
/^(\d{4})-(\d{2})-(\d{2})(?:T|\s)(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/i
)
if (!match) return '-'
const [, year, month, day, hours, minutes, seconds = '00'] = match
const date = new Date(
Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hours) + 8,
Number(minutes),
Number(seconds)
)
)
return formatDateTime(date)
}
// 处理轮询开关变化 // 处理轮询开关变化
const handlePollingChange = (value: string | number | boolean) => { const handlePollingChange = (value: string | number | boolean) => {
emit('update:pollingEnabled', Boolean(value)) emit('update:pollingEnabled', Boolean(value))
@@ -1452,7 +1490,7 @@
} }
:deep(.asset-label--bound-device-name::before) { :deep(.asset-label--bound-device-name::before) {
content: '\e6ef'; content: '\e7d8';
} }
:deep(.asset-label--device-name::before) { :deep(.asset-label--device-name::before) {

View File

@@ -319,8 +319,9 @@
v-model="usedDataForm.data_usage" v-model="usedDataForm.data_usage"
:min="0" :min="0"
:max="usedDataMaxValue" :max="usedDataMaxValue"
:precision="3" :precision="0"
:step="0.1" :step="1"
step-strictly
class="used-data-input__number" class="used-data-input__number"
placeholder="请输入新的套餐真实已用量" placeholder="请输入新的套餐真实已用量"
/> />
@@ -457,7 +458,7 @@
const usedDataMaxValue = computed(() => { const usedDataMaxValue = computed(() => {
const realTotalMb = selectedPackage.value?.real_total_mb ?? 0 const realTotalMb = selectedPackage.value?.real_total_mb ?? 0
return usedDataForm.unit === 'GB' ? Number((realTotalMb / 1024).toFixed(3)) : realTotalMb return usedDataForm.unit === 'GB' ? Math.floor(realTotalMb / 1024) : Math.floor(realTotalMb)
}) })
const validateUsedData = ( const validateUsedData = (
@@ -471,6 +472,11 @@
return return
} }
if (!Number.isInteger(usedDataForm.data_usage)) {
callback(new Error('新的已用量必须为整数'))
return
}
if (dataUsageMb === undefined || dataUsageMb < 0) { if (dataUsageMb === undefined || dataUsageMb < 0) {
callback(new Error('请输入可换算为非负数字 MB 的已用量')) callback(new Error('请输入可换算为非负数字 MB 的已用量'))
return return

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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="设备标识">

View File

@@ -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">

View File

@@ -62,7 +62,7 @@ export function useAssetFormatters() {
const getRealNameStatusType = (status?: number): 'info' | 'success' | 'warning' | 'danger' => { const getRealNameStatusType = (status?: number): 'info' | 'success' | 'warning' | 'danger' => {
if (status === undefined || status === null) return 'info' if (status === undefined || status === null) return 'info'
const map: Record<number, 'info' | 'success' | 'warning' | 'danger'> = { const map: Record<number, 'info' | 'success' | 'warning' | 'danger'> = {
0: 'info', 0: 'danger',
1: 'success' 1: 'success'
} }
return map[status] || 'info' return map[status] || 'info'

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -56,6 +56,7 @@ export interface BindingCard {
msisdn?: string msisdn?: string
is_current: boolean is_current: boolean
status?: number status?: number
carrier_type?: string
carrier_name?: string carrier_name?: string
slot_position?: number slot_position?: number
network_status?: number network_status?: number

View File

@@ -108,6 +108,7 @@
v-model="allocateForm.target_shop_id" v-model="allocateForm.target_shop_id"
:options="shopCascadeOptions" :options="shopCascadeOptions"
:props="shopCascadeProps" :props="shopCascadeProps"
:before-filter="handleAllocateShopBeforeFilter"
placeholder="请选择目标店铺" placeholder="请选择目标店铺"
clearable clearable
filterable filterable
@@ -238,7 +239,10 @@
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> <span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem> </ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type"> <ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type"> <ElRadioGroup
v-model="seriesBindingForm.selection_type"
@change="handleDeviceBatchSelectionTypeChange"
>
<ElRadio label="list">设备ID列表</ElRadio> <ElRadio label="list">设备ID列表</ElRadio>
<ElRadio label="range">设备号范围</ElRadio> <ElRadio label="range">设备号范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -736,7 +740,10 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type"> <ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceAuthorizeForm.selection_type"> <ElRadioGroup
v-model="enterpriseDeviceAuthorizeForm.selection_type"
@change="handleDeviceBatchSelectionTypeChange"
>
<ElRadio label="list">设备号列表</ElRadio> <ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup> </ElRadioGroup>
@@ -842,7 +849,10 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="选设备方式" prop="selection_type"> <ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseDeviceRecallForm.selection_type"> <ElRadioGroup
v-model="enterpriseDeviceRecallForm.selection_type"
@change="handleDeviceBatchSelectionTypeChange"
>
<ElRadio label="list">设备号列表</ElRadio> <ElRadio label="list">设备号列表</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup> </ElRadioGroup>
@@ -1040,6 +1050,62 @@
const batchRealnamePolicyError = ref('') const batchRealnamePolicyError = ref('')
const exportDialogVisible = ref(false) const exportDialogVisible = ref(false)
const shopCascadeOptions = ref<any[]>([]) const shopCascadeOptions = ref<any[]>([])
let allocateShopSearchRequestId = 0
const mapShopCascadeNodes = (items: any[]) => {
const seen = new Set<number>()
return items.reduce<any[]>((nodes, item) => {
if (seen.has(item.id)) return nodes
seen.add(item.id)
nodes.push({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
})
return nodes
}, [])
}
const handleAllocateShopBeforeFilter = async (query: string) => {
const keyword = query.trim()
if (!keyword) return true
const requestId = ++allocateShopSearchRequestId
try {
const shopResponse = await ShopService.getShops({
page: 1,
page_size: 20,
shop_name: keyword
})
if (requestId !== allocateShopSearchRequestId) return false
if (shopResponse.code !== 0) {
shopCascadeOptions.value = []
return false
}
const parentIds = Array.from(
new Set((shopResponse.data.items || []).map((shop) => shop.parent_id ?? null))
)
const cascadeResponses = await Promise.all(
parentIds.map((parentId) =>
ShopService.getShopsCascade({
shop_name: keyword,
parent_id: parentId ?? undefined
})
)
)
if (requestId !== allocateShopSearchRequestId) return false
shopCascadeOptions.value = mapShopCascadeNodes(
cascadeResponses.flatMap((response) => (response.code === 0 ? response.data || [] : []))
)
return true
} catch (error) {
if (requestId === allocateShopSearchRequestId) shopCascadeOptions.value = []
console.error('搜索批量分配目标店铺失败:', error)
return false
}
}
const shopCascadeProps = { const shopCascadeProps = {
lazy: true, lazy: true,
checkStrictly: true, checkStrictly: true,
@@ -1050,12 +1116,7 @@
try { try {
const res = await ShopService.getShopsCascade({ parent_id: parentId }) const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) { if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({ resolve(mapShopCascadeNodes(res.data || []))
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else { } else {
resolve([]) resolve([])
} }
@@ -1072,11 +1133,7 @@
try { try {
const res = await ShopService.getShopsCascade({ parent_id: undefined }) const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) { if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({ shopCascadeOptions.value = mapShopCascadeNodes(res.data || [])
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
} }
} catch (error) { } catch (error) {
console.error('加载顶级店铺失败:', error) console.error('加载顶级店铺失败:', error)
@@ -1355,7 +1412,7 @@
// 搜索表单 // 搜索表单
const searchForm = reactive<Record<string, any>>({ ...initialSearchState }) const searchForm = reactive<Record<string, any>>({ ...initialSearchState })
// 搜索批次号(用于搜索表单) // 批次号仅在用户切换至筛选条件或输入批次号搜索时加载。
const loadSearchBatchNoOptions = async (batchNo?: string) => { const loadSearchBatchNoOptions = async (batchNo?: string) => {
try { try {
const params: any = { const params: any = {
@@ -1374,11 +1431,14 @@
} }
} }
// 搜索批次号
const handleSearchBatchNo = (query: string) => { const handleSearchBatchNo = (query: string) => {
if (query) { loadSearchBatchNoOptions(query || undefined)
loadSearchBatchNoOptions(query) }
} else {
const handleDeviceBatchSelectionTypeChange = (
selectionType: string | number | boolean | undefined
) => {
if (selectionType === DeviceSelectionType.FILTER) {
loadSearchBatchNoOptions() loadSearchBatchNoOptions()
} }
} }
@@ -2515,18 +2575,14 @@
const showEnterpriseDeviceAuthorizeDialog = async () => { const showEnterpriseDeviceAuthorizeDialog = async () => {
resetEnterpriseDeviceAuthorizeForm() resetEnterpriseDeviceAuthorizeForm()
await Promise.all([ await Promise.all([handleSearchEnterprise(), loadTopLevelShopCascadeOptions()])
handleSearchEnterprise(),
loadSearchBatchNoOptions(),
loadTopLevelShopCascadeOptions()
])
enterpriseDeviceAuthorizeDialogVisible.value = true enterpriseDeviceAuthorizeDialogVisible.value = true
enterpriseDeviceAuthorizeFormRef.value?.clearValidate() enterpriseDeviceAuthorizeFormRef.value?.clearValidate()
} }
const showEnterpriseDeviceRecallDialog = async () => { const showEnterpriseDeviceRecallDialog = async () => {
resetEnterpriseDeviceRecallForm() resetEnterpriseDeviceRecallForm()
await Promise.all([handleSearchEnterprise(), loadSearchBatchNoOptions()]) await handleSearchEnterprise()
enterpriseDeviceRecallDialogVisible.value = true enterpriseDeviceRecallDialogVisible.value = true
enterpriseDeviceRecallFormRef.value?.clearValidate() enterpriseDeviceRecallFormRef.value?.clearValidate()
} }
@@ -2667,7 +2723,7 @@
selection_type: selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
}) })
await Promise.all([loadPackageSeriesList(), loadSearchBatchNoOptions()]) await loadPackageSeriesList()
seriesBindingDialogVisible.value = true seriesBindingDialogVisible.value = true
seriesBindingFormRef.value?.clearValidate() seriesBindingFormRef.value?.clearValidate()
} }

View File

@@ -268,8 +268,8 @@
{{ createNewDeviceEmptyMessage }} {{ createNewDeviceEmptyMessage }}
</div> </div>
</ElFormItem> </ElFormItem>
<ElFormItem v-if="createDirectInheritedShopHint" label="归属提示"> <ElFormItem label="归属提示">
<div class="exchange-shop-hint">{{ createDirectInheritedShopHint }}</div> <div class="exchange-shop-hint">新资产会继承旧资产店铺</div>
</ElFormItem> </ElFormItem>
<ElFormItem v-if="createForm.flow_type === 'direct'" label="是否迁移数据"> <ElFormItem v-if="createForm.flow_type === 'direct'" label="是否迁移数据">
<ElSwitch v-model="createForm.migrate_data" /> <ElSwitch v-model="createForm.migrate_data" />
@@ -771,11 +771,6 @@
return undefined return undefined
} }
const createDirectInheritedShopHint = computed(() => {
const shopName = getShopDisplayName(createSelectedNewAsset.value)
return shopName ? `换货完成后将归属:${shopName}` : ''
})
const shipInheritedShopDisplayText = computed(() => { const shipInheritedShopDisplayText = computed(() => {
const shopName = getShopDisplayName(shipSelectedNewAsset.value) const shopName = getShopDisplayName(shipSelectedNewAsset.value)

View File

@@ -112,6 +112,7 @@
v-model="allocateForm.to_shop_id" v-model="allocateForm.to_shop_id"
:options="shopCascadeOptions" :options="shopCascadeOptions"
:props="shopCascadeProps" :props="shopCascadeProps"
:before-filter="handleAllocateShopBeforeFilter"
placeholder="请选择目标店铺" placeholder="请选择目标店铺"
clearable clearable
filterable filterable
@@ -119,7 +120,10 @@
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="allocateForm.selection_type"> <ElRadioGroup
v-model="allocateForm.selection_type"
@change="handleCardBatchSelectionTypeChange"
>
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio> <ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -246,7 +250,10 @@
> >
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="100px"> <ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="100px">
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="recallForm.selection_type"> <ElRadioGroup
v-model="recallForm.selection_type"
@change="handleCardBatchSelectionTypeChange"
>
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio> <ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -415,7 +422,10 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardAuthorizeForm.selection_type"> <ElRadioGroup
v-model="enterpriseCardAuthorizeForm.selection_type"
@change="handleCardBatchSelectionTypeChange"
>
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio> <ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -568,7 +578,10 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardRecallForm.selection_type"> <ElRadioGroup
v-model="enterpriseCardRecallForm.selection_type"
@change="handleCardBatchSelectionTypeChange"
>
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio> <ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -739,7 +752,10 @@
label-width="100" label-width="100"
> >
<ElFormItem label="选卡方式" prop="selection_type"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type"> <ElRadioGroup
v-model="seriesBindingForm.selection_type"
@change="handleCardBatchSelectionTypeChange"
>
<ElRadio label="list">ICCID列表</ElRadio> <ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio> <ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio> <ElRadio label="filter">筛选条件</ElRadio>
@@ -1338,10 +1354,9 @@
} }
} }
// 批量分配对话框批次号选项 // 批次号仅在用户切换至筛选条件时加载;避免弹窗打开即请求导入任务列表。
const allocateBatchNoOptions = ref<any[]>([]) const allocateBatchNoOptions = ref<any[]>([])
// 加载批量分配对话框批次号选项
const loadAllocateBatchNoOptions = async (batchNo?: string) => { const loadAllocateBatchNoOptions = async (batchNo?: string) => {
try { try {
const params: any = { const params: any = {
@@ -1360,11 +1375,14 @@
} }
} }
// 搜索批量分配对话框批次号
const handleSearchAllocateBatchNo = (query: string) => { const handleSearchAllocateBatchNo = (query: string) => {
if (query) { loadAllocateBatchNoOptions(query || undefined)
loadAllocateBatchNoOptions(query) }
} else {
const handleCardBatchSelectionTypeChange = (
selectionType: string | number | boolean | undefined
) => {
if (selectionType === CardSelectionType.FILTER) {
loadAllocateBatchNoOptions() loadAllocateBatchNoOptions()
} }
} }
@@ -1506,6 +1524,62 @@
// 店铺相关 // 店铺相关
const shopCascadeOptions = ref<any[]>([]) const shopCascadeOptions = ref<any[]>([])
let allocateShopSearchRequestId = 0
const mapShopCascadeNodes = (items: any[]) => {
const seen = new Set<number>()
return items.reduce<any[]>((nodes, item) => {
if (seen.has(item.id)) return nodes
seen.add(item.id)
nodes.push({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
})
return nodes
}, [])
}
const handleAllocateShopBeforeFilter = async (query: string) => {
const keyword = query.trim()
if (!keyword) return true
const requestId = ++allocateShopSearchRequestId
try {
const shopResponse = await ShopService.getShops({
page: 1,
page_size: 20,
shop_name: keyword
})
if (requestId !== allocateShopSearchRequestId) return false
if (shopResponse.code !== 0) {
shopCascadeOptions.value = []
return false
}
const parentIds = Array.from(
new Set((shopResponse.data.items || []).map((shop) => shop.parent_id ?? null))
)
const cascadeResponses = await Promise.all(
parentIds.map((parentId) =>
ShopService.getShopsCascade({
shop_name: keyword,
parent_id: parentId ?? undefined
})
)
)
if (requestId !== allocateShopSearchRequestId) return false
shopCascadeOptions.value = mapShopCascadeNodes(
cascadeResponses.flatMap((response) => (response.code === 0 ? response.data || [] : []))
)
return true
} catch (error) {
if (requestId === allocateShopSearchRequestId) shopCascadeOptions.value = []
console.error('搜索批量分配目标店铺失败:', error)
return false
}
}
const shopCascadeProps = { const shopCascadeProps = {
lazy: true, lazy: true,
checkStrictly: true, checkStrictly: true,
@@ -1516,12 +1590,7 @@
try { try {
const res = await ShopService.getShopsCascade({ parent_id: parentId }) const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) { if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({ resolve(mapShopCascadeNodes(res.data || []))
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else { } else {
resolve([]) resolve([])
} }
@@ -1538,11 +1607,7 @@
try { try {
const res = await ShopService.getShopsCascade({ parent_id: undefined }) const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) { if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({ shopCascadeOptions.value = mapShopCascadeNodes(res.data || [])
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
} }
} catch (error) { } catch (error) {
console.error('加载顶级店铺失败:', error) console.error('加载顶级店铺失败:', error)
@@ -1843,6 +1908,19 @@
{ label: '无换卡记录', value: false } { label: '无换卡记录', value: false }
] ]
}, },
{
label: '是否绑设备',
prop: 'is_standalone',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
},
{ {
label: '起始号', label: '起始号',
prop: 'iccid_start', prop: 'iccid_start',
@@ -1860,19 +1938,6 @@
clearable: true, clearable: true,
placeholder: '请输入ICCID结束号' placeholder: '请输入ICCID结束号'
} }
},
{
label: '是否绑设备',
prop: 'is_standalone',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
} }
] ]
@@ -2469,6 +2534,7 @@
return return
} }
allocateDialogVisible.value = true allocateDialogVisible.value = true
shopCascadeOptions.value = []
Object.assign(allocateForm, { Object.assign(allocateForm, {
selection_type: CardSelectionType.LIST, selection_type: CardSelectionType.LIST,
to_shop_id: undefined, to_shop_id: undefined,
@@ -2481,7 +2547,6 @@
remark: '' remark: ''
}) })
loadAllocateCarrierOptions() loadAllocateCarrierOptions()
loadAllocateBatchNoOptions()
if (allocateFormRef.value) { if (allocateFormRef.value) {
allocateFormRef.value.resetFields() allocateFormRef.value.resetFields()
} }
@@ -2507,7 +2572,6 @@
recallFormRef.value.resetFields() recallFormRef.value.resetFields()
} }
loadAllocateCarrierOptions() loadAllocateCarrierOptions()
loadAllocateBatchNoOptions()
} }
// 关闭批量分配对话框 // 关闭批量分配对话框
@@ -2677,7 +2741,6 @@
await Promise.all([ await Promise.all([
handleSearchEnterprise(), handleSearchEnterprise(),
loadAllocateCarrierOptions(), loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions(),
loadTopLevelShopCascadeOptions() loadTopLevelShopCascadeOptions()
]) ])
enterpriseCardAuthorizeDialogVisible.value = true enterpriseCardAuthorizeDialogVisible.value = true
@@ -2686,11 +2749,7 @@
const showEnterpriseCardRecallDialog = async () => { const showEnterpriseCardRecallDialog = async () => {
resetEnterpriseCardRecallForm() resetEnterpriseCardRecallForm()
await Promise.all([ await Promise.all([handleSearchEnterprise(), loadAllocateCarrierOptions()])
handleSearchEnterprise(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
enterpriseCardRecallDialogVisible.value = true enterpriseCardRecallDialogVisible.value = true
enterpriseCardRecallFormRef.value?.clearValidate() enterpriseCardRecallFormRef.value?.clearValidate()
} }
@@ -2810,11 +2869,7 @@
selection_type: selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
}) })
await Promise.all([ await Promise.all([loadPackageSeriesList(), loadAllocateCarrierOptions()])
loadPackageSeriesList(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
seriesBindingDialogVisible.value = true seriesBindingDialogVisible.value = true
seriesBindingFormRef.value?.clearValidate() seriesBindingFormRef.value?.clearValidate()
} }

View File

@@ -113,7 +113,13 @@
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { RefundService } from '@/api/modules' import { RefundService } from '@/api/modules'
import type { Refund, ResubmitRefundRequest, RefundAttachment } from '@/types/api' import {
RefundApprovalStatus,
RefundStatus,
type Refund,
type ResubmitRefundRequest,
type RefundAttachment
} from '@/types/api'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format' import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { toVoucherKeyList, getErrorMessage } from '@/utils/business' import { toVoucherKeyList, getErrorMessage } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue' import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
@@ -456,22 +462,14 @@
resubmitUploadRef.value?.clearFiles(false) resubmitUploadRef.value?.clearFiles(false)
} }
const canResubmit = (item: Refund) => { const canResubmit = (item: Refund) =>
const statusText = [item.approval_status, item.approval_status_name, item.status_name] item.status === RefundStatus.REJECTED ||
.filter(Boolean) item.status === RefundStatus.RETURNED ||
.join(' ') item.approval_status === RefundApprovalStatus.REJECTED ||
.toLowerCase() item.approval_status === RefundApprovalStatus.REVOKED ||
item.approval_status === RefundApprovalStatus.REVOKED_AFTER_APPROVED ||
return ( item.approval_status === RefundApprovalStatus.SUBMIT_FAILED ||
item.status === 3 || item.approval_status === RefundApprovalStatus.RESULT_UNKNOWN
statusText.includes('reject') ||
statusText.includes('revoke') ||
statusText.includes('delete') ||
statusText.includes('驳回') ||
statusText.includes('撤销') ||
statusText.includes('删除')
)
}
const handleShowResubmit = () => { const handleShowResubmit = () => {
if (!refund.value || !canResubmit(refund.value)) return if (!refund.value || !canResubmit(refund.value)) return

View File

@@ -150,6 +150,7 @@
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { import {
RefundStatus, RefundStatus,
RefundApprovalStatus,
type Refund, type Refund,
type RefundQueryParams, type RefundQueryParams,
type ResubmitRefundRequest, type ResubmitRefundRequest,
@@ -734,22 +735,14 @@
return keys.length ? keys : toVoucherKeyList(row.refund_voucher_key) return keys.length ? keys : toVoucherKeyList(row.refund_voucher_key)
} }
const canResubmit = (row: Refund) => { const canResubmit = (row: Refund) =>
const statusText = [row.approval_status, row.approval_status_name, row.status_name] row.status === RefundStatus.REJECTED ||
.filter(Boolean) row.status === RefundStatus.RETURNED ||
.join(' ') row.approval_status === RefundApprovalStatus.REJECTED ||
.toLowerCase() row.approval_status === RefundApprovalStatus.REVOKED ||
row.approval_status === RefundApprovalStatus.REVOKED_AFTER_APPROVED ||
return ( row.approval_status === RefundApprovalStatus.SUBMIT_FAILED ||
row.status === 3 || row.approval_status === RefundApprovalStatus.RESULT_UNKNOWN
statusText.includes('reject') ||
statusText.includes('revoke') ||
statusText.includes('delete') ||
statusText.includes('驳回') ||
statusText.includes('撤销') ||
statusText.includes('删除')
)
}
const canTriggerApproval = (row: Refund) => { const canTriggerApproval = (row: Refund) => {
const approvalStatusEmpty = row.approval_status === undefined || row.approval_status === null const approvalStatusEmpty = row.approval_status === undefined || row.approval_status === null

View File

@@ -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: '订单渠道',

View File

@@ -1080,7 +1080,8 @@
], ],
init_username: [ init_username: [
{ required: true, message: '请输入初始账号用户名', trigger: 'blur' }, { required: true, message: '请输入初始账号用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' },
{ pattern: /^[A-Za-z\d]+$/, message: '用户名只能包含英文或数字', trigger: 'blur' }
], ],
init_password: [ init_password: [
{ required: true, message: '请输入初始账号密码', trigger: 'blur' }, { required: true, message: '请输入初始账号密码', trigger: 'blur' },