Files
tms-erp-web/.workbuddy/memory/MEMORY.md
T
gxwebsoft 6de87ed4e2 style(contract): 优化合同管理页面表单样式与布局
- 调整合同管理备注输入框行数和最大长度
- 细化合同变更页面表单项间距和对齐方式
- 统一弹窗表单项的底部间距样式,改为注释禁用
- 调整 port-terminal 和 region 组件的表单项底部间距
- 修改 vite 配置中 API 请求代理地址
- 优化凭证执行详情搜索表单列宽布局
- 在运单管理页面更新型号字段为非必填,调整变更记录内容格式化显示
- 引入并使用 formatRouteChangeContent 工具函数
- 统一明细按钮显示逻辑,草稿状态也显示详情按钮
- 新增路由变更相关工具函数与样式支持
2026-10-14 12:44:56 +08:00

12 KiB
Raw Blame History

项目长期记忆(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"、列用 computed visibleColumns = columns.filter(c => visibleColumnKeys.includes(c.prop))。⚠️ 不能 v-for + v-if 同元素(Vue3 v-if 优先级更高,取不到 column 变量)。
  • 表格在子组件时(如 payment/components/bill-*-table.vue):子组件加 props searchVisible + emits refresh/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,否则会混进列显隐抽屉。
  • ⚠️ 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 + sass Unexpected '/' → 500),顶层用 /* */。其余见 src/styles/element-ui.scss 与 AGENTS.md 4.4。

表单 / 自检 / 导出

  • placeholder 只写 请输入/请选择;例外:请输入或选择车辆、日期区间 起/止、示例值。校验 message 须带字段名。
  • business-crud-page.vue:option 经 cloneOption 克隆,独立表单页走 PageAvueForm。
  • 自检:curl "localhost:2889/src/xxx.vue" 与 "...?vue&type=style&index=0&scoped=true&lang.scss"(200 / 500)。vite build 在 facelogin.vue 被沙箱中断,不能作唯一校验。dev VITE_APP_API=/api 代理 172.16.203.228:8000。
  • 导出:前端 exportColumns 后端未用,以 XxxExportExcel.java 为准;BladeX BeanUtil 类型不兼容静默跳过(Date → LocalDateTime 丢值,用 DateUtil.fromDate)。

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。