20260822171825

This commit is contained in:
oneao committed 2026-08-22 17:18:27 +08:00
1 parent c43b4c8364
commit 27a32b313d
187 files changed
+34869 -4741

No files matched your search

@@ -96,14 +96,14 @@ defineExpose({ open })
@ok="save"
>
<div class="mm-i18n-quick">
<div class="mm-i18n-quick__key">
<div class="mm-i18n-quick__field">
<label>多语言键</label>
<Input v-model="i18nKey" disabled />
</div>
<div class="mm-i18n-quick__rows">
<div v-for="(item, idx) in entries" :key="idx" class="mm-i18n-quick__row">
<span class="mm-i18n-quick__locale">{{ item.b_name }} ({{ item.b_locale }})</span>
<Input v-model="item.b_value" class="mm-i18n-quick__value" />
<div v-for="(item, idx) in entries" :key="idx" class="mm-i18n-quick__field">
<label>{{ item.b_name }} ({{ item.b_locale }})</label>
<Input v-model="item.b_value" />
</div>
</div>
</div>
@@ -116,35 +116,22 @@ defineExpose({ open })
flex-direction: column;
gap: 12px;
&__key {
display: grid;
grid-template-columns: 88px 1fr;
align-items: center;
gap: 8px;
/* 上下结构:label 在上、输入框在下 */
&__field {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 13px;
color: var(--fms-text-secondary);
text-align: right;
}
}
&__row {
&__rows {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
&__locale {
flex: none;
width: 140px;
color: var(--fms-text-secondary);
font-size: 13px;
}
&__value {
flex: 1;
flex-direction: column;
gap: 10px;
}
}
</style>
@@ -10,8 +10,6 @@ import Select from '@/components/ui/select/select.vue'
import Switch from '@/components/ui/switch/switch.vue'
import Form from '@/components/ui/form/form.vue'
import FormItem from '@/components/ui/form/form-item.vue'
import Tabs from '@/components/ui/tabs/tabs.vue'
import TabPane from '@/components/ui/tabs/tab-pane.vue'
import { buildModuleI18nKey, validateModuleCode } from '@/utils/i18n'
import IconSelect from './IconSelect.vue'
import ModulePowersPanel from './ModulePowersPanel.vue'
@@ -34,6 +32,9 @@ const emit = defineEmits(['update', 'updatePowers', 'i18nSaved'])
const activeSection = ref('basic')
const formData = ref(null)
// 权限点数量(导航项徽标)
const powersCount = computed(() => props.powers?.length ?? 0)
watch(
() => props.menu,
(val) => {
@@ -55,6 +56,16 @@ const isPage = computed(() => formData.value?.b_menu_type === 'page')
const isData = computed(() => formData.value?.b_menu_type === 'data')
const isExternal = computed(() => formData.value?.b_menu_type === 'external')
// 类型变为目录时,若停留在「权限配置」分区(目录不展示该分区)则切回基本信息
watch(
() => formData.value?.b_menu_type,
(type) => {
if (type === 'directory' && activeSection.value === 'powers') {
activeSection.value = 'basic'
}
},
)
/* ---------- 校验规则:错误直接显示在对应 FormItem 下方 ---------- */
const formRules = computed(() => ({
b_code: [
@@ -156,123 +167,162 @@ const i18nItems = computed(() => {
<template>
<div class="mm-menu-detail" v-if="formData">
<Tabs v-model="activeSection" class="mm-menu-tabs">
<TabPane tab-key="basic">
<template #label><Info :size="14" /> 基本信息</template>
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" :columns="2" max-width="720px">
<FormItem label="菜单编码" name="b_code" required>
<Input :model-value="formData.b_code" :disabled="loading" @update:model-value="onCodeInput" />
</FormItem>
<FormItem label="菜单名称" name="b_name" required>
<Input
:model-value="formData.b_name"
:disabled="loading"
@update:model-value="(v) => setField('b_name', v)"
/>
</FormItem>
<FormItem label="菜单类型" name="b_menu_type" required>
<Select
:model-value="formData.b_menu_type"
:options="menuTypeOptions"
:disabled="loading"
@update:model-value="(v) => setField('b_menu_type', v)"
/>
</FormItem>
<FormItem label="上级菜单" name="b_parent_id">
<Input :model-value="parentName" readonly />
</FormItem>
<FormItem label="多语言键" name="b_i18n">
<div class="mm-basic__i18n">
<Input :model-value="formData.b_i18n" readonly />
<button
type="button"
class="mm-basic__i18n-btn"
title="快速设置多语言"
@click="openI18nModal"
>
<Languages :size="16" />
</button>
</div>
</FormItem>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="formData.b_xh"
type="number"
:disabled="loading"
@update:model-value="(v) => setField('b_xh', v)"
/>
</FormItem>
<FormItem label="菜单图标" name="b_icon">
<IconSelect
:model-value="formData.b_icon || ''"
:disabled="loading"
@update:model-value="(v) => setField('b_icon', v)"
/>
</FormItem>
<FormItem label="启用" name="b_canuse">
<Switch
:model-value="Number(formData.b_canuse) === 1"
:disabled="loading"
@update:model-value="(v) => setField('b_canuse', v ? 1 : 0)"
/>
</FormItem>
<div class="mm-config">
<nav class="mm-config-nav">
<div class="mm-config-nav__group">
<div class="mm-config-nav__group-title">概览</div>
<button
type="button"
class="mm-config-nav__item"
:class="{ 'is-active': activeSection === 'basic' }"
@click="activeSection = 'basic'"
>
<Info :size="14" class="mm-config-nav__icon" />
<span class="mm-config-nav__label">基本信息</span>
</button>
</div>
<div v-if="!isDirectory" class="mm-config-nav__group">
<div class="mm-config-nav__group-title">业务能力</div>
<button
type="button"
class="mm-config-nav__item"
:class="{ 'is-active': activeSection === 'powers' }"
@click="activeSection = 'powers'"
>
<Shield :size="14" class="mm-config-nav__icon" />
<span class="mm-config-nav__label">权限配置</span>
<span v-if="powersCount" class="mm-config-nav__badge">{{ powersCount }} 个权限</span>
</button>
</div>
<div class="mm-config-nav__group">
<div class="mm-config-nav__group-title">高级设置</div>
<button
type="button"
class="mm-config-nav__item"
:class="{ 'is-active': activeSection === 'i18n' }"
@click="activeSection = 'i18n'"
>
<Globe :size="14" class="mm-config-nav__icon" />
<span class="mm-config-nav__label">多语言</span>
</button>
</div>
</nav>
<FormItem v-if="isPage || isExternal" label="页面路由" name="b_route" required>
<Input
:model-value="formData.b_route"
:disabled="loading"
placeholder="固定页面路由或外链地址"
@update:model-value="(v) => setField('b_route', v)"
/>
</FormItem>
<div class="mm-config-content">
<div v-show="activeSection === 'basic'" class="mm-config-pane">
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" :columns="2" max-width="720px">
<FormItem label="菜单编码" name="b_code" required>
<Input :model-value="formData.b_code" :disabled="loading" @update:model-value="onCodeInput" />
</FormItem>
<FormItem label="菜单名称" name="b_name" required>
<Input
:model-value="formData.b_name"
:disabled="loading"
@update:model-value="(v) => setField('b_name', v)"
/>
</FormItem>
<FormItem label="菜单类型" name="b_menu_type" required>
<Select
:model-value="formData.b_menu_type"
:options="menuTypeOptions"
:disabled="loading"
@update:model-value="(v) => setField('b_menu_type', v)"
/>
</FormItem>
<FormItem label="上级菜单" name="b_parent_id">
<Input :model-value="parentName" readonly />
</FormItem>
<FormItem label="多语言键" name="b_i18n">
<div class="mm-basic__i18n">
<Input :model-value="formData.b_i18n" readonly />
<button
type="button"
class="mm-basic__i18n-btn"
title="快速设置多语言"
@click="openI18nModal"
>
<Languages :size="16" />
</button>
</div>
</FormItem>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="formData.b_xh"
type="number"
:disabled="loading"
@update:model-value="(v) => setField('b_xh', v)"
/>
</FormItem>
<FormItem label="菜单图标" name="b_icon">
<IconSelect
:model-value="formData.b_icon || ''"
:disabled="loading"
@update:model-value="(v) => setField('b_icon', v)"
/>
</FormItem>
<FormItem label="启用" name="b_canuse">
<Switch
:model-value="Number(formData.b_canuse) === 1"
:disabled="loading"
@update:model-value="(v) => setField('b_canuse', v ? 1 : 0)"
/>
</FormItem>
<FormItem v-if="isData" label="关联模块" name="b_module_id" required>
<Select
:model-value="String(formData.b_module_id || '')"
:options="moduleOptions"
:disabled="loading"
placeholder="选择数据/查询模块"
clearable
@update:model-value="(v) => setField('b_module_id', v)"
/>
</FormItem>
<FormItem v-if="isPage || isExternal" label="页面路由" name="b_route" required>
<Input
:model-value="formData.b_route"
:disabled="loading"
placeholder="固定页面路由或外链地址"
@update:model-value="(v) => setField('b_route', v)"
/>
</FormItem>
<FormItem v-if="isPage || isExternal || isData" label="访问权限编码" name="b_power_code">
<Input
:model-value="formData.b_power_code"
:disabled="loading"
placeholder="对应 s_power.b_code"
@update:model-value="(v) => setField('b_power_code', v)"
/>
</FormItem>
</Form>
</TabPane>
<FormItem v-if="isData" label="关联模块" name="b_module_id" required>
<Select
:model-value="String(formData.b_module_id || '')"
:options="moduleOptions"
:disabled="loading"
placeholder="选择数据/查询模块"
clearable
@update:model-value="(v) => setField('b_module_id', v)"
/>
</FormItem>
<TabPane tab-key="powers">
<template #label><Shield :size="14" /> 权限配置</template>
<ModulePowersPanel
:module-id="String(formData.b_id || '')"
:module-code="formData.b_code || ''"
:powers="powers"
:loading="loading"
power-type="menu"
:resource="formData.b_code || ''"
i18n-prefix="menu"
@update="(v) => emit('updatePowers', v)"
@i18n-saved="(key) => emit('i18nSaved', key)"
/>
</TabPane>
<FormItem v-if="isPage || isExternal || isData" label="访问权限编码" name="b_power_code">
<Input
:model-value="formData.b_power_code"
:disabled="loading"
placeholder="对应 s_power.b_code"
@update:model-value="(v) => setField('b_power_code', v)"
/>
</FormItem>
</Form>
</div>
<TabPane tab-key="i18n">
<template #label><Globe :size="14" /> 多语言</template>
<ModuleI18nPanel
:module-id="String(formData.b_id || '')"
:i18n-items="i18nItems"
:loading="loading"
ref="i18nPanelRef"
/>
</TabPane>
</Tabs>
<div v-if="!isDirectory" v-show="activeSection === 'powers'" class="mm-config-pane">
<ModulePowersPanel
:module-id="String(formData.b_id || '')"
:module-code="formData.b_code || ''"
:powers="powers"
:loading="loading"
power-type="menu"
:resource="formData.b_code || ''"
i18n-prefix="menu"
@update="(v) => emit('updatePowers', v)"
@i18n-saved="(key) => emit('i18nSaved', key)"
/>
</div>
<div v-show="activeSection === 'i18n'" class="mm-config-pane">
<ModuleI18nPanel
:module-id="String(formData.b_id || '')"
:i18n-items="i18nItems"
:loading="loading"
ref="i18nPanelRef"
/>
</div>
</div>
</div>
<I18nQuickSetModal ref="i18nModalRef" @saved="onI18nSaved" />
</div>
@@ -286,15 +336,94 @@ const i18nItems = computed(() => {
flex: 1;
}
.mm-menu-tabs {
/* ---- 分组配置导航 + 内容(与模块配置 mm-config 系列一致) ---- */
.mm-config {
display: flex;
flex: 1;
min-height: 0;
}
:deep(.tabs-content) {
flex: 1;
min-height: 0;
overflow: auto;
.mm-config-nav {
flex: none;
width: 168px;
padding: 10px 8px;
overflow-y: auto;
}
.mm-config-nav__group {
margin-bottom: 12px;
}
.mm-config-nav__group-title {
padding: 2px 8px 4px;
font-size: 11px;
font-weight: 600;
color: var(--fms-text-secondary);
letter-spacing: 0.4px;
}
.mm-config-nav__item {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 5px 8px;
border: none;
border-radius: var(--fms-control-radius);
background: transparent;
color: var(--fms-text);
font-size: 13px;
text-align: left;
cursor: pointer;
&:hover {
background: var(--fms-secondary);
}
&.is-active {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
color: var(--fms-primary);
font-weight: 500;
}
}
.mm-config-nav__icon {
flex: none;
color: var(--fms-text-secondary);
}
.mm-config-nav__item.is-active .mm-config-nav__icon {
color: var(--fms-primary);
}
.mm-config-nav__label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-config-nav__badge {
flex: none;
padding: 0 5px;
border-radius: 999px;
background: var(--fms-secondary);
color: var(--fms-text-secondary);
font-size: 11px;
line-height: 16px;
}
.mm-config-content {
flex: 1;
min-width: 0;
min-height: 0;
overflow: auto;
padding: 12px 16px 16px;
}
.mm-config-pane {
min-width: 0;
}
.mm-basic__i18n {
@@ -8,6 +8,7 @@ import { computed, ref, watch } from 'vue'
import { Languages } from '@lucide/vue'
import Input from '@/components/ui/input/input.vue'
import Select from '@/components/ui/select/select.vue'
import Switch from '@/components/ui/switch/switch.vue'
import Form from '@/components/ui/form/form.vue'
import FormItem from '@/components/ui/form/form-item.vue'
import { buildModuleI18nKey, validateModuleCode } from '@/utils/i18n'
@@ -118,7 +119,8 @@ function onI18nSaved(key) {
<template>
<div v-if="formData" class="mm-basic">
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" :columns="2" max-width="720px">
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" :columns="3" max-width="720px">
<div class="mm-basic__section-title">标识与归属</div>
<FormItem :label="isCategory ? '分类编码' : '模块编码'" name="b_code" required>
<Input :model-value="formData.b_code" :disabled="loading" @update:model-value="onCodeInput" />
</FormItem>
@@ -140,7 +142,7 @@ function onI18nSaved(key) {
<FormItem label="上级模块" name="b_parent_id">
<Input :model-value="parentName" readonly />
</FormItem>
<FormItem label="多语言键" name="b_i18n">
<FormItem label="多语言键" name="b_i18n" :span="2">
<div class="mm-basic__i18n">
<Input :model-value="formData.b_i18n" readonly />
<button
@@ -153,16 +155,9 @@ function onI18nSaved(key) {
</button>
</div>
</FormItem>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="formData.b_xh"
type="number"
:disabled="loading"
@update:model-value="(v) => setField('b_xh', v)"
/>
</FormItem>
<template v-if="isData || isQuery">
<div class="mm-basic__section-title">数据来源</div>
<FormItem label="查询视图" name="b_viewtable" required>
<Input
:model-value="formData.b_viewtable"
@@ -206,7 +201,25 @@ function onI18nSaved(key) {
</FormItem>
</template>
<FormItem label="描述" name="b_bz" :span="2">
<div class="mm-basic__section-title">行为设置</div>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="formData.b_xh"
type="number"
:disabled="loading"
@update:model-value="(v) => setField('b_xh', v)"
/>
</FormItem>
<FormItem label="启用" name="b_canuse">
<Switch
:model-value="Number(formData.b_canuse) === 1"
:disabled="loading"
@update:model-value="(v) => setField('b_canuse', v ? 1 : 0)"
/>
</FormItem>
<div class="mm-basic__section-title">描述</div>
<FormItem label="描述" name="b_bz" :span="3">
<Input
:model-value="formData.b_bz"
type="textarea"
@@ -227,6 +240,20 @@ function onI18nSaved(key) {
flex-direction: column;
}
/* 表单内部分组区块标题:占满整行,纯文字小标题(无分隔线,更轻) */
.mm-basic__section-title {
grid-column: 1 / -1;
padding: 10px 0 6px;
margin-bottom: 2px;
font-size: 13px;
font-weight: 600;
color: var(--fms-text-secondary);
&:first-child {
padding-top: 0;
}
}
.mm-basic__i18n {
display: flex;
align-items: center;
File diff suppressed because it is too large. Load diff