20260727173703
This commit is contained in:
1 parent
a8478d60ed
commit
9dbaef7787
114 files changed
+959
-2588
No files matched your search
@@ -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);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user