fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 56s

This commit is contained in:
luo
2026-10-13 17:21:46 +08:00
parent b2a76adc9a
commit c31183205d
13 changed files with 427 additions and 18 deletions

View File

@@ -234,6 +234,9 @@
// 提供给父组件的方法
defineExpose({
clearSelection: () => tableRef.value?.clearSelection(),
toggleRowSelection: (row: any, selected?: boolean) =>
tableRef.value?.toggleRowSelection(row, selected),
// 展开所有行
expandAll: () => {
const elTable = tableRef.value

View File

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

View 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 ? '解绑成功' : '解绑失败'

View File

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

View File

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