706 lines
19 KiB
Vue
706 lines
19 KiB
Vue
<script setup>
|
|
import { computed, h, ref, watch } from "vue";
|
|
import { message } from "antdv-next";
|
|
import FmsTree from "@/components/FmsTree.vue";
|
|
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
|
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
|
import { createTempId } from "@/utils/tempId";
|
|
import { Plus } from "@lucide/vue";
|
|
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
|
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
|
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
|
|
import ModuleEditConfigPanel from "./components/ModuleEditConfigPanel.vue";
|
|
import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
|
|
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
|
|
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
|
|
|
// ---- 状态 ----
|
|
const treeData = ref([]);
|
|
const treeLoading = ref(false);
|
|
const selectedModuleId = ref(null);
|
|
const skipLoad = ref(false);
|
|
const activeTab = ref("basic");
|
|
|
|
// 原始数据(加载时保存,用于 diff)
|
|
const originals = ref({
|
|
module: null,
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
});
|
|
|
|
// 编辑草稿
|
|
const drafts = ref({
|
|
module: null,
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
});
|
|
|
|
const configLoading = ref(false);
|
|
const configSaving = ref(false);
|
|
|
|
// 当前选中模块对象(从 drafts.module 获取)
|
|
const selectedModule = computed(() => drafts.value.module);
|
|
|
|
// 可见 Tab
|
|
const visibleTabs = computed(() => {
|
|
const type = selectedModule.value?.b_module_type;
|
|
if (!type) return [];
|
|
const tabs = [{ key: "basic", tab: "基本信息", icon: "Info" }];
|
|
if (type === "data") {
|
|
tabs.push(
|
|
{ key: "fields", tab: "字段定义", icon: "Table" },
|
|
{ key: "list", tab: "列表配置", icon: "List" },
|
|
{ key: "edit", tab: "编辑配置", icon: "Edit" },
|
|
{ key: "query", tab: "查询配置", icon: "Search" },
|
|
);
|
|
}
|
|
tabs.push(
|
|
{ key: "powers", tab: "权限配置", icon: "Lock" },
|
|
{ key: "i18n", tab: "多语言", icon: "Translation" },
|
|
);
|
|
return tabs;
|
|
});
|
|
|
|
// ---- 模块树 ----
|
|
function buildTree(modules) {
|
|
const map = new Map();
|
|
const roots = [];
|
|
modules.forEach((m) => {
|
|
m.key = String(m.b_id);
|
|
m.title = m.b_name || m.b_code;
|
|
m.b_parent_id = m.b_parent_id ? String(m.b_parent_id) : null;
|
|
map.set(m.key, m);
|
|
});
|
|
modules.forEach((m) => {
|
|
if (m.b_parent_id && map.has(m.b_parent_id)) {
|
|
const parent = map.get(m.b_parent_id);
|
|
if (!parent.children) parent.children = [];
|
|
parent.children.push(m);
|
|
} else {
|
|
roots.push(m);
|
|
}
|
|
});
|
|
return roots;
|
|
}
|
|
|
|
async function loadTree() {
|
|
treeLoading.value = true;
|
|
try {
|
|
const res = await loadDataApi("b_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
|
treeData.value = buildTree(res.data || []);
|
|
} finally {
|
|
treeLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 模块配置加载 ----
|
|
async function loadModuleConfig(moduleId) {
|
|
configLoading.value = true;
|
|
try {
|
|
const id = String(moduleId);
|
|
const [moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, powersRes] =
|
|
await Promise.all([
|
|
loadDataApi("b_module", `b_id = ${id}`),
|
|
loadDataApi("b_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
|
loadDataApi("b_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
]);
|
|
|
|
const original = {
|
|
module: moduleRes.data?.[0] || null,
|
|
fields: fieldsRes.data || [],
|
|
groups: groupsRes.data || [],
|
|
listConfig: listRes.data || [],
|
|
editConfig: editRes.data || [],
|
|
queryConfig: queryRes.data || [],
|
|
powers: powersRes.data || [],
|
|
};
|
|
originals.value = original;
|
|
drafts.value = cloneData(original);
|
|
activeTab.value = "basic";
|
|
} finally {
|
|
configLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 树节点选择 ----
|
|
function onTreeSelect(keys) {
|
|
if (keys.length > 0) {
|
|
selectedModuleId.value = String(keys[0]);
|
|
}
|
|
}
|
|
|
|
watch(selectedModuleId, (id) => {
|
|
if (!id) return;
|
|
if (skipLoad.value) {
|
|
skipLoad.value = false;
|
|
return;
|
|
}
|
|
loadModuleConfig(id);
|
|
});
|
|
|
|
// ---- 子面板数据变更 ----
|
|
function onUpdate(section, newData) {
|
|
drafts.value[section] = newData;
|
|
}
|
|
|
|
function onSetField(field, value) {
|
|
if (drafts.value.module) {
|
|
drafts.value.module[field] = value;
|
|
}
|
|
}
|
|
|
|
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
|
function onGroupsSaved(newGroups) {
|
|
originals.value.groups = cloneData(newGroups);
|
|
drafts.value.groups = cloneData(newGroups);
|
|
}
|
|
|
|
// ---- 新增模块 ----
|
|
async function onAddModule(parentId, moduleType = "directory") {
|
|
const res = await nextIdApi();
|
|
const newId = String(res.data);
|
|
const newModule = {
|
|
b_id: newId,
|
|
b_code: "",
|
|
b_parent_id: parentId ? String(parentId) : null,
|
|
b_name: "",
|
|
b_i18n: null,
|
|
b_module_type: moduleType,
|
|
b_route: null,
|
|
b_viewtable: null,
|
|
b_savetable: null,
|
|
b_keyfield: null,
|
|
b_orderfield: null,
|
|
b_canmenu: 0,
|
|
b_canuse: 1,
|
|
b_xh: 0,
|
|
};
|
|
|
|
// 更新 originals 和 drafts
|
|
originals.value = {
|
|
module: cloneData(newModule),
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
};
|
|
drafts.value = cloneData(originals.value);
|
|
skipLoad.value = true;
|
|
selectedModuleId.value = newId;
|
|
activeTab.value = "basic";
|
|
|
|
// 刷新树
|
|
await loadTree();
|
|
}
|
|
|
|
const addModuleMenu = {
|
|
items: [
|
|
{ key: "directory", label: "目录" },
|
|
{ key: "page", label: "页面" },
|
|
{ key: "data", label: "数据" },
|
|
],
|
|
onClick: ({ key }) => onAddModule(null, key),
|
|
};
|
|
|
|
// ---- 删除模块 ----
|
|
async function onDeleteModule() {
|
|
const moduleId = selectedModuleId.value;
|
|
if (!moduleId) return;
|
|
try {
|
|
await saveObjectApi([{ table: "b_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
|
message.success("模块已删除");
|
|
selectedModuleId.value = null;
|
|
drafts.value.module = null;
|
|
await loadTree();
|
|
} catch (e) {
|
|
message.error("删除失败:" + (e.message || e));
|
|
}
|
|
}
|
|
|
|
// ---- 统一保存 ----
|
|
async function saveAll() {
|
|
if (!selectedModule.value) return;
|
|
configSaving.value = true;
|
|
|
|
try {
|
|
const saveReqs = [];
|
|
|
|
// b_module 主表
|
|
const moduleData = drafts.value.module;
|
|
const moduleOriginal = originals.value.module;
|
|
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
|
const isNew = !moduleOriginal?.b_code;
|
|
saveReqs.push({
|
|
table: "b_module",
|
|
key_field: "b_id",
|
|
[isNew ? "inserts" : "updates"]: [moduleData],
|
|
});
|
|
}
|
|
|
|
// 子表:fields, listConfig, editConfig, queryConfig, powers
|
|
const subTables = [
|
|
["b_module_field", "fields"],
|
|
["b_module_field_list", "listConfig"],
|
|
["b_module_field_edit", "editConfig"],
|
|
["b_module_field_query", "queryConfig"],
|
|
["b_module_power", "powers"],
|
|
];
|
|
for (const [table, section] of subTables) {
|
|
const current = drafts.value[section] || [];
|
|
const previous = originals.value[section] || [];
|
|
const changes = diffRows(current, previous);
|
|
const req = tableChange(table, changes);
|
|
if (hasChanges(req)) {
|
|
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
|
saveReqs.push(req);
|
|
}
|
|
}
|
|
|
|
if (saveReqs.length === 0) {
|
|
message.info("没有需要保存的变更");
|
|
return;
|
|
}
|
|
|
|
await saveObjectApi(saveReqs);
|
|
|
|
// 更新 originals
|
|
originals.value = cloneData(drafts.value);
|
|
message.success("保存成功");
|
|
await loadTree();
|
|
} catch (e) {
|
|
message.error("保存失败:" + (e.message || e));
|
|
} finally {
|
|
configSaving.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 字段同步 ----
|
|
function typeFromSqlType(sqlType) {
|
|
if (!sqlType) return "input";
|
|
const t = sqlType.toLowerCase();
|
|
if (
|
|
t.includes("char") ||
|
|
t.includes("text") ||
|
|
t.includes("nchar") ||
|
|
t.includes("ntext") ||
|
|
t.includes("xml")
|
|
) {
|
|
if (t.includes("text") || t.includes("ntext") || t.includes("xml")) return "textarea";
|
|
return "input";
|
|
}
|
|
if (t.includes("int") || t.includes("float") || t.includes("double") || t.includes("real"))
|
|
return "number";
|
|
if (t.includes("decimal") || t.includes("numeric") || t.includes("money")) {
|
|
if (t.includes("money")) return "money";
|
|
return "number";
|
|
}
|
|
if (t.includes("date") || t.includes("time")) {
|
|
if (t.includes("date") && (t.includes("time") || t.includes("smalldatetime")))
|
|
return "datetime";
|
|
return "date";
|
|
}
|
|
if (t.includes("bit")) return "switch";
|
|
return "input";
|
|
}
|
|
|
|
async function syncFields() {
|
|
const module = selectedModule.value;
|
|
if (!module?.b_viewtable) {
|
|
message.warning("当前模块未配置查询视图");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
configSaving.value = true;
|
|
const res = await describeApi(module.b_viewtable);
|
|
const columns = res.data || [];
|
|
const existingFields = originals.value.fields || [];
|
|
|
|
// 构建视图列名集合
|
|
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
|
|
|
|
// 已有字段映射(按字段名)
|
|
const existingMap = new Map(
|
|
existingFields.map((f) => [f.b_field?.toLowerCase(), f]),
|
|
);
|
|
|
|
const mergedFields = [];
|
|
|
|
// 遍历视图列
|
|
for (const col of columns) {
|
|
const existing = existingMap.get(col.name.toLowerCase());
|
|
if (existing) {
|
|
// 已有字段:保留原配置
|
|
mergedFields.push({ ...existing, b_canuse: 1 });
|
|
} else {
|
|
// 新字段:创建默认记录
|
|
mergedFields.push({
|
|
b_id: createTempId(),
|
|
b_module_id: String(module.b_id),
|
|
b_field: col.name,
|
|
b_name: col.name,
|
|
b_i18n: null,
|
|
b_type: typeFromSqlType(col.type),
|
|
b_default_value: null,
|
|
b_options: null,
|
|
b_canuse: 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 视图中不存在的已有字段标记为不可用
|
|
for (const existing of existingFields) {
|
|
if (!viewColumnSet.has(existing.b_field?.toLowerCase())) {
|
|
mergedFields.push({ ...existing, b_canuse: 0 });
|
|
}
|
|
}
|
|
|
|
// 保存到 b_module_field
|
|
const changes = diffRows(mergedFields, existingFields);
|
|
const req = tableChange("b_module_field", changes);
|
|
if (hasChanges(req)) {
|
|
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
|
await saveObjectApi([req]);
|
|
message.success(`字段同步完成:${columns.length} 列,新增 ${req.inserts?.length || 0},停用 ${req.deletes?.length || 0}`);
|
|
} else {
|
|
message.info("字段已是最新,无需同步");
|
|
}
|
|
|
|
// 重新加载
|
|
const fieldsRes = await loadDataApi(
|
|
"b_module_field",
|
|
`b_module_id = ${module.b_id}`,
|
|
"b_id ASC",
|
|
);
|
|
originals.value.fields = fieldsRes.data || [];
|
|
drafts.value.fields = cloneData(originals.value.fields);
|
|
} catch (e) {
|
|
message.error("字段同步失败:" + (e.message || e));
|
|
} finally {
|
|
configSaving.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 初始化 ----
|
|
loadTree();
|
|
</script>
|
|
|
|
<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']">
|
|
<a-button><Plus :size="14" />新增</a-button>
|
|
</a-dropdown>
|
|
</div>
|
|
<div v-if="treeData.length === 0" class="mm-tree-empty">
|
|
<a-empty description="暂无模块" />
|
|
</div>
|
|
<FmsTree
|
|
v-else
|
|
class="mm-tree"
|
|
:tree-data="treeData"
|
|
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
|
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
|
@select="onTreeSelect"
|
|
>
|
|
<template #title="{ key, title }">
|
|
<span class="mm-tree-node">
|
|
<span>{{ title }}</span>
|
|
<span class="mm-tree-node-actions">
|
|
<a-button type="text" @click.stop="onAddModule(key)">+</a-button>
|
|
</span>
|
|
</span>
|
|
</template>
|
|
</FmsTree>
|
|
</aside>
|
|
|
|
<!-- 右侧详情 -->
|
|
<main class="mm-main">
|
|
<template v-if="selectedModule">
|
|
<!-- 页面头部 -->
|
|
<div class="mm-header">
|
|
<a-space>
|
|
<a-button type="primary" :loading="configSaving" @click="saveAll">保存</a-button>
|
|
<a-popconfirm title="确定删除该模块?" @confirm="onDeleteModule">
|
|
<a-button danger>删除模块</a-button>
|
|
</a-popconfirm>
|
|
</a-space>
|
|
<div class="mm-header-center">
|
|
<a-space size="middle">
|
|
<span>启用</span>
|
|
<a-switch
|
|
:checked="drafts.module?.b_canuse === 1"
|
|
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
|
/>
|
|
<a-divider type="vertical" />
|
|
<span>显示菜单</span>
|
|
<a-switch
|
|
:checked="drafts.module?.b_canmenu === 1"
|
|
:disabled="drafts.module?.b_module_type === 'data'"
|
|
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
|
/>
|
|
</a-space>
|
|
</div>
|
|
<span class="mm-header-title">{{ selectedModule.b_name || selectedModule.b_code }}</span>
|
|
<span class="mm-header-code">{{ selectedModule.b_code || '(未设置编码)' }}</span>
|
|
<span class="mm-header-type">
|
|
<a-tag>{{ selectedModule.b_module_type === 'directory' ? '目录' : selectedModule.b_module_type === 'page' ? '页面' : '数据' }}</a-tag>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Tab 面板 -->
|
|
<div class="mm-body">
|
|
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
|
<a-tab-pane
|
|
v-for="tab in visibleTabs"
|
|
:key="tab.key"
|
|
:tab="tab.tab"
|
|
>
|
|
<template v-if="configLoading">
|
|
<div class="mm-loading"><a-spin /></div>
|
|
</template>
|
|
<template v-else>
|
|
<ModuleBasicPanel
|
|
v-if="tab.key === 'basic'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:data="drafts.module"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('module', v)"
|
|
/>
|
|
<ModuleFieldsPanel
|
|
v-else-if="tab.key === 'fields'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:fields="drafts.fields"
|
|
:view-table="selectedModule.b_viewtable"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('fields', v)"
|
|
@sync="syncFields"
|
|
/>
|
|
<ModuleListConfigPanel
|
|
v-else-if="tab.key === 'list'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:list-config="drafts.listConfig"
|
|
:fields="drafts.fields"
|
|
:groups="drafts.groups"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('listConfig', v)"
|
|
@update-groups="onGroupsSaved"
|
|
/>
|
|
<ModuleEditConfigPanel
|
|
v-else-if="tab.key === 'edit'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:edit-config="drafts.editConfig"
|
|
:fields="drafts.fields"
|
|
:groups="drafts.groups"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('editConfig', v)"
|
|
@update-groups="onGroupsSaved"
|
|
/>
|
|
<ModuleQueryConfigPanel
|
|
v-else-if="tab.key === 'query'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:query-config="drafts.queryConfig"
|
|
:fields="drafts.fields"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('queryConfig', v)"
|
|
/>
|
|
<ModulePowersPanel
|
|
v-else-if="tab.key === 'powers'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:powers="drafts.powers"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('powers', v)"
|
|
/>
|
|
<ModuleI18nPanel
|
|
v-else-if="tab.key === 'i18n'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:loading="configSaving"
|
|
/>
|
|
</template>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 未选中模块 -->
|
|
<div v-else class="mm-empty">
|
|
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.mm-page {
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
background: var(--fms-bg);
|
|
}
|
|
|
|
.mm-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: none;
|
|
width: 260px;
|
|
border-right: 1px solid var(--fms-border);
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
.mm-sidebar-header {
|
|
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 0;
|
|
}
|
|
|
|
.mm-tree-empty {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 0;
|
|
}
|
|
|
|
.mm-tree-node {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
gap: 4px;
|
|
}
|
|
|
|
.mm-tree-node-actions {
|
|
display: none;
|
|
flex: none;
|
|
}
|
|
|
|
.mm-tree-node:hover .mm-tree-node-actions {
|
|
display: flex;
|
|
}
|
|
|
|
.mm-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
.mm-header {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: none;
|
|
gap: 12px;
|
|
padding: 12px 20px;
|
|
background: var(--fms-surface);
|
|
border-bottom: 1px solid var(--fms-border);
|
|
}
|
|
|
|
.mm-header-center {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mm-header-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--fms-text);
|
|
}
|
|
|
|
.mm-header-code {
|
|
font-size: 13px;
|
|
color: var(--fms-text-secondary);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.mm-header-type {
|
|
flex: none;
|
|
}
|
|
|
|
.mm-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mm-tabs {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
:deep(.ant-tabs-nav) {
|
|
margin-bottom: 0;
|
|
padding: 0 20px;
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
:deep(.ant-tabs-content-holder) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.ant-tabs-content) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.ant-tabs-tabpane) {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.mm-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 200px;
|
|
}
|
|
|
|
.mm-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
}
|
|
</style>
|