20260727173703
This commit is contained in:
1 parent
a8478d60ed
commit
9dbaef7787
114 files changed
+959
-2588
No files matched your search
@@ -34,5 +34,10 @@
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vite": "^8.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"stk-table-vue@1.0.0": "patches/stk-table-vue@1.0.0.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
diff --git a/src/StkTable/features/useAreaSelection.ts b/src/StkTable/features/useAreaSelection.ts
|
||||
index edbdafc2..63eea823 100644
|
||||
--- a/src/StkTable/features/useAreaSelection.ts
|
||||
+++ b/src/StkTable/features/useAreaSelection.ts
|
||||
@@ -158,6 +158,51 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
||||
|
||||
/** 根据 selectionRanges 计算所有选区内 cellKey 的并集(非响应式,用于 DOM 操作) */
|
||||
let selectedCellKeys: Set<string> = new Set();
|
||||
+ let observedThead: HTMLTableSectionElement | null = null;
|
||||
+ let observedTfoot: HTMLTableSectionElement | null = null;
|
||||
+ let cachedHeaderHeight = 0;
|
||||
+ let cachedFooterHeight = 0;
|
||||
+ const sectionResizeObserver =
|
||||
+ typeof ResizeObserver === 'undefined'
|
||||
+ ? null
|
||||
+ : new ResizeObserver(entries => {
|
||||
+ for (const entry of entries) {
|
||||
+ const height = entry.target.getBoundingClientRect().height;
|
||||
+ if (entry.target === observedThead) cachedHeaderHeight = height;
|
||||
+ if (entry.target === observedTfoot) cachedFooterHeight = height;
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ function observeTableSection(
|
||||
+ next: HTMLTableSectionElement | null,
|
||||
+ current: HTMLTableSectionElement | null,
|
||||
+ setCurrent: (section: HTMLTableSectionElement | null) => void,
|
||||
+ setHeight: (height: number) => void,
|
||||
+ ) {
|
||||
+ if (next === current) return;
|
||||
+ if (current) sectionResizeObserver?.unobserve(current);
|
||||
+ setCurrent(next);
|
||||
+ setHeight(next?.getBoundingClientRect().height ?? 0);
|
||||
+ if (next) sectionResizeObserver?.observe(next);
|
||||
+ }
|
||||
+
|
||||
+ function getTableSectionHeights(container: HTMLElement) {
|
||||
+ const thead = container.querySelector<HTMLTableSectionElement>('thead');
|
||||
+ const tfoot = container.querySelector<HTMLTableSectionElement>('tfoot');
|
||||
+ observeTableSection(
|
||||
+ thead,
|
||||
+ observedThead,
|
||||
+ section => (observedThead = section),
|
||||
+ height => (cachedHeaderHeight = height),
|
||||
+ );
|
||||
+ observeTableSection(
|
||||
+ tfoot,
|
||||
+ observedTfoot,
|
||||
+ section => (observedTfoot = section),
|
||||
+ height => (cachedFooterHeight = height),
|
||||
+ );
|
||||
+ return [cachedHeaderHeight, cachedFooterHeight] as const;
|
||||
+ }
|
||||
|
||||
/** 重新计算 selectedCellKeys */
|
||||
function recomputeSelectedCellKeys() {
|
||||
@@ -235,6 +280,22 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
||||
const lastRange = ranges[ranges.length - 1];
|
||||
const { minRow: lrMinRow, maxRow: lrMaxRow, minCol: lrMinCol, maxCol: lrMaxCol } = normalizeRange(lastRange);
|
||||
|
||||
+ // 键盘导航的常见路径只有一个单元格,直接定位目标行和列,
|
||||
+ // 避免每次按键都扫描全部可见行。
|
||||
+ if (ranges.length === 1 && lrMinRow === lrMaxRow && lrMinCol === lrMaxCol) {
|
||||
+ const tr = tbody.querySelector(`tr[data-row-i="${lrMinRow}"]`);
|
||||
+ const col = tableHeaderLast.value[lrMinCol];
|
||||
+ const targetColKey = col ? String(colKeyGen.value(col)) : '';
|
||||
+ const cells = tr?.querySelectorAll('td[data-col-key]') ?? [];
|
||||
+ for (let i = 0; i < cells.length; i++) {
|
||||
+ const td = cells[i] as HTMLElement;
|
||||
+ if (td.getAttribute('data-col-key') !== targetColKey) continue;
|
||||
+ td.classList.add(CELL_RANGE_SELECTED, CELL_RANGE_TOP, CELL_RANGE_BOTTOM, CELL_RANGE_LEFT, CELL_RANGE_RIGHT);
|
||||
+ break;
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
// 遍历所有可见行
|
||||
const trs = tbody.querySelectorAll('tr[data-row-i]');
|
||||
for (let t = 0; t < trs.length; t++) {
|
||||
@@ -317,6 +378,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
removeListener();
|
||||
+ sectionResizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -860,10 +922,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
||||
const col = tableHeaderLast.value[colIndex];
|
||||
if (!row || !col) return;
|
||||
|
||||
- const thead = container.querySelector('thead');
|
||||
- const headerHeight = thead ? thead.offsetHeight : 0;
|
||||
- const tfoot = container.querySelector('tfoot');
|
||||
- const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
||||
+ const [headerHeight, footerHeight] = getTableSectionHeights(container);
|
||||
|
||||
const vs = virtualScroll.value;
|
||||
const vsx = virtualScrollX.value;
|
||||
diff --git a/lib/StkTable-QUd4TJCF.js b/lib/StkTable-QUd4TJCF.js
|
||||
index 512545c2..80fc32bd 100644
|
||||
--- a/lib/StkTable-QUd4TJCF.js
|
||||
+++ b/lib/StkTable-QUd4TJCF.js
|
||||
@@ -444,6 +444,31 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
||||
});
|
||||
/** 根据 selectionRanges 计算所有选区内 cellKey 的并集(非响应式,用于 DOM 操作) */
|
||||
let selectedCellKeys = /* @__PURE__ */ new Set();
|
||||
+ let observedThead = null;
|
||||
+ let observedTfoot = null;
|
||||
+ let cachedHeaderHeight = 0;
|
||||
+ let cachedFooterHeight = 0;
|
||||
+ const sectionResizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => {
|
||||
+ for (const entry of entries) {
|
||||
+ const height = entry.target.getBoundingClientRect().height;
|
||||
+ if (entry.target === observedThead) cachedHeaderHeight = height;
|
||||
+ if (entry.target === observedTfoot) cachedFooterHeight = height;
|
||||
+ }
|
||||
+ });
|
||||
+ function observeTableSection(next, current, setCurrent, setHeight) {
|
||||
+ if (next === current) return;
|
||||
+ if (current) sectionResizeObserver?.unobserve(current);
|
||||
+ setCurrent(next);
|
||||
+ setHeight(next?.getBoundingClientRect().height ?? 0);
|
||||
+ if (next) sectionResizeObserver?.observe(next);
|
||||
+ }
|
||||
+ function getTableSectionHeights(container) {
|
||||
+ const thead = container.querySelector("thead");
|
||||
+ const tfoot = container.querySelector("tfoot");
|
||||
+ observeTableSection(thead, observedThead, (section) => observedThead = section, (height) => cachedHeaderHeight = height);
|
||||
+ observeTableSection(tfoot, observedTfoot, (section) => observedTfoot = section, (height) => cachedFooterHeight = height);
|
||||
+ return [cachedHeaderHeight, cachedFooterHeight];
|
||||
+ }
|
||||
/** 重新计算 selectedCellKeys */
|
||||
function recomputeSelectedCellKeys() {
|
||||
const ranges = selectionRanges.value;
|
||||
@@ -497,6 +522,19 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
||||
if (cellHighlight) {
|
||||
const lastRange = ranges[ranges.length - 1];
|
||||
const { minRow: lrMinRow, maxRow: lrMaxRow, minCol: lrMinCol, maxCol: lrMaxCol } = normalizeRange(lastRange);
|
||||
+ if (ranges.length === 1 && lrMinRow === lrMaxRow && lrMinCol === lrMaxCol) {
|
||||
+ const tr = tbody.querySelector(`tr[data-row-i="${lrMinRow}"]`);
|
||||
+ const col = tableHeaderLast.value[lrMinCol];
|
||||
+ const targetColKey = col ? String(colKeyGen.value(col)) : "";
|
||||
+ const cells = tr?.querySelectorAll("td[data-col-key]") ?? [];
|
||||
+ for (let i = 0; i < cells.length; i++) {
|
||||
+ const td = cells[i];
|
||||
+ if (td.getAttribute("data-col-key") !== targetColKey) continue;
|
||||
+ td.classList.add(CELL_RANGE_SELECTED, CELL_RANGE_TOP, CELL_RANGE_BOTTOM, CELL_RANGE_LEFT, CELL_RANGE_RIGHT);
|
||||
+ break;
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
const trs = tbody.querySelectorAll("tr[data-row-i]");
|
||||
for (let t = 0; t < trs.length; t++) {
|
||||
const tr = trs[t];
|
||||
@@ -559,6 +597,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
removeListener();
|
||||
+ sectionResizeObserver?.disconnect();
|
||||
});
|
||||
/**
|
||||
* 监听数据行数/列数变化,当行列变少时钳制选区与锚点,避免越界
|
||||
@@ -966,10 +1005,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
||||
const row = dataSourceCopy.value[rowIndex];
|
||||
const col = tableHeaderLast.value[colIndex];
|
||||
if (!row || !col) return;
|
||||
- const thead = container.querySelector("thead");
|
||||
- const headerHeight = thead ? thead.offsetHeight : 0;
|
||||
- const tfoot = container.querySelector("tfoot");
|
||||
- const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
||||
+ const [headerHeight, footerHeight] = getTableSectionHeights(container);
|
||||
const vs = virtualScroll.value;
|
||||
const vsx = virtualScrollX.value;
|
||||
const isScrollRowByRow = props.scrollRowByRow;
|
||||
Generated
+7
-2
@@ -4,6 +4,11 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
stk-table-vue@1.0.0:
|
||||
hash: fbea7319b3c9d3c3810998e779324073ad00e23523d1504199fbad4a40ee6933
|
||||
path: patches/stk-table-vue@1.0.0.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -31,7 +36,7 @@ importers:
|
||||
version: 4.7.1(pinia@4.0.2(@vue/devtools-api@8.1.5)(vue@3.5.39))
|
||||
stk-table-vue:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
version: 1.0.0(patch_hash=fbea7319b3c9d3c3810998e779324073ad00e23523d1504199fbad4a40ee6933)
|
||||
vue:
|
||||
specifier: ^3.5.39
|
||||
version: 3.5.39
|
||||
@@ -2845,7 +2850,7 @@ snapshots:
|
||||
source-map@0.6.1:
|
||||
optional: true
|
||||
|
||||
stk-table-vue@1.0.0: {}
|
||||
stk-table-vue@1.0.0(patch_hash=fbea7319b3c9d3c3810998e779324073ad00e23523d1504199fbad4a40ee6933): {}
|
||||
|
||||
stream-parser@0.3.1:
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
loading: { type: Boolean, default: false },
|
||||
size: {
|
||||
type: String,
|
||||
default: "large",
|
||||
validator: (value) => ["small", "default", "large"].includes(value),
|
||||
},
|
||||
description: { type: String, default: "" },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fms-loading-overlay" :aria-busy="loading">
|
||||
<div class="fms-loading-overlay__content" :class="{ 'is-loading': loading }">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-show="loading" class="fms-loading-overlay__mask">
|
||||
<a-spin :size="size" :description="description || undefined" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fms-loading-overlay,
|
||||
.fms-loading-overlay__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fms-loading-overlay {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fms-loading-overlay__content.is-loading {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fms-loading-overlay__mask {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in srgb, var(--fms-surface, #fff) 72%, transparent);
|
||||
cursor: wait;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import FmsQueryToolbar from "./FmsQueryToolbar.vue";
|
||||
import { useModuleList } from "./useModuleList";
|
||||
|
||||
const props = defineProps({
|
||||
moduleCode: { type: String, default: "" },
|
||||
dataCode: { type: String, default: "" },
|
||||
columnExtensions: { type: Object, default: () => ({}) },
|
||||
rowActions: { type: Array, default: () => [] },
|
||||
rowClassName: { type: Function, default: undefined },
|
||||
@@ -20,7 +20,7 @@ const emit = defineEmits(["row-click", "cell-click", "row-action", "expand", "lo
|
||||
const slots = useSlots();
|
||||
const tableRef = ref(null);
|
||||
const moduleList = useModuleList({
|
||||
moduleCode: toRef(props, "moduleCode"),
|
||||
dataCode: toRef(props, "dataCode"),
|
||||
pageSize: props.pageSize,
|
||||
});
|
||||
|
||||
|
||||
@@ -240,17 +240,29 @@ export function useModuleList(options = {}) {
|
||||
appliedFilters.value = filters.filter(Boolean);
|
||||
}
|
||||
|
||||
async function resolveConfiguredModule() {
|
||||
const moduleCode = String(unref(options.moduleCode) || "").trim();
|
||||
if (moduleCode) {
|
||||
async function resolvePageModule(runtimeData) {
|
||||
const pageId = requireModuleId(runtimeData.b_parent_id);
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_id = ${pageId} AND b_module_type = 'page' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const module = response.data?.[0];
|
||||
if (!module) throw new Error(`数据编码 ${runtimeData.b_code} 没有对应的启用页面`);
|
||||
return module;
|
||||
}
|
||||
|
||||
async function resolveConfiguredModules() {
|
||||
const dataCode = String(unref(options.dataCode) || route.meta.dataCode || "").trim();
|
||||
if (dataCode) {
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_code = N'${escapeSqlText(moduleCode)}' AND b_canuse = 1`,
|
||||
`b_code = N'${escapeSqlText(dataCode)}' AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const module = response.data?.[0];
|
||||
if (!module) throw new Error(`未找到编码为 ${moduleCode} 的启用模块`);
|
||||
return module;
|
||||
const runtimeData = response.data?.[0];
|
||||
if (!runtimeData) throw new Error(`未找到编码为 ${dataCode} 的启用数据模块`);
|
||||
return { runtimePage: await resolvePageModule(runtimeData), runtimeData };
|
||||
}
|
||||
|
||||
const routePath = escapeSqlText(route.path);
|
||||
@@ -259,9 +271,9 @@ export function useModuleList(options = {}) {
|
||||
`b_route = N'${routePath}' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const module = response.data?.[0];
|
||||
if (!module) throw new Error(`未找到路由 ${route.path} 对应的启用模块`);
|
||||
return module;
|
||||
const runtimePage = response.data?.[0];
|
||||
if (!runtimePage) throw new Error(`未找到路由 ${route.path} 对应的启用模块`);
|
||||
return { runtimePage, runtimeData: await resolveDataModule(runtimePage) };
|
||||
}
|
||||
|
||||
async function resolveDataModule(runtimePage) {
|
||||
@@ -281,8 +293,7 @@ export function useModuleList(options = {}) {
|
||||
configLoading.value = true;
|
||||
configError.value = "";
|
||||
try {
|
||||
const runtimePage = await resolveConfiguredModule();
|
||||
const runtimeData = await resolveDataModule(runtimePage);
|
||||
const { runtimePage, runtimeData } = await resolveConfiguredModules();
|
||||
const dataId = requireModuleId(runtimeData.b_id);
|
||||
const [fieldResponse, listResponse, queryResponse] = await Promise.all([
|
||||
loadDataApi("s_module_field", `b_module_id = ${dataId}`, "b_id ASC"),
|
||||
@@ -426,9 +437,13 @@ export function useModuleList(options = {}) {
|
||||
return keyField ? row?.[keyField] : JSON.stringify(row);
|
||||
}
|
||||
|
||||
watch(() => [String(unref(options.moduleCode) || ""), route.path], initialize, {
|
||||
immediate: true,
|
||||
});
|
||||
watch(
|
||||
() => [String(unref(options.dataCode) || route.meta.dataCode || ""), route.path],
|
||||
initialize,
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true;
|
||||
|
||||
@@ -51,12 +51,9 @@ useResizeObserver(rootRef, ([entry]) => {
|
||||
});
|
||||
|
||||
const NAVIGATION_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"]);
|
||||
const NAVIGATION_IDLE_MS = 80;
|
||||
const rowEditingStates = new WeakMap();
|
||||
let activeEditingRow = null;
|
||||
let pendingEditingTarget = null;
|
||||
let navigationEvent = null;
|
||||
let navigationTimer = null;
|
||||
const pressedNavigationKeys = new Set();
|
||||
|
||||
function getRowKey(row) {
|
||||
@@ -232,41 +229,80 @@ function initVirtualScroll() {
|
||||
return tableRef.value?.initVirtualScroll?.();
|
||||
}
|
||||
|
||||
function clearNavigationTimer() {
|
||||
if (navigationTimer) clearTimeout(navigationTimer);
|
||||
navigationTimer = null;
|
||||
let editActivateTimer = null;
|
||||
let lastEditActivateAt = 0;
|
||||
let isRepeatNavigation = false;
|
||||
|
||||
function clearEditActivateTimer() {
|
||||
if (editActivateTimer) {
|
||||
clearTimeout(editActivateTimer);
|
||||
editActivateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘导航停止(松开按键或停顿)后再激活编辑器,合并连续/按住按键,
|
||||
// 避免滚动过程中每帧创建/销毁编辑器组件导致掉帧。
|
||||
function scheduleEditActivation() {
|
||||
clearEditActivateTimer();
|
||||
editActivateTimer = setTimeout(() => {
|
||||
editActivateTimer = null;
|
||||
commitPendingEditing();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function commitPendingEditing() {
|
||||
clearNavigationTimer();
|
||||
const target = pendingEditingTarget;
|
||||
pendingEditingTarget = null;
|
||||
if (!target) return;
|
||||
const row = props.dataSource.find(
|
||||
(item) => item === target.row || String(getRowKey(item)) === String(getRowKey(target.row)),
|
||||
);
|
||||
if (row) activateEditing(row, target.focusCol);
|
||||
if (row) {
|
||||
activateEditing(row, target.focusCol);
|
||||
lastEditActivateAt = performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePendingEditing() {
|
||||
clearNavigationTimer();
|
||||
navigationTimer = setTimeout(commitPendingEditing, NAVIGATION_IDLE_MS);
|
||||
function removeNavigationListeners() {
|
||||
window.removeEventListener("keyup", handleKeyupCapture, true);
|
||||
window.removeEventListener("blur", handleWindowBlur);
|
||||
}
|
||||
|
||||
function handleKeydownCapture(event) {
|
||||
if (!NAVIGATION_KEYS.has(event.key)) return;
|
||||
if (!pressedNavigationKeys.size) {
|
||||
window.addEventListener("keyup", handleKeyupCapture, true);
|
||||
window.addEventListener("blur", handleWindowBlur);
|
||||
}
|
||||
pressedNavigationKeys.add(event.key);
|
||||
const currentEvent = { repeat: event.repeat };
|
||||
navigationEvent = currentEvent;
|
||||
queueMicrotask(() => {
|
||||
if (navigationEvent === currentEvent) navigationEvent = null;
|
||||
});
|
||||
// event.repeat=true 表示系统自动重复(按住方向键不松),用于区分“按住滚动”与“离散点按”
|
||||
if (event.repeat) isRepeatNavigation = true;
|
||||
}
|
||||
|
||||
function handleKeyupCapture(event) {
|
||||
if (!NAVIGATION_KEYS.has(event.key)) return;
|
||||
pressedNavigationKeys.delete(event.key);
|
||||
if (!pressedNavigationKeys.size) commitPendingEditing();
|
||||
if (!pressedNavigationKeys.delete(event.key) || pressedNavigationKeys.size) return;
|
||||
|
||||
removeNavigationListeners();
|
||||
|
||||
if (isRepeatNavigation) {
|
||||
// 长按已经结束,立即恢复最终目标行,不再额外等待防抖时间。
|
||||
isRepeatNavigation = false;
|
||||
clearEditActivateTimer();
|
||||
commitPendingEditing();
|
||||
return;
|
||||
}
|
||||
|
||||
// 快速离散连按仍使用短防抖,避免连续挂载/卸载整行编辑器。
|
||||
if (pendingEditingTarget) scheduleEditActivation();
|
||||
}
|
||||
|
||||
function handleWindowBlur() {
|
||||
removeNavigationListeners();
|
||||
pressedNavigationKeys.clear();
|
||||
isRepeatNavigation = false;
|
||||
clearEditActivateTimer();
|
||||
commitPendingEditing();
|
||||
}
|
||||
|
||||
// 选区变化后,目标行进入编辑,活动单元格获得焦点。
|
||||
@@ -278,22 +314,41 @@ function handleAreaSelectionChange(ranges) {
|
||||
const col = target ? displayColumns.value[target.col] : null;
|
||||
if (!row) {
|
||||
pendingEditingTarget = null;
|
||||
clearNavigationTimer();
|
||||
clearActiveEditing();
|
||||
clearEditActivateTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const focusCol = col?.editor ? col.dataIndex : null;
|
||||
if (navigationEvent?.repeat) {
|
||||
pendingEditingTarget = { row, focusCol };
|
||||
clearActiveEditing();
|
||||
schedulePendingEditing();
|
||||
// 键盘导航期间(按下方向键等):
|
||||
// - 按住方向键滚动(event.repeat=true):全程保持 0 编辑器,避免长按下反复创建组件导致卡顿/闪烁;
|
||||
// 松手后再激活到最终行。
|
||||
// - 慢速离散点按(非 repeat):立即激活,编辑器逐行跟随、跟手;
|
||||
// - 快速连按(间隔 <150ms):防抖合并,避免连按时每键挂载掉帧。
|
||||
if (pressedNavigationKeys.size > 0) {
|
||||
if (isRepeatNavigation) {
|
||||
pendingEditingTarget = { row, focusCol };
|
||||
clearActiveEditing();
|
||||
} else {
|
||||
const now = performance.now();
|
||||
if (now - lastEditActivateAt >= 150) {
|
||||
pendingEditingTarget = null;
|
||||
clearEditActivateTimer();
|
||||
activateEditing(row, focusCol);
|
||||
lastEditActivateAt = now;
|
||||
} else {
|
||||
pendingEditingTarget = { row, focusCol };
|
||||
scheduleEditActivation();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 非键盘导航(鼠标点击等):立即激活
|
||||
pendingEditingTarget = null;
|
||||
clearNavigationTimer();
|
||||
clearEditActivateTimer();
|
||||
activateEditing(row, focusCol);
|
||||
lastEditActivateAt = performance.now();
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -316,15 +371,19 @@ watch(
|
||||
(editable) => {
|
||||
if (editable) return;
|
||||
pendingEditingTarget = null;
|
||||
removeNavigationListeners();
|
||||
pressedNavigationKeys.clear();
|
||||
clearNavigationTimer();
|
||||
isRepeatNavigation = false;
|
||||
clearEditActivateTimer();
|
||||
clearActiveEditing();
|
||||
tableRef.value?.clearSelectedArea?.();
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeNavigationListeners();
|
||||
clearEditActivateTimer();
|
||||
pressedNavigationKeys.clear();
|
||||
clearNavigationTimer();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
@@ -338,12 +397,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="fms-table"
|
||||
@keydown.capture="handleKeydownCapture"
|
||||
@keyup.capture="handleKeyupCapture"
|
||||
>
|
||||
<div ref="rootRef" class="fms-table" @keydown.capture="handleKeydownCapture">
|
||||
<StkTable
|
||||
ref="tableRef"
|
||||
class="fms-table__inner"
|
||||
@@ -363,7 +417,7 @@ defineExpose({
|
||||
:bordered="bordered"
|
||||
:no-data-full="noDataFull"
|
||||
:show-overflow="showOverflow"
|
||||
:area-selection="true"
|
||||
:area-selection="editable"
|
||||
@area-selection-change="handleAreaSelectionChange"
|
||||
@row-order-change="handleRowOrderChange"
|
||||
>
|
||||
|
||||
@@ -31,12 +31,13 @@ function createFocusHandler(ctx, row, col, selectText = false) {
|
||||
if (document.activeElement === input) return;
|
||||
|
||||
const instance = component?.exposed || component?.proxy;
|
||||
const opts = selectText ? { cursor: "all", preventScroll: true } : { preventScroll: true };
|
||||
if (instance?.focus) {
|
||||
instance.focus(selectText ? { cursor: "all" } : undefined);
|
||||
instance.focus(opts);
|
||||
return;
|
||||
}
|
||||
|
||||
input?.focus();
|
||||
input?.focus(opts);
|
||||
if (selectText) input?.select();
|
||||
};
|
||||
}
|
||||
@@ -84,7 +85,7 @@ function createSelect(row, col, editor, ctx) {
|
||||
style: { width: "100%" },
|
||||
getPopupContainer: (trigger) => trigger.parentNode,
|
||||
"onUpdate:value": (value) => {
|
||||
const resolved = value ?? (editor.emptyValue ?? null);
|
||||
const resolved = value ?? editor.emptyValue ?? null;
|
||||
row[col.dataIndex] = resolved;
|
||||
notifyChange(ctx, editor, row, col, resolved);
|
||||
},
|
||||
|
||||
@@ -12,12 +12,17 @@ const permissionStore = usePermissionStore();
|
||||
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
const openKeys = ref(["system"]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
function canAccessMenu(item) {
|
||||
if (item.dataCode) return permissionStore.canData(item.dataCode);
|
||||
if (item.moduleCode) return permissionStore.canModule(item.moduleCode);
|
||||
return true;
|
||||
}
|
||||
const menuItems = computed(() => {
|
||||
const systemPages = [
|
||||
{ key: "/system/module-management", label: "模块管理", moduleCode: "m_metadata" },
|
||||
{ key: "/system/i18n-type", label: "多语言类型", moduleCode: "m_i18n_type" },
|
||||
{ key: "/system/login-log", label: "登录日志" },
|
||||
].filter((item) => !item.moduleCode || permissionStore.canModule(item.moduleCode));
|
||||
{ key: "/system/login-log", label: "登录日志", dataCode: "d_login_log" },
|
||||
].filter(canAccessMenu);
|
||||
const items = [{ key: "/dashboard", icon: menuIcon(House), label: "工作台" }];
|
||||
if (systemPages.length)
|
||||
items.push({
|
||||
|
||||
@@ -34,11 +34,14 @@ router.beforeEach(async (to) => {
|
||||
return getRedirectPath(to.query.redirect);
|
||||
}
|
||||
|
||||
if (to.meta.moduleCode) {
|
||||
if (to.meta.dataCode || to.meta.moduleCode) {
|
||||
const permissionStore = usePermissionStore();
|
||||
try {
|
||||
await permissionStore.load();
|
||||
if (!permissionStore.canModule(to.meta.moduleCode)) {
|
||||
const canAccess = to.meta.dataCode
|
||||
? permissionStore.canData(to.meta.dataCode)
|
||||
: permissionStore.canModule(to.meta.moduleCode);
|
||||
if (!canAccess) {
|
||||
message.warning("没有该模块的访问权限");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const routes = [
|
||||
title: "登录日志",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/login-log",
|
||||
dataCode: "d_login_log",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,13 +6,18 @@ import { useAuthStore } from "./auth";
|
||||
export const usePermissionStore = defineStore("permissions", () => {
|
||||
const authStore = useAuthStore();
|
||||
const loadedPrincipalKey = ref("");
|
||||
const modules = ref([]);
|
||||
const grantedPageIds = ref([]);
|
||||
const moduleCodes = ref([]);
|
||||
const powerCodes = ref([]);
|
||||
let loadingPromise = null;
|
||||
let loadingPrincipalKey = "";
|
||||
let loadVersion = 0;
|
||||
|
||||
const moduleSet = computed(() => new Set(moduleCodes.value));
|
||||
const moduleByCode = computed(
|
||||
() => new Map(modules.value.map((module) => [String(module.b_code || ""), module])),
|
||||
);
|
||||
const grantedPageSet = computed(() => new Set(grantedPageIds.value));
|
||||
const powerSet = computed(() => new Set(powerCodes.value));
|
||||
const isG3soft = computed(() => {
|
||||
return (
|
||||
@@ -32,6 +37,8 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
loadingPromise = null;
|
||||
loadingPrincipalKey = "";
|
||||
loadedPrincipalKey.value = elevatedKey;
|
||||
modules.value = [];
|
||||
grantedPageIds.value = [];
|
||||
moduleCodes.value = [];
|
||||
powerCodes.value = [];
|
||||
return;
|
||||
@@ -48,22 +55,23 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"),
|
||||
loadDataApi("s_module_power", "b_canuse = 1", "b_id ASC"),
|
||||
])
|
||||
.then(([modules, userModules, userPowers, powers]) => {
|
||||
.then(([moduleResponse, userModules, userPowers, powers]) => {
|
||||
if (requestVersion !== loadVersion) return;
|
||||
const moduleCodeById = new Map(
|
||||
modules.data.map((module) => [String(module.b_id), module.b_code]),
|
||||
);
|
||||
const activeModules = moduleResponse.data || [];
|
||||
const moduleById = new Map(activeModules.map((module) => [String(module.b_id), module]));
|
||||
const powerById = new Map(powers.data.map((power) => [String(power.b_id), power]));
|
||||
const pageIds = userModules.data
|
||||
.map((item) => String(item.b_module_id || ""))
|
||||
.filter(Boolean);
|
||||
modules.value = activeModules;
|
||||
grantedPageIds.value = pageIds;
|
||||
moduleCodes.value = userModules.data
|
||||
.map((item) => moduleCodeById.get(String(item.b_module_id)))
|
||||
.map((item) => moduleById.get(String(item.b_module_id))?.b_code)
|
||||
.filter(Boolean);
|
||||
powerCodes.value = userPowers.data
|
||||
.map((item) => powerById.get(String(item.b_power_id)))
|
||||
.filter(Boolean)
|
||||
.map((power) => {
|
||||
const moduleCode = moduleCodeById.get(String(power.b_module_id));
|
||||
return moduleCode ? `${moduleCode}.${power.b_code}` : "";
|
||||
})
|
||||
.map((power) => `${String(power.b_module_id)}.${power.b_code}`)
|
||||
.filter(Boolean);
|
||||
loadedPrincipalKey.value = principalKey;
|
||||
})
|
||||
@@ -76,13 +84,31 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
return request;
|
||||
}
|
||||
|
||||
function canModule(moduleCode) {
|
||||
return isG3soft.value || !moduleCode || moduleSet.value.has(moduleCode);
|
||||
function resolvePageId(code, expectedType) {
|
||||
const module = moduleByCode.value.get(String(code || ""));
|
||||
if (!module || (expectedType && module.b_module_type !== expectedType)) return "";
|
||||
if (module.b_module_type === "page") return String(module.b_id || "");
|
||||
if (module.b_module_type === "data") return String(module.b_parent_id || "");
|
||||
return "";
|
||||
}
|
||||
|
||||
function canPower(moduleCode, powerCode) {
|
||||
function canModule(moduleCode) {
|
||||
if (isG3soft.value || !moduleCode) return true;
|
||||
const pageId = resolvePageId(moduleCode);
|
||||
return !!pageId && grantedPageSet.value.has(pageId);
|
||||
}
|
||||
|
||||
function canData(dataCode) {
|
||||
if (isG3soft.value || !dataCode) return true;
|
||||
const pageId = resolvePageId(dataCode, "data");
|
||||
return !!pageId && grantedPageSet.value.has(pageId);
|
||||
}
|
||||
|
||||
function canPower(scopeCode, powerCode) {
|
||||
if (isG3soft.value) return true;
|
||||
const pageId = resolvePageId(scopeCode);
|
||||
return (
|
||||
isG3soft.value || (canModule(moduleCode) && powerSet.value.has(`${moduleCode}.${powerCode}`))
|
||||
!!pageId && grantedPageSet.value.has(pageId) && powerSet.value.has(`${pageId}.${powerCode}`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,6 +117,8 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
loadingPromise = null;
|
||||
loadingPrincipalKey = "";
|
||||
loadedPrincipalKey.value = "";
|
||||
modules.value = [];
|
||||
grantedPageIds.value = [];
|
||||
moduleCodes.value = [];
|
||||
powerCodes.value = [];
|
||||
}
|
||||
@@ -100,5 +128,15 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
clear,
|
||||
);
|
||||
|
||||
return { load, canModule, canPower, clear, moduleCodes, powerCodes, isG3soft };
|
||||
return {
|
||||
load,
|
||||
canModule,
|
||||
canData,
|
||||
canPower,
|
||||
clear,
|
||||
grantedPageIds,
|
||||
moduleCodes,
|
||||
powerCodes,
|
||||
isG3soft,
|
||||
};
|
||||
});
|
||||
@@ -14,6 +14,25 @@ export function isValidI18nKey(key) {
|
||||
return /^[A-Za-z][A-Za-z0-9_.:-]{1,149}$/.test(String(key || ""));
|
||||
}
|
||||
|
||||
export function buildModuleItemI18nKey(prefix, moduleCode, itemCode) {
|
||||
const normalizedModuleCode = String(moduleCode || "").trim();
|
||||
const normalizedItemCode = String(itemCode || "").trim();
|
||||
return normalizedModuleCode && normalizedItemCode
|
||||
? `${prefix}.${normalizedModuleCode}.${normalizedItemCode}`
|
||||
: null;
|
||||
}
|
||||
|
||||
export function syncModuleItemI18nKeys(rows, prefix, moduleCode, codeField) {
|
||||
let changed = false;
|
||||
for (const row of rows || []) {
|
||||
const key = buildModuleItemI18nKey(prefix, moduleCode, row?.[codeField]);
|
||||
if (row.b_i18n === key) continue;
|
||||
row.b_i18n = key;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function groupI18nRows(rows) {
|
||||
const groups = {};
|
||||
rows.forEach((item) => {
|
||||
|
||||
@@ -17,9 +17,10 @@ const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
moduleId: { type: String, required: true },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "update"]);
|
||||
const emit = defineEmits(["update:visible", "saved"]);
|
||||
|
||||
const localGroups = ref([]);
|
||||
const selectedGroupId = ref(null);
|
||||
@@ -81,7 +82,7 @@ function addGroup(parentId) {
|
||||
b_parent_id: parentId || null,
|
||||
b_title: "",
|
||||
b_i18n: null,
|
||||
b_xh: localGroups.value.length * 10,
|
||||
b_xh: (localGroups.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
};
|
||||
localGroups.value.push(newGroup);
|
||||
@@ -90,6 +91,17 @@ function addGroup(parentId) {
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
const hasChildren = localGroups.value.some(
|
||||
(group) => String(group.b_parent_id) === selectedGroupId.value,
|
||||
);
|
||||
if (hasChildren) {
|
||||
message.warning("该分组下还有子分组,请先删除子分组");
|
||||
return;
|
||||
}
|
||||
if (props.referencedGroupIds.map(String).includes(selectedGroupId.value)) {
|
||||
message.warning("该分组仍被列表或编辑配置引用,请先清空引用");
|
||||
return;
|
||||
}
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
|
||||
selectedGroupId.value = null;
|
||||
}
|
||||
@@ -114,7 +126,7 @@ async function saveAllGroups() {
|
||||
]);
|
||||
remapForeignKeys(localGroups.value, ["b_parent_id"], idMapping);
|
||||
await saveObjectApi([req]);
|
||||
emit("update", cloneData(localGroups.value));
|
||||
emit("saved", cloneData(localGroups.value), idMapping);
|
||||
message.success("分组已保存");
|
||||
emit("update:visible", false);
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,7 +10,7 @@ const props = defineProps({
|
||||
parentName: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
const emit = defineEmits(["update", "i18nSaved"]);
|
||||
|
||||
const formData = ref(null);
|
||||
|
||||
@@ -23,7 +23,6 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isDirectory = computed(() => formData.value?.b_module_type === "directory");
|
||||
const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
|
||||
@@ -56,6 +55,7 @@ function openI18nModal() {
|
||||
|
||||
function onI18nSaved(key) {
|
||||
setField("b_i18n", key);
|
||||
emit("i18nSaved", key);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -65,21 +65,13 @@ function onI18nSaved(key) {
|
||||
<a-form layout="vertical" class="basic-form" :disabled="loading">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input
|
||||
:value="formData.b_code"
|
||||
placeholder="如 m_sea"
|
||||
@update:value="onCodeInput"
|
||||
/>
|
||||
<a-form-item :label="isData ? '模块编码' : '菜单编码'" required>
|
||||
<a-input :value="formData.b_code" @update:value="onCodeInput" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input
|
||||
:value="formData.b_name"
|
||||
placeholder="模块显示名称"
|
||||
@update:value="(v) => setField('b_name', v)"
|
||||
/>
|
||||
<a-input :value="formData.b_name" @update:value="(v) => setField('b_name', v)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
@@ -100,18 +92,15 @@ function onI18nSaved(key) {
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="上级模块">
|
||||
<a-input :value="parentName" readonly placeholder="通过左侧树选中后自动设置" />
|
||||
<a-input :value="parentName" readonly />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="多语言键">
|
||||
<div class="i18n-key-row">
|
||||
<a-input :value="formData.b_i18n" readonly placeholder="填写模块编码后自动生成" />
|
||||
<a-input :value="formData.b_i18n" readonly />
|
||||
<a-button
|
||||
class="i18n-key-btn"
|
||||
type="text"
|
||||
@@ -123,7 +112,7 @@ function onI18nSaved(key) {
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="formData.b_xh"
|
||||
@@ -132,60 +121,44 @@ function onI18nSaved(key) {
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isPage" :span="8">
|
||||
<a-form-item label="页面路由" required>
|
||||
<a-input :value="formData.b_route" @update:value="(v) => setField('b_route', v)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isData" :span="8">
|
||||
<a-form-item label="查询视图" required>
|
||||
<a-input
|
||||
:value="formData.b_viewtable"
|
||||
@update:value="(v) => setField('b_viewtable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isData" :span="8">
|
||||
<a-form-item label="保存表">
|
||||
<a-input
|
||||
:value="formData.b_savetable"
|
||||
@update:value="(v) => setField('b_savetable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isData" :span="8">
|
||||
<a-form-item label="主键字段">
|
||||
<a-input
|
||||
:value="formData.b_keyfield"
|
||||
@update:value="(v) => setField('b_keyfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isData" :span="8">
|
||||
<a-form-item label="默认排序">
|
||||
<a-input
|
||||
:value="formData.b_orderfield"
|
||||
@update:value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 页面/数据 专用字段 -->
|
||||
<template v-if="!isDirectory">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8" v-if="isPage">
|
||||
<a-form-item label="页面路由" required>
|
||||
<a-input
|
||||
:value="formData.b_route"
|
||||
placeholder="如 /sea/list"
|
||||
@update:value="(v) => setField('b_route', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="查询视图" required>
|
||||
<a-input
|
||||
:value="formData.b_viewtable"
|
||||
placeholder="如 v_sea"
|
||||
@update:value="(v) => setField('b_viewtable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="保存表">
|
||||
<a-input
|
||||
:value="formData.b_savetable"
|
||||
placeholder="如 d_sea"
|
||||
@update:value="(v) => setField('b_savetable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" v-if="isData">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="主键字段">
|
||||
<a-input
|
||||
:value="formData.b_keyfield"
|
||||
placeholder="如 b_id"
|
||||
@update:value="(v) => setField('b_keyfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="默认排序">
|
||||
<a-input
|
||||
:value="formData.b_orderfield"
|
||||
placeholder="如 b_xh ASC"
|
||||
@update:value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
<I18nQuickSetModal ref="i18nModalRef" @saved="onI18nSaved" />
|
||||
|
||||
@@ -8,10 +8,11 @@ const props = defineProps({
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
const emit = defineEmits(["update", "groupsSaved"]);
|
||||
|
||||
const localEdit = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
@@ -62,7 +63,7 @@ function makeSwitchCol(field, title, width) {
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
editor: { type: "checkbox" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +75,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
@@ -87,7 +87,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => groupOptions.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
@@ -123,7 +122,8 @@ const columns = shallowRef([
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="emitUpdate"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -131,7 +131,8 @@ const columns = shallowRef([
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
@@ -16,44 +16,24 @@ const FIELD_TYPES = [
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
moduleCode: { type: String, default: "" },
|
||||
fields: { type: Array, default: () => [] },
|
||||
viewTable: { type: String, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "sync"]);
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
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 })) || [];
|
||||
() => [props.fields, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localFields.value && moduleCode === previous[1]) return;
|
||||
const rows = val?.map((row) => ({ ...row })) || [];
|
||||
const changed = syncModuleItemI18nKeys(rows, "field", moduleCode, "b_field");
|
||||
localFields.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -64,56 +44,17 @@ function emitUpdate() {
|
||||
|
||||
// 编辑器已直接更新当前行,这里只向父级同步草稿。
|
||||
function onCellChange() {
|
||||
syncModuleItemI18nKeys(localFields.value, "field", props.moduleCode, "b_field");
|
||||
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,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
@@ -122,71 +63,31 @@ const columns = ref([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: FIELD_TYPES,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_default_value",
|
||||
title: "默认值",
|
||||
width: 120,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 140,
|
||||
editor: { type: "input", onChange: () => emitUpdate() },
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
actions: [
|
||||
{
|
||||
type: "delete",
|
||||
label: "删除",
|
||||
confirm: "确定删除该字段?",
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fields-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<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"
|
||||
@@ -194,7 +95,6 @@ const columns = ref([
|
||||
:editable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
:row-selection="rowSelection"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="onCellChange"
|
||||
/>
|
||||
@@ -209,13 +109,6 @@ const columns = ref([
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px 0;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
i18nKeys: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
@@ -28,6 +29,7 @@ const isLoading = ref(false);
|
||||
const searchKey = ref("");
|
||||
const activeKey = ref(null);
|
||||
const tableRef = ref(null);
|
||||
let loadRequestId = 0;
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
@@ -61,10 +63,16 @@ function buildPivot(flat) {
|
||||
}
|
||||
|
||||
async function loadI18n() {
|
||||
const requestId = ++loadRequestId;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const keys = [...new Set((props.i18nKeys || []).filter(Boolean).map(String))];
|
||||
const quotedKeys = keys.map((key) => "'" + key.replace(/'/g, "''") + "'");
|
||||
const where = keys.length ? "b_key IN (" + quotedKeys.join(", ") + ")" : null;
|
||||
const [i18nRes] = await Promise.all([
|
||||
loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC"),
|
||||
where
|
||||
? loadDataApi("b_i18n", where, "b_key ASC, b_locale ASC")
|
||||
: Promise.resolve({ data: [] }),
|
||||
loadLangTypes(),
|
||||
]);
|
||||
const flat = (i18nRes.data || []).map((r) => ({
|
||||
@@ -73,12 +81,34 @@ async function loadI18n() {
|
||||
b_locale: r.b_locale,
|
||||
b_value: r.b_value,
|
||||
}));
|
||||
if (requestId !== loadRequestId) return;
|
||||
originalsFlat.value = cloneData(flat);
|
||||
pivotRows.value = buildPivot(flat);
|
||||
} catch (e) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
if (requestId === loadRequestId) message.error("加载多语言失败:" + (e.message || e));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
if (requestId === loadRequestId) isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshKey(key) {
|
||||
if (!key) return;
|
||||
const escaped = String(key).replace(/'/g, "''");
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n", "b_key = '" + escaped + "'", "b_locale ASC");
|
||||
const rows = (res.data || []).map((row) => ({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
}));
|
||||
originalsFlat.value = [
|
||||
...originalsFlat.value.filter((row) => row.b_key !== key),
|
||||
...cloneData(rows),
|
||||
];
|
||||
pivotRows.value = [...pivotRows.value.filter((row) => row.b_key !== key), ...buildPivot(rows)];
|
||||
} catch (e) {
|
||||
message.error("刷新多语言失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +184,24 @@ function flatten(rows) {
|
||||
return flat;
|
||||
}
|
||||
|
||||
// 由父级「同步字段」调用:把默认翻译条目({ key, locale, value })并入草稿行。
|
||||
// 仅当面板中尚不存在该 b_key 时才追加,避免覆盖用户在面板里已配置的翻译。
|
||||
function mergeDefaults(entries) {
|
||||
if (!entries?.length) return;
|
||||
const existingKeys = new Set(pivotRows.value.map((r) => r.b_key));
|
||||
const toAdd = entries.filter((e) => e.key && !existingKeys.has(e.key));
|
||||
if (!toAdd.length) return;
|
||||
pivotRows.value = [
|
||||
...pivotRows.value,
|
||||
...toAdd.map((e) => ({
|
||||
_id: createTempId(),
|
||||
b_key: e.key,
|
||||
_localeIds: {},
|
||||
[e.locale]: e.value,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
// 由父级统一保存调用:返回 b_i18n 的变更请求(无变更返回 null),保存成功后父级调用 markSaved 同步基线
|
||||
async function buildSaveRequest() {
|
||||
const flat = flatten(pivotRows.value);
|
||||
@@ -168,7 +216,7 @@ function markSaved() {
|
||||
originalsFlat.value = cloneData(flatten(pivotRows.value));
|
||||
}
|
||||
|
||||
defineExpose({ buildSaveRequest, markSaved });
|
||||
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey });
|
||||
|
||||
const displayedData = computed(() => {
|
||||
if (!searchKey.value) return pivotRows.value;
|
||||
|
||||
@@ -8,10 +8,11 @@ const props = defineProps({
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
const emit = defineEmits(["update", "groupsSaved"]);
|
||||
|
||||
const localList = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
@@ -64,7 +65,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
@@ -75,7 +75,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => groupOptions.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -83,7 +82,7 @@ const columns = shallowRef([
|
||||
title: "显示",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
@@ -118,7 +117,8 @@ const columns = shallowRef([
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="emitUpdate"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -126,7 +126,8 @@ const columns = shallowRef([
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
@@ -13,7 +14,7 @@ const props = defineProps({
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
const emit = defineEmits(["update", "i18nSaved"]);
|
||||
|
||||
const localPowers = ref([]);
|
||||
const activeKey = ref(null);
|
||||
@@ -24,11 +25,10 @@ watch(
|
||||
() => [props.powers, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localPowers.value && moduleCode === previous[1]) return;
|
||||
localPowers.value = (val || []).map((r) => ({
|
||||
...r,
|
||||
b_i18n:
|
||||
r.b_code && props.moduleCode ? `power.${props.moduleCode}.${r.b_code}` : (r.b_i18n ?? null),
|
||||
}));
|
||||
const rows = (val || []).map((row) => ({ ...row }));
|
||||
const changed = syncModuleItemI18nKeys(rows, "power", moduleCode, "b_code");
|
||||
localPowers.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -46,7 +46,7 @@ async function addRow() {
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
b_i18n: null,
|
||||
b_xh: localPowers.value.length * 10,
|
||||
b_xh: (localPowers.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
@@ -89,7 +89,7 @@ function batchDelete() {
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
const delSet = new Set(keys);
|
||||
localPowers.value = localPowers.value.filter((r) => !delSet.has(r.b_id));
|
||||
localPowers.value = localPowers.value.filter((r) => !delSet.has(String(r.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = "";
|
||||
emitUpdate();
|
||||
@@ -99,6 +99,7 @@ function batchDelete() {
|
||||
|
||||
// 编辑器已直接更新当前行,这里只向父级同步草稿。
|
||||
function onCellChange() {
|
||||
syncModuleItemI18nKeys(localPowers.value, "power", props.moduleCode, "b_code");
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
@@ -106,8 +107,7 @@ function onCellChange() {
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
function openI18n(row) {
|
||||
const key =
|
||||
row.b_code && props.moduleCode ? `power.${props.moduleCode}.${row.b_code}` : row.b_i18n;
|
||||
const key = buildModuleItemI18nKey("power", props.moduleCode, row.b_code);
|
||||
if (!key) {
|
||||
message.warning("请先填写操作编码");
|
||||
return;
|
||||
@@ -208,7 +208,7 @@ const columns = ref([
|
||||
@change="onCellChange"
|
||||
/>
|
||||
</div>
|
||||
<I18nQuickSetModal ref="i18nModalRef" />
|
||||
<I18nQuickSetModal ref="i18nModalRef" @saved="(key) => emit('i18nSaved', key)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ function addRow() {
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: localQuery.value.length * 10,
|
||||
b_xh: (localQuery.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
@@ -85,7 +85,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -95,7 +94,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: COMPONENT_OPTIONS,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -105,7 +103,6 @@ const columns = shallowRef([
|
||||
editor: {
|
||||
type: "select",
|
||||
options: OPERATOR_OPTIONS,
|
||||
onChange: () => emitUpdate(),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认查询值", width: 140 },
|
||||
@@ -114,7 +111,7 @@ const columns = shallowRef([
|
||||
title: "启用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", onChange: () => emitUpdate() },
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
@@ -149,7 +146,8 @@ const columns = shallowRef([
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(ev, row) => (activeKey = row.b_id)"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="emitUpdate"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||||
import {
|
||||
cloneData,
|
||||
diffRows,
|
||||
allocateTemporaryIds,
|
||||
remapForeignKeys,
|
||||
tableChange,
|
||||
hasChanges,
|
||||
} from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
||||
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
||||
@@ -23,11 +26,11 @@ import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
||||
// ---- 状态 ----
|
||||
const treeData = ref([]);
|
||||
const treeLoading = ref(false);
|
||||
const expandedTreeKeys = ref([]);
|
||||
const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
const contextNode = ref(null);
|
||||
const contextMenuOpen = ref(false);
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
const node = contextNode.value;
|
||||
@@ -71,17 +74,47 @@ const drafts = ref({
|
||||
|
||||
const configLoading = ref(false);
|
||||
const configSaving = ref(false);
|
||||
const configSyncing = ref(false);
|
||||
const i18nPanelRef = ref(null);
|
||||
const loadedModuleId = ref(null);
|
||||
let configRequestId = 0;
|
||||
let operationId = 0;
|
||||
|
||||
// ref 写在 v-for 内部会被 Vue 收集为数组,这里统一取单个组件实例
|
||||
function getI18nPanel() {
|
||||
const r = i18nPanelRef.value;
|
||||
return Array.isArray(r) ? r[0] : r;
|
||||
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
|
||||
function setI18nPanelRef(instance) {
|
||||
i18nPanelRef.value = instance || null;
|
||||
}
|
||||
|
||||
// 当前选中模块对象(从 drafts.module 获取)
|
||||
const selectedModule = computed(() => drafts.value.module);
|
||||
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
|
||||
const syncDisabled = computed(
|
||||
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
|
||||
);
|
||||
const isCurrentModule = computed(
|
||||
() =>
|
||||
loadedModuleId.value != null && String(loadedModuleId.value) === String(selectedModuleId.value),
|
||||
);
|
||||
const i18nKeys = computed(() => {
|
||||
const keys = new Set();
|
||||
const add = (key) => {
|
||||
if (key) keys.add(String(key));
|
||||
};
|
||||
add(drafts.value.module?.b_i18n);
|
||||
for (const section of ["fields", "groups", "listConfig", "powers"]) {
|
||||
for (const row of drafts.value[section] || []) add(row.b_i18n);
|
||||
}
|
||||
return [...keys];
|
||||
});
|
||||
const referencedGroupIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const section of ["listConfig", "editConfig"]) {
|
||||
for (const row of drafts.value[section] || []) {
|
||||
if (row.b_group_id != null) ids.add(String(row.b_group_id));
|
||||
}
|
||||
}
|
||||
return [...ids];
|
||||
});
|
||||
|
||||
// 上级模块名称(从模块树解析 b_parent_id 对应的名称,用于基本信息展示)
|
||||
const parentModuleName = computed(() => {
|
||||
@@ -102,20 +135,20 @@ const parentModuleName = computed(() => {
|
||||
const visibleTabs = computed(() => {
|
||||
const type = selectedModule.value?.b_module_type;
|
||||
if (!type) return [];
|
||||
const tabs = [{ key: "basic", tab: "基本信息", icon: "Info" }];
|
||||
const tabs = [{ key: "basic", tab: "基本信息" }];
|
||||
if (type === "data") {
|
||||
tabs.push(
|
||||
{ key: "fields", tab: "字段定义", icon: "Table" },
|
||||
{ key: "list", tab: "列表配置", icon: "List" },
|
||||
{ key: "edit", tab: "编辑配置", icon: "Edit" },
|
||||
{ key: "query", tab: "查询配置", icon: "Search" },
|
||||
{ key: "fields", tab: "字段定义" },
|
||||
{ key: "list", tab: "列表配置" },
|
||||
{ key: "edit", tab: "编辑配置" },
|
||||
{ key: "query", tab: "查询配置" },
|
||||
);
|
||||
}
|
||||
if (type === "page") {
|
||||
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
|
||||
tabs.push({ key: "powers", tab: "权限配置" });
|
||||
}
|
||||
if (type !== "directory") {
|
||||
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
|
||||
tabs.push({ key: "i18n", tab: "多语言" });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
@@ -142,6 +175,38 @@ function buildTree(modules) {
|
||||
return roots;
|
||||
}
|
||||
|
||||
function sortTree(nodes) {
|
||||
nodes.sort(
|
||||
(a, b) =>
|
||||
Number(a.b_xh || 0) - Number(b.b_xh || 0) || String(a.b_id).localeCompare(String(b.b_id)),
|
||||
);
|
||||
for (const node of nodes) {
|
||||
if (node.children?.length) sortTree(node.children);
|
||||
}
|
||||
}
|
||||
|
||||
// 普通保存后直接更新现有节点,避免重新请求并重建整棵树导致展开状态丢失。
|
||||
function updateSavedTreeNode(moduleData) {
|
||||
let node = findNode(moduleData.b_id);
|
||||
if (!node) {
|
||||
node = { ...cloneData(moduleData), children: [] };
|
||||
const parent = moduleData.b_parent_id ? findNode(moduleData.b_parent_id) : null;
|
||||
if (parent) {
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
treeData.value.push(node);
|
||||
}
|
||||
}
|
||||
const children = node.children;
|
||||
Object.assign(node, cloneData(moduleData), {
|
||||
key: String(moduleData.b_id),
|
||||
title: moduleData.b_name || moduleData.b_code,
|
||||
});
|
||||
if (children) node.children = children;
|
||||
sortTree(treeData.value);
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
@@ -157,32 +222,56 @@ 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("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"),
|
||||
const moduleType = findNode(id)?.b_module_type;
|
||||
const requests = [["module", loadDataApi("s_module", `b_id = ${id}`)]];
|
||||
if (moduleType === "data") {
|
||||
requests.push(
|
||||
["fields", loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC")],
|
||||
[
|
||||
"groups",
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"listConfig",
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"editConfig",
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"queryConfig",
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
);
|
||||
} else if (moduleType === "page") {
|
||||
requests.push([
|
||||
"powers",
|
||||
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
}
|
||||
const responses = Object.fromEntries(
|
||||
await Promise.all(requests.map(async ([key, request]) => [key, await request])),
|
||||
);
|
||||
|
||||
const original = {
|
||||
module: moduleRes.data?.[0] || null,
|
||||
fields: fieldsRes.data || [],
|
||||
groups: groupsRes.data || [],
|
||||
listConfig: listRes.data || [],
|
||||
editConfig: editRes.data || [],
|
||||
queryConfig: queryRes.data || [],
|
||||
powers: powersRes.data || [],
|
||||
module: responses.module.data?.[0] || null,
|
||||
fields: responses.fields?.data || [],
|
||||
groups: responses.groups?.data || [],
|
||||
listConfig: responses.listConfig?.data || [],
|
||||
editConfig: responses.editConfig?.data || [],
|
||||
queryConfig: responses.queryConfig?.data || [],
|
||||
powers: responses.powers?.data || [],
|
||||
};
|
||||
if (requestId !== configRequestId) return;
|
||||
originals.value = original;
|
||||
drafts.value = cloneData(original);
|
||||
loadedModuleId.value = id;
|
||||
activeTab.value = "basic";
|
||||
} catch (e) {
|
||||
if (requestId === configRequestId) {
|
||||
loadedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
message.error("模块配置加载失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
@@ -192,6 +281,7 @@ async function loadModuleConfig(moduleId, requestId) {
|
||||
|
||||
// ---- 树节点选择 ----
|
||||
function onTreeSelect(keys) {
|
||||
if (busy.value) return;
|
||||
if (keys.length > 0) {
|
||||
selectedModuleId.value = String(keys[0]);
|
||||
}
|
||||
@@ -199,8 +289,11 @@ function onTreeSelect(keys) {
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
const requestId = ++configRequestId;
|
||||
operationId++;
|
||||
if (!id) {
|
||||
configLoading.value = false;
|
||||
loadedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
return;
|
||||
}
|
||||
if (skipLoad.value) {
|
||||
@@ -208,6 +301,7 @@ watch(selectedModuleId, (id) => {
|
||||
configLoading.value = false;
|
||||
return;
|
||||
}
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(id, requestId);
|
||||
});
|
||||
|
||||
@@ -223,11 +317,19 @@ function onSetField(field, value) {
|
||||
}
|
||||
|
||||
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
||||
function onGroupsSaved(newGroups) {
|
||||
function onGroupsSaved(newGroups, idMapping) {
|
||||
if (idMapping?.size) {
|
||||
remapForeignKeys(drafts.value.listConfig || [], ["b_group_id"], idMapping);
|
||||
remapForeignKeys(drafts.value.editConfig || [], ["b_group_id"], idMapping);
|
||||
}
|
||||
originals.value.groups = cloneData(newGroups);
|
||||
drafts.value.groups = cloneData(newGroups);
|
||||
}
|
||||
|
||||
function onI18nSaved(key) {
|
||||
void i18nPanelRef.value?.refreshKey?.(key);
|
||||
}
|
||||
|
||||
// ---- 新增模块 ----
|
||||
async function onAddModule(parentId, moduleType = "directory") {
|
||||
const res = await nextIdApi();
|
||||
@@ -262,10 +364,12 @@ async function onAddModule(parentId, moduleType = "directory") {
|
||||
drafts.value = cloneData(originals.value);
|
||||
skipLoad.value = true;
|
||||
selectedModuleId.value = newId;
|
||||
loadedModuleId.value = newId;
|
||||
activeTab.value = "basic";
|
||||
|
||||
// 刷新树
|
||||
await loadTree();
|
||||
updateSavedTreeNode(newModule);
|
||||
if (parentId && !expandedTreeKeys.value.includes(String(parentId))) {
|
||||
expandedTreeKeys.value = [...expandedTreeKeys.value, String(parentId)];
|
||||
}
|
||||
}
|
||||
|
||||
const addModuleMenu = {
|
||||
@@ -279,7 +383,7 @@ function onTreeRightClick({ node }) {
|
||||
}
|
||||
|
||||
function onContextMenuClick({ key }) {
|
||||
contextMenuOpen.value = false;
|
||||
if (busy.value) return;
|
||||
const node = contextNode.value;
|
||||
contextNode.value = null;
|
||||
if (!node) return;
|
||||
@@ -345,7 +449,9 @@ async function onDeleteNodeById(moduleId) {
|
||||
|
||||
// ---- 统一保存 ----
|
||||
async function saveAll() {
|
||||
if (!selectedModule.value) return;
|
||||
if (!selectedModule.value || !isCurrentModule.value || busy.value) return;
|
||||
const moduleId = String(selectedModule.value.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
configSaving.value = true;
|
||||
|
||||
try {
|
||||
@@ -354,8 +460,26 @@ async function saveAll() {
|
||||
// s_module 主表
|
||||
const moduleData = drafts.value.module;
|
||||
const moduleOriginal = originals.value.module;
|
||||
if (!String(moduleData?.b_code || "").trim()) {
|
||||
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
|
||||
return;
|
||||
}
|
||||
if (!String(moduleData?.b_name || "").trim()) {
|
||||
message.warning("请填写模块名称");
|
||||
return;
|
||||
}
|
||||
if (moduleData.b_module_type === "page" && !String(moduleData.b_route || "").trim()) {
|
||||
message.warning("请填写页面路由");
|
||||
return;
|
||||
}
|
||||
if (moduleData.b_module_type === "data" && !String(moduleData.b_viewtable || "").trim()) {
|
||||
message.warning("请填写查询视图");
|
||||
return;
|
||||
}
|
||||
syncModuleItemI18nKeys(drafts.value.fields, "field", moduleData.b_code, "b_field");
|
||||
syncModuleItemI18nKeys(drafts.value.powers, "power", moduleData.b_code, "b_code");
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
saveReqs.push({
|
||||
table: "s_module",
|
||||
key_field: "b_id",
|
||||
@@ -383,8 +507,10 @@ async function saveAll() {
|
||||
}
|
||||
|
||||
// 多语言:由各子面板收集变更,统一在此保存
|
||||
const i18nPanel = getI18nPanel();
|
||||
if (i18nPanel) {
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
const i18nPanel = i18nPanelRef.value;
|
||||
if (typeof i18nPanel?.buildSaveRequest === "function") {
|
||||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||||
if (i18nReq) saveReqs.push(i18nReq);
|
||||
}
|
||||
@@ -398,9 +524,9 @@ async function saveAll() {
|
||||
|
||||
// 更新 originals
|
||||
originals.value = cloneData(drafts.value);
|
||||
getI18nPanel()?.markSaved();
|
||||
i18nPanelRef.value?.markSaved?.();
|
||||
updateSavedTreeNode(moduleData);
|
||||
message.success("保存成功");
|
||||
await loadTree();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} finally {
|
||||
@@ -529,19 +655,20 @@ function buildMissingDefaultConfigs(module, fields) {
|
||||
return generated;
|
||||
}
|
||||
|
||||
async function syncFields(tableName) {
|
||||
async function syncFields() {
|
||||
const module = selectedModule.value;
|
||||
const view = tableName || module?.b_viewtable;
|
||||
if (!view) {
|
||||
message.warning("请先配置查询视图或输入表名");
|
||||
return;
|
||||
}
|
||||
const view = String(module?.b_viewtable || "").trim();
|
||||
if (!view || busy.value) return;
|
||||
const moduleId = String(module.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
|
||||
try {
|
||||
configSaving.value = true;
|
||||
configSyncing.value = true;
|
||||
const res = await describeApi(view);
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
const columns = res.data || [];
|
||||
const existingFields = originals.value.fields || [];
|
||||
const existingFields = drafts.value.fields || [];
|
||||
|
||||
// 构建视图列名集合
|
||||
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
|
||||
@@ -580,9 +707,9 @@ async function syncFields(tableName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 字段多语言键只补空值,保留已有人工配置。
|
||||
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
|
||||
mergedFields.forEach((field) => {
|
||||
if (!field.b_i18n) field.b_i18n = `field.${field.b_field}`;
|
||||
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
|
||||
});
|
||||
|
||||
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
|
||||
@@ -594,6 +721,29 @@ async function syncFields(tableName) {
|
||||
fieldReq.deletes || [],
|
||||
]);
|
||||
|
||||
// 为"有 b_i18n 键"的字段生成多语言默认配置(b_i18n 草稿行),注入 i18n 面板,
|
||||
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
|
||||
const candidateI18n = mergedFields
|
||||
.filter((f) => f.b_canuse === 1 && f.b_i18n)
|
||||
.map((f) => ({ key: f.b_i18n, value: f.b_name || f.b_field }));
|
||||
let primaryLocale = "zh-CN";
|
||||
try {
|
||||
const langRes = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
const langs = langRes.data || [];
|
||||
if (langs.length) {
|
||||
// 默认字段名翻译写到"简体中文"(zh-CN),找不到再退回排序第一的语言
|
||||
const zh = langs.find((l) => l.b_code === "zh-CN");
|
||||
primaryLocale = (zh || langs[0]).b_code;
|
||||
}
|
||||
} catch (e) {
|
||||
// 取不到语言类型时回退默认值
|
||||
}
|
||||
const i18nEntries = candidateI18n.map((e) => ({
|
||||
key: e.key,
|
||||
locale: primaryLocale,
|
||||
value: e.value,
|
||||
}));
|
||||
|
||||
const generated = buildMissingDefaultConfigs(module, mergedFields);
|
||||
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
|
||||
const generatedCount = generatedSections.reduce(
|
||||
@@ -601,21 +751,15 @@ async function syncFields(tableName) {
|
||||
0,
|
||||
);
|
||||
|
||||
if (!hasChanges(fieldReq) && !generatedCount) {
|
||||
message.info("字段和默认配置已是最新,无需同步");
|
||||
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
|
||||
message.success("同步成功");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
|
||||
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
|
||||
if (i18nEntries.length) {
|
||||
i18nPanelRef.value?.mergeDefaults?.(i18nEntries);
|
||||
}
|
||||
|
||||
for (const section of generatedSections) {
|
||||
@@ -623,16 +767,13 @@ async function syncFields(tableName) {
|
||||
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 ? ",请点击保存" : ""}`,
|
||||
);
|
||||
drafts.value.fields = cloneData(mergedFields);
|
||||
|
||||
message.success("同步成功");
|
||||
} catch (e) {
|
||||
message.error("字段同步失败:" + (e.message || e));
|
||||
} finally {
|
||||
configSaving.value = false;
|
||||
configSyncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,20 +787,23 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-sidebar-header">
|
||||
<span class="mm-sidebar-title">模块列表</span>
|
||||
<a-dropdown :menu="addModuleMenu" :trigger="['click']">
|
||||
<a-button><Plus :size="14" />新增</a-button>
|
||||
<a-dropdown :menu="addModuleMenu" :trigger="['click']" :disabled="busy">
|
||||
<a-button :disabled="busy"><Plus :size="14" />新增</a-button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div v-if="treeData.length === 0" class="mm-tree-empty">
|
||||
<div v-if="treeLoading" class="mm-tree-empty">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else-if="treeData.length === 0" class="mm-tree-empty">
|
||||
<a-empty description="暂无模块" />
|
||||
</div>
|
||||
<a-dropdown v-else :trigger="['contextmenu']" :menu="contextMenuItems">
|
||||
<div
|
||||
style="flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column"
|
||||
>
|
||||
<div class="mm-tree-container">
|
||||
<FmsTree
|
||||
class="mm-tree"
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
@select="onTreeSelect"
|
||||
@@ -677,66 +821,84 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="configSaving" @click="saveAll">保存</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<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="configSaving || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || configSyncing"
|
||||
@click="saveAll"
|
||||
>保存</a-button
|
||||
>
|
||||
<a-button
|
||||
v-if="selectedModule.b_module_type === 'data'"
|
||||
:loading="configSyncing"
|
||||
:disabled="syncDisabled"
|
||||
@click="syncFields"
|
||||
>
|
||||
同步字段
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="busy || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<span class="mm-header-title">{{
|
||||
selectedModule.b_name || selectedModule.b_code
|
||||
}}</span>
|
||||
<span class="mm-header-code">{{ selectedModule.b_code || "(未设置编码)" }}</span>
|
||||
<span class="mm-header-type">
|
||||
<a-tag>{{
|
||||
selectedModule.b_module_type === "directory"
|
||||
? "目录"
|
||||
: selectedModule.b_module_type === "page"
|
||||
? "页面"
|
||||
: "数据"
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<span class="mm-header-title">{{ selectedModule.b_name || selectedModule.b_code }}</span>
|
||||
<span class="mm-header-code">{{ selectedModule.b_code || "(未设置编码)" }}</span>
|
||||
<span class="mm-header-type">
|
||||
<a-tag>{{
|
||||
selectedModule.b_module_type === "directory"
|
||||
? "目录"
|
||||
: selectedModule.b_module_type === "page"
|
||||
? "页面"
|
||||
: "数据"
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane v-for="tab in visibleTabs" :key="tab.key" :tab="tab.tab">
|
||||
<template v-if="configLoading">
|
||||
<div class="mm-loading"><a-spin /></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:tab="tab.tab"
|
||||
:force-render="tab.key === 'i18n'"
|
||||
>
|
||||
<ModuleBasicPanel
|
||||
v-if="tab.key === 'basic'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:data="drafts.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('module', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleFieldsPanel
|
||||
v-else-if="tab.key === 'fields'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:fields="drafts.fields"
|
||||
:view-table="selectedModule.b_viewtable"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('fields', v)"
|
||||
@sync="syncFields"
|
||||
/>
|
||||
<ModuleListConfigPanel
|
||||
v-else-if="tab.key === 'list'"
|
||||
@@ -744,9 +906,10 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:list-config="drafts.listConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:loading="configSaving"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('listConfig', v)"
|
||||
@update-groups="onGroupsSaved"
|
||||
@groups-saved="onGroupsSaved"
|
||||
/>
|
||||
<ModuleEditConfigPanel
|
||||
v-else-if="tab.key === 'edit'"
|
||||
@@ -754,16 +917,17 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:edit-config="drafts.editConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:loading="configSaving"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('editConfig', v)"
|
||||
@update-groups="onGroupsSaved"
|
||||
@groups-saved="onGroupsSaved"
|
||||
/>
|
||||
<ModuleQueryConfigPanel
|
||||
v-else-if="tab.key === 'query'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModulePowersPanel
|
||||
@@ -771,25 +935,27 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:powers="drafts.powers"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('powers', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleI18nPanel
|
||||
v-else-if="tab.key === 'i18n'"
|
||||
ref="i18nPanelRef"
|
||||
:ref="setI18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:loading="configSaving"
|
||||
:i18n-keys="i18nKeys"
|
||||
:loading="busy"
|
||||
/>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -835,6 +1001,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.mm-tree-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mm-tree-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -3,5 +3,5 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FmsModuleListPage module-code="d_login_log" />
|
||||
<FmsModuleListPage />
|
||||
</template>
|
||||
Reference in new issue
Block a user