diff --git a/.workbuddy/memory/2026-08-28.md b/.workbuddy/memory/2026-08-28.md
new file mode 100644
index 0000000..017fcab
--- /dev/null
+++ b/.workbuddy/memory/2026-08-28.md
@@ -0,0 +1,60 @@
+# 2026-08-28 工作日志
+
+## contract-manage 表单底部操作栏间距/文案修复
+- 文件:`src/views/business/contract-manage.vue`
+- 问题:底部浮动操作栏 `.contract-manage-page__footer` 用 `gap:8px`,与全站统一 12px 标准不一致("间距不一致不统一")。
+- 修复:去掉 `gap:8px`,改用 `.el-button + .el-button { margin-left:12px }`,对齐全站"去 gap、12px"规范。
+- 文案:底部"关闭"→"取消"(底部操作栏统一命名规范;详情弹窗/流程图弹窗的"关闭"为只读/纯关闭场景,按规范保留)。
+
+## contract-manage 草稿编辑页按钮缺失修复
+- 问题:编辑态(含草稿编辑)底部只有单个"提交"(submitEdit),缺"暂存/提交临时合同/提交正式合同",与新增页不一致。
+- 修复:新增 computed `isDraftEdit`(`formMode==='edit' && form.approvalStatus==='draft'`);footer 三按钮条件由 `formMode==='add'` 改为 `formMode==='add' || isDraftEdit`,草稿编辑复用 `submitCreate`(按 id upsert + temporary/formal 提升),非草稿编辑仍走 submitEdit 单个"提交"。
+
+## contract-manage-change 变更页底部操作栏统一改造
+- 文件:`src/views/business/contract-manage-change.vue`
+- 问题:`.page-footer` 左侧对齐、`gap:16px`、不悬浮,且按钮顺序/命名(提交→关闭)与新增页规范相反。
+- 修复:
+ - DOM:按钮重排为 `[取消][提交]`(次要居左、主操作居右),"关闭"→"取消"。
+ - 样式:`.page-footer` 改为 `position:fixed` 悬浮吸底(left:230px / avue--collapse 60px / avue-layout--horizontal 0)、`justify-content:flex-end` 右对齐、去 gap 改用 `.el-button+.el-button{margin-left:12px}` 统一 12px、白底+上边框+上投影。
+ - 容器 `.contract-change-page` 加 `padding-bottom:72px`,避免内容被悬浮栏遮挡。
+
+## contract-manage 新增页"合同文件"标题未加粗修复
+- 文件:`src/views/business/contract-manage.vue`
+- 问题:`attachment-section` 子组件内渲染的 `.dialog-section-title` 不受当前页面 scoped `> .dialog-section-title` 样式命中,导致"合同文件"标题字重未加粗(截图反馈)。
+- 修复:在 `.contract-manage-form__section` 样式规则中补充 `:deep(.dialog-section-title)`,使直接子元素和子组件内部标题均统一为 16px/600/#303133。
+
+## contract-manage 搜索栏"时间段快捷筛选"组位置前置
+- 文件:`src/views/business/contract-manage.vue` + `src/option/business/contract-manage.js`
+- 第一轮(理解偏差):把"签约类型"select 和 check-tag 组一起塞进 `#search-menu` slot。主人纠正:「签约类型」是普通搜索字段,应与第一行 8 个搜索项同排,不是 slot 内的辅助元素。
+- 第二轮(终态):
+ - option 把 `signType` 加 `search:true / searchOrder:10 / searchPlaceholder:'请选择'`,作为 Avue 标准搜索字段,与其它 8 项同排(放在末尾)。
+ - 页面 `#search-menu` slot 移除「签约类型」select,仅留 5 个 el-check-tag。
+ - 页面 data 移除冗余的 `contractSignType:''`,onLoad/searchReset 不再手动维护 signType(Avue search form 自动绑定到 query)。
+ - 样式:`.el-select { width: 180px }` 已成 dead code,移除。
+ - `:deep(...)` 调整:去掉 `margin-right:auto`,只留 `order:-1`。`.avue-form__menu` 本身 `flex / flex-end`,check-tag 组排首项后,整体仍紧靠右侧,与 `[查询][重置][收起]` 形成 `[快捷筛选] [查询] [重置] [收起]` 右对齐序列。
+
+## 登录验证码:滑块(behavior) → 图形字符验证码(image)
+- 文件:`src/config/website.js`(第14行 `captchaType: 'behavior'` → `'image'`)、`src/page/login/userlogin.vue`、`src/styles/login.scss`
+- 背景:登录页原 `captchaType:'behavior'`(滑块/旋转行为验证)。需求改成截图中的「输入框 + 右侧独立彩色字符验证码图片」。
+- 改动:
+ 1. `website.js` 改 `captchaType: 'image'` 启用图形验证码分支(代码原本就有 image 模式的 UI 与 `getCaptcha()` 逻辑,仅未启用)。
+ 2. `userlogin.vue` image 分支模板:验证码图片从 `el-input` 的 `#append` 内联改为 form-item 内 `.login-code-wrap` 并排独立方块(输入框 flex:1 + 右侧 130px 图片块),点击刷新。
+ 3. `login.scss` 重写 `.login-code/.login-code-wrap/.login-code-box/.login-code-img`:flex 并排、图片块带 1px 边框 + hover 主色高亮、`object-fit:fill` 撑满。
+- 验证:`vite build --outDir /tmp/tms-verify-build` 全量编译通过(exit 0,无报错)。注意原 `pnpm build` 因 WorkBuddy safe-delete 钩子拦截清空 dist/(741 文件)而中断,非代码问题。
+- 注意:登录页 `activeName` 默认 `'iam'`(IAM 统一登录),图形验证码在 `userlogin.vue`,需点「账号密码登录」才可见;验证码图片依赖后端 `/blade-auth/oauth/captcha` 返回的 `data.key` + `data.image`。
+
+## 全站「所属组织」搜索统一为树形下拉(el-tree-select)
+- 背景:合同管理页「所属组织」原为 cascader(radio 风格),客户档案页(vehicle/customer-archive)已用树形下拉。主人要求全站统一为树形。
+- 统一方案:新建可复用 mixin `src/mixins/organization-search.js`(封装 organizationTreeOptions 树数据、loadOrganizationOptions()、renderOrganizationSearch(scope) 渲染函数,自动识别 organizationName / deptName 两种 prop 并挂 column.renderSearch;树节点 node-key='id')。
+- 适用两种架构:
+ 1. **avue-crud 搜索**(renderSearch 模式):option 列加 `searchslot:true`,视图混入 mixin + created/初始化调 loadOrganizationOptions。已改:contract-manage(option 去 cascader 改 searchslot,视图用 renderOrganizationSearch 渲染函数)、transportCapacity 的 driver/vehicle/ship 三页(option 去 type:select 改 searchslot,视图混 mixin)。
+ 2. **自定义搜索表单**(query.deptName 文本框模式):替换 `` 为 ``,node-key='id',@update:model-value 用 findOrgById(val)?.rawLabel 把 id 转回部门名写入 query.deptName(保后端按名称过滤兼容)。已改 payment 模块 4 处:invoice-application / invoice-receipt / payment-application / components/bill-payment-search。
+- 关键坑:mixin 内部树数据命名为 `organizationTreeOptions`(非 organizationOptions),与 transportCapacity 等页面表单自用的扁平 `organizationOptions` 隔离,避免相互覆盖破坏表单 select。
+- 待办(同自定义表单模式,未改):settlement 模块 4 个视图(transport-reconciliation/formal-settlement/pre-settlement/settlement-adjustment,搜索用动态 v-for 渲染 searchFields,需给 deptName 加 `type:'tree'` 分支)、system/user.vue(deptName)。business 模块(waybill-manage/transport-plan 等)所属组织为 `hide:true` 不展示搜索,无需改。
+
+## project-apply 业务部门栏占满宽度修复
+- 文件:`src/views/business/project-apply.vue`
+- 问题:「重大项目补录」(独立表单页 /business/project-apply/form?mode=majorSupplement)下"业务部门"项横跨整行,与上下两行半宽风格不一致。
+- 根因:`` 误加 `class="project-apply-form__item--full"`(该类对应样式 `:deep(.project-apply-form__item--full) { grid-column: 1 / -1; ... }` 是横跨整行的:本意只用于"项目由来说明"等多行文本字段)。
+- 修复:移除该 class,业务部门恢复为半宽(一行两列网格的常规项),仅"项目由来说明"等需要全宽铺开的字段仍保留 full。同时把样式块上方注释由「占满整行的表单项(业务部门 / 项目由来说明)」改为「占满整行的表单项(如项目由来说明等多行文本字段)」,避免再误读。
+
diff --git a/src/config/website.js b/src/config/website.js
index 5172e82..9580713 100644
--- a/src/config/website.js
+++ b/src/config/website.js
@@ -11,7 +11,7 @@ export default {
tenantMode: true, // 是否开启租户模式
tenantId: '000000', // 管理组租户编号
captchaMode: true, // 是否开启验证码模式
- captchaType: 'behavior', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证)
+ captchaType: 'image', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证)
switchMode: false, // 是否开启登录切换角色部门
lockPage: '/lock', // 锁屏页面地址
tokenTime: 3000, // 定时刷新token间隔(单位:毫秒)
diff --git a/src/mixins/organization-search.js b/src/mixins/organization-search.js
new file mode 100644
index 0000000..10f3061
--- /dev/null
+++ b/src/mixins/organization-search.js
@@ -0,0 +1,115 @@
+import { getDeptTree } from '@/api/system/dept';
+import { ElTreeSelect } from 'element-plus';
+import { h } from 'vue';
+
+/**
+ * 所属组织 统一搜索 mixin
+ * ------------------------------------------------------------------
+ * 全站「所属组织」搜索栏统一为树形下拉(el-tree-select),风格与客户档案
+ * (vehicle/customer-archive.vue)保持一致。
+ *
+ * 接入步骤(在使用了 avue-crud 的页面中):
+ * 1. 混入本 mixin:mixins: [organizationSearch]
+ * 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
+ * 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
+ *
+ * 说明:
+ * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
+ * - 树节点标识统一用 id(node-key='id'),返回值为部门 id
+ * - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法,
+ * 组件自身方法优先级更高,会覆盖本 mixin 的默认实现
+ * - 搜索值(id)需在各自的 searchChange / normalizeSearch 中按后端要求
+ * 转换为部门名称(参考 contract-manage 的 normalizeSearch)
+ */
+const ORG_PROPS = ['organizationName', 'deptName'];
+
+export default {
+ data() {
+ return {
+ // 树形组织数据,仅供搜索栏 el-tree-select 使用;
+ // 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖
+ organizationTreeOptions: [],
+ organizationTreeFlatOptions: [],
+ organizationLoading: false,
+ };
+ },
+ methods: {
+ loadOrganizationOptions() {
+ if (this.organizationLoading) return;
+ this.organizationLoading = true;
+ const tenantId = this.userInfo?.tenantId;
+ getDeptTree(tenantId)
+ .then(res => {
+ const source = res.data?.data || res.data || [];
+ const tree = this.excludeExternalOrganization
+ ? this.excludeExternalOrganization(source)
+ : source;
+ this.organizationTreeOptions = this.normalizeOrgTree(tree);
+ this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions);
+ const option = this.tableOption || this.option;
+ if (!option) return;
+ ORG_PROPS.forEach(prop => {
+ const column = this.findColumn?.(option.column, prop);
+ if (column) {
+ column.renderSearch = scope => this.renderOrganizationSearch(scope, prop);
+ }
+ });
+ })
+ .finally(() => {
+ this.organizationLoading = false;
+ });
+ },
+ // 默认排除「外部组织」节点;页面可覆盖
+ excludeExternalOrganization(tree = []) {
+ return (tree || []).reduce((result, item) => {
+ const name = item.title || item.deptName || item.name || item.label || '';
+ if (String(name).trim() === '外部组织') return result;
+ result.push({
+ ...item,
+ children: this.excludeExternalOrganization(item.children || []),
+ });
+ return result;
+ }, []);
+ },
+ normalizeOrgTree(tree = []) {
+ return (tree || []).map(item => {
+ const label = item.title || item.deptName || item.name || item.label || '';
+ const children = this.normalizeOrgTree(item.children || []);
+ return {
+ ...item,
+ id: String(item.id),
+ label,
+ rawLabel: label,
+ children: children.length ? children : undefined,
+ };
+ });
+ },
+ flattenOrgTree(tree = []) {
+ return (tree || []).flatMap(item => [
+ { id: String(item.id), label: item.label, rawLabel: item.rawLabel },
+ ...this.flattenOrgTree(item.children || []),
+ ]);
+ },
+ findOrgById(id) {
+ const value = Array.isArray(id) ? id.at(-1) : id;
+ return this.organizationTreeFlatOptions.find(item => String(item.id) === String(value));
+ },
+ renderOrganizationSearch(scope, prop = 'organizationName') {
+ return h(ElTreeSelect, {
+ modelValue: scope.row?.[prop] || '',
+ 'onUpdate:modelValue': value => {
+ if (scope.row) scope.row[prop] = value;
+ },
+ data: this.organizationTreeOptions,
+ 'node-key': 'id',
+ 'check-strictly': true,
+ filterable: true,
+ clearable: true,
+ 'render-after-expand': false,
+ style: 'width: 100%',
+ placeholder: '请选择 所属组织',
+ props: { label: 'label', value: 'id', children: 'children' },
+ });
+ },
+ },
+};
diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js
index c604260..b416f07 100644
--- a/src/option/business/contract-manage.js
+++ b/src/option/business/contract-manage.js
@@ -251,6 +251,7 @@ export const option = createCrudOption([
searchPlaceholder: '请输入',
order: 180,
slot: true,
+ overHidden: true,
minWidth: 240,
rules: textRule('合同名称', 100, true),
},
@@ -285,20 +286,11 @@ export const option = createCrudOption([
prop: 'organizationName',
search: true,
searchOrder: 3,
- searchType: 'cascader',
- searchPlaceholder: '全部',
+ searchslot: true,
+ searchPlaceholder: '请选择 所属组织',
formslot: true,
order: 120,
dicData: [],
- props: {
- label: 'label',
- value: 'id',
- children: 'children',
- },
- checkStrictly: true,
- emitPath: false,
- showAllLevels: false,
- filterable: true,
minWidth: 150,
rules: selectRule('所属组织'),
},
@@ -318,7 +310,9 @@ export const option = createCrudOption([
label: '签约类型',
prop: 'signType',
type: 'select',
- search: false,
+ search: true,
+ searchOrder: 10,
+ searchPlaceholder: '请选择',
span: 8,
order: 190,
dicData: signTypeOptions,
diff --git a/src/option/transportCapacity/driver.js b/src/option/transportCapacity/driver.js
index 0027a84..25c004c 100644
--- a/src/option/transportCapacity/driver.js
+++ b/src/option/transportCapacity/driver.js
@@ -81,9 +81,7 @@ export const option = {
prop: 'organizationName',
search: true,
searchOrder: 1,
- type: 'select',
- filterable: true,
- dicData: [],
+ searchslot: true,
slot: true,
minWidth: 160,
},
diff --git a/src/option/transportCapacity/transport-ship.js b/src/option/transportCapacity/transport-ship.js
index dced0f4..8e4c44a 100644
--- a/src/option/transportCapacity/transport-ship.js
+++ b/src/option/transportCapacity/transport-ship.js
@@ -37,9 +37,7 @@ export const option = {
prop: 'organizationName',
search: true,
searchOrder: 1,
- type: 'select',
- filterable: true,
- dicData: [],
+ searchslot: true,
slot: true,
minWidth: 160,
},
diff --git a/src/option/transportCapacity/transport-vehicle.js b/src/option/transportCapacity/transport-vehicle.js
index 99f9e47..50247ad 100644
--- a/src/option/transportCapacity/transport-vehicle.js
+++ b/src/option/transportCapacity/transport-vehicle.js
@@ -46,9 +46,7 @@ export const option = {
prop: 'organizationName',
search: true,
searchOrder: 1,
- type: 'select',
- filterable: true,
- dicData: [],
+ searchslot: true,
slot: true,
minWidth: 150,
},
diff --git a/src/page/login/userlogin.vue b/src/page/login/userlogin.vue
index 5972557..83b7910 100644
--- a/src/page/login/userlogin.vue
+++ b/src/page/login/userlogin.vue
@@ -46,21 +46,22 @@
-
-
-
-
-
-
-
![]()
-
-
-
+
+
+
+
+
+
+
+
![验证码]()
+
+
-
- {{
- displayValue(draft.planName)
- }}
- {{
- transportModeLabel(draft.transportMode)
- }}
- {{
- isDefaultPlan(draft) ? '是' : '否'
- }}
- {{ displayValue(draft.remark) }}
-
-
-
-
+
+ {{
+ displayValue(draft.planName)
+ }}
+ {{
+ transportModeLabel(draft.transportMode)
+ }}
+ {{
+ isDefaultPlan(draft) ? '是' : '否'
+ }}
+ {{ displayValue(draft.remark) }}
+
+
+
+
-
+ visible && ensureTransportModes()"
- >
- visible && ensureTransportModes()"
+ >
+ 默认方案
- >默认方案
+
-
+
-
-
+ >
+
+
+
+添加规则
@@ -362,6 +366,7 @@ import { getList as getFeeItemList } from '@/api/base/fee-item';
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { InfoFilled } from '@element-plus/icons-vue';
import { getDictionary } from '@/api/system/dictbiz';
+import SectionCard from '@/components/section-card/main.vue';
const clone = value => JSON.parse(JSON.stringify(value));
const defaultRule = () => ({
@@ -391,7 +396,7 @@ const defaultRule = () => ({
});
export default {
- components: { InfoFilled },
+ components: { SectionCard, InfoFilled },
props: {
modelValue: Boolean,
value: { type: Object, default: () => ({}) },
@@ -874,9 +879,6 @@ export default {
this.visible = false;
});
},
- showDefaultPlanTip() {
- this.$message.info('同一运输方式仅支持配置一个默认计费方案');
- },
openMatch(row, index) {
this.matchIndex = index;
this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) };
diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue
index 3a4267e..53746e9 100644
--- a/src/views/business/contract-manage-change.vue
+++ b/src/views/business/contract-manage-change.vue
@@ -83,7 +83,10 @@
-
+
@@ -151,11 +154,11 @@ export default {
};
-
diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue
index 7399584..7095ca7 100644
--- a/src/views/business/contract-manage.vue
+++ b/src/views/business/contract-manage.vue
@@ -33,16 +33,7 @@
-
-
签约类型:
-
-
-
+
-
+
{{ row.contractName }}
-
@@ -492,8 +483,8 @@