<script setup lang="ts">
|
import { onMounted, reactive, ref } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { Download, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
import { ElMessage } from 'element-plus'
|
import { downloadTalentImportTemplate, exportTalents, getTalents, importTalents } from '../api/admin'
|
import type { Talent, TalentImportResult } from '../types/admin'
|
|
const router = useRouter()
|
const loading = ref(false)
|
const exporting = ref(false)
|
const importDialogVisible = ref(false)
|
const importing = ref(false)
|
const importFile = ref<File>()
|
const importResult = ref<TalentImportResult>()
|
const talents = ref<Talent[]>([])
|
|
const levels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
|
|
const filters = reactive({
|
keyword: '',
|
mobile: '',
|
cardNo: '',
|
level: '',
|
status: undefined as number | undefined,
|
registerRange: [] as string[],
|
})
|
|
function queryFilters() {
|
return {
|
keyword: filters.keyword || undefined,
|
mobile: filters.mobile || undefined,
|
cardNo: filters.cardNo || undefined,
|
level: filters.level || undefined,
|
status: filters.status,
|
registerStart: filters.registerRange[0] || undefined,
|
registerEnd: filters.registerRange[1] || undefined,
|
}
|
}
|
|
const pagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0,
|
})
|
|
function statusText(status?: number) {
|
return status === 0 ? '停用' : '正常'
|
}
|
|
function areaText(type?: string) {
|
return type === 'OUTSIDE' ? '外地' : '本地'
|
}
|
|
function formatDateTime(value?: string) {
|
return value ? value.replace('T', ' ') : '-'
|
}
|
|
async function loadData() {
|
loading.value = true
|
try {
|
const result = await getTalents({
|
page: pagination.page,
|
size: pagination.size,
|
...queryFilters(),
|
})
|
talents.value = result.records
|
pagination.total = result.total
|
} catch {
|
talents.value = []
|
pagination.total = 0
|
} finally {
|
loading.value = false
|
}
|
}
|
|
function search() {
|
pagination.page = 1
|
loadData()
|
}
|
|
function resetFilters() {
|
filters.keyword = ''
|
filters.mobile = ''
|
filters.cardNo = ''
|
filters.level = ''
|
filters.status = undefined
|
filters.registerRange = []
|
search()
|
}
|
|
function openCreate() {
|
router.push('/talents/new')
|
}
|
|
function openDetail(row: Talent) {
|
router.push(`/talents/${row.id}`)
|
}
|
|
function saveBlob(blob: Blob, filename: string) {
|
const url = URL.createObjectURL(blob)
|
const link = document.createElement('a')
|
link.href = url
|
link.download = filename
|
document.body.appendChild(link)
|
link.click()
|
link.remove()
|
URL.revokeObjectURL(url)
|
}
|
|
function openImport() {
|
importFile.value = undefined
|
importResult.value = undefined
|
importDialogVisible.value = true
|
}
|
|
function selectImportFile() {
|
const input = document.createElement('input')
|
input.type = 'file'
|
input.accept = '.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
input.onchange = () => {
|
const file = input.files?.[0]
|
input.remove()
|
if (!file) return
|
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
ElMessage.warning('请选择 .xlsx 格式的 Excel 文件')
|
return
|
}
|
if (file.size > 10 * 1024 * 1024) {
|
ElMessage.warning('Excel 文件不能超过 10MB')
|
return
|
}
|
importFile.value = file
|
importResult.value = undefined
|
}
|
input.click()
|
}
|
|
async function downloadImportTemplate() {
|
const blob = await downloadTalentImportTemplate()
|
saveBlob(blob, '持卡人批量导入模板.xlsx')
|
}
|
|
async function submitImport() {
|
if (!importFile.value) {
|
ElMessage.warning('请先选择需要导入的 Excel 文件')
|
return
|
}
|
importing.value = true
|
try {
|
importResult.value = await importTalents(importFile.value)
|
if (importResult.value.successCount > 0) {
|
pagination.page = 1
|
await loadData()
|
}
|
if (importResult.value.failureCount === 0) {
|
ElMessage.success(`成功导入 ${importResult.value.successCount} 条持卡人数据`)
|
} else {
|
ElMessage.warning(`导入完成:成功 ${importResult.value.successCount} 条,失败 ${importResult.value.failureCount} 条`)
|
}
|
} finally {
|
importing.value = false
|
}
|
}
|
|
async function download() {
|
exporting.value = true
|
try {
|
const blob = await exportTalents(queryFilters())
|
saveBlob(blob, `持卡人列表-${new Date().toISOString().slice(0, 10)}.xlsx`)
|
ElMessage.success('持卡人列表已下载')
|
} finally {
|
exporting.value = false
|
}
|
}
|
|
onMounted(loadData)
|
</script>
|
|
<template>
|
<div class="page-stack prototype-page talent-prototype-page">
|
<section class="panel">
|
<div class="panel-header">
|
<h3>查询表格</h3>
|
</div>
|
|
<el-form class="prototype-filter-form" :model="filters" label-width="96px">
|
<el-form-item label="持卡人">
|
<el-input v-model="filters.keyword" placeholder="请输入持卡人名称" clearable />
|
</el-form-item>
|
<el-form-item label="持卡人手机">
|
<el-input v-model="filters.mobile" placeholder="请输入持卡人手机号" clearable />
|
</el-form-item>
|
<el-form-item label="注册时间">
|
<el-date-picker
|
v-model="filters.registerRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="YYYY-MM-DD"
|
unlink-panels
|
clearable
|
class="talent-register-range"
|
/>
|
</el-form-item>
|
<el-form-item label="人才卡号">
|
<el-input v-model="filters.cardNo" placeholder="请输入人才卡号" clearable />
|
</el-form-item>
|
<el-form-item label="卡片等级">
|
<el-select v-model="filters.level" placeholder="全部" clearable>
|
<el-option label="全部" value="" />
|
<el-option v-for="level in levels" :key="level" :label="level" :value="level" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-select v-model="filters.status" placeholder="全部" clearable>
|
<el-option label="全部" :value="undefined" />
|
<el-option label="正常" :value="1" />
|
<el-option label="停用" :value="0" />
|
</el-select>
|
</el-form-item>
|
<div class="prototype-filter-actions">
|
<el-button type="primary" :icon="Search" @click="search">查询</el-button>
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
</div>
|
</el-form>
|
|
<div class="prototype-table-tools">
|
<div class="prototype-table-tools-left">
|
<el-button type="primary" :icon="Plus" @click="openCreate">新建</el-button>
|
<el-button :icon="Upload" @click="openImport">批量导入</el-button>
|
</div>
|
<el-button :icon="Download" :loading="exporting" @click="download">下载</el-button>
|
</div>
|
|
<el-table v-loading="loading" :data="talents" border class="prototype-table" empty-text="暂无持卡人数据">
|
<el-table-column type="index" label="编号" width="64" align="center" />
|
<el-table-column prop="name" label="持卡人名称" min-width="130" />
|
<el-table-column prop="mobile" label="持卡人手机号" width="140" />
|
<el-table-column prop="talentCardNo" label="人才卡号" width="130">
|
<template #default="{ row }">{{ row.talentCardNo || '-' }}</template>
|
</el-table-column>
|
<el-table-column prop="talentLevel" label="卡片等级" width="120">
|
<template #default="{ row }">{{ row.talentLevel || '-' }}</template>
|
</el-table-column>
|
<el-table-column label="外地/本地" width="100" align="center">
|
<template #default="{ row }">{{ areaText(row.sourceAreaType) }}</template>
|
</el-table-column>
|
<el-table-column prop="orderCount" label="订单数" width="92" align="center">
|
<template #default="{ row }">{{ row.orderCount || 0 }}</template>
|
</el-table-column>
|
<el-table-column label="注册时间" width="170">
|
<template #default="{ row }">{{ formatDateTime(row.registerTime) }}</template>
|
</el-table-column>
|
<el-table-column label="状态" width="86" align="center">
|
<template #default="{ row }">{{ statusText(row.status) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="90" fixed="right" align="center">
|
<template #default="{ row }">
|
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="pagination-row">
|
<el-pagination
|
v-model:current-page="pagination.page"
|
v-model:page-size="pagination.size"
|
background
|
layout="total, sizes, prev, pager, next"
|
:page-sizes="[10, 20, 50]"
|
:total="pagination.total"
|
@change="loadData"
|
/>
|
</div>
|
</section>
|
|
<el-dialog v-model="importDialogVisible" title="批量导入持卡人" width="620px" destroy-on-close>
|
<div class="talent-import-dialog">
|
<div class="talent-import-guide">
|
<strong>导入说明</strong>
|
<span>模板中的 11 项均为必填;服务专员须填写已启用的服务专员姓名。单次最多导入 1000 条,仅支持 .xlsx 文件。</span>
|
<el-button link type="primary" :icon="Download" @click="downloadImportTemplate">下载导入模板</el-button>
|
</div>
|
|
<button type="button" class="talent-import-picker" @click="selectImportFile">
|
<el-icon><Upload /></el-icon>
|
<strong>{{ importFile?.name || '选择 Excel 文件' }}</strong>
|
<span>{{ importFile ? `${(importFile.size / 1024).toFixed(1)} KB` : '文件大小不超过 10MB' }}</span>
|
</button>
|
|
<div v-if="importResult" class="talent-import-result">
|
<div class="talent-import-summary">
|
<span>成功 <strong>{{ importResult.successCount }}</strong> 条</span>
|
<span>失败 <strong class="is-error">{{ importResult.failureCount }}</strong> 条</span>
|
</div>
|
<el-table
|
v-if="importResult.failures.length"
|
:data="importResult.failures"
|
border
|
max-height="220"
|
size="small"
|
>
|
<el-table-column prop="rowNumber" label="行号" width="72" align="center" />
|
<el-table-column prop="name" label="姓名" width="130">
|
<template #default="{ row }">{{ row.name || '-' }}</template>
|
</el-table-column>
|
<el-table-column prop="reason" label="失败原因" min-width="220" />
|
</el-table>
|
</div>
|
</div>
|
|
<template #footer>
|
<el-button @click="importDialogVisible = false">关闭</el-button>
|
<el-button type="primary" :loading="importing" :disabled="!importFile" @click="submitImport">
|
确认导入
|
</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|