20260729231854
This commit is contained in:
1 parent
1c839b707a
commit
84673b3d3d
39 files changed
+4683
-281
No files matched your search
@@ -1,22 +1,93 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, useAttrs, watch } from "vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const attrs = useAttrs();
|
||||
const handledEventAttrs = new Set([
|
||||
"onDrop",
|
||||
"onDragStart",
|
||||
"onDragstart",
|
||||
"onDragEnter",
|
||||
"onDragenter",
|
||||
"onDragLeave",
|
||||
"onDragleave",
|
||||
"onDragOver",
|
||||
"onDragover",
|
||||
"onDragEnd",
|
||||
"onDragend",
|
||||
"onRightClick",
|
||||
"onUpdate:expandedKeys",
|
||||
]);
|
||||
|
||||
const treeAttrs = computed(() => {
|
||||
const result = {};
|
||||
for (const key in attrs) {
|
||||
if (!handledEventAttrs.has(key)) result[key] = attrs[key];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
treeData: { type: Array, default: () => [] },
|
||||
expandedKeys: { type: Array, default: undefined },
|
||||
blockNode: { type: Boolean, default: true },
|
||||
virtual: { type: Boolean, default: true },
|
||||
draggable: { type: [Boolean, Function, Object], default: false },
|
||||
allowDrop: { type: Function, default: null },
|
||||
rootKey: { type: [String, Number], default: "__fms_tree_root__" },
|
||||
rootTitle: { type: String, default: "全部分类" },
|
||||
// 右键菜单生成函数:(node) => { items: [...] }
|
||||
contextMenu: { type: Function, default: null },
|
||||
contextMenuDisabled: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["menuClick"]);
|
||||
// 虚拟滚动与拖拽冲突:draggable 开启时强制关闭 virtual
|
||||
const effectiveVirtual = computed(() => {
|
||||
if (props.draggable) return false;
|
||||
return props.virtual;
|
||||
});
|
||||
|
||||
const effectiveTreeData = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: props.rootKey,
|
||||
title: props.rootTitle,
|
||||
isFmsRoot: true,
|
||||
children: props.treeData,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"menuClick",
|
||||
"drop",
|
||||
"dragStart",
|
||||
"dragEnter",
|
||||
"dragLeave",
|
||||
"dragOver",
|
||||
"dragEnd",
|
||||
"rightClick",
|
||||
"update:expandedKeys",
|
||||
]);
|
||||
|
||||
let rootExpansionInitialized = false;
|
||||
watch(
|
||||
() => props.expandedKeys,
|
||||
(keys) => {
|
||||
if (rootExpansionInitialized || !Array.isArray(keys)) return;
|
||||
rootExpansionInitialized = true;
|
||||
const hasRoot = keys.some((key) => String(key) === String(props.rootKey));
|
||||
if (!hasRoot) emit("update:expandedKeys", [props.rootKey, ...keys]);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const contextNode = ref(null);
|
||||
|
||||
function onTreeRightClick({ node }) {
|
||||
contextNode.value = node;
|
||||
function onTreeRightClick(info) {
|
||||
contextNode.value = info.node;
|
||||
emit("rightClick", info);
|
||||
}
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
@@ -31,6 +102,10 @@ function onMenuClick({ key }) {
|
||||
contextNode.value = null;
|
||||
if (node) emit("menuClick", { key, node });
|
||||
}
|
||||
|
||||
function onExpandedKeysChange(keys) {
|
||||
emit("update:expandedKeys", keys);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,10 +117,23 @@ function onMenuClick({ key }) {
|
||||
>
|
||||
<div class="fms-tree-wrapper">
|
||||
<a-tree
|
||||
v-bind="$attrs"
|
||||
v-bind="treeAttrs"
|
||||
:tree-data="effectiveTreeData"
|
||||
:expanded-keys="expandedKeys"
|
||||
:default-expanded-keys="[rootKey]"
|
||||
:block-node="blockNode"
|
||||
:virtual="virtual"
|
||||
:virtual="effectiveVirtual"
|
||||
show-line
|
||||
:draggable="draggable"
|
||||
:allow-drop="allowDrop"
|
||||
@right-click="onTreeRightClick"
|
||||
@drop="$emit('drop', $event)"
|
||||
@dragstart="$emit('dragStart', $event)"
|
||||
@dragenter="$emit('dragEnter', $event)"
|
||||
@dragleave="$emit('dragLeave', $event)"
|
||||
@dragover="$emit('dragOver', $event)"
|
||||
@dragend="$emit('dragEnd', $event)"
|
||||
@update:expanded-keys="onExpandedKeysChange"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
@@ -53,7 +141,25 @@ function onMenuClick({ key }) {
|
||||
</a-tree>
|
||||
</div>
|
||||
</a-dropdown>
|
||||
<a-tree v-else v-bind="$attrs" :block-node="blockNode" :virtual="virtual">
|
||||
<a-tree
|
||||
v-else
|
||||
v-bind="treeAttrs"
|
||||
:tree-data="effectiveTreeData"
|
||||
:expanded-keys="expandedKeys"
|
||||
:default-expanded-keys="[rootKey]"
|
||||
:block-node="blockNode"
|
||||
:virtual="effectiveVirtual"
|
||||
show-line
|
||||
:draggable="draggable"
|
||||
:allow-drop="allowDrop"
|
||||
@drop="$emit('drop', $event)"
|
||||
@dragstart="$emit('dragStart', $event)"
|
||||
@dragenter="$emit('dragEnter', $event)"
|
||||
@dragleave="$emit('dragLeave', $event)"
|
||||
@dragover="$emit('dragOver', $event)"
|
||||
@dragend="$emit('dragEnd', $event)"
|
||||
@update:expanded-keys="onExpandedKeysChange"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
|
||||
@@ -322,7 +322,6 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
grid-template-columns: minmax(0, 1fr) 172px;
|
||||
gap: 14px;
|
||||
padding: 10px 12px 12px;
|
||||
border-left: 1px solid var(--fms-border, #e5e7eb);
|
||||
border-bottom: 1px solid var(--fms-border, #e5e7eb);
|
||||
background: var(--fms-surface, #fff);
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ const routes = [
|
||||
title: "合作伙伴",
|
||||
section: "基础数据",
|
||||
menuKey: "/base/othercompany",
|
||||
moduleId: "base_othercompany",
|
||||
dataCode: "b_othercompany",
|
||||
moduleId: "b_othercompany",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
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 { escapeSqlText } from "@/components/fms-module-list/moduleListUtils";
|
||||
import { loadDataApi } from "@/services/api";
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
|
||||
const ALL_CATEGORIES_KEY = "__all_categories__";
|
||||
const CATEGORY_COLUMNS = [
|
||||
"b_id",
|
||||
"b_parent_id",
|
||||
"b_code",
|
||||
"b_name",
|
||||
"b_level",
|
||||
"b_path",
|
||||
"b_xh",
|
||||
"b_canuse",
|
||||
];
|
||||
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"];
|
||||
|
||||
const categories = ref([]);
|
||||
const categoryLoading = ref(false);
|
||||
@@ -41,14 +32,7 @@ const categoryTree = computed(() => {
|
||||
}),
|
||||
});
|
||||
sortTree(roots);
|
||||
return [
|
||||
{
|
||||
key: ALL_CATEGORIES_KEY,
|
||||
title: "全部合作伙伴",
|
||||
b_xh: 0,
|
||||
children: roots,
|
||||
},
|
||||
];
|
||||
return roots;
|
||||
});
|
||||
|
||||
const categoryFilterCondition = computed(() => {
|
||||
@@ -121,6 +105,265 @@ function onCategorySelect(keys) {
|
||||
if (keys.length) selectedCategoryKey.value = String(keys[0]);
|
||||
}
|
||||
|
||||
// ---- 右键菜单 & 分类维护 ----
|
||||
const addModalVisible = ref(false);
|
||||
const addModalParentId = ref(null);
|
||||
const addModalParentName = ref("");
|
||||
const editingCategoryId = ref(null);
|
||||
const addForm = reactive({ b_name: "" });
|
||||
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 = "";
|
||||
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 || "";
|
||||
addModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveCategory() {
|
||||
const name = (addForm.b_name || "").trim();
|
||||
if (!name) {
|
||||
message.warning("请填写名称");
|
||||
return;
|
||||
}
|
||||
addSaving.value = true;
|
||||
try {
|
||||
if (editingCategoryId.value) {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_othercompany_bmfl",
|
||||
key_field: "b_id",
|
||||
updates: [{ b_id: editingCategoryId.value, b_name: name }],
|
||||
},
|
||||
]);
|
||||
message.success("分类已修改");
|
||||
addModalVisible.value = false;
|
||||
await loadCategories();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算 b_xh:同级最大值 + 10
|
||||
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_othercompany_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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
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 hasCategoryData(categoryId) {
|
||||
const response = await pageDataApi({
|
||||
viewName: "b_othercompany",
|
||||
orderBy: "b_id ASC",
|
||||
searchColumns: ["b_id"],
|
||||
searchCondition: `[b_cate_id] = N'${escapeSqlText(categoryId)}'`,
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
});
|
||||
return Boolean(response.data?.rows?.length);
|
||||
}
|
||||
|
||||
async function hasPersistedChildCategory(categoryId) {
|
||||
const response = await pageDataApi({
|
||||
viewName: "b_othercompany_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;
|
||||
try {
|
||||
if (await hasCategoryData(categoryId)) {
|
||||
message.warning("该分类下还有合作伙伴数据,不能删除");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("分类数据检查失败:" + (error?.message || error));
|
||||
return;
|
||||
} finally {
|
||||
deleteSaving.value = false;
|
||||
}
|
||||
|
||||
deleteSaving.value = true;
|
||||
Modal.confirm({
|
||||
title: `确定删除「${node.title}」?`,
|
||||
content: "此操作不可恢复",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onCancel: () => {
|
||||
deleteSaving.value = false;
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
const [hasChild, hasData] = await Promise.all([
|
||||
hasPersistedChildCategory(categoryId),
|
||||
hasCategoryData(categoryId),
|
||||
]);
|
||||
if (hasChild) {
|
||||
message.warning("该分类下还有子分类,不能删除");
|
||||
return;
|
||||
}
|
||||
if (hasData) {
|
||||
message.warning("该分类下还有合作伙伴数据,不能删除");
|
||||
return;
|
||||
}
|
||||
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_othercompany_bmfl",
|
||||
key_field: "b_id",
|
||||
deletes: [{ b_id: categoryId }],
|
||||
},
|
||||
]);
|
||||
if (selectedCategoryKey.value === categoryId) {
|
||||
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
|
||||
}
|
||||
message.success("分类已删除");
|
||||
await loadCategories();
|
||||
} catch (error) {
|
||||
message.error("删除失败:" + (error?.message || error));
|
||||
throw error;
|
||||
} finally {
|
||||
deleteSaving.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadCategories);
|
||||
</script>
|
||||
|
||||
@@ -129,26 +372,15 @@ onMounted(loadCategories);
|
||||
<a-splitter class="othercompany-splitter" lazy>
|
||||
<a-splitter-panel :default-size="250" :min="210" :max="380" collapsible>
|
||||
<aside class="category-panel">
|
||||
<header class="category-header">
|
||||
<div class="category-title">合作伙伴分类</div>
|
||||
<a-tooltip title="刷新分类">
|
||||
<a-button
|
||||
type="text"
|
||||
:disabled="categoryLoading"
|
||||
aria-label="刷新分类"
|
||||
@click="loadCategories"
|
||||
>
|
||||
<RefreshCw :size="15" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</header>
|
||||
<div class="category-tree-stage">
|
||||
<FmsTree
|
||||
v-model:expanded-keys="expandedCategoryKeys"
|
||||
:tree-data="categoryTree"
|
||||
:selected-keys="[selectedCategoryKey]"
|
||||
show-line
|
||||
: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>
|
||||
@@ -165,6 +397,22 @@ onMounted(loadCategories);
|
||||
</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>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -194,35 +442,12 @@ onMounted(loadCategories);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 4px 6px 4px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-header :deep(.ant-btn) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.category-tree-stage {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 6px 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.category-tree-stage :deep(.ant-tree) {
|
||||
|
||||
+17
-1
@@ -1,12 +1,28 @@
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const BUSINESS_CODE_FIELDS = [
|
||||
"b_ywno",
|
||||
"b_htno",
|
||||
"b_code",
|
||||
"b_account",
|
||||
"b_bill_no",
|
||||
"b_invoice_no",
|
||||
"b_business_id",
|
||||
];
|
||||
|
||||
function defaultFieldId(fields) {
|
||||
const field = (fields || []).find(
|
||||
const textFields = (fields || []).filter(
|
||||
(item) =>
|
||||
Number(item.b_canuse) === 1 &&
|
||||
item.b_field !== "b_id" &&
|
||||
(item.b_type === "input" || item.b_type === "textarea"),
|
||||
);
|
||||
let field = null;
|
||||
for (const fieldName of BUSINESS_CODE_FIELDS) {
|
||||
field = textFields.find((item) => item.b_field === fieldName);
|
||||
if (field) break;
|
||||
}
|
||||
if (!field) field = textFields[0];
|
||||
return field?.b_id != null ? String(field.b_id) : null;
|
||||
}
|
||||
|
||||
@@ -73,11 +73,11 @@ function addGroup(parentId) {
|
||||
selectedGroupId.value = String(newGroup.b_id);
|
||||
}
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
function deleteGroupById(id) {
|
||||
if (!id) return;
|
||||
let hasChildren = false;
|
||||
for (const group of localGroups.value) {
|
||||
if (String(group.b_parent_id) === selectedGroupId.value) {
|
||||
if (String(group.b_parent_id) === String(id)) {
|
||||
hasChildren = true;
|
||||
break;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ function deleteGroup() {
|
||||
}
|
||||
let isReferenced = false;
|
||||
for (const groupId of props.referencedGroupIds) {
|
||||
if (String(groupId) === selectedGroupId.value) {
|
||||
if (String(groupId) === String(id)) {
|
||||
isReferenced = true;
|
||||
break;
|
||||
}
|
||||
@@ -97,8 +97,27 @@ function deleteGroup() {
|
||||
message.warning("该分组仍被列表或编辑配置引用,请先清空引用");
|
||||
return;
|
||||
}
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
|
||||
selectedGroupId.value = null;
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== String(id));
|
||||
if (String(selectedGroupId.value) === String(id)) {
|
||||
selectedGroupId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildContextMenu(node) {
|
||||
if (!node) return { items: [] };
|
||||
const items = [];
|
||||
items.push({ key: "add-child", label: "添加子分组" });
|
||||
items.push({ type: "divider" });
|
||||
items.push({ key: "delete", label: "删除", danger: true });
|
||||
return { items };
|
||||
}
|
||||
|
||||
function onMenuClick({ key, node }) {
|
||||
if (key === "add-child") {
|
||||
addGroup(node.key);
|
||||
} else if (key === "delete") {
|
||||
deleteGroupById(node.key);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
@@ -142,57 +161,54 @@ function onClose() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="visible" title="分组管理" width="650px" @cancel="onClose" @ok="saveAllGroups">
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="分组管理"
|
||||
width="680px"
|
||||
:destroy-on-hidden="true"
|
||||
@cancel="onClose"
|
||||
@ok="saveAllGroups"
|
||||
>
|
||||
<div class="group-manager">
|
||||
<div class="group-sidebar">
|
||||
<div class="group-sidebar-header">
|
||||
<span>分组树</span>
|
||||
<a-button size="small" type="primary" @click="addGroup(null)">新增</a-button>
|
||||
</div>
|
||||
<FmsTree
|
||||
v-if="treeData.length > 0"
|
||||
class="group-tree"
|
||||
:tree-data="treeData"
|
||||
:selected-keys="selectedGroupId ? [selectedGroupId] : []"
|
||||
:context-menu="buildContextMenu"
|
||||
@select="onTreeSelect"
|
||||
>
|
||||
<template #title="{ key, title: nodeTitle }">
|
||||
<span class="group-tree-node">
|
||||
<span>{{ nodeTitle }}</span>
|
||||
<span class="group-tree-node-actions">
|
||||
<a-button size="small" type="text" @click.stop="addGroup(key)">+</a-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
<a-empty v-else description="暂无分组" />
|
||||
@menu-click="onMenuClick"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selectedGroup" class="group-detail">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="分组标题" required>
|
||||
<a-input v-model:value="selectedGroup.b_title" />
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="selectedGroup.b_i18n"
|
||||
@update:value="(v) => (selectedGroup.b_i18n = v || null)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number v-model:value="selectedGroup.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-checkbox
|
||||
:checked="selectedGroup.b_canuse === 1"
|
||||
@update:checked="(v) => (selectedGroup.b_canuse = v ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button danger size="small" @click="deleteGroup">删除此分组</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="group-detail">
|
||||
<template v-if="selectedGroup">
|
||||
<a-form layout="vertical" class="group-form">
|
||||
<a-form-item label="标题" required>
|
||||
<a-input v-model:value="selectedGroup.b_title" />
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="selectedGroup.b_i18n"
|
||||
@update:value="(v) => (selectedGroup.b_i18n = v || null)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="group-form-row">
|
||||
<a-form-item label="排序号" style="margin-bottom: 0">
|
||||
<a-input-number v-model:value="selectedGroup.b_xh" :min="0" style="width: 100px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用" style="margin-bottom: 0">
|
||||
<a-checkbox
|
||||
:checked="selectedGroup.b_canuse === 1"
|
||||
@update:checked="(v) => (selectedGroup.b_canuse = v ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
<div v-else class="group-detail-empty">
|
||||
<a-empty description="右键树节点可添加 / 删除分组" />
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="选择左侧分组节点编辑" class="group-detail-empty" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -200,52 +216,26 @@ function onClose() {
|
||||
<style scoped lang="scss">
|
||||
.group-manager {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 350px;
|
||||
gap: 20px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.group-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
width: 240px;
|
||||
width: 200px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: var(--fms-bg);
|
||||
}
|
||||
|
||||
.group-tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.group-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group-tree-node-actions {
|
||||
display: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.group-tree-node:hover .group-tree-node-actions {
|
||||
display: flex;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.group-detail {
|
||||
@@ -253,10 +243,23 @@ function onClose() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-form {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-form-row {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.group-detail-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 260px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { createAutoCodeDraft } from "../autoCode";
|
||||
import { createAutoCodeDraft } from "../common";
|
||||
|
||||
const DATE_FORMAT_OPTIONS = [
|
||||
{ label: "不使用日期", value: "" },
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script setup>
|
||||
import { computed, ref, shallowRef, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "groupsSaved"]);
|
||||
const emit = defineEmits(["update", "groupsSaved", "syncList", "syncQuery"]);
|
||||
|
||||
const localEdit = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
@@ -61,10 +64,54 @@ const groupOptions = computed(() => {
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
function syncToList() {
|
||||
if (props.loading) return;
|
||||
localEdit.value = localEdit.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
const visibleMap = new Map();
|
||||
for (const row of localEdit.value) {
|
||||
if (row.b_field_id != null) {
|
||||
visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
if (visibleMap.size === 0) {
|
||||
message.info("编辑配置中没有字段");
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const updated = (props.listConfig || []).map((row) => {
|
||||
const target = visibleMap.get(String(row.b_field_id));
|
||||
if (target !== undefined) {
|
||||
count++;
|
||||
return { ...row, b_visible: target };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
emit("syncList", updated);
|
||||
message.success(`已同步 ${count} 条到列表配置`);
|
||||
}
|
||||
|
||||
function syncToQuery() {
|
||||
if (props.loading) return;
|
||||
const visibleMap = new Map();
|
||||
for (const row of localEdit.value) {
|
||||
if (row.b_field_id != null) {
|
||||
visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
if (visibleMap.size === 0) {
|
||||
message.info("编辑配置中没有字段");
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const updated = (props.queryConfig || []).map((row) => {
|
||||
const target = visibleMap.get(String(row.b_field_id));
|
||||
if (target !== undefined) {
|
||||
count++;
|
||||
return { ...row, b_canuse: target };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
emit("syncQuery", updated);
|
||||
message.success(`已同步 ${count} 条到查询配置`);
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
@@ -115,20 +162,6 @@ const columns = shallowRef([
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
actions: [
|
||||
{
|
||||
type: "danger",
|
||||
label: "删除",
|
||||
confirm: { title: "确定删除该记录?", okText: "删除" },
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -137,6 +170,12 @@ const columns = shallowRef([
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button :disabled="loading" @click="groupManagerVisible = true">分组管理</a-button>
|
||||
<a-button :disabled="loading || !listConfig.length" @click="syncToList"
|
||||
>同步到列表配置</a-button
|
||||
>
|
||||
<a-button :disabled="loading || !queryConfig.length" @click="syncToQuery"
|
||||
>同步到查询配置</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script setup>
|
||||
import { computed, ref, shallowRef, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "groupsSaved"]);
|
||||
const emit = defineEmits(["update", "groupsSaved", "syncEdit", "syncQuery"]);
|
||||
|
||||
const localList = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
@@ -61,10 +64,54 @@ const groupOptions = computed(() => {
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
function syncToEdit() {
|
||||
if (props.loading) return;
|
||||
localList.value = localList.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
const visibleMap = new Map();
|
||||
for (const row of localList.value) {
|
||||
if (row.b_field_id != null) {
|
||||
visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
if (visibleMap.size === 0) {
|
||||
message.info("列表配置中没有字段");
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const updated = (props.editConfig || []).map((row) => {
|
||||
const target = visibleMap.get(String(row.b_field_id));
|
||||
if (target !== undefined) {
|
||||
count++;
|
||||
return { ...row, b_visible: target };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
emit("syncEdit", updated);
|
||||
message.success(`已同步 ${count} 条到编辑显示`);
|
||||
}
|
||||
|
||||
function syncToQuery() {
|
||||
if (props.loading) return;
|
||||
const visibleMap = new Map();
|
||||
for (const row of localList.value) {
|
||||
if (row.b_field_id != null) {
|
||||
visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
if (visibleMap.size === 0) {
|
||||
message.info("列表配置中没有字段");
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const updated = (props.queryConfig || []).map((row) => {
|
||||
const target = visibleMap.get(String(row.b_field_id));
|
||||
if (target !== undefined) {
|
||||
count++;
|
||||
return { ...row, b_canuse: target };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
emit("syncQuery", updated);
|
||||
message.success(`已同步 ${count} 条到查询配置`);
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
@@ -95,20 +142,6 @@ const columns = shallowRef([
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
actions: [
|
||||
{
|
||||
type: "danger",
|
||||
label: "删除",
|
||||
confirm: { title: "确定删除该记录?", okText: "删除" },
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -117,6 +150,12 @@ const columns = shallowRef([
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button :disabled="loading" @click="groupManagerVisible = true">分组管理</a-button>
|
||||
<a-button :disabled="loading || !editConfig.length" @click="syncToEdit"
|
||||
>同步到编辑显示</a-button
|
||||
>
|
||||
<a-button :disabled="loading || !queryConfig.length" @click="syncToQuery"
|
||||
>同步到查询配置</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -79,12 +79,6 @@ function addRow() {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
localQuery.value = localQuery.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
@@ -122,20 +116,6 @@ const columns = shallowRef([
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
actions: [
|
||||
{
|
||||
type: "danger",
|
||||
label: "删除",
|
||||
confirm: { title: "确定删除该记录?", okText: "删除" },
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
syncModuleItemI18nKeys,
|
||||
validateModuleCode,
|
||||
} from "@/utils/i18n";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
||||
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
||||
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
|
||||
@@ -30,7 +29,7 @@ import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
|
||||
import ModuleAutoCodePanel from "./components/ModuleAutoCodePanel.vue";
|
||||
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
|
||||
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
||||
import { createAutoCodeDraft } from "./autoCode";
|
||||
import { createAutoCodeDraft } from "./common";
|
||||
|
||||
// ---- 状态 ----
|
||||
const appStore = useAppStore();
|
||||
@@ -43,6 +42,10 @@ const activeTab = ref("basic");
|
||||
function buildTreeContextMenu(node) {
|
||||
const items = [];
|
||||
if (!node) return { items };
|
||||
if (node.isFmsRoot) {
|
||||
items.push({ label: "添加目录", key: "add-directory" });
|
||||
return { items };
|
||||
}
|
||||
if (node.b_module_type === "directory") {
|
||||
items.push(
|
||||
{ label: "添加目录", key: "add-directory" },
|
||||
@@ -241,11 +244,131 @@ async function loadTree() {
|
||||
try {
|
||||
const res = await loadDataApi("s_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
treeData.value = buildModuleTree(res.data || []);
|
||||
unsavedModuleIds.clear();
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 拖拽 ----
|
||||
const dragNodeKey = ref(null);
|
||||
const unsavedModuleIds = new Set();
|
||||
|
||||
function onTreeDragStart(info) {
|
||||
dragNodeKey.value = info.node.key;
|
||||
}
|
||||
|
||||
function isDescendantOf(ancestorKey, descendantKey) {
|
||||
const ancestorNode = findNode(ancestorKey);
|
||||
if (!ancestorNode) return false;
|
||||
const walk = (nodes) => {
|
||||
for (const n of nodes || []) {
|
||||
if (String(n.key || n.b_id) === String(descendantKey)) return true;
|
||||
if (walk(n.children)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return walk(ancestorNode.children);
|
||||
}
|
||||
|
||||
function allowDrop({ dragNode, dropNode }) {
|
||||
let dragKey = dragNodeKey.value;
|
||||
if (dragNode && dragNode.key) {
|
||||
dragKey = dragNode.key;
|
||||
}
|
||||
if (!dragKey || !dropNode || dragNode?.isFmsRoot) return false;
|
||||
if (String(dropNode.key) === String(dragKey)) return false;
|
||||
if (isDescendantOf(dragKey, dropNode.key)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onTreeDrop(info) {
|
||||
const dropKey = info.node.key;
|
||||
const dragKey = info.dragNode.key;
|
||||
const dropPos = info.node.pos.split("-");
|
||||
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
|
||||
|
||||
const data = cloneData(treeData.value);
|
||||
|
||||
// 从树中移除拖拽节点
|
||||
let dragObj = null;
|
||||
function removeFromTree(nodes, key) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (String(nodes[i].key) === String(key)) {
|
||||
dragObj = nodes.splice(i, 1)[0];
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children && removeFromTree(nodes[i].children, key)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
removeFromTree(data, dragKey);
|
||||
if (!dragObj) {
|
||||
dragNodeKey.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 插入到新位置
|
||||
function insertIntoTree(nodes, dropNodeKey, parentNode) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (String(nodes[i].key) === String(dropNodeKey)) {
|
||||
if (!info.dropToGap) {
|
||||
// 拖放到节点上 → 成为其子节点
|
||||
nodes[i].children = nodes[i].children || [];
|
||||
nodes[i].children.unshift(dragObj);
|
||||
dragObj.b_parent_id = nodes[i].b_id;
|
||||
} else if (dropPosition === -1) {
|
||||
// 拖放到节点上方 → 同级插入
|
||||
nodes.splice(i, 0, dragObj);
|
||||
dragObj.b_parent_id = parentNode ? parentNode.b_id : null;
|
||||
} else {
|
||||
// 拖放到节点下方 → 同级插入
|
||||
nodes.splice(i + 1, 0, dragObj);
|
||||
dragObj.b_parent_id = parentNode ? parentNode.b_id : null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (info.node.isFmsRoot) {
|
||||
dragObj.b_parent_id = null;
|
||||
data.unshift(dragObj);
|
||||
} else {
|
||||
insertIntoTree(data, dropKey, null);
|
||||
}
|
||||
|
||||
treeData.value = data;
|
||||
|
||||
const isUnsaved = unsavedModuleIds.has(String(dragObj.b_id));
|
||||
|
||||
// 未保存的模块只更新本地数据,等用户点击"保存"时一并落库
|
||||
if (isUnsaved) {
|
||||
if (drafts.value.module && String(drafts.value.module.b_id) === String(dragObj.b_id)) {
|
||||
drafts.value.module.b_parent_id = dragObj.b_parent_id || null;
|
||||
}
|
||||
dragNodeKey.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 已存在的模块直接持久化到后端
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "s_module",
|
||||
key_field: "b_id",
|
||||
updates: [{ b_id: String(dragObj.b_id), b_parent_id: dragObj.b_parent_id || null }],
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
message.error("移动失败:" + (e.message || e));
|
||||
await loadTree();
|
||||
} finally {
|
||||
dragNodeKey.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 模块配置加载 ----
|
||||
async function loadModuleConfig(moduleId) {
|
||||
configLoading.value = true;
|
||||
@@ -321,6 +444,10 @@ async function loadModuleConfig(moduleId) {
|
||||
// ---- 树节点选择 ----
|
||||
function onTreeSelect(keys, info) {
|
||||
if (busy.value) return;
|
||||
if (info?.node?.isFmsRoot) {
|
||||
selectedModuleId.value = null;
|
||||
return;
|
||||
}
|
||||
const nodeKey = info?.node?.key ?? keys[0];
|
||||
if (nodeKey == null) return;
|
||||
|
||||
@@ -537,22 +664,19 @@ async function onAddModule(parentId, moduleType = "directory") {
|
||||
selectedModuleId.value = newId;
|
||||
loadedModuleId.value = newId;
|
||||
activeTab.value = "basic";
|
||||
unsavedModuleIds.add(newId);
|
||||
updateSavedTreeNode(newModule);
|
||||
if (parentId && !expandedTreeKeys.value.includes(String(parentId))) {
|
||||
expandedTreeKeys.value = [...expandedTreeKeys.value, String(parentId)];
|
||||
}
|
||||
}
|
||||
|
||||
const addModuleMenu = {
|
||||
items: [{ key: "directory", label: "目录" }],
|
||||
onClick: ({ key }) => onAddModule(null, key),
|
||||
};
|
||||
|
||||
// ---- 右键菜单 ----
|
||||
function onTreeMenuClick({ key, node }) {
|
||||
if (busy.value) return;
|
||||
if (key.startsWith("add-")) {
|
||||
onAddModule(node.key, key.replace("add-", ""));
|
||||
const parentId = node.isFmsRoot ? null : node.key;
|
||||
onAddModule(parentId, key.replace("add-", ""));
|
||||
} else if (key === "delete") {
|
||||
if (hasChildNode(node.key)) {
|
||||
Modal.warning({
|
||||
@@ -749,6 +873,7 @@ async function saveAll() {
|
||||
if (i18nPanelRef.value) {
|
||||
i18nPanelRef.value.markSaved();
|
||||
}
|
||||
unsavedModuleIds.delete(moduleId);
|
||||
updateSavedTreeNode(moduleData);
|
||||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||||
message.success("保存成功");
|
||||
@@ -984,43 +1109,43 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
|
||||
<template>
|
||||
<div class="mm-page">
|
||||
<!-- 左侧模块树 -->
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-sidebar-header">
|
||||
<span class="mm-sidebar-title">模块列表</span>
|
||||
<a-dropdown :menu="addModuleMenu" :trigger="['click']" :disabled="busy">
|
||||
<a-button :disabled="busy"><Plus :size="14" />新增</a-button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<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>
|
||||
<FmsTree
|
||||
v-else
|
||||
class="mm-tree"
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:context-menu="buildTreeContextMenu"
|
||||
:context-menu-disabled="busy"
|
||||
@select="onTreeSelect"
|
||||
@menu-click="onTreeMenuClick"
|
||||
>
|
||||
<template #title="node">
|
||||
<span class="mm-tree-node">
|
||||
<span>{{ node.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
</aside>
|
||||
<a-splitter class="mm-splitter" lazy>
|
||||
<a-splitter-panel :default-size="260" :min="210" :max="440" collapsible>
|
||||
<!-- 左侧模块树 -->
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-tree-stage">
|
||||
<div v-if="treeLoading" class="mm-tree-empty">
|
||||
<a-spin />
|
||||
</div>
|
||||
<FmsTree
|
||||
v-else
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
: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">
|
||||
<span>{{ node.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
</div>
|
||||
</aside>
|
||||
</a-splitter-panel>
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<a-splitter-panel :min="720">
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
@@ -1100,23 +1225,31 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
v-else-if="tab.key === 'list'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:list-config="drafts.listConfig"
|
||||
:edit-config="drafts.editConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('listConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-edit="(v) => onUpdate('editConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleEditConfigPanel
|
||||
v-else-if="tab.key === 'edit'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts.editConfig"
|
||||
:list-config="drafts.listConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('editConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-list="(v) => onUpdate('listConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleQueryConfigPanel
|
||||
v-else-if="tab.key === 'query'"
|
||||
@@ -1159,50 +1292,43 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</a-splitter-panel>
|
||||
</a-splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mm-page {
|
||||
display: flex;
|
||||
.mm-page,
|
||||
.mm-splitter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.mm-page {
|
||||
background: var(--fms-bg);
|
||||
}
|
||||
|
||||
.mm-sidebar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
width: 260px;
|
||||
border-right: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.mm-sidebar-header {
|
||||
.mm-tree-stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
.mm-tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mm-tree-empty {
|
||||
@@ -1224,10 +1350,11 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
|
||||
.mm-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user