Compare commits

..

2 Commits

Author SHA1 Message Date
e683c2cc00 style(pagination): 优化分页组件上一页/下一页样式及环境配置说明
- 取消分页组件中上一页、下一页按钮的额外宽度设置,使其与页码按钮等宽
- 恢复上一页、下一页的原生箭头图标显示,取消自定义文字和隐藏图标的样式规则
- 更新分页相关的注释,明确首页/尾页按钮依旧由脚本控制不注入,上一页/下一页显示原生箭头
- 修改 .env.development 配置,增加 API 代理和跨域说明,避免直接使用绝对地址引发的 CORS 问题
- 优化 VITE_APP_API 配置为 /api,配合 Vite 代理减少跨域错误,保障开发环境稳定运行
2026-08-05 14:35:36 +08:00
3268f12501 style(pagination): 优化分页组件上一页/下一页样式及环境配置说明
- 取消分页组件中上一页、下一页按钮的额外宽度设置,使其与页码按钮等宽
- 恢复上一页、下一页的原生箭头图标显示,取消自定义文字和隐藏图标的样式规则
- 更新分页相关的注释,明确首页/尾页按钮依旧由脚本控制不注入,上一页/下一页显示原生箭头
- 修改 .env.development 配置,增加 API 代理和跨域说明,避免直接使用绝对地址引发的 CORS 问题
- 优化 VITE_APP_API 配置为 /api,配合 Vite 代理减少跨域错误,保障开发环境稳定运行
2026-08-05 14:35:30 +08:00
7 changed files with 261 additions and 27 deletions

View File

@@ -3,6 +3,8 @@
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
#接口地址 #接口地址
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝)
VITE_APP_API=/api VITE_APP_API=/api
#调试参数 #调试参数

View File

@@ -0,0 +1,18 @@
# 2026-08-04 工作记录
## 撰写《AI 辅助编程规范与实战示例》文档
- 应业主想了解"如何用 AI 编程"的需求,以 `src/views/base/cargo-type.vue`(货物类型模块)为实例,撰写对外说明文档。
- 产出文件:`/Users/gxwebsoft/VUE/tms-erp-web-ws/AI辅助编程规范与实战示例.md`
- 文档结构:协作流程总览 → AGENTS.md 规范条款 → cargo-type 模块实例拆解(三级文件分离、校验三处一致、审计字段用姓名、导入失败明细加工、样式规范)→ 人机协作人工兜底项 → 5 条经验 → 给业主的价值建议。
- 文档引用了真实代码片段cargo-type.vue / option / api / utils/import-excel.js强调需求卡 + AGENTS.md + 复用 utils 三件套作为"单一事实来源"。
## 生成 Word(.docx) 版本
- 用 docx-jsmanaged node workspace将《AI 辅助编程规范与实战示例.md》转成 Word 文档:`/Users/gxwebsoft/VUE/tms-erp-web-ws/AI辅助编程规范与实战示例.docx`
- 踩坑docx-js 会在段落间注入非法空节点 `<0/>``<N/>`),导致 XML 不合法;后处理用正则剔除 `<\d+/>` 后重新校验通过。
- 生成脚本:`/Users/gxwebsoft/.workbuddy/binaries/node/workspace/gen_doc.js`(含标题/表格/代码块/项目符号样式)。
## 分页组件:上一页/下一页改为原生箭头图标
- 需求:全站不隐藏上一页/下一页,改显示原生箭头图标;首页/尾页保持不注入。
- 改动文件:`src/styles/element-ui.scss`3 处历史遗留规则)、`src/utils/pagination.js`(注释同步)。
- 删除的遗留规则:①隐藏原生 `.el-icon` 箭头;②用 `::before` 注入「上一页/下一页」文字;③整体 `display:none` 隐藏 `.btn-prev/.btn-next`。首页/尾页仍由 pagination.js 增强器不注入,且各页面 layout 无 first/last。
- 效果:上一页/下一页显示 Element Plus 原生箭头(‹ 带边框与页码按钮视觉统一hover 变主色,禁用态灰显。

View File

@@ -0,0 +1,12 @@
# 2026-08-05 工作记录
## 排查:改线上接口后前端"运行不了"CORS 根因)
- 现象:用户将 `.env.development``VITE_APP_API` 改为 `http://172.16.203.228:8000/api`(直连线上/测试后端)后仍运行不了。
- 根因Saber3 的 axios 配了 `withCredentials = true``src/axios.js:79`。该后端openresty + blade-gateway对跨域请求返回 `Access-Control-Allow-Origin: *``Access-Control-Allow-Credentials: true`,浏览器规定 credentials 模式下 Allow-Origin 不能是 `*`,非法组合会被直接拒绝 → 所有接口失败。
- 之前用 `/api` 能跑是因为走 Vite 代理,浏览器请求同源 localhost:2888不触发跨域。
- 已改动:
1. `.env.development``VITE_APP_API` 改回 `/api`,并加注释说明两种模式。
2. `vite.config.mjs``server.proxy['/api']` target 改为 `http://172.16.203.228:8000`,并去掉 `rewrite` 去掉 `/api` 的逻辑(保留 `/api` 前缀,对齐 openresty 期望路径)。
- 注意:改 `.env` / `vite.config.mjs` 后必须重启 `npm run dev`Vite 启动时加载环境变量,运行中改动不会热更新)。
- 若确需直连绝对地址(如生产/部署):必须让后端 CORS 把 Allow-Origin 改为具体前端域名blade-gateway 的 CorsConfig 用 `setAllowedOriginPatterns` 或显式域名),不可用 `*` 配合 credentials。

