20260806224941

This commit is contained in:
oneao committed 2026-08-06 22:49:41 +08:00
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>