fix: 换货
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m12s

This commit is contained in:
sexygoat
2026-06-03 17:53:19 +08:00
parent 3e6b3981f4
commit c5dae2993b
23 changed files with 797 additions and 10804 deletions

View File

@@ -10,7 +10,8 @@ export interface ExchangeQueryParams {
page?: number
page_size?: number
status?: number // 换货状态
identifier?: string // 资产标识符(模糊匹配
flow_type?: string // 流程类型shipping/direct
identifier?: string // 资产标识符(模糊匹配,同时匹配旧资产、新资产)
created_at_start?: string // 创建时间起始
created_at_end?: string // 创建时间结束
}
@@ -20,6 +21,9 @@ export interface CreateExchangeRequest {
exchange_reason: string // 换货原因
old_asset_type: string // 旧资产类型 (iot_card 或 device)
old_identifier: string // 旧资产标识符(ICCID/虚拟号/IMEI/SN)
flow_type?: string // 流程类型shipping/direct默认 shipping
new_identifier?: string // 新资产标识符direct 时必填)
migrate_data?: boolean // 是否迁移数据(默认 false
remark?: string // 备注(可选)
}
@@ -34,6 +38,10 @@ export interface ExchangeResponse {
new_asset_identifier: string
status: number // 换货状态1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
status_text: string
flow_type: string // 流程类型shipping/direct
flow_type_name: string // 流程类型名称
shipped_at?: string | null // 发货时间(仅 shipping 发货后有值)
completed_at?: string | null // 换货完成时间
recipient_name?: string
recipient_phone?: string
recipient_address?: string

View File

@@ -55,9 +55,15 @@
return types[status] || 'info'
}
// 详情页配置
const detailSections: DetailSection[] = [
{
// 根据流程类型动态生成详情页配置
const detailSections = computed<DetailSection[]>(() => {
if (!exchangeDetail.value) return []
const flowType = exchangeDetail.value.flow_type || 'shipping' // 历史数据兼容
const sections: DetailSection[] = []
// 基本信息
sections.push({
title: '基本信息',
fields: [
{ label: '换货单号', prop: 'exchange_no' },
@@ -65,6 +71,10 @@
label: '状态',
render: (data) => h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
},
{
label: '流程类型',
formatter: (_, data) => data.flow_type_name || (flowType === 'direct' ? '直接换货' : '物流换货')
},
{
label: '换货原因',
prop: 'exchange_reason',
@@ -86,44 +96,57 @@
prop: 'new_asset_identifier'
}
]
},
{
title: '收货信息',
fields: [
{
label: '收货人姓名',
formatter: (value) => value || '--',
prop: 'recipient_name'
},
{
label: '收货人电话',
formatter: (value) => value || '--',
prop: 'recipient_phone'
},
{
label: '收货地址',
formatter: (value) => value || '--',
prop: 'recipient_address',
fullWidth: true
}
]
},
{
title: '物流信息',
fields: [
{
label: '快递公司',
formatter: (value) => value || '--',
prop: 'express_company'
},
{
label: '快递单号',
formatter: (value) => value || '--',
prop: 'express_no'
}
]
},
{
})
// 收货信息(仅物流换货显示)
if (flowType === 'shipping') {
sections.push({
title: '收货信息',
fields: [
{
label: '收货人姓名',
formatter: (value) => value || '--',
prop: 'recipient_name'
},
{
label: '收货人电话',
formatter: (value) => value || '--',
prop: 'recipient_phone'
},
{
label: '收货地址',
formatter: (value) => value || '--',
prop: 'recipient_address',
fullWidth: true
}
]
})
// 物流信息(仅物流换货显示)
sections.push({
title: '物流信息',
fields: [
{
label: '快递公司',
formatter: (value) => value || '--',
prop: 'express_company'
},
{
label: '快递单号',
formatter: (value) => value || '--',
prop: 'express_no'
},
{
label: '发货时间',
formatter: (value) => (value ? formatDateTime(value) : '--'),
prop: 'shipped_at'
}
]
})
}
// 其他信息
sections.push({
title: '其他信息',
fields: [
{
@@ -132,6 +155,11 @@
prop: 'remark',
fullWidth: true
},
{
label: '完成时间',
formatter: (value) => (value ? formatDateTime(value) : '--'),
prop: 'completed_at'
},
{
label: '创建时间',
formatter: (value) => formatDateTime(value),
@@ -143,8 +171,10 @@
prop: 'updated_at'
}
]
}
]
})
return sections
})
// 加载换货单详情
const loadExchangeDetail = async () => {

View File

@@ -50,6 +50,25 @@
@closed="handleCloseCreateDialog"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem label="流程类型" prop="flow_type">
<ElSelect
v-model="createForm.flow_type"
placeholder="请选择流程类型"
style="width: 100%"
@change="handleFlowTypeChange"
>
<ElOption label="物流换货" value="shipping" />
<ElOption label="直接换货" value="direct" />
</ElSelect>
<div style="margin-top: 4px; font-size: 12px; color: #909399">
<span v-if="createForm.flow_type === 'shipping'">
物流换货需要填写收货信息发货等步骤
</span>
<span v-else-if="createForm.flow_type === 'direct'">
直接换货创建后自动完成无需物流环节
</span>
</div>
</ElFormItem>
<ElFormItem label="换货原因" prop="exchange_reason">
<ElSelect
v-model="createForm.exchange_reason"
@@ -151,6 +170,74 @@
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'iot_card'"
label="新资产标识符"
prop="new_identifier"
>
<ElSelect
v-model="createForm.new_identifier"
filterable
remote
reserve-keyword
placeholder="请输入ICCID搜索"
:remote-method="searchNewIotCards"
:loading="newCardSearchLoading"
style="width: 100%"
clearable
>
<ElOption
v-for="card in newIotCardOptions"
:key="card.id"
:label="`${card.iccid} (${card.msisdn || '无接入号'})`"
:value="card.iccid"
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ card.msisdn || '无接入号' }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'device'"
label="新资产标识符"
prop="new_identifier"
>
<ElSelect
v-model="createForm.new_identifier"
filterable
remote
reserve-keyword
placeholder="请输入虚拟号或IMEI搜索"
:remote-method="searchNewDevices"
:loading="newDeviceSearchLoading"
style="width: 100%"
clearable
>
<ElOption
v-for="device in newDeviceOptions"
:key="device.id"
:label="`${device.virtual_no} (${device.device_name || device.imei})`"
:value="device.virtual_no"
>
<div style="display: flex; justify-content: space-between">
<span>{{ device.virtual_no }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ device.device_name || device.imei }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="createForm.flow_type === 'direct'" label="是否迁移数据">
<ElSwitch v-model="createForm.migrate_data" />
<div style="margin-top: 4px; margin-left: 8px; font-size: 12px; color: #909399">
开启后将迁移钱包余额套餐记录到新资产
</div>
</ElFormItem>
<ElFormItem label="备注">
<ElInput
v-model="createForm.remark"
@@ -257,6 +344,7 @@
// 搜索表单
const searchForm = reactive({
status: undefined,
flow_type: undefined, // 流程类型筛选
identifier: '',
created_at_range: [],
created_at_start: '',
@@ -269,6 +357,9 @@
exchange_reason_other: '',
old_asset_type: '',
old_identifier: '',
flow_type: 'shipping', // 流程类型,默认物流换货
new_identifier: '', // 新资产标识符direct 时必填)
migrate_data: false, // 是否迁移数据(默认 false
remark: ''
})
@@ -286,11 +377,27 @@
callback()
}
const validateNewIdentifier = (_rule: any, value: string, callback: any) => {
if (createForm.flow_type !== 'direct') {
callback()
return
}
if (!value || !value.trim()) {
callback(new Error('请输入新资产标识符'))
return
}
callback()
}
const createRules = reactive<FormRules>({
exchange_reason: [{ required: true, message: '请选择换货原因', trigger: 'change' }],
exchange_reason_other: [{ validator: validateOtherExchangeReason, trigger: 'blur' }],
old_asset_type: [{ required: true, message: '请选择旧资产类型', trigger: 'change' }],
old_identifier: [{ required: true, message: '请选择旧资产标识符', trigger: 'change' }]
old_identifier: [{ required: true, message: '请选择旧资产标识符', trigger: 'change' }],
flow_type: [{ required: true, message: '请选择流程类型', trigger: 'change' }],
new_identifier: [{ validator: validateNewIdentifier, trigger: 'blur' }]
})
// 旧资产搜索相关
@@ -299,6 +406,12 @@
const oldDeviceOptions = ref<Device[]>([])
const oldDeviceSearchLoading = ref(false)
// 新资产搜索相关(直接换货使用)
const newIotCardOptions = ref<StandaloneIotCard[]>([])
const newCardSearchLoading = ref(false)
const newDeviceOptions = ref<Device[]>([])
const newDeviceSearchLoading = ref(false)
// 发货表单
const shipForm = reactive({
new_identifier: '',
@@ -338,6 +451,19 @@
{ label: '已取消', value: 5 }
]
},
{
label: '流程类型',
prop: 'flow_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '物流换货', value: 'shipping' },
{ label: '直接换货', value: 'direct' }
]
},
{
label: '资产标识符',
prop: 'identifier',
@@ -384,6 +510,12 @@
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'flow_type',
label: '流程类型',
width: 120,
formatter: (row: any) => row.flow_type_name || (row.flow_type === 'direct' ? '直接换货' : '物流换货')
},
{
prop: 'old_asset_type',
label: '旧资产类型',
@@ -474,6 +606,9 @@
if (searchForm.status !== undefined && searchForm.status !== null) {
params.status = searchForm.status
}
if (searchForm.flow_type) {
params.flow_type = searchForm.flow_type
}
if (searchForm.identifier) {
params.identifier = searchForm.identifier
}
@@ -547,6 +682,25 @@
}
}
// 流程类型变更
const handleFlowTypeChange = async () => {
// 清空新资产标识符和数据迁移选项
createForm.new_identifier = ''
createForm.migrate_data = false
newIotCardOptions.value = []
newDeviceOptions.value = []
createFormRef.value?.clearValidate('new_identifier')
// 如果选择直接换货且已选择旧资产类型,加载默认新资产列表
if (createForm.flow_type === 'direct' && createForm.old_asset_type) {
if (createForm.old_asset_type === 'iot_card') {
await loadDefaultNewIotCards()
} else if (createForm.old_asset_type === 'device') {
await loadDefaultNewDevices()
}
}
}
// 旧资产类型变更
const handleOldAssetTypeChange = async () => {
// 清空旧资产标识符
@@ -554,11 +708,26 @@
oldIotCardOptions.value = []
oldDeviceOptions.value = []
// 清空新资产标识符(如果是直接换货)
if (createForm.flow_type === 'direct') {
createForm.new_identifier = ''
newIotCardOptions.value = []
newDeviceOptions.value = []
}
// 根据类型加载默认数据
if (createForm.old_asset_type === 'iot_card') {
await loadDefaultIotCards()
// 如果是直接换货,也加载新资产列表
if (createForm.flow_type === 'direct') {
await loadDefaultNewIotCards()
}
} else if (createForm.old_asset_type === 'device') {
await loadDefaultDevices()
// 如果是直接换货,也加载新资产列表
if (createForm.flow_type === 'direct') {
await loadDefaultNewDevices()
}
}
}
@@ -664,6 +833,102 @@
}
}
// 搜索新IoT卡直接换货使用
const searchNewIotCards = async (query: string) => {
if (!query) {
// 清空搜索时加载默认列表
await loadDefaultNewIotCards()
return
}
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
iccid: query,
page: 1,
page_size: 20
})
if (res.code === 0 && res.data) {
newIotCardOptions.value = res.data.items || []
if (newIotCardOptions.value.length === 0) {
ElMessage.info('未找到匹配的IoT卡')
}
}
} catch (error) {
console.error('搜索IoT卡失败:', error)
newIotCardOptions.value = []
} finally {
newCardSearchLoading.value = false
}
}
// 加载默认新IoT卡列表
const loadDefaultNewIotCards = async () => {
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 10
})
if (res.code === 0 && res.data) {
newIotCardOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载IoT卡列表失败:', error)
newIotCardOptions.value = []
} finally {
newCardSearchLoading.value = false
}
}
// 搜索新设备(直接换货使用)
const searchNewDevices = async (query: string) => {
if (!query) {
// 清空搜索时加载默认列表
await loadDefaultNewDevices()
return
}
newDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
keyword: query,
page: 1,
page_size: 20
})
if (res.code === 0 && res.data) {
newDeviceOptions.value = res.data.items || []
if (newDeviceOptions.value.length === 0) {
ElMessage.info('未找到匹配的设备')
}
}
} catch (error) {
console.error('搜索设备失败:', error)
newDeviceOptions.value = []
} finally {
newDeviceSearchLoading.value = false
}
}
// 加载默认新设备列表
const loadDefaultNewDevices = async () => {
newDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
page: 1,
page_size: 10
})
if (res.code === 0 && res.data) {
newDeviceOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载设备列表失败:', error)
newDeviceOptions.value = []
} finally {
newDeviceSearchLoading.value = false
}
}
// 确认创建
const handleConfirmCreate = () => {
createFormRef.value?.validate(async (valid) => {
@@ -676,12 +941,21 @@
? createForm.exchange_reason_other.trim()
: createForm.exchange_reason
const res = await ExchangeService.createExchange({
const requestData: any = {
exchange_reason: exchangeReason,
old_asset_type: createForm.old_asset_type,
old_identifier: createForm.old_identifier,
flow_type: createForm.flow_type,
remark: createForm.remark || undefined
})
}
// 直接换货时需要传递新资产标识符和数据迁移标志
if (createForm.flow_type === 'direct') {
requestData.new_identifier = createForm.new_identifier
requestData.migrate_data = createForm.migrate_data
}
const res = await ExchangeService.createExchange(requestData)
if (res.code === 0) {
ElMessage.success('换货单创建成功')
createDialogVisible.value = false
@@ -701,6 +975,8 @@
// 清空搜索选项
oldIotCardOptions.value = []
oldDeviceOptions.value = []
newIotCardOptions.value = []
newDeviceOptions.value = []
}
// 查看详情
@@ -712,7 +988,22 @@
const getActions = (row: ExchangeResponse) => {
const actions: any[] = []
const status = row.status
const flowType = row.flow_type || 'shipping' // 历史数据兼容
// 直接换货不支持任何操作(发货、确认完成、取消)
if (flowType === 'direct') {
// 状态4已完成旧资产转新direct 也支持)
if (status === 4 && hasAuth('exchange:renew')) {
actions.push({
label: '旧资产转新',
handler: () => handleRenewExchange(row),
type: 'warning'
})
}
return actions
}
// 物流换货流程
// 状态1待填写信息取消换货
if (status === 1) {
if (hasAuth('exchange:cancel')) {

View File

@@ -968,7 +968,7 @@
// 构建请求数据
const data: any = {
series_name: form.series_name,
description: form.description || undefined,
description: form.description,
enable_one_time_commission: form.one_time_commission_config.enable
}