20260913231607

This commit is contained in:
oneao committed 2026-09-13 23:16:08 +08:00
1 parent f3ad727f1c
commit c31329d387
179 files changed
+10846 -2684

No files matched your search

+2 -21
View File
@@ -323,27 +323,8 @@ function getNodeValueLabel(node) {
const option = getFieldOptions(node.field).find((item) => item.value === node.value)
return option?.label || node.value
}
const quickQueryAst = computed(() => {
const children = []
quickSearchFields.value.forEach((field) => {
const value = field.type === 'numrange'
? { min: searchForm[`${field.key}Min`], max: searchForm[`${field.key}Max`] }
: searchForm[field.key]
const hasValue = field.type === 'numrange' ? value.min !== '' || value.max !== '' : value !== '' && value !== null
if (hasValue) children.push({ type: 'condition', field: field.path || field.key, operator: field.type === 'input' ? 'contains' : field.type === 'select' ? 'eq' : 'between', value })
})
return { type: 'group', logic: 'and', children }
})
const advancedQueryAst = computed(() => {
const children = []
const advanced = advancedNodes.filter((node) => hasNodeValue(node.value)).map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
if (advanced.length) children.push({ type: 'group', logic: advancedLogic.value, children: advanced })
advancedGroups.forEach((group) => {
const groupChildren = group.nodes.filter((node) => hasNodeValue(node.value)).map((node) => ({ type: 'condition', field: fieldByKey.value[node.field]?.path || node.field, operator: node.operator, value: node.value }))
if (groupChildren.length) children.push({ type: 'group', logic: group.logic, children: groupChildren })
})
return { type: 'group', logic: 'and', children }
})
// 条件 AST 由 fms-module-common/queryUtils 统一构造(astToSql 生成 search_condition),
// 本页是布局原型,不做查询执行,不重复维护一套 AST。
const advancedConditionCount = computed(() => allAdvancedNodes.value.filter((node) => hasNodeValue(node.value)).length)
const querySummary = computed(() => {
const items = []
@@ -34,7 +34,7 @@ const isExternal = computed(() => maindata.value?.b_menu_type === 'external')
/**
* 路由地址候选:由静态路由表(@/router/routes)派生的页面路由,
* Select 的默认筛选即对 label / value 做包含匹配,等价于 like '%关键词%',
* 输入 "/module" 命中路径、输入 "模块" 命中标题。
* 输入路径片段(如 "/demo")命中路径、输入标题关键词(如 "组件库")命中标题。
* 库里已存但已不在路由表中的旧值(历史数据 / 路由已下线)补进首项,
* 否则它会变成下拉里选不到的孤立值。
*/
@@ -17,7 +17,7 @@
import { computed, nextTick, ref, watch } from 'vue'
import { useHotkeys } from '@/composables/useHotkeys'
import { FolderTree, Save, RefreshCw } from '@lucide/vue'
import { Button, Splitter, SplitterPanel, Modal, Message } from '@/components/ui'
import { Button, Splitter, SplitterPanel, Modal, Message, Spin } from '@/components/ui'
import { loadDataApi, saveObjectApi } from '@/services/api'
import {
buildTree,
@@ -859,7 +859,7 @@ loadModuleOptions()
</Splitter>
</div>
<div v-else-if="configLoading" class="mm-config-state">
<span class="mm-config-state__spinner"></span>
<Spin />
<span>加载中...</span>
</div>
<div v-else class="mm-config-state">暂无配置数据</div>
@@ -942,11 +942,11 @@ loadModuleOptions()
}
/* ---- Splitter 面板 ----
* 面板左右留 3px(白卡之间灰缝 3 + 分隔条 6 + 3 = 12px)。
* 面板不再左右留白,两张白卡紧贴;灰缝由分隔条自身的触发区宽度
* (--fms-splitter-trigger-size,默认 6px)承担。
* 常显线平时透明,hover / 拖拽时显示整条 100% 高度的细线 + 手柄提示 */
.mm-body-splitter :deep(> .fms-splitter-panel) {
overflow: hidden;
padding: 0 3px;
}
.mm-body-splitter :deep(.fms-splitter-bar::before) {
@@ -1064,19 +1064,4 @@ loadModuleOptions()
color: var(--fms-text-secondary);
font-size: 14px;
}
.mm-config-state__spinner {
width: 20px;
height: 20px;
border: 2px solid var(--fms-border);
border-top-color: var(--fms-primary);
border-radius: 50%;
animation: mm-spin 0.6s linear infinite;
}
@keyframes mm-spin {
to {
transform: rotate(360deg);
}
}
</style>
@@ -204,7 +204,6 @@ async function addStandardActions() {
function removeRow(row) {
powers.value = powers.value.filter((item) => item !== row)
Message.success('已删除权限点')
}
</script>
@@ -1,4 +1,4 @@
<script setup>
<script setup>
/**
* 表单设计器(FormDesignEditor)——主表表单画布设计(s_field_edit)。
* 纯编辑器逻辑,不与业务打交道:数据经 props 注入、变更经 emit 上抛。
@@ -85,7 +85,15 @@ let syncedSchema = null
watch(
schemaModel,
(val) => {
if (val === syncedSchema) return
// 变更若为「本编辑器回写后再经父级 round-trip」的同一内容,则不重复解析:
// 仅凭引用相等(===)在 defineModel 子→父→子回流下会失效(回流交出结构相同的新引用),
// 导致 rows/分组被重新 schemaToEditor、_rowKey 重新生成而丢失画布/属性面板选中态。
if (
val === syncedSchema ||
(syncedSchema && stableStringify(val ?? {}) === stableStringify(syncedSchema ?? {}))
) {
return
}
const parsed = schemaToEditor('edit', val || createDefaultSchema('edit'), props.moduleId)
rows.value = parsed.rows
groups.value = parsed.groups
@@ -77,7 +77,15 @@ let syncedSchema = null
watch(
schemaModel,
(val) => {
if (val === syncedSchema) return
// 变更若为「本编辑器回写后再经父级 round-trip」的同一内容,则不重复解析:
// 仅凭引用相等(===)在 defineModel 子→父→子回流下会失效(回流交出结构相同的新引用),
// 导致 rows/分组被重新 schemaToEditor、_rowKey 重新生成而丢失画布/属性面板选中态。
if (
val === syncedSchema ||
(syncedSchema && stableStringify(val ?? {}) === stableStringify(syncedSchema ?? {}))
) {
return
}
const parsed = schemaToEditor(
schemaType.value,
val || createDefaultSchema(schemaType.value),
@@ -34,7 +34,6 @@ const emit = defineEmits([
'toggle',
'colspan-change',
'height-change',
'placeholder-change',
'remove-selected',
])
@@ -47,9 +46,6 @@ function onColspanInput(value) {
function onHeightChange(value) {
emit('height-change', value)
}
function onPlaceholderChange(value) {
emit('placeholder-change', value)
}
function removeSelected() {
emit('remove-selected')
}
@@ -88,7 +84,6 @@ function removeSelected() {
</span>
</div>
<template v-if="selectedRow">
<div class="mm-fdesign__prop-group">基础信息</div>
<div class="mm-fdesign__prop">
<label>字段标题</label>
<span class="mm-fdesign__prop-static">{{ fieldName(selectedRow) }}</span>
@@ -97,19 +92,6 @@ function removeSelected() {
<label>控件类型</label>
<span class="mm-fdesign__prop-static">{{ controlTypeLabel(selectedRow) }}</span>
</div>
<div class="mm-fdesign__prop mm-fdesign__prop--full">
<label for="mm-fdesign-placeholder">占位符</label>
<Input
id="mm-fdesign-placeholder"
class="mm-fdesign__prop-input"
maxlength="100"
:model-value="selectedRow.b_placeholder || ''"
:disabled="loading"
placeholder="输入框内的浅色提示文字"
@change="onPlaceholderChange"
/>
</div>
<div class="mm-fdesign__prop-group">布局</div>
<div class="mm-fdesign__prop-row">
<div
class="mm-fdesign__prop"
@@ -149,7 +131,6 @@ function removeSelected() {
</div>
</div>
</div>
<div class="mm-fdesign__prop-group">状态</div>
<div class="mm-fdesign__toggles">
<div class="mm-fdesign__toggle-line">
<span>必填</span>
@@ -184,7 +165,6 @@ function removeSelected() {
/>
</div>
</div>
<div class="mm-fdesign__prop-group">操作</div>
<Button
type="danger"
:disabled="loading"
@@ -274,20 +254,6 @@ function removeSelected() {
}
}
.mm-fdesign__prop-group {
margin-top: 14px;
color: var(--fms-text);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
border-left: 3px solid var(--fms-primary);
padding-left: 8px;
&:first-child {
margin-top: 0;
}
}
.mm-fdesign__prop {
margin-top: 10px;
@@ -360,14 +326,6 @@ function removeSelected() {
font-size: 11px;
}
/* 属性面板常规输入:压缩 ui Input 高度 */
.mm-fdesign__prop-input.input-wrapper {
height: 26px;
min-height: 0;
padding: 0 8px;
font-size: 11px;
}
.mm-fdesign__toggles {
margin-top: 10px;
}
@@ -6,15 +6,8 @@
* 纯展示(mock 值),无编辑交互;未来运行时表单引擎可复用同一渲染结构。
*/
import { computed } from 'vue'
import {
Checkbox,
DatePicker,
Form,
FormGroup,
FormItem,
Input,
Select,
} from '@/components/ui'
import { Form, FormGroup, FormItem } from '@/components/ui'
import FmsField from '@/components/fms-field/FmsField.vue'
import { useFieldLookup } from '../common/useStructureEditing'
import { spanOf } from './useFormGrid'
@@ -38,6 +31,19 @@ function fieldType(row) {
return fieldOf(row)?.b_type || 'input'
}
/* 预览为 mock 形态(不绑值):只喂给内部控件展示用的选项/行数/占位,
日期与复选框无占位,与运行时形态一致 */
function previewPropsOf(row) {
const type = fieldType(row)
const extra = {}
if (type === 'select') extra.options = []
if (type === 'textarea') extra.rows = 2
if (row.b_placeholder && type !== 'checkbox' && type !== 'date' && type !== 'datetime') {
extra.placeholder = row.b_placeholder
}
return extra
}
/* ================= 分组结构推导 ================= */
// 分组按 b_xh 排序;行按 b_group_id 归组(未分组行垫底,仅在非空时展示)。
// 运行时形态分组占满整行(span 48),与设计画布 48 宫格一致。
@@ -91,52 +97,11 @@ const hasAnyField = computed(() => (props.rows || []).length > 0)
:required="Number(row.b_required) === 1"
:span="spanOf(row)"
>
<Input
v-if="fieldType(row) === 'input'"
:placeholder="row.b_placeholder"
:disabled="Number(row.b_disabled) === 1"
:readonly="Number(row.b_readonly) === 1"
/>
<Input
v-else-if="fieldType(row) === 'textarea'"
type="textarea"
:rows="2"
:placeholder="row.b_placeholder"
:disabled="Number(row.b_disabled) === 1"
:readonly="Number(row.b_readonly) === 1"
/>
<Select
v-else-if="fieldType(row) === 'select'"
:placeholder="row.b_placeholder"
:options="[]"
:disabled="Number(row.b_disabled) === 1"
/>
<Checkbox
v-else-if="fieldType(row) === 'checkbox'"
:disabled="Number(row.b_disabled) === 1"
>{{ fieldOf(row)?.b_default_value || '选项' }}</Checkbox>
<DatePicker
v-else-if="fieldType(row) === 'date'"
:disabled="Number(row.b_disabled) === 1"
/>
<DatePicker
v-else-if="fieldType(row) === 'datetime'"
format="YYYY-MM-DD HH:mm"
:disabled="Number(row.b_disabled) === 1"
/>
<Input
v-else-if="fieldType(row) === 'number' || fieldType(row) === 'money'"
type="number"
:placeholder="row.b_placeholder"
:disabled="Number(row.b_disabled) === 1"
:readonly="Number(row.b_readonly) === 1"
/>
<Input
v-else
type="text"
:placeholder="row.b_placeholder"
<FmsField
:type="fieldType(row)"
:disabled="Number(row.b_disabled) === 1"
:readonly="Number(row.b_readonly) === 1"
:control-props="previewPropsOf(row)"
/>
</FormItem>
</FormGroup>
@@ -104,7 +104,6 @@ function isOn(field) {
<!-- 列属性 -->
<template v-if="selectedRow">
<div class="mm-fdesign__prop-group">基础信息</div>
<div class="mm-fdesign__prop">
<label>列标题</label>
<span class="mm-fdesign__prop-static">{{ fieldName(selectedRow) }}</span>
@@ -125,7 +124,6 @@ function isOn(field) {
@change="(v) => emit('width-commit', v)"
/>
</div>
<div v-if="statusChips.length" class="mm-fdesign__prop-group">状态</div>
<div
v-if="statusChips.length"
class="mm-fdesign__status-chips"
@@ -147,7 +145,6 @@ function isOn(field) {
<component :is="chip.iconOff && !isOn(chip.flag) ? chip.iconOff : chip.icon" :size="12" />
</button>
</div>
<div v-if="showRowDelete" class="mm-fdesign__prop-group">操作</div>
<Button
v-if="showRowDelete"
type="danger"
@@ -161,7 +158,6 @@ function isOn(field) {
<!-- 分组带属性 -->
<template v-else-if="selectedBand">
<div class="mm-fdesign__prop-group">分组带</div>
<div class="mm-fdesign__prop">
<label for="mm-tdesign-band-title">带标题</label>
<Input
@@ -194,7 +190,6 @@ function isOn(field) {
/>
</div>
</div>
<div class="mm-fdesign__prop-group">操作</div>
<Button
type="outline"
:disabled="loading"
@@ -314,20 +309,6 @@ function isOn(field) {
line-height: 1.7;
}
.mm-fdesign__prop-group {
margin-top: 14px;
color: var(--fms-text);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
border-left: 3px solid var(--fms-primary);
padding-left: 8px;
&:first-child {
margin-top: 0;
}
}
.mm-fdesign__prop {
margin-top: 10px;
@@ -31,7 +31,7 @@ import {
ShieldCheck,
Table2,
} from '@lucide/vue'
import { Button, Splitter, SplitterPanel, Tabs, Modal, Message } from '@/components/ui'
import { Button, Splitter, SplitterPanel, Tabs, Modal, Message, Spin } from '@/components/ui'
import { loadDataApi, saveObjectApi } from '@/services/api'
import {
buildTree,
@@ -763,8 +763,7 @@ async function loadModuleConfig(moduleId) {
// 权限点引用的共用动作动词键(action.*)跨模块公用:按精确键并入查询,
// 已存在的键进入基线后 diff 不会重复插入(模块使用权限键即模块名键,已在条件内)
for (const power of powers_org.value || []) {
const key =
String(power.b_i18n || '').trim() || buildPowerI18nKey(power.b_action, id)
const key = String(power.b_i18n || '').trim() || buildPowerI18nKey(power.b_action, id)
if (key && key.startsWith('action.')) i18nKeys.push(key)
}
const i18nRes = await loadDataApi(
@@ -1091,7 +1090,7 @@ loadModuleTree()
</div>
</div>
<div v-else-if="configLoading" class="mm-config-state">
<span class="mm-config-state__spinner"></span>
<Spin />
<span>加载中...</span>
</div>
<div v-else class="mm-config-state">暂无配置数据</div>
@@ -1157,7 +1156,6 @@ loadModuleTree()
.mm-body-splitter :deep(> .fms-splitter-panel) {
overflow: hidden;
padding: 0 3px;
}
.mm-body-splitter :deep(.fms-splitter-bar::before) {
@@ -1274,19 +1272,4 @@ loadModuleTree()
color: var(--fms-text-secondary);
font-size: 13px;
}
.mm-config-state__spinner {
width: 18px;
height: 18px;
border: 2px solid var(--fms-border);
border-top-color: var(--fms-primary);
border-radius: 50%;
animation: mm-config-spin 0.8s linear infinite;
}
@keyframes mm-config-spin {
to {
transform: rotate(360deg);
}
}
</style>