20260726225220
This commit is contained in:
1 parent
e3c7c6f8da
commit
a8478d60ed
170 files changed
+6786
-1678
No files matched your search
@@ -1,259 +0,0 @@
|
||||
<script setup>
|
||||
import { h, shallowRef } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Input, InputNumber, Select } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
|
||||
const COLUMN_COUNT = 20;
|
||||
const ROW_COUNT = 4000;
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: "启用", value: 1 },
|
||||
{ label: "禁用", value: 0 },
|
||||
];
|
||||
|
||||
function randInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
function randText(len) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let s = "";
|
||||
for (let i = 0; i < len; i++) s += chars[randInt(0, chars.length - 1)];
|
||||
return s;
|
||||
}
|
||||
function generateValue(c, i) {
|
||||
switch (c % 5) {
|
||||
case 0:
|
||||
return randText(randInt(4, 10));
|
||||
case 1:
|
||||
return randInt(0, 99999);
|
||||
case 2:
|
||||
return STATUS_OPTIONS[randInt(0, 1)].value;
|
||||
case 3:
|
||||
return `名称-${i}-${c}`;
|
||||
default:
|
||||
return `${randInt(1000, 9999)}-${randText(4)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildData() {
|
||||
const rows = [];
|
||||
for (let i = 0; i < ROW_COUNT; i++) {
|
||||
const row = { id: `R${i}` };
|
||||
for (let c = 1; c <= COLUMN_COUNT; c++) row[`col${c}`] = generateValue(c, i);
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const dataSource = shallowRef(buildData());
|
||||
|
||||
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
||||
const editingKey = shallowRef(null);
|
||||
const editSnapshot = shallowRef(null);
|
||||
|
||||
function editConfigFor(c) {
|
||||
switch (c % 5) {
|
||||
case 1:
|
||||
return { type: "number" };
|
||||
case 2:
|
||||
return { type: "select", options: STATUS_OPTIONS };
|
||||
default:
|
||||
return { type: "text", maxlength: 64 };
|
||||
}
|
||||
}
|
||||
|
||||
// 单元格渲染:编辑行显示控件,其他行直接返回文本值(让 stk-table 用原生 textContent 渲染,零 VNode 开销)
|
||||
function makeCellEditor(dataIndex, edit) {
|
||||
const selectOptions = edit.options || [];
|
||||
return (props) => {
|
||||
const { row, cellValue } = props;
|
||||
if (row.id !== editingKey.value) {
|
||||
// 非编辑态:直接返回文本字符串,不创建 VNode 包装
|
||||
if (edit.type === "select") {
|
||||
const opt = selectOptions.find((o) => o.value === cellValue);
|
||||
return opt ? opt.label : (cellValue ?? "");
|
||||
}
|
||||
return cellValue ?? "";
|
||||
}
|
||||
// 编辑态:返回编辑组件 VNode
|
||||
const update = (v) => {
|
||||
row[dataIndex] = v;
|
||||
};
|
||||
const common = {
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
};
|
||||
if (edit.type === "number") {
|
||||
return h(InputNumber, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
min: edit.min,
|
||||
max: edit.max,
|
||||
precision: edit.precision,
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
}
|
||||
if (edit.type === "select") {
|
||||
return h(Select, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
options: selectOptions,
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
}
|
||||
return h(Input, {
|
||||
...common,
|
||||
value: cellValue,
|
||||
maxlength: edit.maxlength,
|
||||
"onUpdate:value": update,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function buildColumns() {
|
||||
const cols = [
|
||||
{
|
||||
dataIndex: "id",
|
||||
title: "编码",
|
||||
width: 120,
|
||||
fixed: "left",
|
||||
customCell: makeCellEditor("id", { type: "text", maxlength: 32 }),
|
||||
},
|
||||
];
|
||||
for (let c = 1; c <= COLUMN_COUNT; c++) {
|
||||
const edit = editConfigFor(c);
|
||||
cols.push({
|
||||
dataIndex: `col${c}`,
|
||||
title: `列 ${c}`,
|
||||
width: 140,
|
||||
sorter: edit.type === "number" ? true : undefined,
|
||||
customCell: makeCellEditor(`col${c}`, edit),
|
||||
});
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
const columns = shallowRef(buildColumns());
|
||||
|
||||
function onRowClick(ev, row) {
|
||||
if (row.id === editingKey.value) return;
|
||||
editingKey.value = row.id;
|
||||
editSnapshot.value = JSON.parse(JSON.stringify(row));
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
if (editingKey.value == null) return;
|
||||
editingKey.value = null;
|
||||
editSnapshot.value = null;
|
||||
message.success("已保存当前行修改");
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
if (editingKey.value != null && editSnapshot.value) {
|
||||
const idx = dataSource.value.findIndex((r) => r.id === editingKey.value);
|
||||
if (idx >= 0) dataSource.value[idx] = editSnapshot.value;
|
||||
}
|
||||
editingKey.value = null;
|
||||
editSnapshot.value = null;
|
||||
message.info("已取消编辑");
|
||||
}
|
||||
|
||||
function regenerate() {
|
||||
editingKey.value = null;
|
||||
editSnapshot.value = null;
|
||||
dataSource.value = buildData();
|
||||
message.success("已重新生成随机数据");
|
||||
}
|
||||
|
||||
function rowClassName(row) {
|
||||
return row.id === editingKey.value ? "fms-row-editing" : "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="table-demo">
|
||||
<header class="table-demo__toolbar">
|
||||
<div class="table-demo__title">
|
||||
<strong>大数据表格 Demo</strong>
|
||||
<span class="table-demo__meta">
|
||||
共 {{ dataSource.length }} 行 · {{ columns.length }} 列 ·
|
||||
<template v-if="editingKey">正在编辑:{{ editingKey }}</template>
|
||||
<template v-else>点击任意行进入编辑</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-demo__actions">
|
||||
<a-button :disabled="editingKey == null" type="primary" @click="saveEdit">保存</a-button>
|
||||
<a-button :disabled="editingKey == null" @click="cancelEdit">取消</a-button>
|
||||
<a-button @click="regenerate">重新生成</a-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="table-demo__body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="dataSource"
|
||||
row-key="id"
|
||||
:row-class-name="rowClassName"
|
||||
show-overflow
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
color: var(--fms-text);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.table-demo__toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.table-demo__title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-demo__title strong {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-demo__meta {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-demo__actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-demo__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
::deep(.fms-row-editing) {
|
||||
background: rgba(22, 119, 255, 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@ import { h, shallowRef } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Input, InputNumber, Select } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FpsMonitor from "@/components/FpsMonitor.vue";
|
||||
|
||||
const COLUMN_COUNT = 20;
|
||||
const ROW_COUNT = 4000;
|
||||
@@ -192,7 +191,6 @@ function rowClassName(row) {
|
||||
</header>
|
||||
|
||||
<section class="table-demo__body">
|
||||
<FpsMonitor />
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="dataSource"
|
||||
@@ -256,7 +254,7 @@ function rowClassName(row) {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
::deep(.fms-row-editing) {
|
||||
:deep(.fms-row-editing) {
|
||||
background: rgba(22, 119, 255, 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -41,9 +41,15 @@ const form = reactive({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.reason !== "session-replaced") return;
|
||||
const reason = route.query.reason;
|
||||
if (reason !== "session-replaced" && reason !== "expired") return;
|
||||
|
||||
if (reason === "session-replaced") {
|
||||
message.warning("账号已在其他设备登录,请重新登录");
|
||||
} else {
|
||||
message.warning("登录已过期或已失效,请重新登录");
|
||||
}
|
||||
|
||||
message.warning("账号已在其他设备登录,请重新登录");
|
||||
const query = { ...route.query };
|
||||
delete query.reason;
|
||||
router.replace({ name: "login", query });
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
cloneData,
|
||||
diffRows,
|
||||
allocateTemporaryIds,
|
||||
remapForeignKeys,
|
||||
tableChange,
|
||||
hasChanges,
|
||||
} from "@/utils/dataChanges";
|
||||
@@ -22,7 +23,6 @@ const emit = defineEmits(["update:visible", "update"]);
|
||||
|
||||
const localGroups = ref([]);
|
||||
const selectedGroupId = ref(null);
|
||||
const editForm = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.groups,
|
||||
@@ -37,34 +37,29 @@ const treeData = computed(() => {
|
||||
});
|
||||
|
||||
function buildTreeData(items) {
|
||||
const map = new Map();
|
||||
const nodes = new Map();
|
||||
const roots = [];
|
||||
items.forEach((item) => {
|
||||
item.key = String(item.b_id);
|
||||
map.set(item.key, item);
|
||||
});
|
||||
items.forEach((item) => {
|
||||
if (item.b_parent_id && map.has(String(item.b_parent_id))) {
|
||||
const parent = map.get(String(item.b_parent_id));
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(item);
|
||||
|
||||
for (const item of items) {
|
||||
const key = String(item.b_id);
|
||||
nodes.set(key, {
|
||||
key,
|
||||
title: item.b_title || `分组-${item.b_id}`,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const node = nodes.get(String(item.b_id));
|
||||
const parent = item.b_parent_id ? nodes.get(String(item.b_parent_id)) : null;
|
||||
if (parent && parent !== node) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
roots.push(item);
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
return roots.map((r) => ({
|
||||
key: r.key,
|
||||
title: r.b_title || `分组-${r.b_id}`,
|
||||
children:
|
||||
r.children?.map((c) => ({
|
||||
key: c.key,
|
||||
title: c.b_title || `分组-${c.b_id}`,
|
||||
children: c.children?.map((gc) => ({
|
||||
key: gc.key,
|
||||
title: gc.b_title || `分组-${gc.b_id}`,
|
||||
})),
|
||||
})) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
const selectedGroup = computed(() =>
|
||||
@@ -74,11 +69,8 @@ const selectedGroup = computed(() =>
|
||||
function onTreeSelect(keys) {
|
||||
if (keys.length > 0) {
|
||||
selectedGroupId.value = String(keys[0]);
|
||||
const g = selectedGroup.value;
|
||||
editForm.value = g ? { ...g } : null;
|
||||
} else {
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,26 +86,33 @@ function addGroup(parentId) {
|
||||
};
|
||||
localGroups.value.push(newGroup);
|
||||
selectedGroupId.value = String(newGroup.b_id);
|
||||
editForm.value = { ...newGroup };
|
||||
}
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
if (localGroups.value.some((group) => !String(group.b_title || "").trim())) {
|
||||
message.warning("请填写分组标题");
|
||||
return;
|
||||
}
|
||||
const original = cloneData(props.groups || []);
|
||||
const changes = diffRows(localGroups.value, original);
|
||||
const req = tableChange("b_module_field_group", changes);
|
||||
const req = tableChange("s_module_field_group", changes);
|
||||
if (!hasChanges(req)) {
|
||||
message.info("没有需要保存的变更");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
const idMapping = await allocateTemporaryIds([
|
||||
req.inserts || [],
|
||||
req.updates || [],
|
||||
req.deletes || [],
|
||||
]);
|
||||
remapForeignKeys(localGroups.value, ["b_parent_id"], idMapping);
|
||||
await saveObjectApi([req]);
|
||||
emit("update", cloneData(localGroups.value));
|
||||
message.success("分组已保存");
|
||||
@@ -155,28 +154,24 @@ function onClose() {
|
||||
</FmsTree>
|
||||
<a-empty v-else description="暂无分组" />
|
||||
</div>
|
||||
<div class="group-detail" v-if="editForm">
|
||||
<div v-if="selectedGroup" class="group-detail">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="分组标题" required>
|
||||
<a-input :value="editForm.b_title" @update:value="(v) => (editForm.b_title = v)" />
|
||||
<a-input v-model:value="selectedGroup.b_title" />
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="editForm.b_i18n"
|
||||
@update:value="(v) => (editForm.b_i18n = v || null)"
|
||||
:value="selectedGroup.b_i18n"
|
||||
@update:value="(v) => (selectedGroup.b_i18n = v || null)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="editForm.b_xh"
|
||||
:min="0"
|
||||
@update:value="(v) => (editForm.b_xh = v)"
|
||||
/>
|
||||
<a-input-number v-model:value="selectedGroup.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch
|
||||
:checked="editForm.b_canuse === 1"
|
||||
@update:checked="(v) => (editForm.b_canuse = v ? 1 : 0)"
|
||||
:checked="selectedGroup.b_canuse === 1"
|
||||
@update:checked="(v) => (selectedGroup.b_canuse = v ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
|
||||
@@ -17,6 +17,7 @@ const formData = ref(null);
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
if (val === formData.value) return;
|
||||
formData.value = val ? { ...val } : null;
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -27,7 +28,7 @@ const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", { ...formData.value });
|
||||
if (formData.value) emit("update", formData.value);
|
||||
}
|
||||
|
||||
function setField(field, value) {
|
||||
@@ -37,9 +38,10 @@ function setField(field, value) {
|
||||
}
|
||||
|
||||
// 多语言键由模块编码自动生成(module.<编码>),无需手动填写。
|
||||
// 编码变化时始终以完整编码重新派生,避免只生成首字母后不再更新。
|
||||
function onCodeInput(v) {
|
||||
setField("b_code", v);
|
||||
if (!formData.value.b_i18n && v) {
|
||||
if (v) {
|
||||
formData.value.b_i18n = `module.${v}`;
|
||||
emitUpdate();
|
||||
}
|
||||
@@ -60,7 +62,7 @@ function onI18nSaved(key) {
|
||||
<template>
|
||||
<div class="basic-panel" v-if="formData">
|
||||
<div class="panel-body">
|
||||
<a-form layout="vertical" class="basic-form">
|
||||
<a-form layout="vertical" class="basic-form" :disabled="loading">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块编码" required>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref, shallowRef, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -21,16 +20,22 @@ const activeKey = ref(null);
|
||||
watch(
|
||||
() => props.editConfig,
|
||||
(val) => {
|
||||
if (val === localEdit.value) return;
|
||||
localEdit.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit(
|
||||
"update",
|
||||
localEdit.value.map((r) => ({ ...r })),
|
||||
);
|
||||
emit("update", localEdit.value);
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localEdit.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
@@ -45,21 +50,8 @@ const groupOptions = computed(() =>
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localEdit.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: localEdit.value.length * 10,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localEdit.value = localEdit.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
@@ -120,17 +112,19 @@ const columns = shallowRef([
|
||||
<div class="edit-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增行</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
<a-button :disabled="loading" @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="localEdit"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, shallowRef, watch } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
|
||||
const FIELD_TYPES = [
|
||||
@@ -25,24 +26,95 @@ const emit = defineEmits(["update", "sync"]);
|
||||
const localFields = ref([]);
|
||||
const activeKey = ref(null);
|
||||
|
||||
// 同步:未配置查询视图时,手动输入表/视图名
|
||||
const syncOpen = ref(false);
|
||||
const syncTable = ref("");
|
||||
|
||||
function onSyncClick() {
|
||||
if (props.viewTable) {
|
||||
emit("sync", props.viewTable);
|
||||
return;
|
||||
}
|
||||
syncTable.value = "";
|
||||
syncOpen.value = true;
|
||||
}
|
||||
|
||||
function onSyncConfirm() {
|
||||
const name = syncTable.value.trim();
|
||||
if (!name) {
|
||||
message.warning("请输入表或视图名");
|
||||
return;
|
||||
}
|
||||
syncOpen.value = false;
|
||||
emit("sync", name);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fields,
|
||||
(val) => {
|
||||
if (val === localFields.value) return;
|
||||
localFields.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit(
|
||||
"update",
|
||||
localFields.value.map((r) => ({ ...r })),
|
||||
);
|
||||
emit("update", localFields.value);
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
// 编辑器已直接更新当前行,这里只向父级同步草稿。
|
||||
function onCellChange() {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// 行多选:selectedRowKeys 由 FmsTable 的选择列回写;
|
||||
// 传了 rowSelection 时 FmsTable 会在最前列插入复选框列(也是单击进入编辑态所需的机制)。
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localFields.value = localFields.value.filter((r) => r.b_id !== record.b_id);
|
||||
const k = String(record.b_id);
|
||||
if (selectedRowKeys.value.includes(k)) {
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== k);
|
||||
}
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// 批量删除选中行
|
||||
function batchDelete() {
|
||||
if (props.loading) return;
|
||||
const keys = selectedRowKeys.value;
|
||||
if (!keys.length) return;
|
||||
Modal.confirm({
|
||||
title: `确定删除选中的 ${keys.length} 条记录?`,
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
const delSet = new Set(keys);
|
||||
localFields.value = localFields.value.filter((r) => !delSet.has(r.b_id));
|
||||
selectedRowKeys.value = [];
|
||||
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = "";
|
||||
emitUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
||||
{
|
||||
dataIndex: "b_name",
|
||||
title: "字段名称",
|
||||
width: 140,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
title: "控件类型",
|
||||
@@ -53,8 +125,18 @@ const columns = shallowRef([
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 120 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 140 },
|
||||
{
|
||||
dataIndex: "b_default_value",
|
||||
title: "默认值",
|
||||
width: 120,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 140,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
@@ -62,6 +144,19 @@ const columns = shallowRef([
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
actions: [
|
||||
{
|
||||
type: "delete",
|
||||
label: "删除",
|
||||
confirm: "确定删除该字段?",
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -69,18 +164,39 @@ const columns = shallowRef([
|
||||
<div class="fields-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button v-if="viewTable" type="primary" @click="emit('sync')">
|
||||
同步字段 ({{ viewTable }})
|
||||
<a-button type="primary" :disabled="loading" @click="onSyncClick">
|
||||
同步字段{{ viewTable ? ` (${viewTable})` : "" }}
|
||||
</a-button>
|
||||
<a-button danger :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
|
||||
批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
|
||||
</a-button>
|
||||
<span v-if="!viewTable" class="panel-hint">未配置查询视图,点击后手动输入表名</span>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:open="syncOpen"
|
||||
title="同步字段"
|
||||
@ok="onSyncConfirm"
|
||||
:confirm-loading="loading"
|
||||
>
|
||||
<p>当前模块未配置查询视图,请填写要同步字段的表或视图名:</p>
|
||||
<a-input
|
||||
v-model:value="syncTable"
|
||||
placeholder="例如 s_log_login 或 v_biz"
|
||||
@press-enter="onSyncConfirm"
|
||||
/>
|
||||
</a-modal>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="localFields"
|
||||
:editable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
:row-selection="rowSelection"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="onCellChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FpsMonitor from "@/components/FpsMonitor.vue";
|
||||
import { loadDataApi } from "@/services/api";
|
||||
import {
|
||||
cloneData,
|
||||
@@ -86,6 +85,7 @@ async function loadI18n() {
|
||||
watch(() => props.moduleId, loadI18n, { immediate: true });
|
||||
|
||||
async function addRow() {
|
||||
if (props.loading || isLoading.value) return;
|
||||
const newId = createTempId();
|
||||
// 用新引用替换(而非 push),否则 displayedData 这个 computed 因返回同一数组引用而不会触发重渲染
|
||||
pivotRows.value = [...pivotRows.value, { _id: newId, b_key: "", _localeIds: {} }];
|
||||
@@ -96,6 +96,7 @@ async function addRow() {
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading || isLoading.value) return;
|
||||
pivotRows.value = pivotRows.value.filter((r) => r._id !== record._id);
|
||||
// 同步清理选择状态,避免选中项被删后 keys 残留
|
||||
const k = String(record._id);
|
||||
@@ -116,6 +117,7 @@ const rowSelection = computed(() => ({
|
||||
|
||||
// 批量删除选中行(整条 key,含其全部语言)
|
||||
function batchDelete() {
|
||||
if (props.loading || isLoading.value) return;
|
||||
const keys = selectedRowKeys.value;
|
||||
if (!keys.length) return;
|
||||
Modal.confirm({
|
||||
@@ -189,9 +191,13 @@ const columns = computed(() => {
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
type: "actions",
|
||||
actions: [
|
||||
{ type: "delete", label: "删除", confirm: "确定删除该记录(含其全部语言)?", onClick: (row) => deleteRow(row) },
|
||||
{
|
||||
type: "delete",
|
||||
label: "删除",
|
||||
confirm: "确定删除该记录(含其全部语言)?",
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
});
|
||||
return cols;
|
||||
@@ -204,22 +210,29 @@ const columns = computed(() => {
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model:value="searchKey"
|
||||
:disabled="loading || isLoading"
|
||||
placeholder="搜索键"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button type="primary" @click="addRow">新增翻译</a-button>
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="batchDelete">
|
||||
<a-button type="primary" :disabled="loading || isLoading" @click="addRow"
|
||||
>新增翻译</a-button
|
||||
>
|
||||
<a-button
|
||||
danger
|
||||
:disabled="loading || isLoading || !selectedRowKeys.length"
|
||||
@click="batchDelete"
|
||||
>
|
||||
批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FpsMonitor />
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data-source="displayedData"
|
||||
:editable="!loading && !isLoading"
|
||||
row-key="_id"
|
||||
:active-row-key="activeKey"
|
||||
:row-selection="rowSelection"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref, shallowRef, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -21,16 +20,22 @@ const activeKey = ref(null);
|
||||
watch(
|
||||
() => props.listConfig,
|
||||
(val) => {
|
||||
if (val === localList.value) return;
|
||||
localList.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit(
|
||||
"update",
|
||||
localList.value.map((r) => ({ ...r })),
|
||||
);
|
||||
emit("update", localList.value);
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localList.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
@@ -45,23 +50,8 @@ const groupOptions = computed(() =>
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localList.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: null,
|
||||
b_visible: 1,
|
||||
b_xh: localList.value.length * 10,
|
||||
b_width: null,
|
||||
b_sortable: 0,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localList.value = localList.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
@@ -77,7 +67,6 @@ const columns = shallowRef([
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_title", title: "列标题", width: 120 },
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
@@ -96,13 +85,6 @@ const columns = shallowRef([
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
@@ -125,17 +107,19 @@ const columns = shallowRef([
|
||||
<div class="list-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增列</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
<a-button :disabled="loading" @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="localList"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
|
||||
@@ -3,7 +3,6 @@ import { computed, h, nextTick, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FpsMonitor from "@/components/FpsMonitor.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
@@ -23,7 +22,8 @@ const tableRef = ref(null);
|
||||
// 多语言键按 power.<模块代码>.<操作编码> 自动生成,无需手动填写。
|
||||
watch(
|
||||
() => [props.powers, props.moduleCode],
|
||||
([val]) => {
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localPowers.value && moduleCode === previous[1]) return;
|
||||
localPowers.value = (val || []).map((r) => ({
|
||||
...r,
|
||||
b_i18n:
|
||||
@@ -34,13 +34,11 @@ watch(
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit(
|
||||
"update",
|
||||
localPowers.value.map((r) => ({ ...r })),
|
||||
);
|
||||
emit("update", localPowers.value);
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
if (props.loading) return;
|
||||
const newId = createTempId();
|
||||
localPowers.value.push({
|
||||
b_id: newId,
|
||||
@@ -59,6 +57,7 @@ async function addRow() {
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localPowers.value = localPowers.value.filter((r) => r.b_id !== record.b_id);
|
||||
// 同步清理选择状态,避免选中项被删后 keys 残留
|
||||
const k = String(record.b_id);
|
||||
@@ -80,6 +79,7 @@ const rowSelection = computed(() => ({
|
||||
|
||||
// 批量删除选中行
|
||||
function batchDelete() {
|
||||
if (props.loading) return;
|
||||
const keys = selectedRowKeys.value;
|
||||
if (!keys.length) return;
|
||||
Modal.confirm({
|
||||
@@ -97,47 +97,9 @@ function batchDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── 编辑状态管理 ──
|
||||
// 通过 row._editing = { mode: "cell", col: dataIndex } 控制单元格编辑。
|
||||
// area-selection-change 统一响应鼠标点击和键盘方向键,无需额外 row-click 处理。
|
||||
// 编辑状态变更后需触发表格重渲染。
|
||||
function handleEditEnd() {
|
||||
let hasEditing = false;
|
||||
localPowers.value.forEach((r) => {
|
||||
if (r._editing) {
|
||||
delete r._editing;
|
||||
hasEditing = true;
|
||||
}
|
||||
});
|
||||
if (hasEditing) localPowers.value = [...localPowers.value];
|
||||
}
|
||||
|
||||
function handleAreaSelectionChange(ranges) {
|
||||
if (!ranges?.length) {
|
||||
handleEditEnd();
|
||||
return;
|
||||
}
|
||||
const begin = ranges[0].index.begin;
|
||||
const row = localPowers.value[begin.row];
|
||||
const area = tableRef.value?.getSelectedArea();
|
||||
if (!row || !area) return;
|
||||
|
||||
const col = area.cols[begin.col];
|
||||
if (!col || col.type !== "input") {
|
||||
handleEditEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
const rowId = String(row.b_id);
|
||||
// 目标未变则跳过,避免方向键连续触发全表重渲染
|
||||
const cur = row._editing;
|
||||
if (cur && cur.mode === "cell" && cur.col === col.dataIndex && cur.rowId === rowId) return;
|
||||
|
||||
localPowers.value.forEach((r) => {
|
||||
if (r._editing) delete r._editing;
|
||||
});
|
||||
row._editing = { mode: "cell", col: col.dataIndex, rowId };
|
||||
localPowers.value = [...localPowers.value];
|
||||
// 编辑器已直接更新当前行,这里只向父级同步草稿。
|
||||
function onCellChange() {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// ── 快速设置多语言 ──
|
||||
@@ -197,14 +159,29 @@ const I18nKeyCell = ({ row, cellValue }) =>
|
||||
);
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160, customCell: I18nKeyCell },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, type: "input", onChange: () => emitUpdate(), _onEditEnd: handleEditEnd },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, type: "checkbox", onChange: () => emitUpdate() },
|
||||
{ dataIndex: "actions", title: "操作", width: 60, type: "actions", actions: [
|
||||
{ type: "delete", label: "删除", confirm: "确定删除该记录?", onClick: (row) => deleteRow(row) },
|
||||
] },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, editor: { type: "input" } },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
actions: [
|
||||
{
|
||||
type: "delete",
|
||||
label: "删除",
|
||||
confirm: "确定删除该记录?",
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -212,24 +189,23 @@ const columns = ref([
|
||||
<div class="powers-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增权限</a-button>
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="batchDelete">
|
||||
<a-button type="primary" :disabled="loading" @click="addRow">新增权限</a-button>
|
||||
<a-button danger :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
|
||||
批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FpsMonitor />
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
v-model:columns="columns"
|
||||
:data-source="localPowers"
|
||||
:editable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
:row-selection="rowSelection"
|
||||
:area-selection="true"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@area-selection-change="handleAreaSelectionChange"
|
||||
@change="onCellChange"
|
||||
/>
|
||||
</div>
|
||||
<I18nQuickSetModal ref="i18nModalRef" />
|
||||
@@ -263,22 +239,4 @@ const columns = ref([
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 编辑态 input 样式
|
||||
:deep(.fms-editable-input) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #1677ff;
|
||||
border-radius: 4px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
|
||||
&:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -32,16 +32,22 @@ const activeKey = ref(null);
|
||||
watch(
|
||||
() => props.queryConfig,
|
||||
(val) => {
|
||||
if (val === localQuery.value) return;
|
||||
localQuery.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit(
|
||||
"update",
|
||||
localQuery.value.map((r) => ({ ...r })),
|
||||
);
|
||||
emit("update", localQuery.value);
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localQuery.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
@@ -51,6 +57,7 @@ const availableFields = computed(() =>
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
if (props.loading) return;
|
||||
localQuery.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
@@ -65,6 +72,7 @@ function addRow() {
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localQuery.value = localQuery.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
@@ -130,16 +138,19 @@ const columns = shallowRef([
|
||||
<div class="query-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增查询条件</a-button>
|
||||
<a-button type="primary" :disabled="loading" @click="addRow">新增查询条件</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="localQuery"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +72,13 @@ const drafts = ref({
|
||||
const configLoading = ref(false);
|
||||
const configSaving = ref(false);
|
||||
const i18nPanelRef = ref(null);
|
||||
let configRequestId = 0;
|
||||
|
||||
// ref 写在 v-for 内部会被 Vue 收集为数组,这里统一取单个组件实例
|
||||
function getI18nPanel() {
|
||||
const r = i18nPanelRef.value;
|
||||
return Array.isArray(r) ? r[0] : r;
|
||||
}
|
||||
|
||||
// 当前选中模块对象(从 drafts.module 获取)
|
||||
const selectedModule = computed(() => drafts.value.module);
|
||||
@@ -138,7 +145,7 @@ function buildTree(modules) {
|
||||
async function loadTree() {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const res = await loadDataApi("b_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
const res = await loadDataApi("s_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
treeData.value = buildTree(res.data || []);
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
@@ -146,19 +153,19 @@ async function loadTree() {
|
||||
}
|
||||
|
||||
// ---- 模块配置加载 ----
|
||||
async function loadModuleConfig(moduleId) {
|
||||
async function loadModuleConfig(moduleId, requestId) {
|
||||
configLoading.value = true;
|
||||
try {
|
||||
const id = String(moduleId);
|
||||
const [moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, powersRes] =
|
||||
await Promise.all([
|
||||
loadDataApi("b_module", `b_id = ${id}`),
|
||||
loadDataApi("b_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
||||
loadDataApi("b_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("b_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("b_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("b_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("b_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module", `b_id = ${id}`),
|
||||
loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
|
||||
const original = {
|
||||
@@ -170,11 +177,16 @@ async function loadModuleConfig(moduleId) {
|
||||
queryConfig: queryRes.data || [],
|
||||
powers: powersRes.data || [],
|
||||
};
|
||||
if (requestId !== configRequestId) return;
|
||||
originals.value = original;
|
||||
drafts.value = cloneData(original);
|
||||
activeTab.value = "basic";
|
||||
} catch (e) {
|
||||
if (requestId === configRequestId) {
|
||||
message.error("模块配置加载失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
configLoading.value = false;
|
||||
if (requestId === configRequestId) configLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,12 +198,17 @@ function onTreeSelect(keys) {
|
||||
}
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
if (!id) return;
|
||||
if (skipLoad.value) {
|
||||
skipLoad.value = false;
|
||||
const requestId = ++configRequestId;
|
||||
if (!id) {
|
||||
configLoading.value = false;
|
||||
return;
|
||||
}
|
||||
loadModuleConfig(id);
|
||||
if (skipLoad.value) {
|
||||
skipLoad.value = false;
|
||||
configLoading.value = false;
|
||||
return;
|
||||
}
|
||||
void loadModuleConfig(id, requestId);
|
||||
});
|
||||
|
||||
// ---- 子面板数据变更 ----
|
||||
@@ -314,7 +331,7 @@ async function onDeleteNodeById(moduleId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveObjectApi([{ table: "b_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
||||
await saveObjectApi([{ table: "s_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
||||
message.success("模块已删除");
|
||||
if (selectedModuleId.value === moduleId) {
|
||||
selectedModuleId.value = null;
|
||||
@@ -334,13 +351,13 @@ async function saveAll() {
|
||||
try {
|
||||
const saveReqs = [];
|
||||
|
||||
// b_module 主表
|
||||
// s_module 主表
|
||||
const moduleData = drafts.value.module;
|
||||
const moduleOriginal = originals.value.module;
|
||||
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
saveReqs.push({
|
||||
table: "b_module",
|
||||
table: "s_module",
|
||||
key_field: "b_id",
|
||||
[isNew ? "inserts" : "updates"]: [moduleData],
|
||||
});
|
||||
@@ -348,11 +365,11 @@ async function saveAll() {
|
||||
|
||||
// 子表:fields, listConfig, editConfig, queryConfig, powers
|
||||
const subTables = [
|
||||
["b_module_field", "fields"],
|
||||
["b_module_field_list", "listConfig"],
|
||||
["b_module_field_edit", "editConfig"],
|
||||
["b_module_field_query", "queryConfig"],
|
||||
["b_module_power", "powers"],
|
||||
["s_module_field", "fields"],
|
||||
["s_module_field_list", "listConfig"],
|
||||
["s_module_field_edit", "editConfig"],
|
||||
["s_module_field_query", "queryConfig"],
|
||||
["s_module_power", "powers"],
|
||||
];
|
||||
for (const [table, section] of subTables) {
|
||||
const current = drafts.value[section] || [];
|
||||
@@ -366,8 +383,9 @@ async function saveAll() {
|
||||
}
|
||||
|
||||
// 多语言:由各子面板收集变更,统一在此保存
|
||||
if (i18nPanelRef.value) {
|
||||
const i18nReq = await i18nPanelRef.value.buildSaveRequest();
|
||||
const i18nPanel = getI18nPanel();
|
||||
if (i18nPanel) {
|
||||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||||
if (i18nReq) saveReqs.push(i18nReq);
|
||||
}
|
||||
|
||||
@@ -380,7 +398,7 @@ async function saveAll() {
|
||||
|
||||
// 更新 originals
|
||||
originals.value = cloneData(drafts.value);
|
||||
i18nPanelRef.value?.markSaved();
|
||||
getI18nPanel()?.markSaved();
|
||||
message.success("保存成功");
|
||||
await loadTree();
|
||||
} catch (e) {
|
||||
@@ -390,7 +408,7 @@ async function saveAll() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 字段同步 ----
|
||||
// ---- 字段同步与默认配置 ----
|
||||
function typeFromSqlType(sqlType) {
|
||||
if (!sqlType) return "input";
|
||||
const t = sqlType.toLowerCase();
|
||||
@@ -419,16 +437,109 @@ function typeFromSqlType(sqlType) {
|
||||
return "input";
|
||||
}
|
||||
|
||||
async function syncFields() {
|
||||
function guessFieldWidth(fieldName) {
|
||||
const n = (fieldName || "").toLowerCase();
|
||||
if (n === "b_id" || n.endsWith("_id")) return 100;
|
||||
if (n.includes("time") || n.includes("date") || n.includes("_at")) return 160;
|
||||
if (n.includes("name")) return 150;
|
||||
if (n.includes("code") || n.includes("type")) return 120;
|
||||
if (n.includes("ip")) return 140;
|
||||
if (n.includes("status") || n.includes("state") || n.includes("result")) return 90;
|
||||
return 120;
|
||||
}
|
||||
|
||||
function guessQueryComponent(b_type) {
|
||||
const t = (b_type || "").toLowerCase();
|
||||
if (t === "date" || t === "datetime") return "date-picker";
|
||||
if (t === "select" || t === "switch") return "select";
|
||||
if (t === "number" || t === "money") return "number-range";
|
||||
return "input";
|
||||
}
|
||||
|
||||
function guessQueryOperator(b_type) {
|
||||
const t = (b_type || "").toLowerCase();
|
||||
if (t === "datetime" || t === "number" || t === "money") return "between";
|
||||
return "=";
|
||||
}
|
||||
|
||||
function configuredFieldIds(section) {
|
||||
return new Set(
|
||||
[...(originals.value[section] || []), ...(drafts.value[section] || [])]
|
||||
.map((row) => row.b_field_id)
|
||||
.filter((id) => id != null)
|
||||
.map(String),
|
||||
);
|
||||
}
|
||||
|
||||
function buildMissingDefaultConfigs(module, fields) {
|
||||
const moduleId = String(module.b_id);
|
||||
const listFieldIds = configuredFieldIds("listConfig");
|
||||
const editFieldIds = configuredFieldIds("editConfig");
|
||||
const queryFieldIds = configuredFieldIds("queryConfig");
|
||||
const generated = { listConfig: [], editConfig: [], queryConfig: [] };
|
||||
|
||||
fields
|
||||
.filter((field) => field.b_canuse === 1)
|
||||
.forEach((field, index) => {
|
||||
const fieldId = String(field.b_id);
|
||||
const order = (index + 1) * 10;
|
||||
|
||||
if (!listFieldIds.has(fieldId)) {
|
||||
generated.listConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: field.b_i18n || null,
|
||||
b_visible: 1,
|
||||
b_xh: order,
|
||||
b_width: guessFieldWidth(field.b_field),
|
||||
b_sortable: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!editFieldIds.has(fieldId)) {
|
||||
generated.editConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: order,
|
||||
});
|
||||
}
|
||||
|
||||
if (!queryFieldIds.has(fieldId)) {
|
||||
generated.queryConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_component: guessQueryComponent(field.b_type),
|
||||
b_operator: guessQueryOperator(field.b_type),
|
||||
b_default_value: null,
|
||||
b_xh: order,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return generated;
|
||||
}
|
||||
|
||||
async function syncFields(tableName) {
|
||||
const module = selectedModule.value;
|
||||
if (!module?.b_viewtable) {
|
||||
message.warning("当前模块未配置查询视图");
|
||||
const view = tableName || module?.b_viewtable;
|
||||
if (!view) {
|
||||
message.warning("请先配置查询视图或输入表名");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
configSaving.value = true;
|
||||
const res = await describeApi(module.b_viewtable);
|
||||
const res = await describeApi(view);
|
||||
const columns = res.data || [];
|
||||
const existingFields = originals.value.fields || [];
|
||||
|
||||
@@ -469,27 +580,55 @@ async function syncFields() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到 b_module_field
|
||||
// 字段多语言键只补空值,保留已有人工配置。
|
||||
mergedFields.forEach((field) => {
|
||||
if (!field.b_i18n) field.b_i18n = `field.${field.b_field}`;
|
||||
});
|
||||
|
||||
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
|
||||
const changes = diffRows(mergedFields, existingFields);
|
||||
const req = tableChange("b_module_field", changes);
|
||||
if (hasChanges(req)) {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
await saveObjectApi([req]);
|
||||
message.success(
|
||||
`字段同步完成:${columns.length} 列,新增 ${req.inserts?.length || 0},停用 ${req.deletes?.length || 0}`,
|
||||
);
|
||||
} else {
|
||||
message.info("字段已是最新,无需同步");
|
||||
const fieldReq = tableChange("s_module_field", changes);
|
||||
await allocateTemporaryIds([
|
||||
fieldReq.inserts || [],
|
||||
fieldReq.updates || [],
|
||||
fieldReq.deletes || [],
|
||||
]);
|
||||
|
||||
const generated = buildMissingDefaultConfigs(module, mergedFields);
|
||||
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
|
||||
const generatedCount = generatedSections.reduce(
|
||||
(total, section) => total + generated[section].length,
|
||||
0,
|
||||
);
|
||||
|
||||
if (!hasChanges(fieldReq) && !generatedCount) {
|
||||
message.info("字段和默认配置已是最新,无需同步");
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
const fieldsRes = await loadDataApi(
|
||||
"b_module_field",
|
||||
`b_module_id = ${module.b_id}`,
|
||||
"b_id ASC",
|
||||
if (hasChanges(fieldReq)) {
|
||||
// 字段先保存以取得真实 ID,自动生成的配置留在草稿中由顶部“保存”统一提交。
|
||||
await saveObjectApi([fieldReq]);
|
||||
const fieldsRes = await loadDataApi(
|
||||
"s_module_field",
|
||||
`b_module_id = ${module.b_id}`,
|
||||
"b_id ASC",
|
||||
);
|
||||
originals.value.fields = fieldsRes.data || [];
|
||||
drafts.value.fields = cloneData(originals.value.fields);
|
||||
}
|
||||
|
||||
for (const section of generatedSections) {
|
||||
if (!generated[section].length) continue;
|
||||
drafts.value[section] = [...(drafts.value[section] || []), ...cloneData(generated[section])];
|
||||
}
|
||||
|
||||
const disabledCount = existingFields.filter(
|
||||
(field) => field.b_canuse === 1 && !viewColumnSet.has(field.b_field?.toLowerCase()),
|
||||
).length;
|
||||
message.success(
|
||||
`字段同步完成:${columns.length} 列,新增 ${fieldReq.inserts?.length || 0},停用 ${disabledCount};自动生成列表 ${generated.listConfig.length}、编辑 ${generated.editConfig.length}、查询 ${generated.queryConfig.length} 项配置${generatedCount ? ",请点击保存" : ""}`,
|
||||
);
|
||||
originals.value.fields = fieldsRes.data || [];
|
||||
drafts.value.fields = cloneData(originals.value.fields);
|
||||
} catch (e) {
|
||||
message.error("字段同步失败:" + (e.message || e));
|
||||
} finally {
|
||||
@@ -498,7 +637,7 @@ async function syncFields() {
|
||||
}
|
||||
|
||||
// ---- 初始化 ----
|
||||
loadTree();
|
||||
void loadTree().catch((e) => message.error("模块树加载失败:" + (e.message || e)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -549,13 +688,14 @@ loadTree();
|
||||
<span>启用</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="configSaving"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="drafts.module?.b_module_type === 'data'"
|
||||
:disabled="configSaving || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
</a-space>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FmsModuleListPage module-code="d_login_log" />
|
||||
</template>
|
||||
Reference in new issue
Block a user