Files
BMS/bms-front/src/views/data/region/index.vue
T
2026-10-29 11:30:26 +08:00

493 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:span-method="objectSpanMethod"
row-key="regionId"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Refresh"
:loading="syncLoading"
@click="handleSync"
v-hasPermi="['data:region:sync']"
>同步国家数据</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="List"
@click="openSyncLogDialog"
v-hasPermi="['data:region:log:list']"
>同步日志</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="selection.length !== 1"
@click="handleUpdate(selection[0])"
v-hasPermi="['data:region:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleDelete(ids)"
v-hasPermi="['data:region:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['data:region:list']"
>导出</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-switch
v-model="row.status"
active-value="0"
inactive-value="1"
@change="handleStatusChange(row)"
v-hasPermi="['data:region:edit']"
/>
</template>
<template #col-flagEmoji="{ row }">
<img v-if="row.flagUrl" :src="row.flagUrl" alt="" class="region-flag" />
<span v-else class="region-flag-emoji">{{ row.flagEmoji }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:region:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row.regionId)" v-hasPermi="['data:region:remove']">删除</el-button>
</template>
</basic-table>
<!-- 修改区域对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="regionRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="中文名" prop="countryNameCn">
<el-input v-model="form.countryNameCn" placeholder="请输入中文名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="英文名" prop="countryNameEn">
<el-input v-model="form.countryNameEn" placeholder="请输入英文名" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="ISO两位码" prop="countryCode2">
<el-input v-model="form.countryCode2" placeholder="如: DE" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="ISO三位码" prop="countryCode3">
<el-input v-model="form.countryCode3" placeholder="如: DEU" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="洲/大区" prop="continent">
<el-input v-model="form.continent" placeholder="如: Europe" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="子区域" prop="subRegion">
<el-input v-model="form.subRegion" placeholder="如: Western Europe" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="货币代码" prop="currencyCode">
<el-input v-model="form.currencyCode" placeholder="如: EUR" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话前缀" prop="phonePrefix">
<el-input v-model="form.phonePrefix" placeholder="如: +49" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否独立" prop="isIndependent">
<el-radio-group v-model="form.isIndependent">
<el-radio value="1">是</el-radio>
<el-radio value="0">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否欧盟" prop="isEu">
<el-radio-group v-model="form.isEu">
<el-radio value="1">是</el-radio>
<el-radio value="0">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 同步日志对话框 -->
<el-dialog title="同步日志" v-model="logDialogVisible" width="1200px" append-to-body top="8vh">
<basic-table
ref="logTableRef"
:columns="logColumns"
:search-columns="logSearchColumns"
:query="logQueryList"
row-key="logId"
:show-search="true"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleLogDelete(ids)"
v-hasPermi="['data:region:log:remove']"
>删除</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-tag :type="logStatusType(row.status)">
{{ logStatusText(row.status) }}
</el-tag>
</template>
</basic-table>
</el-dialog>
</div>
</template>
<script setup name="Region">
import { listRegion, getRegion, updateRegion, delRegion, changeRegionStatus, syncRegion } from "@/api/data/region"
import { getSyncLog, listSyncLog, delSyncLog } from "@/api/data/region-log"
import { nextTick, onBeforeUnmount, onMounted, ref, reactive, toRefs, watch, getCurrentInstance } from "vue"
const { proxy } = getCurrentInstance()
const tableRef = ref()
const open = ref(false)
const title = ref("")
const syncLoading = ref(false)
const syncLogId = ref(null)
const syncTimer = ref(null)
// ====== 同步日志对话框 ======
const logDialogVisible = ref(false)
const logTableRef = ref()
const logSearchColumns = [
{ label: "状态", prop: "status", component: "select", options: [
{ label: "待处理", value: "PENDING" },
{ label: "处理中", value: "PROCESSING" },
{ label: "成功", value: "SUCCESS" },
{ label: "失败", value: "FAILED" }
], placeholder: "状态", width: "160px" },
{ label: "创建时间", prop: "dateRange", component: "daterange", width: "320px" }
]
const logColumns = [
{ label: "日志ID", prop: "logId", align: "center", width: 80 },
{ label: "同步类型", prop: "syncType", align: "center", width: 100 },
{ label: "状态", prop: "status", align: "center", width: 100 },
{ label: "总数", prop: "totalCount", align: "center", width: 80 },
{ label: "新增数", prop: "insertCount", align: "center", width: 80 },
{ label: "更新数", prop: "updateCount", align: "center", width: 80 },
{ label: "失败数", prop: "failCount", align: "center", width: 80 },
{ label: "耗时(ms)", prop: "durationMs", align: "center", width: 100 },
{ label: "错误信息", prop: "errorMsg", align: "center", showOverflowTooltip: true },
{ label: "创建时间", prop: "createTime", align: "center", width: 180, parseTime: true }
]
function logQueryList(params) {
const p = proxy.addDateRange({ ...params }, params.dateRange)
delete p.dateRange
return listSyncLog(p)
}
function logStatusText(status) {
const map = { PENDING: "待处理", PROCESSING: "处理中", SUCCESS: "成功", FAILED: "失败" }
return map[status] || status
}
function logStatusType(status) {
const map = { PENDING: "info", PROCESSING: "warning", SUCCESS: "success", FAILED: "danger" }
return map[status] || "info"
}
function handleLogDelete(logIds) {
proxy.$modal.confirm('是否确认删除日志编号为"' + logIds + '"的数据项?').then(function () {
return delSyncLog(logIds)
}).then(() => {
logTableRef.value.refresh()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
function openSyncLogDialog() {
logDialogVisible.value = true
nextTick(() => { logTableRef.value && logTableRef.value.reload() })
}
// 搜索项配置
const searchColumns = [
{ label: "中文名", prop: "countryNameCn", component: "input", placeholder: "请输入中文名", width: "200px" },
{ label: "英文名", prop: "countryNameEn", component: "input", placeholder: "请输入英文名", width: "200px" },
{ label: "国家代码", prop: "countryCode2", component: "input", placeholder: "如: DE", width: "160px" },
{ label: "洲/大区", prop: "continent", component: "input", placeholder: "如: Europe", width: "200px" },
{ label: "状态", prop: "status", component: "select", options: [{ label: "正常", value: "0" }, { label: "停用", value: "1" }], placeholder: "状态", width: "140px" }
]
// 表格列配置
const columns = [
{ label: "区域ID", prop: "regionId", align: "center", width: 80 },
{ label: "国旗", prop: "flagEmoji", align: "center", width: 70 },
{ label: "中文名", prop: "countryNameCn", align: "center" },
{ label: "英文名", prop: "countryNameEn", align: "center" },
{ label: "国家代码", prop: "countryCode2", align: "center", width: 80 },
{ label: "ISO三位码", prop: "countryCode3", align: "center", width: 90 },
{ label: "洲/大区", prop: "continent", align: "center", width: 120 },
{ label: "子区域", prop: "subRegion", align: "center", width: 140 },
{ label: "货币", prop: "currencyCode", align: "center", width: 80 },
{ label: "电话前缀", prop: "phonePrefix", align: "center", width: 90 },
{ label: "状态", prop: "status", align: "center", width: 80 },
{ label: "操作", prop: "action", align: "center", width: 150, showInToolbar: false }
]
// 查询函数
function queryList(params) {
return listRegion(params)
}
// 单元格合并:按「洲/大区」continent 合并相同大区的行(仅作用于 continent 列)
// 关键:el-table 在 :data 引用变化时会重新调用 span-method,
// 但这里 BasicTable 内部的 tableData 是异步请求回来的,父组件拿不到其引用,
// 因此用 watch + nextTick + el-table.doLayout() 强制刷新。
const spanMap = ref([])
function rebuildSpanMap() {
const rows = tableRef.value?.getData?.() || []
const map = new Array(rows.length)
for (let i = 0; i < rows.length; i++) {
const cur = rows[i]?.continent
if (i > 0 && rows[i - 1]?.continent === cur) {
map[i] = { rowspan: 0, colspan: 0 }
} else {
let count = 1
while (i + count < rows.length && rows[i + count]?.continent === cur) count++
map[i] = { rowspan: count, colspan: 1 }
}
}
spanMap.value = map
// 强制 el-table 重新调用 span-method
tableRef.value?.getTableRef?.()?.doLayout?.()
}
function objectSpanMethod({ column, rowIndex }) {
if (column.property !== 'continent') return { rowspan: 1, colspan: 1 }
return spanMap.value[rowIndex] || { rowspan: 1, colspan: 1 }
}
// 监听 BasicTable 数据变化
// 关键:expose.data 是 ref 对象;getData() 是 ()=>tableData.value 函数
// watch 源用 getData() 函数(Vue 会调用 source getter 并比较返回值),
// 每次 tableData.value 被重新赋值(分页/搜索/刷新),返回值(新数组)变化,watch 触发
onMounted(() => {
let lastLen = -1
watch(
() => {
const ref = tableRef.value?.data
return ref?.value || []
},
(rows) => {
if (rows.length !== lastLen) {
lastLen = rows.length
nextTick(rebuildSpanMap)
}
},
{ immediate: true }
)
})
const data = reactive({
form: {},
rules: {
countryNameEn: [{ required: true, message: "英文名不能为空", trigger: "blur" }],
countryCode2: [{ required: true, message: "ISO两位码不能为空", trigger: "blur" }]
}
})
const { form, rules } = toRefs(data)
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 表单重置 */
function reset() {
form.value = {
regionId: undefined,
countryNameCn: undefined,
countryNameEn: undefined,
countryCode2: undefined,
countryCode3: undefined,
continent: undefined,
subRegion: undefined,
currencyCode: undefined,
phonePrefix: undefined,
isIndependent: "1",
isEu: "0",
remark: undefined
}
proxy.resetForm("regionRef")
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const regionId = row.regionId
getRegion(regionId).then(response => {
form.value = response.data
open.value = true
title.value = "修改区域"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["regionRef"].validate(valid => {
if (valid) {
updateRegion(form.value).then(() => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
tableRef.value.refresh()
})
}
})
}
/** 删除按钮操作 */
function handleDelete(regionIds) {
proxy.$modal.confirm('是否确认删除区域编号为"' + regionIds + '"的数据项?').then(function () {
return delRegion(regionIds)
}).then(() => {
tableRef.value.refresh()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 修改状态 */
function handleStatusChange(row) {
// 优先显示:中文名 > 英文名 > 国家代码
const displayName = row.countryNameCn || row.countryNameEn || row.countryCode2 || '';
if (row.regionId === undefined || row.regionId === null || row.regionId === '' || !displayName) {
nextTick(() => {
row.status = row.status === "0" ? "1" : "0"
})
return
}
if (row.__originalStatus !== undefined && row.__originalStatus === row.status) {
return
}
row.__originalStatus = row.status === "0" ? "1" : "0"
const text = row.status === "0" ? "启用" : "停用"
proxy.$modal.confirm(`确认要${text}「${displayName}」国家/区域吗?`).then(function () {
return changeRegionStatus(row.regionId, row.status)
}).then(() => {
row.__originalStatus = row.status
proxy.$modal.msgSuccess(text + "成功")
}).catch(function () {
const restored = row.status === "0" ? "1" : "0"
row.__originalStatus = restored
nextTick(() => { row.status = restored })
})
}
/** 同步国家数据 */
function handleSync() {
proxy.$modal.confirm('确认从第三方API同步国家数据?').then(function () {
syncLoading.value = true
return syncRegion()
}).then(res => {
syncLogId.value = res.logId
proxy.$modal.msgSuccess("同步任务已创建,正在后台执行...")
// 轮询同步状态
syncTimer.value = setInterval(() => {
getSyncLog(syncLogId.value).then(response => {
const log = response.data
if (log.status === "SUCCESS") {
clearInterval(syncTimer.value)
syncTimer.value = null
syncLoading.value = false
tableRef.value.refresh()
proxy.$modal.msgSuccess(`同步完成!总计${log.totalCount}条,新增${log.insertCount}条,更新${log.updateCount}条,失败${log.failCount}条`)
} else if (log.status === "FAILED") {
clearInterval(syncTimer.value)
syncTimer.value = null
syncLoading.value = false
proxy.$modal.msgError("同步失败:" + log.errorMsg)
}
})
}, 3000)
}).catch(() => {
syncLoading.value = false
})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download("data/region/export", {
...tableRef.value.getQueryParams()
}, `region_${new Date().getTime()}.xlsx`)
}
/** 组件卸载前清理定时器 */
onBeforeUnmount(() => {
if (syncTimer.value) {
clearInterval(syncTimer.value)
syncTimer.value = null
}
})
</script>
<style scoped>
.region-flag {
width: 30px;
height: 30px;
object-fit: contain;
display: block;
margin: 0 auto;
}
.region-flag-emoji {
font-size: 18px;
line-height: 30px;
}
</style>