This commit is contained in:
@@ -234,6 +234,9 @@
|
||||
|
||||
// 提供给父组件的方法
|
||||
defineExpose({
|
||||
clearSelection: () => tableRef.value?.clearSelection(),
|
||||
toggleRowSelection: (row: any, selected?: boolean) =>
|
||||
tableRef.value?.toggleRowSelection(row, selected),
|
||||
// 展开所有行
|
||||
expandAll: () => {
|
||||
const elTable = tableRef.value
|
||||
|
||||
@@ -22,3 +22,26 @@ export const getExchangeBusinessErrorMessage = (value: unknown): string | undefi
|
||||
const failure = getBusinessFailure(value)
|
||||
return failure?.msg || failure?.message
|
||||
}
|
||||
|
||||
export interface ExchangeNewAssetContext {
|
||||
asset_identifier?: string
|
||||
associated_phones?: string[] | null
|
||||
}
|
||||
|
||||
// 新资产上下文由提交时选中的资产提供,绝不回退到旧资产。
|
||||
export const getExchangePhoneAssociationQuery = (
|
||||
failure: unknown,
|
||||
newAsset: ExchangeNewAssetContext
|
||||
): { asset_identifier: string; phone: string } | undefined => {
|
||||
const response = getBusinessFailure(failure)
|
||||
if (Number(response?.code) !== 1202) return undefined
|
||||
const asset_identifier = newAsset.asset_identifier?.trim() || ''
|
||||
const responsePhone = typeof response?.data?.phone === 'string' ? response.data.phone.trim() : ''
|
||||
const phone =
|
||||
responsePhone ||
|
||||
newAsset.associated_phones
|
||||
?.find((value) => typeof value === 'string' && value.trim())
|
||||
?.trim() ||
|
||||
''
|
||||
return asset_identifier && phone ? { asset_identifier, phone } : undefined
|
||||
}
|
||||
|
||||
30
src/utils/business/phoneAssetAssociation.ts
Normal file
30
src/utils/business/phoneAssetAssociation.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { PhoneAssetAssociation } from '@/types/api/phoneAsset'
|
||||
|
||||
export interface PhoneAssetAssociationTarget {
|
||||
asset_identifier: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
// 只接受单个字符串,拒绝重复 query 参数导致的歧义,不把长 ICCID 转成数值。
|
||||
export const getPhoneAssetAssociationTarget = (
|
||||
query: Record<string, unknown>
|
||||
): PhoneAssetAssociationTarget => ({
|
||||
asset_identifier: typeof query.asset_identifier === 'string' ? query.asset_identifier.trim() : '',
|
||||
phone: typeof query.phone === 'string' ? query.phone.trim() : ''
|
||||
})
|
||||
|
||||
export const getMatchingPhoneAssetAssociations = (
|
||||
rows: PhoneAssetAssociation[],
|
||||
target: PhoneAssetAssociationTarget
|
||||
): PhoneAssetAssociation[] => {
|
||||
if (!target.asset_identifier || !target.phone) return []
|
||||
return rows.filter(
|
||||
(row) =>
|
||||
row.status === 1 &&
|
||||
row.asset_identifier === target.asset_identifier &&
|
||||
row.phone === target.phone
|
||||
)
|
||||
}
|
||||
|
||||
export const getPhoneAssetUnbindResultName = (success: boolean): string =>
|
||||
success ? '解绑成功' : '解绑失败'
|
||||
@@ -446,7 +446,9 @@
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import {
|
||||
getExchangePhoneConflictMessage,
|
||||
getExchangeBusinessErrorMessage
|
||||
getExchangeBusinessErrorMessage,
|
||||
getExchangePhoneAssociationQuery,
|
||||
type ExchangeNewAssetContext
|
||||
} from '@/utils/business/exchangeError'
|
||||
|
||||
defineOptions({ name: 'ExchangeManagement' })
|
||||
@@ -455,7 +457,11 @@
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const handleExchangeFailure = async (failure: unknown, fallback: string) => {
|
||||
const handleExchangeFailure = async (
|
||||
failure: unknown,
|
||||
fallback: string,
|
||||
newAsset: ExchangeNewAssetContext = {}
|
||||
) => {
|
||||
const conflictMessage = getExchangePhoneConflictMessage(failure)
|
||||
if (!conflictMessage) {
|
||||
const normalized = normalizeApiError(failure)
|
||||
@@ -467,6 +473,7 @@
|
||||
return
|
||||
}
|
||||
const canManageAssociations = hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.list)
|
||||
const associationQuery = getExchangePhoneAssociationQuery(failure, newAsset)
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
canManageAssociations
|
||||
@@ -481,7 +488,8 @@
|
||||
dangerouslyUseHTMLString: false
|
||||
}
|
||||
)
|
||||
if (canManageAssociations) await router.push(RoutesAlias.PhoneAssetAssociation)
|
||||
if (canManageAssociations)
|
||||
await router.push({ path: RoutesAlias.PhoneAssetAssociation, query: associationQuery })
|
||||
} catch {
|
||||
// 用户关闭提示,不执行解绑或改写换货状态。
|
||||
}
|
||||
@@ -735,6 +743,7 @@
|
||||
type ShopSelectableOption = {
|
||||
shop_id?: number | null
|
||||
shop_name?: string | null
|
||||
associated_phones?: string[]
|
||||
}
|
||||
|
||||
const getShopDisplayName = (option?: ShopSelectableOption | null) => {
|
||||
@@ -1552,6 +1561,11 @@
|
||||
createFormRef.value?.validate(async (valid) => {
|
||||
if (!valid || createLoading.value) return
|
||||
|
||||
// 提交时冻结新资产上下文;请求或提示期间改动表单也不能跳到其他资产。
|
||||
const newAsset: ExchangeNewAssetContext = {
|
||||
asset_identifier: createForm.new_identifier,
|
||||
associated_phones: [...(createSelectedNewAsset.value?.associated_phones || [])]
|
||||
}
|
||||
createLoading.value = true
|
||||
try {
|
||||
const exchangeReason =
|
||||
@@ -1583,9 +1597,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
await handleExchangeFailure(res, '换货单创建失败')
|
||||
await handleExchangeFailure(res, '换货单创建失败', newAsset)
|
||||
} catch (error) {
|
||||
await handleExchangeFailure(error, '换货单创建失败')
|
||||
await handleExchangeFailure(error, '换货单创建失败', newAsset)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
@@ -1810,6 +1824,9 @@
|
||||
const handleCompleteExchange = async (row: ExchangeResponse) => {
|
||||
if (completeLoadingId.value === row.id) return
|
||||
|
||||
const newAsset: ExchangeNewAssetContext = {
|
||||
asset_identifier: row.new_asset_identifier || undefined
|
||||
}
|
||||
completeLoadingId.value = row.id
|
||||
|
||||
try {
|
||||
@@ -1829,11 +1846,11 @@
|
||||
// 刷新失败也不能吞掉原操作的 1202 业务响应。
|
||||
await Promise.allSettled([refreshExchangeDetail(row.id), loadExchangeList()])
|
||||
if (res.code === 0) ElMessage.success('操作成功')
|
||||
else await handleExchangeFailure(res, '确认完成失败')
|
||||
else await handleExchangeFailure(res, '确认完成失败', newAsset)
|
||||
} catch (error) {
|
||||
// 仅重新读取原单状态,不乐观标记完成,错误对象可能包含完整手机号,不输出日志。
|
||||
await Promise.allSettled([refreshExchangeDetail(row.id), loadExchangeList()])
|
||||
await handleExchangeFailure(error, '确认完成失败')
|
||||
await handleExchangeFailure(error, '确认完成失败', newAsset)
|
||||
} finally {
|
||||
completeLoadingId.value = null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<ArtTableFullScreen>
|
||||
<div class="phone-asset-association-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:filter="searchForm"
|
||||
@update:filter="Object.assign(searchForm, $event)"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@@ -33,6 +34,8 @@
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="associationTableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="associationList"
|
||||
:currentPage="pagination.page"
|
||||
@@ -130,9 +133,19 @@
|
||||
<span class="text-danger">失败 {{ batchResult?.fail_count ?? 0 }} 项</span>
|
||||
</div>
|
||||
<ElTable :data="batchResult?.items || []" max-height="320" size="small" border>
|
||||
<ElTableColumn prop="asset_type" label="资产类型" width="110" />
|
||||
<ElTableColumn label="资产类型" width="110"
|
||||
><template #default="{ row }">{{
|
||||
row.asset_type === 'iot_card' ? '物联网卡' : '设备'
|
||||
}}</template></ElTableColumn
|
||||
>
|
||||
<ElTableColumn prop="asset_id" label="资产ID" width="90" />
|
||||
<ElTableColumn prop="success" label="结果" width="80" />
|
||||
<ElTableColumn label="结果" width="100"
|
||||
><template #default="{ row }"
|
||||
><ElTag :type="row.success ? 'success' : 'danger'">{{
|
||||
getPhoneAssetUnbindResultName(row.success)
|
||||
}}</ElTag></template
|
||||
></ElTableColumn
|
||||
>
|
||||
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||||
<ElTableColumn prop="reason" label="失败原因" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
@@ -207,8 +220,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
computed,
|
||||
h,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch
|
||||
} from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElTag, FormInstance, FormRules } from 'element-plus'
|
||||
import { PhoneAssetAssociationService } from '@/api/modules'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
@@ -219,6 +243,13 @@
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import ArtTable from '@/components/core/tables/ArtTable.vue'
|
||||
import {
|
||||
getPhoneAssetAssociationTarget,
|
||||
getMatchingPhoneAssetAssociations,
|
||||
getPhoneAssetUnbindResultName,
|
||||
type PhoneAssetAssociationTarget
|
||||
} from '@/utils/business/phoneAssetAssociation'
|
||||
import type {
|
||||
BatchUnbindAssetItem,
|
||||
BatchUnbindPhoneAssetAssociationResponse,
|
||||
@@ -229,6 +260,7 @@
|
||||
defineOptions({ name: 'PhoneAssetAssociation' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { hasAuth } = useAuth()
|
||||
const perms = AUGUST_PERMISSIONS.phoneAssetAssociation
|
||||
|
||||
@@ -240,6 +272,9 @@
|
||||
const loading = ref(false)
|
||||
const associationList = ref<PhoneAssetAssociation[]>([])
|
||||
const selectedRows = ref<PhoneAssetAssociation[]>([])
|
||||
const associationTableRef = ref<InstanceType<typeof ArtTable>>()
|
||||
const pendingAutoSelect = ref<PhoneAssetAssociationTarget | null>(null)
|
||||
let listRequestId = 0
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const initialSearchState = {
|
||||
@@ -391,6 +426,13 @@
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
const requestId = ++listRequestId
|
||||
const autoSelectTarget = pendingAutoSelect.value
|
||||
const canAutoSelect = canBatchUnbind.value
|
||||
associationTableRef.value?.clearSelection()
|
||||
selectedRows.value = []
|
||||
associationList.value = []
|
||||
pagination.total = 0
|
||||
loading.value = true
|
||||
try {
|
||||
syncDateRange()
|
||||
@@ -403,26 +445,43 @@
|
||||
created_at_start: toRfc3339(searchForm.created_at_start),
|
||||
created_at_end: toRfc3339(searchForm.created_at_end, true)
|
||||
})
|
||||
if (requestId !== listRequestId) return
|
||||
if (res.code === 0 && res.data) {
|
||||
associationList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
await nextTick()
|
||||
if (requestId !== listRequestId) return
|
||||
if (
|
||||
autoSelectTarget &&
|
||||
canAutoSelect &&
|
||||
canBatchUnbind.value &&
|
||||
searchForm.asset_identifier.trim() === autoSelectTarget.asset_identifier &&
|
||||
searchForm.phone.trim() === autoSelectTarget.phone
|
||||
) {
|
||||
const matches = getMatchingPhoneAssetAssociations(associationList.value, autoSelectTarget)
|
||||
matches.forEach((row) => associationTableRef.value?.toggleRowSelection(row, true))
|
||||
}
|
||||
// 只在路由进入后选一次,后续手工取消/筛选/翻页不重新选中。
|
||||
pendingAutoSelect.value = null
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取手机号资产关联列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取手机号资产关联列表失败:', error)
|
||||
if (requestId === listRequestId) ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (requestId === listRequestId) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
pendingAutoSelect.value = null
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pendingAutoSelect.value = null
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
@@ -432,11 +491,13 @@
|
||||
}
|
||||
|
||||
const handleSizeChange = (pageSize: number) => {
|
||||
pendingAutoSelect.value = null
|
||||
pagination.pageSize = pageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pendingAutoSelect.value = null
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
@@ -593,7 +654,8 @@
|
||||
batchResult.value = res.data
|
||||
batchUnbindDialogVisible.value = false
|
||||
batchResultVisible.value = true
|
||||
getTableData()
|
||||
pendingAutoSelect.value = null
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error('批量解绑失败:', error)
|
||||
} finally {
|
||||
@@ -691,9 +753,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
let pageActive = false
|
||||
const loadRouteTarget = () => {
|
||||
const target = getPhoneAssetAssociationTarget(route.query)
|
||||
Object.assign(searchForm, { ...initialSearchState, ...target, dateRange: [] })
|
||||
pagination.page = 1
|
||||
pagination.pageSize = 20
|
||||
pendingAutoSelect.value = target.asset_identifier && target.phone ? target : null
|
||||
void getTableData()
|
||||
}
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
pageActive = true
|
||||
loadRouteTarget()
|
||||
})
|
||||
onActivated(() => {
|
||||
if (pageActive) return // 首次挂载已加载。
|
||||
pageActive = true
|
||||
loadRouteTarget()
|
||||
})
|
||||
onDeactivated(() => {
|
||||
pageActive = false
|
||||
++listRequestId
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
pageActive = false
|
||||
++listRequestId
|
||||
})
|
||||
watch(
|
||||
() => [route.path, route.query.asset_identifier, route.query.phone],
|
||||
() => {
|
||||
if (pageActive && route.path === RoutesAlias.PhoneAssetAssociation) loadRouteTarget()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user