feat: 换货新资产归属继承提示

This commit is contained in:
luo
2026-07-20 10:50:23 +08:00
parent 2c8524485b
commit fac05db39a
7 changed files with 485 additions and 17 deletions

View File

@@ -54,9 +54,14 @@ export class AssetService extends BaseService {
*/
static resolveAsset(
identifier: string,
params?: AssetResolveParams
params?: AssetResolveParams,
config?: Record<string, any>
): Promise<BaseResponse<AssetResolveResponse>> {
return this.getOne<AssetResolveResponse>(`/api/admin/assets/resolve/${identifier}`, params)
return this.getOne<AssetResolveResponse>(
`/api/admin/assets/resolve/${identifier}`,
params,
config
)
}
/**

View File

@@ -47,6 +47,8 @@ export interface ExchangeResponse {
recipient_address?: string
express_company?: string
express_no?: string
inherited_shop_id?: number | null
inherited_shop_name?: string | null
remark?: string
created_at: string
updated_at: string
@@ -114,8 +116,8 @@ export class ExchangeService extends BaseService {
* POST /api/admin/exchanges/{id}/complete
* @param id 换货单ID
*/
static completeExchange(id: number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/exchanges/${id}/complete`, {})
static completeExchange(id: number): Promise<BaseResponse<ExchangeResponse>> {
return this.post<BaseResponse<ExchangeResponse>>(`/api/admin/exchanges/${id}/complete`, {})
}
/**

View File

@@ -45,6 +45,12 @@
const exchangeDetail = ref<ExchangeResponse | null>(null)
const exchangeId = ref<number>(0)
const formatExchangeShopName = (shopName?: string | null, shopId?: number | null) => {
if (shopName) return shopName
if (shopId === null || shopId === 0) return '平台'
return '--'
}
const getStatusType = (status: number): TagProps['type'] => {
const types: Record<number, TagProps['type']> = {
1: 'warning',
@@ -96,6 +102,11 @@
label: '新资产标识符',
formatter: (value) => value || '--',
prop: 'new_asset_identifier'
},
{
label: '继承归属店铺',
formatter: (_, data) =>
formatExchangeShopName(data.inherited_shop_name, data.inherited_shop_id)
}
]
})

View File

@@ -170,6 +170,9 @@
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="createOldAssetShopDisplay" label="旧资产所属店铺">
<ElInput :model-value="createOldAssetShopDisplay" readonly disabled />
</ElFormItem>
<ElFormItem
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'iot_card'"
label="新资产标识符"
@@ -232,6 +235,9 @@
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="createDirectInheritedShopHint" label="归属提示">
<div class="exchange-shop-hint">{{ createDirectInheritedShopHint }}</div>
</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">
@@ -250,7 +256,9 @@
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton @click="createDialogVisible = false" :disabled="createLoading"
>取消</ElButton
>
<ElButton type="primary" @click="handleConfirmCreate" :loading="createLoading">
确认创建
</ElButton>
@@ -267,11 +275,70 @@
@closed="handleCloseShipDialog"
>
<ElForm ref="shipFormRef" :model="shipForm" :rules="shipRules" label-width="120px">
<ElFormItem label="新资产标识符" prop="new_identifier">
<ElInput
<ElFormItem
v-if="shipAssetType === 'iot_card'"
label="新资产标识符"
prop="new_identifier"
>
<ElSelect
v-model="shipForm.new_identifier"
placeholder="请输入新资产标识符(ICCID/虚拟号/IMEI/SN)"
/>
filterable
remote
reserve-keyword
placeholder="请输入ICCID搜索"
:remote-method="searchShipNewIotCards"
: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-else-if="shipAssetType === 'device'"
label="新资产标识符"
prop="new_identifier"
>
<ElSelect
v-model="shipForm.new_identifier"
filterable
remote
reserve-keyword
placeholder="请输入虚拟号或IMEI搜索"
:remote-method="searchShipNewDevices"
: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="shipInheritedShopDisplayText" label="归属提示">
<div class="exchange-shop-hint">{{ shipInheritedShopDisplayText }}</div>
</ElFormItem>
<ElFormItem label="快递公司" prop="express_company">
<ElInput v-model="shipForm.express_company" placeholder="请输入快递公司" />
@@ -289,7 +356,7 @@
<template #footer>
<div class="dialog-footer">
<ElButton @click="shipDialogVisible = false">取消</ElButton>
<ElButton @click="shipDialogVisible = false" :disabled="shipLoading">取消</ElButton>
<ElButton type="primary" @click="handleConfirmShip" :loading="shipLoading">
确认发货
</ElButton>
@@ -411,6 +478,7 @@
const newCardSearchLoading = ref(false)
const newDeviceOptions = ref<Device[]>([])
const newDeviceSearchLoading = ref(false)
const completeLoadingId = ref<number | null>(null)
// 发货表单
const shipForm = reactive({
@@ -426,6 +494,88 @@
express_no: [{ required: true, message: '请输入快递单号', trigger: 'blur' }]
})
type ShopSelectableOption = {
shop_id?: number | null
shop_name?: string | null
}
const getShopDisplayName = (option?: ShopSelectableOption | null) => {
if (!option) return ''
if (option.shop_name) return option.shop_name
if (option.shop_id === null || option.shop_id === 0) return '平台'
return ''
}
const createSelectedOldAsset = computed<ShopSelectableOption | null>(() => {
if (!createForm.old_identifier) return null
if (createForm.old_asset_type === 'iot_card') {
return (
oldIotCardOptions.value.find((card) => card.iccid === createForm.old_identifier) || null
)
}
if (createForm.old_asset_type === 'device') {
return (
oldDeviceOptions.value.find((device) => device.virtual_no === createForm.old_identifier) ||
null
)
}
return null
})
const createSelectedNewAsset = computed<ShopSelectableOption | null>(() => {
if (!createForm.new_identifier) return null
if (createForm.old_asset_type === 'iot_card') {
return (
newIotCardOptions.value.find((card) => card.iccid === createForm.new_identifier) || null
)
}
if (createForm.old_asset_type === 'device') {
return (
newDeviceOptions.value.find((device) => device.virtual_no === createForm.new_identifier) ||
null
)
}
return null
})
const shipAssetType = computed(() => currentExchangeRow.value?.old_asset_type || '')
const shipSelectedNewAsset = computed<ShopSelectableOption | null>(() => {
if (!shipForm.new_identifier) return null
if (shipAssetType.value === 'iot_card') {
return newIotCardOptions.value.find((card) => card.iccid === shipForm.new_identifier) || null
}
if (shipAssetType.value === 'device') {
return (
newDeviceOptions.value.find((device) => device.virtual_no === shipForm.new_identifier) ||
null
)
}
return null
})
const createOldAssetShopDisplay = computed(() => getShopDisplayName(createSelectedOldAsset.value))
const createDirectInheritedShopHint = computed(() => {
const shopName = getShopDisplayName(createSelectedNewAsset.value)
return shopName ? `换货完成后将归属:${shopName}` : ''
})
const shipInheritedShopDisplayText = computed(() => {
const shopName = getShopDisplayName(shipSelectedNewAsset.value)
return shopName ? `换货完成后将归属:${shopName}` : ''
})
// 分页
const pagination = reactive({
page: 1,
@@ -732,11 +882,25 @@
}
}
const refreshExchangeDetail = async (id: number) => {
try {
const res = await ExchangeService.getExchangeDetail(id)
if (res.code === 0 && res.data) {
return res.data
}
} catch (error) {
console.error('刷新换货详情失败:', error)
}
return null
}
// 加载默认IoT卡列表
const loadDefaultIotCards = async () => {
oldCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
page: 1,
page_size: 10
})
@@ -787,6 +951,7 @@
oldCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
keyword: query,
page: 1,
page_size: 20
@@ -845,6 +1010,36 @@
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
keyword: 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
}
}
const searchShipNewIotCards = async (query: string) => {
if (!query) {
await loadDefaultShipNewIotCards()
return
}
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
status: 1,
keyword: query,
page: 1,
page_size: 20
@@ -868,6 +1063,27 @@
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
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 loadDefaultShipNewIotCards = async () => {
newCardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
is_standalone: true,
status: 1,
page: 1,
page_size: 10
})
@@ -911,6 +1127,34 @@
}
}
const searchShipNewDevices = async (query: string) => {
if (!query) {
await loadDefaultShipNewDevices()
return
}
newDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
status: 1,
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
@@ -930,10 +1174,29 @@
}
}
const loadDefaultShipNewDevices = async () => {
newDeviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
status: 1,
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) => {
if (!valid) return
if (!valid || createLoading.value) return
createLoading.value = true
try {
@@ -961,7 +1224,10 @@
ElMessage.success('换货单创建成功')
createDialogVisible.value = false
await loadExchangeList()
return
}
ElMessage.error(res.msg || '换货单创建失败')
} catch (error) {
console.error('创建换货单失败:', error)
} finally {
@@ -1081,29 +1347,44 @@
}
// 发货
const handleShipExchange = (row: ExchangeResponse) => {
const handleShipExchange = async (row: ExchangeResponse) => {
currentExchangeRow.value = row
// 重置表单
shipForm.new_identifier = ''
shipForm.express_company = ''
shipForm.express_no = ''
shipForm.migrate_data = true
newIotCardOptions.value = []
newDeviceOptions.value = []
shipDialogVisible.value = true
if (row.old_asset_type === 'iot_card') {
await loadDefaultShipNewIotCards()
} else if (row.old_asset_type === 'device') {
await loadDefaultShipNewDevices()
}
}
// 确认发货
const handleConfirmShip = () => {
shipFormRef.value?.validate(async (valid) => {
if (!valid || !currentExchangeRow.value) return
if (!valid || !currentExchangeRow.value || shipLoading.value) return
shipLoading.value = true
try {
const res = await ExchangeService.shipExchange(currentExchangeRow.value.id, shipForm)
if (res.code === 0) {
const latestDetail = await refreshExchangeDetail(currentExchangeRow.value.id)
if (latestDetail) {
currentExchangeRow.value = latestDetail
}
ElMessage.success('发货成功')
shipDialogVisible.value = false
await loadExchangeList()
return
}
ElMessage.error(res.msg || '发货失败')
} catch (error) {
console.error('发货失败:', error)
} finally {
@@ -1115,25 +1396,52 @@
// 关闭发货对话框
const handleCloseShipDialog = () => {
shipFormRef.value?.resetFields()
currentExchangeRow.value = null
newIotCardOptions.value = []
newDeviceOptions.value = []
}
// 确认完成
const handleCompleteExchange = (row: ExchangeResponse) => {
if (completeLoadingId.value === row.id) return
completeLoadingId.value = row.id
ElMessageBox.confirm('确定要确认换货完成吗?', '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
type: 'warning',
beforeClose: async (action, instance, done) => {
if (action !== 'confirm') {
done()
return
}
instance.confirmButtonLoading = true
instance.confirmButtonText = '提交中...'
try {
const res = await ExchangeService.completeExchange(row.id)
if (res.code === 0) {
ElMessage.success('操作成功')
await refreshExchangeDetail(row.id)
await loadExchangeList()
ElMessage.success('操作成功')
done()
return
}
ElMessage.error(res.msg || '确认完成失败')
} catch (error) {
console.error('确认完成失败:', error)
} finally {
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
}
}
})
.then(() => {})
.finally(() => {
completeLoadingId.value = null
})
.catch(() => {})
}
@@ -1168,5 +1476,15 @@
<style lang="scss" scoped>
.exchange-management-page {
height: 100%;
.exchange-shop-hint {
width: 100%;
padding: 10px 12px;
line-height: 1.5;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 6px;
}
}
</style>