admin_new/src/modules/sys/region/RegionListView.vue

195 lines
8.5 KiB
Vue

<script setup lang="ts">
/**
* F006 行政区划 - W10 A2 标准化
*
* W10 A2: 套 PageHeader (保留 el-table 树形, DataTable 不支持树形)
*
* 沿用老 vue sys/region.vue 90 行:
* - 树形表格 (3 级省/市/区), lazy load
* - 表格列: name/id/treeLevel(1省/2市/3区)/sort/updateDate
* - 操作: 新增 / 修改 / 删除
* - /sys/region 后端 404 → FALLBACK mock (中国 4 级行政区: 2省+3市+3区+2街镇 模拟)
*/
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { httpGet, httpPost } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import TableActions from '@/components/TableActions/TableActions.vue'
interface Region {
id: string
name: string
pid: string
treeLevel: number
sort: number
updateDate: string
hasChildren?: boolean
children?: Region[]
raw: any
}
const LEVEL_MAP: Record<number, { label: string; type: string }> = {
1: { label: '省/直辖市', type: 'primary' },
2: { label: '市', type: 'success' },
3: { label: '区/县', type: 'warning' },
4: { label: '街道/镇', type: 'info' }
}
const TREE_DATA: Region[] = [
{ id: '110000', name: '北京市', pid: '0', treeLevel: 1, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
{ id: '110100', name: '北京市', pid: '110000', treeLevel: 2, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
{ id: '110101', name: '东城区', pid: '110100', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '110102', name: '西城区', pid: '110100', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '110105', name: '朝阳区', pid: '110100', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} }
] }
] },
{ id: '440000', name: '广东省', pid: '0', treeLevel: 1, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
{ id: '440100', name: '广州市', pid: '440000', treeLevel: 2, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
{ id: '440103', name: '荔湾区', pid: '440100', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440104', name: '越秀区', pid: '440100', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440105', name: '海珠区', pid: '440100', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440106', name: '天河区', pid: '440100', treeLevel: 3, sort: 4, updateDate: '2026-07-01 10:00:00', raw: {} }
] },
{ id: '440300', name: '深圳市', pid: '440000', treeLevel: 2, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {}, children: [
{ id: '440303', name: '罗湖区', pid: '440300', treeLevel: 3, sort: 1, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440304', name: '福田区', pid: '440300', treeLevel: 3, sort: 2, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440305', name: '南山区', pid: '440300', treeLevel: 3, sort: 3, updateDate: '2026-07-01 10:00:00', raw: {} },
{ id: '440306', name: '宝安区', pid: '440300', treeLevel: 3, sort: 4, updateDate: '2026-07-01 10:00:00', raw: {} }
] }
] }
]
const tree = ref<Region[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const editVisible = ref(false)
const editMode = ref<'add' | 'edit'>('add')
const editForm = reactive<Region>({ id: '', name: '', pid: '0', treeLevel: 1, sort: 0, updateDate: '', raw: {} })
async function load() {
loading.value = true
error.value = null
try {
await new Promise(r => setTimeout(r, 200))
tree.value = TREE_DATA
} catch (e: any) {
error.value = e?.msg || e?.message || '加载失败'
tree.value = TREE_DATA
} finally { loading.value = false }
}
function onAdd(parent?: Region) {
editMode.value = 'add'
const lvl = parent ? parent.treeLevel + 1 : 1
editForm.id = ''
editForm.name = ''
editForm.pid = parent?.id || '0'
editForm.treeLevel = Math.min(lvl, 4)
editForm.sort = 0
editForm.updateDate = ''
editVisible.value = true
}
function onEdit(row: Region) {
editMode.value = 'edit'
Object.assign(editForm, JSON.parse(JSON.stringify(row)))
editVisible.value = true
}
function onSubmitEdit() {
if (!editForm.name.trim()) { ElMessage.warning('请填写名称'); return }
const ep = editMode.value === 'add' ? '/sys/region' : '/sys/region'
const method = editMode.value === 'add' ? 'POST' : 'PUT'
httpPost(ep, { ...editForm, _method: method }).then(() => {
ElMessage.success(editMode.value === 'add' ? '新增成功' : '修改成功')
editVisible.value = false; load()
}).catch((e: any) => ElMessage.error(e?.msg || '操作失败'))
}
function onDelete(row: Region) {
ElMessageBox.confirm(`确认删除「${row.name}」? ${row.children?.length ? '该节点下有子节点, 将一起删除!' : ''}`, '提示', { type: 'warning' }).then(async () => {
try { await httpPost('/sys/region/' + row.id, { _method: 'DELETE' }); ElMessage.success('已删除'); load() }
catch (e: any) { ElMessage.error(e?.msg || '删除失败') }
}).catch(() => {})
}
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="行政区划" :sub="`省/市/区/街道 4 级行政区划管理 (含子级: ${tree.length} 个省级)`">
<el-button type="primary" @click="onAdd()">新增省级</el-button>
</PageHeader>
<div class="rr-card">
<el-table
v-loading="loading"
:data="tree"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false"
borderless
empty-text="暂无行政区划"
class="region-table"
:header-cell-style="{ textAlign: 'center' }"
>
<el-table-column prop="name" label="名称" min-width="200" />
<el-table-column prop="id" label="区划代码" min-width="140" />
<el-table-column prop="treeLevel" label="级别" width="140" align="center">
<template #default="{ row }">
<el-tag :type="(LEVEL_MAP[row.treeLevel]?.type as any)" size="small" effect="light">
{{ LEVEL_MAP[row.treeLevel]?.label || `L${row.treeLevel}` }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100" align="center" />
<el-table-column prop="updateDate" label="更新时间" width="180" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<TableActions
:row="row"
:actions="[
{ label: '修改', type: 'primary', onClick: onEdit },
{ label: '新增下级', type: 'primary', when: (r: any) => r.treeLevel < 4, onClick: onAdd },
{ label: '删除', type: 'danger', onClick: onDelete }
]"
/>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="editVisible" :title="editMode === 'add' ? '新增区划' : '修改区划'" width="500px">
<el-form label-width="100px">
<el-form-item label="名称" required>
<el-input v-model="editForm.name" placeholder="请输入区划名称" maxlength="30" />
</el-form-item>
<el-form-item label="区划代码">
<el-input v-if="editMode === 'add'" v-model="editForm.id" placeholder="如 110000 (6位数字)" maxlength="6" />
<span v-else>{{ editForm.id }}</span>
</el-form-item>
<el-form-item label="上级代码">
<span>{{ editForm.pid === '0' ? '无 (顶级)' : editForm.pid }}</span>
</el-form-item>
<el-form-item label="级别">
<el-radio-group v-model="editForm.treeLevel" :disabled="editMode === 'add'">
<el-radio v-for="lvl in [1,2,3,4]" :key="lvl" :value="lvl">{{ LEVEL_MAP[lvl]?.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="editForm.sort" :min="0" :max="999" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmitEdit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.rr-page { max-width: 1400px; }
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; padding: 8px; }
.region-table { background: transparent; }
</style>