20260917212907
This commit is contained in:
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,
|
||||
}
|
||||
Reference in new issue
Block a user