20260804173126
This commit is contained in:
1 parent
e17bbe9db3
commit
75b1ccaa6d
36 files changed
+3521
-718
No files matched your search
@@ -4,7 +4,7 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu
|
||||
|
||||
<template>
|
||||
<div class="contact-page">
|
||||
<FmsModuleListPage module-code="v_b_contact" />
|
||||
<FmsModuleListPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -396,7 +396,6 @@ onMounted(() => {
|
||||
<a-splitter-panel :min="520">
|
||||
<main class="partner-panel">
|
||||
<FmsModuleListPage
|
||||
module-code="v_b_othercompany"
|
||||
:fixed-search-condition="categoryFilterCondition"
|
||||
:column-extensions="columnExtensions"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Pencil, Plus, RefreshCw, Save, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
@@ -7,11 +8,13 @@ import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import { resolveRoutePageModuleId } from "@/module-runtime/runtimeResolver";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
|
||||
const ALL_CATEGORIES_KEY = "__fms_tree_root__";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const route = useRoute();
|
||||
const permissionStore = usePermissionStore();
|
||||
const categories = ref([]);
|
||||
const dataModules = ref([]);
|
||||
@@ -21,7 +24,7 @@ const categoryLoading = ref(false);
|
||||
const moduleLoading = ref(false);
|
||||
const selectedCategoryKey = ref("");
|
||||
const selectedModuleId = ref("");
|
||||
const selectedProfileId = ref("");
|
||||
const selectedPageNodeId = ref("");
|
||||
const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]);
|
||||
const modalVisible = ref(false);
|
||||
const modalParentId = ref(null);
|
||||
@@ -35,14 +38,14 @@ const form = reactive({ b_name: "", b_module_id: "" });
|
||||
const categoryById = computed(
|
||||
() => new Map(categories.value.map((item) => [String(item.b_id), item])),
|
||||
);
|
||||
const tableKey = computed(() => `${selectedModuleId.value}:${selectedProfileId.value}`);
|
||||
const shouldLoadData = computed(() => Boolean(selectedModuleId.value && selectedProfileId.value));
|
||||
const tableKey = computed(() => selectedPageNodeId.value);
|
||||
const shouldLoadData = computed(() => Boolean(selectedPageNodeId.value));
|
||||
const canCreateRow = computed(() => permissionStore.canAction(selectedModuleId.value, "create"));
|
||||
const canUpdateRow = computed(() => permissionStore.canAction(selectedModuleId.value, "update"));
|
||||
const canDeleteRow = computed(() => permissionStore.canAction(selectedModuleId.value, "delete"));
|
||||
|
||||
function canManageTree(actionCode) {
|
||||
return permissionStore.canAction("base_otherdata", actionCode);
|
||||
return permissionStore.canAction(otherDataPageId.value, actionCode);
|
||||
}
|
||||
|
||||
// 编辑表格:当前模块列表实例、选中行、保存/删除状态
|
||||
@@ -62,7 +65,7 @@ const selectedCategoryKeys = computed(() =>
|
||||
);
|
||||
|
||||
// 切换数据模块时清空编辑状态,避免残留脏标记与选中项。
|
||||
watch([selectedModuleId, selectedProfileId], () => {
|
||||
watch(selectedPageNodeId, () => {
|
||||
selectedRowKeys.value = [];
|
||||
dirtyCount.value = 0;
|
||||
});
|
||||
@@ -71,17 +74,16 @@ const modalTitle = computed(() => {
|
||||
return modalType.value === "node" ? "添加节点" : "添加分类";
|
||||
});
|
||||
const moduleOptions = computed(() => {
|
||||
const usedCodes = new Set();
|
||||
const usedModuleIds = new Set();
|
||||
for (const item of categories.value) {
|
||||
if (String(item.b_id) === String(editingCategoryId.value || "")) continue;
|
||||
if (getNodeType(item) !== "module") continue;
|
||||
const module = moduleById.value.get(String(item.b_module_id));
|
||||
if (module?.b_code) usedCodes.add(String(module.b_code));
|
||||
if (item.b_module_id != null) usedModuleIds.add(String(item.b_module_id));
|
||||
}
|
||||
|
||||
const options = [];
|
||||
for (const item of dataModules.value) {
|
||||
if (!item.b_code || usedCodes.has(String(item.b_code))) continue;
|
||||
if (usedModuleIds.has(String(item.b_id))) continue;
|
||||
options.push({
|
||||
label: `${appStore.t(item.b_i18n, item.b_name || item.b_code)}(${item.b_code})`,
|
||||
value: String(item.b_id),
|
||||
@@ -117,10 +119,10 @@ function buildCategoryTreeNode(item) {
|
||||
let title = "";
|
||||
const moduleId = item.b_module_id == null ? "" : String(item.b_module_id);
|
||||
const module = moduleById.value.get(moduleId);
|
||||
const moduleCode = module?.b_code || "";
|
||||
const fallbackLabel = module?.b_code || "";
|
||||
const nodeType = getNodeType(item);
|
||||
if (nodeType === "module") {
|
||||
title = appStore.t(module?.b_i18n, module?.b_name || item.b_name || moduleCode);
|
||||
title = appStore.t(module?.b_i18n, module?.b_name || item.b_name || fallbackLabel);
|
||||
} else {
|
||||
title = appStore.t(item.b_i18n, item.b_name);
|
||||
}
|
||||
@@ -133,7 +135,6 @@ function buildCategoryTreeNode(item) {
|
||||
b_id: String(item.b_id),
|
||||
b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id),
|
||||
b_node_type: nodeType,
|
||||
b_module_code: moduleCode,
|
||||
b_module_id: moduleId || null,
|
||||
b_xh: item.b_xh,
|
||||
children: [],
|
||||
@@ -142,9 +143,7 @@ function buildCategoryTreeNode(item) {
|
||||
|
||||
function getNodeType(item) {
|
||||
if (!item) return "category";
|
||||
if (item.b_node_type === "module_ref" || item.b_node_type === "widget") return "module";
|
||||
if (item.b_node_type === "module") return "module";
|
||||
if (item.b_module_id != null) return "module";
|
||||
if (item.b_node_type === "module_ref") return "module";
|
||||
return "category";
|
||||
}
|
||||
|
||||
@@ -153,20 +152,20 @@ function updateSelectedModule(key) {
|
||||
|
||||
if (!key) {
|
||||
selectedModuleId.value = "";
|
||||
selectedProfileId.value = "";
|
||||
selectedPageNodeId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === ALL_CATEGORIES_KEY) {
|
||||
selectedModuleId.value = "";
|
||||
selectedProfileId.value = "";
|
||||
selectedPageNodeId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const category = categoryById.value.get(key);
|
||||
if (!category || getNodeType(category) !== "module") {
|
||||
selectedModuleId.value = "";
|
||||
selectedProfileId.value = "";
|
||||
selectedPageNodeId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,12 +173,12 @@ function updateSelectedModule(key) {
|
||||
const profileId = String(category.b_profile_id || "");
|
||||
if (Number(category.b_canuse) !== 1 || !module?.b_id || !profileId) {
|
||||
selectedModuleId.value = "";
|
||||
selectedProfileId.value = "";
|
||||
selectedPageNodeId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
selectedModuleId.value = String(module.b_id);
|
||||
selectedProfileId.value = profileId;
|
||||
selectedPageNodeId.value = String(category.b_id);
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
@@ -258,12 +257,7 @@ async function loadDataModules() {
|
||||
}
|
||||
|
||||
async function loadOtherDataPage() {
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
"b_code = N'base_otherdata' AND b_module_type = 'page' AND b_canuse = 1",
|
||||
"b_id ASC",
|
||||
);
|
||||
otherDataPageId.value = response.data?.[0]?.b_id || null;
|
||||
otherDataPageId.value = await resolveRoutePageModuleId(route.path);
|
||||
}
|
||||
|
||||
function onCategorySelect(keys, info) {
|
||||
@@ -280,7 +274,7 @@ function onCategorySelect(keys, info) {
|
||||
|
||||
function buildCategoryContextMenu(node) {
|
||||
const items = [];
|
||||
const isCategory = !node?.isFmsRoot && node?.b_node_type === "category";
|
||||
const isCategory = !node?.isFmsRoot && getNodeType(node) === "category";
|
||||
if ((node?.isFmsRoot || isCategory) && canManageTree("create")) {
|
||||
items.push(
|
||||
{
|
||||
@@ -640,8 +634,7 @@ onMounted(() => {
|
||||
v-if="shouldLoadData"
|
||||
ref="moduleListRef"
|
||||
:key="tableKey"
|
||||
:module-id="selectedModuleId"
|
||||
:profile-id="selectedProfileId"
|
||||
:page-node-id="selectedPageNodeId"
|
||||
:editable="true"
|
||||
:row-draggable="true"
|
||||
:row-selection="rowSelection"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Copy, Plus, RefreshCw, Save, Trash2 } from "@lucide/vue";
|
||||
import { AlertTriangle, CheckCircle2, Copy, Plus, RefreshCw, Save, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleTable from "@/components/fms-module-list/FmsModuleTable.vue";
|
||||
import { hasFieldType } from "@/module-runtime/fieldRegistry";
|
||||
import ModuleFieldsPanel from "./ModuleFieldsPanel.vue";
|
||||
import ModuleListConfigPanel from "./ModuleListConfigPanel.vue";
|
||||
import ModuleEditConfigPanel from "./ModuleEditConfigPanel.vue";
|
||||
@@ -10,7 +12,14 @@ import ModuleAutoCodePanel from "./ModuleAutoCodePanel.vue";
|
||||
import ModulePowersPanel from "./ModulePowersPanel.vue";
|
||||
import ModuleI18nPanel from "./ModuleI18nPanel.vue";
|
||||
|
||||
const props = defineProps({ controller: { type: Object, required: true } });
|
||||
const props = defineProps({
|
||||
controller: { type: Object, required: true },
|
||||
showList: { type: Boolean, default: true },
|
||||
allowDelete: { type: Boolean, default: true },
|
||||
showActions: { type: Boolean, default: true },
|
||||
showI18n: { type: Boolean, default: true },
|
||||
});
|
||||
const emit = defineEmits(["saved"]);
|
||||
const activeTab = ref("basic");
|
||||
const createOpen = ref(false);
|
||||
const cloneOpen = ref(false);
|
||||
@@ -32,6 +41,31 @@ const createForm = reactive({
|
||||
b_engine_key: "",
|
||||
});
|
||||
const cloneForm = reactive({ b_code: "", b_name: "" });
|
||||
const moduleTypeLabels = {
|
||||
page: "页面",
|
||||
data: "数据",
|
||||
report: "报表",
|
||||
dashboard: "仪表盘",
|
||||
workflow: "工作流",
|
||||
};
|
||||
|
||||
function moduleTypeLabel(type) {
|
||||
return moduleTypeLabels[type] || type || "";
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.controller.selectedId.value,
|
||||
() => {
|
||||
activeTab.value = "basic";
|
||||
},
|
||||
);
|
||||
|
||||
const previewFields = computed(() =>
|
||||
(props.controller.selected.value?.fields || []).filter((field) => Number(field.b_canuse) === 1),
|
||||
);
|
||||
const previewAvailable = computed(() =>
|
||||
previewFields.value.every((field) => hasFieldType(field.b_type || "input")),
|
||||
);
|
||||
|
||||
const currentProfiles = computed(() => {
|
||||
const selected = props.controller.selected.value;
|
||||
@@ -164,6 +198,7 @@ async function saveResource() {
|
||||
const i18nRequest = await i18nPanelRef.value?.buildSaveRequest?.();
|
||||
await props.controller.save(i18nRequest ? [i18nRequest] : []);
|
||||
i18nPanelRef.value?.markSaved?.();
|
||||
emit("saved", props.controller.selected.value);
|
||||
message.success("模块配置已保存");
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
@@ -224,8 +259,8 @@ function onGroupsSaved(groups, idMapping) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workspace">
|
||||
<aside class="resource-pane">
|
||||
<div class="workspace" :class="{ 'workspace--detail-only': !showList }">
|
||||
<aside v-if="showList" class="resource-pane">
|
||||
<div class="toolbar">
|
||||
<a-button v-if="controller.can('create')" type="primary" @click="resetCreate"
|
||||
><Plus :size="15" />新增资源</a-button
|
||||
@@ -243,13 +278,36 @@ function onGroupsSaved(groups, idMapping) {
|
||||
@click="controller.selectResource(item.b_id)"
|
||||
>
|
||||
<span>{{ item.b_name }}</span
|
||||
><small>{{ item.b_code }} · {{ item.b_module_type }}</small>
|
||||
><small>{{ item.b_code }} · {{ moduleTypeLabel(item.b_module_type) }}</small>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="detail-pane">
|
||||
<template v-if="controller.selected.value">
|
||||
<div class="toolbar toolbar--right">
|
||||
<a-popover title="配置检查" trigger="click">
|
||||
<template #content>
|
||||
<div v-if="controller.configurationIssues.value.length" class="validation-list">
|
||||
<div
|
||||
v-for="(issue, index) in controller.configurationIssues.value"
|
||||
:key="`${issue.level}-${index}`"
|
||||
:class="`validation-item validation-item--${issue.level}`"
|
||||
>
|
||||
{{ issue.message }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>当前配置完整</span>
|
||||
</template>
|
||||
<a-button>
|
||||
<AlertTriangle v-if="controller.configurationIssues.value.length" :size="15" />
|
||||
<CheckCircle2 v-else :size="15" />
|
||||
{{
|
||||
controller.configurationIssues.value.length
|
||||
? `${controller.configurationIssues.value.length} 项配置问题`
|
||||
: "配置完整"
|
||||
}}
|
||||
</a-button>
|
||||
</a-popover>
|
||||
<template
|
||||
v-if="controller.selected.value.b_module_type === 'data' && controller.can('update')"
|
||||
>
|
||||
@@ -278,7 +336,7 @@ function onGroupsSaved(groups, idMapping) {
|
||||
@click="saveResource"
|
||||
><Save :size="15" />保存</a-button
|
||||
>
|
||||
<a-button v-if="controller.can('delete')" danger @click="removeResource"
|
||||
<a-button v-if="allowDelete && controller.can('delete')" danger @click="removeResource"
|
||||
><Trash2 :size="15" />删除</a-button
|
||||
>
|
||||
</div>
|
||||
@@ -299,7 +357,7 @@ function onGroupsSaved(groups, idMapping) {
|
||||
<a-col :span="8"
|
||||
><a-form-item label="资源类型"
|
||||
><a-input
|
||||
:value="controller.selected.value.b_module_type"
|
||||
:value="moduleTypeLabel(controller.selected.value.b_module_type)"
|
||||
disabled /></a-form-item
|
||||
></a-col>
|
||||
<a-col v-if="controller.selected.value.b_module_type === 'page'" :span="8"
|
||||
@@ -481,20 +539,34 @@ function onGroupsSaved(groups, idMapping) {
|
||||
@update="(rows) => (controller.selected.value.autoCode = rows)"
|
||||
/></a-tab-pane>
|
||||
</template>
|
||||
<a-tab-pane v-if="controller.can('update')" key="actions" tab="操作"
|
||||
<a-tab-pane v-if="showActions && controller.can('update')" key="actions" tab="操作"
|
||||
><ModulePowersPanel
|
||||
:module-id="controller.selectedId.value"
|
||||
:powers="controller.selected.value.actions"
|
||||
:loading="controller.loading.value"
|
||||
@update="(rows) => (controller.selected.value.actions = rows)"
|
||||
/></a-tab-pane>
|
||||
<a-tab-pane v-if="controller.can('update')" key="i18n" tab="多语言"
|
||||
<a-tab-pane v-if="showI18n && controller.can('update')" key="i18n" tab="多语言"
|
||||
><ModuleI18nPanel
|
||||
ref="i18nPanelRef"
|
||||
:module-id="controller.selectedId.value"
|
||||
:i18n-items="i18nItems"
|
||||
:loading="controller.loading.value"
|
||||
/></a-tab-pane>
|
||||
<a-tab-pane
|
||||
v-if="controller.selected.value.b_module_type === 'data'"
|
||||
key="preview"
|
||||
tab="列表预览"
|
||||
>
|
||||
<div v-if="previewAvailable" class="runtime-preview">
|
||||
<FmsModuleTable
|
||||
:fields="previewFields"
|
||||
:list-config="controller.selected.value.listConfig || []"
|
||||
:data-source="[]"
|
||||
/>
|
||||
</div>
|
||||
<a-result v-else status="warning" title="存在未注册的字段控件,暂时无法预览" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<a-empty v-else description="请选择模块资源" />
|
||||
@@ -510,9 +582,6 @@ function onGroupsSaved(groups, idMapping) {
|
||||
:options="[
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据模块', value: 'data' },
|
||||
{ label: '报表', value: 'report' },
|
||||
{ label: '仪表盘', value: 'dashboard' },
|
||||
{ label: '工作流', value: 'workflow' },
|
||||
]" /></a-form-item
|
||||
></a-col>
|
||||
<a-col :span="8"
|
||||
@@ -590,6 +659,34 @@ function onGroupsSaved(groups, idMapping) {
|
||||
min-height: 0;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workspace--detail-only {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.validation-list {
|
||||
display: grid;
|
||||
max-width: 420px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.validation-item {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.validation-item--error {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.validation-item--warning {
|
||||
color: #ad6800;
|
||||
}
|
||||
|
||||
.runtime-preview {
|
||||
height: 100%;
|
||||
min-height: 280px;
|
||||
}
|
||||
.resource-pane {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -646,6 +743,7 @@ function onGroupsSaved(groups, idMapping) {
|
||||
.resource-tabs :deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.resource-tabs :deep(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
@@ -658,4 +756,21 @@ function onGroupsSaved(groups, idMapping) {
|
||||
.detail-pane > :deep(.ant-empty) {
|
||||
margin-top: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar--right {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.basic-form {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.basic-form :deep(.ant-col-8),
|
||||
.basic-form :deep(.ant-col-12) {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
import { invalidateModuleRuntimeCache } from "@/module-runtime";
|
||||
|
||||
export function useFunctionStructure(appStore, authorize = () => false) {
|
||||
const rows = ref([]);
|
||||
@@ -76,6 +77,7 @@ export function useFunctionStructure(appStore, authorize = () => false) {
|
||||
b_xh: nextSort(values.b_parent_id),
|
||||
};
|
||||
await saveObjectApi([{ table: "s_function_node", key_field: "b_id", inserts: [row] }]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await load();
|
||||
select(id);
|
||||
}
|
||||
@@ -94,6 +96,7 @@ export function useFunctionStructure(appStore, authorize = () => false) {
|
||||
b_xh: Number(values.b_xh) || 0,
|
||||
};
|
||||
await saveObjectApi([{ table: "s_function_node", key_field: "b_id", updates: [update] }]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -125,6 +128,7 @@ export function useFunctionStructure(appStore, authorize = () => false) {
|
||||
updates: [{ b_id: String(nodeId), b_parent_id: parentId || null, b_xh: Number(xh) || 0 }],
|
||||
},
|
||||
]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -144,6 +148,7 @@ export function useFunctionStructure(appStore, authorize = () => false) {
|
||||
await saveObjectApi([
|
||||
{ table: "s_function_node", key_field: "b_id", deletes: [{ b_id: nodeId }] },
|
||||
]);
|
||||
invalidateModuleRuntimeCache();
|
||||
selectedId.value = "";
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { computed, ref } from "vue";
|
||||
import { describeApi, loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
import { allocateTemporaryIds, cloneData, diffRows, remapForeignKeys } from "@/utils/dataChanges";
|
||||
import { validateModuleCode } from "@/utils/i18n";
|
||||
import { invalidateModuleConfigCache, invalidateModuleRuntimeCache } from "@/module-runtime";
|
||||
|
||||
const CAPABILITY_TABLES = [
|
||||
["s_module_field", "fields"],
|
||||
@@ -75,6 +76,87 @@ function validateResource(resource) {
|
||||
}
|
||||
}
|
||||
|
||||
function findDuplicateValues(rows, field) {
|
||||
const seen = new Set();
|
||||
const duplicates = new Set();
|
||||
for (const row of rows || []) {
|
||||
const value = String(row?.[field] ?? "");
|
||||
if (!value) continue;
|
||||
if (seen.has(value)) duplicates.add(value);
|
||||
seen.add(value);
|
||||
}
|
||||
return [...duplicates];
|
||||
}
|
||||
|
||||
function collectConfigurationIssues(resource, profiles) {
|
||||
if (!resource) return [];
|
||||
const issues = [];
|
||||
try {
|
||||
validateResource(resource);
|
||||
} catch (error) {
|
||||
issues.push({ level: "error", message: error.message || String(error) });
|
||||
}
|
||||
|
||||
const duplicateActions = findDuplicateValues(resource.actions, "b_code");
|
||||
if (duplicateActions.length) {
|
||||
issues.push({ level: "error", message: `操作编码重复:${duplicateActions.join("、")}` });
|
||||
}
|
||||
if (resource.b_module_type !== "data") return issues;
|
||||
|
||||
const moduleProfiles = profiles.filter(
|
||||
(item) => String(item.b_module_id) === String(resource.b_id) && Number(item.b_canuse) === 1,
|
||||
);
|
||||
const defaultProfiles = moduleProfiles.filter((item) => Number(item.b_default) === 1);
|
||||
if (defaultProfiles.length !== 1) {
|
||||
issues.push({ level: "error", message: "启用的数据模块必须且只能有一个默认使用方式" });
|
||||
}
|
||||
|
||||
const activeFields = (resource.fields || []).filter((field) => Number(field.b_canuse) === 1);
|
||||
const activeFieldIds = new Set(activeFields.map((field) => String(field.b_id)));
|
||||
const activeFieldNames = new Set(activeFields.map((field) => String(field.b_field)));
|
||||
if (!activeFields.length) {
|
||||
issues.push({ level: "warning", message: "当前没有启用字段,请先同步字段" });
|
||||
}
|
||||
if (resource.b_keyfield && !activeFieldNames.has(String(resource.b_keyfield))) {
|
||||
issues.push({ level: "error", message: `主键字段 ${resource.b_keyfield} 不在启用字段中` });
|
||||
}
|
||||
|
||||
const groupIds = new Set((resource.groups || []).map((group) => String(group.b_id)));
|
||||
for (const [rowsKey, label] of [
|
||||
["listConfig", "列表"],
|
||||
["formConfig", "表单"],
|
||||
["queryConfig", "查询"],
|
||||
]) {
|
||||
const rows = resource[rowsKey] || [];
|
||||
const duplicateFields = findDuplicateValues(rows, "b_field_id");
|
||||
if (duplicateFields.length) {
|
||||
issues.push({ level: "error", message: `${label}配置存在重复字段` });
|
||||
}
|
||||
if (rows.some((row) => !activeFieldIds.has(String(row.b_field_id)))) {
|
||||
issues.push({ level: "error", message: `${label}配置引用了未启用或不存在的字段` });
|
||||
}
|
||||
if (
|
||||
rowsKey !== "queryConfig" &&
|
||||
rows.some((row) => row.b_group_id != null && !groupIds.has(String(row.b_group_id)))
|
||||
) {
|
||||
issues.push({ level: "error", message: `${label}配置引用了不存在的布局分组` });
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of activeFields) {
|
||||
if (!field.b_options) continue;
|
||||
try {
|
||||
JSON.parse(field.b_options);
|
||||
} catch {
|
||||
issues.push({
|
||||
level: "error",
|
||||
message: `字段 ${field.b_field} 的控件参数不是有效 JSON`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function useModuleResources(appStore, authorize = () => false) {
|
||||
const resources = ref([]);
|
||||
const profiles = ref([]);
|
||||
@@ -85,6 +167,11 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
function invalidateRuntime(moduleId, profileId = "") {
|
||||
invalidateModuleRuntimeCache({ moduleId, profileId });
|
||||
invalidateModuleConfigCache({ moduleId, profileId });
|
||||
}
|
||||
|
||||
const selected = computed(() => draft.value);
|
||||
const selectedProfile = computed(() => {
|
||||
return (
|
||||
@@ -92,6 +179,9 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
);
|
||||
});
|
||||
const isDirty = computed(() => JSON.stringify(draft.value) !== JSON.stringify(original.value));
|
||||
const configurationIssues = computed(() =>
|
||||
collectConfigurationIssues(draft.value, profiles.value),
|
||||
);
|
||||
|
||||
async function loadResources() {
|
||||
const [moduleResponse, profileResponse] = await Promise.all([
|
||||
@@ -303,6 +393,7 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
});
|
||||
}
|
||||
await saveObjectApi(saves);
|
||||
invalidateRuntime(moduleId);
|
||||
selectedId.value = moduleId;
|
||||
activeProfileId.value = values.b_module_type === "data" ? String(ids[1]) : "";
|
||||
await loadResources();
|
||||
@@ -423,6 +514,7 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
appendCapabilityChanges(saves, changesByTable);
|
||||
saves.push(...extraRequests);
|
||||
await saveObjectApi(saves);
|
||||
invalidateRuntime(moduleId, draft.value.b_profile_id);
|
||||
await loadResources();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
@@ -572,6 +664,7 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
{ table: "s_module_field_form", key_field: "b_id", inserts: forms },
|
||||
{ table: "s_module_field_query", key_field: "b_id", inserts: queries },
|
||||
]);
|
||||
invalidateRuntime(moduleId, profileId);
|
||||
selectedId.value = moduleId;
|
||||
activeProfileId.value = profileId;
|
||||
await loadResources();
|
||||
@@ -638,6 +731,7 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
await saveObjectApi(
|
||||
deletions.map(([table, key]) => ({ table, key_field: key, deletes: [{ [key]: moduleId }] })),
|
||||
);
|
||||
invalidateRuntime(moduleId);
|
||||
selectedId.value = "";
|
||||
await loadResources();
|
||||
}
|
||||
@@ -653,6 +747,7 @@ export function useModuleResources(appStore, authorize = () => false) {
|
||||
saving,
|
||||
can: authorize,
|
||||
isDirty,
|
||||
configurationIssues,
|
||||
load: loadResources,
|
||||
selectResource,
|
||||
createResource,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
import { invalidateModuleRuntimeCache } from "@/module-runtime";
|
||||
|
||||
export function usePageComposition(appStore, authorize = () => false) {
|
||||
const pages = ref([]);
|
||||
const resources = ref([]);
|
||||
const profiles = ref([]);
|
||||
const allNodes = ref([]);
|
||||
const nodes = ref([]);
|
||||
const selectedPageId = ref("");
|
||||
const selectedNodeId = ref("");
|
||||
@@ -30,7 +32,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
});
|
||||
|
||||
async function loadCatalog() {
|
||||
const [pageResponse, resourceResponse, profileResponse] = await Promise.all([
|
||||
const [pageResponse, resourceResponse, profileResponse, nodeResponse] = await Promise.all([
|
||||
loadDataApi("s_module", "b_module_type = 'page' AND b_canuse = 1", "b_code ASC"),
|
||||
loadDataApi(
|
||||
"s_module",
|
||||
@@ -38,26 +40,23 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
"b_module_type ASC, b_code ASC",
|
||||
),
|
||||
loadDataApi("s_module_profile", "b_canuse = 1", "b_module_id ASC, b_xh ASC, b_id ASC"),
|
||||
loadDataApi("v_s_page_node", "", "b_page_module_id ASC, b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
pages.value = pageResponse.data || [];
|
||||
resources.value = resourceResponse.data || [];
|
||||
profiles.value = profileResponse.data || [];
|
||||
allNodes.value = nodeResponse.data || [];
|
||||
if (!selectedPageId.value && pages.value.length)
|
||||
selectedPageId.value = String(pages.value[0].b_id);
|
||||
}
|
||||
|
||||
async function loadNodes() {
|
||||
function syncSelectedPageNodes() {
|
||||
const pageId = String(selectedPageId.value || "");
|
||||
if (!pageId) {
|
||||
nodes.value = [];
|
||||
return;
|
||||
}
|
||||
const response = await loadDataApi(
|
||||
"v_s_page_node",
|
||||
`b_page_module_id = ${pageId}`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
);
|
||||
nodes.value = response.data || [];
|
||||
nodes.value = allNodes.value.filter((item) => String(item.b_page_module_id) === pageId);
|
||||
if (
|
||||
selectedNodeId.value &&
|
||||
!nodes.value.some((item) => String(item.b_id) === selectedNodeId.value)
|
||||
@@ -66,11 +65,21 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNodes() {
|
||||
const response = await loadDataApi(
|
||||
"v_s_page_node",
|
||||
"",
|
||||
"b_page_module_id ASC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
allNodes.value = response.data || [];
|
||||
syncSelectedPageNodes();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadCatalog();
|
||||
await loadNodes();
|
||||
syncSelectedPageNodes();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -79,7 +88,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
async function selectPage(id) {
|
||||
selectedPageId.value = String(id || "");
|
||||
selectedNodeId.value = "";
|
||||
await loadNodes();
|
||||
syncSelectedPageNodes();
|
||||
}
|
||||
|
||||
function selectNode(id) {
|
||||
@@ -122,6 +131,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
b_xh: nextSort(values.b_parent_id),
|
||||
};
|
||||
await saveObjectApi([{ table: "s_page_node", key_field: "b_id", inserts: [row] }]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await loadNodes();
|
||||
selectNode(id);
|
||||
}
|
||||
@@ -143,6 +153,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
],
|
||||
},
|
||||
]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await loadNodes();
|
||||
}
|
||||
|
||||
@@ -178,6 +189,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
updates: [{ b_id: String(nodeId), b_parent_id: parentId || null, b_xh: Number(xh) || 0 }],
|
||||
},
|
||||
]);
|
||||
invalidateModuleRuntimeCache();
|
||||
await loadNodes();
|
||||
}
|
||||
|
||||
@@ -195,6 +207,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
throw new Error("该页面节点仍被角色或用户授权引用");
|
||||
}
|
||||
await saveObjectApi([{ table: "s_page_node", key_field: "b_id", deletes: [{ b_id: nodeId }] }]);
|
||||
invalidateModuleRuntimeCache();
|
||||
selectedNodeId.value = "";
|
||||
await loadNodes();
|
||||
}
|
||||
@@ -203,6 +216,7 @@ export function usePageComposition(appStore, authorize = () => false) {
|
||||
pages,
|
||||
resources,
|
||||
profiles,
|
||||
allNodes,
|
||||
nodes,
|
||||
treeData,
|
||||
selectedPageId,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { h, onMounted, ref } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Boxes, LayoutPanelTop, Network } from "@lucide/vue";
|
||||
import { Boxes, LayoutPanelTop, Network, SlidersHorizontal, Workflow } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import BusinessStructurePanel from "./components/BusinessStructurePanel.vue";
|
||||
import FunctionStructurePanel from "./components/FunctionStructurePanel.vue";
|
||||
import PageCompositionPanel from "./components/PageCompositionPanel.vue";
|
||||
import ModuleResourcePanel from "./components/ModuleResourcePanel.vue";
|
||||
@@ -14,7 +15,8 @@ import { useModuleResources } from "./composables/useModuleResources";
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const canManage = (actionCode) => permissionStore.canAction("m_metadata", actionCode);
|
||||
const activeArea = ref("function");
|
||||
const activeMode = ref("structure");
|
||||
const advancedArea = ref("function");
|
||||
const loadedAreas = new Set();
|
||||
const functionController = useFunctionStructure(appStore, canManage);
|
||||
const pageController = usePageComposition(appStore, canManage);
|
||||
@@ -28,51 +30,92 @@ const controllers = {
|
||||
async function loadArea(area) {
|
||||
if (loadedAreas.has(area)) return;
|
||||
try {
|
||||
await controllers[area].load();
|
||||
loadedAreas.add(area);
|
||||
if (area === "structure") {
|
||||
await Promise.all(Object.values(controllers).map((controller) => controller.load()));
|
||||
loadedAreas.add("structure");
|
||||
for (const key of Object.keys(controllers)) loadedAreas.add(key);
|
||||
} else {
|
||||
await controllers[area].load();
|
||||
loadedAreas.add(area);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
function changeArea(area) {
|
||||
if (area === activeArea.value) return;
|
||||
if (activeArea.value === "resource" && resourceController.isDirty.value) {
|
||||
function changeContext(nextMode, nextArea = advancedArea.value) {
|
||||
const currentUsesResource = activeMode.value === "structure" || advancedArea.value === "resource";
|
||||
const nextUsesResource = nextMode === "structure" || nextArea === "resource";
|
||||
const apply = async () => {
|
||||
activeMode.value = nextMode;
|
||||
advancedArea.value = nextArea;
|
||||
await loadArea(nextMode === "structure" ? "structure" : nextArea);
|
||||
};
|
||||
if (currentUsesResource && !nextUsesResource && resourceController.isDirty.value) {
|
||||
Modal.confirm({
|
||||
title: "当前模块有未保存修改",
|
||||
content: "切换维护区域将放弃这些修改。",
|
||||
okText: "放弃并切换",
|
||||
cancelText: "继续编辑",
|
||||
onOk: async () => {
|
||||
activeArea.value = area;
|
||||
await loadArea(area);
|
||||
await resourceController.load();
|
||||
await apply();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
activeArea.value = area;
|
||||
loadArea(area);
|
||||
apply();
|
||||
}
|
||||
|
||||
onMounted(() => loadArea(activeArea.value));
|
||||
function changeMode(mode) {
|
||||
if (mode === activeMode.value) return;
|
||||
changeContext(mode);
|
||||
}
|
||||
|
||||
function changeAdvancedArea(area) {
|
||||
if (area === advancedArea.value) return;
|
||||
changeContext("advanced", area);
|
||||
}
|
||||
|
||||
onMounted(() => loadArea("structure"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="module-management-page">
|
||||
<header class="area-switcher">
|
||||
<a-segmented
|
||||
:value="activeArea"
|
||||
:value="activeMode"
|
||||
:options="[
|
||||
{ value: 'structure', label: '目录与页面', icon: () => h(Workflow, { size: 15 }) },
|
||||
{ value: 'advanced', label: '高级维护', icon: () => h(SlidersHorizontal, { size: 15 }) },
|
||||
]"
|
||||
@change="changeMode"
|
||||
/>
|
||||
<a-segmented
|
||||
v-if="activeMode === 'advanced'"
|
||||
class="advanced-switcher"
|
||||
:value="advancedArea"
|
||||
:options="[
|
||||
{ value: 'function', label: '功能结构', icon: () => h(Network, { size: 15 }) },
|
||||
{ value: 'page', label: '页面编排', icon: () => h(LayoutPanelTop, { size: 15 }) },
|
||||
{ value: 'resource', label: '模块资源', icon: () => h(Boxes, { size: 15 }) },
|
||||
]"
|
||||
@change="changeArea"
|
||||
@change="changeAdvancedArea"
|
||||
/>
|
||||
</header>
|
||||
<section class="area-content">
|
||||
<FunctionStructurePanel v-if="activeArea === 'function'" :controller="functionController" />
|
||||
<PageCompositionPanel v-else-if="activeArea === 'page'" :controller="pageController" />
|
||||
<BusinessStructurePanel
|
||||
v-if="activeMode === 'structure'"
|
||||
:app-store="appStore"
|
||||
:function-controller="functionController"
|
||||
:page-controller="pageController"
|
||||
:resource-controller="resourceController"
|
||||
/>
|
||||
<FunctionStructurePanel
|
||||
v-else-if="advancedArea === 'function'"
|
||||
:controller="functionController"
|
||||
/>
|
||||
<PageCompositionPanel v-else-if="advancedArea === 'page'" :controller="pageController" />
|
||||
<ModuleResourcePanel v-else :controller="resourceController" />
|
||||
</section>
|
||||
</div>
|
||||
@@ -97,9 +140,24 @@ onMounted(() => loadArea(activeArea.value));
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
.advanced-switcher {
|
||||
margin-left: auto;
|
||||
}
|
||||
.area-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.area-switcher {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.advanced-switcher {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -69,7 +69,7 @@ const rowActions = [
|
||||
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<FmsModuleListPage module-code="b_user" :row-actions="rowActions">
|
||||
<FmsModuleListPage :row-actions="rowActions">
|
||||
<template #actions>
|
||||
<a-button v-if="permissionStore.canAction('b_user', 'update')" @click="openRoleManager">
|
||||
<ShieldCheck :size="15" />角色维护
|
||||
|
||||
Reference in new issue
Block a user