Binary file not shown.

View File

@@ -0,0 +1,225 @@
# AI 辅助编程规范与实战示例——以「货物类型模块」为例
> 文档目的:向业主说明我们团队如何用 AI 协作完成前端开发,包含协作流程、工程规范、以及一个可直接对照代码的真实模块示例,便于业主了解我们的工作方式、质量保障手段与可复用的经验。
---
## 一、背景:我们为什么用 AI 编程
本项目Saber3 企业级后台)是一个模块多、表单多、校验规则多、样式规范严苛的大型 Vue 3 工程。传统人工开发存在两个痛点:
1. **重复劳动多**:每个 CRUD 模块的列表、搜索、新增、编辑、导入、导出结构高度相似,但样式细节(表格线色、偶数行底色、操作列宽度、弹窗竖条等)又极易被写错。
2. **规范易漂移**:数十个模块由不同人开发,若没有统一约束,代码风格、校验口径、审计字段展示会逐渐不一致。
我们引入 AI 编程的核心思路不是"让 AI 自由发挥",而是**把人类沉淀的工程规范喂给 AI让 AI 在规范的边界内批量、稳定地产出代码,再由人工做业务与视觉复核**。这样既提效,又保住质量底线。
---
## 二、我们的 AI 协作流程(总览)
```
需求澄清 ──> 需求卡(单一事实来源)──> AGENTS.md工程规范──> AI 生成代码
│ │
└──────────────── 人工复核 / 验收 ──────────────────────────┘
```
四个关键阶段:
| 阶段 | 产物 | 负责人 | 说明 |
| ------- | --------------------------- | ------- | -------------------------------- |
| 1. 需求澄清 | 模块需求卡(.md | 产品/前端 | 用 `模块需求卡生成规范.md` 统一格式,拆成 REQ 编号项 |
| 2. 规范约束 | AGENTS.md | 架构/前端 | 把技术栈、风格、样式、校验、审计等写成"对 AI 的强制条款" |
| 3. 代码生成 | view / option / api / utils | AI + 前端 | AI 按 AGENTS.md 生成,前端实时纠偏 |
| 4. 复核验收 | 可运行模块 | 前端/测试 | 校验业务规则、视觉规范、边界异常 |
**核心原则:需求卡和 AGENTS.md 是 AI 与人对齐的"单一事实来源",不靠口头交代。**
---
## 三、工程规范如何约束 AIAGENTS.md 核心条款节选)
我们维护了一份 `AGENTS.md`,作为 AI 每次动手前的"必读规范"。与本示例强相关的条款有:
### 3.1 文件同构结构
```
src/
├── views/base/cargo-type.vue # 页面(模板 + 交互逻辑)
├── option/base/cargo-type.js # Avue 表格/表单配置(与 views 同构路径)
└── api/base/cargo-type.js # 接口层(按业务模块组织)
```
> 规范要点Avue 配置独立存放于 `src/option/`,与 `views` 和 `api` 保持同构的模块路径映射。AI 生成时不会把配置写死在页面里。
### 3.2 样式规范(极易被写错,故用规范强制)
- 表格线条颜色统一 `#EFF1F7`;偶数行背景 `#FAFAFA`,固定列须与行背景一致;
- 操作列同时展示"查看/编辑/删除"时宽度不小于 `220px`
- `.avue-crud__header` 顶部间距 `12px`
- `.basic-container__card` 内层 `el-card` 去卡片化(`padding: 0`
- 主色统一 `#409eff`
### 3.3 表单校验规范
- 金额类非负、经纬度范围、备注类 ≤200 字(前端阻止提交并提示);
- 新增、导入、编辑三处的校验口径必须一致;
- 审计字段(创建人/更新人)**禁止直接展示用户 ID**,须绑定后端返回的姓名字段(`createUserName`)。
### 3.4 批量导入失败明细规范(本项目重点)
- 部分数据导入失败时,前端必须自动下载后端返回的失败 Excel
- 失败 Excel 字段顺序须与原模板一致,最后追加"导入失败原因"列;
- 前端需根据 `content-type` 区分"JSON 成功响应"与"Excel 文件流"
- 失败明细文件命名含业务名、导入失败明细、时间戳。
---
## 四、实例拆解:货物类型模块
### 4.1 模块能力一览
货物类型是一个**两级层级**的主数据模块一级货物类型2 位编码)+ 二级货物类型4 位编码,前 2 位须与上级一致)。功能包括:列表、条件查询、新增、编辑、查看、删除、批量删除、**批量导入 + 模板下载 + 失败明细加工**、批量导出。
### 4.2 文件同构结构(真实存在)
```
src/views/base/cargo-type.vue # 589 行,页面 + 交互
src/option/base/cargo-type.js # 189 行Avue 列配置 + 校验器
src/api/base/cargo-type.js # 74 行7 个接口方法
src/utils/import-excel.js # 109 行,通用导入弹窗(被本模块复用)
```
### 4.3 AI 按规范落地的关键代码点
下面每一条都对应前述规范条款,可直接对照源码。
**① 三级文件分离(对应 3.1**
页面只负责交互,配置与接口独立:
```js
// src/views/base/cargo-type.vue
import { getList, getNextCode, getParentOptions, remove, submit } from '@/api/base/cargo-type';
import { getCargoTypeOption } from '@/option/base/cargo-type';
```
**② 表单校验三处一致(对应 3.3**
前端表单option与 JS 内 `validateRow` 共用同一套规则口径,避免"表单能过、导入却崩"
```js
// src/views/base/cargo-type.vue —— 提交前的统一校验
validateRow(row) {
const pattern = row.typeLevel === 1 ? /^\d{2}$/ : /^\d{4}$/;
if (!pattern.test(row.cargoCode)) { this.$message.warning('货物类型编码格式不正确'); return false; }
if (row.typeLevel === 2 && !row.cargoCode.startsWith(row.parentCargoCode)) {
this.$message.warning('二级编码前2位必须与上级货物类型编码一致'); return false;
}
if (row.remark.length > 200) { this.$message.warning('备注不能超过200字'); return false; }
return true;
}
```
```js
// src/option/base/cargo-type.js —— 弹窗表单的同源校验器
{ validator: (rule, value, callback) => {
const form = ctx.form || {};
const pattern = form.typeLevel === 1 ? /^\d{2}$/ : /^\d{4}$/;
// 同样的规则:格式 + 二级前缀一致性
}, trigger: 'blur' }
```
**③ 审计字段用姓名不用 ID对应 3.3**
```vue
<template #createUserName="{ row }">
{{ row.createUserName || row.createUser || '' }}
</template>
```
列表直接绑定 `createUserName`,避免出现一串无意义的用户 ID。
**④ 导入失败明细加工(对应 3.4,本项目 AI 协作的亮点)**
后端只返回原始失败原因文本AI 被要求生成一个**可独立测试的小函数** `decorateCargoTypeFailDetail`,把原因按 A./B./C. 编号、追加规则说明、仅"导入失败原因"列标红、自动加宽列:
```js
// src/views/base/cargo-type.vue
export const decorateCargoTypeFailDetail = async workbook => {
// 1. 映射表头列号
// 2. 清理非原因列的多余红色字体
// 3. 把每行原因拆条、编号、拼接规则说明
// 4. 仅操作"导入失败原因"列样式(黑字、无填充、自动换行)
// 5. 按 CARGO_TYPE_FAIL_COL_WIDTH 加宽各列
};
```
该函数通过通用导入工具 `import-excel.js``failDetailDecorator` 钩子注入,做到**模块零耦合复用**
```js
// 复用通用导入弹窗,仅传入本模块的加工函数
handleImport() {
openImportDialog(this, '货物类型', undefined, {
failDetailDecorator: decorateCargoTypeFailDetail,
});
}
```
**⑤ 样式规范落地(对应 3.2**
```scss
/* src/views/base/cargo-type.vue <style> */
:deep(.el-table) {
--el-table-border-color: #eff1f7; /* 表格线条统一色 */
}
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa; /* 偶数行底色,固定列同色 */
}
:deep(.basic-container__card) > .el-card__body { padding: 0; } /* 去卡片化 */
```
### 4.4 人机协作中AI 不擅长、需要人工兜底的部分
诚实地说AI 不是万能的。本模块在协作中由人工重点把控的有:
1. **业务规则澄清**:一级/二级层级关系、编码前缀约束这类"业务语义",需要产品/前端先讲清AI 才能写出正确校验。
2. **视觉细节微调**:表格行高、表头高度、间距的最终眼检,依赖人工在浏览器里确认。
3. **前后端字段契约**`CargoTypeVO` 是否返回 `createUserName`、失败 Excel 的表头文案必须与后端对齐AI 无法替你确认。
4. **破坏性操作确认**:删除/批量删除的二次确认文案与风险,由人工定稿。
---
## 五、经验沉淀(可复用的 5 条)
1. **把规范写成 `AGENTS.md`,比口头交代可靠 10 倍。** AI 每次都会读,规范不会因人离职而丢失。
2. **需求卡是 AI 与人对齐的单一事实来源。** 用统一格式REQ 编号 + 功能点/异常边界/反例)避免"我以为你要的是……"。
3. **把重复能力抽成 utils。**`import-excel.js`新模块导入直接复用AI 只需写"本模块特有的加工函数"。
4. **校验三处一致**:前端表单、前端导入、后端校验口径对齐,且一处改、三处同步改。
5. **让 AI 生成"小而纯"的函数**(如失败明细加工函数),比让它改一整个 600 行页面更可控、更易测、更易复核。
---
## 六、给业主的价值与建议
| 维度 | 传统方式 | 我们的 AI 协作方式 |
| ------- | --------- | ---------------- |
| 单模块开发周期 | 长,易返工 | 短,规范内一次成型 |
| 多模块一致性 | 易漂移 | AGENTS.md 强制统一 |
| 规范传承 | 靠老人带 | 沉淀为文件,新人/AI 即取即用 |
| 质量风险 | 校验、审计字段易漏 | 规范条款逐项约束 |
**建议**:业主要求交付的不只是代码,更是"可持续维护的规范资产"。我们交付的每个模块背后都有需求卡 + AGENTS.md + 复用 utils 三件套,后续无论是换人维护还是扩展新模块,成本都显著更低。
---
*附:本示例涉及的全部源码路径*
- `src/views/base/cargo-type.vue`
- `src/option/base/cargo-type.js`
- `src/api/base/cargo-type.js`
- `src/utils/import-excel.js`
- `AGENTS.md`(工程规范总纲)
- `模块需求卡生成规范.md`(需求卡格式规范)

View File

@@ -337,12 +337,7 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
} }
.el-pagination .btn-prev, // 上一页 / 下一页 箭头按钮与页码按钮等宽,不再单独加宽
.el-pagination .btn-next,
.erp-pagination-first,
.erp-pagination-last {
min-width: 54px;
}
.el-pagination .el-pager { .el-pagination .el-pager {
gap: 6px; gap: 6px;
@@ -350,18 +345,7 @@
padding: 0; padding: 0;
} }
.el-pagination .btn-prev .el-icon, // 上一页 / 下一页 改为原生箭头图标:不再隐藏原生图标,也不再注入「上一页 / 下一页」文字
.el-pagination .btn-next .el-icon {
display: none;
}
.el-pagination .btn-prev::before {
content: '上一页';
}
.el-pagination .btn-next::before {
content: '下一页';
}
.el-pagination.is-background .el-pager li.is-active, .el-pagination.is-background .el-pager li.is-active,
.el-pagination .el-pager li.is-active { .el-pagination .el-pager li.is-active {
@@ -447,14 +431,7 @@
color: #fff; color: #fff;
} }
// 全站统一:隐藏分页的「上一页 / 下一页」原生导航按钮 // 全站统一:分页展示「上一页 / 下一页」原生箭头图标(不再隐藏;首页 / 尾页仍不注入)
// (「首页 / 尾页」由 src/utils/pagination.js 的全局增强器统一不再注入)
.el-pagination {
.btn-prev,
.btn-next {
display: none !important;
}
}
// 全站统一:隐藏操作栏按钮前的图标 // 全站统一:隐藏操作栏按钮前的图标
.avue-crud__menu .el-button--text i, .avue-crud__menu .el-button--text i,

View File

@@ -111,7 +111,7 @@ const enhancePagination = pagination => {
normalizeComponentPageConfigs(pagination); normalizeComponentPageConfigs(pagination);
// 全站统一:不再注入「首页 / 尾页」按钮; // 全站统一:不再注入「首页 / 尾页」按钮;
// 原生「上一页 / 下一页」由全局样式src/styles/element-ui.scss统一隐藏 // 「上一页 / 下一页」统一展示为原生箭头图标(由全局样式 src/styles/element-ui.scss 控制)。
if (jumpWrapper && !pagination.querySelector(`.${JUMP_BUTTON_CLASS}`)) { if (jumpWrapper && !pagination.querySelector(`.${JUMP_BUTTON_CLASS}`)) {
const jumpButton = createPaginationButton(JUMP_BUTTON_CLASS, '跳转', () => { const jumpButton = createPaginationButton(JUMP_BUTTON_CLASS, '跳转', () => {
const { jumpInput } = getPaginationState(pagination); const { jumpInput } = getPaginationState(pagination);