20260806224941
This commit is contained in:
1 parent
6baa339efa
commit
6938deef39
12 files changed
+367
-300
No files matched your search
@@ -1,10 +1,33 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsMemoInput from "@/components/FmsMemoInput.vue";
|
||||
import { buildModuleI18nKey } from "@/utils/i18n";
|
||||
import { getIcon, getIconNames } from "@/utils/lucideIcons";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
const iconOptions = ref([]);
|
||||
const iconsReady = ref(false);
|
||||
|
||||
async function loadIconOptions() {
|
||||
if (iconsReady.value) return;
|
||||
const names = await getIconNames();
|
||||
iconOptions.value = names.map((name) => ({
|
||||
value: name,
|
||||
label: name,
|
||||
icon: getIcon(name),
|
||||
}));
|
||||
iconsReady.value = true;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadIconOptions();
|
||||
});
|
||||
|
||||
function getIconByName(name) {
|
||||
return getIcon(name);
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
data: { type: Object, default: null },
|
||||
@@ -28,12 +51,16 @@ watch(
|
||||
const isPage = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "page";
|
||||
});
|
||||
const isDirectory = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "directory";
|
||||
});
|
||||
const isData = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "data";
|
||||
});
|
||||
const isDataCategory = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "data_category";
|
||||
});
|
||||
const showIconField = computed(() => isDirectory.value || isPage.value);
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", formData.value);
|
||||
@@ -138,6 +165,47 @@ function onI18nSaved(key) {
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="showIconField" :span="8">
|
||||
<a-form-item label="菜单图标">
|
||||
<a-select
|
||||
v-if="iconsReady"
|
||||
:value="formData.b_icon || ''"
|
||||
:options="iconOptions"
|
||||
:virtual="true"
|
||||
:list-item-height="32"
|
||||
:list-height="280"
|
||||
show-search
|
||||
:filter-option="(input, option) => option.value.toLowerCase().includes(input.toLowerCase())"
|
||||
allow-clear
|
||||
placeholder="搜索图标名称"
|
||||
@update:value="(v) => setField('b_icon', v)"
|
||||
>
|
||||
<template #optionRender="{ option }">
|
||||
<span class="icon-option">
|
||||
<component
|
||||
:is="option.data.icon"
|
||||
v-if="option.data.icon"
|
||||
class="icon-option__icon"
|
||||
:size="16"
|
||||
/>
|
||||
<span>{{ option.data.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #labelRender="{ label, value }">
|
||||
<span class="icon-option">
|
||||
<component
|
||||
:is="getIconByName(value)"
|
||||
v-if="value"
|
||||
class="icon-option__icon"
|
||||
:size="16"
|
||||
/>
|
||||
<span>{{ label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</a-select>
|
||||
<a-input v-else :value="formData.b_icon || ''" readonly placeholder="图标加载中..." />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isPage" :span="8">
|
||||
<a-form-item label="页面路由" required>
|
||||
<a-input :value="formData.b_route" @update:value="(v) => setField('b_route', v)" />
|
||||
@@ -215,4 +283,15 @@ function onI18nSaved(key) {
|
||||
.i18n-key-btn {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.icon-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&__icon {
|
||||
flex: none;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, h, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { computed, h, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||||
@@ -41,9 +41,7 @@ const treeLoading = ref(false);
|
||||
const expandedTreeKeys = ref([]);
|
||||
const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const expandTimer = ref(null);
|
||||
const activeTab = ref("basic");
|
||||
let expandDelayResolve = null;
|
||||
function buildTreeContextMenu(node) {
|
||||
const items = [];
|
||||
const iconSize = 14;
|
||||
@@ -361,71 +359,16 @@ async function loadModuleConfig(moduleId, options = {}) {
|
||||
}
|
||||
|
||||
// ---- 树节点选择 ----
|
||||
function clearExpandTimer() {
|
||||
if (expandTimer.value != null) {
|
||||
clearTimeout(expandTimer.value);
|
||||
expandTimer.value = null;
|
||||
}
|
||||
if (expandDelayResolve) {
|
||||
expandDelayResolve(false);
|
||||
expandDelayResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForExpandAnimation() {
|
||||
return new Promise((resolve) => {
|
||||
expandDelayResolve = (ready) => {
|
||||
resolve(ready);
|
||||
};
|
||||
expandTimer.value = setTimeout(() => {
|
||||
expandTimer.value = null;
|
||||
expandDelayResolve = null;
|
||||
resolve(true);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
function onTreeSelect(keys, info) {
|
||||
if (busy.value) return;
|
||||
if (info?.node?.isFmsRoot) {
|
||||
selectedModuleId.value = null;
|
||||
clearExpandTimer();
|
||||
return;
|
||||
}
|
||||
const nodeKey = info?.node?.key ?? keys[0];
|
||||
if (nodeKey == null) return;
|
||||
|
||||
const key = String(nodeKey);
|
||||
const node = findNode(key);
|
||||
const hasChildren = node?.children?.length;
|
||||
|
||||
clearExpandTimer();
|
||||
|
||||
if (!hasChildren) {
|
||||
// 叶子节点:立即加载数据
|
||||
selectedModuleId.value = key;
|
||||
return;
|
||||
}
|
||||
|
||||
// 父节点:处理展开/收起
|
||||
const expandedKeys = expandedTreeKeys.value.map(String);
|
||||
const isCurrentlyExpanded = expandedKeys.includes(key);
|
||||
|
||||
if (isCurrentlyExpanded) {
|
||||
// 收起:立即生效,同时加载数据
|
||||
expandedTreeKeys.value = expandedKeys.filter((item) => item !== key);
|
||||
selectedModuleId.value = key;
|
||||
} else {
|
||||
// 展开:立即高亮,下一帧发请求;结果回写最多等到展开动画结束。
|
||||
expandedTreeKeys.value = [...expandedKeys, key];
|
||||
skipLoad.value = true;
|
||||
selectedModuleId.value = key;
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(key, {
|
||||
waitForPaint: true,
|
||||
applyReady: waitForExpandAnimation(),
|
||||
});
|
||||
}
|
||||
selectedModuleId.value = String(nodeKey);
|
||||
}
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
@@ -846,6 +789,8 @@ async function saveAll() {
|
||||
unsavedModuleIds.delete(moduleId);
|
||||
updateSavedTreeNode(moduleData);
|
||||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||||
// 刷新权限数据,让菜单立即反映模块结构变更
|
||||
permissionStore.load().catch(() => {});
|
||||
message.success("保存成功");
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
@@ -1091,8 +1036,6 @@ void permissionStore
|
||||
.load()
|
||||
.then(loadTree)
|
||||
.catch((e) => message.error("模块树加载失败:" + (e.message || e)));
|
||||
|
||||
onBeforeUnmount(clearExpandTimer);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1161,15 +1104,17 @@ onBeforeUnmount(clearExpandTimer);
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="
|
||||
busy || ['data', 'data_category'].includes(drafts.module?.b_module_type)
|
||||
"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
<template v-if="drafts.module?.b_module_type !== 'data'">
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="
|
||||
busy || ['data', 'data_category'].includes(drafts.module?.b_module_type)
|
||||
"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="drafts.module?.b_module_type === 'data'">
|
||||
<a-divider type="vertical" />
|
||||
<span>自动编码</span>
|
||||
|
||||
Reference in new issue
Block a user