20260917212907

This commit is contained in:
oneao committed 2026-09-17 21:29:08 +08:00
1 parent 6fa3d8535e
commit 5cbf745bbf
115 files changed
+4104 -85

No files matched your search

@@ -454,3 +454,72 @@ export function describeAdvancedQuery(state, itemByField, translate) {
}
return joinPreviewParts(parts, translate)
}
/* ---------- AST → 高级条件界面状态(buildAdvancedAst 的反向,用于回显已保存的条件) ---------- */
let queryRowSeq = 0
function nextQueryRowId() {
queryRowSeq += 1
return `${Date.now()}-${queryRowSeq}`
}
/**
* AST → 高级条件界面状态。
* 界面只支持「根层 + 一层条件组」(与 FmsAdvancedQueryPanel 一致):组内再出现组时,
* 把内层 children 拍平进该组并置 degraded=true,由调用方决定是否提示「条件已按单层展开」。
* @param {Object} ast 条件 AST({ type:'group'|'condition', children, join, field, operator, value })
* @param {Object} [options] { idFactory } 自定义界面行 id 生成器(测试用)
* @returns {{ nodes: Array, groups: Array, degraded: boolean }}
*/
export function advancedQueryFromAst(ast, { idFactory } = {}) {
const makeId = typeof idFactory === 'function' ? idFactory : nextQueryRowId
const nodes = []
const groups = []
let degraded = false
let at = 0
const nextAt = () => {
at += 1
return at
}
const toNode = (condition, join) => ({
id: makeId(),
at: nextAt(),
join: join === 'or' ? 'or' : 'and',
field: condition.field == null ? '' : String(condition.field),
operator: condition.operator || 'eq',
value:
condition.value === undefined
? emptyConditionValue(undefined, condition.operator || 'eq')
: condition.value,
})
/** 组内子项 → 条件行(界面组只有一行层,嵌套组在这里被拍平) */
const flattenInto = (children, target) => {
for (const child of children || []) {
if (child?.type === 'group') {
degraded = true
flattenInto(child.children, target)
} else if (child?.type === 'condition') {
target.push(toNode(child, child.join))
}
}
}
const children = ast?.type === 'group' ? ast.children || [] : ast ? [ast] : []
for (const child of children) {
if (child?.type === 'group') {
const innerNodes = []
flattenInto(child.children, innerNodes)
groups.push({
id: makeId(),
at: nextAt(),
join: child.join === 'or' ? 'or' : 'and',
nodes: innerNodes,
})
} else if (child?.type === 'condition') {
nodes.push(toNode(child, child.join))
}
}
return { nodes, groups, degraded }
}
@@ -484,7 +484,7 @@ function controlPropsOf(row) {
<div v-if="!hasAnyField" class="fme-empty">
该模块未配置可编辑字段(请在模块管理的表单设计中添加)
</div>
<Form v-else :model="formData" layout="vertical" :columns="24" :gap="{ row: 16, col: 12 }">
<Form v-else :model="formData" :columns="24" :gap="{ row: 16, col: 12 }">
<FormGroup
v-for="section in sections"
:key="section.key"
@@ -1,7 +1,7 @@
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ChevronDown, Filter, Search, Settings } from '@lucide/vue'
import { Button } from '@/components/ui'
import { Button, Form, FormItem } from '@/components/ui'
import FmsQueryControl from './FmsQueryControl.vue'
import {
buildQuickAst,
@@ -82,15 +82,26 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true })
<div class="fms-query-toolbar">
<div class="query-card">
<div class="query-layout">
<!-- 条件字段走组件库 Form/FormItem:label 排版与全系统一致(并跟随全局布局配置),
Form 根用 display:contents 透明化,真正参与 .query-grid 网格的是里面的 FormItem -->
<div class="query-grid" :style="gridStyle">
<div v-for="item in visibleQuickItems" :key="item.field.b_field" class="query-field">
<label class="query-field__label">{{ labelOf(item) }}</label>
<FmsQueryControl
v-model="filters[item.field.b_field]"
:item="item"
@enter="handleSearch"
/>
</div>
<!-- 横向布局下标签按内容收缩(定宽会让短标签拖出一段空白),
长标签由 .query-field 里的 max-width 截断,不会独占整列 -->
<Form class="query-fields" :model="filters" label-width="auto" label-align="left">
<FormItem
v-for="item in visibleQuickItems"
:key="item.field.b_field"
class="query-field"
:name="item.field.b_field"
:label="labelOf(item)"
>
<FmsQueryControl
v-model="filters[item.field.b_field]"
:item="item"
@enter="handleSearch"
/>
</FormItem>
</Form>
</div>
<div class="query-actions">
<div class="action-group">
@@ -153,19 +164,30 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true })
gap: 8px 16px;
align-items: end;
}
/* Form 只作上下文容器:不产生盒模型,子项直接成为 .query-grid 的网格单元 */
.query-fields {
display: contents;
}
.query-field {
display: flex;
min-width: 0;
flex-direction: column;
/* 网格已用 gap 控制间距,去掉 FormItem 自带的下外边距 */
margin-bottom: 0;
gap: 4px;
}
.query-field__label {
/* 条件标签沿用查询区原有观感:次要色 + 单行省略(组件库默认是主文字色)。
纵向时标签独占一行、占满列宽,不做宽度限制 */
.query-field :deep(.fms-form-item-label) {
overflow: hidden;
color: var(--fms-text-secondary, #6f7784);
font-size: var(--fms-font-size);
white-space: nowrap;
text-overflow: ellipsis;
}
/* 横向:宽度由内容决定(Form 传 label-width="auto"),只兜上限让超长字段名截断、
不把控件挤没;标签与控件的间距从组件库默认 12px 收到 6px */
.query-fields.fms-form-horizontal .query-field :deep(.fms-form-item-label) {
max-width: 72px;
padding-right: 6px;
}
/* 操作区:紧跟字段右侧、排成一行,右对齐;分「执行」「查询方式/配置」两组,中间竖分隔线 */
.query-actions {
display: flex;
@@ -48,15 +48,16 @@ function preserveInputDraft(row, dataIndex) {
clearInputDraft(row, dataIndex)
}
function resolveOptions(editor) {
return typeof editor.options === 'function' ? editor.options() : (editor.options ?? [])
// options 允许传函数并接收当前行:列选项依赖同行其它列时(如按目标模块列出目标字段)用它
function resolveOptions(editor, row) {
return typeof editor.options === 'function' ? editor.options(row) : (editor.options ?? [])
}
function displayValue(row, col, editor) {
const value = row[col.dataIndex]
const text =
editor.type === 'select'
? (resolveOptions(editor).find((item) => String(item.value) === String(value))?.label ??
? (resolveOptions(editor, row).find((item) => String(item.value) === String(value))?.label ??
value)
: value
const content = text ?? ''
@@ -135,7 +136,7 @@ function createInput(row, col, editor, ctx) {
}
function createSelect(row, col, editor, ctx) {
const options = resolveOptions(editor)
const options = resolveOptions(editor, row)
const focus = createFocusHandler(ctx, row, col)
return h(Select, {
modelValue: row[col.dataIndex] ?? undefined,
@@ -0,0 +1,51 @@
/**
* UI 组件库全局配置(运行时可改,reactive)
*
* 组件读取优先级统一为:自身 prop > 全局配置 > 组件内置默认。
* 需要支持全局覆盖的 prop,其 default 一律写 undefined,由组件自己按上面顺序回落,
* 这样「显式传值」永远压过全局配置。
*
* 用法:
* import { setUIConfig } from '@/components/ui'
* setUIConfig({ form: { layout: 'vertical' } }) // 局部合并,未提及的键保持原值
*/
import { reactive } from 'vue'
/** 组件库内置默认值,同时也是 resetUIConfig 的基线 */
function createDefaults() {
return {
/** Form 相关默认值 */
form: {
/** 'horizontal'(label 在左)| 'vertical'(label 在上) */
layout: 'horizontal',
/** 'left' | 'right' */
labelAlign: 'right',
/** 横向布局的 label 宽度;undefined 表示使用样式默认值 --fms-form-label-width */
labelWidth: undefined,
/** 'change' | 'blur' */
validateTrigger: 'change',
},
}
}
export const uiConfig = reactive(createDefaults())
/**
* 合并式更新:按组件维度浅合并,只覆盖传入的键。
* @param {Object} patch { form: { layout: 'vertical' }, ... }
*/
export function setUIConfig(patch) {
Object.entries(patch || {}).forEach(([scope, value]) => {
if (!value || typeof value !== 'object') return
uiConfig[scope] = { ...uiConfig[scope], ...value }
})
}
/** 恢复内置默认值(测试隔离、切换租户级配置时用) */
export function resetUIConfig() {
const defaults = createDefaults()
Object.keys(uiConfig).forEach((key) => {
if (!(key in defaults)) delete uiConfig[key]
})
Object.assign(uiConfig, defaults)
}
@@ -1,6 +1,7 @@
<script setup>
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { isEmptyValue, validateRules } from './validate.js'
import { uiConfig } from '../config'
/* ---------- props / emits / slots ---------- */
const props = defineProps({
@@ -67,19 +68,28 @@ const errorMessage = computed(() => errors.value[0] || '')
const helpText = computed(() => errorMessage.value || props.help)
const showHelp = computed(() => helpText.value !== '')
const labelAlign = computed(() => props.labelAlign ?? form?.labelAlign?.value ?? 'left')
const labelWidth = computed(() => props.labelWidth ?? form?.labelWidth?.value)
const validateTrigger = computed(() => form?.validateTrigger?.value ?? 'change')
const labelAlign = computed(
() => props.labelAlign ?? form?.labelAlign?.value ?? uiConfig.form.labelAlign ?? 'left',
)
const labelWidth = computed(
() => props.labelWidth ?? form?.labelWidth?.value ?? uiConfig.form.labelWidth,
)
const validateTrigger = computed(
() => form?.validateTrigger?.value ?? uiConfig.form.validateTrigger ?? 'change',
)
// Form 已下发回落后的 layout;独立使用(无 Form 包裹)时再读一次全局配置
const formLayout = computed(() => form?.layout?.value ?? uiConfig.form.layout ?? 'horizontal')
const rootClasses = computed(() => [
'fms-form-item',
form?.layout?.value === 'vertical' ? 'fms-form-item-vertical' : '',
formLayout.value === 'vertical' ? 'fms-form-item-vertical' : '',
hasError.value ? 'fms-form-item-error' : '',
])
// 横向布局才应用定宽 label;vertical 下 label 自然占一行
const labelStyle = computed(() => {
if (form?.layout?.value === 'vertical' || labelWidth.value == null) return undefined
if (formLayout.value === 'vertical' || labelWidth.value == null) return undefined
return { width: labelWidth.value }
})
@@ -179,7 +189,7 @@ onBeforeUnmount(() => {
v-if="label !== '' || $slots.label"
class="fms-form-item-label"
:class="{
'fms-form-label-vertical': form?.layout?.value === 'vertical',
'fms-form-label-vertical': formLayout === 'vertical',
'fms-form-label-right': labelAlign === 'right',
}"
:style="labelStyle"
@@ -1,6 +1,7 @@
<script setup>
import { computed, provide } from 'vue'
import { fluidColumnGap, normalizeGap } from './form-utils'
import { uiConfig } from '../config'
/* ---------- props / emits / slots ---------- */
const props = defineProps({
@@ -8,17 +9,18 @@ const props = defineProps({
model: { type: Object, required: true },
/** 表单级规则:{ [name]: Rule[] },FormItem 级 rules 优先级更高 */
rules: { type: Object, default: () => ({}) },
/** 布局:horizontal(label 在左)| vertical(label 在上) */
/** 布局:horizontal(label 在左)| vertical(label 在上)
* 不传则取全局配置 uiConfig.form.layout */
layout: {
type: String,
default: 'horizontal',
validator: (value) => ['horizontal', 'vertical'].includes(value),
default: undefined,
validator: (value) => value === undefined || ['horizontal', 'vertical'].includes(value),
},
/** label 文本对齐方式 */
/** label 文本对齐方式;不传则取全局配置 uiConfig.form.labelAlign */
labelAlign: {
type: String,
default: 'right',
validator: (value) => ['left', 'right'].includes(value),
default: undefined,
validator: (value) => value === undefined || ['left', 'right'].includes(value),
},
/** label 宽度(仅横向布局生效;默认使用 --fms-form-label-width) */
labelWidth: { type: [String, Number], default: undefined },
@@ -31,11 +33,11 @@ const props = defineProps({
maxWidth: { type: [String, Number], default: undefined },
/** 必填红星标记 */
requiredMark: { type: Boolean, default: true },
/** 校验时机:change(值变化即校验)| blur(失焦时校验) */
/** 校验时机:change(值变化即校验)| blur(失焦时校验);不传则取全局配置 */
validateTrigger: {
type: String,
default: 'change',
validator: (value) => ['change', 'blur'].includes(value),
default: undefined,
validator: (value) => value === undefined || ['change', 'blur'].includes(value),
},
/** 表单项间距(参考 antdv-next Row gutter):单个数字表示行/列等距,或对象 { row, col } 分别控制行/列间距,单位 px。
* 单列布局(columns <= 1)时 row 生效(纵向堆叠);多列网格(columns > 1)时 row+col 都生效。
@@ -45,6 +47,14 @@ const props = defineProps({
const emit = defineEmits(['submit', 'submitFailed', 'reset'])
/* ---------- 默认值回落:自身 prop > 全局配置 > 内置默认 ---------- */
const resolvedLayout = computed(() => props.layout ?? uiConfig.form.layout ?? 'horizontal')
const resolvedLabelAlign = computed(() => props.labelAlign ?? uiConfig.form.labelAlign ?? 'right')
const resolvedLabelWidth = computed(() => props.labelWidth ?? uiConfig.form.labelWidth)
const resolvedValidateTrigger = computed(
() => props.validateTrigger ?? uiConfig.form.validateTrigger ?? 'change',
)
/* ---------- 布局样式 ---------- */
// 列数 / 最大宽度 / 间距(gap)统一在此应用到根元素。
// 单列纵向布局:display:flex + flex-direction:column + row-gap;
@@ -128,15 +138,17 @@ function onReset(event) {
/* ---------- 上下文 ---------- */
// 下发给 FormItem:model / rules 用 computed 包裹——父级可能整体替换这两个 prop 的对象
// (如切换编辑目标、克隆草稿),provide 只执行一次,直接传引用会让 FormItem 校验读到过期对象
// layout / labelAlign / labelWidth / validateTrigger 下发的是回落后的最终值,
// 让 FormItem 不必再关心全局配置
provide('fms-form', {
model: computed(() => props.model),
rules: computed(() => props.rules),
layout: computed(() => props.layout),
labelAlign: computed(() => props.labelAlign),
labelWidth: computed(() => props.labelWidth),
layout: resolvedLayout,
labelAlign: resolvedLabelAlign,
labelWidth: resolvedLabelWidth,
columns: computed(() => props.spanColumns || props.columns),
requiredMark: computed(() => props.requiredMark),
validateTrigger: computed(() => props.validateTrigger),
validateTrigger: resolvedValidateTrigger,
registerField,
unregisterField,
})
@@ -148,7 +160,10 @@ defineExpose({ validateAll })
<template>
<form
class="fms-form"
:class="[`fms-form-${layout}`, { 'fms-form-grid': columns > 1, 'fms-form-gap': hasGap }]"
:class="[
`fms-form-${resolvedLayout}`,
{ 'fms-form-grid': columns > 1, 'fms-form-gap': hasGap },
]"
:style="formStyle"
@submit="onSubmit"
@reset="onReset"
@@ -1,6 +1,7 @@
/**
* UI 组件库统一出口
* 用法:import { Button, Input, Modal, Message } from '@/components/ui'
* 全局组件默认值:import { setUIConfig, uiConfig } from '@/components/ui'
*/
import Button from './button/button.vue'
import ButtonGroup from './button-group/button-group.vue'
@@ -34,6 +35,7 @@ import Tooltip from './tooltip/tooltip.vue'
import Tree from './tree/tree.vue'
import { Message } from './message/message-manager'
import { resetUIConfig, setUIConfig, uiConfig } from './config'
/** 命令式确认对话框:Modal.confirm({ title, content, okText, onOk, ... }) */
Modal.confirm = modalConfirm
@@ -69,4 +71,8 @@ export {
Tag,
Tooltip,
Tree,
// 全局默认值配置
uiConfig,
setUIConfig,
resetUIConfig,
}