20260902223822

This commit is contained in:
oneao committed 2026-09-02 22:38:23 +08:00
1 parent db7813a2d2
commit c8940f6e03
11 files changed
+327 -922

No files matched your search

@@ -10,6 +10,8 @@ defineOptions({ inheritAttrs: false })
const props = defineProps({
virtual: { type: Boolean, default: true },
virtualX: { type: Boolean, default: true },
// 是否在非横向虚拟滚动时自动铺满容器;默认按列配置宽度展示。
autoFitWidth: { type: Boolean, default: false },
// 列宽拖拽默认开启;false=关闭;{ disabled: (col) => boolean }=按列禁用
// (铺满容器的弹性列应禁用拖拽,见库文档「列宽铺满容器hack方式」)
colResizable: { type: [Boolean, Object], default: true },
@@ -73,7 +75,7 @@ const tableTheme = computed(() => props.theme ?? (appStore.darkMode ? 'dark' : '
// 需从 vnode.props 探测;绑定关系在模板编写时确定,运行期不会变化,无需响应式。
const columnsBound =
'onUpdate:columns' in (getCurrentInstance()?.vnode.props ?? {})
const effectiveColResizable = columnsBound ? props.colResizable : false
const effectiveColResizable = columnsBound && !props.autoFitWidth ? props.colResizable : false
// Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
// rAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
@@ -276,6 +278,14 @@ const seqConfig = computed(() => {
width: Number(cfg.width ?? 50),
}
})
const seqWidth = ref(seqConfig.value.width)
watch(
() => seqConfig.value.width,
(width) => {
seqWidth.value = width
},
{ immediate: true },
)
const columnDragConfig = computed(() => {
if (!columnsBound || !props.columnDraggable) return false
const configured = props.columnDraggable === true ? {} : props.columnDraggable
@@ -298,16 +308,18 @@ const columnDragConfig = computed(() => {
}
})
const hasRowSelection = computed(() => Boolean(props.rowSelection))
const selectionWidth = ref(44)
const dragRowColumn = {
type: 'dragRow',
dataIndex: '__drag_row__',
title: '',
width: 40,
resizable: false,
}
const dragRowWidth = ref(dragRowColumn.width)
function getHeaderAlign(column) {
if (column.headerAlign) return column.headerAlign
if (column.type === 'seq') return 'center'
if (isSelectionColumn(column)) return 'center'
if (column.dataIndex === 'actions') return 'center'
if (column.actions && column.actions.length) return 'center'
@@ -322,14 +334,13 @@ const displayColumns = computed(() => {
if (seq.enabled) {
cols.unshift({
type: 'seq',
width: seq.width,
width: seqWidth.value,
dataIndex: '',
title: seq.title,
resizable: false,
})
}
if (props.rowDraggable) cols.unshift(dragRowColumn)
if (hasRowSelection.value) cols.unshift(selectionColumn)
if (props.rowDraggable) cols.unshift({ ...dragRowColumn, width: dragRowWidth.value })
if (hasRowSelection.value) cols.unshift({ ...selectionColumn, width: selectionWidth.value })
return cols.map((column) => ({
...column,
headerAlign: getHeaderAlign(column),
@@ -338,6 +349,15 @@ const displayColumns = computed(() => {
// 剥离内部列(选择列、序号列、拖拽列)后回传,避免污染父组件 columns
function onColumnsUpdate(columns) {
for (const column of columns || []) {
if (isSelectionColumn(column) && column.width != null) {
selectionWidth.value = Number.parseInt(column.width, 10) || selectionWidth.value
} else if (column.type === 'seq' && column.width != null) {
seqWidth.value = Number.parseInt(column.width, 10) || seqWidth.value
} else if (column.type === 'dragRow' && column.width != null) {
dragRowWidth.value = Number.parseInt(column.width, 10) || dragRowWidth.value
}
}
const originalByKey = new Map(
props.columns.map((col) => [String(col.key ?? col.dataIndex ?? ''), col]),
)
@@ -592,7 +612,7 @@ defineExpose({
<div
ref="rootRef"
class="fms-table"
:class="[bordered ? 'fms-table--bordered' : '']"
:class="[bordered ? 'fms-table--bordered' : '', autoFitWidth ? 'fms-table--auto-fit-width' : '']"
@keydown.capture="handleKeydownCapture"
>
<StkTable
@@ -660,6 +680,11 @@ defineExpose({
will-change: transform;
}
}
/* 不自适应时按列配置宽度展示;自适应模式不启用列宽拖拽,使用库默认布局。 */
.fms-table:not(.fms-table--auto-fit-width) :deep(.stk-table-main) {
flex: none;
}
</style>
<!-- 非 scoped 全局样式:选择列原生 checkbox 外观模拟 antdv-next Checkbox,
@@ -684,15 +709,6 @@ defineExpose({
}
}
/* 列宽铺满容器(库文档「列宽铺满容器hack方式」):
开启 col-resizable 后库默认把 .stk-table-main 设为 flex:none + fit-content,
表格收缩到内容宽。非 virtual-x(列数 ≤ 阈值、未设宽列走原生自适应)时覆盖回
flex:1 使表格铺满容器;配合「固定列设 width、弹性列设 minWidth 且禁用其拖拽」
的列配置约定使用。宽表(virtual-x,横向滚动)保持库的收缩行为。 */
.fms-table__inner.stk-table:not(.virtual-x) .stk-table-main {
flex: 1;
}
.fms-checkbox {
display: flex;
align-items: center;
@@ -2,7 +2,7 @@
/**
* 菜单管理:管理 s_menu 导航树。
* 布局:左侧菜单树 + 右侧属性编辑面板。
* 菜单类型:directory(目录)、page(固定页面)、data(数据页面)、external(外链)。
* 菜单类型:directory(目录)、page(页面)、external(外链)。
*/
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import { useHotkeys } from '@/composables/useHotkeys'
@@ -15,7 +15,6 @@ import {
Folder,
FileText,
ExternalLink,
Database,
RefreshCw,
Languages,
} from '@lucide/vue'
@@ -24,7 +23,6 @@ import {
Checkbox,
Input,
Select,
Switch,
Splitter,
SplitterPanel,
Tree,
@@ -63,22 +61,19 @@ const nonDefaultLanguages = I18N_LANGUAGES.filter((l) => !l.default)
/* ================= 类型映射 ================= */
const menuTypeOptions = [
{ label: '目录', value: 'directory' },
{ label: '固定页面', value: 'page' },
{ label: '数据页面', value: 'data' },
{ label: '页面', value: 'page' },
{ label: '外链', value: 'external' },
]
const menuTypeLabel = {
directory: '目录',
page: '页面',
data: '数据',
external: '外链',
}
const menuTypeIconMap = {
directory: Folder,
page: FileText,
data: Database,
external: ExternalLink,
}
@@ -119,8 +114,10 @@ const parentMenuName = computed(() => {
const menuType = computed(() => selectedMenu.value?.b_menu_type || null)
const isDirectory = computed(() => menuType.value === 'directory')
const isPage = computed(() => menuType.value === 'page')
const isData = computed(() => menuType.value === 'data')
const isExternal = computed(() => menuType.value === 'external')
const menuTypeLocked = computed(
() => !!selectedMenu.value && !unsavedMenuIds.has(String(selectedMenu.value.b_id)),
)
/* ================= 树加载 ================= */
watch(menuKeyword, (kw) => {
@@ -175,7 +172,6 @@ function buildMenuContextMenu(node) {
const items = [
{ key: 'add-directory', label: '新增目录', icon: Folder, disabled: isUnsaved },
{ key: 'add-page', label: '新增页面', icon: FileText, disabled: isUnsaved },
{ key: 'add-data', label: '新增数据页面', icon: Database, disabled: isUnsaved },
{ key: 'add-external', label: '新增外链', icon: ExternalLink, disabled: isUnsaved },
]
if (node) {
@@ -189,7 +185,6 @@ function onMenuContextMenuClick({ item, node }) {
const parentId = node ? String(node.b_id) : null
if (item.key === 'add-directory') void addMenu(parentId, 'directory')
else if (item.key === 'add-page') void addMenu(parentId, 'page')
else if (item.key === 'add-data') void addMenu(parentId, 'data')
else if (item.key === 'add-external') void addMenu(parentId, 'external')
else if (item.key === 'delete') void deleteMenu(parentId)
}
@@ -198,7 +193,6 @@ function onMenuContextMenuClick({ item, node }) {
const toolbarAddOptions = [
{ key: 'directory', label: '新增目录', icon: Folder },
{ key: 'page', label: '新增页面', icon: FileText },
{ key: 'data', label: '新增数据页面', icon: Database },
{ key: 'external', label: '新增外链', icon: ExternalLink },
]
@@ -316,7 +310,6 @@ function menuRowOf(node) {
b_menu_type: node.b_menu_type,
b_route: node.b_route ?? null,
b_module_id: node.b_module_id ?? null,
b_power_code: node.b_power_code ?? null,
b_icon: node.b_icon ?? null,
b_xh: node.b_xh,
b_canuse: node.b_canuse,
@@ -335,7 +328,6 @@ async function addMenu(parentId, menuTypeValue = 'page') {
b_menu_type: menuTypeValue,
b_route: null,
b_module_id: null,
b_power_code: null,
b_icon: null,
b_xh: 10,
b_canuse: 1,
@@ -385,7 +377,7 @@ async function deleteMenu(id) {
okType: 'danger',
onOk: async () => {
try {
const menuPowerCode = menu.b_power_code
const menuPowerCode = menu.b_code ? `menu.${String(menu.b_code).trim()}` : ''
const reqs = [{ table: 's_i18n', key_field: 'b_id', deletes: [{ b_module_id: menuId }] }]
// 清理权限点:访问(type=menu 按 b_resource 关联)+ 操作/按钮(type=action 按 b_module_id 关联)
if (menuPowerCode) {
@@ -433,7 +425,7 @@ async function loadMenuConfig(menuId) {
}
i18nValues.value = i18nValuesInit
originalI18nValues.value = { ...i18nValuesInit }
// 操作/按钮权限点:仅 page/data/external 才有(目录是导航容器,无按钮权限)
// 操作/按钮权限点:仅页面/外链才有(目录是导航容器,无按钮权限)
powersLoading.value = true
try {
const powerRes =
@@ -587,22 +579,56 @@ async function saveMenu() {
const menuData = drafts.value?.menu
if (!menuData) return false
const isNew = unsavedMenuIds.has(menuId)
const allowedMenuTypes = new Set(['directory', 'page', 'external'])
if (!allowedMenuTypes.has(menuData.b_menu_type)) {
Message.warning('菜单类型只能是目录、页面或外链')
return false
}
if (!isNew && menuData.b_menu_type !== originals.value?.menu?.b_menu_type) {
Message.warning('已保存菜单不允许修改菜单类型')
return false
}
if (menuData.b_parent_id && String(menuData.b_parent_id) === menuId) {
Message.warning('上级菜单不能选择当前菜单')
return false
}
const menuTypeRules = {
directory: { route: false, module: false },
page: { route: true, module: true },
external: { route: true, module: false },
}
const typeRule = menuTypeRules[menuData.b_menu_type]
if (typeRule.route && !String(menuData.b_route || '').trim()) {
Message.warning(
menuData.b_menu_type === 'external'
? '外链菜单必须填写外链地址'
: '页面菜单必须填写路由地址',
)
return false
}
if (typeRule.module && !menuData.b_module_id) {
Message.warning('页面菜单必须绑定模块')
return false
}
if (!typeRule.route && menuData.b_route) menuData.b_route = null
if (!typeRule.module && menuData.b_module_id) menuData.b_module_id = null
// 简体中文名称 = 表单 b_name(默认语言,直接存菜单表)
// 菜单编码由用户填写,但保存时统一去除首尾空格,作为稳定标识使用。
const menuCode = String(menuData.b_code || '').trim()
menuData.b_code = menuCode
// 受管 i18n 键:菜单统一 menu. 前缀
const menuI18nKey = `menu.${menuData.b_code}`
const menuI18nKey = `menu.${menuCode}`
menuData.b_i18n = menuI18nKey
// 目录是纯导航容器:可见性由子菜单决定,自身不需要访问权限编码,也不生成权限点。
// 仅 page/data/external 需要访问权限编码;留空时按菜单编码自动生成(权限跟着菜单走)
// 目录是纯导航容器:可见性由子菜单决定,自身不生成菜单访问权限点。
// 页面类型菜单的访问权限统一由菜单编码派生。
const menuIsDirectory = menuData.b_menu_type === 'directory'
if (menuIsDirectory) {
menuData.b_power_code = null
} else if (!menuData.b_power_code) {
menuData.b_power_code = `menu.${menuData.b_code}`
}
const powerCode = menuCode ? `menu.${menuCode}` : ''
const isNew = unsavedMenuIds.has(menuId)
const req = { table: 's_menu', key_field: 'b_id' }
if (isNew) {
req.inserts = [menuData]
@@ -632,12 +658,13 @@ async function saveMenu() {
reqs.push(i18nReq)
}
// 目录是纯导航容器:自身不需要访问权限编码与权限点。
// 目录是纯导航容器:自身不需要菜单访问权限与按钮权限点。
// 若该菜单此前(或改为目录前)生成过权限记录,保存时一并清理,避免孤儿数据:
// - 访问权限(s_power.b_power_type='menu',按原权限编码关联)
// - 访问权限(s_power.b_power_type='menu',按菜单编码派生的权限字符关联)
// - 操作/按钮权限(s_power.b_power_type='action',按 b_module_id=菜单 id 关联)
if (menuIsDirectory) {
const prevPowerCode = originals.value?.menu?.b_power_code
const previousCode = String(originals.value?.menu?.b_code || '').trim()
const prevPowerCode = previousCode ? `menu.${previousCode}` : ''
if (prevPowerCode) {
reqs.push({
table: 's_power',
@@ -652,9 +679,17 @@ async function saveMenu() {
})
}
// 访问权限点(s_power.b_power_type='menu',自动同步)
const powerCode = (menuData.b_power_code || '').trim()
// 访问权限点(s_power.b_power_type='menu',由菜单编码自动同步)
if (powerCode) {
const previousCode = String(originals.value?.menu?.b_code || '').trim()
const previousPowerCode = previousCode ? `menu.${previousCode}` : ''
if (previousPowerCode && previousPowerCode !== powerCode) {
reqs.push({
table: 's_power',
key_field: 'b_id',
deletes: [{ b_power_type: 'menu', b_resource: previousPowerCode }],
})
}
// 查是否有已存在的访问权限(按 b_resource = 菜单权限编码)
const accessPowerRes = menuData.b_id
? await loadDataApi('s_power', `b_power_type = 'menu' AND b_resource = '${powerCode}'`)
@@ -814,16 +849,12 @@ function onI18nModalChange(values) {
function onMenuTypeChange(v) {
if (!drafts.value?.menu) return
if (menuTypeLocked.value) return
drafts.value.menu.b_menu_type = v
// Clear type-specific fields when switching
// 目录和外链不绑定业务模块;页面同时使用路由和模块。
if (v === 'directory') {
drafts.value.menu.b_route = null
drafts.value.menu.b_module_id = null
drafts.value.menu.b_power_code = null
} else if (v === 'page') {
drafts.value.menu.b_module_id = null
} else if (v === 'data') {
drafts.value.menu.b_route = null
} else if (v === 'external') {
drafts.value.menu.b_module_id = null
}
@@ -863,9 +894,7 @@ const powers = ref([])
const powersBaseline = ref('')
const powersLoading = ref(false)
/** 权限点表格列配置
* 列宽铺满约定(库文档「列宽铺满容器hack方式」):固定列设 width,
* 弹性列(权限名称)设 minWidth 占满剩余宽度,并在 colResizable 中禁用其拖拽。 */
/** 权限点表格列配置 */
const powerColumns = shallowRef([
{
dataIndex: 'b_code',
@@ -876,7 +905,7 @@ const powerColumns = shallowRef([
{
dataIndex: 'b_name',
title: '权限名称',
minWidth: 150,
width: 180,
editor: { type: 'input' },
},
{
@@ -911,24 +940,6 @@ const rowSelection = computed(() => ({
},
}))
/** 访问权限展示(派生自菜单,跟随 b_power_code / b_name / b_canuse,不可编辑) */
const accessPowerDesc = computed(() => {
const menu = drafts.value?.menu
if (!menu) return ''
return menu.b_power_code || `menu.${menu.b_code || ''}`
})
/** 根据菜单编码自动生成访问权限编码 */
function autoPowerCode() {
if (!drafts.value?.menu) return
const code = (drafts.value.menu.b_code || '').trim()
if (!code) {
Message.warning('请先填写菜单编码')
return
}
setField('b_power_code', `menu.${code}`)
}
/** 新增操作/按钮权限点 */
async function addPowerRow() {
if (powersLoading.value) return
@@ -972,9 +983,9 @@ const selectedLibCodes = ref([])
const libTableRef = ref(null)
const powerLibraryColumns = shallowRef([
// 列宽铺满约定同 powerColumns:编码固定宽,名称弹性占满
{ dataIndex: 'b_code', title: '权限编码', width: 100 },
{ dataIndex: 'b_name', title: '权限名称', minWidth: 80 },
// 权限库按容器自然分配列宽,不提供固定宽度,也不启用列宽拖拽
{ dataIndex: 'b_code', title: '权限编码' },
{ dataIndex: 'b_name', title: '权限名称' },
])
const libRowSelection = computed(() => ({
@@ -1037,7 +1048,7 @@ function importSelectedPowers() {
Message.success(`已导入 ${added.length} 条权限`)
}
/* ================= 加载模块列表(供 data 类型下拉选择) ================= */
/* ================= 加载模块列表(供页面类型下拉选择) ================= */
const moduleOptions = ref([])
let moduleOptionsLoaded = false
@@ -1198,112 +1209,96 @@ loadPowerLibrary()
<div class="mm-pane__title">菜单配置</div>
</div>
<Form
ref="formRef"
:model="drafts.menu"
:rules="{
b_code: [{ required: true, message: '请填写菜单编码' }],
b_name: [{ required: true, message: '请填写菜单名称' }],
b_menu_type: [{ required: true, message: '请选择菜单类型' }],
b_route:
isPage || isExternal
? [{ required: true, message: '请填写路由地址' }]
ref="formRef"
:model="drafts.menu"
:rules="{
b_code: [{ required: true, message: '请填写菜单编码' }],
b_name: [{ required: true, message: '请填写菜单名称' }],
b_menu_type: [{ required: true, message: '请选择菜单类型' }],
b_route:
isPage || isExternal
? [{ required: true, message: '请填写路由地址' }]
: [],
b_module_id: isPage
? [{ required: true, message: '请选择绑定模块' }]
: [],
b_module_id: isData ? [{ required: true, message: '请选择绑定模块' }] : [],
}"
layout="vertical"
:gap="20"
>
<FormItem label="菜单名称" name="b_name" required>
<div class="mm-name-field">
}"
layout="vertical"
:gap="20"
>
<FormItem label="菜单名称" name="b_name" required>
<div class="mm-name-field">
<Input
class="mm-name-field__input"
:model-value="drafts.menu.b_name"
@update:model-value="(v) => setField('b_name', v)"
/>
<Button
type="ghost"
class="mm-name-field__i18n"
title="多语言名称"
@click="i18nModalVisible = true"
>
<Languages :size="16" />
</Button>
</div>
</FormItem>
<FormItem label="菜单编码" name="b_code" required>
<Input
class="mm-name-field__input"
:model-value="drafts.menu.b_name"
@update:model-value="(v) => setField('b_name', v)"
:model-value="drafts.menu.b_code"
@update:model-value="(v) => setField('b_code', v)"
/>
<Button
type="ghost"
class="mm-name-field__i18n"
title="多语言名称"
@click="i18nModalVisible = true"
>
<Languages :size="16" />
</Button>
</div>
</FormItem>
<FormItem label="菜单编码" name="b_code" required>
<Input
:model-value="drafts.menu.b_code"
@update:model-value="(v) => setField('b_code', v)"
/>
</FormItem>
<FormItem label="菜单类型" name="b_menu_type" required>
<Select
:model-value="drafts.menu.b_menu_type"
:options="menuTypeOptions"
@update:model-value="onMenuTypeChange"
/>
</FormItem>
<FormItem label="上级菜单" name="b_parent_id">
<Input :model-value="parentMenuName || '根节点(顶级菜单)'" readonly />
</FormItem>
<FormItem label="图标" name="b_icon">
<IconPicker
:model-value="drafts.menu.b_icon"
@update:model-value="(v) => setField('b_icon', v)"
/>
</FormItem>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="drafts.menu.b_xh"
type="number"
@update:model-value="(v) => setField('b_xh', Number(v) || 0)"
/>
</FormItem>
<!-- page: 路由地址 -->
<FormItem v-if="isPage" label="路由地址" name="b_route" required>
<Input
:model-value="drafts.menu.b_route"
@update:model-value="(v) => setField('b_route', v)"
/>
</FormItem>
<!-- data: 绑定模块 -->
<FormItem v-if="isData" label="绑定模块" name="b_module_id" required>
<Select
:model-value="drafts.menu.b_module_id"
:options="moduleOptions"
@update:model-value="(v) => setField('b_module_id', v)"
/>
</FormItem>
<!-- external: 外链地址 -->
<FormItem v-if="isExternal" label="外链地址" name="b_route" required>
<Input
:model-value="drafts.menu.b_route"
@update:model-value="(v) => setField('b_route', v)"
/>
</FormItem>
<FormItem label="启用" name="b_canuse">
<Switch
:model-value="Number(drafts.menu.b_canuse) === 1"
@update:model-value="(v) => setField('b_canuse', v ? 1 : 0)"
/>
</FormItem>
<FormItem v-if="!isDirectory" label="访问权限编码" name="b_power_code">
<div class="mm-power-code-field">
</FormItem>
<FormItem label="菜单类型" name="b_menu_type" required>
<Select
:model-value="drafts.menu.b_menu_type"
:options="menuTypeOptions"
:disabled="menuTypeLocked || busy"
@update:model-value="onMenuTypeChange"
/>
</FormItem>
<FormItem label="上级菜单" name="b_parent_id">
<Input :model-value="parentMenuName || '根节点(顶级菜单)'" readonly />
</FormItem>
<!-- page/external: 路由地址 -->
<FormItem v-if="isPage" label="路由地址" name="b_route" required>
<Input
class="mm-power-code-field__input"
:model-value="drafts.menu.b_power_code"
@update:model-value="(v) => setField('b_power_code', v)"
:model-value="drafts.menu.b_route"
@update:model-value="(v) => setField('b_route', v)"
/>
<Button type="outline" size="small" @click="autoPowerCode">
<RefreshCw :size="12" /> 自动生成
</Button>
</div>
</FormItem>
</Form>
</FormItem>
<FormItem v-if="isExternal" label="外链地址" name="b_route" required>
<Input
:model-value="drafts.menu.b_route"
@update:model-value="(v) => setField('b_route', v)"
/>
</FormItem>
<!-- page: 绑定模块 -->
<FormItem v-if="isPage" label="绑定模块" name="b_module_id" required>
<Select
:model-value="drafts.menu.b_module_id"
:options="moduleOptions"
@update:model-value="(v) => setField('b_module_id', v)"
/>
</FormItem>
<FormItem label="图标" name="b_icon">
<IconPicker
:model-value="drafts.menu.b_icon"
@update:model-value="(v) => setField('b_icon', v)"
/>
</FormItem>
<FormItem label="排序号" name="b_xh">
<Input
:model-value="drafts.menu.b_xh"
type="number"
@update:model-value="(v) => setField('b_xh', Number(v) || 0)"
/>
</FormItem>
</Form>
</div>
</SplitterPanel>
<!-- 权限面板:仅 page/data/external(目录无权限) -->
<!-- 权限面板:仅页面/外链(目录无权限) -->
<SplitterPanel v-if="!isDirectory">
<div class="mm-power-card">
<div class="mm-power-card__head">
@@ -1338,7 +1333,7 @@ loadPowerLibrary()
:editable="true"
row-key="b_id"
:row-selection="rowSelection"
:col-resizable="{ disabled: (col) => col.dataIndex === 'b_name' }"
:col-resizable="true"
@change="() => {}"
/>
</div>
@@ -1380,7 +1375,7 @@ loadPowerLibrary()
:seq="false"
row-key="b_code"
:row-selection="libRowSelection"
:col-resizable="{ disabled: (col) => col.dataIndex === 'b_name' }"
:auto-fit-width="true"
@change="() => {}"
/>
</div>
@@ -1624,18 +1619,6 @@ loadPowerLibrary()
padding: 0 8px;
}
/* 访问权限编码行 */
.mm-power-code-field {
display: flex;
align-items: center;
gap: 8px;
}
.mm-power-code-field__input {
flex: 1;
min-width: 0;
}
/* ---- 右侧主区 ---- */
.mm-main {
display: flex;