20260730173012
This commit is contained in:
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,16 @@ const menuItems = computed(() => {
|
||||
label: moduleLabel("base_othercompany", "/base/othercompany", "合作伙伴"),
|
||||
moduleId: "base_othercompany",
|
||||
},
|
||||
{
|
||||
key: "/base/contact",
|
||||
label: moduleLabel("v_b_contact", "/base/contact", "联系人"),
|
||||
moduleId: "v_b_contact",
|
||||
},
|
||||
{
|
||||
key: "/base/otherdata",
|
||||
label: moduleLabel("m_base_otherdata", "/base/otherdata", "其他数据"),
|
||||
moduleId: "m_base_otherdata",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const systemPages = [
|
||||
{
|
||||
@@ -52,6 +62,11 @@ const menuItems = computed(() => {
|
||||
label: moduleLabel("d_login_log", "/system/login-log", "登录日志"),
|
||||
moduleId: "d_login_log",
|
||||
},
|
||||
{
|
||||
key: "/system/user",
|
||||
label: moduleLabel("b_user", "/system/user", "用户管理"),
|
||||
moduleId: "b_user",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const items = [
|
||||
{
|
||||
@@ -152,6 +167,19 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
line-height: 36px;
|
||||
transition: padding-inline-start 0.2s ease;
|
||||
}
|
||||
:deep(.ant-menu-inline-collapsed) {
|
||||
.ant-menu-title-content {
|
||||
display: none !important;
|
||||
}
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
padding-inline-start: 20px !important;
|
||||
}
|
||||
.ant-menu-item-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brand {
|
||||
|
||||
@@ -22,7 +22,29 @@ const routes = [
|
||||
title: "合作伙伴",
|
||||
section: "基础数据",
|
||||
menuKey: "/base/othercompany",
|
||||
moduleId: "b_othercompany",
|
||||
moduleId: "v_b_othercompany",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "base/contact",
|
||||
name: "base-contact",
|
||||
component: () => import("@/views/base/contact/index.vue"),
|
||||
meta: {
|
||||
title: "联系人",
|
||||
section: "基础数据",
|
||||
menuKey: "/base/contact",
|
||||
moduleId: "v_b_contact",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "base/otherdata",
|
||||
name: "base-otherdata",
|
||||
component: () => import("@/views/base/otherdata/index.vue"),
|
||||
meta: {
|
||||
title: "其他数据",
|
||||
section: "基础数据",
|
||||
menuKey: "/base/otherdata",
|
||||
moduleId: "m_base_otherdata",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,6 +80,17 @@ const routes = [
|
||||
moduleId: "s_login_log",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/user",
|
||||
name: "system-user",
|
||||
component: () => import("@/views/system/user/index.vue"),
|
||||
meta: {
|
||||
title: "用户管理",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/user",
|
||||
moduleId: "b_user",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contact-page">
|
||||
<FmsModuleListPage data-code="v_b_contact" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contact-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.contact-page :deep(.fms-module-list-page) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -390,7 +390,7 @@ onMounted(loadCategories);
|
||||
<a-splitter-panel :min="520">
|
||||
<main class="partner-panel">
|
||||
<FmsModuleListPage
|
||||
data-code="b_othercompany"
|
||||
data-code="v_b_othercompany"
|
||||
:fixed-search-condition="categoryFilterCondition"
|
||||
:column-extensions="columnExtensions"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, reactive, ref } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Pencil, Plus, RefreshCw, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
|
||||
const ALL_CATEGORIES_KEY = "__fms_tree_root__";
|
||||
const CATEGORY_COLUMNS = [
|
||||
"b_id", "b_parent_id", "b_name", "b_level", "b_path",
|
||||
"b_xh", "b_canuse", "b_module_code",
|
||||
];
|
||||
|
||||
const categories = ref([]);
|
||||
const categoryLoading = ref(false);
|
||||
const selectedCategoryKey = ref(ALL_CATEGORIES_KEY);
|
||||
const selectedModuleCode = ref("");
|
||||
const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]);
|
||||
|
||||
const categoryById = computed(
|
||||
() => new Map(categories.value.map((item) => [String(item.b_id), item])),
|
||||
);
|
||||
|
||||
const categoryTree = computed(() => {
|
||||
const roots = buildTree(categories.value, {
|
||||
mapNode: (item) => ({
|
||||
key: String(item.b_id),
|
||||
title: Number(item.b_canuse) === 1
|
||||
? item.b_name
|
||||
: `${item.b_name}(停用)`,
|
||||
b_id: String(item.b_id),
|
||||
b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id),
|
||||
b_module_code: item.b_module_code || "",
|
||||
b_xh: item.b_xh,
|
||||
children: [],
|
||||
}),
|
||||
});
|
||||
sortTree(roots);
|
||||
return roots;
|
||||
});
|
||||
|
||||
async function loadCategories() {
|
||||
categoryLoading.value = true;
|
||||
try {
|
||||
const response = await loadDataApi(
|
||||
"b_other_bmfl",
|
||||
"",
|
||||
"b_path ASC, b_xh ASC, b_id ASC",
|
||||
CATEGORY_COLUMNS,
|
||||
);
|
||||
categories.value = response.data || [];
|
||||
|
||||
// 如果之前选中的节点被删了,回到根
|
||||
const existingKeys = new Set(categories.value.map((item) => String(item.b_id)));
|
||||
if (
|
||||
selectedCategoryKey.value !== ALL_CATEGORIES_KEY &&
|
||||
!existingKeys.has(selectedCategoryKey.value)
|
||||
) {
|
||||
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
|
||||
selectedModuleCode.value = "";
|
||||
}
|
||||
|
||||
// 展开所有有子节点的父节点
|
||||
const parentKeys = categories.value
|
||||
.map((item) => (item.b_parent_id == null ? "" : String(item.b_parent_id)))
|
||||
.filter(Boolean);
|
||||
expandedCategoryKeys.value = [
|
||||
...new Set([...expandedCategoryKeys.value, ALL_CATEGORIES_KEY, ...parentKeys]),
|
||||
];
|
||||
} catch (error) {
|
||||
message.error("分类加载失败:" + (error?.message || error));
|
||||
} finally {
|
||||
categoryLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCategorySelect(keys) {
|
||||
if (!keys.length) return;
|
||||
const key = String(keys[0]);
|
||||
selectedCategoryKey.value = key;
|
||||
|
||||
// 判断节点是否关联了数据模块
|
||||
const cat = categoryById.value.get(key);
|
||||
if (cat && cat.b_module_code) {
|
||||
selectedModuleCode.value = cat.b_module_code;
|
||||
} else {
|
||||
selectedModuleCode.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 右键菜单 & 分类维护 ----
|
||||
const addModalVisible = ref(false);
|
||||
const addModalParentId = ref(null);
|
||||
const addModalParentName = ref("");
|
||||
const editingCategoryId = ref(null);
|
||||
const addForm = reactive({ b_name: "", b_module_code: "" });
|
||||
const addSaving = ref(false);
|
||||
const deleteSaving = ref(false);
|
||||
const categoryModalTitle = computed(() =>
|
||||
editingCategoryId.value ? "修改分类" : "新增分类",
|
||||
);
|
||||
|
||||
function buildCategoryContextMenu(node) {
|
||||
if (!node) return { items: [] };
|
||||
const items = [
|
||||
{
|
||||
key: "add-child",
|
||||
label: node.isFmsRoot ? "新增根分类" : "添加子分类",
|
||||
icon: h(Plus, { size: 14 }),
|
||||
},
|
||||
];
|
||||
if (node.isFmsRoot) {
|
||||
items.push({
|
||||
key: "refresh",
|
||||
label: "刷新分类",
|
||||
icon: h(RefreshCw, { size: 14 }),
|
||||
});
|
||||
}
|
||||
if (!node.isFmsRoot) {
|
||||
items.push(
|
||||
{
|
||||
key: "edit",
|
||||
label: "修改分类",
|
||||
icon: h(Pencil, { size: 14 }),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: h(Trash2, { size: 14 }),
|
||||
danger: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
return { items };
|
||||
}
|
||||
|
||||
function onCategoryMenuClick({ key, node }) {
|
||||
if (key === "add-child") {
|
||||
editingCategoryId.value = null;
|
||||
addModalParentId.value = node.isFmsRoot ? null : node.b_id || node.key;
|
||||
addModalParentName.value = node.isFmsRoot ? "" : node.title || "";
|
||||
addForm.b_name = "";
|
||||
addForm.b_module_code = "";
|
||||
addModalVisible.value = true;
|
||||
return;
|
||||
}
|
||||
if (key === "edit") {
|
||||
openEditCategoryModal(node);
|
||||
return;
|
||||
}
|
||||
if (key === "refresh") {
|
||||
loadCategories();
|
||||
return;
|
||||
}
|
||||
if (key === "delete") confirmDeleteCategory(node);
|
||||
}
|
||||
|
||||
function openEditCategoryModal(node) {
|
||||
if (!node || node.isFmsRoot) return;
|
||||
const categoryId = String(node.b_id || node.key || "");
|
||||
const category = categoryById.value.get(categoryId);
|
||||
if (!category) {
|
||||
message.error("未找到需要修改的分类");
|
||||
return;
|
||||
}
|
||||
|
||||
const parentId = category.b_parent_id == null ? null : String(category.b_parent_id);
|
||||
const parent = parentId ? categoryById.value.get(parentId) : null;
|
||||
editingCategoryId.value = categoryId;
|
||||
addModalParentId.value = parentId;
|
||||
addModalParentName.value = parent?.b_name || "";
|
||||
addForm.b_name = category.b_name || "";
|
||||
addForm.b_module_code = category.b_module_code || "";
|
||||
addModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveCategory() {
|
||||
const name = (addForm.b_name || "").trim();
|
||||
if (!name) {
|
||||
message.warning("请填写名称");
|
||||
return;
|
||||
}
|
||||
const moduleCode = (addForm.b_module_code || "").trim() || null;
|
||||
addSaving.value = true;
|
||||
try {
|
||||
if (editingCategoryId.value) {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
updates: [{
|
||||
b_id: editingCategoryId.value,
|
||||
b_name: name,
|
||||
b_module_code: moduleCode,
|
||||
}],
|
||||
},
|
||||
]);
|
||||
message.success("分类已修改");
|
||||
addModalVisible.value = false;
|
||||
await loadCategories();
|
||||
|
||||
// 如果当前选中节点被修改了模块代码,更新选中
|
||||
if (selectedCategoryKey.value === editingCategoryId.value) {
|
||||
const cat = categoryById.value.get(editingCategoryId.value);
|
||||
selectedModuleCode.value = cat?.b_module_code || "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const idRes = await nextIdApi();
|
||||
const newId = String(idRes.data);
|
||||
|
||||
let level = 1;
|
||||
let path = newId;
|
||||
if (addModalParentId.value) {
|
||||
const parent = categoryById.value.get(String(addModalParentId.value));
|
||||
if (parent) {
|
||||
level = (Number(parent.b_level) || 0) + 1;
|
||||
path = (parent.b_path || String(parent.b_id)) + "/" + newId;
|
||||
}
|
||||
}
|
||||
|
||||
const siblings = categories.value.filter((item) =>
|
||||
addModalParentId.value
|
||||
? String(item.b_parent_id) === String(addModalParentId.value)
|
||||
: item.b_parent_id == null,
|
||||
);
|
||||
const maxXh = siblings.reduce((max, item) => Math.max(max, Number(item.b_xh) || 0), 0);
|
||||
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
inserts: [
|
||||
{
|
||||
b_id: newId,
|
||||
b_parent_id: addModalParentId.value || null,
|
||||
b_name: name,
|
||||
b_level: level,
|
||||
b_path: path,
|
||||
b_xh: maxXh + 10,
|
||||
b_canuse: 1,
|
||||
b_module_code: moduleCode,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
message.success("分类已添加");
|
||||
addModalVisible.value = false;
|
||||
await loadCategories();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} finally {
|
||||
addSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasChildCategory(categoryId) {
|
||||
for (const category of categories.value) {
|
||||
if (String(category.b_parent_id || "") === categoryId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function hasPersistedChildCategory(categoryId) {
|
||||
const response = await pageDataApi({
|
||||
viewName: "b_other_bmfl",
|
||||
orderBy: "b_id ASC",
|
||||
searchColumns: ["b_id"],
|
||||
searchCondition: `[b_parent_id] = ${categoryId}`,
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
});
|
||||
return Boolean(response.data?.rows?.length);
|
||||
}
|
||||
|
||||
async function confirmDeleteCategory(node) {
|
||||
if (!node || node.isFmsRoot || deleteSaving.value) return;
|
||||
const categoryId = String(node.b_id || node.key || "");
|
||||
if (!/^\d+$/.test(categoryId)) {
|
||||
message.error("分类 ID 无效,无法删除");
|
||||
return;
|
||||
}
|
||||
if (hasChildCategory(categoryId)) {
|
||||
message.warning("该分类下还有子分类,不能删除");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteSaving.value = true;
|
||||
Modal.confirm({
|
||||
title: `确定删除「${node.title}」?`,
|
||||
content: "此操作不可恢复",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onCancel: () => {
|
||||
deleteSaving.value = false;
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
const hasChild = await hasPersistedChildCategory(categoryId);
|
||||
if (hasChild) {
|
||||
message.warning("该分类下还有子分类,不能删除");
|
||||
return;
|
||||
}
|
||||
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
deletes: [{ b_id: categoryId }],
|
||||
},
|
||||
]);
|
||||
if (selectedCategoryKey.value === categoryId) {
|
||||
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
|
||||
selectedModuleCode.value = "";
|
||||
}
|
||||
message.success("分类已删除");
|
||||
await loadCategories();
|
||||
} catch (error) {
|
||||
message.error("删除失败:" + (error?.message || error));
|
||||
throw error;
|
||||
} finally {
|
||||
deleteSaving.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadCategories);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="otherdata-page">
|
||||
<a-splitter class="otherdata-splitter" lazy>
|
||||
<a-splitter-panel :default-size="250" :min="210" :max="380" collapsible>
|
||||
<aside class="category-panel">
|
||||
<div class="category-tree-stage">
|
||||
<FmsTree
|
||||
v-model:expanded-keys="expandedCategoryKeys"
|
||||
:tree-data="categoryTree"
|
||||
:selected-keys="[selectedCategoryKey]"
|
||||
root-title="其他数据"
|
||||
:context-menu="buildCategoryContextMenu"
|
||||
:context-menu-disabled="categoryLoading || addSaving || deleteSaving"
|
||||
@select="onCategorySelect"
|
||||
@menu-click="onCategoryMenuClick"
|
||||
/>
|
||||
<div v-if="categoryLoading" class="category-loading">
|
||||
<a-spin size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</a-splitter-panel>
|
||||
|
||||
<a-splitter-panel :min="520">
|
||||
<main class="data-panel">
|
||||
<FmsModuleListPage
|
||||
v-if="selectedModuleCode"
|
||||
:key="selectedModuleCode"
|
||||
:data-code="selectedModuleCode"
|
||||
/>
|
||||
<a-empty
|
||||
v-else
|
||||
description="请选择具体数据项(叶子节点)"
|
||||
/>
|
||||
</main>
|
||||
</a-splitter-panel>
|
||||
</a-splitter>
|
||||
|
||||
<a-modal
|
||||
v-model:open="addModalVisible"
|
||||
:title="categoryModalTitle"
|
||||
:confirm-loading="addSaving"
|
||||
@ok="saveCategory"
|
||||
>
|
||||
<a-form layout="vertical" style="margin-top: 16px">
|
||||
<a-form-item v-if="addModalParentName" label="上级分类">
|
||||
<a-input :value="addModalParentName" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称">
|
||||
<a-input v-model:value="addForm.b_name" :maxlength="150" />
|
||||
</a-form-item>
|
||||
<a-form-item label="数据模块代码">
|
||||
<a-input
|
||||
v-model:value="addForm.b_module_code"
|
||||
:maxlength="100"
|
||||
placeholder="留空则为分类分组节点;填入则为数据模块叶子节点(如 d_other_port)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.otherdata-page,
|
||||
.otherdata-splitter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.otherdata-page {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.category-panel,
|
||||
.data-panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-tree-stage {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.category-tree-stage :deep(.ant-tree) {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.category-tree-stage :deep(.ant-tree-node-content-wrapper) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgb(255 255 255 / 70%);
|
||||
}
|
||||
|
||||
.data-panel :deep(.fms-module-list-page) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -47,12 +47,12 @@ function onRowOrderChange(rows) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
const fieldNameMap = computed(() => {
|
||||
const map = new Map();
|
||||
for (const field of props.fields || []) {
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
map.set(String(field.b_id), field.b_name || field.b_field);
|
||||
}
|
||||
return options;
|
||||
return map;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() => {
|
||||
@@ -119,9 +119,10 @@ const columns = shallowRef([
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
customCell: ({ row }) => {
|
||||
const id = row.b_field_id;
|
||||
if (id == null) return "";
|
||||
return fieldNameMap.value.get(String(id)) || id;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -302,6 +302,15 @@ function flatten(rows) {
|
||||
return flat;
|
||||
}
|
||||
|
||||
// 由父级「同步字段」调用:移除匹配指定 _managedId 的透视行(字段定义已删除时清理残留翻译)。
|
||||
function removeByManagedIds(ids) {
|
||||
if (!ids || ids.size === 0) return;
|
||||
const idSet = new Set(Array.from(ids).map(String));
|
||||
pivotRows.value = pivotRows.value.filter(
|
||||
(row) => !(row._managedId && idSet.has(String(row._managedId))),
|
||||
);
|
||||
}
|
||||
|
||||
// 由父级「同步字段」调用:把默认翻译条目({ key, locale, value, managedId })并入草稿行。
|
||||
// 已存在同 b_key 的行只补全空值(不覆盖用户配置),避免新增重复键;逻辑见 utils/i18n.mergeI18nDefaults。
|
||||
function mergeDefaults(entries) {
|
||||
@@ -374,7 +383,7 @@ function markSaved() {
|
||||
originalsFlat.value = cloneData(flatten(pivotRows.value));
|
||||
}
|
||||
|
||||
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows });
|
||||
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows, removeByManagedIds });
|
||||
|
||||
const displayedData = computed(() => {
|
||||
const kw = searchKey.value.toLowerCase();
|
||||
|
||||
@@ -47,12 +47,12 @@ function onRowOrderChange(rows) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
const fieldNameMap = computed(() => {
|
||||
const map = new Map();
|
||||
for (const field of props.fields || []) {
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
map.set(String(field.b_id), field.b_name || field.b_field);
|
||||
}
|
||||
return options;
|
||||
return map;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() => {
|
||||
@@ -119,12 +119,18 @@ const columns = shallowRef([
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
customCell: ({ row }) => {
|
||||
const id = row.b_field_id;
|
||||
if (id == null) return "";
|
||||
return fieldNameMap.value.get(String(id)) || id;
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
{
|
||||
dataIndex: "b_width",
|
||||
title: "列宽",
|
||||
width: 80,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
|
||||
@@ -56,12 +56,12 @@ function onRowOrderChange(rows) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
const fieldNameMap = computed(() => {
|
||||
const map = new Map();
|
||||
for (const field of props.fields || []) {
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
map.set(String(field.b_id), field.b_name || field.b_field);
|
||||
}
|
||||
return options;
|
||||
return map;
|
||||
});
|
||||
|
||||
function addRow() {
|
||||
@@ -84,9 +84,10 @@ const columns = shallowRef([
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: () => availableFields.value,
|
||||
customCell: ({ row }) => {
|
||||
const id = row.b_field_id;
|
||||
if (id == null) return "";
|
||||
return fieldNameMap.value.get(String(id)) || id;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,6 +38,7 @@ const treeLoading = ref(false);
|
||||
const expandedTreeKeys = ref([]);
|
||||
const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const expandTimer = ref(null);
|
||||
const activeTab = ref("basic");
|
||||
function buildTreeContextMenu(node) {
|
||||
const items = [];
|
||||
@@ -442,24 +443,54 @@ async function loadModuleConfig(moduleId) {
|
||||
}
|
||||
|
||||
// ---- 树节点选择 ----
|
||||
function clearExpandTimer() {
|
||||
if (expandTimer.value != null) {
|
||||
clearTimeout(expandTimer.value);
|
||||
expandTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onTreeSelect(keys, info) {
|
||||
if (busy.value) return;
|
||||
if (info?.node?.isFmsRoot) {
|
||||
selectedModuleId.value = null;
|
||||
clearExpandTimer();
|
||||
return;
|
||||
}
|
||||
const nodeKey = info?.node?.key ?? keys[0];
|
||||
if (nodeKey == null) return;
|
||||
|
||||
const key = String(nodeKey);
|
||||
selectedModuleId.value = key;
|
||||
|
||||
const node = findNode(key);
|
||||
if (!node?.children?.length) return;
|
||||
const hasChildren = node?.children?.length;
|
||||
|
||||
clearExpandTimer();
|
||||
|
||||
if (!hasChildren) {
|
||||
// 叶子节点:立即加载数据
|
||||
selectedModuleId.value = key;
|
||||
return;
|
||||
}
|
||||
|
||||
// 父节点:处理展开/收起
|
||||
const expandedKeys = expandedTreeKeys.value.map(String);
|
||||
expandedTreeKeys.value = expandedKeys.includes(key)
|
||||
? expandedKeys.filter((item) => item !== key)
|
||||
: [...expandedKeys, key];
|
||||
const isCurrentlyExpanded = expandedKeys.includes(key);
|
||||
|
||||
if (isCurrentlyExpanded) {
|
||||
// 收起:立即生效,同时加载数据
|
||||
expandedTreeKeys.value = expandedKeys.filter((item) => item !== key);
|
||||
selectedModuleId.value = key;
|
||||
} else {
|
||||
// 展开:立即高亮选中节点,动画完毕后再加载数据
|
||||
expandedTreeKeys.value = [...expandedKeys, key];
|
||||
skipLoad.value = true;
|
||||
selectedModuleId.value = key; // 立即高亮,watch 中 skipLoad=true 跳过加载
|
||||
expandTimer.value = setTimeout(() => {
|
||||
expandTimer.value = null;
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(key);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
@@ -1009,11 +1040,26 @@ async function syncFields() {
|
||||
}
|
||||
}
|
||||
|
||||
// 视图中不存在的已有字段标记为不可用
|
||||
// 视图中不存在的已有字段:从字段定义中移除,并清理其在列表/编辑/查询配置中的残留行。
|
||||
const removedFieldIds = new Set();
|
||||
for (const existing of existingFields) {
|
||||
const fieldName = existing.b_field ? existing.b_field.toLowerCase() : "";
|
||||
if (!viewColumnSet.has(fieldName)) {
|
||||
mergedFields.push({ ...existing, b_canuse: 0 });
|
||||
removedFieldIds.add(String(existing.b_id));
|
||||
}
|
||||
}
|
||||
if (removedFieldIds.size > 0) {
|
||||
for (const section of ["listConfig", "editConfig", "queryConfig"]) {
|
||||
drafts.value[section] = (drafts.value[section] || []).filter(
|
||||
(row) => !removedFieldIds.has(String(row.b_field_id)),
|
||||
);
|
||||
}
|
||||
// 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:<b_id>)
|
||||
if (i18nPanelRef.value) {
|
||||
const managedIds = new Set(
|
||||
Array.from(removedFieldIds).map((id) => `field:${id}`),
|
||||
);
|
||||
i18nPanelRef.value.removeByManagedIds(managedIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1125,12 +1171,8 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:context-menu="buildTreeContextMenu"
|
||||
:context-menu-disabled="busy"
|
||||
draggable
|
||||
:allow-drop="allowDrop"
|
||||
@select="onTreeSelect"
|
||||
@menu-click="onTreeMenuClick"
|
||||
@drop="onTreeDrop"
|
||||
@drag-start="onTreeDragStart"
|
||||
>
|
||||
<template #title="node">
|
||||
<span class="mm-tree-node">
|
||||
|
||||
@@ -39,7 +39,7 @@ function batchDelete() {
|
||||
const deletes = keys.map((b_id) => ({ b_id }));
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "s_log_login",
|
||||
table: "s_login_log",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<FmsModuleListPage data-code="b_user" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.user-page :deep(.fms-module-list-page) {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user