- formal-settlement.vue 中移除旧搜索区,移除多余样式,实现结算表格面板按激活标签条件渲染 - 替换 formal-settlement-table-panel.vue 为基于 avue-crud,简化表格和操作,优化分页和搜索事件处理 - 新版 formal-settlement 中统一使用 onLoad 替代 loadTable 方法,简化数据加载逻辑 - 删除不必要的样式代码,统一表格奇偶行背景颜色,调整整体样式提升视觉一致性 - 结算导入页面 settlement-import.vue 同样迁移至 avue-crud 组件实现,实现搜索、分页、刷新等功能事件解耦 - 优化分页数据结构,统一使用 currentPage 和 pageSize 命名,支持多页容量选择 - 调整部分接口调用及事件回调,保障新组件兼容已有业务流程及权限控制 - 移除废弃的内部变量与混入,针对权限控制和字段渲染进行明确封装和暴露 - 保持功能行为不变的前提下提升代码可维护性和复用性,增强组件封装与状态同步能力
17 KiB
项目长期记忆(tms-erp-web-ws / Saber3)
后端:/Users/gxwebsoft/JAVA/tms-api(tms-erp-api-ws 滞后,别看错仓库)。
列表 / 表单 / 详情三态同组件分流
/xxx、/xxx/form、/xxx/detail 指向同一 .vue;form/detail 在 src/router/views/index.js 是 component: Layout + 空 children.path + meta:{keepAlive:false}(detail 另带 activeMenu)。组件内按 $route.path 分流,必须 watch:{$route} 重新 init;标签标题取 query.name;独立页容器固定 <div v-if>,不用 el-dialog。
⚠️ keep-alive 三坑
背景(router/tab.js + page/index/index.vue):按 fullPath 建 wrapper,<keep-alive :include="tagsKeep"> 缓存,每标签一实例、永不销毁,deactivate 后仍随全局 $route 重渲染。
坑一:独立页容器禁用 <component :is> 在 div / el-dialog 间切换。 append-to-body 走 Teleport,TeleportImpl.move 忽略 moveType,不会把 DOM 搬回缓存容器 → 切菜单冒出旧弹窗。修法:固定 <div v-if> + deactivated(){closeInnerDialogs()} + beforeUnmount()(写在子组件也生效)。已修 loading-manage / project-apply / waybill-manage-page;未修 settlement/components/{pre,formal}-settlement-editor.vue、transport-reconciliation-editor.vue、waybill-import-dialog.vue。
坑二:宿主 view 直绑全局 $route.query.* 给子组件 prop。 缓存实例被灌入当前页 id → 拿错 id 查后端(批量弹「运单管理不存在」),live computed 还会误 push 标签。修法:routePathLocked 锁路径 + watcher / push 前判 isOwnedRouteActive + 宿主绑定加路径守卫。⚠️ routePathLocked 必须放 data()(Options API 顺序 data → computed → watch(immediate) → created;放 created 会读到空值、挡掉首次加载)。已修 waybill-manage-page / transport-plan-page + 三个宿主 view。
坑四:avue-crud 弹窗 append-to-body 默认 true(dialog-form3.js:validData(tableOption.dialogAppendToBody, true)),DOM Teleport 到 body。 于是页面里以页面容器为祖先的 scoped 选择器(.xxx-page :deep(.yyy))命中不到弹窗内容,尤其 el-cascader(inline-block、宽度按内容收缩,不像 el-input/el-autocomplete 有 --el-input-width:100% 兜底)会明显比同行 input 窄。修法:option 里配 dialogCustomClass: 'xxx-dialog',规则写到 src/styles/element-ui.scss 的 .xxx-dialog ...(项目既有约定,10+ 处同款);或把基准类放在 Teleport 内部元素上(如 ship.vue 的 .ship-form 挂在 el-form 上,自身带 scope 属性,照样生效)。
坑五:avue 表单项控件不是 .el-form-item__content 的直接子级。 form/index3.js 渲染为 content > div > [div|elTooltip] > 控件,所以 .el-form-item__content > .el-cascader 这类直接子级选择器对 avue 表单全部失效(手写 <el-form-item> 才生效),一律用后代选择器。
坑六:labelWidth: 'auto' 在 Element Plus 里逐项计算,不是全表单共享。 实测同一弹窗:3 字 54/64px、2 字 40px、4 字 68px、4 字带必填星号 78px;label 越宽 → 控件可用宽度越小,同级控件宽度就不一致(车牌号 input 486 vs 使用部门 cascader 472)。要让同一行控件等宽必须统一 label 宽度:.xxx-dialog .el-form-item__label { flex: 0 0 calc(Nem + 24px) !important; width: calc(Nem + 24px) !important }(N=本列最长汉字数,与 ship.vue、.dialog-form--carded 同款)。
验证 avue 弹窗 DOM/宽度的可复用办法: 项目根建临时 html(Vite 直接服务)→ 引真实 option 与 src/styles/element-ui.scss,挂 avue-crud 并 mounted(){ this.$refs.crud.rowAdd() } 弹出真弹窗;用 agent-browser(/Users/gxwebsoft/.workbuddy/binaries/node/workspace/node_modules/.bin/agent-browser)open + eval 读 DOM 链路与 getBoundingClientRect,用完删文件。
坑三:路由表重复定义 → 内容区整页空白(2026-09-18)。 /business/contract-manage/detail 在 router/views/index.js 定义两次、子路由同名 合同详情。重名 addRoute 会 removeRoute(name) 移除旧子记录,但不从父记录 children 里摘除,第一条 Layout 壳记录留在 matcher 且靠前 → resolve() 命中它 → $route.matched.length === 1 → 二级 router-view 无匹配 → 侧栏/标签正常但内容空白。修法:删重复定义。排查:扫「顶层 path + 首个子路由 name」重复项。
列表页右上角按钮组(table-header-tools 标准件)
⚠️ 2026-10-14 修正:最终方向是手写列表页改用 avue-crud。用户要求与其它模块"完全一致",而 avue 的列显隐抽屉(el-drawer+表格,隐藏/冻结/过滤/排序,行拖拽)是 avue-crud 内部子组件(inject: ['crud']),脱离 avue-crud 拿不出来;第 4 键也是 avue 的"表格↔卡片视图"切换。所以 table-header-tools 只是过渡仿制件,新页面不要再接,直接写 avue-crud。
手写 el-table 列表页若暂时保留,右上角统一用 src/components/table-header-tools/main.vue,复刻 avue-crud 默认 4 键:刷新 el-icon-refresh / 列设置 el-icon-operation / 搜索显隐 el-icon-search / 表格尺寸 el-icon-grid(default→large→small 循环)。
- 接入范式:
<table-header-tools :columns="columns" v-model:visible-column-keys="visibleColumnKeys" v-model:table-size="tableSize" v-model:search-visible="searchVisible" @refresh="loadTable" />;配套:搜索区v-if="searchVisible"、el-table:size="tableSize"、列用 computedvisibleColumns = columns.filter(c => visibleColumnKeys.includes(c.prop))。⚠️ 不能v-for+v-if同元素(Vue3 v-if 优先级更高,取不到 column 变量)。 - 表格在子组件时(如
payment/components/bill-*-table.vue):子组件加 propssearchVisible+ emitsrefresh/update:searchVisible,父页面v-model:search-visible+@refresh,搜索子组件加v-if="searchVisible"。 icon="el-icon-xxx"字符串依赖 Avue 的全局 el-icon 映射(main.js只注册 EP 的 PascalCase 图标),临时验证页没app.use(Avue)会渲染成空圆按钮 —— 别误判成组件 bug。- 组件已加
deactivated()关列设置弹窗(防 keep-alive 下 Teleport 弹窗残留)。 - 已接入(过渡态,待逐页改 avue):business/{master-order,loading-manage,voucher-manage}、settlement、payment 模块手写页。
avue-crud 列表页改造要点(payment 模块样板,2026-10-14)
- 列显隐抽屉:点第 2 键弹出,标题"列显隐"(语言包
crud.showTitle),表头 列名/隐藏/冻结/过滤/排序。直接读写 avue crud 的列配置,页面无需实现。 - 第 4 键:
handleGridShow()= 表格 ↔ 卡片视图(tableCard按列通用渲染),页面无需写卡片模板。 searchOrder是降序排序(值越大越靠前):list.sort((a,b) => (b.searchOrder||0) - (a.searchOrder||0))。首次改造踩过坑。searchIndex: 4+searchIcon: true→ 默认只显示前 4 个搜索项 + "展开/收起"(columnLen > searchIndex 时才出现该按钮)。- 搜索项 label 与表格列 label 不同(列「收款方」/搜索「客户名称」):单独定义 hide 列 +
searchProp指向后端字段(avue 在 search-change 里自动改名)。 - 搜索专用列加
hide: true+showColumn: false,否则会混进列显隐抽屉。 - ⚠️
createTime/updateTime必须显式给width: 180(全站统一,参考option/base/common-address.js)。不写宽度的列参与"剩余空间均分",容易被压到 80px 出现2026-0...截断 —— 这是列缺 width,不是操作列没固定;menuFixed: 'right'各页早已配好(main.js全局crudOption.menuWidth: 180,avue 默认 220),别用"加宽操作列"来处理时间列截断。创建人/更新人给minWidth: 110。 - ⚠️ column 数组顺序 = 表格列顺序:搜索专用列别插在表格列中间(会把表格列顶到前面);deptName 这种"既搜索又展示"的列要放回表格列的正确位置。
- 分页:
page: { currentPage, pageSize, pageSizes, total };on-load由 avue 内部(table-page)在 created / 翻页 / 改页容量时触发,current-change/size-change里只改 page 不要再调 onLoad(否则双请求)。 onLoad(page)只用this.query(avue 传的 page 对象含 total/pageSizes 等分页字段,别 spread 进查询参数)。- 搜索参数转换放
searchChange(params, done):日期区间数组拆成xxxStartDate/xxxEndDate;所属组织级联用normalizeOrganizationSearch(params, 'deptName')(见mixins/organization-search.js,option 里该列加searchslot: true)。 - 单元格空值统一
-:列级formatter: (row, value) => ...;金额Number(v||0).toFixed(2);状态/链接用slot: true+#prop插槽。 - 操作列用
#menu插槽(menuWidth: 320),addBtn/viewBtn/delBtn/editBtn: false关掉 avue 自带按钮。
地址/行政区划展示(全站既有规则)
「省/市/区县」都要空格分段,但省与自治区不重复展示:上海市浦东新区→上海市 浦东新区,广西壮族自治区南宁市邕宁区→南宁市 邕宁区(同 formatRoadLocation / formatRouteCityDistrict / 总单列表 formatRoadAddress 一致)。
- 统一入口:
src/utils/region-address.js的formatRegionAddress(value)(按后缀切分行政区划,州+市、县+区连写合并;只有整串都是行政区划才重排,站点名「南宁站/广州南站」与含门牌的详细地址原样返回)。v-for 列表里用它做展示时,直接把函数挂进methods用 ES6 简写(formatRegionAddress,)。 - ⚠️ 路线节点的名称字段来自后端
routeJson:routes[i].departureName= 第 i 段到货地/途经地(不是本段发货地),末段 = 最终收货地;master.departureName/arrivalName才是总发货/总收货。总单调度/详情都按「上一个节点的 departureName」拼分段起点。
样式硬规则
- 底栏
flex; justify-content:flex-end,不写 gap(靠 EP 默认.el-button+.el-button{margin-left:12px});浮动底栏必须position:fixed(祖先 overflow:hidden 使 sticky 失效)+:global(.avue--collapse .x){left:60px}、:global(.avue-layout--horizontal .x){left:0},:global()要包整个选择器。 - 按钮层级:次要=无 type;中间步骤=
primary plain;主操作=primary(禁绿、禁两个蓝实心)。顺序[辅助][取消][保存草稿][提交]。 - 独立页标题
.archive-page-form__title;分组用全局<section-card>;上传证件区width:100%; max-width:240px+.el-upload{height:151px}。 - ⚠️
<style scoped lang="scss">顶层禁//注释(Vite5 + sassUnexpected '/'→ 500),顶层用/* */。其余见src/styles/element-ui.scss与AGENTS.md4.4。 - 搜索栏「展开/收起」唯一标准写法(settlement 5 处已用、avue 页面同款):
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">{{ searchExpanded ? '收起' : '展开' }}</el-button>放「查询/重置」之后(最右);禁el-tooltip包图标按钮、禁文案「折叠」。ArrowUp/ArrowDown要 import 并放data()(:icon是表达式)。 显示条件与 avue 一致(header-search3.js的isSearchIcon = searchIcon===true && columnLen > searchIndex):搜索项 ≤4 项不渲染按钮 —— 故 data 里维护searchIndex: 4+searchItemCount: N,用 computedshowSearchToggle控制。搜索栏操作区gap: 8px各模块一致,不要去改。
表单 / 自检 / 导出
- placeholder 只写
请输入/请选择;例外:请输入或选择车辆、日期区间起/止、示例值。校验 message 须带字段名。 business-crud-page.vue:option 经cloneOption克隆,独立表单页走PageAvueForm。- 自检:
curl "localhost:2888/src/xxx.vue"与"...?vue&type=style&index=0&scoped=true&lang.scss"(200 / 500)。vite build在facelogin.vue被沙箱中断,不能作唯一校验。devVITE_APP_API=/api代理172.16.203.228:8000。 - 导出:前端
exportColumns后端未用,以XxxExportExcel.java为准;BladeXBeanUtil类型不兼容静默跳过(Date→LocalDateTime丢值,用DateUtil.fromDate)。
avue-crud 才是「右侧 4 键」的唯一来源(2026-10-20 定论)
用户要的右侧工具栏是 avue-crud 原生:刷新 / 列显隐(el-drawer,含 隐藏/冻结/过滤/排序 + 行拖拽)/ 搜索显隐 / 显示方式(表格↔卡片)。
- 源码:
node_modules/@smallwei/avue/lib/packages/element-plus/crud/menu/header-menu3.js(refreshBtn/columnBtn/searchBtn/gridBtn),gridBtn→handleGridShow()切gridShow;图标默认在同目录config.js。「列显隐」文案来自 avue 语言包crud.showTitle(项目src/lang/zh.js已覆盖)。 - 列显隐抽屉依赖
inject: ['crud'],脱离 avue-crud 拿不出来;components/table-header-tools的「列设置勾选弹窗 + 表格尺寸循环」是仿制件,做不到这两点。所以「要和 avue 一致」= 整页改用 avue-crud,没有中间方案。 - ⚠️ 搜索自定义控件用
column.renderSearch = scope => h(...)(scope.row可用,同mixins/organization-search.js)。不要用searchslot:avue 3.9.2 全库无此字段,且header-search2.js的searchXxx → xxx映射会把searchslot变成slot: true。 - avue-crud 上的
${prop}-search插槽确实存在(index2.js的getSlotList(["-search"])),但作用域没有row(只有 value/column/label/size/readonly/disabled/dic)——mileage-record.vue里{ row }是错的写法,别照抄。 formatter签名:detail.js→column.formatter(row, row[column.prop], result, column)。on-load由table-page2.js在 created / 翻页 / 改页容量时触发,参数是内部defaultPage(已合并外部 page)。分页若由父组件持有、子组件包一层 avue-crud,用 computedcrudPage{get/set}+ setter 里Object.assign(this.page, value)原地回写。- 已改:settlement-import.vue、formal-settlement.vue + components/formal-settlement-table-panel.vue(receipt/payment 共用)。未改:pre-settlement、settlement-adjustment、receivable-payable-detail、transport-reconciliation-table-panel、business 与 payment 的手写页。
- ⚠️
src/option/settlement/formalSettlementTable.js别删:formal-settlement-editor.vue还依赖它的 candidate/detail/source/summary 列。 - 沙箱里 curl 打不通本地 dev server(一律 502)。改用
vite.createServer({middlewareMode:true})+server.transformRequest(path)就地转换校验,还能暴露依赖解析失败。
Element Plus 图标两种用法的机制差异(易误判)
main.js 里 for (...ElementPlusIconsVue) app.component(key, component) 全局注册了全部图标,因此模板里可以写标签形式 <Location />(靠全局注册解析)。
但 :icon="OfficeBuilding" 是表达式,编译成 _ctx.OfficeBuilding,只会从 data / props / computed / setup 返回值里取 —— 组件对象必须放进 data();放进 components: {} 注册里模板取不到,会静默渲染不出图标(不报错,只是空白)。
项目既有惯例:需要 :icon 的图标一律 import 后在 data() 里 return { Xxx }(transport-plan-page、waybill-manage-page、business-crud-page 都是这么写的)。
注:标签形式(如 <Location />)不依赖 data,放不放 data 都能显示;仅 :icon 必须放 data。
全局 showOverflowTooltip: true 与自定义多行单元格(2026-10-14 踩)
main.js 的 app.use(ElementPlus, { table: { showOverflowTooltip: true } })(+ Avue crudOption.showOverflowTooltip: true)→ 全站所有表格列默认开启 hover 提示,EP 会给该列 .cell 加 .el-tooltip 类(white-space: nowrap + 固定宽度 + 溢出省略)。
后果:列里放自定义多行 / 分组内容时,内层文本继承 nowrap → 横向溢出 → 单元格被裁切 + hover 弹提示(极易被误判成"组件 bug")。
修法(列级豁免,不动全局):① 列上显式 :show-overflow-tooltip="false";② 内层容器补 white-space: normal 兜底。两处都写最稳。
☑️ 已用于路线变更「变更内容」列(loading-manage.vue / components/waybill-manage-page.vue / components/business-crud-page.vue 三处)。
另:styles/element-ui.scss 里已有两条基于 .cell.el-tooltip 的既有豁免规则(操作列 :has(.el-link)、class-name="is-ellipsis-link"),改表格列时先看这两条。