feat: 换货新资产归属继承提示
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Change: 换货新资产归属继承提示
|
||||
|
||||
## Why
|
||||
|
||||
当前换货流程在选择新资产后,页面没有明确告知换货完成后的最终归属店铺,运营人员容易误以为可以手工选择目标店铺,进而对换货结果产生误判。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在换货创建入口、发货确认入口、换货详情页补充旧资产所属店铺和新资产继承归属店铺的展示。
|
||||
- 保持现有换货创建、发货、确认完成接口和入参语义不变,不新增目标店铺选择控件或目标店铺提交字段。
|
||||
- 扩展换货响应数据,返回 `inherited_shop_id` 和 `inherited_shop_name` 供前端统一展示最终归属店铺。
|
||||
- 在创建、发货、确认完成流程中补充提交中禁用、防重复提交、后端归属校验失败提示,以及成功后的详情刷新。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `exchange-management`
|
||||
- Related active changes: `update-exchange-flow-direct-type`(本提案建立在现有换货流程类型改造之上,需合并考虑)
|
||||
- Affected code:
|
||||
- `src/api/modules/exchange.ts`
|
||||
- `src/views/asset-management/exchange-management/index.vue`
|
||||
- `src/views/asset-management/exchange-management/detail.vue`
|
||||
- 相关旧资产/新资产搜索结果类型或映射逻辑(如需补充店铺名称展示数据)
|
||||
@@ -0,0 +1,88 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 换货归属继承上下文展示
|
||||
|
||||
系统 SHALL 在换货创建和发货确认流程中展示旧资产当前所属店铺,并在用户选定新资产后以只读方式提示换货完成后的继承归属店铺,避免用户误认为可以手工指定目标店铺。
|
||||
|
||||
#### Scenario: 创建入口展示旧资产所属店铺
|
||||
|
||||
- **GIVEN** 用户进入现有换货创建入口
|
||||
- **WHEN** 用户选择旧资产
|
||||
- **THEN** 系统 MUST 显示该旧资产当前所属店铺名称
|
||||
- **AND** 该所属店铺信息 MUST be read-only
|
||||
|
||||
#### Scenario: 选择新资产后展示继承归属提示
|
||||
|
||||
- **GIVEN** 用户正在换货创建或发货确认流程中操作
|
||||
- **WHEN** 用户选择新资产
|
||||
- **THEN** 系统 MUST 显示只读提示 `换货完成后将归属:{店铺名称}`
|
||||
- **AND** `{店铺名称}` MUST 表示该新资产换货完成后的最终归属店铺
|
||||
|
||||
#### Scenario: 页面不提供目标店铺选择控件
|
||||
|
||||
- **WHEN** 用户查看换货创建或发货确认入口
|
||||
- **THEN** 系统 MUST NOT 渲染目标店铺选择控件
|
||||
- **AND** 用户 MUST NOT 能够手工覆盖继承归属店铺
|
||||
|
||||
### Requirement: 换货归属继承接口契约
|
||||
|
||||
系统 SHALL 复用现有换货创建、发货和确认完成接口,并在换货数据模型中返回继承后的店铺标识和店铺名称,供创建结果、发货结果、完成结果和详情页统一展示。
|
||||
|
||||
#### Scenario: 创建请求保持既有字段语义
|
||||
|
||||
- **WHEN** 前端调用 `POST /api/admin/exchanges`
|
||||
- **THEN** 请求体 MUST 继续沿用旧资产、新资产、换货类型和资料迁移字段
|
||||
- **AND** 请求体 MUST NOT 新增目标店铺字段
|
||||
|
||||
#### Scenario: 发货和完成请求不新增目标店铺字段
|
||||
|
||||
- **WHEN** 前端调用 `POST /api/admin/exchanges/{id}/ship` 或 `POST /api/admin/exchanges/{id}/complete`
|
||||
- **THEN** 请求体 MUST NOT 新增目标店铺字段
|
||||
- **AND** 后端 MUST 继续按现有换货流程处理归属继承
|
||||
|
||||
#### Scenario: 换货响应返回继承店铺字段
|
||||
|
||||
- **WHEN** 前端收到换货创建、发货、确认完成结果或读取换货详情
|
||||
- **THEN** 返回的 `data` MUST 包含 `inherited_shop_id:int64` 和 `inherited_shop_name:string`
|
||||
- **AND** `inherited_shop_name` MUST 表示换货完成后新资产最终归属的店铺名称
|
||||
|
||||
### Requirement: 换货详情继承归属展示
|
||||
|
||||
系统 SHALL 在换货详情页展示继承后的店铺名称,并在关键操作成功后刷新详情数据,保证页面展示的归属信息与后端一致。
|
||||
|
||||
#### Scenario: 详情页展示继承后的店铺名称
|
||||
|
||||
- **GIVEN** 用户进入换货详情页
|
||||
- **WHEN** 详情数据加载成功
|
||||
- **THEN** 页面 MUST 展示继承后的店铺名称
|
||||
- **AND** 页面 MUST NOT 隐藏该归属字段
|
||||
|
||||
#### Scenario: 发货或确认完成成功后刷新详情
|
||||
|
||||
- **GIVEN** 用户完成换货发货或确认完成操作
|
||||
- **WHEN** 接口返回成功
|
||||
- **THEN** 系统 MUST 重新拉取该换货单详情
|
||||
- **AND** 刷新后的详情 MUST 展示最新的继承归属店铺信息
|
||||
|
||||
### Requirement: 换货归属继承错误与提交状态处理
|
||||
|
||||
系统 SHALL 在新资产归属校验失败时直接展示后端错误,并在创建、发货、确认完成提交期间禁用操作按钮,防止重复提交,同时保证失败后可以重试。
|
||||
|
||||
#### Scenario: 新资产属于其他店铺时展示后端错误
|
||||
|
||||
- **GIVEN** 用户选择了不允许用于当前换货流程的新资产
|
||||
- **WHEN** 后端返回“新资产属于其他店铺”或等价归属校验错误
|
||||
- **THEN** 系统 MUST 直接展示后端错误信息
|
||||
- **AND** 系统 MUST NOT 提供手工修改目标店铺的替代入口
|
||||
|
||||
#### Scenario: 提交期间禁用按钮防止重复提交
|
||||
|
||||
- **WHEN** 用户提交创建、发货或确认完成操作
|
||||
- **THEN** 当前操作按钮 MUST 进入禁用或 loading 状态直到请求结束
|
||||
- **AND** 在请求完成前的重复点击 MUST NOT 触发第二次提交
|
||||
|
||||
#### Scenario: 请求失败后恢复可重试状态
|
||||
|
||||
- **WHEN** 创建、发货或确认完成请求失败
|
||||
- **THEN** 系统 MUST 恢复当前操作按钮可用状态
|
||||
- **AND** 页面 MUST 保留当前已展示的资产与归属提示,便于用户修正后重试
|
||||
@@ -0,0 +1,22 @@
|
||||
## 1. API Contract
|
||||
|
||||
- [x] 1.1 更新换货数据模型,补充 `inherited_shop_id` 和 `inherited_shop_name` 响应字段,并覆盖创建、发货、确认完成及详情读取场景。
|
||||
- [x] 1.2 保持 `POST /api/admin/exchanges`、`POST /api/admin/exchanges/{id}/ship`、`POST /api/admin/exchanges/{id}/complete` 的既有入参语义,不新增目标店铺字段。
|
||||
|
||||
## 2. Exchange Create And Ship UI
|
||||
|
||||
- [x] 2.1 在换货创建入口中,选择旧资产后展示旧资产所属店铺。
|
||||
- [x] 2.2 在换货创建入口和发货确认入口中,选择新资产后展示只读提示 `换货完成后将归属:{店铺名称}`。
|
||||
- [x] 2.3 确保上述页面不新增目标店铺选择控件,也不允许手工覆盖继承归属。
|
||||
|
||||
## 3. Exchange Detail And Submission States
|
||||
|
||||
- [x] 3.1 在换货详情页展示继承后的店铺名称,并在发货或确认完成成功后重新拉取详情。
|
||||
- [x] 3.2 在创建、发货、确认完成提交期间禁用操作按钮,防止重复提交。
|
||||
- [x] 3.3 在新资产归属校验失败时展示后端错误,并在失败后恢复可重试状态。
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 验证创建、发货确认、详情三个入口均能正确展示旧资产所属店铺和继承归属店铺。
|
||||
- [x] 4.2 验证加载中、失败提示、重复提交防护和成功后详情刷新的交互状态。
|
||||
- [x] 4.3 运行相关前端校验,并执行 `openspec validate update-exchange-inherited-shop-hint --strict`。
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user