feat: 劳务协议编辑改独立页 + 去 el-card 圆角框
- 新增 LaborProtocolEdit.vue (独立页, 新建/编辑复用, 跟 ExpertNew.vue 风格一致)
- 加路由 /admin/labor-protocol/new 和 /admin/labor-protocol/edit/:id
- LaborProtocol.vue 删 el-dialog + 改为 router.push 跳转
- 新增 .form-card.nested { border: none; box-shadow: none; } 去 el-card 圆角框
- 新增 _self/admin_labor_protocol.md 审查报告
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
# /admin/labor-protocol 页面端到端审查
|
||||
|
||||
**审查日期**: 2026-08-19
|
||||
**审查范围**: 前端 → 后端 → DB → 原型
|
||||
**审查者**: Claude Code (page-tech-review skill)
|
||||
|
||||
---
|
||||
|
||||
## 0. 组件定位 (四跳链)
|
||||
|
||||
| 跳 | 命中 |
|
||||
|---|---|
|
||||
| ① 角色菜单 | `AdminLayout.vue:86` `MENU.admin[网站管理]` → `{ path: '/admin/labor-protocol', title: '劳务协议配置', icon: Document }` |
|
||||
| ② 路由 | `router/index.js:50` → `name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue')` |
|
||||
| ③ 组件 | `ry-vue3/src/views/admin/LaborProtocol.vue` |
|
||||
| ④ 原型 | **❌ 无对应原型** (proto/html/components/ 中无 "劳务协议配置" 页面, 最近的是 `admin-settings.html` 是系统设置, 不同业务) |
|
||||
|
||||
**注意**: 这是新建页面, **没有原型图可对比**。从 `HeguiConstants.java` 的 3 个硬编码 HTML 字符串 (`DEFAULT_PROTOCOL` / `BBS_PROTOCOL` / `JIANYAN_PROTOCOL`) 迁移而来。
|
||||
|
||||
---
|
||||
|
||||
## 1. 主要功能 + 可见性
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||
|---|---|---|---|
|
||||
| 列表 (分页) | `LaborProtocol.vue:25-37` | `GET /business/laborProtocolTemplate/list` | `biz_labor_protocol_template` |
|
||||
| 筛选 (按模板名/状态) | `LaborProtocol.vue:8-19` | 同 list (mapper `<if>` 过滤) | `biz_labor_protocol_template` |
|
||||
| 全部启用 (下拉) | (无 UI, 接口预留) | `GET /business/laborProtocolTemplate/allEnabled` | 同 |
|
||||
| 默认模板 (单条) | (无 UI, 接口预留) | `GET /business/laborProtocolTemplate/default` | 同 |
|
||||
| 详情 | (无 UI, 接口预留) | `GET /business/laborProtocolTemplate/{id}` | 同 |
|
||||
| 新建 | `LaborProtocol.vue:140-152` | `POST /business/laborProtocolTemplate` | 同 |
|
||||
| 编辑 | 同上 (dialog 复用) | `PUT /business/laborProtocolTemplate` | 同 |
|
||||
| 删除 (批量/ 单条) | `LaborProtocol.vue:160-167` | `DELETE /business/laborProtocolTemplate/{id}` | 同 |
|
||||
| 设为默认 | `LaborProtocol.vue:153-158` | `PUT /business/laborProtocolTemplate/{id}/default` | 同 (事务保证全局唯一) |
|
||||
|
||||
### 1.2 可见性 (三层过滤)
|
||||
|
||||
| 层 | 来源 | 校验字段 |
|
||||
|---|---|---|
|
||||
| 前端菜单 | `AdminLayout.vue:86` | 仅 `admin` 角色可见 |
|
||||
| 路由守卫 | `router/index.js:23` `meta: { role: 'admin' }` | admin token 校验 |
|
||||
| 后端 | 无强制 role 校验 (依赖 Spring Security 注解 `@PreAuthorize` 缺失) | ⚠️ **任何登录用户都能调** |
|
||||
|
||||
**不能看到本页面的人群**: leader / manager / doctor / executor / sponsor — 他们各自走自己的角色页面。
|
||||
|
||||
**数据隔离**: ⚠️ **没有按角色二次校验** — `BizLaborProtocolTemplateController` 没有 `@PreAuthorize("hasRole('admin')")`, 任何已登录用户都能调这 8 个接口。这是设计疏忽。
|
||||
|
||||
---
|
||||
|
||||
## 2. 筛选项 (filter-form)
|
||||
|
||||
`LaborProtocol.vue:8-19`:
|
||||
|
||||
| UI label | 控件 | 绑定字段 | 后端 SQL (`selectList`) | 命中表.字段 | 选项来源 | 原型对照 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 模板名 | `el-input` | `q.templateName` | `<if test="templateName != null and templateName != ''">and template_name like concat('%', #{templateName}, '%')</if>` | `biz_labor_protocol_template.template_name` (varchar100) | — | N/A (无原型) |
|
||||
| 状态 | `el-select` (Y=启用 / N=禁用) | `q.status` | `<if test="status != null and status != ''">and status = #{status}</if>` | `biz_labor_protocol_template.status` (char(1)) | **写死** (硬编码 Y/N) | N/A |
|
||||
| 查询/重置/新建 按钮 | — | — | — | — | — | N/A |
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具栏按钮 (toolbar)
|
||||
|
||||
`LaborProtocol.vue:21-24` + dialog 操作:
|
||||
|
||||
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|
||||
|---|---|---|---|---|
|
||||
| 查询 | 触发表单 submit | (前端) | — | N/A |
|
||||
| 重置 | 清 q, 重 load | (前端) | — | N/A |
|
||||
| 新建模板 | 打开 dialog | `POST /business/laborProtocolTemplate` | `biz_labor_protocol_template` | N/A |
|
||||
| 编辑 (行内) | `openEdit(row)` 填表单 | `PUT /business/laborProtocolTemplate` | 同 | N/A |
|
||||
| 设为默认 (行内) | `setDefault(row)` | `PUT /business/laborProtocolTemplate/{id}/default` | 同 (事务, 清空其他 default_flag) | N/A |
|
||||
| 删除 (行内) | `removeRow(row)` | `DELETE /business/laborProtocolTemplate/{id}` | 同 | N/A |
|
||||
|
||||
---
|
||||
|
||||
## 4. 表格 (el-table)
|
||||
|
||||
`LaborProtocol.vue:28-50`
|
||||
|
||||
### 4.1 数据来源
|
||||
|
||||
`BizLaborProtocolTemplateMapper.xml` `selectList`:
|
||||
|
||||
```sql
|
||||
select id, template_name, template_content, default_flag, sort_order, status, remark,
|
||||
create_by, create_time, update_by, update_time
|
||||
from biz_labor_protocol_template
|
||||
[where name like concat('%', #{templateName}, '%')] -- 模板名模糊匹配
|
||||
[and status = #{status}] -- 状态精确匹配
|
||||
[and default_flag = #{defaultFlag}] -- 默认标记精确匹配 (预留,前端未用)
|
||||
order by sort_order asc, id asc
|
||||
```
|
||||
|
||||
走 `startPage() + selectList()` 走 RuoYi PageHelper 分页。
|
||||
|
||||
### 4.2 列映射
|
||||
|
||||
| 列名 | `row.*` | DB 表.字段 | 原型对照 |
|
||||
|---|---|---|---|
|
||||
| ID | `row.id` | `biz_labor_protocol_template.id` | N/A |
|
||||
| 模板名 | `row.templateName` | `.template_name` (varchar100) | N/A |
|
||||
| 排序 | `row.sortOrder` | `.sort_order` (int) | N/A |
|
||||
| 默认 (默认 | `row.defaultFlag === 'Y'` | ` `) → el-tag success | `.default_flag` (char(1)) | N/A |
|
||||
| 状态 (状态 | `row.status === 'Y'` | ` ? 'success' : 'info'` | `.status` (char(1)) | N/A |
|
||||
| 备注 | `row.remark` | `.remark` (varchar500) | N/A |
|
||||
| 更新时间 | `row.updateTime` | `.update_time` (datetime) | N/A |
|
||||
| 操作 | — | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 5. 行内操作按钮
|
||||
|
||||
`LaborProtocol.vue:43-48`:
|
||||
|
||||
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 |
|
||||
|---|---|---|---|---|
|
||||
| 编辑 | `openEdit(row)` | `PUT /business/laborProtocolTemplate` | `service.update(entity)` → `mapper.updateByPrimaryKey` | `.template_name/.template_content/.sort_order/.status/.remark/.default_flag` |
|
||||
| 设为默认 | `setDefault(row)` | `PUT /business/laborProtocolTemplate/{id}/default` | `service.setDefault(id)` → `@Transactional`: `mapper.clearAllDefault()` 全部置 N → `mapper.updateByPrimaryKey` 目标置 Y | `.default_flag` (整张表) |
|
||||
| 删除 | `removeRow(row)` | `DELETE /business/laborProtocolTemplate/{id}` | `service.deleteByPrimaryKeys([id])` → `mapper.deleteByPrimaryKeys` | 整行 |
|
||||
|
||||
**真实落点**:
|
||||
- 设为默认:**事务保证全局唯一** (`@Transactional` 在 `BizLaborProtocolTemplateServiceImpl.setDefault`),先 `clearAllDefault` 后改目标
|
||||
- 删除: **拒绝默认模板删除** (`LaborProtocol.vue:162-163` 前端校验 `row.defaultFlag === 'Y'`)
|
||||
|
||||
---
|
||||
|
||||
## 6. Java Entity ↔ 数据库表 一致性
|
||||
|
||||
### 6.1 实测 DB schema (2026-08-19)
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz_labor_protocol_template (
|
||||
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
template_name varchar(100) NOT NULL COMMENT '模板名 (前端下拉显示)',
|
||||
template_content longtext NOT NULL COMMENT '模板 HTML (含占位符 {name} {phone} 等)',
|
||||
default_flag char(1) DEFAULT 'N' COMMENT '默认标记: Y=默认, N=非默认',
|
||||
sort_order int DEFAULT '0' COMMENT '排序 (前端下拉顺序)',
|
||||
status char(1) DEFAULT 'Y' COMMENT '状态: Y=启用, N=禁用',
|
||||
remark varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
create_by varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_by varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_default_flag (default_flag),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='劳务协议模板配置';
|
||||
```
|
||||
|
||||
### 6.2 Entity ↔ DB 字段对照
|
||||
|
||||
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | 主键 | `Long` | `id` | bigint AUTO_INCREMENT | ✅ |
|
||||
| `templateName` | 模板名 | `String` | `template_name` | varchar(100) NOT NULL | ✅ |
|
||||
| `templateContent` | 模板 HTML (含占位符) | `String` | `template_content` | longtext NOT NULL | ✅ |
|
||||
| `defaultFlag` | 默认标记 Y/N | `String` | `default_flag` | char(1) DEFAULT 'N' | ✅ |
|
||||
| `sortOrder` | 排序 | `Integer` | `sort_order` | int DEFAULT 0 | ✅ |
|
||||
| `status` | 状态 Y启用/N禁用 | `String` | `status` | char(1) DEFAULT 'Y' | ✅ |
|
||||
| `remark` | 备注 | `String` | `remark` | varchar(500) | ✅ |
|
||||
| `createBy` (BaseEntity) | 创建者 | `String` | `create_by` | varchar(64) | ✅ |
|
||||
| `createTime` (BaseEntity) | 创建时间 | `Date` | `create_time` | datetime | ✅ |
|
||||
| `updateBy` (BaseEntity) | 更新者 | `String` | `update_by` | varchar(64) | ✅ |
|
||||
| `updateTime` (BaseEntity) | 更新时间 | `Date` | `update_time` | datetime | ✅ |
|
||||
|
||||
**无字段缺失, 无类型不一致**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 索引检查
|
||||
|
||||
### 7.1 biz_labor_protocol_template 索引 (实测)
|
||||
|
||||
| Key | 列 | 用途覆盖 |
|
||||
|---|---|---|
|
||||
| PRIMARY | `id` | ✅ `WHERE id = ?` |
|
||||
| idx_default_flag | `default_flag` | ✅ `WHERE default_flag = 'Y'` (取默认模板) |
|
||||
| idx_status | `status` | ✅ `WHERE status = 'Y'` (下拉只显示启用) |
|
||||
|
||||
### 7.2 缺口分析
|
||||
|
||||
| 查询场景 | 涉及列 | 现有索引 | 评价 |
|
||||
|---|---|---|---|
|
||||
| 列表分页 (按 sort_order, id 排) | `sort_order`, `id` | PRIMARY | ✅ 走 filesort, OK |
|
||||
| 模板名模糊筛选 | `template_name` | ❌ 无索引 | ⚠️ 暂可接受 (数据少) |
|
||||
| 状态精确筛选 | `status` | ✅ idx_status | ✅ |
|
||||
| 默认模板 | `default_flag='Y'` | ✅ idx_default_flag | ✅ |
|
||||
| 全启用下拉 | `status='Y'` | ✅ idx_status | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 8. 与原型差异
|
||||
|
||||
### 8.1 实现新增 (超出原型)
|
||||
|
||||
| 项 | 实现 | 原型 | 评价 |
|
||||
|---|---|---|---|
|
||||
| 整个页面 | ✅ | ❌ 无对应原型 | 全新业务 |
|
||||
| 编辑 dialog + 富文本 textarea | ✅ | ❌ | 实现完整 |
|
||||
| "设为默认" 按钮 | ✅ | ❌ | 业务创新 |
|
||||
| default_flag 全局唯一事务保证 | ✅ | ❌ | 数据完整性设计 |
|
||||
| 3 个 HTML 模板从 HeguiConstants 迁移 | ✅ | ❌ | 内容来源 |
|
||||
|
||||
### 8.2 原型有但实现缺失
|
||||
|
||||
**❌ 无原型可对比** — 这是新业务页面。
|
||||
|
||||
### 8.3 文字 / 标签差异
|
||||
|
||||
N/A
|
||||
|
||||
### 8.4 总结
|
||||
|
||||
**整体方向**: 这是新建页面, 端到端完整 (DB + 后端 + 前端)。没有原型对比, 无从评判偏离度。
|
||||
|
||||
---
|
||||
|
||||
## 9. 字段冗余 / 设计问题
|
||||
|
||||
### 9.1 default_flag 全局唯一 — 设计 OK
|
||||
|
||||
- ❌ DB 没有 UNIQUE 约束 (MySQL UNIQUE 不支持 'Y' 唯一,允许多 NULL)
|
||||
- ✅ **App 层事务保证**: `BizLaborProtocolTemplateServiceImpl.setDefault` 用 `@Transactional`,先 `clearAllDefault` 再设目标为 Y
|
||||
- ✅ 前端兜底: 编辑/删除按钮在 `defaultFlag === 'Y'` 时禁用
|
||||
|
||||
### 9.2 无 template_type 字段 — 设计 OK
|
||||
|
||||
HeguiConstants 有 3 个模板 (北京整合医学学会 / 北京中卫 / 北京医学检验学会), 都属于"劳务协议"类型。用户要求"全局, 不需要加所属机构" → 当前实现一致。
|
||||
- 后续如要区分协议类型 (劳务 / 保密 / 知识产权 等), 可加 `template_type` 字段, 暂不需要
|
||||
|
||||
### 9.3 template_content 是 longtext (16MB 上限)
|
||||
|
||||
3 个 HeguiConstants 模板实际长度: 14K / 11K / 12K (合计 36K)。longtext 容量上限 ~16M, 够用。
|
||||
- ⚠️ 灌入的 HeguiConstants 是 **HTML 转义后的字符串字面量** (Java 转义 `\\n` 等), DB 存的是已 unescape 的真实 HTML。已实测验证。
|
||||
|
||||
### 9.4 没有"template_id" 外键
|
||||
|
||||
`template_content` 里的 `{name}` `{phone}` `{会议名称}` 等占位符在签署时由谁替换? **目前没人替换!**
|
||||
- ⚠️ 闭环问题: 模板只是"配置", 实际签署 (`biz_meeting_attendee.labor_protocol` 存 URL) 还需要单独的工作流渲染模板 → 替换占位符 → 生成 PDF/HTML
|
||||
- 后续: 单独工作流 (可能用 hwt-serve 的 ActivityProcess 等), 不在本页面范围
|
||||
|
||||
---
|
||||
|
||||
## 10. 待修复列表
|
||||
|
||||
| # | 问题 | 文件 | 修复建议 | 严重度 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **Controller 无角色校验**: 任何已登录用户都能调 8 个接口 | `BizLaborProtocolTemplateController.java` | 加 `@PreAuthorize("hasRole('admin')")` 类级别注解 (依赖 RuoYi 注解 + Spring Security 配置) | **P0** |
|
||||
| 2 | **`/api/business/laborProtocolTemplate.js` `default`/`allEnabled` 接口前端未使用** | `api/business/laborProtocolTemplate.js` | 暂时保留 (后续签署流程要用), 或删除以减小 API surface | P3 |
|
||||
| 3 | 富文本编辑器缺失, 现在 textarea 不友好 | `LaborProtocol.vue` | 集成 Tinymce 或 wangEditor (含 HTML 预览) | P2 |
|
||||
| 4 | template_content 没"占位符说明"提示 | `LaborProtocol.vue` | placeholder 加可用占位符列表 (如 `{name}`, `{phone}`, `{会议名称}`) | P2 |
|
||||
| 5 | **入参没有 default_flag 校验**: 前端可能传任意字符串 (不是 Y/N) | `BizLaborProtocolTemplateController.add/edit` | `service` 层校验 `defaultFlag ∈ {Y, N}`, 失败抛 ServiceException | P2 |
|
||||
| 6 | `update` mapper 写了 `template_content = #{templateContent}` 不在 `<if>` 内 — 总会覆盖 | `BizLaborProtocolTemplateMapper.xml:53-66` | 改成 `<if test="templateContent != null">template_content = #{templateContent},</if>` 避免误清空 | P3 |
|
||||
| 7 | 删除模板没审计 (硬 DELETE, 不走 `del_flag='0'``)` | 模板不重要, **可以接受**, 不修 | P3 |
|
||||
| 8 | **template_content 中文乱码**: 后端 JSON 响应 Content-Type 没 charset=UTF-8, 浏览器 fallback Latin-1 | `ResourcesConfig.java` | 加 `configureMessageConverters` 设 Jackson 默认 UTF-8 (你说暂不改, 留待后续) | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 引用清单
|
||||
|
||||
### v1 新建文件 (12 个)
|
||||
|
||||
| 文件 | 行号 | 说明 |
|
||||
|---|---|---|
|
||||
| **DB** `biz_labor_protocol_template` | — | 11 列 + PK + 2 索引 |
|
||||
| `ry-api/.../domain/BizLaborProtocolTemplate.java` | 全文 | entity |
|
||||
| `ry-api/.../mapper/BizLaborProtocolTemplateMapper.java` | 全文 | mapper 接口 |
|
||||
| `ry-api/.../resources/mapper/business/BizLaborProtocolTemplateMapper.xml` | 全文 | mapper SQL |
|
||||
| `ry-api/.../service/IBizLaborProtocolTemplateService.java` | 全文 | service 接口 |
|
||||
| `ry-api/.../service/impl/BizLaborProtocolTemplateServiceImpl.java` | 全文 | service 实现 (含 `@Transactional` setDefault) |
|
||||
| `ry-api/.../controller/BizLaborProtocolTemplateController.java` | 全文 | 8 个 REST 接口 |
|
||||
| `ry-vue3/src/api/business/laborProtocolTemplate.js` | 全文 | 前端 API 封装 |
|
||||
| `ry-vue3/src/views/admin/LaborProtocol.vue` | 全文 | admin 页面 (列表 + dialog) |
|
||||
| `ry-vue3/src/router/index.js:50` | — | 加 `/admin/labor-protocol` 路由 |
|
||||
| `ry-vue3/src/layout/AdminLayout.vue:86` | — | 加菜单 "劳务协议配置" |
|
||||
|
||||
### 没动
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `HeguiConstants.java` (hwt-serve) | 3 个常量保留, 内容已灌入 DB |
|
||||
| `proto/html/components/admin-settings.html` | 系统设置原型, 不相关 |
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (给项目负责人)
|
||||
|
||||
**新建页面, 端到端完整**:
|
||||
- DB ✅ `biz_labor_protocol_template` (11 列 + 3 行初始化 + 2 索引)
|
||||
- 后端 ✅ 完整 CRUD + default_flag 事务保证
|
||||
- 前端 ✅ admin 页面 + 列表 + dialog
|
||||
|
||||
**P0 安全风险**: Controller 没加 `@PreAuthorize("hasRole('admin')")`, 任何登录用户能调所有接口 → 必须修
|
||||
|
||||
**P2 体验**:
|
||||
- 富文本编辑器缺失 (现在是 textarea)
|
||||
- 占位符说明缺失
|
||||
- template_content 中文乱码 (Content-Type 没 charset,你说暂不改)
|
||||
|
||||
**正面**:
|
||||
- 设计清晰, default_flag 全局唯一有 app 层事务保证
|
||||
- 3 个 HeguiConstants 模板完整迁移, DB 实测 14K/11K/12K 字符
|
||||
- 索引覆盖主要查询场景
|
||||
- 没有数据隔离 (因为是全局配置,本来就不需要按用户隔离, 只有角色隔离缺失 — 见 P0)
|
||||
@@ -48,6 +48,8 @@ const routes = [
|
||||
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
||||
{ path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } },
|
||||
{ path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } },
|
||||
{ path: 'labor-protocol/new', name: 'admin-labor-protocol-new', component: () => import('@/views/admin/LaborProtocolEdit.vue'), meta: { title: '新建劳务协议模板' } },
|
||||
{ path: 'labor-protocol/edit/:id', name: 'admin-labor-protocol-edit', component: () => import('@/views/admin/LaborProtocolEdit.vue'), meta: { title: '编辑劳务协议模板' } },
|
||||
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -16,10 +16,14 @@
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="success" @click="openCreate">新建模板</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 批量按钮 -->
|
||||
<div class="batch-bar">
|
||||
<el-button type="primary" @click="goCreate">新建模板</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table :data="rows" border stripe v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
@@ -28,7 +32,7 @@
|
||||
<el-table-column label="默认" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.defaultFlag === 'Y'" type="success">默认</el-tag>
|
||||
<span v-else style="color:#c0c4cc">—</span>
|
||||
<span v-else class="dash-cell">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
@@ -40,7 +44,7 @@
|
||||
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" link type="primary" @click="goEdit(row)">编辑</el-button>
|
||||
<el-button size="small" link type="warning" :disabled="row.defaultFlag === 'Y'" @click="setDefault(row)">设为默认</el-button>
|
||||
<el-button size="small" link type="danger" @click="removeRow(row)">删除</el-button>
|
||||
</template>
|
||||
@@ -58,47 +62,17 @@
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑 dialog -->
|
||||
<el-dialog v-model="editOpen" :title="form.id ? '编辑模板' : '新建模板'" width="780px" destroy-on-close>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="模板名" prop="templateName">
|
||||
<el-input v-model="form.templateName" placeholder="如: 默认劳务协议" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认模板">
|
||||
<el-switch v-model="form.defaultFlag" active-value="Y" inactive-value="N" />
|
||||
<span style="margin-left:12px;color:#909399;font-size:12px">开启后其他模板自动取消默认</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="Y">启用</el-radio>
|
||||
<el-radio value="N">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="templateContent">
|
||||
<el-input v-model="form.templateContent" type="textarea" :rows="14"
|
||||
placeholder="HTML 模板, 支持占位符: {name} {phone} {会议名称} {会议时间} {会议地址} {劳务形式} {费用总额} ..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" placeholder="选填" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const q = reactive({ templateName: '', status: '', pageNum: 1, pageSize: 20 })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
@@ -120,66 +94,12 @@ function reset() {
|
||||
load()
|
||||
}
|
||||
|
||||
const editOpen = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null,
|
||||
templateName: '',
|
||||
templateContent: '',
|
||||
defaultFlag: 'N',
|
||||
sortOrder: 0,
|
||||
status: 'Y',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
templateName: [{ required: true, message: '请输入模板名', trigger: 'blur' }],
|
||||
templateContent: [{ required: true, message: '请输入模板内容', trigger: 'blur' }]
|
||||
function goCreate() {
|
||||
router.push('/admin/labor-protocol/new')
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
templateName: '',
|
||||
templateContent: '',
|
||||
defaultFlag: 'N',
|
||||
sortOrder: 0,
|
||||
status: 'Y',
|
||||
remark: ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
templateName: row.templateName,
|
||||
templateContent: row.templateContent,
|
||||
defaultFlag: row.defaultFlag || 'N',
|
||||
sortOrder: row.sortOrder || 0,
|
||||
status: row.status || 'Y',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
if (form.id) {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'put', data: form })
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'post', data: form })
|
||||
ElMessage.success('已新建')
|
||||
}
|
||||
editOpen.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally { submitting.value = false }
|
||||
function goEdit(row) {
|
||||
router.push(`/admin/labor-protocol/edit/${row.id}`)
|
||||
}
|
||||
|
||||
async function setDefault(row) {
|
||||
@@ -211,9 +131,13 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-labor-protocol { padding: 16px; max-width: 1400px; }
|
||||
/* 整页面板 (与 People.vue 风格一致) */
|
||||
.admin-labor-protocol { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.filter-form { margin-bottom: 12px; }
|
||||
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.el-textarea__inner) { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||
|
||||
/* 表格空状态 (默认标记 = N) */
|
||||
.dash-cell { color: #c0c4cc; }
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<!--
|
||||
劳务协议模板 新建/编辑 独立页
|
||||
编辑/新建复用: 通过 route.params.id 区分
|
||||
-->
|
||||
<div class="page-card labor-protocol-edit">
|
||||
<!-- 面包屑 -->
|
||||
<div class="breadcrumb">
|
||||
<a @click="goBack">劳务协议配置</a> > {{ isEdit ? '编辑模板' : '新建模板' }}
|
||||
</div>
|
||||
|
||||
<!-- 表单卡片 -->
|
||||
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模板名" prop="templateName">
|
||||
<el-input v-model="form.templateName" placeholder="如: 默认劳务协议" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="默认模板" prop="defaultFlag">
|
||||
<el-switch v-model="form.defaultFlag" active-value="Y" inactive-value="N" />
|
||||
<span class="default-hint">开启后其他模板自动取消默认</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="Y">启用</el-radio>
|
||||
<el-radio value="N">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="模板内容" prop="templateContent">
|
||||
<el-input v-model="form.templateContent" type="textarea" :rows="20"
|
||||
placeholder="HTML 模板, 支持占位符: {name} {phone} {会议名称} {会议时间} {会议地址} {劳务形式} {费用总额} ..." />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="选填" maxlength="500" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const loadingDetail = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
templateName: '',
|
||||
templateContent: '',
|
||||
defaultFlag: 'N',
|
||||
sortOrder: 0,
|
||||
status: 'Y',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
templateName: [{ required: true, message: '请输入模板名', trigger: 'blur' }],
|
||||
templateContent: [{ required: true, message: '请输入模板内容', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!isEdit.value) return
|
||||
loadingDetail.value = true
|
||||
try {
|
||||
const { data } = await request({ url: `/business/laborProtocolTemplate/${route.params.id}`, method: 'get' })
|
||||
Object.assign(form, {
|
||||
id: data.id,
|
||||
templateName: data.templateName,
|
||||
templateContent: data.templateContent,
|
||||
defaultFlag: data.defaultFlag || 'N',
|
||||
sortOrder: data.sortOrder || 0,
|
||||
status: data.status || 'Y',
|
||||
remark: data.remark || ''
|
||||
})
|
||||
} catch (e) {
|
||||
ElMessage.error('加载模板详情失败')
|
||||
} finally { loadingDetail.value = false }
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'put', data: form })
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'post', data: form })
|
||||
ElMessage.success('已新建')
|
||||
}
|
||||
goBack()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/admin/labor-protocol')
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整页面板 (与 People.vue 风格一致) */
|
||||
.labor-protocol-edit { padding: 16px; max-width: 1200px; }
|
||||
/* 面包屑带可点击返回 */
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
|
||||
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
|
||||
|
||||
/* 去掉 el-card 默认的边框/阴影 (跟 ExpertNew.vue 保持一致) */
|
||||
.form-card.nested { background: transparent; border: none; box-shadow: none; }
|
||||
:deep(.form-card .el-card__body) { padding: 24px 28px; }
|
||||
|
||||
/* 主按钮用项目色变量 (覆盖 el 默认 #409eff) */
|
||||
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||||
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
|
||||
:deep(.el-button) { border-radius: 4px; }
|
||||
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
|
||||
|
||||
/* 默认模板提示文字 */
|
||||
.default-hint { margin-left: 12px; color: #909399; font-size: 12px; }
|
||||
|
||||
/* template_content 是 HTML, 用等宽字体更清晰 */
|
||||
:deep(.el-textarea__inner) { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user