20260730173012

This commit is contained in:
oneao committed 2026-07-30 17:30:13 +08:00
1 parent 84673b3d3d
commit 4929b78434
25 files changed
+1796 -68

No files matched your search

@@ -1,5 +1,15 @@
<script setup>
import { computed, h, nextTick, reactive, ref, shallowRef, useSlots, watch } from "vue";
import {
computed,
h,
nextTick,
onActivated,
reactive,
ref,
shallowRef,
useSlots,
watch,
} from "vue";
import { useRoute } from "vue-router";
import { message } from "antdv-next";
import { Columns3, RefreshCw, Save } from "@lucide/vue";
@@ -33,6 +43,9 @@ const slots = useSlots();
const route = useRoute();
const appStore = useAppStore();
const tableRef = ref(null);
// keep-alive 中的缓存实例必须保留各自的路由上下文,不能跟随全局 route 变化。
const instanceRoutePath = route.path;
const instanceRouteDataCode = route.meta.dataCode;
// ── 内部状态 ──
const dataModule = ref(null);
@@ -91,12 +104,11 @@ function resolveKeyField(module, fields) {
}
function getDataCode() {
return String(props.dataCode || route.meta.dataCode || "").trim();
return String(props.dataCode || instanceRouteDataCode || "").trim();
}
async function loadModuleConfig() {
const code = getDataCode();
const currentPath = route.path;
configLoading.value = true;
configError.value = "";
@@ -122,7 +134,7 @@ async function loadModuleConfig() {
throw new Error(`数据编码 ${runtimeData.b_code} 没有对应的启用页面`);
}
} else {
const routePath = escapeSqlText(currentPath);
const routePath = escapeSqlText(instanceRoutePath);
const pageRes = await loadDataApi(
"s_module",
`b_route = N'${routePath}' AND b_canuse = 1`,
@@ -160,7 +172,7 @@ async function loadModuleConfig() {
"b_xh ASC, b_id ASC",
),
]);
if (getDataCode() !== code || route.path !== currentPath) return false;
if (getDataCode() !== code) return false;
const fields = [];
const fieldById = new Map();
@@ -205,12 +217,12 @@ async function loadModuleConfig() {
keyField.value = resolveKeyField(runtimeData, fields);
return true;
} catch (error) {
if (getDataCode() === code && route.path === currentPath) {
if (getDataCode() === code) {
configError.value = error?.message || String(error);
}
return false;
} finally {
if (getDataCode() === code && route.path === currentPath) {
if (getDataCode() === code) {
configLoading.value = false;
}
}
@@ -239,7 +251,6 @@ async function fetchData() {
const currentModule = dataModule.value;
const viewName = currentModule.b_viewtable;
const currentCode = getDataCode();
const currentPath = route.path;
const requestVersion = ++dataRequestVersion;
const conditions = [];
const fixedCondition = String(props.fixedSearchCondition || "").trim();
@@ -259,15 +270,14 @@ async function fetchData() {
});
if (requestVersion !== dataRequestVersion || dataModule.value !== currentModule) return;
if (getDataCode() !== currentCode || route.path !== currentPath) return;
if (getDataCode() !== currentCode) return;
dataSource.value = res.data?.rows || [];
pagination.total = Number(res.data?.total ?? res.data?.row_count) || 0;
} catch (error) {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
getDataCode() === currentCode
) {
dataSource.value = [];
pagination.total = 0;
@@ -277,8 +287,7 @@ async function fetchData() {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
getDataCode() === currentCode
) {
dataLoading.value = false;
}
@@ -412,7 +421,19 @@ async function handleMoreAction({ key }) {
}
// ── 生命周期 ──
watch(() => [getDataCode(), route.path], initialize, { immediate: true });
let hasActivated = false;
void initialize();
onActivated(async () => {
if (!hasActivated) {
hasActivated = true;
return;
}
await nextTick();
tableRef.value?.refreshLayout?.();
});
watch(() => props.dataCode, initialize);
watch(
() => props.fixedSearchCondition,
@@ -41,7 +41,7 @@ function formatCellValue(value, field, optionInfo) {
if (field.b_type === "date") return formatDateValue(value, false);
if (field.b_type === "datetime") return formatDateValue(value, true);
if (field.b_type === "checkbox") {
return h(Checkbox, { checked: value === true || Number(value) === 1, disabled: true });
return h(Checkbox, { checked: value === true || Number(value) === 1 });
}
if (field.b_type === "select") {
const option = optionInfo.options.find((item) => String(item.value) === String(value));
@@ -199,6 +199,10 @@ function resetColumns() {
tableRef.value?.initVirtualScroll?.();
}
function refreshLayout() {
tableRef.value?.refreshLayout?.();
}
function getColumnSettings() {
const configByFieldId = new Map();
for (const config of props.listConfig || []) {
@@ -282,6 +286,7 @@ watch(
defineExpose({
resetColumns,
refreshLayout,
getColumnSettings,
clearSelectedArea: () => tableRef.value?.clearSelectedArea?.(),
});
@@ -85,6 +85,28 @@ function clearActiveEditing() {
if (state) state.value = { editing: false, focusCol: null, initialValues: null };
}
// 父级可能在 change 后回灌同 rowKey 的新对象,编辑状态需要随行对象迁移。
function rebindActiveEditingRow(rows) {
if (!activeEditingRow || rows.includes(activeEditingRow)) return;
const previousRow = activeEditingRow;
const previousState = rowEditingStates.get(previousRow)?.value;
const previousKey = getRowKey(previousRow);
const replacement =
previousKey == null ? null : rows.find((row) => String(getRowKey(row)) === String(previousKey));
clearActiveEditing();
if (!replacement || !previousState?.editing) return;
const state = getRowEditingState(replacement);
activeEditingRow = replacement;
state.value = {
editing: true,
focusCol: previousState.focusCol,
initialValues: captureEditableValues(replacement),
};
}
function activateEditing(row, focusCol) {
if (!props.editable || !row) return;
const state = getRowEditingState(row);
@@ -402,7 +424,7 @@ function handleAreaSelectionChange(ranges) {
watch(
[() => props.activeRowKey, () => props.dataSource],
async ([rowKey]) => {
if (activeEditingRow && !props.dataSource.includes(activeEditingRow)) clearActiveEditing();
rebindActiveEditingRow(props.dataSource);
await nextTick();
if (rowKey == null) {
tableRef.value?.setCurrentRow?.(undefined, { silent: true });
@@ -1,7 +1,42 @@
import { h } from "vue";
import { h, toRaw } from "vue";
import { Input, Select } from "antdv-next";
const stop = (event) => event.stopPropagation();
const inputDrafts = new WeakMap();
function getInputDraft(row, dataIndex) {
const drafts = inputDrafts.get(row);
return drafts?.has(dataIndex) ? drafts.get(dataIndex) : row[dataIndex];
}
function setInputDraft(row, dataIndex, value) {
let drafts = inputDrafts.get(row);
if (!drafts) {
drafts = new Map();
inputDrafts.set(row, drafts);
}
drafts.set(dataIndex, value);
}
function clearInputDraft(row, dataIndex) {
const drafts = inputDrafts.get(row);
if (!drafts) return;
drafts.delete(dataIndex);
if (!drafts.size) inputDrafts.delete(row);
}
function commitInputDraft(row, dataIndex, value) {
clearInputDraft(row, dataIndex);
row[dataIndex] = value;
}
function preserveInputDraft(row, dataIndex) {
const drafts = inputDrafts.get(row);
if (!drafts?.has(dataIndex)) return;
// 卸载兜底不触发整表更新;正常路径会在 blur 中响应式提交并通知父级。
toRaw(row)[dataIndex] = drafts.get(dataIndex);
clearInputDraft(row, dataIndex);
}
function resolveOptions(editor) {
return typeof editor.options === "function" ? editor.options() : (editor.options ?? []);
@@ -52,15 +87,16 @@ function createInput(row, col, editor, ctx) {
const focus = createFocusHandler(ctx, row, col, true);
return h(Input, {
type: editor.inputType || "text",
value: row[col.dataIndex] ?? "",
value: getInputDraft(row, col.dataIndex) ?? "",
size: "small",
class: "fms-cell-editor fms-cell-editor-root",
"aria-label": col.title || col.dataIndex,
"onUpdate:value": (value) => {
row[col.dataIndex] = value;
setInputDraft(row, col.dataIndex, value);
},
onBlur: (event) => {
const value = event.target.value;
commitInputDraft(row, col.dataIndex, value);
notifyChange(ctx, editor, row, col, value);
},
onKeydown: (event) => {
@@ -75,6 +111,7 @@ function createInput(row, col, editor, ctx) {
onClick: stop,
onVnodeMounted: focus,
onVnodeUpdated: focus,
onVnodeBeforeUnmount: () => preserveInputDraft(row, col.dataIndex),
});
}