20260826173130
This commit is contained in:
1 parent
9957757b5d
commit
0bbbbd3ecc
15 files changed
+2472
-874
No files matched your search
@@ -121,6 +121,8 @@
|
||||
|
||||
.tabs-top .tabs-nav-extra:last-child {
|
||||
margin-left: 8px;
|
||||
/* 右侧操作区与导航栏右边缘留白(组件默认,使用方无需再覆盖) */
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.tabs-left .tabs-nav-extra:first-child,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, defineComponent, h, ref, watch } from 'vue'
|
||||
import { computed, defineComponent, h, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import Splitter from '@/components/ui/splitter/splitter.vue'
|
||||
@@ -57,7 +57,6 @@ watch(
|
||||
)
|
||||
|
||||
/* ---------- 侧栏宽度(展开态可拖拽调宽,由 Splitter 接管) ---------- */
|
||||
const SIDEBAR_MIN = 180
|
||||
const SIDEBAR_MAX = 400
|
||||
const SIDEBAR_DEFAULT = 220
|
||||
// 折叠态:48px(对齐 shadcn sidebar 默认 3rem,仅容图标)
|
||||
@@ -76,6 +75,68 @@ function onSplitterResize(sizes) {
|
||||
const width = Math.round(Number(sizes[0]))
|
||||
if (Number.isFinite(width)) sidebarWidth.value = width
|
||||
}
|
||||
|
||||
/* 展开/折叠逻辑:
|
||||
* - 折叠:侧栏固定 48px(不可拖拽)
|
||||
* - 展开:恢复为拖拽宽度;但若上次被拖得过窄(< 默认 220),自动恢复默认宽度,
|
||||
* 避免「拖窄 → 折叠 → 再展开」后侧栏变成一条细缝。
|
||||
* 用 watch 监听折叠态,无论从顶栏按钮还是菜单点击触发展开都生效 */
|
||||
watch(
|
||||
() => appStore.sidebarCollapsed,
|
||||
(collapsed) => {
|
||||
if (!collapsed && sidebarWidth.value < SIDEBAR_DEFAULT) {
|
||||
sidebarWidth.value = SIDEBAR_DEFAULT
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/* 顶栏折叠/展开按钮的点击逻辑(经 provide 提供给 AppTopbar):
|
||||
* - 折叠态:展开(宽度由上方 watch 自动恢复)
|
||||
* - 展开态且宽度已被拖到很窄(< 默认 220):不折叠,直接展开到默认宽度
|
||||
* ——用户把侧栏拖没了以后点按钮,期望的是"恢复宽度"而不是"再折叠"
|
||||
* - 其余情况:正常折叠(48px) */
|
||||
function toggleSidebar() {
|
||||
if (appStore.sidebarCollapsed) {
|
||||
appStore.setSidebar(false)
|
||||
return
|
||||
}
|
||||
if (sidebarWidth.value < SIDEBAR_DEFAULT) {
|
||||
sidebarWidth.value = SIDEBAR_DEFAULT
|
||||
return
|
||||
}
|
||||
appStore.setSidebar(true)
|
||||
}
|
||||
provide('toggleSidebar', toggleSidebar)
|
||||
|
||||
/* ---------- 内容可视高度(页面撑满用) ----------
|
||||
* 实测 .fms-content 可视高度(clientHeight - 纵向 padding),写入 CSS 变量
|
||||
* --fms-content-height(随 DOM 继承给页面所有后代)。
|
||||
* 需要「固定高度 + 内部滚动」的页面(如工具栏固定、表格内部滚动)直接写:
|
||||
* height: var(--fms-content-height)
|
||||
* 替代 calc(100vh - 116px) 这类魔数——顶栏/页签/padding 等布局内部变化时自动跟随,
|
||||
* 页面无需改动。 */
|
||||
const contentRef = ref(null)
|
||||
let contentResizeObserver = null
|
||||
|
||||
function syncContentHeight() {
|
||||
const el = contentRef.value
|
||||
if (!el) return
|
||||
const style = getComputedStyle(el)
|
||||
const height =
|
||||
el.clientHeight - parseFloat(style.paddingTop || '0') - parseFloat(style.paddingBottom || '0')
|
||||
el.style.setProperty('--fms-content-height', `${height}px`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// ResizeObserver 首次 observe 会立即回调一次,初始值无需额外同步
|
||||
contentResizeObserver = new ResizeObserver(syncContentHeight)
|
||||
if (contentRef.value) contentResizeObserver.observe(contentRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
contentResizeObserver?.disconnect()
|
||||
contentResizeObserver = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -89,10 +150,11 @@ function onSplitterResize(sizes) {
|
||||
@resize-start="dragging = true"
|
||||
@resize-end="dragging = false"
|
||||
>
|
||||
<!-- 侧栏面板:折叠态固定 48px 且不可调整 -->
|
||||
<!-- 侧栏面板:紧贴左/上/下边缘(无 padding),侧栏卡片右侧阴影与主区分隔。
|
||||
拖拽无最小宽度限制(可拖到任意窄),仅限制最大宽度 -->
|
||||
<SplitterPanel
|
||||
class="fms-sidebar-panel"
|
||||
:size="sidebarSize"
|
||||
:min="appStore.sidebarCollapsed ? undefined : SIDEBAR_MIN"
|
||||
:max="appStore.sidebarCollapsed ? undefined : SIDEBAR_MAX"
|
||||
:resizable="!appStore.sidebarCollapsed"
|
||||
>
|
||||
@@ -105,7 +167,7 @@ function onSplitterResize(sizes) {
|
||||
<AppTopbar />
|
||||
|
||||
<div class="fms-main">
|
||||
<div class="fms-content">
|
||||
<div ref="contentRef" class="fms-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<div :key="route.fullPath" class="fms-page-wrap">
|
||||
@@ -167,19 +229,28 @@ function onSplitterResize(sizes) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 非折叠态:分隔条命中区改为悬浮(width: 0 + 负 margin),不占 flex 宽度。
|
||||
* 否则侧栏内容右 padding(8px) + 分隔条占位(6px) 叠加成 14px 空隙,
|
||||
* 而右侧仅 8px,两侧不对称(折叠态因 display:none 掩盖了此问题)。
|
||||
* 注意同样只能用 > 限定为布局自身的直接子级分隔条:
|
||||
* 用后代选择器时 scoped :deep 会把主面板内嵌套 Splitter 的分隔条
|
||||
* 一并压成 0 宽,导致内层(如 FmsFile)分隔条不可见、无法拖拽 */
|
||||
/* 非折叠态:分隔条悬浮命中区(width: 0 + margin: 0,不参与 flex 布局)。
|
||||
* 注意:原实现用负 margin(0 -3px)做悬浮命中区,但 flex 中负 margin 会把相邻面板
|
||||
* 拉近 6px,抵消主区面板 padding-left 8px → 侧栏白卡与右侧内容(顶栏/灰画布)只剩
|
||||
* 2px 灰缝,视觉上"紧贴"。命中区改由绝对定位的 ::after 扩展,布局不再受影响。 */
|
||||
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar) {
|
||||
width: 0;
|
||||
margin: 0 calc(var(--fms-splitter-trigger-size) / -2);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 命中区:绝对定位在分隔条两侧各扩展 3px(共 6px),不占 flex 空间,
|
||||
* 且能正常触发 bar 的 pointerdown 拖拽 */
|
||||
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(var(--fms-splitter-trigger-size) / -2);
|
||||
width: var(--fms-splitter-trigger-size);
|
||||
}
|
||||
|
||||
/* 侧栏面板宽度过渡:折叠/展开切换平滑;拖拽时禁用保证跟手 */
|
||||
.fms-layout-splitter .fms-splitter-panel {
|
||||
transition: flex-basis 180ms ease;
|
||||
@@ -189,22 +260,25 @@ function onSplitterResize(sizes) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* 主区面板:四周留 8px 间距(含左侧),与侧栏拉开标准间隙。
|
||||
* 折叠态(窄侧栏 48px)左侧归 0,避免过窄时两侧都留间隙 */
|
||||
/* 主区面板:只留左右 8px padding(顶栏/内容区紧贴上边缘)。
|
||||
* 折叠态与展开态一致,保持 8px 左灰缝,避免侧栏折叠时与右侧紧贴 */
|
||||
.fms-main-panel {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.fms-layout.is-collapsed .fms-main-panel {
|
||||
padding: 8px 8px 8px 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* 覆盖 Splitter 面板默认 overflow: auto(scoped 同名规则特异性更高,需带 .fms-layout 提权),
|
||||
* 让主卡片左侧阴影完整绘制到侧栏背景上(对齐 old2 的 z-index 效果) */
|
||||
* 让顶栏/卡片阴影与圆角完整绘制 */
|
||||
.fms-layout .fms-splitter-panel.fms-main-panel {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 侧栏面板同样需要 overflow: visible:
|
||||
* 否则默认 overflow: auto 会把侧栏卡片的右侧投影(box-shadow 向右投射 4px)
|
||||
* 裁掉,侧栏与右侧内容之间失去可见分隔,看起来"紧贴" */
|
||||
.fms-layout .fms-splitter-panel.fms-sidebar-panel {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 主区纵向列:顶栏在灰底上独立成条,下方白卡与其留 8px 间距 */
|
||||
.fms-main-col {
|
||||
display: flex;
|
||||
@@ -215,8 +289,9 @@ function onSplitterResize(sizes) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 主区内容容器:白底承载内容,与透明侧栏(灰底)形成明显区分;
|
||||
* 圆角 + 阴影让白卡在灰底上浮起,层次清晰 */
|
||||
/* 主区内容容器:灰底画布(--fms-background),不再是白卡。
|
||||
* 页面在此画布上自行放置白卡(background: var(--fms-card) + 圆角 + 阴影),
|
||||
* 靠「灰底 + 白卡」形成层次,避免用大量边框线分隔 */
|
||||
.fms-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -224,20 +299,28 @@ function onSplitterResize(sizes) {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
background: var(--fms-background);
|
||||
}
|
||||
|
||||
/* 内容区:白卡内滚动区,顶部由 .fms-tabs 的底边线分隔。
|
||||
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠) */
|
||||
/* 内容区:灰底画布内的滚动区(顶部由 .fms-tabs 的底边线分隔)。
|
||||
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠)。
|
||||
* overflow: auto hidden —— 只允许纵向滚动,禁止横向滚动条:
|
||||
* 侧栏折叠/展开过渡(flex-basis 180ms)瞬间内容宽度会短暂超出容器,
|
||||
* 若允许 x 滚动会出现滚动条闪现;页面内容的横向滚动应由内部容器(表格/splitter)处理。
|
||||
* 页面需要「固定高度 + 内部滚动」时用 height: var(--fms-content-height)
|
||||
* (由布局实测写入,随顶栏/页签/padding 变化自动跟随),不要手写 calc(100vh - xx) 魔数。
|
||||
* 页面内部的分层白卡:background: var(--fms-card) + border-radius + box-shadow,
|
||||
* 卡间用 margin/gap 留出灰缝,不要用 border 画分隔线 */
|
||||
.fms-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 12px 16px 16px;
|
||||
overflow: auto hidden;
|
||||
/* 只保留上下 padding(8px),左右为 0:
|
||||
* 页面内容紧贴灰画布左右边缘(与主区面板的左右 8px 间距配合),
|
||||
* 让页面白卡尽可能占满横向空间 */
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 页面过渡外层包裹:稳定的 DOM 根,供 transition 的 out-in 模式直接管理,
|
||||
|
||||
@@ -158,6 +158,14 @@ function toNavItem(node) {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
/* 白色侧栏,紧贴左/上/下边缘(父面板无 padding、无圆角)。
|
||||
* 右侧投影参考 soybean-admin 的 sider 设计:
|
||||
* box-shadow: 2px 0 8px 0 rgb(29, 35, 41, 0.05)
|
||||
* 2px 右偏移、8px 模糊、5% 透明度 —— 极淡克制的细腻投影,紧贴右缘自然分隔,
|
||||
* 不用浓阴影(会显脏)。深色模式下 --fms-card 自动变为深灰卡,无需额外覆盖 */
|
||||
background: var(--fms-card);
|
||||
box-shadow: 2px 0 4px 0 rgb(29, 35, 41, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 团队切换(对齐 SidebarHeader p-2 + TeamSwitcher h-12) */
|
||||
@@ -210,6 +218,7 @@ function toNavItem(node) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
/* 主色底 + 白图标(与白侧栏形成反差,保持醒目) */
|
||||
background: var(--fms-primary);
|
||||
color: var(--fms-primary-text);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Moon,
|
||||
Palette,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Sun,
|
||||
} from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -27,6 +28,11 @@ const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
/* 侧栏折叠/展开按钮逻辑由布局(DefaultLayout)提供:
|
||||
* 折叠态 → 展开并恢复宽度;展开态但被拖得很窄 → 展开到默认宽度(不折叠);
|
||||
* 其余 → 正常折叠。兜底为 store 的简单切换 */
|
||||
const toggleSidebar = inject('toggleSidebar', () => appStore.toggleSidebar())
|
||||
|
||||
/* 深浅色切换:原 ThemeToggle 组件逻辑内联,按钮样式复用 .fms-icon-btn */
|
||||
const isDark = computed(() => appStore.darkMode)
|
||||
function toggleDark() {
|
||||
@@ -95,15 +101,15 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
|
||||
<template>
|
||||
<header class="fms-topbar">
|
||||
<!-- 左侧:折叠按钮 -->
|
||||
<!-- 左侧:折叠/展开按钮(图标随状态切换:折叠态显示"展开"图标) -->
|
||||
<div class="fms-topbar-left">
|
||||
<Button
|
||||
type="ghost"
|
||||
class="fms-icon-btn"
|
||||
:title="appStore.sidebarCollapsed ? '展开侧栏' : '折叠侧栏'"
|
||||
@click="appStore.toggleSidebar()"
|
||||
@click="toggleSidebar()"
|
||||
>
|
||||
<PanelLeft />
|
||||
<component :is="appStore.sidebarCollapsed ? PanelRight : PanelLeft" />
|
||||
</Button>
|
||||
|
||||
<span class="fms-separator" aria-hidden="true" />
|
||||
@@ -212,10 +218,12 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
/* 顶栏独立成一块:与 .fms-main 同款白底 + 圆角 + 同款投影,与下方白卡并列分层 */
|
||||
/* 顶栏紧贴上边缘(主区面板上下无 padding);左下+右下圆角。
|
||||
* 向下投影参考 soybean-admin 的 header 设计(0 1px 2px rgb(0,21,41,0.08)),
|
||||
* 配合底圆角让白卡从灰画布上微微浮起,与侧栏的"右侧投影"角色互补 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
border-radius: 0 0 var(--fms-control-radius) var(--fms-control-radius);
|
||||
box-shadow: 0 2px 4px rgb(0, 21, 41, 0.08);
|
||||
}
|
||||
|
||||
.fms-topbar-left,
|
||||
|
||||
@@ -218,8 +218,7 @@ function onToggle() {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致)。
|
||||
侧栏为浅灰底时白底+阴影会形成突兀的白块,改用主色淡底融入浅灰侧栏 */
|
||||
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致) */
|
||||
.fms-nav-link.is-active {
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||||
color: var(--fms-primary);
|
||||
|
||||
@@ -149,9 +149,9 @@ const totalCells = computed(() => columns.value.length * dataSource.value.length
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
/* 页面高度 = fms-content 可视内容高度(100vh - 顶栏40 - 布局gap8 - 页签40 - 内容padding 12+16),
|
||||
/* 高度 = 内容区可视高度(由布局实测写入 --fms-content-height,自动跟随顶栏/页签/padding),
|
||||
让滚动条落在表格内部而不是整个页面滚动 */
|
||||
height: calc(100vh - 116px);
|
||||
height: var(--fms-content-height, calc(100vh - 116px));
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
@@ -204,16 +204,16 @@ const totalCells = computed(() => columns.value.length * dataSource.value.length
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ---- demo 卡片:工具栏 + 表格 ---- */
|
||||
/* ---- demo 卡片:工具栏 + 表格(灰底画布上的白卡,靠阴影分层,无边框线) ---- */
|
||||
.demo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 8px;
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.demo-toolbar {
|
||||
|
||||
@@ -70,9 +70,9 @@ const counter = ref(0)
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.detail-hint {
|
||||
|
||||
@@ -193,9 +193,9 @@ function clearEvents() {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
|
||||
&--grow {
|
||||
flex: 1;
|
||||
|
||||
+90
-8
@@ -12,7 +12,7 @@ import Modal from '@/components/ui/modal/modal.vue'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import Switch from '@/components/ui/switch/switch.vue'
|
||||
import Tree from '@/components/ui/tree/tree.vue'
|
||||
import { Plus, Pencil, Trash2 } from '@lucide/vue'
|
||||
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown } from '@lucide/vue'
|
||||
import { buildTree } from '@/utils/tree'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from '@/utils/dataChanges'
|
||||
@@ -112,6 +112,24 @@ function toggleLayout(node) {
|
||||
node.b_layout_direction = node.b_layout_direction === 'horizontal' ? 'vertical' : 'horizontal'
|
||||
}
|
||||
|
||||
/* 同级排序:上移/下移(交换同级顺序并重新编号 b_xh,随保存提交) */
|
||||
function moveGroup(node, delta) {
|
||||
if (!node) return
|
||||
const parentId = node.b_parent_id == null ? null : String(node.b_parent_id)
|
||||
const siblings = localGroups.value.filter(
|
||||
(g) => (g.b_parent_id == null ? null : String(g.b_parent_id)) === parentId,
|
||||
)
|
||||
siblings.sort((a, b) => (a.b_xh || 0) - (b.b_xh || 0))
|
||||
const idx = siblings.findIndex((g) => String(g.b_id) === String(node.b_id))
|
||||
const target = idx + delta
|
||||
if (idx < 0 || target < 0 || target >= siblings.length) return
|
||||
const [moved] = siblings.splice(idx, 1)
|
||||
siblings.splice(target, 0, moved)
|
||||
siblings.forEach((g, i) => {
|
||||
g.b_xh = (i + 1) * 10
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除:有子分组 / 仍被引用时禁止 */
|
||||
function deleteGroup(node) {
|
||||
if (!node) return
|
||||
@@ -168,6 +186,10 @@ async function save() {
|
||||
@ok="save"
|
||||
>
|
||||
<div class="mm-groups">
|
||||
<div class="mm-groups__head">
|
||||
<div class="mm-groups__head-title">分组结构</div>
|
||||
<span class="mm-groups__head-count">共 {{ localGroups.length }} 个分组</span>
|
||||
</div>
|
||||
<div class="mm-groups__add">
|
||||
<input
|
||||
ref="addInputRef"
|
||||
@@ -216,6 +238,25 @@ async function save() {
|
||||
{{ node.b_title || '未命名分组' }}
|
||||
</span>
|
||||
<span class="mm-groups__node-actions" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
title="上移"
|
||||
aria-label="上移分组"
|
||||
@click="moveGroup(node, -1)"
|
||||
>
|
||||
<ArrowUp :size="13" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
title="下移"
|
||||
aria-label="下移分组"
|
||||
@click="moveGroup(node, 1)"
|
||||
>
|
||||
<ArrowDown :size="13" />
|
||||
</button>
|
||||
<span class="mm-groups__node-divider"></span>
|
||||
<Switch
|
||||
:model-value="Number(node.b_canuse) === 1"
|
||||
title="启用/停用"
|
||||
@@ -268,6 +309,26 @@ async function save() {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ---- 顶部摘要 ---- */
|
||||
.mm-groups__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
.mm-groups__head-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
.mm-groups__head-count {
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
/* ---- 顶部输入即新增 ---- */
|
||||
.mm-groups__add {
|
||||
display: flex;
|
||||
@@ -310,20 +371,26 @@ async function save() {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-secondary-hover);
|
||||
}
|
||||
|
||||
/* 节点行:名称撑满,操作靠右 */
|
||||
/* 节点行:名称撑满,操作靠右;hover 时显示完整操作 */
|
||||
.mm-groups__node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 1px 0;
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-card);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-groups__node-name {
|
||||
@@ -346,26 +413,41 @@ async function save() {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 移动按钮与其它操作的分隔线 */
|
||||
.mm-groups__node-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
margin: 0 2px;
|
||||
background: var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-groups__node-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--fms-text-secondary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-secondary);
|
||||
color: var(--fms-text);
|
||||
border-color: var(--fms-border);
|
||||
}
|
||||
|
||||
&.is-danger:hover {
|
||||
background: #fdecec;
|
||||
color: #d95757;
|
||||
border-color: #f5c6c6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1051
-789
File diff suppressed because it is too large.
Load diff
@@ -363,7 +363,11 @@ async function onSwitchSave() {
|
||||
switchSaving.value = true
|
||||
try {
|
||||
const ok = await saveModule()
|
||||
if (!ok) return
|
||||
if (!ok) {
|
||||
// 保存失败(原因已由 saveModule 内部提示):留在当前对象,弹窗不关闭,可重试或放弃
|
||||
Message.warning('保存未完成,已留在当前对象,请处理后再切换')
|
||||
return
|
||||
}
|
||||
moduleSelectedId.value = state.pendingId
|
||||
switchConfirm.value = null
|
||||
} finally {
|
||||
@@ -527,6 +531,26 @@ async function deleteModule(id) {
|
||||
if (powers.length) {
|
||||
reqs.push({ table: 's_power', key_field: 'b_id', deletes: powers.map((r) => ({ b_id: r.b_id })) })
|
||||
}
|
||||
// 多语言(s_i18n 无 b_module_id 字段,需按模块编码匹配 b_key 前缀)。
|
||||
// 若不清理,删除后残留的孤儿键会被同编码新模块的多语言面板加载,
|
||||
// 进而产生指向已不存在记录的 UPDATE,导致后端「修改记录数异常: 0」。
|
||||
const moduleCode = String(module.b_code || '').trim()
|
||||
if (moduleCode) {
|
||||
const prefix = module.b_module_type === 'category' ? 'menu' : 'module'
|
||||
const i18nRes = await loadDataApi(
|
||||
's_i18n',
|
||||
`b_key = '${prefix}.${moduleCode}' OR b_key LIKE '${prefix}.${moduleCode}.%' ` +
|
||||
`OR b_key LIKE 'field.${moduleCode}.%' OR b_key LIKE 'power.${moduleCode}.%'`,
|
||||
)
|
||||
const i18nRows = i18nRes.data || []
|
||||
if (i18nRows.length) {
|
||||
reqs.push({
|
||||
table: 's_i18n',
|
||||
key_field: 'b_id',
|
||||
deletes: i18nRows.map((r) => ({ b_id: r.b_id })),
|
||||
})
|
||||
}
|
||||
}
|
||||
reqs.push({ table: 's_module', key_field: 'b_id', deletes: [{ b_id: id }] })
|
||||
await saveObjectApi(reqs)
|
||||
// 本地移除整棵子树(未保存子孙随父一并清理),并清除未保存标记
|
||||
@@ -1003,7 +1027,6 @@ loadModuleTree()
|
||||
<Tabs
|
||||
v-if="drafts"
|
||||
v-model="activeSection"
|
||||
type="pill"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="configNavItems"
|
||||
@@ -1235,9 +1258,17 @@ loadModuleTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Splitter 适配:面板内容各自内部滚动,避免双滚动条 */
|
||||
/* Splitter 适配:
|
||||
* - 面板内容各自内部滚动,避免双滚动条
|
||||
* - 面板左右留 3px,两张白卡之间形成灰缝(3 + 分隔条 6 + 3 = 12px),无需边框线
|
||||
* - 隐藏分隔条常显的 1px 线(hover 时的手柄仍会显示,可拖拽调宽) */
|
||||
.mm-body-splitter :deep(> .fms-splitter-panel) {
|
||||
overflow: hidden;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.mm-body-splitter :deep(.fms-splitter-bar::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- 左树顶部 header:搜索框在左,操作在右,无边框无标题 ---- */
|
||||
@@ -1296,6 +1327,11 @@ loadModuleTree()
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
/* 灰底画布上的白卡:白底 + 圆角 + 阴影,与右侧内容卡用灰缝分隔 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mm-tree-panel__scroll {
|
||||
@@ -1350,6 +1386,10 @@ loadModuleTree()
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
/* 灰底画布上的白卡:与左侧树卡用灰缝分隔 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.mm-main__empty {
|
||||
@@ -1379,28 +1419,9 @@ loadModuleTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于 header 内左侧,操作按钮贴右 ---- */
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于工作区顶部,操作按钮在 rightExtra 插槽 ---- */
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型,不作内部覆盖,组件默认样式即可) ---- */
|
||||
.mm-config-nav.tabs {
|
||||
flex: none;
|
||||
/* 与左侧树 header 的分割线对齐:左右两栏 header 同高同线,视觉统一 */
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-config-nav :deep(.tabs-nav) {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 标签列表由 Scrollbar 组件接管滚动,这里只保持垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-list) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* rightExtra 操作区与标签在同一行垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-extra) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 标签内布局:图标 + 文字 + badge */
|
||||
|
||||
@@ -62,9 +62,9 @@ const index = computed(() => Number(route.params.id ?? 0))
|
||||
|
||||
.fms-test-page__card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@@ -1152,6 +1152,10 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
expect(wrapper.text()).toContain('未分组')
|
||||
// 属性面板初始未选中字段(子标题显示"未选中")
|
||||
expect(wrapper.find('.mm-fdesign__inspector .mm-fdesign__subhead').text()).toContain('未选中')
|
||||
// 左栏只放字段:不显示「字段库」标题与布局组件入口
|
||||
expect(wrapper.text()).not.toContain('字段库')
|
||||
expect(wrapper.text()).not.toContain('布局组件')
|
||||
expect(wrapper.text()).not.toContain('分组容器')
|
||||
// 数据视图表格默认隐藏
|
||||
expect(wrapper.find('.mm-edit__data').attributes('style')).toContain('display: none')
|
||||
})
|
||||
@@ -1220,8 +1224,8 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
]
|
||||
const wrapper = mountEdit({ fields: extraFields })
|
||||
await nextTick()
|
||||
// 字段库渲染全部字段项(4 字段 + 2 布局组件)
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(6)
|
||||
// 字段库渲染全部字段项(4 个字段,无布局组件)
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(4)
|
||||
// 画布分组渲染现有字段
|
||||
expect(wrapper.findAll('.mm-fdesign__field').length).toBe(3)
|
||||
})
|
||||
@@ -2354,7 +2358,7 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
const subtitles = wrapper.findAll('.mm-fdesign__lib-subtitle').map((s) => s.text())
|
||||
// 首次出现顺序:输入框(input)、金额(money)、下拉选择(select)、文本域(textarea)
|
||||
expect(subtitles).toEqual(['输入框', '金额', '下拉选择', '文本域'])
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(6) // 4 字段 + 2 布局组件
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(4) // 4 字段,无布局组件
|
||||
})
|
||||
|
||||
it('智能偏向判定:按比例最近边缘,鼠标偏向哪条边就插到哪边', async () => {
|
||||
@@ -2710,13 +2714,16 @@ describe('ModuleEditConfigPanel(子表表格设计)', () => {
|
||||
|
||||
// 带一个分组(g1 金额信息)的挂载:f1/f2 在组内,f3 未分组
|
||||
|
||||
it('头部编辑方式下拉:显示表单设计/表格设计,切换 emit update:editMode', async () => {
|
||||
it('头部编辑方式分段控件:主表表单/子表表格,切换 emit update:editMode', async () => {
|
||||
const wrapper = mountTableEdit()
|
||||
await nextTick()
|
||||
// 第一个 Select 为头部编辑方式(modelValue = props.editMode)
|
||||
const modeSelect = wrapper.findAllComponents({ name: 'Select' })[0]
|
||||
expect(modeSelect.props('modelValue')).toBe('table')
|
||||
modeSelect.vm.$emit('update:model-value', 'form')
|
||||
const modeBtns = wrapper.findAll('.mm-edit__mode-btn')
|
||||
expect(modeBtns).toHaveLength(2)
|
||||
expect(modeBtns[0].text()).toContain('主表表单')
|
||||
expect(modeBtns[1].text()).toContain('子表表格')
|
||||
// 当前 editMode=table → 第二个按钮激活
|
||||
expect(modeBtns[1].classes()).toContain('is-active')
|
||||
await modeBtns[0].trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('update:editMode').at(-1)[0]).toBe('form')
|
||||
})
|
||||
@@ -2915,7 +2922,10 @@ describe('ModuleEditConfigPanel(子表表格设计)', () => {
|
||||
}
|
||||
|
||||
function previewHandle(fieldId) {
|
||||
return document.body.querySelector(`.mm-tdesign__resize-handle[data-field-id="${fieldId}"]`)
|
||||
// 限定在预览弹窗内查询列宽手柄,避免命中残留 DOM
|
||||
return document.body.querySelector(
|
||||
`.mm-tdesign__preview-wrap .mm-tdesign__resize-handle[data-field-id="${fieldId}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
it('预览弹窗拖拽调整列宽:实时更新表头,松手后 emit update(b_width)', async () => {
|
||||
|
||||
Reference in new issue
Block a user