From fbfd5cde893f09c3cf8cf6de0abeb3c025c140be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 11 Sep 2026 10:41:09 +0800 Subject: [PATCH 01/28] =?UTF-8?q?fix(api):=20=E7=A7=BB=E9=99=A4=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E8=B5=84=E9=87=91=E9=A3=8E=E9=99=A9=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 由于后端/fund-risk-stats端点尚未实现,删除列表页中refreshFundRiskStats调用 - 从data中移除fundRiskStats相关数据,避免接口请求报错 - 列表头部风险筛选标签调整,仅显示“高风险/中风险”,去掉数量显示 - api/business/project-apply.js中保留getFundRiskStats定义,并添加注释说明后端就绪后恢复调用 --- .workbuddy/memory/2026-09-10.md | 6 ++++++ src/api/business/project-apply.js | 1 + 2 files changed, 7 insertions(+) create mode 100644 .workbuddy/memory/2026-09-10.md diff --git a/.workbuddy/memory/2026-09-10.md b/.workbuddy/memory/2026-09-10.md new file mode 100644 index 0000000..db2cbed --- /dev/null +++ b/.workbuddy/memory/2026-09-10.md @@ -0,0 +1,6 @@ +# 2026-09-10 + +- 移除 project-apply 的 fund-risk-stats 请求(后端未实现端点,每次列表加载都弹 No endpoint 报错):删除 onLoad 内 refreshFundRiskStats 调用与该方法、data 中 fundRiskStats;头部风险标签去掉「(数量)」只留「高风险/中风险」;api/business/project-apply.js 保留 getFundRiskStats 定义并注释「后端就绪后恢复」。 +EOF2 +} +cat >> /Users/gxwebsoft/VUE/tms-erp-web-ws/.workbuddy/memory/2026-09-10.md <<'EOF' diff --git a/src/api/business/project-apply.js b/src/api/business/project-apply.js index cf95c97..826caa6 100644 --- a/src/api/business/project-apply.js +++ b/src/api/business/project-apply.js @@ -10,6 +10,7 @@ export const submit = api.submit; export const remove = api.remove; // 项目资金使用风险统计(高风险 / 中风险数量),供列表头部快速筛选标签使用 +// 暂未启用:后端 /fund-risk-stats 端点尚未实现,列表页已移除调用,等后端就绪后恢复 export const getFundRiskStats = params => request({ url: `${baseUrl}/fund-risk-stats`, From 2131e99ec4440c4b4741798b41406ee5b410ce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 11 Sep 2026 12:30:05 +0800 Subject: [PATCH 02/28] =?UTF-8?q?feat(contract-manage):=20=E5=B0=86?= =?UTF-8?q?=E5=90=88=E5=90=8C=E8=AF=A6=E6=83=85=E7=94=B1=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=8B=AC=E7=AB=8B=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增路由 /business/contract-manage/detail,详情页共享合同管理组件 - 列表、表单、详情三分支用 isFormPage 和 isDetailPage 控制 - openDetail 方法改为跳转详情路由,initDetailPage 获取详情数据 - 新增 resetDetailState 和 closeDetailPage 方法,复用关闭逻辑 - 详情页去除弹窗高度限制,样式改为页面自滚动 - 详情页底栏使用固定位置关闭按钮,保持页面布局一致 - watch 和 created 中添加对 isDetailPage 的初始化调用和监听 --- .workbuddy/memory/2026-09-11.md | 28 +++ .workbuddy/memory/MEMORY.md | 9 +- src/router/views/index.js | 12 ++ src/views/business/contract-manage.vue | 174 +++++++++++------- src/views/business/project-apply.vue | 2 +- src/views/business/temporary-credit-limit.vue | 8 + src/views/transportCapacity/driver.vue | 7 +- src/views/transportCapacity/ship.vue | 6 +- src/views/transportCapacity/vehicle.vue | 7 +- vite.config.mjs | 26 +-- 10 files changed, 189 insertions(+), 90 deletions(-) create mode 100644 .workbuddy/memory/2026-09-11.md diff --git a/.workbuddy/memory/2026-09-11.md b/.workbuddy/memory/2026-09-11.md new file mode 100644 index 0000000..e1c56db --- /dev/null +++ b/.workbuddy/memory/2026-09-11.md @@ -0,0 +1,28 @@ +# 2026-09-11 + +## 上传证件区域固定宽度导致跨机器错位(transportCapacity) +- 现象:driver.vue 新增弹窗的身份证/大头照上传框在本机对齐,他人电脑不对齐。 +- 根因:`--large` 上传框写死 `width:240px`(ship 写 248px),而弹窗 `width:90%` + `el-col :span=6` 列宽是弹性的、上方输入框 `width:100%`;列宽随分辨率 / Windows 系统缩放 / 浏览器缩放变化,240px 只在特定宽度下恰好等于列宽。叠加 `labelWidth:auto` 靠 JS 实测字体(苹方 vs 微软雅黑)宽度,label 列宽也不同 → 起始位置漂移。 +- 修复:三个文件改为 `width:100%; max-width:240px; flex:none`(ship 保留 248px 上限),窄屏跟随列宽、宽屏保持比例上限,左边缘恒与输入框对齐。 + - `src/views/transportCapacity/driver.vue`(.driver-uploader--large) + - `src/views/transportCapacity/vehicle.vue`(.vehicle-uploader--large) + - `src/views/transportCapacity/ship.vue`(.ship-uploader--large) +- 用 @vue/compiler-sfc(脚本放项目根目录)验证三个 SFC 编译通过。 +- 教训:本项目所有「固定 px 宽度的上传/控件」都要配 `max-width` + `width:100%`,否则高分屏/低分屏下必然错位。 + +## 合同详情由弹窗改为独立页面(contract-manage) +- 路由:新增 `/business/contract-manage/detail`(`src/router/views/index.js`,name『合同详情』,meta `keepAlive:false` + `activeMenu:'/business/contract-manage'`),组件仍复用 `contract-manage.vue`(与 form 页同一套路)。 +- 页面内三分支:`v-if="!isFormPage && !isDetailPage"`(列表)/ `v-else-if="isFormPage"`(表单)/ `v-else`(详情)。**v-else 必须紧邻 form 分支的 ``**,中间不能插其它组件,否则 Vue 编译报「v-else 无相邻 v-if」——弹窗类组件(billing-plan-editor / 附件预览 / 变更记录详情 / 流程)放在三个分支之后。 +- 关键改动:`openDetail` 改 `router.push({ path:'/business/contract-manage/detail', query:{ id, name } })`;新增 `initDetailPage()`(按 query.id 拉详情,`resetDetailState()` 先清空)、`closeDetailPage()`(closeTag + 回列表)、`isDetailPage` / `detailPageTitle` computed;`created` 与 `$route.fullPath` watch 都要加 `else if (this.isDetailPage)`。 +- 样式:`.contract-manage-detail--page { max-height:none; overflow:visible }`,去掉弹窗的 74vh 限制;底栏复用 `.contract-manage-page__footer`(fixed 浮动),只读详情页按钮保留「关闭」。 +- 校验方式:dev server 已运行时 `curl "http://localhost:2889/src/views/business/contract-manage.vue"` 返回 200 即 Vite 编译通过(比整包 build 快)。 + +## 只给「查看弹窗」加宽 label(temporary-credit-limit) +- 场景:详情页长 label(「剩余项目资金使用额度(万元)」)换行超 2 行撑坏详情表格,但又不能影响新增/编辑页。 +- **关键发现:Avue 的 `avue-form` 在 detail 模式下根节点会挂 `.avue--detail` 类**(源码 `node_modules/@smallwei/avue/lib/packages/element-plus/form/index3.js` 中 `class: normalizeClass([b(), { 'avue--detail': isDetail }])`)。所以「只针对详情态」的选择器就是: + ```scss + .temporary-credit-limit-dialog .avue--detail .el-form-item__label { width:210px !important; flex:0 0 210px !important; } + ``` + 新增/编辑弹窗无 `.avue--detail`,天然不受影响,不需要在 option 里做 mode 判断。 +- 校验样式块编译:dev server 请求 `?vue&type=style&index=1&lang.scss`(**lang 必须写 scss,写 css 会 500**),grep 规则确认产物。 +- 结论备忘:长 label 优先「加宽 + 换行 ≤2 行」,不要用单行省略 + tooltip —— 详情是纯阅读场景,hover 才能看全字段名的可发现性差。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index be1c62d..f6252f5 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -30,8 +30,13 @@ - Avue 内置弹窗 footer 真实 class 是 `.avue-dialog__footer`,须 `position:sticky; bottom:0; flex:none; background:#fff` 贴底。 ### 上传证件区域尺寸(transportCapacity) -- 身份证/行驶证/道路运输证等固定 **240×151px**(1.586:1)。 -- scoped 覆盖 `.xxx-uploader--large { width:240px }` 与 `--large .el-upload { width:100%; height:151px }`。 +- 身份证/行驶证/道路运输证等目标比例 **240×151px**(1.586:1),ship 为 248px 上限。 +- ⚠️ **不能写死 `width:240px`**:弹窗 `width:90%` + 列 `el-col :span=6` 是弹性的,固定 px 只在特定分辨率/系统缩放下恰好等于列宽(本机对齐、他人错位)。统一写法: + ```scss + .xxx-uploader--large { display:block; width:100%; max-width:240px; flex:none; margin:0; } + .xxx-uploader--large .el-upload { width:100%; height:151px; } + ``` + 2026-09-11 已修正 driver / vehicle / ship 三处。 ### 上传图片点击放大预览 - `src/components/image-upload-field/main.vue` 用 ``,阻止冒泡到 el-upload。 diff --git a/src/router/views/index.js b/src/router/views/index.js index a22af30..458a44e 100644 --- a/src/router/views/index.js +++ b/src/router/views/index.js @@ -287,6 +287,18 @@ export default [ }, ], }, + { + path: '/business/contract-manage/detail', + component: Layout, + children: [ + { + path: '', + name: '合同详情', + meta: { keepAlive: false, activeMenu: '/business/contract-manage' }, + component: () => import('@/views/business/contract-manage.vue'), + }, + ], + }, { path: '/business/contract-manage/change', component: Layout, diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index c8fc9fd..8977a22 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -1,6 +1,8 @@ -
+
取消 + + diff --git a/src/views/settlement/pre-settlement-form.vue b/src/views/settlement/pre-settlement-form.vue index b70ec75..9bd3549 100644 --- a/src/views/settlement/pre-settlement-form.vue +++ b/src/views/settlement/pre-settlement-form.vue @@ -50,6 +50,7 @@ export default { }, goBack() { removeSettlementTransfer(this.$route.query.transferToken); + this.$router.$avueRouter.closeTag(); this.$router.push('/settlement/pre-settlement'); }, syncTagTitle() { diff --git a/src/views/settlement/pre-settlement-public-view.vue b/src/views/settlement/pre-settlement-public-view.vue new file mode 100644 index 0000000..1ddec11 --- /dev/null +++ b/src/views/settlement/pre-settlement-public-view.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/views/settlement/receivable-payable-detail.vue b/src/views/settlement/receivable-payable-detail.vue index 0d92d07..faf5b97 100644 --- a/src/views/settlement/receivable-payable-detail.vue +++ b/src/views/settlement/receivable-payable-detail.vue @@ -1153,28 +1153,16 @@ export default { minWidth: 130, align: 'right', }; - const dynamicColumns = this.isPayable - ? [ - { - label: '运输费', - prop: 'tableFreightAmount', - feeSummaryType: 'freight', - minWidth: 130, - align: 'right', - }, - otherFeeColumn, - ] - : [ - ...this.tableFeeItemNames.map((name, index) => ({ - label: name, - prop: `tableFeeItem${index}`, - feeItemName: name, - dynamic: true, - minWidth: 130, - align: 'right', - })), - otherFeeColumn, - ]; + const dynamicColumns = [ + { + label: '运输费', + prop: 'tableFreightAmount', + feeSummaryType: 'freight', + minWidth: 130, + align: 'right', + }, + otherFeeColumn, + ]; if (totalIndex < 0) return [...columns, ...dynamicColumns]; columns.splice(totalIndex, 0, ...dynamicColumns); return columns; @@ -1498,7 +1486,7 @@ export default { const res = await api.getList(this.page.current, this.page.size, params); const data = this.unwrapPage(res); this.rows = (data.records || []).map(this.decorateRow); - this.tableFeeItemNames = this.isPayable ? [] : this.collectFeeItemNames(this.rows); + this.tableFeeItemNames = []; this.page.total = data.total || 0; } finally { this.loading = false; @@ -1738,7 +1726,7 @@ export default { }); const adjusted = { ...item, - transportQuantity: Number(item.transportQuantity || 0), + transportQuantity: this.normalizeAdjustTransportQuantity(item), mileage: item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1 ? null @@ -1767,6 +1755,21 @@ export default { feeSourceLabel(value) { return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手动录入' : '自动生成'; }, + normalizeAdjustTransportQuantity(item = {}) { + const value = item.transportQuantity; + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return ''; + } + const element = item.billingFactor; + // 按车辆/固定金额生成时运输量会被写成占位 1,调整弹窗无实际运输量时不展示 + if ( + (element === '按车辆' || element === '固定金额(整单一口价)') && + Number(value) === 1 + ) { + return ''; + } + return Number(value); + }, adjustBillingTypes(row) { return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || []; }, @@ -1894,7 +1897,12 @@ export default { model: row.model, billingFactor: row.billingFactor, billingType: row.billingType, - transportQuantity: row.transportQuantity, + transportQuantity: + row.transportQuantity === '' || + row.transportQuantity === null || + row.transportQuantity === undefined + ? null + : Number(row.transportQuantity), priceUnit: row.priceUnit, unitPrice: row.unitPrice, mileage: row.mileage, diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index e30e593..565ad53 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -143,7 +143,8 @@ :status="oaSyncProgressStatus" :stroke-width="12" /> -
+
当前阶段:{{ oaSyncStageLabel }}
+
当前阶段:{{ oaSyncStageLabel }},第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页
@@ -177,6 +178,7 @@ import { getDeptTree, syncOaCompany, syncOaDepartment, + clearNonTopDept, } from '@/api/system/dept'; import { getLeaderList } from '@/api/system/user'; import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive'; @@ -215,6 +217,7 @@ export default { oaSyncCancelled: false, oaSyncStatus: 'running', oaSyncStage: 'company', + oaSyncClearNonTop: false, oaSyncController: null, oaSyncProgress: { current: 0, @@ -287,8 +290,9 @@ export default { return; } const parentId = this.normalizeParentId(this.form?.parentId); + // 顶级组织不选择上级时,编码不依赖上级编码 if (!parentId) { - callback(new Error('请先选择上级组织')); + callback(); return; } if (!this.parentDept || String(this.parentDept.id) !== String(parentId)) { @@ -429,13 +433,6 @@ export default { props: { label: 'title', }, - rules: [ - { - required: true, - message: '请选择上级组织', - trigger: 'click', - }, - ], }, { label: '所属租户', @@ -520,6 +517,9 @@ export default { return '自动同步组织'; }, oaSyncStageLabel() { + if (this.oaSyncStage === 'clear') { + return '清除非顶级组织'; + } return this.oaSyncStage === 'department' ? '同步部门' : '同步公司'; }, oaSyncTotalPage() { @@ -555,21 +555,34 @@ export default { }, methods: { handleOaOrgSync() { - this.$confirm('确定从OA先同步公司、再同步部门?', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - }).then(() => { - this.startOaOrgSync(); - }); + this.$confirm( + '是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接同步。', + '提示', + { + confirmButtonText: '是', + cancelButtonText: '否', + distinguishCancelAndClose: true, + closeOnClickModal: false, + type: 'warning', + } + ) + .then(() => { + this.startOaOrgSync(true); + }) + .catch(action => { + if (action === 'cancel') { + this.startOaOrgSync(false); + } + }); }, - startOaOrgSync() { + startOaOrgSync(clearNonTop) { this.oaSyncLoading = true; this.oaSyncVisible = true; this.oaSyncRunning = true; this.oaSyncCancelled = false; this.oaSyncStatus = 'running'; - this.oaSyncStage = 'company'; + this.oaSyncClearNonTop = !!clearNonTop; + this.oaSyncStage = clearNonTop ? 'clear' : 'company'; this.oaSyncController = new AbortController(); this.oaSyncProgress = { current: 0, @@ -607,6 +620,14 @@ export default { }, async runOaOrgSyncPages() { try { + if (this.oaSyncClearNonTop) { + this.oaSyncStage = 'clear'; + await clearNonTopDept(this.oaSyncController?.signal); + if (this.oaSyncCancelled) { + this.oaSyncStatus = 'cancelled'; + return; + } + } this.oaSyncStage = 'company'; await this.runOaOrgSyncStage(syncOaCompany); if (this.oaSyncCancelled) { @@ -821,6 +842,11 @@ export default { .filter(Boolean); return result.length ? result.join('、') : '-'; }, + normalizeTopParent(row) { + if (!this.normalizeParentId(row?.parentId)) { + row.parentId = 0; + } + }, isRootDept(row) { return row && (row.parentId === 0 || String(row.parentId) === '0'); }, @@ -836,6 +862,7 @@ export default { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); row.leaderId = func.join(row.leaderId); + this.normalizeTopParent(row); if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) { this.$message.warning('请选择承运商'); loading(); @@ -865,6 +892,7 @@ export default { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); row.leaderId = func.join(row.leaderId); + this.normalizeTopParent(row); if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) { this.$message.warning('请选择承运商'); loading(); diff --git a/src/views/system/user.vue b/src/views/system/user.vue index 107f5be..6d400fb 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -205,7 +205,7 @@ type="password" show-password autocomplete="new-password" - placeholder="请输入登录密码" + placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*" /> @@ -332,7 +332,12 @@ - + @@ -424,6 +429,7 @@ import { mapGetters } from 'vuex'; import { h } from 'vue'; import { getToken } from '@/utils/auth'; import { downloadXls } from '@/utils/util'; +import { validateLoginPassword } from '@/utils/validate'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; import func from '@/utils/func'; @@ -487,8 +493,7 @@ export default { passwordForm: {}, passwordRules: { password: [ - { required: true, message: '请输入新密码', trigger: 'blur' }, - { min: 6, max: 32, message: '密码长度在6到32个字符', trigger: 'blur' }, + { required: true, validator: validateLoginPassword, trigger: 'blur' }, ], password2: [ { required: true, message: '请再次输入新密码', trigger: 'blur' }, @@ -497,6 +502,7 @@ export default { }, userRules: { account: [{ required: true, message: '请输入账号名', trigger: 'blur' }], + password: [{ required: true, validator: validateLoginPassword, trigger: 'blur' }], realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }], phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }], deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }], diff --git a/src/views/system/userinfo.vue b/src/views/system/userinfo.vue index a0d55f8..002ff50 100644 --- a/src/views/system/userinfo.vue +++ b/src/views/system/userinfo.vue @@ -91,7 +91,7 @@