821 lines
24 KiB
Vue
821 lines
24 KiB
Vue
<script setup>
|
||
import { computed, onMounted, reactive, ref } from "vue";
|
||
import { App } from "antdv-next";
|
||
import { Database, FileText, RotateCcw, Save, ShieldCheck, Trash2 } from "@lucide/vue";
|
||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||
import { useAuthStore } from "@/stores/auth";
|
||
import { usePermissionStore } from "@/stores/permissions";
|
||
import { I18N_LOCALE_CODES } from "@/utils/i18n";
|
||
import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue";
|
||
import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue";
|
||
import ModulePowerPanel from "./components/ModulePowerPanel.vue";
|
||
import ModuleTreePanel from "./components/ModuleTreePanel.vue";
|
||
|
||
const PROTECTED_MODULE_CODES = ["m_module", "m_i18n", "m_biz"];
|
||
const { message } = App.useApp();
|
||
const authStore = useAuthStore();
|
||
const permissionStore = usePermissionStore();
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const selectedKey = ref("");
|
||
const activeTab = ref("basic");
|
||
const isNew = ref(false);
|
||
const deleteConfirmOpen = ref(false);
|
||
const pendingDeleteKey = ref("");
|
||
const original = ref("");
|
||
const originalTranslationKeys = ref([]);
|
||
const data = reactive({
|
||
modules: [],
|
||
fields: [],
|
||
powers: [],
|
||
translations: [],
|
||
userModules: [],
|
||
userPowers: [],
|
||
});
|
||
const draft = reactive({ module: null, fields: [], powers: [], translations: {} });
|
||
|
||
const canCreate = computed(() => permissionStore.canPower("m_module", "create"));
|
||
const canUpdate = computed(() => permissionStore.canPower("m_module", "update"));
|
||
const canDelete = computed(() => permissionStore.canPower("m_module", "delete"));
|
||
const canSave = computed(() => (isNew.value ? canCreate.value : canUpdate.value));
|
||
const dirty = computed(() => Boolean(draft.module) && serializeDraft() !== original.value);
|
||
const parentOptions = computed(() =>
|
||
data.modules
|
||
.filter((item) => item.b_id !== draft.module?.b_id && item.b_type !== "data")
|
||
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_code + ")" })),
|
||
);
|
||
const directoryParentOptions = computed(() =>
|
||
data.modules
|
||
.filter((item) => item.b_type === "directory")
|
||
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_code + ")" })),
|
||
);
|
||
const moduleKindLabel = computed(() => {
|
||
if (draft.module?.b_type === "directory") return "模块";
|
||
return draft.module?.b_route?.trim() ? "页面模块" : "数据模块";
|
||
});
|
||
const pendingDeleteModule = computed(() =>
|
||
data.modules.find((item) => item.b_code === pendingDeleteKey.value),
|
||
);
|
||
|
||
function clone(value) {
|
||
return JSON.parse(JSON.stringify(value));
|
||
}
|
||
|
||
function serializeDraft() {
|
||
return JSON.stringify({
|
||
module: draft.module,
|
||
fields: draft.fields,
|
||
powers: draft.powers,
|
||
translations: draft.translations,
|
||
});
|
||
}
|
||
|
||
function rowKeys() {
|
||
return [
|
||
draft.module?.b_i18n,
|
||
...draft.fields.map((item) => item.b_i18n),
|
||
...draft.powers.map((item) => item.b_i18n),
|
||
].filter(Boolean);
|
||
}
|
||
|
||
function clearDraft() {
|
||
selectedKey.value = "";
|
||
isNew.value = false;
|
||
draft.module = null;
|
||
draft.fields = [];
|
||
draft.powers = [];
|
||
draft.translations = {};
|
||
original.value = "";
|
||
originalTranslationKeys.value = [];
|
||
activeTab.value = "basic";
|
||
}
|
||
|
||
function selectModule(moduleCode) {
|
||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||
if (!module) return;
|
||
|
||
selectedKey.value = moduleCode;
|
||
isNew.value = false;
|
||
draft.module = clone(module);
|
||
draft.fields = clone(data.fields.filter((item) => item.b_module_id === module.b_id));
|
||
draft.powers = clone(data.powers.filter((item) => item.b_module_id === module.b_id));
|
||
const keys = new Set(rowKeys());
|
||
data.translations.forEach((item) => {
|
||
if (String(item.b_key).split(".").includes(module.b_code)) keys.add(item.b_key);
|
||
});
|
||
|
||
draft.translations = {};
|
||
data.translations
|
||
.filter((item) => keys.has(item.b_key))
|
||
.forEach((item) => {
|
||
draft.translations[item.b_key] ||= {};
|
||
draft.translations[item.b_key][item.b_locale] = item.b_value;
|
||
});
|
||
|
||
originalTranslationKeys.value = Array.from(keys);
|
||
original.value = serializeDraft();
|
||
if (module.b_type === "directory") activeTab.value = "basic";
|
||
}
|
||
|
||
async function loadAll(preferredKey = selectedKey.value) {
|
||
loading.value = true;
|
||
try {
|
||
const [modules, fields, powers, translations, userModules, userPowers] = await Promise.all([
|
||
loadDataApi("b_module", null, "b_xh ASC"),
|
||
loadDataApi("b_module_field", null, "b_xh ASC"),
|
||
loadDataApi("b_module_power", null, "b_xh ASC"),
|
||
loadDataApi("b_i18n", null, "b_key ASC"),
|
||
loadDataApi("b_user_module", null, "b_id ASC"),
|
||
loadDataApi("b_user_power", null, "b_id ASC"),
|
||
]);
|
||
Object.assign(data, {
|
||
modules: modules.data,
|
||
fields: fields.data,
|
||
powers: powers.data,
|
||
translations: translations.data,
|
||
userModules: userModules.data,
|
||
userPowers: userPowers.data,
|
||
});
|
||
const target = data.modules.some((item) => item.b_code === preferredKey) ? preferredKey : "";
|
||
|
||
if (target) {
|
||
selectModule(target);
|
||
} else {
|
||
clearDraft();
|
||
}
|
||
} catch (error) {
|
||
message.error(error.message || "模块配置加载失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function nextModuleCode(prefix) {
|
||
const existing = new Set(data.modules.map((item) => item.b_code));
|
||
let index = 1;
|
||
while (existing.has(prefix + index)) index += 1;
|
||
return prefix + index;
|
||
}
|
||
|
||
function parentForNew(contextKey) {
|
||
const context = data.modules.find((item) => item.b_code === contextKey);
|
||
if (!context) return null;
|
||
return context.b_type === "directory" ? context.b_id : context.b_parent_id || null;
|
||
}
|
||
|
||
async function addModule(kind, contextKey) {
|
||
const isDirectory = kind === "directory";
|
||
const id = String((await nextIdApi()).data);
|
||
const code = nextModuleCode(isDirectory ? "m_dir_new_" : "m_page_new_");
|
||
const parentId = parentForNew(contextKey);
|
||
const route = isDirectory ? null : "/" + code.replace(/^m_/, "").replaceAll("_", "-");
|
||
|
||
draft.module = {
|
||
b_id: id,
|
||
b_code: code,
|
||
b_parent_id: parentId,
|
||
b_name: isDirectory ? "新模块" : "新页面",
|
||
b_i18n: "module." + code,
|
||
b_type: isDirectory ? "directory" : "page",
|
||
b_route: route,
|
||
b_viewtable: null,
|
||
b_savetable: null,
|
||
b_keyfield: isDirectory ? null : "b_id",
|
||
b_orderfield: isDirectory ? null : "b_id DESC",
|
||
b_canmenu: isDirectory || route ? 1 : 0,
|
||
b_canuse: 1,
|
||
b_xh: 100,
|
||
};
|
||
draft.fields = [];
|
||
draft.powers = [];
|
||
originalTranslationKeys.value = [];
|
||
draft.translations = {
|
||
["module." + code]: { "zh-CN": draft.module.b_name },
|
||
};
|
||
selectedKey.value = code;
|
||
isNew.value = true;
|
||
original.value = "";
|
||
activeTab.value = "basic";
|
||
}
|
||
|
||
async function handleAdd(kind, contextKey) {
|
||
try {
|
||
await addModule(kind === "module" ? "directory" : "page", contextKey);
|
||
} catch (error) {
|
||
message.error(error.message || "模块创建失败");
|
||
}
|
||
}
|
||
|
||
function updateModuleCode(value) {
|
||
draft.module.b_code = String(value || "").toLowerCase();
|
||
}
|
||
|
||
function normalizeModuleType() {
|
||
if (draft.module.b_type === "directory") return;
|
||
draft.module.b_type = draft.module.b_route?.trim() ? "page" : "data";
|
||
if (draft.module.b_type === "data") draft.module.b_canmenu = 0;
|
||
}
|
||
|
||
function validate() {
|
||
const module = draft.module;
|
||
if (!/^[a-z][a-z0-9_]{1,49}$/.test(module.b_code)) return "模块编码格式不正确";
|
||
if (!module.b_name?.trim()) return "模块名称不能为空";
|
||
if (data.modules.some((item) => item.b_code === module.b_code && item.b_id !== module.b_id)) {
|
||
return "模块编码已经存在";
|
||
}
|
||
if (
|
||
module.b_type === "directory" &&
|
||
(module.b_route || module.b_viewtable || module.b_savetable)
|
||
) {
|
||
return "模块不能配置路由或数据表";
|
||
}
|
||
if (module.b_type === "data" && module.b_canmenu === 1) return "数据模块不能显示在菜单";
|
||
|
||
const identifiers = [module.b_viewtable, module.b_savetable, module.b_keyfield].filter(Boolean);
|
||
if (identifiers.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))) {
|
||
return "表名和字段名只能包含字母、数字和下划线";
|
||
}
|
||
const orderItemPattern =
|
||
/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*(?:\s+(?:ASC|DESC))?$/i;
|
||
if (
|
||
module.b_orderfield &&
|
||
String(module.b_orderfield)
|
||
.split(",")
|
||
.some((item) => !orderItemPattern.test(item.trim()))
|
||
) {
|
||
return "排序条件格式不正确";
|
||
}
|
||
|
||
const fieldCodes = draft.fields.map((item) => item.b_field);
|
||
if (new Set(fieldCodes).size !== fieldCodes.length) return "字段编码不能重复";
|
||
if (fieldCodes.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))) {
|
||
return "字段编码格式不正确";
|
||
}
|
||
|
||
const powerCodes = draft.powers.map((item) => item.b_code);
|
||
if (new Set(powerCodes).size !== powerCodes.length) return "权限代码不能重复";
|
||
if (
|
||
draft.fields.some((item) => !item.b_name?.trim()) ||
|
||
draft.powers.some((item) => !item.b_name?.trim())
|
||
) {
|
||
return "字段名称和权限名称不能为空";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function diffRows(current, previous) {
|
||
const oldMap = new Map(previous.map((item) => [String(item.b_id), item]));
|
||
const currentMap = new Map(current.map((item) => [String(item.b_id), item]));
|
||
return {
|
||
inserts: current.filter((item) => !oldMap.has(String(item.b_id))),
|
||
updates: current.filter((item) => {
|
||
const old = oldMap.get(String(item.b_id));
|
||
return old && JSON.stringify(item) !== JSON.stringify(old);
|
||
}),
|
||
deletes: previous
|
||
.filter((item) => !currentMap.has(String(item.b_id)))
|
||
.map((item) => ({ b_id: item.b_id })),
|
||
};
|
||
}
|
||
|
||
async function translationChanges(previousKeys) {
|
||
const currentKeys = new Set([...rowKeys(), ...Object.keys(draft.translations)]);
|
||
const relevantKeys = new Set([...previousKeys, ...currentKeys]);
|
||
const oldRows = data.translations.filter((item) => relevantKeys.has(item.b_key));
|
||
const desired = [];
|
||
const missing = [];
|
||
|
||
for (const key of currentKeys) {
|
||
for (const locale of I18N_LOCALE_CODES) {
|
||
const value = draft.translations[key]?.[locale]?.trim();
|
||
if (!value) continue;
|
||
const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale);
|
||
if (old) {
|
||
desired.push({ ...old, b_value: value });
|
||
} else {
|
||
missing.push({ key, locale, value });
|
||
}
|
||
}
|
||
}
|
||
oldRows
|
||
.filter((item) => !I18N_LOCALE_CODES.includes(item.b_locale))
|
||
.forEach((item) => desired.push(item));
|
||
|
||
const ids = [];
|
||
for (let index = 0; index < missing.length; index += 1) {
|
||
const response = await nextIdApi();
|
||
ids.push(String(response.data));
|
||
}
|
||
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||
missing.forEach((item, index) => {
|
||
desired.push({
|
||
b_id: ids[index],
|
||
b_key: item.key,
|
||
b_locale: item.locale,
|
||
b_value: item.value,
|
||
b_inputuser_id: String(authStore.user.id),
|
||
b_inputdatetime: now,
|
||
b_updateuser_id: null,
|
||
b_updatedatetime: null,
|
||
});
|
||
});
|
||
|
||
const changes = diffRows(desired, oldRows);
|
||
changes.updates = changes.updates.map((item) => ({
|
||
...item,
|
||
b_updateuser_id: String(authStore.user.id),
|
||
b_updatedatetime: now,
|
||
}));
|
||
return changes;
|
||
}
|
||
|
||
async function saveModule() {
|
||
normalizeModuleType();
|
||
const error = validate();
|
||
if (error) {
|
||
message.warning(error);
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
const oldModule = isNew.value
|
||
? null
|
||
: data.modules.find((item) => item.b_code === selectedKey.value);
|
||
const oldFields = oldModule
|
||
? data.fields.filter((item) => item.b_module_id === oldModule.b_id)
|
||
: [];
|
||
const oldPowers = oldModule
|
||
? data.powers.filter((item) => item.b_module_id === oldModule.b_id)
|
||
: [];
|
||
const previousKeys = new Set(
|
||
[
|
||
...originalTranslationKeys.value,
|
||
oldModule?.b_i18n,
|
||
...oldFields.map((item) => item.b_i18n),
|
||
...oldPowers.map((item) => item.b_i18n),
|
||
].filter(Boolean),
|
||
);
|
||
const fieldChanges = diffRows(draft.fields, oldFields);
|
||
const powerChanges = diffRows(draft.powers, oldPowers);
|
||
const removedPowerIds = new Set(powerChanges.deletes.map((item) => String(item.b_id)));
|
||
const assignedPowerDeletes = data.userPowers
|
||
.filter((item) => removedPowerIds.has(String(item.b_power_id)))
|
||
.map((item) => ({ b_id: item.b_id }));
|
||
const requests = [];
|
||
|
||
if (assignedPowerDeletes.length) {
|
||
requests.push({
|
||
table: "b_user_power",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: assignedPowerDeletes,
|
||
});
|
||
}
|
||
|
||
const i18nChanges = await translationChanges(previousKeys);
|
||
requests.push(
|
||
{
|
||
table: "b_module",
|
||
key_field: "b_id",
|
||
inserts: isNew.value ? [draft.module] : [],
|
||
updates: isNew.value ? [] : [draft.module],
|
||
deletes: [],
|
||
},
|
||
{ table: "b_module_field", key_field: "b_id", ...fieldChanges },
|
||
{ table: "b_module_power", key_field: "b_id", ...powerChanges },
|
||
{ table: "b_i18n", key_field: "b_id", ...i18nChanges },
|
||
);
|
||
await saveObjectApi(
|
||
requests.filter(
|
||
(request) => request.inserts.length || request.updates.length || request.deletes.length,
|
||
),
|
||
);
|
||
|
||
const savedKey = draft.module.b_code;
|
||
message.success("模块配置已保存");
|
||
await loadAll(savedKey);
|
||
} catch (error) {
|
||
message.error(error.message || "模块保存失败");
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
function requestDeleteModule(moduleCode) {
|
||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||
message.warning("请先删除下级模块");
|
||
return;
|
||
}
|
||
|
||
pendingDeleteKey.value = moduleCode;
|
||
deleteConfirmOpen.value = true;
|
||
}
|
||
|
||
async function deleteModule(moduleCode = draft.module?.b_code) {
|
||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||
message.warning("请先删除下级模块");
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
const powers = data.powers.filter((item) => item.b_module_id === module.b_id);
|
||
const powerIds = new Set(powers.map((item) => String(item.b_id)));
|
||
const fields = data.fields.filter((item) => item.b_module_id === module.b_id);
|
||
|
||
const requests = [
|
||
{
|
||
table: "b_user_power",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: data.userPowers
|
||
.filter((item) => powerIds.has(String(item.b_power_id)))
|
||
.map((item) => ({ b_id: item.b_id })),
|
||
},
|
||
{
|
||
table: "b_user_module",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: data.userModules
|
||
.filter((item) => item.b_module_id === module.b_id)
|
||
.map((item) => ({ b_id: item.b_id })),
|
||
},
|
||
{
|
||
table: "b_module_power",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: powers.map((item) => ({ b_id: item.b_id })),
|
||
},
|
||
{
|
||
table: "b_module_field",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: fields.map((item) => ({ b_id: item.b_id })),
|
||
},
|
||
{
|
||
table: "b_module",
|
||
key_field: "b_id",
|
||
inserts: [],
|
||
updates: [],
|
||
deletes: [{ b_id: module.b_id }],
|
||
},
|
||
];
|
||
await saveObjectApi(
|
||
requests.filter(
|
||
(request) => request.inserts.length || request.updates.length || request.deletes.length,
|
||
),
|
||
);
|
||
|
||
message.success("模块已删除");
|
||
deleteConfirmOpen.value = false;
|
||
pendingDeleteKey.value = "";
|
||
await loadAll(selectedKey.value === moduleCode ? "" : selectedKey.value);
|
||
} catch (error) {
|
||
message.error(error.message || "模块删除失败");
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
function resetDraft() {
|
||
if (isNew.value) {
|
||
clearDraft();
|
||
return;
|
||
}
|
||
selectModule(selectedKey.value);
|
||
}
|
||
|
||
function setBoolean(record, field, checked) {
|
||
record[field] = checked ? 1 : 0;
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await permissionStore.load();
|
||
await loadAll();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="module-management">
|
||
<ModuleTreePanel
|
||
:modules="data.modules"
|
||
:protected-keys="PROTECTED_MODULE_CODES"
|
||
:selected-key="selectedKey"
|
||
:loading="loading"
|
||
:can-create="canCreate"
|
||
:can-delete="canDelete"
|
||
@select="selectModule"
|
||
@add="handleAdd"
|
||
@delete="requestDeleteModule"
|
||
@refresh="loadAll()"
|
||
/>
|
||
|
||
<main class="module-workspace">
|
||
<a-spin :spinning="loading || saving">
|
||
<template v-if="draft.module">
|
||
<header class="workspace-header">
|
||
<div class="title-block">
|
||
<div class="title-line">
|
||
<h1>{{ draft.module.b_name }}</h1>
|
||
<a-tag>{{ draft.module.b_code }}</a-tag>
|
||
<a-tag color="blue">{{ moduleKindLabel }}</a-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-switches">
|
||
<div class="switch-item">
|
||
<span class="switch-label">菜单显示</span>
|
||
<a-switch
|
||
:checked="draft.module.b_canmenu === 1"
|
||
:disabled="draft.module.b_type !== 'directory' && !draft.module.b_route?.trim()"
|
||
@change="setBoolean(draft.module, 'b_canmenu', $event)"
|
||
/>
|
||
</div>
|
||
<div class="switch-item">
|
||
<span class="switch-label">
|
||
{{ draft.module.b_type === "directory" ? "启用模块" : "启用页面" }}
|
||
</span>
|
||
<a-switch
|
||
:checked="draft.module.b_canuse === 1"
|
||
@change="setBoolean(draft.module, 'b_canuse', $event)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-actions">
|
||
<a-button :disabled="!dirty" @click="resetDraft">
|
||
<RotateCcw :size="15" />取消
|
||
</a-button>
|
||
<a-popconfirm
|
||
title="确定删除当前模块吗?"
|
||
:disabled="
|
||
isNew || !canDelete || PROTECTED_MODULE_CODES.includes(draft.module.b_code)
|
||
"
|
||
@confirm="deleteModule()"
|
||
>
|
||
<a-button
|
||
danger
|
||
:disabled="
|
||
isNew || !canDelete || PROTECTED_MODULE_CODES.includes(draft.module.b_code)
|
||
"
|
||
>
|
||
<Trash2 :size="15" />删除
|
||
</a-button>
|
||
</a-popconfirm>
|
||
<a-button
|
||
type="primary"
|
||
:loading="saving"
|
||
:disabled="!dirty || !canSave"
|
||
@click="saveModule"
|
||
>
|
||
<Save :size="15" />保存
|
||
</a-button>
|
||
</div>
|
||
</header>
|
||
|
||
<a-tabs v-model:active-key="activeTab" class="workspace-tabs">
|
||
<a-tab-pane key="basic">
|
||
<template #tab>
|
||
<span class="tab-label"><FileText :size="15" />基本信息</span>
|
||
</template>
|
||
<ModuleBasicInfoPanel
|
||
v-model:module="draft.module"
|
||
v-model:translations="draft.translations"
|
||
:directory-parent-options="directoryParentOptions"
|
||
:parent-options="parentOptions"
|
||
@update-module-code="updateModuleCode"
|
||
/>
|
||
</a-tab-pane>
|
||
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="fields">
|
||
<template #tab>
|
||
<span class="tab-label">
|
||
<Database :size="15" />字段定义 <a-badge :count="draft.fields.length" />
|
||
</span>
|
||
</template>
|
||
<ModuleFieldDefinitionPanel
|
||
v-model:fields="draft.fields"
|
||
:module-id="draft.module.b_id"
|
||
:module-code="draft.module.b_code"
|
||
/>
|
||
</a-tab-pane>
|
||
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="powers">
|
||
<template #tab>
|
||
<span class="tab-label">
|
||
<ShieldCheck :size="15" />操作权限 <a-badge :count="draft.powers.length" />
|
||
</span>
|
||
</template>
|
||
<ModulePowerPanel
|
||
v-model:powers="draft.powers"
|
||
:module-id="draft.module.b_id"
|
||
:module-code="draft.module.b_code"
|
||
/>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</template>
|
||
<a-empty
|
||
v-else
|
||
class="workspace-empty"
|
||
description="请选择模块,或右键树节点创建模块和页面"
|
||
/>
|
||
</a-spin>
|
||
</main>
|
||
|
||
<a-modal
|
||
v-model:open="deleteConfirmOpen"
|
||
title="删除模块"
|
||
ok-text="删除"
|
||
cancel-text="取消"
|
||
ok-type="danger"
|
||
:confirm-loading="saving"
|
||
@ok="deleteModule(pendingDeleteKey)"
|
||
>
|
||
<p>
|
||
确定删除
|
||
{{ pendingDeleteModule?.b_name || pendingDeleteKey }}
|
||
吗?关联字段和权限将一并删除;共享多语言资源会保留在多语言中心。
|
||
</p>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.module-management {
|
||
box-sizing: border-box;
|
||
display: grid;
|
||
grid-template-columns: 250px minmax(0, 1fr);
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
}
|
||
|
||
.module-workspace {
|
||
display: flex;
|
||
height: 100%;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0 10px 6px;
|
||
}
|
||
|
||
.workspace-header {
|
||
display: flex;
|
||
min-height: 56px;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.header-switches,
|
||
.switch-item,
|
||
.title-line,
|
||
.header-actions,
|
||
.tab-label {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-switches {
|
||
flex: 0 0 auto;
|
||
gap: 16px;
|
||
padding: 0 14px;
|
||
}
|
||
|
||
.switch-item {
|
||
gap: 8px;
|
||
}
|
||
|
||
.switch-label {
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.title-line {
|
||
gap: 9px;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
color: #111827;
|
||
font-size: 20px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.header-actions {
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.tab-label {
|
||
gap: 6px;
|
||
}
|
||
|
||
.workspace-tabs {
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.workspace-empty {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 100%;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
margin: 0;
|
||
}
|
||
|
||
:deep(.ant-spin-nested-loading),
|
||
:deep(.ant-spin-container) {
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
:deep(.module-workspace > .ant-spin) {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.module-workspace > .ant-spin > .ant-spin-container),
|
||
:deep(.module-workspace > .ant-spin-nested-loading > .ant-spin-container) {
|
||
display: flex;
|
||
height: 100%;
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.workspace-tabs > .ant-tabs-body-holder) {
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
:deep(.workspace-tabs > .ant-tabs-body-holder > .ant-tabs-body),
|
||
:deep(.workspace-tabs > .ant-tabs-body-holder > .ant-tabs-body > .ant-tabs-content),
|
||
:deep(.workspace-tabs .ant-tabs-tabpane) {
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
:deep(.workspace-tabs .ant-tabs-tabpane) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.workspace-tabs > .ant-tabs-nav) {
|
||
flex: 0 0 auto;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.module-management {
|
||
grid-template-columns: 220px minmax(0, 1fr);
|
||
}
|
||
|
||
.workspace-header {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
padding-block: 8px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.module-management {
|
||
grid-template-rows: 180px minmax(0, 1fr);
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.module-workspace {
|
||
padding: 0 8px 8px;
|
||
}
|
||
}
|
||
</style>
|