20260929172211

This commit is contained in:
oneao committed 2026-09-29 17:22:12 +08:00
1 parent ebeeca45b6
commit f337ff1621
16 files changed
+1997 -134

No files matched your search

@@ -1602,6 +1602,7 @@ defineExpose({
v-if="views.length || keywordConfig || quickItems.length"
class="fms-module-card query-card"
>
<!-- 页签是「在哪个队列里干活」的上级入口(分组可多行),独占一行 -->
<FmsWorkQueueTabs
:views="views"
:active-codes="viewCodes"
@@ -1609,23 +1610,28 @@ defineExpose({
@update:active-codes="changeView"
@load-counts="requestTabCounts"
/>
<FmsKeywordSearch
v-if="keywordConfig"
v-model="keyword"
:placeholder="keywordPlaceholder"
@search="searchKeyword"
/>
<FmsQuickFilters
v-if="quickItems.length"
v-model="quickValues"
:items="quickItems"
:loading="loading"
:advanced-count="appliedFilterCount"
:advanced-enabled="moreFilters.length > 0"
@search="search"
@reset="resetSearch"
@open-advanced="openFilters"
/>
<!-- 条件行:关键词定宽最左,快捷条件横排居中,查询/重置/更多筛选贴右端 -->
<div v-if="keywordConfig || quickItems.length" class="query-row">
<FmsKeywordSearch
v-if="keywordConfig"
v-model="keyword"
class="query-row__keyword"
:placeholder="keywordPlaceholder"
@search="searchKeyword"
/>
<FmsQuickFilters
v-if="quickItems.length"
class="query-row__quick"
v-model="quickValues"
:items="quickItems"
:loading="loading"
:advanced-count="appliedFilterCount"
:advanced-enabled="moreFilters.length > 0"
@search="search"
@reset="resetSearch"
@open-advanced="openFilters"
/>
</div>
<FmsActiveFilterPills
:pills="activePills"
@remove="removePill"
@@ -1806,7 +1812,7 @@ defineExpose({
box-shadow: 0 2px 6px rgb(31 35 41 / 4%);
}
/* 查询卡:页签 / 关键词 / 快捷筛选 / 条件回显纵向排列,卡间 8px 灰缝由父级 gap 提供 */
/* 查询卡:页签 / 条件行 / 条件回显纵向排列,卡间 8px 灰缝由父级 gap 提供 */
.query-card {
display: flex;
flex: none;
@@ -1815,6 +1821,27 @@ defineExpose({
padding: 10px 16px 12px;
}
/* 条件行:关键词 + 快捷筛选(条件横排 + 动作簇贴右端)同排,页签因此不必跟着条件一起换行。
末行对齐:中文横排下关键词与条件同为一行高,末行对齐等于顶对齐;
非中文竖排时条件是「label 在上、控件在下」,末行对齐才能让关键词框与控件行齐平 */
.query-row {
display: flex;
min-width: 0;
align-items: flex-end;
gap: 16px;
}
/* 关键词定宽:不随条件数伸缩,位置跨模块、跨屏宽一致 */
.query-row__keyword {
flex: none;
width: 240px;
}
.query-row__quick {
flex: 1;
min-width: 0;
}
/* 表格卡:工具条 + 表格 + 分页,撑满剩余高度,内部滚动 */
.table-card {
display: flex;
@@ -100,55 +100,67 @@ function isDateBeforeLimit(valueToCompare) {
</script>
<template>
<!-- 日期预设:语义区间(今天/本周/本月/近三个月)优先,选「自定义」才落到两端日期 -->
<FmsField
v-if="showDatePreset"
v-model="datePreset"
class="fms-query-control"
type="select"
:disabled="disabled"
:control-props="{ options: datePresetOptions, filterable: false }"
/>
<!-- 日期区间:预设下拉与「自定义」的起止两端并排一行。
选语义区间时只显示下拉(下拉铺满整格,不留空档),选「自定义」时下拉收窄、两端出现 ——
两者不再上下堆叠 -->
<div
v-if="isRange && (!showDatePreset || datePreset === 'custom')"
class="fms-query-control fms-query-control__range"
:class="{ 'fms-query-control__range--compact': compact }"
v-if="showDatePreset || isRange"
class="fms-query-control fms-query-control__row"
:class="{ 'fms-query-control__row--preset': showDatePreset }"
>
<!-- 日期区间:两端是两个独立控件,各自只受另一端约束;不接 enter —— Enter 已被日期面板用于开合 -->
<template v-if="isDate">
<FmsField
v-model="startValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{
...dateFormatProps,
timeDefault: 'start',
disabledDate: isDateAfterLimit,
}"
/>
<span class="fms-query-control__separator">-</span>
<FmsField
v-model="endValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{ ...dateFormatProps, timeDefault: 'end', disabledDate: isDateBeforeLimit }"
/>
</template>
<template v-else>
<FmsField
v-model="startValue"
:type="isNumeric ? 'number' : 'input'"
:disabled="disabled"
@keydown="onEnter"
/>
<span class="fms-query-control__separator">-</span>
<FmsField
v-model="endValue"
:type="isNumeric ? 'number' : 'input'"
:disabled="disabled"
@keydown="onEnter"
/>
</template>
<FmsField
v-if="showDatePreset"
v-model="datePreset"
class="fms-query-control__preset"
type="select"
:disabled="disabled"
:control-props="{ options: datePresetOptions, filterable: false }"
/>
<div
v-if="isRange && (!showDatePreset || datePreset === 'custom')"
class="fms-query-control__range"
:class="{ 'fms-query-control__range--compact': compact }"
>
<!-- 日期区间:两端是两个独立控件,各自只受另一端约束;不接 enter —— Enter 已被日期面板用于开合 -->
<template v-if="isDate">
<FmsField
v-model="startValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{
...dateFormatProps,
timeDefault: 'start',
disabledDate: isDateAfterLimit,
}"
/>
<span class="fms-query-control__separator">-</span>
<FmsField
v-model="endValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{
...dateFormatProps,
timeDefault: 'end',
disabledDate: isDateBeforeLimit,
}"
/>
</template>
<template v-else>
<FmsField
v-model="startValue"
:type="isNumeric ? 'number' : 'input'"
:disabled="disabled"
@keydown="onEnter"
/>
<span class="fms-query-control__separator">-</span>
<FmsField
v-model="endValue"
:type="isNumeric ? 'number' : 'input'"
:disabled="disabled"
@keydown="onEnter"
/>
</template>
</div>
</div>
<!-- checkbox 转三态下拉(全部/是/否):选项与判定逻辑留在本组件,渲染交给 FmsField -->
@@ -192,6 +204,27 @@ function isDateBeforeLimit(valueToCompare) {
</template>
<style scoped lang="scss">
/* 条件控件行:并排的预设下拉 + 起止两端(普通区间只有一个子项,等同原来的整格控件) */
.fms-query-control__row {
display: flex;
width: 100%;
min-width: 0;
align-items: center;
gap: 8px;
}
/* 预设下拉平时铺满整格(选语义区间时没有两端,不留空档);
选「自定义」后两端占 70%,下拉收缩到只放得下预设文案。
两个子项都经 FmsField 渲染(FmsField 自身不产生元素),用 :deep 命中才稳 */
.fms-query-control__row--preset :deep(.fms-query-control__preset) {
flex: 1 1 96px;
min-width: 96px;
}
.fms-query-control__row--preset :deep(.fms-query-control__range) {
flex: 0 1 70%;
}
.fms-query-control__range {
display: grid;
width: 100%;
@@ -10,11 +10,19 @@ import { computed } from 'vue'
import { ChevronDown, Filter, Search } from '@lucide/vue'
import { Button, Form, FormItem } from '@/components/ui'
import FmsQueryControl from './FmsQueryControl.vue'
import { queryItemLabel } from '@/components/fms-module-common/queryUtils'
import {
isDateQuery,
isRangeQuery,
queryItemLabel,
} from '@/components/fms-module-common/queryUtils'
import { useI18nStore } from '@/stores/i18n'
/** 条件列宽上限:4 列整行 = 4×288 + 3×16(列间距)= 1200px,不含右侧操作区 */
const GRID_MAX_COLUMN_WIDTH = 288
/** 单列列宽(px):普通单值控件(input / select / 三态 checkbox)够用即可,不必占满整行 */
const COLUMN_WIDTH = 200
/** 宽控件占的列数:区间要放下两端,日期区间还要并排放预设下拉,多选的标签也占宽 */
const WIDE_COLUMN_SPAN = 2
/** 一行最多铺的列数(1 列 = COLUMN_WIDTH),超出换行;与旧系统「定宽列 + 宽控件跨两列」一致 */
const MAX_COLUMNS = 4
const props = defineProps({
// 快捷筛选条件项(已按配置顺序排序、已按字段权限裁剪)
@@ -35,10 +43,27 @@ const values = computed({
set: (next) => emit('update:modelValue', next),
})
/** 网格列数按实际条件数收缩(最多 4 列):只有一两个条件时不应铺满整行 */
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${Math.min(Math.max(props.items.length, 1), 4)}, minmax(0, ${GRID_MAX_COLUMN_WIDTH}px))`,
}))
/** 控件横向需求大的条件:区间(两端)/ 日期(预设下拉 + 两端)/ 多选(标签占宽) */
function needsWideColumn(item) {
const multiValue = ['in', 'not_in'].includes(item.config.b_operator)
return isRangeQuery(item) || isDateQuery(item) || multiValue
}
/** 栅格列数按各条件占用的列数求和后收缩(最多 MAX_COLUMNS 列):
* 只有一两个普通条件时不该铺满整行,宽控件也不会因列数不够被挤到下一行 */
const gridStyle = computed(() => {
const units = (props.items || []).reduce(
(total, item) => total + (needsWideColumn(item) ? WIDE_COLUMN_SPAN : 1),
0,
)
const columns = Math.min(Math.max(units, 1), MAX_COLUMNS)
return { gridTemplateColumns: `repeat(${columns}, minmax(0, ${COLUMN_WIDTH}px))` }
})
/** 宽控件跨两列:跨列写在 FormItem 上,栅格才给它两列的位置 */
function columnStyleOf(item) {
return needsWideColumn(item) ? { gridColumn: `span ${WIDE_COLUMN_SPAN}` } : undefined
}
function labelOf(item) {
return item.label || queryItemLabel(item, t)
@@ -54,25 +79,32 @@ function submit() {
<template>
<div class="fms-quick-filters">
<div class="quick-grid" :style="gridStyle">
<Form :model="values" label-width="72px" label-align="right">
<FormItem
v-for="item in items"
:key="item.field.b_field"
class="quick-field"
:name="item.field.b_field"
:label="labelOf(item)"
>
<FmsQueryControl
:model-value="values[item.field.b_field]"
:item="item"
date-presets
@update:model-value="setValue(item.field.b_field, $event)"
@enter="submit"
/>
</FormItem>
</Form>
</div>
<!-- 栅格落在 Form 自身:FormItem 是它的直接子元素,条件才能横向铺开。
若再套一层栅格容器包住 Form,整张表单只是该容器的唯一子项,FormItem 会退回块级纵向堆叠 -->
<Form
class="quick-grid"
:style="gridStyle"
:model="values"
label-width="72px"
label-align="right"
>
<FormItem
v-for="item in items"
:key="item.field.b_field"
class="quick-field"
:style="columnStyleOf(item)"
:name="item.field.b_field"
:label="labelOf(item)"
>
<FmsQueryControl
:model-value="values[item.field.b_field]"
:item="item"
date-presets
@update:model-value="setValue(item.field.b_field, $event)"
@enter="submit"
/>
</FormItem>
</Form>
<div class="quick-actions">
<Button type="primary" :loading="loading" @click="submit">
@@ -97,10 +129,12 @@ function submit() {
</template>
<style scoped lang="scss">
/* 末行对齐:中文横排(label 在左)下条件与动作簇同为一行高,末行对齐等于顶对齐;
非中文竖排(label 在上)时动作簇才会落到控件那一行的底边,不与 label 平齐 */
.fms-quick-filters {
display: flex;
min-width: 0;
align-items: flex-start;
align-items: flex-end;
gap: 16px;
}
@@ -109,6 +143,11 @@ function submit() {
flex: 1;
min-width: 0;
gap: 12px 16px;
/* 行距只由栅格的 row-gap 提供,取消 FormItem 自带的下间距 */
:deep(.fms-form-item) {
margin-bottom: 0;
}
}
:deep(.quick-field) {
@@ -23,7 +23,8 @@ const props = defineProps({
// 是否在非横向虚拟滚动时自动铺满容器;默认按列配置宽度展示。
autoFitWidth: { type: Boolean, default: false },
// 列宽拖拽默认开启;false=关闭;{ disabled: (col) => boolean }=按列禁用
// (铺满容器的弹性列应禁用拖拽,见库文档「列宽铺满容器hack方式」)
// (铺满容器的弹性列应禁用拖拽,见库文档「列宽铺满容器hack方式」)。
// 列上显式写了 resizable: false 的(操作列、展开列)一律不可拖,无需再传 disabled。
colResizable: { type: [Boolean, Object], default: true },
// 边框自绘:true=外框圆角描边+行分隔线(默认);false=无外框仅行分隔线(嵌入卡片场景)。
// stk-table 库内边框恒为 false,不用其渐变边框。
@@ -86,7 +87,24 @@ const tableTheme = computed(() => props.theme ?? (appStore.darkMode ? 'dark' : '
// 直接关闭这两个功能。声明了 update:columns emit 后监听器不会进 attrs,
// 需从 vnode.props 探测;绑定关系在模板编写时确定,运行期不会变化,无需响应式。
const columnsBound = 'onUpdate:columns' in (getCurrentInstance()?.vnode.props ?? {})
const effectiveColResizable = columnsBound && !props.autoFitWidth ? props.colResizable : false
// 列宽拖拽开关:库只提供「全局开关 + disabled(col) 回调」这一种按列控制方式,
// 没有按列的 resizable,所以开启时统一转成回调形式,顺带落实列上显式声明的
// resizable: false —— 操作列 / 展开列的宽度由代码算(拖了不落库、列重建即丢)。
// 必须按列禁掉操作列:它 fixed: 'right' 且左边缘压在最后一列上,库对右侧固定列的
// 「左侧把手」拖的是这一列自己,用户拖那根分割线就会改到操作列宽度(拖左列、动操作列)。
const effectiveColResizable = (() => {
if (!columnsBound || props.autoFitWidth || !props.colResizable) return false
const configured = props.colResizable === true ? {} : props.colResizable
return {
disabled: (column) => {
if (column.resizable === false) return true
return typeof configured.disabled === 'function'
? Boolean(configured.disabled(column))
: Boolean(configured.disabled)
},
}
})()
// Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
// rAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
@@ -733,7 +751,11 @@ defineExpose({
.fms-table__inner.stk-table.bordered-v {
th:last-child,
td:last-child {
background-image: none;
/* 只抹掉最右一列的外侧右边线(外框由 .fms-table--bordered 画)。
不能整体清空 background-image:库会给「被前面内容穿透遮挡」的右固定列补
fixed-cell--border-left,并用 --bg-border-left 画它左边的分隔线;整份清空会
把这根线一起干掉,表现为操作列压住内容却没有分割线。 */
--bg-border-right: linear-gradient(transparent, transparent);
}
}