750 lines
23 KiB
Vue
750 lines
23 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="phone-asset-association-page" id="table-full-screen">
|
||
<ArtSearchBar
|
||
v-model:filter="searchForm"
|
||
:items="searchFormItems"
|
||
:show-expand="false"
|
||
@reset="handleReset"
|
||
@search="handleSearch"
|
||
/>
|
||
|
||
<ElCard shadow="never" class="art-table-card">
|
||
<ArtTableHeader
|
||
:columnList="columnOptions"
|
||
v-model:columns="columnChecks"
|
||
@refresh="handleRefresh"
|
||
>
|
||
<template #left>
|
||
<ElButton
|
||
type="danger"
|
||
plain
|
||
:disabled="selectedRows.length === 0"
|
||
@click="openBatchUnbindDialog"
|
||
v-if="canBatchUnbind"
|
||
>
|
||
批量解绑
|
||
</ElButton>
|
||
<ElButton type="primary" plain @click="openImportDialog" v-if="canImportCreate">
|
||
CSV解绑导入
|
||
</ElButton>
|
||
<ElButton @click="goUnbindImportTasks" v-if="canImportPage">解绑导入任务</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<ArtTable
|
||
:loading="loading"
|
||
:data="associationList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
:actions="getActions"
|
||
:actionsWidth="100"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<template #default>
|
||
<ElTableColumn type="selection" width="55" />
|
||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||
</template>
|
||
</ArtTable>
|
||
</ElCard>
|
||
</div>
|
||
|
||
<!-- 单条解绑 -->
|
||
<ElDialog
|
||
v-model="unbindDialogVisible"
|
||
title="解除手机号资产关联"
|
||
width="480px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<ElForm ref="unbindFormRef" :model="unbindForm" :rules="unbindRules" label-width="90px">
|
||
<ElFormItem label="资产标识">
|
||
<span>{{ unbindForm.asset_identifier || '-' }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="手机号">
|
||
<span>{{ unbindForm.phone || '-' }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="解除原因" prop="reason">
|
||
<ElInput
|
||
v-model="unbindForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="请填写解除原因(1~500 字符)"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="二次确认" prop="confirmed">
|
||
<ElCheckbox v-model="unbindForm.confirmed">我已确认解除该关联关系</ElCheckbox>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="unbindDialogVisible = false">取消</ElButton>
|
||
<ElButton type="danger" :loading="unbindLoading" @click="confirmUnbind">确认解绑</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 批量解绑 -->
|
||
<ElDialog
|
||
v-model="batchUnbindDialogVisible"
|
||
title="批量解除手机号资产关联"
|
||
width="520px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<ElAlert
|
||
type="warning"
|
||
:closable="false"
|
||
:title="`已选择 ${selectedRows.length} 项资产(按资产类型与ID去重后执行,上限 200)`"
|
||
style="margin-bottom: 12px"
|
||
/>
|
||
<ElForm ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="90px">
|
||
<ElFormItem label="解除原因" prop="reason">
|
||
<ElInput
|
||
v-model="batchForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="请填写解除原因(1~500 字符)"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="二次确认" prop="confirmed">
|
||
<ElCheckbox v-model="batchForm.confirmed">我已确认解除上述全部关联关系</ElCheckbox>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="batchUnbindDialogVisible = false">取消</ElButton>
|
||
<ElButton type="danger" :loading="batchUnbindLoading" @click="confirmBatchUnbind">
|
||
确认批量解绑
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 批量解绑结果 -->
|
||
<ElDialog v-model="batchResultVisible" title="批量解绑结果" width="560px">
|
||
<div class="batch-result-summary">
|
||
<span>成功 {{ batchResult?.success_count ?? 0 }} 项</span>
|
||
<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 prop="asset_id" label="资产ID" width="90" />
|
||
<ElTableColumn prop="success" label="结果" width="80" />
|
||
<ElTableColumn prop="unbound_count" label="解除关系数" width="100" />
|
||
<ElTableColumn prop="reason" label="失败原因" show-overflow-tooltip />
|
||
</ElTable>
|
||
<template #footer>
|
||
<ElButton type="primary" @click="batchResultVisible = false">关闭</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- CSV 解绑导入 -->
|
||
<ElDialog
|
||
v-model="importDialogVisible"
|
||
title="CSV 解绑导入"
|
||
width="520px"
|
||
:close-on-click-modal="false"
|
||
@closed="handleImportDialogClosed"
|
||
>
|
||
<div class="import-tips">
|
||
<span>模板表头:</span>
|
||
<ElButton link type="primary" @click="downloadTemplate">下载模板</ElButton>
|
||
<div class="import-tips-detail">
|
||
资产标识,备注(备注可选);支持 UTF-8(可带 BOM),非 UTF-8 按 GBK 解码;无行数上限。
|
||
</div>
|
||
</div>
|
||
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="90px">
|
||
<ElFormItem label="CSV 文件" prop="file">
|
||
<ElUpload
|
||
ref="uploadRef"
|
||
drag
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
accept=".csv"
|
||
:on-change="handleFileChange"
|
||
:on-exceed="handleFileExceed"
|
||
>
|
||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择 .csv 文件</em></div>
|
||
</ElUpload>
|
||
</ElFormItem>
|
||
<ElFormItem label="解除原因" prop="reason">
|
||
<ElInput
|
||
v-model="importForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="请填写任务级解除原因(1~500 字符)"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="二次确认" prop="confirmed">
|
||
<ElCheckbox v-model="importForm.confirmed">我已确认按模板批量解除关联关系</ElCheckbox>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" :loading="importLoading" @click="confirmImport"
|
||
>创建导入任务</ElButton
|
||
>
|
||
</template>
|
||
</ElDialog>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import {
|
||
ElMessage,
|
||
ElTag,
|
||
FormInstance,
|
||
FormRules,
|
||
UploadFile,
|
||
UploadInstance
|
||
} from 'element-plus'
|
||
import { UploadFilled } from '@element-plus/icons-vue'
|
||
import { PhoneAssetAssociationService, StorageService } from '@/api/modules'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import { normalizeApiError } from '@/utils/business/apiError'
|
||
import { RoutesAlias } from '@/router/routesAlias'
|
||
import { AUGUST_PERMISSIONS } from '@/config/constants'
|
||
import type { SearchFormItem } from '@/types'
|
||
import type {
|
||
BatchUnbindAssetItem,
|
||
BatchUnbindPhoneAssetAssociationResponse,
|
||
PhoneAssetAssociation,
|
||
PhoneAssetAssociationStatus
|
||
} from '@/types/api'
|
||
|
||
defineOptions({ name: 'PhoneAssetAssociation' })
|
||
|
||
const router = useRouter()
|
||
const { hasAuth } = useAuth()
|
||
const perms = AUGUST_PERMISSIONS.phoneAssetAssociation
|
||
|
||
const canBatchUnbind = computed(() => hasAuth(perms.unbindBatch))
|
||
const canImportCreate = computed(() => hasAuth(perms.unbindImportCreate))
|
||
const canImportPage = computed(() => hasAuth(perms.unbindImportPage))
|
||
const canUnbind = computed(() => hasAuth(perms.unbind))
|
||
|
||
const loading = ref(false)
|
||
const associationList = ref<PhoneAssetAssociation[]>([])
|
||
const selectedRows = ref<PhoneAssetAssociation[]>([])
|
||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||
|
||
const initialSearchState = {
|
||
asset_identifier: '',
|
||
phone: '',
|
||
status: undefined as PhoneAssetAssociationStatus | undefined,
|
||
dateRange: [] as string[],
|
||
created_at_start: '',
|
||
created_at_end: ''
|
||
}
|
||
|
||
const searchForm = reactive({ ...initialSearchState })
|
||
|
||
const statusOptions = [
|
||
{ label: '有效', value: 1 as const },
|
||
{ label: '已失效', value: 0 as const }
|
||
]
|
||
|
||
const searchFormItems: SearchFormItem[] = [
|
||
{
|
||
label: '资产标识',
|
||
prop: 'asset_identifier',
|
||
type: 'input',
|
||
config: { clearable: true, placeholder: 'ICCID / 虚拟号 / IMEI / SN / 接入号(精确匹配)' }
|
||
},
|
||
{
|
||
label: '手机号',
|
||
prop: 'phone',
|
||
type: 'input',
|
||
config: { clearable: true, placeholder: '完整手机号(精确匹配)' }
|
||
},
|
||
{
|
||
label: '关联状态',
|
||
prop: 'status',
|
||
type: 'select',
|
||
config: { clearable: true, placeholder: '全部' },
|
||
options: () => statusOptions
|
||
},
|
||
{
|
||
label: '创建时间',
|
||
prop: 'dateRange',
|
||
type: 'datetimerange',
|
||
config: {
|
||
type: 'datetimerange',
|
||
rangeSeparator: '至',
|
||
startPlaceholder: '开始时间',
|
||
endPlaceholder: '结束时间',
|
||
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||
}
|
||
}
|
||
]
|
||
|
||
const columnOptions = [
|
||
{ label: '资产类型', prop: 'asset_type' },
|
||
{ label: '资产标识', prop: 'asset_identifier' },
|
||
{ label: '手机号', prop: 'phone' },
|
||
{ label: '建立时间', prop: 'established_at' },
|
||
{ label: '来源', prop: 'source' },
|
||
{ label: '状态', prop: 'status' },
|
||
{ label: '失效时间', prop: 'invalidated_at' },
|
||
{ label: '失效方式', prop: 'invalidation_method_name' },
|
||
{ label: '失效原因', prop: 'invalidation_reason' }
|
||
]
|
||
|
||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||
{
|
||
prop: 'asset_type',
|
||
label: '资产类型',
|
||
width: 100,
|
||
formatter: (row: PhoneAssetAssociation) =>
|
||
row.asset_type === 'iot_card' ? '物联网卡' : '设备'
|
||
},
|
||
{
|
||
prop: 'asset_identifier',
|
||
label: '资产标识',
|
||
minWidth: 200,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: PhoneAssetAssociation) =>
|
||
h(
|
||
'span',
|
||
{
|
||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||
onClick: () => goToAsset(row)
|
||
},
|
||
row.asset_identifier
|
||
)
|
||
},
|
||
{ prop: 'phone', label: '手机号', width: 140 },
|
||
{
|
||
prop: 'established_at',
|
||
label: '建立时间',
|
||
minWidth: 170,
|
||
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.established_at) || '-'
|
||
},
|
||
{
|
||
prop: 'source',
|
||
label: '来源',
|
||
width: 120,
|
||
formatter: () => 'H5 短信验证'
|
||
},
|
||
{
|
||
prop: 'status',
|
||
label: '状态',
|
||
width: 90,
|
||
formatter: (row: PhoneAssetAssociation) =>
|
||
h(
|
||
ElTag,
|
||
{ type: row.status === 1 ? 'success' : 'info' },
|
||
() => row.status_name || (row.status === 1 ? '有效' : '已失效')
|
||
)
|
||
},
|
||
{
|
||
prop: 'invalidated_at',
|
||
label: '失效时间',
|
||
minWidth: 170,
|
||
formatter: (row: PhoneAssetAssociation) => formatDateTime(row.invalidated_at) || '-'
|
||
},
|
||
{
|
||
prop: 'invalidation_method_name',
|
||
label: '失效方式',
|
||
width: 130,
|
||
formatter: (row: PhoneAssetAssociation) => row.invalidation_method_name || '-'
|
||
},
|
||
{
|
||
prop: 'invalidation_reason',
|
||
label: '失效原因',
|
||
minWidth: 160,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: PhoneAssetAssociation) => row.invalidation_reason || '-'
|
||
}
|
||
])
|
||
|
||
// ========== 列表数据 ==========
|
||
|
||
const syncDateRange = () => {
|
||
if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
|
||
searchForm.created_at_start = searchForm.dateRange[0] || ''
|
||
searchForm.created_at_end = searchForm.dateRange[1] || ''
|
||
} else {
|
||
searchForm.created_at_start = ''
|
||
searchForm.created_at_end = ''
|
||
}
|
||
}
|
||
|
||
const toRfc3339 = (date: string, endOfDay = false) => {
|
||
if (!date) return undefined
|
||
if (date.includes('T')) return date
|
||
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}+08:00`
|
||
}
|
||
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
syncDateRange()
|
||
const res = await PhoneAssetAssociationService.getAssociations({
|
||
page: pagination.page,
|
||
page_size: pagination.pageSize,
|
||
asset_identifier: searchForm.asset_identifier || undefined,
|
||
phone: searchForm.phone || undefined,
|
||
status: searchForm.status,
|
||
created_at_start: toRfc3339(searchForm.created_at_start),
|
||
created_at_end: toRfc3339(searchForm.created_at_end, true)
|
||
})
|
||
if (res.code === 0 && res.data) {
|
||
associationList.value = res.data.items || []
|
||
pagination.total = res.data.total || 0
|
||
} else {
|
||
ElMessage.error(res.msg || '获取手机号资产关联列表失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取手机号资产关联列表失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '获取手机号资产关联列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleReset = () => {
|
||
Object.assign(searchForm, { ...initialSearchState })
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
const handleSearch = () => {
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
const handleRefresh = () => {
|
||
getTableData()
|
||
}
|
||
|
||
const handleSizeChange = (pageSize: number) => {
|
||
pagination.pageSize = pageSize
|
||
getTableData()
|
||
}
|
||
|
||
const handleCurrentChange = (page: number) => {
|
||
pagination.page = page
|
||
getTableData()
|
||
}
|
||
|
||
const handleSelectionChange = (rows: PhoneAssetAssociation[]) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
const goToAsset = (row: PhoneAssetAssociation) => {
|
||
router.push({
|
||
path: RoutesAlias.AssetInformation,
|
||
query:
|
||
row.asset_type === 'iot_card'
|
||
? { iccid: row.asset_identifier }
|
||
: { virtual_no: row.asset_identifier }
|
||
})
|
||
}
|
||
|
||
const getActions = (row: PhoneAssetAssociation) => {
|
||
const actions: any[] = []
|
||
if (row.status === 1 && canUnbind.value) {
|
||
actions.push({ label: '解绑', handler: () => openUnbindDialog(row), type: 'danger' })
|
||
}
|
||
return actions
|
||
}
|
||
|
||
// ========== 单条解绑 ==========
|
||
|
||
const unbindDialogVisible = ref(false)
|
||
const unbindLoading = ref(false)
|
||
const unbindFormRef = ref<FormInstance>()
|
||
const unbindForm = reactive<{
|
||
id: number
|
||
asset_identifier: string
|
||
phone: string
|
||
reason: string
|
||
confirmed: boolean
|
||
}>({
|
||
id: 0,
|
||
asset_identifier: '',
|
||
phone: '',
|
||
reason: '',
|
||
confirmed: false
|
||
})
|
||
|
||
const unbindRules: FormRules = {
|
||
reason: [
|
||
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||
],
|
||
confirmed: [
|
||
{
|
||
validator: (_rule, value: boolean, callback) => {
|
||
if (!value) callback(new Error('请先勾选二次确认'))
|
||
else callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
const openUnbindDialog = (row: PhoneAssetAssociation) => {
|
||
unbindForm.id = row.id
|
||
unbindForm.asset_identifier = row.asset_identifier
|
||
unbindForm.phone = row.phone
|
||
unbindForm.reason = ''
|
||
unbindForm.confirmed = false
|
||
unbindDialogVisible.value = true
|
||
}
|
||
|
||
const confirmUnbind = async () => {
|
||
if (!(await unbindFormRef.value?.validate().catch(() => false))) return
|
||
unbindLoading.value = true
|
||
try {
|
||
const res = await PhoneAssetAssociationService.unbindAssociation(
|
||
unbindForm.id,
|
||
unbindForm.reason.trim(),
|
||
unbindForm.confirmed
|
||
)
|
||
if (res.code !== 0) {
|
||
ElMessage.error(res.msg || '解除关联失败')
|
||
return
|
||
}
|
||
ElMessage.success('解除关联成功')
|
||
unbindDialogVisible.value = false
|
||
getTableData()
|
||
} catch (error) {
|
||
console.error('解除关联失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '解除关联失败')
|
||
} finally {
|
||
unbindLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 批量解绑 ==========
|
||
|
||
const batchUnbindDialogVisible = ref(false)
|
||
const batchUnbindLoading = ref(false)
|
||
const batchFormRef = ref<FormInstance>()
|
||
const batchForm = reactive({ reason: '', confirmed: false, assets: [] as BatchUnbindAssetItem[] })
|
||
const batchResult = ref<BatchUnbindPhoneAssetAssociationResponse | null>(null)
|
||
const batchResultVisible = ref(false)
|
||
|
||
const batchRules: FormRules = {
|
||
reason: [
|
||
{ required: true, message: '请填写解除原因', trigger: 'blur' },
|
||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||
],
|
||
confirmed: [
|
||
{
|
||
validator: (_rule, value: boolean, callback) => {
|
||
if (!value) callback(new Error('请先勾选二次确认'))
|
||
else callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
const buildUniqueAssets = (rows: PhoneAssetAssociation[]): BatchUnbindAssetItem[] => {
|
||
const map = new Map<string, BatchUnbindAssetItem>()
|
||
rows.forEach((row) => {
|
||
if (row.status !== 1) return
|
||
const key = `${row.asset_type}:${row.asset_id}`
|
||
if (!map.has(key)) map.set(key, { asset_type: row.asset_type, asset_id: row.asset_id })
|
||
})
|
||
return Array.from(map.values()).slice(0, 200)
|
||
}
|
||
|
||
const openBatchUnbindDialog = () => {
|
||
batchForm.assets = buildUniqueAssets(selectedRows.value)
|
||
if (batchForm.assets.length === 0) {
|
||
ElMessage.warning('请选择有效关联的资产')
|
||
return
|
||
}
|
||
batchForm.reason = ''
|
||
batchForm.confirmed = false
|
||
batchResult.value = null
|
||
batchUnbindDialogVisible.value = true
|
||
}
|
||
|
||
const confirmBatchUnbind = async () => {
|
||
if (!(await batchFormRef.value?.validate().catch(() => false))) return
|
||
batchUnbindLoading.value = true
|
||
try {
|
||
const res = await PhoneAssetAssociationService.batchUnbind({
|
||
assets: batchForm.assets,
|
||
reason: batchForm.reason.trim(),
|
||
confirmed: batchForm.confirmed
|
||
})
|
||
if (res.code !== 0 || !res.data) {
|
||
ElMessage.error(res.msg || '批量解绑失败')
|
||
return
|
||
}
|
||
batchResult.value = res.data
|
||
batchUnbindDialogVisible.value = false
|
||
batchResultVisible.value = true
|
||
getTableData()
|
||
} catch (error) {
|
||
console.error('批量解绑失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '批量解绑失败')
|
||
} finally {
|
||
batchUnbindLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== CSV 解绑导入 ==========
|
||
|
||
const importDialogVisible = ref(false)
|
||
const importLoading = ref(false)
|
||
const uploadRef = ref<UploadInstance>()
|
||
const importFormRef = ref<FormInstance>()
|
||
const importForm = reactive({
|
||
file: null as File | null,
|
||
reason: '',
|
||
confirmed: false
|
||
})
|
||
const importRules: FormRules = {
|
||
file: [
|
||
{
|
||
validator: (_rule, _value, callback) => {
|
||
if (!importForm.file) callback(new Error('请选择 CSV 文件'))
|
||
else callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
],
|
||
reason: [
|
||
{ required: true, message: '请填写任务级解除原因', trigger: 'blur' },
|
||
{ min: 1, max: 500, message: '解除原因长度为 1~500 字符', trigger: 'blur' }
|
||
],
|
||
confirmed: [
|
||
{
|
||
validator: (_rule, value: boolean, callback) => {
|
||
if (!value) callback(new Error('请先勾选二次确认'))
|
||
else callback()
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
const openImportDialog = () => {
|
||
importForm.file = null
|
||
importForm.reason = ''
|
||
importForm.confirmed = false
|
||
importDialogVisible.value = true
|
||
}
|
||
|
||
const handleImportDialogClosed = () => {
|
||
uploadRef.value?.clearFiles()
|
||
}
|
||
|
||
const handleFileChange = (file: UploadFile) => {
|
||
const rawFile = file.raw
|
||
if (!rawFile) return
|
||
if (!rawFile.name.toLowerCase().endsWith('.csv')) {
|
||
ElMessage.error('解绑导入仅支持 .csv 文件')
|
||
uploadRef.value?.clearFiles()
|
||
importForm.file = null
|
||
return
|
||
}
|
||
importForm.file = rawFile
|
||
}
|
||
|
||
const handleFileExceed = () => {
|
||
ElMessage.warning('仅支持上传一个 CSV 文件')
|
||
}
|
||
|
||
const downloadTemplate = () => {
|
||
const content = '\uFEFF资产标识,备注\n'
|
||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8' })
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = '手机号资产解绑导入模板.csv'
|
||
link.click()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
const goUnbindImportTasks = () => {
|
||
router.push(RoutesAlias.PhoneAssetUnbindImportTasks)
|
||
}
|
||
|
||
const confirmImport = async () => {
|
||
if (!(await importFormRef.value?.validate().catch(() => false))) return
|
||
const file = importForm.file
|
||
if (!file) return
|
||
importLoading.value = true
|
||
try {
|
||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||
file_name: file.name,
|
||
content_type: 'text/csv',
|
||
purpose: 'phone_unbind_import'
|
||
})
|
||
if (uploadUrlRes.code !== 0 || !uploadUrlRes.data) {
|
||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||
return
|
||
}
|
||
const { upload_url, file_key } = uploadUrlRes.data
|
||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||
|
||
const res = await PhoneAssetAssociationService.createUnbindImport({
|
||
file_key,
|
||
reason: importForm.reason.trim(),
|
||
confirmed: importForm.confirmed
|
||
})
|
||
if (res.code !== 0 || !res.data) {
|
||
ElMessage.error(res.msg || '创建解绑导入任务失败')
|
||
return
|
||
}
|
||
ElMessage.success(`解绑导入任务已创建!任务编号:${res.data.task_no}`)
|
||
importDialogVisible.value = false
|
||
goUnbindImportTasks()
|
||
} catch (error) {
|
||
console.error('创建解绑导入任务失败:', error)
|
||
ElMessage.error(normalizeApiError(error).message || '创建解绑导入任务失败')
|
||
} finally {
|
||
importLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.phone-asset-association-page {
|
||
height: 100%;
|
||
}
|
||
|
||
.batch-result-summary {
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
|
||
.text-danger {
|
||
color: var(--el-color-danger);
|
||
margin-left: 16px;
|
||
}
|
||
}
|
||
|
||
.import-tips {
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
|
||
.import-tips-detail {
|
||
margin-top: 4px;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
</style>
|