Files
f9-web-ai-test/web/src/views/base/registry/index.vue
T
2026-10-08 15:44:07 +08:00

336 lines
9.5 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.
<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>