336 lines
9.5 KiB
Vue
336 lines
9.5 KiB
Vue
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import request from '../../../utils/request';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
|
||
const allData = ref([]);
|
||
const treeData = ref([]);
|
||
const loading = ref(false);
|
||
const selectedPath = ref('');
|
||
const treeRef = ref(null);
|
||
|
||
const query = reactive({ keyword: '', pageIndex: 1, pageSize: 20 });
|
||
const pagedList = ref([]);
|
||
const filteredTotal = ref(0);
|
||
|
||
const dataTypeOptions = ['string', 'int', 'decimal', 'bool', 'json', 'array'];
|
||
|
||
const dialogVisible = ref(false);
|
||
const dialogTitle = ref('新增配置项');
|
||
const isEdit = ref(false);
|
||
const editId = ref(null);
|
||
const form = reactive({
|
||
category: '',
|
||
keyName: '',
|
||
value: '',
|
||
dataType: 'string',
|
||
description: '',
|
||
isActive: true
|
||
});
|
||
|
||
// 按命名空间(反斜杠分层)构建树,并统计各节点(含子级)配置项数量
|
||
function buildTree(list) {
|
||
const roots = [];
|
||
const map = {};
|
||
const paths = [...new Set(list.map((x) => (x.category || '').trim()).filter(Boolean))].sort();
|
||
|
||
paths.forEach((path) => {
|
||
const parts = path.split('\\');
|
||
let cur = '';
|
||
parts.forEach((part, i) => {
|
||
cur = i === 0 ? part : `${cur}\\${part}`;
|
||
if (!map[cur]) {
|
||
const node = { id: cur, label: part, children: [] };
|
||
map[cur] = node;
|
||
if (i === 0) roots.push(node);
|
||
else map[parts.slice(0, i).join('\\')].children.push(node);
|
||
}
|
||
});
|
||
});
|
||
|
||
const countOf = (path) =>
|
||
list.filter((x) => x.category === path || (x.category || '').startsWith(`${path}\\`)).length;
|
||
Object.values(map).forEach((node) => {
|
||
node.label = `${node.label} (${countOf(node.id)})`;
|
||
});
|
||
|
||
return roots;
|
||
}
|
||
|
||
function applyFilter() {
|
||
let data = allData.value;
|
||
if (selectedPath.value) {
|
||
data = data.filter(
|
||
(x) => x.category === selectedPath.value || (x.category || '').startsWith(`${selectedPath.value}\\`)
|
||
);
|
||
}
|
||
const kw = (query.keyword || '').trim();
|
||
if (kw) {
|
||
data = data.filter(
|
||
(x) =>
|
||
(x.keyName || '').includes(kw) ||
|
||
(x.value || '').includes(kw) ||
|
||
(x.description || '').includes(kw) ||
|
||
(x.category || '').includes(kw)
|
||
);
|
||
}
|
||
filteredTotal.value = data.length;
|
||
const start = (query.pageIndex - 1) * query.pageSize;
|
||
pagedList.value = data.slice(start, start + query.pageSize);
|
||
}
|
||
|
||
async function loadAll() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await request.get('/sys/registry');
|
||
allData.value = res.data || [];
|
||
treeData.value = buildTree(allData.value);
|
||
applyFilter();
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleNodeClick(node) {
|
||
selectedPath.value = node.id;
|
||
query.pageIndex = 1;
|
||
applyFilter();
|
||
}
|
||
|
||
function handleClearNode() {
|
||
selectedPath.value = '';
|
||
treeRef.value?.setCurrentKey(null);
|
||
query.pageIndex = 1;
|
||
applyFilter();
|
||
}
|
||
|
||
function handleSearch() {
|
||
query.pageIndex = 1;
|
||
applyFilter();
|
||
}
|
||
|
||
function handlePageChange(page) {
|
||
query.pageIndex = page;
|
||
applyFilter();
|
||
}
|
||
|
||
function openCreate() {
|
||
isEdit.value = false;
|
||
dialogTitle.value = '新增配置项';
|
||
Object.assign(form, {
|
||
category: selectedPath.value || '',
|
||
keyName: '',
|
||
value: '',
|
||
dataType: 'string',
|
||
description: '',
|
||
isActive: true
|
||
});
|
||
dialogVisible.value = true;
|
||
}
|
||
|
||
function openEdit(row) {
|
||
isEdit.value = true;
|
||
editId.value = row.id;
|
||
dialogTitle.value = '修改配置项';
|
||
Object.assign(form, {
|
||
category: row.category,
|
||
keyName: row.keyName,
|
||
value: row.value,
|
||
dataType: row.dataType || 'string',
|
||
description: row.description || '',
|
||
isActive: row.isActive
|
||
});
|
||
dialogVisible.value = true;
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (!form.category) {
|
||
ElMessage.warning('请填写命名空间');
|
||
return;
|
||
}
|
||
if (!form.keyName) {
|
||
ElMessage.warning('请填写键名');
|
||
return;
|
||
}
|
||
const payload = {
|
||
category: form.category.trim(),
|
||
keyName: form.keyName.trim(),
|
||
value: form.value ?? '',
|
||
dataType: form.dataType,
|
||
description: form.description || '',
|
||
isActive: form.isActive
|
||
};
|
||
try {
|
||
if (isEdit.value) {
|
||
await request.put(`/sys/registry/${editId.value}`, payload);
|
||
ElMessage.success('修改成功');
|
||
} else {
|
||
await request.post('/sys/registry', payload);
|
||
ElMessage.success('保存成功');
|
||
}
|
||
dialogVisible.value = false;
|
||
loadAll();
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function handleDelete(row) {
|
||
await ElMessageBox.confirm(`确定删除配置项「${row.keyName}」吗?`, '提示', { type: 'warning' });
|
||
try {
|
||
await request.delete(`/sys/registry/${row.id}`);
|
||
ElMessage.success('删除成功');
|
||
loadAll();
|
||
} catch (e) {}
|
||
}
|
||
|
||
const currentPathLabel = computed(() => selectedPath.value || '全部配置项');
|
||
|
||
onMounted(loadAll);
|
||
</script>
|
||
|
||
<template>
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>注册表</span>
|
||
<el-button type="primary" @click="openCreate">新增配置项</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="registry-body">
|
||
<div class="tree-panel">
|
||
<div class="tree-title">
|
||
<span>命名空间</span>
|
||
<el-button size="small" link type="primary" @click="handleClearNode">全部</el-button>
|
||
</div>
|
||
<el-tree
|
||
ref="treeRef"
|
||
:data="treeData"
|
||
node-key="id"
|
||
highlight-current
|
||
default-expand-all
|
||
:props="{ label: 'label', children: 'children' }"
|
||
@node-click="handleNodeClick"
|
||
/>
|
||
</div>
|
||
|
||
<div class="table-panel">
|
||
<div class="search-bar">
|
||
<el-input
|
||
v-model="query.keyword"
|
||
placeholder="键名 / 值 / 描述 / 命名空间"
|
||
clearable
|
||
style="width: 260px"
|
||
@keyup.enter="handleSearch"
|
||
@clear="handleSearch"
|
||
/>
|
||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||
<span class="path-tip">当前:{{ currentPathLabel }}</span>
|
||
</div>
|
||
|
||
<el-table v-loading="loading" :data="pagedList" border stripe>
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column prop="keyName" label="键名" min-width="150" />
|
||
<el-table-column prop="value" label="值" min-width="220" show-overflow-tooltip />
|
||
<el-table-column prop="dataType" label="数据类型" width="100" />
|
||
<el-table-column prop="description" label="描述" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="是否启用" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.isActive ? 'success' : 'info'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="category" label="命名空间" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
|
||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
:current-page="query.pageIndex"
|
||
:page-size="query.pageSize"
|
||
:total="filteredTotal"
|
||
layout="total, prev, pager, next"
|
||
class="pager"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px">
|
||
<el-form :model="form" label-width="100px">
|
||
<el-form-item label="命名空间" required>
|
||
<el-input v-model="form.category" placeholder="如:基础数据\工种" />
|
||
</el-form-item>
|
||
<el-form-item label="键名" required>
|
||
<el-input v-model="form.keyName" placeholder="如:业务类型" />
|
||
</el-form-item>
|
||
<el-form-item label="值">
|
||
<el-input v-model="form.value" type="textarea" :rows="3" placeholder="数组类型存逗号分隔字符串;json 类型存 JSON 文本" />
|
||
</el-form-item>
|
||
<el-form-item label="数据类型">
|
||
<el-select v-model="form.dataType" style="width: 100%">
|
||
<el-option v-for="t in dataTypeOptions" :key="t" :label="t" :value="t" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="描述">
|
||
<el-input v-model="form.description" placeholder="配置说明" />
|
||
</el-form-item>
|
||
<el-form-item label="是否启用">
|
||
<el-switch v-model="form.isActive" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</el-card>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.registry-body {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
.tree-panel {
|
||
width: 240px;
|
||
flex-shrink: 0;
|
||
border-right: 1px solid var(--el-border-color-lighter);
|
||
padding-right: 8px;
|
||
max-height: 620px;
|
||
overflow: auto;
|
||
}
|
||
.tree-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
.table-panel {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.search-bar {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
.path-tip {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
}
|
||
.pager {
|
||
margin-top: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
</style>
|