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

+1567
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
@echo off @echo off
set "JAVA_HOME=D:\devtool\jdk\jdk21" set "JAVA_HOME=D:\devtool\jdk\jdk-21"
set "MAVEN_HOME=D:\devtool\maven" set "MAVEN_HOME=D:\devtool\maven\apache-maven-3.8.9"
set "PATH=%JAVA_HOME%\bin;%MAVEN_HOME%\bin;%PATH%" set "PATH=%JAVA_HOME%\bin;%MAVEN_HOME%\bin;%PATH%"
cd /d "%~dp0" cd /d "%~dp0"
View File
Whitespace-only changes.
+54
View File
@@ -0,0 +1,54 @@
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< cn.g3soft:fms-api >--------------------------
[INFO] Building fms-api 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:4.1.0:run (default-cli) > test-compile @ fms-api >>>
[INFO]
[INFO] --- maven-resources-plugin:3.5.0:resources (default-resources) @ fms-api ---
[INFO] Copying 1 resource from src\main\resources to target\classes
[INFO] Copying 0 resource from src\main\resources to target\classes
[INFO]
[INFO] --- maven-compiler-plugin:3.15.0:compile (default-compile) @ fms-api ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- maven-resources-plugin:3.5.0:testResources (default-testResources) @ fms-api ---
[INFO] skip non existing resourceDirectory D:\workspace\code\fms\fms-api\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.15.0:testCompile (default-testCompile) @ fms-api ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< spring-boot-maven-plugin:4.1.0:run (default-cli) < test-compile @ fms-api <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:4.1.0:run (default-cli) @ fms-api ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.0)
2026-09-29T08:30:17.346+08:00 INFO 12408 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Starting FmsApiApplication using Java 21 with PID 12408 (D:\workspace\code\fms\fms-api\target\classes started by Administrator in D:\workspace\code\fms\fms-api)
2026-09-29T08:30:17.356+08:00 INFO 12408 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : No active profile set, falling back to 1 default profile: "default"
2026-09-29T08:30:17.440+08:00 INFO 12408 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2026-09-29T08:30:17.440+08:00 INFO 12408 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2026-09-29T08:30:19.863+08:00 INFO 12408 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8088 (http)
2026-09-29T08:30:19.910+08:00 INFO 12408 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-09-29T08:30:19.911+08:00 INFO 12408 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.22]
2026-09-29T08:30:20.005+08:00 INFO 12408 --- [fms-api] [ restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 2562 ms
2026-09-29T08:30:20.821+08:00 INFO 12408 --- [fms-api] [ restartedMain] c.g3soft.fmsapi.utils.IpRegionSearcher : ip2region Searcher 初始化完成,数据大小 11114380 字节
2026-09-29T08:30:21.243+08:00 INFO 12408 --- [fms-api] [ restartedMain] c.g.f.database.OrgDatabaseConfigWatcher : 已启动机构配置目录监听: D:\workspace\code\fms\fms-api\config\dbconfigs
2026-09-29T08:30:21.781+08:00 INFO 12408 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8088 (http) with context path '/api'
2026-09-29T08:30:21.791+08:00 INFO 12408 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Started FmsApiApplication in 5.29 seconds (process running for 7.111)
2026-09-29T08:39:25.507+08:00 INFO 12408 --- [fms-api] [nio-8088-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/api] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-09-29T08:39:25.508+08:00 INFO 12408 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-09-29T08:39:25.511+08:00 INFO 12408 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
2026-09-29T08:39:25.854+08:00 WARN 12408 --- [fms-api] [nio-8088-exec-1] com.alibaba.druid.pool.DruidDataSource : removeAbandoned is true, not use in production.
2026-09-29T08:39:26.354+08:00 INFO 12408 --- [fms-api] [nio-8088-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1,G3HD} inited
@@ -1602,6 +1602,7 @@ defineExpose({
v-if="views.length || keywordConfig || quickItems.length" v-if="views.length || keywordConfig || quickItems.length"
class="fms-module-card query-card" class="fms-module-card query-card"
> >
<!-- 页签是「在哪个队列里干活」的上级入口(分组可多行),独占一行 -->
<FmsWorkQueueTabs <FmsWorkQueueTabs
:views="views" :views="views"
:active-codes="viewCodes" :active-codes="viewCodes"
@@ -1609,23 +1610,28 @@ defineExpose({
@update:active-codes="changeView" @update:active-codes="changeView"
@load-counts="requestTabCounts" @load-counts="requestTabCounts"
/> />
<FmsKeywordSearch <!-- 条件行:关键词定宽最左,快捷条件横排居中,查询/重置/更多筛选贴右端 -->
v-if="keywordConfig" <div v-if="keywordConfig || quickItems.length" class="query-row">
v-model="keyword" <FmsKeywordSearch
:placeholder="keywordPlaceholder" v-if="keywordConfig"
@search="searchKeyword" v-model="keyword"
/> class="query-row__keyword"
<FmsQuickFilters :placeholder="keywordPlaceholder"
v-if="quickItems.length" @search="searchKeyword"
v-model="quickValues" />
:items="quickItems" <FmsQuickFilters
:loading="loading" v-if="quickItems.length"
:advanced-count="appliedFilterCount" class="query-row__quick"
:advanced-enabled="moreFilters.length > 0" v-model="quickValues"
@search="search" :items="quickItems"
@reset="resetSearch" :loading="loading"
@open-advanced="openFilters" :advanced-count="appliedFilterCount"
/> :advanced-enabled="moreFilters.length > 0"
@search="search"
@reset="resetSearch"
@open-advanced="openFilters"
/>
</div>
<FmsActiveFilterPills <FmsActiveFilterPills
:pills="activePills" :pills="activePills"
@remove="removePill" @remove="removePill"
@@ -1806,7 +1812,7 @@ defineExpose({
box-shadow: 0 2px 6px rgb(31 35 41 / 4%); box-shadow: 0 2px 6px rgb(31 35 41 / 4%);
} }
/* 查询卡:页签 / 关键词 / 快捷筛选 / 条件回显纵向排列,卡间 8px 灰缝由父级 gap 提供 */ /* 查询卡:页签 / 条件行 / 条件回显纵向排列,卡间 8px 灰缝由父级 gap 提供 */
.query-card { .query-card {
display: flex; display: flex;
flex: none; flex: none;
@@ -1815,6 +1821,27 @@ defineExpose({
padding: 10px 16px 12px; 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 { .table-card {
display: flex; display: flex;
@@ -100,55 +100,67 @@ function isDateBeforeLimit(valueToCompare) {
</script> </script>
<template> <template>
<!-- 日期预设:语义区间(今天/本周/本月/近三个月)优先,选「自定义」才落到两端日期 --> <!-- 日期区间:预设下拉与「自定义」的起止两端并排一行。
<FmsField 选语义区间时只显示下拉(下拉铺满整格,不留空档),选「自定义」时下拉收窄、两端出现 ——
v-if="showDatePreset" 两者不再上下堆叠 -->
v-model="datePreset"
class="fms-query-control"
type="select"
:disabled="disabled"
:control-props="{ options: datePresetOptions, filterable: false }"
/>
<div <div
v-if="isRange && (!showDatePreset || datePreset === 'custom')" v-if="showDatePreset || isRange"
class="fms-query-control fms-query-control__range" class="fms-query-control fms-query-control__row"
:class="{ 'fms-query-control__range--compact': compact }" :class="{ 'fms-query-control__row--preset': showDatePreset }"
> >
<!-- 日期区间:两端是两个独立控件,各自只受另一端约束;不接 enter —— Enter 已被日期面板用于开合 --> <FmsField
<template v-if="isDate"> v-if="showDatePreset"
<FmsField v-model="datePreset"
v-model="startValue" class="fms-query-control__preset"
:type="isDateTime ? 'datetime' : 'date'" type="select"
:disabled="disabled" :disabled="disabled"
:control-props="{ :control-props="{ options: datePresetOptions, filterable: false }"
...dateFormatProps, />
timeDefault: 'start', <div
disabledDate: isDateAfterLimit, v-if="isRange && (!showDatePreset || datePreset === 'custom')"
}" class="fms-query-control__range"
/> :class="{ 'fms-query-control__range--compact': compact }"
<span class="fms-query-control__separator">-</span> >
<FmsField <!-- 日期区间:两端是两个独立控件,各自只受另一端约束;不接 enter —— Enter 已被日期面板用于开合 -->
v-model="endValue" <template v-if="isDate">
:type="isDateTime ? 'datetime' : 'date'" <FmsField
:disabled="disabled" v-model="startValue"
:control-props="{ ...dateFormatProps, timeDefault: 'end', disabledDate: isDateBeforeLimit }" :type="isDateTime ? 'datetime' : 'date'"
/> :disabled="disabled"
</template> :control-props="{
<template v-else> ...dateFormatProps,
<FmsField timeDefault: 'start',
v-model="startValue" disabledDate: isDateAfterLimit,
:type="isNumeric ? 'number' : 'input'" }"
:disabled="disabled" />
@keydown="onEnter" <span class="fms-query-control__separator">-</span>
/> <FmsField
<span class="fms-query-control__separator">-</span> v-model="endValue"
<FmsField :type="isDateTime ? 'datetime' : 'date'"
v-model="endValue" :disabled="disabled"
:type="isNumeric ? 'number' : 'input'" :control-props="{
:disabled="disabled" ...dateFormatProps,
@keydown="onEnter" timeDefault: 'end',
/> disabledDate: isDateBeforeLimit,
</template> }"
/>
</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> </div>
<!-- checkbox 转三态下拉(全部/是/否):选项与判定逻辑留在本组件,渲染交给 FmsField --> <!-- checkbox 转三态下拉(全部/是/否):选项与判定逻辑留在本组件,渲染交给 FmsField -->
@@ -192,6 +204,27 @@ function isDateBeforeLimit(valueToCompare) {
</template> </template>
<style scoped lang="scss"> <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 { .fms-query-control__range {
display: grid; display: grid;
width: 100%; width: 100%;
@@ -10,11 +10,19 @@ import { computed } from 'vue'
import { ChevronDown, Filter, Search } from '@lucide/vue' import { ChevronDown, Filter, Search } from '@lucide/vue'
import { Button, Form, FormItem } from '@/components/ui' import { Button, Form, FormItem } from '@/components/ui'
import FmsQueryControl from './FmsQueryControl.vue' 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' import { useI18nStore } from '@/stores/i18n'
/** 条件列宽上限:4 列整行 = 4×288 + 3×16(列间距)= 1200px,不含右侧操作区 */ /** 单列列宽(px):普通单值控件(input / select / 三态 checkbox)够用即可,不必占满整行 */
const GRID_MAX_COLUMN_WIDTH = 288 const COLUMN_WIDTH = 200
/** 宽控件占的列数:区间要放下两端,日期区间还要并排放预设下拉,多选的标签也占宽 */
const WIDE_COLUMN_SPAN = 2
/** 一行最多铺的列数(1 列 = COLUMN_WIDTH),超出换行;与旧系统「定宽列 + 宽控件跨两列」一致 */
const MAX_COLUMNS = 4
const props = defineProps({ const props = defineProps({
// 快捷筛选条件项(已按配置顺序排序、已按字段权限裁剪) // 快捷筛选条件项(已按配置顺序排序、已按字段权限裁剪)
@@ -35,10 +43,27 @@ const values = computed({
set: (next) => emit('update:modelValue', next), set: (next) => emit('update:modelValue', next),
}) })
/** 网格列数按实际条件数收缩(最多 4 列):只有一两个条件时不应铺满整行 */ /** 控件横向需求大的条件:区间(两端)/ 日期(预设下拉 + 两端)/ 多选(标签占宽) */
const gridStyle = computed(() => ({ function needsWideColumn(item) {
gridTemplateColumns: `repeat(${Math.min(Math.max(props.items.length, 1), 4)}, minmax(0, ${GRID_MAX_COLUMN_WIDTH}px))`, 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) { function labelOf(item) {
return item.label || queryItemLabel(item, t) return item.label || queryItemLabel(item, t)
@@ -54,25 +79,32 @@ function submit() {
<template> <template>
<div class="fms-quick-filters"> <div class="fms-quick-filters">
<div class="quick-grid" :style="gridStyle"> <!-- 栅格落在 Form 自身:FormItem 是它的直接子元素,条件才能横向铺开。
<Form :model="values" label-width="72px" label-align="right"> 若再套一层栅格容器包住 Form,整张表单只是该容器的唯一子项,FormItem 会退回块级纵向堆叠 -->
<FormItem <Form
v-for="item in items" class="quick-grid"
:key="item.field.b_field" :style="gridStyle"
class="quick-field" :model="values"
:name="item.field.b_field" label-width="72px"
:label="labelOf(item)" label-align="right"
> >
<FmsQueryControl <FormItem
:model-value="values[item.field.b_field]" v-for="item in items"
:item="item" :key="item.field.b_field"
date-presets class="quick-field"
@update:model-value="setValue(item.field.b_field, $event)" :style="columnStyleOf(item)"
@enter="submit" :name="item.field.b_field"
/> :label="labelOf(item)"
</FormItem> >
</Form> <FmsQueryControl
</div> :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"> <div class="quick-actions">
<Button type="primary" :loading="loading" @click="submit"> <Button type="primary" :loading="loading" @click="submit">
@@ -97,10 +129,12 @@ function submit() {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/* 末行对齐:中文横排(label 在左)下条件与动作簇同为一行高,末行对齐等于顶对齐;
非中文竖排(label 在上)时动作簇才会落到控件那一行的底边,不与 label 平齐 */
.fms-quick-filters { .fms-quick-filters {
display: flex; display: flex;
min-width: 0; min-width: 0;
align-items: flex-start; align-items: flex-end;
gap: 16px; gap: 16px;
} }
@@ -109,6 +143,11 @@ function submit() {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 12px 16px; gap: 12px 16px;
/* 行距只由栅格的 row-gap 提供,取消 FormItem 自带的下间距 */
:deep(.fms-form-item) {
margin-bottom: 0;
}
} }
:deep(.quick-field) { :deep(.quick-field) {
@@ -23,7 +23,8 @@ const props = defineProps({
// 是否在非横向虚拟滚动时自动铺满容器;默认按列配置宽度展示。 // 是否在非横向虚拟滚动时自动铺满容器;默认按列配置宽度展示。
autoFitWidth: { type: Boolean, default: false }, autoFitWidth: { type: Boolean, default: false },
// 列宽拖拽默认开启;false=关闭;{ disabled: (col) => boolean }=按列禁用 // 列宽拖拽默认开启;false=关闭;{ disabled: (col) => boolean }=按列禁用
// (铺满容器的弹性列应禁用拖拽,见库文档「列宽铺满容器hack方式」) // (铺满容器的弹性列应禁用拖拽,见库文档「列宽铺满容器hack方式」)。
// 列上显式写了 resizable: false 的(操作列、展开列)一律不可拖,无需再传 disabled。
colResizable: { type: [Boolean, Object], default: true }, colResizable: { type: [Boolean, Object], default: true },
// 边框自绘:true=外框圆角描边+行分隔线(默认);false=无外框仅行分隔线(嵌入卡片场景)。 // 边框自绘:true=外框圆角描边+行分隔线(默认);false=无外框仅行分隔线(嵌入卡片场景)。
// stk-table 库内边框恒为 false,不用其渐变边框。 // stk-table 库内边框恒为 false,不用其渐变边框。
@@ -86,7 +87,24 @@ const tableTheme = computed(() => props.theme ?? (appStore.darkMode ? 'dark' : '
// 直接关闭这两个功能。声明了 update:columns emit 后监听器不会进 attrs, // 直接关闭这两个功能。声明了 update:columns emit 后监听器不会进 attrs,
// 需从 vnode.props 探测;绑定关系在模板编写时确定,运行期不会变化,无需响应式。 // 需从 vnode.props 探测;绑定关系在模板编写时确定,运行期不会变化,无需响应式。
const columnsBound = 'onUpdate:columns' in (getCurrentInstance()?.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 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。 // Tab 面板从隐藏恢复时,等到容器拥有有效尺寸再刷新虚拟滚动布局。
// rAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。 // rAF 将同一帧内的多次 ResizeObserver 通知合并,避免重复重排。
@@ -733,7 +751,11 @@ defineExpose({
.fms-table__inner.stk-table.bordered-v { .fms-table__inner.stk-table.bordered-v {
th:last-child, th:last-child,
td: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);
} }
} }
@@ -844,9 +844,10 @@ function openPreview() {
</Modal> </Modal>
<!-- 预览:上方复刻列表页查询卡(可交互),下方给出生成的查询条件 --> <!-- 预览:上方复刻列表页查询卡(可交互),下方给出生成的查询条件 -->
<Modal v-model:open="previewOpen" title="查询预览" width="920px" :footer="false"> <Modal v-model:open="previewOpen" title="查询预览" width="1180px" :footer="false">
<div class="mm-preview"> <div class="mm-preview">
<div v-if="previewHasEntry" class="mm-preview__card"> <div v-if="previewHasEntry" class="mm-preview__card">
<!-- 与列表页同排布:页签独占一行,关键词与快捷筛选同行 -->
<FmsWorkQueueTabs <FmsWorkQueueTabs
v-if="previewData.views.length" v-if="previewData.views.length"
:views="previewData.views" :views="previewData.views"
@@ -855,22 +856,29 @@ function openPreview() {
@update:active-codes="previewViewCodes = $event" @update:active-codes="previewViewCodes = $event"
@load-counts="() => {}" @load-counts="() => {}"
/> />
<FmsKeywordSearch <div
v-if="previewData.keywordSearch" v-if="previewData.keywordSearch || previewData.quickFilters.length"
v-model="previewKeyword" class="mm-preview__row"
:placeholder="previewKeywordPlaceholder" >
@search="() => {}" <FmsKeywordSearch
/> v-if="previewData.keywordSearch"
<FmsQuickFilters v-model="previewKeyword"
v-if="previewData.quickFilters.length" class="mm-preview__keyword"
v-model="previewQuickValues" :placeholder="previewKeywordPlaceholder"
:items="previewData.quickFilters" @search="() => {}"
:advanced-count="previewFilterCount" />
:advanced-enabled="previewMoreFilters.length > 0" <FmsQuickFilters
@search="() => {}" v-if="previewData.quickFilters.length"
@reset="resetPreviewQuick" class="mm-preview__quick"
@open-advanced="previewFilterOpen = true" v-model="previewQuickValues"
/> :items="previewData.quickFilters"
:advanced-count="previewFilterCount"
:advanced-enabled="previewMoreFilters.length > 0"
@search="() => {}"
@reset="resetPreviewQuick"
@open-advanced="previewFilterOpen = true"
/>
</div>
</div> </div>
<p v-else class="mm-preview__empty">当前配置没有产生任何查询入口</p> <p v-else class="mm-preview__empty">当前配置没有产生任何查询入口</p>
@@ -1157,6 +1165,24 @@ function openPreview() {
background: var(--fms-surface); background: var(--fms-surface);
} }
/* 条件行:与列表页查询卡同排布(关键词定宽最左,快捷筛选占满其余,末行对齐) */
.mm-preview__row {
display: flex;
min-width: 0;
align-items: flex-end;
gap: 16px;
}
.mm-preview__keyword {
flex: none;
width: 240px;
}
.mm-preview__quick {
flex: 1;
min-width: 0;
}
.mm-preview__empty { .mm-preview__empty {
margin: 0; margin: 0;
color: var(--fms-text-secondary); color: var(--fms-text-secondary);
@@ -17,7 +17,7 @@
*/ */
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Ban, Copy, Eye, Layers, Plus, Send, SquarePen, Trash2 } from '@lucide/vue' import { CircleQuestionMark, Copy, Plus, Trash2 } from '@lucide/vue'
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -29,6 +29,7 @@ import {
Modal, Modal,
Select, Select,
Tabs, Tabs,
Tooltip,
} from '@/components/ui' } from '@/components/ui'
import { FmsModuleListPage } from '@/components/fms-module-list' import { FmsModuleListPage } from '@/components/fms-module-list'
import { loadDataApi } from '@/services/api' import { loadDataApi } from '@/services/api'
@@ -95,10 +96,11 @@ function openDesign(definitionId) {
router.push({ name: 'system-workflow-design', query: { id: definitionId } }) router.push({ name: 'system-workflow-design', query: { id: definitionId } })
} }
/* 操作列只出文字,不带图标:图标会把按钮撑宽、超出操作列的预留宽度 */
const definitionRowActions = computed(() => [ const definitionRowActions = computed(() => [
{ key: 'design', label: '设计', icon: SquarePen, onClick: (row) => openDesign(row.b_id) }, { key: 'design', label: '设计', onClick: (row) => openDesign(row.b_id) },
{ key: 'versions', label: '版本', icon: Layers, onClick: (row) => openVersionDrawer(row) }, { key: 'versions', label: '版本', onClick: (row) => openVersionDrawer(row) },
{ key: 'delete', label: '删除', icon: Trash2, danger: true, onClick: (row) => handleDeleteDefinition(row) }, { key: 'delete', label: '删除', danger: true, onClick: (row) => handleDeleteDefinition(row) },
]) ])
function handleDeleteDefinition(row) { function handleDeleteDefinition(row) {
@@ -239,18 +241,16 @@ function handleRetireVersion(row) {
} }
const versionRowActions = computed(() => [ const versionRowActions = computed(() => [
{ key: 'design', label: '设计', icon: SquarePen, onClick: (row) => openDesign(row.b_definition_id) }, { key: 'design', label: '设计', onClick: (row) => openDesign(row.b_definition_id) },
{ {
key: 'publish', key: 'publish',
label: '发布', label: '发布',
icon: Send,
visible: (row) => row.b_status === 'draft', visible: (row) => row.b_status === 'draft',
onClick: (row) => handlePublishVersion(row), onClick: (row) => handlePublishVersion(row),
}, },
{ {
key: 'retire', key: 'retire',
label: '停用', label: '停用',
icon: Ban,
danger: true, danger: true,
visible: (row) => row.b_status === 'published', visible: (row) => row.b_status === 'published',
onClick: (row) => handleRetireVersion(row), onClick: (row) => handleRetireVersion(row),
@@ -414,8 +414,8 @@ function handleBatchDeleteBindings() {
} }
const bindingRowActions = computed(() => [ const bindingRowActions = computed(() => [
{ key: 'edit', label: '编辑', icon: SquarePen, onClick: (row) => openBindingEdit(row) }, { key: 'edit', label: '编辑', onClick: (row) => openBindingEdit(row) },
{ key: 'delete', label: '删除', icon: Trash2, danger: true, onClick: (row) => handleDeleteBinding(row) }, { key: 'delete', label: '删除', danger: true, onClick: (row) => handleDeleteBinding(row) },
]) ])
/* ============================================================================ /* ============================================================================
@@ -432,7 +432,7 @@ function openInstanceDetail(row) {
} }
const instanceRowActions = computed(() => [ const instanceRowActions = computed(() => [
{ key: 'view', label: '查看', icon: Eye, onClick: (row) => openInstanceDetail(row) }, { key: 'view', label: '查看', onClick: (row) => openInstanceDetail(row) },
]) ])
</script> </script>
@@ -511,7 +511,25 @@ const instanceRowActions = computed(() => [
<FormItem name="moduleId" label="业务模块" required :span="1"> <FormItem name="moduleId" label="业务模块" required :span="1">
<Select v-model="bindingForm.moduleId" :options="moduleOptions" placeholder="选择要接入审批的模块" /> <Select v-model="bindingForm.moduleId" :options="moduleOptions" placeholder="选择要接入审批的模块" />
</FormItem> </FormItem>
<FormItem name="event" label="业务事件" required :span="1"> <!-- 业务事件决定「这次提交走哪条流程」,用 ? 提示解释三种取值,避免和流程内的条件分支混淆 -->
<FormItem name="event" required :span="1">
<template #label>
<span class="wf-binding-event-label">
业务事件
<Tooltip placement="top">
<template #content>
<div class="wf-binding-event-tip">
<p class="wf-binding-event-tip__title">业务事件决定这次提交走哪条流程:</p>
<p><b>送审(submit)</b>:单据第一次提交审批;被驳回结束(作废)后重新发起也走这里。</p>
<p><b>重新提交(resubmit)</b>:单据被退回修改后,发起人改完在原单上再次提交。</p>
<p><b>变更(change)</b>:单据已审批通过后发生业务变更(如改金额)需再次审批;多数模块用不到。</p>
<p class="wf-binding-event-tip__note">一次提交只会命中一条绑定:按「业务模块 + 业务事件 + 条件」匹配,多条命中时取优先级最小的一条。</p>
</div>
</template>
<CircleQuestionMark class="wf-binding-event-help" :size="14" aria-label="业务事件说明" />
</Tooltip>
</span>
</template>
<Select v-model="bindingForm.event" :options="EVENT_OPTIONS" /> <Select v-model="bindingForm.event" :options="EVENT_OPTIONS" />
</FormItem> </FormItem>
<FormItem name="versionId" label="流程版本" required :span="1"> <FormItem name="versionId" label="流程版本" required :span="1">
@@ -566,8 +584,15 @@ const instanceRowActions = computed(() => [
min-height: 0; min-height: 0;
} }
/* 页签栏做成白卡:页面画布是灰底,列表内容是白卡,页签裸在灰底上会显得突兀。
左右 8px 内边距 + 标签自身 8px = 下方卡片内容的 16px,保证首项文字左对齐 */
.wf-page__nav.tabs { .wf-page__nav.tabs {
flex: none; flex: none;
padding: 0 8px;
margin-bottom: 8px;
background: var(--fms-card, #fff);
border-radius: 6px;
box-shadow: 0 2px 6px rgb(31 35 41 / 4%);
} }
/* 各页签的列表铺满剩余高度:v-show 切换时只换显示,列表状态与滚动位置都保留 */ /* 各页签的列表铺满剩余高度:v-show 切换时只换显示,列表状态与滚动位置都保留 */
@@ -588,4 +613,53 @@ const instanceRowActions = computed(() => [
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
/* 「业务事件」标签上的 ? 图标:纯提示,不参与表单校验 */
.wf-binding-event-label {
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.wf-binding-event-help {
/* label 宽固定,图标作为 flex 项会被压缩导致部分被折叠,这里禁止收缩 */
flex: none;
color: var(--fms-text-secondary);
cursor: help;
}
/* label 文本区默认 overflow: hidden(超长省略号),会把超出固定 label 宽的 ? 图标裁掉 */
.fms-form-item[data-field='event'] :deep(.fms-form-label-text) {
overflow: visible;
}
</style>
<style>
/* ---- 业务事件提示浮层排版 ----
浮层经 Teleport 渲染到 body,scoped 样式命中不到,故用非 scoped 类;
同时清掉 <p> 浏览器默认 margin,否则多行说明会把内容块撑得虚高。 */
.wf-binding-event-tip {
width: 320px;
font-weight: 400;
}
.wf-binding-event-tip p {
margin: 0;
}
.wf-binding-event-tip p + p {
margin-top: 6px;
}
.wf-binding-event-tip__title {
font-weight: 600;
}
.wf-binding-event-tip__note {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--fms-border-soft);
color: var(--fms-text-secondary);
}
</style> </style>
@@ -181,7 +181,12 @@ describe('fms-table 虚拟滚动验证', () => {
await nextTick() await nextTick()
await new Promise((r) => setTimeout(r, 100)) await new Promise((r) => setTimeout(r, 100))
const stkTable = wrapper.findComponent(StkTable) const stkTable = wrapper.findComponent(StkTable)
expect(stkTable.props('colResizable')).toBe(true) // 开启时统一传 { disabled } 回调(库只支持这种按列控制方式):普通列可拖,
// 列上显式写 resizable: false 的(操作列 / 展开列)不拖
const colResizable = stkTable.props('colResizable')
expect(colResizable).toMatchObject({ disabled: expect.any(Function) })
expect(colResizable.disabled({ dataIndex: 'c0', width: 120 })).toBe(false)
expect(colResizable.disabled({ dataIndex: '__actions__', resizable: false })).toBe(true)
expect(stkTable.props('headerDrag')).toBeTruthy() expect(stkTable.props('headerDrag')).toBeTruthy()
wrapper.unmount() wrapper.unmount()
}) })
@@ -199,7 +204,9 @@ describe('fms-table 虚拟滚动验证', () => {
await nextTick() await nextTick()
expect(wrapper.find('.fms-table').classes()).not.toContain('fms-table--auto-fit-width') expect(wrapper.find('.fms-table').classes()).not.toContain('fms-table--auto-fit-width')
expect(wrapper.findComponent(StkTable).props('colResizable')).toBe(true) expect(wrapper.findComponent(StkTable).props('colResizable')).toMatchObject({
disabled: expect.any(Function),
})
wrapper.unmount() wrapper.unmount()
}) })
@@ -63,13 +63,15 @@ describe('fms-module-list components', () => {
en.unmount() en.unmount()
}) })
it('快捷筛选网格列数按条件数收缩(只有一个条件时不占 4 列宽)', () => { it('快捷筛选列数按条件占用收缩(普通条件 200px 一列,区间/日期跨两列)', () => {
const one = mount(FmsQuickFilters, { const one = mount(FmsQuickFilters, {
props: { items: [queryItem('b_name')], modelValue: {} }, props: { items: [queryItem('b_name')], modelValue: {} },
}) })
expect(one.find('.quick-grid').element.style.gridTemplateColumns).toBe( expect(one.find('.quick-grid').element.style.gridTemplateColumns).toBe(
'repeat(1, minmax(0, 288px))', 'repeat(1, minmax(0, 200px))',
) )
// 普通单值控件只占一列,不跨列
expect(one.find('.quick-field').element.style.gridColumn).toBe('')
one.unmount() one.unmount()
const four = mount(FmsQuickFilters, { const four = mount(FmsQuickFilters, {
@@ -79,9 +81,21 @@ describe('fms-module-list components', () => {
}, },
}) })
expect(four.find('.quick-grid').element.style.gridTemplateColumns).toBe( expect(four.find('.quick-grid').element.style.gridTemplateColumns).toBe(
'repeat(4, minmax(0, 288px))', 'repeat(4, minmax(0, 200px))',
) )
four.unmount() four.unmount()
// 日期区间(预设下拉 + 两端)跨两列:列数按占用列求和,宽控件不会被挤到下一行
const mixed = mount(FmsQuickFilters, {
props: { items: [rangeQueryItem('date'), queryItem('b_canuse')], modelValue: {} },
})
expect(mixed.find('.quick-grid').element.style.gridTemplateColumns).toBe(
'repeat(3, minmax(0, 200px))',
)
const [wide, narrow] = mixed.findAll('.quick-field')
expect(wide.element.style.gridColumn).toBe('span 2')
expect(narrow.element.style.gridColumn).toBe('')
mixed.unmount()
}) })
it('快捷筛选「查询」提交草稿、「重置」交给页面处理', async () => { it('快捷筛选「查询」提交草稿、「重置」交给页面处理', async () => {
@@ -216,7 +216,7 @@ describe('模块权限面板:权限点表(定宽 / 密集)', () => {
// colResizable + v-model:columns:表格按列宽定宽,拖过的列宽可回写 // colResizable + v-model:columns:表格按列宽定宽,拖过的列宽可回写
const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0] const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0]
expect(powerTable.props('colResizable')).toBe(true) expect(powerTable.props('colResizable')).toMatchObject({ disabled: expect.any(Function) })
}) })
}) })
@@ -444,7 +444,7 @@ describe('绑定模块面板:权限区表头与表宽', () => {
// colResizable + v-model:columns:表格按列宽定宽(不自铺满整栏),拖过的列宽可回写 // colResizable + v-model:columns:表格按列宽定宽(不自铺满整栏),拖过的列宽可回写
const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0] const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0]
expect(powerTable.props('colResizable')).toBe(true) expect(powerTable.props('colResizable')).toMatchObject({ disabled: expect.any(Function) })
}) })
}) })
Submodule code/test/form-js updated: 39406918f6...d00e32ab92.
Submodule code/test/vue-vben-admin updated: edbc42caaa...2d7087602b.