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
set "JAVA_HOME=D:\devtool\jdk\jdk21"
set "MAVEN_HOME=D:\devtool\maven"
set "JAVA_HOME=D:\devtool\jdk\jdk-21"
set "MAVEN_HOME=D:\devtool\maven\apache-maven-3.8.9"
set "PATH=%JAVA_HOME%\bin;%MAVEN_HOME%\bin;%PATH%"
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"
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);
}
}
@@ -844,9 +844,10 @@ function openPreview() {
</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 v-if="previewHasEntry" class="mm-preview__card">
<!-- 与列表页同排布:页签独占一行,关键词与快捷筛选同行 -->
<FmsWorkQueueTabs
v-if="previewData.views.length"
:views="previewData.views"
@@ -855,22 +856,29 @@ function openPreview() {
@update:active-codes="previewViewCodes = $event"
@load-counts="() => {}"
/>
<FmsKeywordSearch
v-if="previewData.keywordSearch"
v-model="previewKeyword"
:placeholder="previewKeywordPlaceholder"
@search="() => {}"
/>
<FmsQuickFilters
v-if="previewData.quickFilters.length"
v-model="previewQuickValues"
:items="previewData.quickFilters"
:advanced-count="previewFilterCount"
:advanced-enabled="previewMoreFilters.length > 0"
@search="() => {}"
@reset="resetPreviewQuick"
@open-advanced="previewFilterOpen = true"
/>
<div
v-if="previewData.keywordSearch || previewData.quickFilters.length"
class="mm-preview__row"
>
<FmsKeywordSearch
v-if="previewData.keywordSearch"
v-model="previewKeyword"
class="mm-preview__keyword"
:placeholder="previewKeywordPlaceholder"
@search="() => {}"
/>
<FmsQuickFilters
v-if="previewData.quickFilters.length"
class="mm-preview__quick"
v-model="previewQuickValues"
:items="previewData.quickFilters"
:advanced-count="previewFilterCount"
:advanced-enabled="previewMoreFilters.length > 0"
@search="() => {}"
@reset="resetPreviewQuick"
@open-advanced="previewFilterOpen = true"
/>
</div>
</div>
<p v-else class="mm-preview__empty">当前配置没有产生任何查询入口</p>
@@ -1157,6 +1165,24 @@ function openPreview() {
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 {
margin: 0;
color: var(--fms-text-secondary);
@@ -17,7 +17,7 @@
*/
import { computed, reactive, ref } from 'vue'
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 {
Button,
Checkbox,
@@ -29,6 +29,7 @@ import {
Modal,
Select,
Tabs,
Tooltip,
} from '@/components/ui'
import { FmsModuleListPage } from '@/components/fms-module-list'
import { loadDataApi } from '@/services/api'
@@ -95,10 +96,11 @@ function openDesign(definitionId) {
router.push({ name: 'system-workflow-design', query: { id: definitionId } })
}
/* 操作列只出文字,不带图标:图标会把按钮撑宽、超出操作列的预留宽度 */
const definitionRowActions = computed(() => [
{ key: 'design', label: '设计', icon: SquarePen, onClick: (row) => openDesign(row.b_id) },
{ key: 'versions', label: '版本', icon: Layers, onClick: (row) => openVersionDrawer(row) },
{ key: 'delete', label: '删除', icon: Trash2, danger: true, onClick: (row) => handleDeleteDefinition(row) },
{ key: 'design', label: '设计', onClick: (row) => openDesign(row.b_id) },
{ key: 'versions', label: '版本', onClick: (row) => openVersionDrawer(row) },
{ key: 'delete', label: '删除', danger: true, onClick: (row) => handleDeleteDefinition(row) },
])
function handleDeleteDefinition(row) {
@@ -239,18 +241,16 @@ function handleRetireVersion(row) {
}
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',
label: '发布',
icon: Send,
visible: (row) => row.b_status === 'draft',
onClick: (row) => handlePublishVersion(row),
},
{
key: 'retire',
label: '停用',
icon: Ban,
danger: true,
visible: (row) => row.b_status === 'published',
onClick: (row) => handleRetireVersion(row),
@@ -414,8 +414,8 @@ function handleBatchDeleteBindings() {
}
const bindingRowActions = computed(() => [
{ key: 'edit', label: '编辑', icon: SquarePen, onClick: (row) => openBindingEdit(row) },
{ key: 'delete', label: '删除', icon: Trash2, danger: true, onClick: (row) => handleDeleteBinding(row) },
{ key: 'edit', label: '编辑', onClick: (row) => openBindingEdit(row) },
{ key: 'delete', label: '删除', danger: true, onClick: (row) => handleDeleteBinding(row) },
])
/* ============================================================================
@@ -432,7 +432,7 @@ function openInstanceDetail(row) {
}
const instanceRowActions = computed(() => [
{ key: 'view', label: '查看', icon: Eye, onClick: (row) => openInstanceDetail(row) },
{ key: 'view', label: '查看', onClick: (row) => openInstanceDetail(row) },
])
</script>
@@ -511,7 +511,25 @@ const instanceRowActions = computed(() => [
<FormItem name="moduleId" label="业务模块" required :span="1">
<Select v-model="bindingForm.moduleId" :options="moduleOptions" placeholder="选择要接入审批的模块" />
</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" />
</FormItem>
<FormItem name="versionId" label="流程版本" required :span="1">
@@ -566,8 +584,15 @@ const instanceRowActions = computed(() => [
min-height: 0;
}
/* 页签栏做成白卡:页面画布是灰底,列表内容是白卡,页签裸在灰底上会显得突兀。
左右 8px 内边距 + 标签自身 8px = 下方卡片内容的 16px,保证首项文字左对齐 */
.wf-page__nav.tabs {
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 切换时只换显示,列表状态与滚动位置都保留 */
@@ -588,4 +613,53 @@ const instanceRowActions = computed(() => [
height: 100%;
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>
@@ -181,7 +181,12 @@ describe('fms-table 虚拟滚动验证', () => {
await nextTick()
await new Promise((r) => setTimeout(r, 100))
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()
wrapper.unmount()
})
@@ -199,7 +204,9 @@ describe('fms-table 虚拟滚动验证', () => {
await nextTick()
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()
})
@@ -63,13 +63,15 @@ describe('fms-module-list components', () => {
en.unmount()
})
it('快捷筛选网格列数按条件数收缩(只有一个条件时不占 4 列宽)', () => {
it('快捷筛选列数按条件占用收缩(普通条件 200px 一列,区间/日期跨两列)', () => {
const one = mount(FmsQuickFilters, {
props: { items: [queryItem('b_name')], modelValue: {} },
})
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()
const four = mount(FmsQuickFilters, {
@@ -79,9 +81,21 @@ describe('fms-module-list components', () => {
},
})
expect(four.find('.quick-grid').element.style.gridTemplateColumns).toBe(
'repeat(4, minmax(0, 288px))',
'repeat(4, minmax(0, 200px))',
)
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 () => {
@@ -216,7 +216,7 @@ describe('模块权限面板:权限点表(定宽 / 密集)', () => {
// colResizable + v-model:columns:表格按列宽定宽,拖过的列宽可回写
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:表格按列宽定宽(不自铺满整栏),拖过的列宽可回写
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.