修改目录

This commit is contained in:
wkc
2026-03-03 16:14:16 +08:00
parent c8b041f4b9
commit 521bb80b2f
438 changed files with 15313 additions and 21773 deletions

View File

@@ -1,306 +0,0 @@
# 员工实体关系添加员工名称字段设计
## 1. 需求概述
在员工实体关系列表和详情中添加员工名称字段,通过身份证号(personId)关联员工信息表(ccdi_base_staff)获取姓名。
**涉及模块:** 员工实体关系 (ccdi_staff_enterprise_relation)
**展示位置:**
- 列表页面 (table 列)
- 详情接口返回
**数据来源:** 通过 personId 关联 ccdi_base_staff 表的 id_card 字段获取 name 字段
**空值处理:** 当 personId 在员工信息表中不存在时,显示为空
## 2. 技术方案
采用 MyBatis 关联查询(JOIN)方式,在查询时动态获取员工姓名,不修改表结构。
### 2.1 优势
- ✅ 无需修改数据库表结构
- ✅ 数据始终与员工信息表同步
- ✅ 实施简单,风险低
- ✅ 性能影响可控
## 3. 数据库层设计
### 3.1 SQL查询改造
`CcdiStaffEnterpriseRelationMapper.xml` 中修改列表查询和详情查询:
```sql
SELECT
ser.id,
ser.person_id,
bs.name AS person_name, -- 通过JOIN获取员工姓名
ser.relation_person_post,
ser.social_credit_code,
ser.enterprise_name,
ser.status,
ser.remark,
ser.data_source,
ser.is_employee,
ser.is_emp_family,
ser.is_customer,
ser.is_cust_family,
ser.create_time,
ser.update_time,
ser.created_by,
ser.updated_by
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
WHERE ser.status = 1
```
### 3.2 关键点
- 使用 `LEFT JOIN` 确保即使员工信息不存在,关系记录也会返回
-`personId``ccdi_base_staff` 中不存在时,`person_name` 为 NULL
- 数据库表结构不需要修改
### 3.3 索引优化
确保 `ccdi_base_staff.id_card` 字段有索引:
```sql
-- 检查索引
SHOW INDEX FROM ccdi_base_staff WHERE Key_name = 'idx_id_card';
-- 如果没有索引,创建
CREATE INDEX idx_id_card ON ccdi_base_staff(id_card);
```
## 4. 后端代码层设计
### 4.1 VO层修改
**文件:** `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiStaffEnterpriseRelationVO.java`
```java
/** 身份证号 */
@Schema(description = "身份证号")
private String personId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String personName;
/** 关联人在企业的职务 */
@Schema(description = "关联人在企业的职务")
private String relationPersonPost;
```
### 4.2 Mapper接口
**文件:** `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffEnterpriseRelationMapper.xml`
修改查询方法,添加 LEFT JOIN:
```xml
<select id="selectRelationList" resultType="CcdiStaffEnterpriseRelationVO">
SELECT
ser.id,
ser.person_id,
bs.name AS person_name,
ser.relation_person_post,
ser.social_credit_code,
ser.enterprise_name,
ser.status,
ser.remark,
ser.data_source,
ser.is_employee,
ser.is_emp_family,
ser.is_customer,
ser.is_cust_family,
ser.create_time,
ser.update_time,
ser.created_by,
ser.updated_by
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
<where>
<if test="personId != null and personId != ''">
AND ser.person_id LIKE CONCAT('%', #{personId}, '%')
</if>
<if test="enterpriseName != null and enterpriseName != ''">
AND ser.enterprise_name LIKE CONCAT('%', #{enterpriseName}, '%')
</if>
<if test="status != null">
AND ser.status = #{status}
</if>
</where>
ORDER BY ser.create_time DESC
</select>
```
同样修改 `selectRelationById` 方法。
### 4.3 Service层
`ICcdiStaffEnterpriseRelationService.java` 和实现类无需大改,MyBatis Plus 会自动填充 JOIN 的字段。
## 5. 前端代码层设计
### 5.1 列表页面修改
**文件:** `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
在表格列定义中添加员工姓名列:
```vue
<el-table-column label="身份证号" align="center" prop="personId" width="180" />
<el-table-column label="员工姓名" align="center" prop="personName" width="100" />
<el-table-column label="职务" align="center" prop="relationPersonPost" width="120" />
```
**位置建议:** 放在"身份证号"列之后,方便用户对照查看
### 5.2 API接口
**文件:** `ruoyi-ui/src/api/ccdiStaffEnterpriseRelation.js`
无需修改,接口会自动返回新增的 `personName` 字段。
### 5.3 详情页面
如果存在详情对话框,同样添加员工姓名显示:
```vue
<el-form-item label="身份证号">{{ form.personId }}</el-form-item>
<el-form-item label="员工姓名">{{ form.personName }}</el-form-item>
<el-form-item label="职务">{{ form.relationPersonPost }}</el-form-item>
```
## 6. 错误处理和边界情况
### 6.1 数据空值处理
- **场景**: personId 在 `ccdi_base_staff` 表中不存在
- **处理**: `personName` 为 NULL,前端显示为空字符串
- **前端展示**: Element UI table 会自动将 null 显示为空
### 6.2 数据一致性
- **场景**: 员工信息表的姓名后续被修改
- **影响**: 下次查询时自动获取最新姓名,无需同步
- **优势**: JOIN 方案天然保证数据一致性
### 6.3 性能考虑
- **索引**: 确保 `ccdi_base_staff.id_card` 字段有索引
- **查询优化**: LEFT JOIN 对性能影响较小
- **分页**: 已有分页机制,单页数据量有限
### 6.4 特殊字符处理
- 员工姓名可能包含特殊字符,MyBatis 和 JSON 序列化会自动处理
- 无需额外转义逻辑
## 7. 测试策略
### 7.1 单元测试
创建测试用例覆盖以下场景:
```java
// 测试场景1: 员工信息存在
assertEquals("张三", result.getPersonName());
// 测试场景2: 员工信息不存在
assertNull(result.getPersonName());
// 测试场景3: 姓名包含特殊字符
assertEquals("张三·李四", result.getPersonName());
// 测试场景4: 批量数据性能测试
List<CcdiStaffEnterpriseRelationVO> list = mapper.selectRelationList(query);
assertTrue(list.size() > 0);
```
### 7.2 接口测试
使用测试脚本验证:
- 列表接口返回 `personName` 字段
- 详情接口返回 `personName` 字段
- 分页查询正常工作
- 空值处理正确
### 7.3 前端测试
手动验证:
- 列表页面正确显示员工姓名
- 空值显示为空
- 列表排序、筛选功能正常
### 7.4 数据准备测试
准备测试数据:
- 已有员工的关系记录
- 无对应员工的关系记录
- 批量数据的性能测试
## 8. 实施步骤
### 步骤1: 修改 VO 类
- 文件: `CcdiStaffEnterpriseRelationVO.java`
- 添加 `personName` 字段及注解
### 步骤2: 修改 Mapper XML
- 文件: `CcdiStaffEnterpriseRelationMapper.xml`
- 修改列表查询和详情查询,添加 LEFT JOIN
### 步骤3: 修改前端列表页
- 文件: `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
- 在表格中添加员工姓名列
### 步骤4: 检查数据库索引
- 检查 `ccdi_base_staff.id_card` 是否有索引
- 如果没有,执行创建索引 SQL
### 步骤5: 测试验证
- 运行后端,测试接口返回
- 运行前端,验证页面显示
- 生成测试报告
### 步骤6: 更新文档
- 更新 API 文档
- 更新数据库设计文档
## 9. 影响范围
**修改文件:**
- 后端: 2个文件 (VO + Mapper XML)
- 前端: 1个文件 (列表页面)
- 数据库: 0个表结构修改
**涉及模块:**
- 员工实体关系 (ccdi_staff_enterprise_relation)
**风险评估:**
- 低风险: 仅查询层面的改动,不影响数据写入
- 性能影响可控: 通过索引优化
- 兼容性好: 新增字段不影响现有功能
## 10. 后续优化建议
### 10.1 缓存优化
如果员工信息表数据量大且变动不频繁,可以考虑:
- 使用 Redis 缓存员工信息
- 减少数据库查询次数
### 10.2 搜索增强
可以支持按员工姓名搜索关系记录:
- 在查询条件中添加姓名搜索
- 需要修改查询 DTO 和 Mapper XML
### 10.3 其他模块
如果其他模块也有类似需求,可以复用此方案:
- 员工亲属关系 (ccdi_staff_fmy_relation) - 已有 personName 字段
- 员工招聘 (ccdi_staff_recruitment)
- 员工调动 (ccdi_staff_transfer)

View File

@@ -1,949 +0,0 @@
# 员工实体关系添加员工名称字段实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 在员工实体关系列表和详情中添加员工名称字段,通过 LEFT JOIN 查询员工信息表获取姓名
**架构:** 使用 MyBatis 的 LEFT JOIN 在查询层关联 `ccdi_base_staff` 表,通过 `person_id = id_card` 关联获取员工姓名,不修改表结构
**技术栈:** Spring Boot 3, MyBatis Plus 3.5.10, Vue 2.6, Element UI 2.15
---
## 前置条件检查
### Task 1: 检查数据库索引
**文件:**
- 数据库: `ccdi_base_staff`
**Step 1: 连接数据库并检查索引**
使用 MCP 连接数据库:
```
连接配置从 application.yml 读取
```
**Step 2: 执行索引检查 SQL**
```sql
SHOW INDEX FROM ccdi_base_staff WHERE Key_name = 'idx_id_card';
```
预期: 如果索引存在,返回索引信息;如果不存在,返回空结果
**Step 3: 如果索引不存在,创建索引**
```sql
CREATE INDEX idx_id_card ON ccdi_base_staff(id_card);
```
预期: Query OK, 0 rows affected
**Step 4: 验证索引创建成功**
```sql
SHOW INDEX FROM ccdi_base_staff WHERE Key_name = 'idx_id_card';
```
预期: 返回新创建的索引信息
**Step 5: 记录结果**
如果索引已创建,在实施笔记中记录:
```markdown
- [x] 数据库索引已创建
```
---
## 后端实现
### Task 2: 修改 VO 类添加员工姓名字段
**文件:**
- Modify: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiStaffEnterpriseRelationVO.java`
**Step 1: 在 personId 字段后添加 personName 字段**
`CcdiStaffEnterpriseRelationVO.java` 的第 30 行之后添加:
```java
/** 身份证号 */
@Schema(description = "身份证号")
private String personId;
/** 员工姓名 */
@Schema(description = "员工姓名")
private String personName;
/** 关联人在企业的职务 */
@Schema(description = "关联人在企业的职务")
private String relationPersonPost;
```
**Step 2: 保存文件**
**Step 3: 提交更改**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiStaffEnterpriseRelationVO.java
git commit -m "feat(staff-enterprise-relation): 添加员工姓名字段到VO"
```
预期: Commit 成功
---
### Task 3: 修改 Mapper XML - 列表查询
**文件:**
- Modify: `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffEnterpriseRelationMapper.xml`
**Step 1: 找到列表查询的 SQL**
查找 `selectRelationList` 或类似的列表查询方法
**Step 2: 修改 SELECT 子句,添加员工姓名**
将原来的:
```xml
SELECT ser.id, ser.person_id, ser.relation_person_post, ...
```
修改为:
```xml
SELECT
ser.id,
ser.person_id,
bs.name AS person_name,
ser.relation_person_post,
ser.social_credit_code,
ser.enterprise_name,
ser.status,
ser.remark,
ser.data_source,
ser.is_employee,
ser.is_emp_family,
ser.is_customer,
ser.is_cust_family,
ser.create_time,
ser.update_time,
ser.created_by,
ser.updated_by
```
**Step 3: 修改 FROM 子句,添加 LEFT JOIN**
将原来的:
```xml
FROM ccdi_staff_enterprise_relation ser
```
修改为:
```xml
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
```
**Step 4: 保存文件**
**Step 5: 提交更改**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffEnterpriseRelationMapper.xml
git commit -m "feat(staff-enterprise-relation): 列表查询添加员工姓名JOIN"
```
预期: Commit 成功
---
### Task 4: 修改 Mapper XML - 详情查询
**文件:**
- Modify: `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffEnterpriseRelationMapper.xml`
**Step 1: 找到详情查询的 SQL**
查找 `selectRelationById` 或类似的详情查询方法
**Step 2: 应用与列表查询相同的修改**
1. 在 SELECT 子句中添加 `bs.name AS person_name`
2. 在 FROM 子句中添加 `LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card`
完整的查询应该类似:
```xml
SELECT
ser.id,
ser.person_id,
bs.name AS person_name,
ser.relation_person_post,
ser.social_credit_code,
ser.enterprise_name,
ser.status,
ser.remark,
ser.data_source,
ser.is_employee,
ser.is_emp_family,
ser.is_customer,
ser.is_cust_family,
ser.create_time,
ser.update_time,
ser.created_by,
ser.updated_by
FROM ccdi_staff_enterprise_relation ser
LEFT JOIN ccdi_base_staff bs ON ser.person_id = bs.id_card
WHERE ser.id = #{id}
```
**Step 3: 保存文件**
**Step 4: 提交更改**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffEnterpriseRelationMapper.xml
git commit -m "feat(staff-enterprise-relation): 详情查询添加员工姓名JOIN"
```
预期: Commit 成功
---
### Task 5: 编写接口测试脚本
**文件:**
- Create: `doc/test_staff_enterprise_relation_person_name.bat`
**Step 1: 创建测试脚本**
创建测试脚本验证接口返回 `personName` 字段:
```bash
@echo off
chcp 65001 > nul
echo ========================================
echo 员工实体关系员工姓名字段测试
echo ========================================
echo.
REM 获取 token
echo [1/5] 获取登录 token...
curl -s -X POST "http://localhost:8080/login/test" \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":\"admin123\"}" \
> token_response.json
for /f "tokens=2 delims=:\"" %%a in ('findstr "\"token\"" token_response.json') do set TOKEN=%%a
echo Token: %TOKEN%
echo.
REM 测试列表接口
echo [2/5] 测试列表接口...
curl -s -X GET "http://localhost:8080/ccdi/staffEnterpriseRelation/list?pageNum=1&pageSize=10" \
-H "Authorization: Bearer %TOKEN%" \
> list_response.json
echo 检查 personName 字段是否在响应中...
findstr /C:"personName" list_response.json > nul
if %errorlevel% equ 0 (
echo [SUCCESS] 列表接口包含 personName 字段
) else (
echo [FAIL] 列表接口缺少 personName 字段
type list_response.json
)
echo.
REM 测试详情接口
echo [3/5] 获取第一条记录的 ID...
for /f "tokens=2 delims=:\" %%a in ('findstr /C:"\"id\":" list_response.json ^| findstr /N "." ^| findstr "^1:"') do (
set FIRST_LINE=%%a
)
REM 这里需要手动解析,暂时使用固定 ID 进行测试
echo 注意: 请手动查看 list_response.json 中的有效 ID
echo.
REM 使用示例 ID 测试详情接口
echo [4/5] 测试详情接口 (ID: 1)...
curl -s -X GET "http://localhost:8080/ccdi/staffEnterpriseRelation/1" \
-H "Authorization: Bearer %TOKEN%" \
> detail_response.json
echo 检查 personName 字段是否在响应中...
findstr /C:"personName" detail_response.json > nul
if %errorlevel% equ 0 (
echo [SUCCESS] 详情接口包含 personName 字段
) else (
echo [FAIL] 详情接口缺少 personName 字段
type detail_response.json
)
echo.
REM 查看响应内容
echo [5/5] 查看列表响应内容...
type list_response.json
echo.
echo ========================================
echo 测试完成
echo ========================================
pause
```
**Step 2: 保存文件**
**Step 3: 提交测试脚本**
```bash
git add doc/test_staff_enterprise_relation_person_name.bat
git commit -m "test(staff-enterprise-relation): 添加员工姓名字段测试脚本"
```
---
### Task 6: 后端编译验证
**文件:**
- Build: Maven project
**Step 1: 清理并编译项目**
```bash
cd ruoyi-admin
mvn clean compile
```
预期: BUILD SUCCESS
**Step 2: 检查编译错误**
如果有编译错误,检查:
1. VO 类语法是否正确
2. Mapper XML 语法是否正确
3. 是否有依赖问题
**Step 3: 如果编译成功,记录日志**
在实施笔记中记录:
```markdown
- [x] 后端编译成功
```
---
## 前端实现
### Task 7: 修改列表页面添加员工姓名列
**文件:**
- Modify: `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
**Step 1: 找到表格列定义部分**
查找 `<el-table>` 组件中的列定义
**Step 2: 在身份证号列后添加员工姓名列**
`personId` 列之后添加:
```vue
<el-table-column label="身份证号" align="center" prop="personId" width="180" />
<el-table-column label="员工姓名" align="center" prop="personName" width="100" />
<el-table-column label="职务" align="center" prop="relationPersonPost" width="120" />
```
**Step 3: 如果有搜索表单,也可以添加员工姓名搜索**
在搜索表单中添加:
```vue
<el-form-item label="员工姓名" prop="personName">
<el-input
v-model="queryParams.personName"
placeholder="请输入员工姓名"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
```
注意: 如果添加搜索功能,需要同步修改后端的 Mapper XML 查询条件
**Step 4: 保存文件**
**Step 5: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue
git commit -m "feat(staff-enterprise-relation): 列表页面添加员工姓名列"
```
预期: Commit 成功
---
### Task 8: 前端编译验证
**文件:**
- Build: Vue project
**Step 1: 进入前端目录**
```bash
cd ruoyi-ui
```
**Step 2: 安装依赖(如果需要)**
```bash
npm install
```
**Step 3: 开发模式启动**
```bash
npm run dev
```
预期: 编译成功,服务启动在端口 80
**Step 4: 检查编译错误**
如果有编译错误,检查:
1. Vue 组件语法是否正确
2. 是否有依赖问题
**Step 5: 停止开发服务器**
`Ctrl+C` 停止
**Step 6: 记录日志**
在实施笔记中记录:
```markdown
- [x] 前端编译成功
```
---
## 测试阶段
### Task 9: 后端集成测试
**文件:**
- Test: 使用 MCP 或测试脚本
**Step 1: 启动后端服务**
```bash
cd ruoyi-admin
mvn spring-boot:run
```
预期: 服务启动成功,监听端口 8080
**Step 2: 运行测试脚本**
```bash
doc\test_staff_enterprise_relation_person_name.bat
```
**Step 3: 验证测试结果**
检查:
1. 列表接口是否返回 `personName` 字段
2. 详情接口是否返回 `personName` 字段
3. 员工信息存在时,姓名是否正确显示
4. 员工信息不存在时,字段是否为 null
**Step 4: 记录测试结果**
在实施笔记中记录:
```markdown
- [x] 后端接口测试通过
- [ ] personName 字段正确返回
- [ ] 员工信息存在时姓名正确
- [ ] 员工信息不存在时为 null
```
**Step 5: 停止后端服务**
`Ctrl+C` 停止
---
### Task 10: 前端集成测试
**文件:**
- Test: 浏览器手动测试
**Step 1: 启动后端服务**
```bash
cd ruoyi-admin
mvn spring-boot:run
```
**Step 2: 启动前端服务**
新开终端:
```bash
cd ruoyi-ui
npm run dev
```
**Step 3: 浏览器访问测试**
访问: `http://localhost`
**Step 4: 登录系统**
用户名: `admin`
密码: `admin123`
**Step 5: 导航到员工实体关系页面**
系统管理 > 员工实体关系
**Step 6: 验证列表显示**
检查:
1. 列表中是否显示"员工姓名"列
2. 员工姓名是否正确显示
3. 无员工信息时,是否显示为空
**Step 7: 测试详情查看**
点击某条记录的"查看"按钮,验证详情对话框中是否显示员工姓名
**Step 8: 测试分页和搜索**
1. 切换分页,验证员工姓名持续显示
2. 使用身份证号搜索,验证结果正确
3. 如果实现了姓名搜索,测试姓名搜索功能
**Step 9: 记录测试结果**
在实施笔记中记录:
```markdown
- [x] 前端页面测试通过
- [ ] 员工姓名列正确显示
- [ ] 空值正确显示
- [ ] 分页正常
- [ ] 搜索功能正常
```
**Step 10: 停止服务**
`Ctrl+C` 停止前后端服务
---
### Task 11: 性能测试
**文件:**
- Test: 性能验证
**Step 1: 准备测试数据**
确保数据库中有足够多的测试数据(至少 1000 条)
**Step 2: 启动后端服务**
```bash
cd ruoyi-admin
mvn spring-boot:run
```
**Step 3: 测试分页查询性能**
使用测试脚本或浏览器开发者工具,测量:
- 第一页查询响应时间
- 中间页查询响应时间
- 最后一页查询响应时间
**Step 4: 对比性能数据**
与修改前的性能对比,验证:
- 响应时间增加是否在可接受范围内(< 100ms)
- 如果性能下降明显,考虑优化索引
**Step 5: 记录性能测试结果**
在实施笔记中记录:
```markdown
- [x] 性能测试完成
- [ ] 平均响应时间: ___ ms
- [ ] 性能是否可接受: 是/否
```
---
### Task 12: 边界情况测试
**文件:**
- Test: 边界场景验证
**测试场景 1: personId 为空**
**Step 1:** 在数据库中插入一条 `person_id` 为 NULL 的记录
**Step 2:** 在列表中查看该记录
预期: 记录正常显示,员工姓名为空
**测试场景 2: personId 在员工信息表中不存在**
**Step 1:** 在数据库中插入一条 `person_id` 为不存在身份证号的记录
**Step 2:** 在列表中查看该记录
预期: 记录正常显示,员工姓名为空
**测试场景 3: 员工姓名包含特殊字符**
**Step 1:** 确保员工信息表中有姓名包含特殊字符的记录(如 "张三·李四")
**Step 2:** 在列表中查看该记录
预期: 员工姓名正确显示,特殊字符无乱码
**测试场景 4: 大量数据查询**
**Step 1:** 测试查询 100 条记录/页
**Step 2:** 测试查询 500 条记录/页
预期: 所有记录的员工姓名都正确显示,无性能问题
**Step 3: 记录边界测试结果**
在实施笔记中记录:
```markdown
- [x] 边界测试完成
- [ ] personId 为空: 通过/失败
- [ ] personId 不存在: 通过/失败
- [ ] 特殊字符: 通过/失败
- [ ] 大量数据: 通过/失败
```
---
## 文档更新
### Task 13: 更新 API 文档
**文件:**
- Modify: `doc/api-docs/api/` 下的相关文档
**Step 1: 查找现有的 API 文档**
找到员工实体关系相关的 API 文档
**Step 2: 在响应参数中添加 personName 字段**
在列表接口和详情接口的响应参数中添加:
```markdown
| 字段名 | 类型 | 说明 |
|--------|------|------|
| personId | String | 身份证号 |
| personName | String | 员工姓名(通过关联查询获取) |
| relationPersonPost | String | 关联人在企业的职务 |
```
**Step 3: 添加说明**
在文档中添加说明:
```markdown
**注意:**
- personName 字段通过 LEFT JOIN ccdi_base_staff 表获取
- 如果 personId 在员工信息表中不存在,personName 为 null
```
**Step 4: 保存并提交**
```bash
git add doc/api-docs/
git commit -m "docs(staff-enterprise-relation): 更新API文档,添加员工姓名字段说明"
```
---
### Task 14: 更新数据库设计文档
**文件:**
- Modify: `doc/database-docs/` 下的相关文档
**Step 1: 更新视图说明**
`ccdi_staff_enterprise_relation` 表的说明中添加:
```markdown
## 关联查询
该表在查询时会关联 `ccdi_base_staff` 表获取员工姓名:
- 关联字段: ccdi_staff_enterprise_relation.person_id = ccdi_base_staff.id_card
- 获取字段: ccdi_base_staff.name AS person_name
- 关联方式: LEFT JOIN(确保即使员工信息不存在也能返回关系记录)
```
**Step 2: 保存并提交**
```bash
git add doc/database-docs/
git commit -m "docs(staff-enterprise-relation): 更新数据库设计文档,添加关联查询说明"
```
---
### Task 15: 生成测试报告
**文件:**
- Create: `doc/test-reports/2026-02-11-staff-enterprise-relation-person-name-test-report.md`
**Step 1: 创建测试报告**
```markdown
# 员工实体关系员工姓名字段测试报告
**测试日期:** 2026-02-11
**测试人员:** [测试人员姓名]
**测试环境:** 开发环境
## 1. 功能测试
### 1.1 列表接口测试
| 测试项 | 测试场景 | 预期结果 | 实际结果 | 状态 |
|--------|----------|----------|----------|------|
| personName 字段返回 | 调用列表接口 | 响应包含 personName 字段 | | PASS/FAIL |
| 员工信息存在 | personId 在员工表中存在 | 返回正确员工姓名 | | PASS/FAIL |
| 员工信息不存在 | personId 在员工表中不存在 | personName 为 null | | PASS/FAIL |
### 1.2 详情接口测试
| 测试项 | 测试场景 | 预期结果 | 实际结果 | 状态 |
|--------|----------|----------|----------|------|
| personName 字段返回 | 调用详情接口 | 响应包含 personName 字段 | | PASS/FAIL |
| 员工信息存在 | personId 在员工表中存在 | 返回正确员工姓名 | | PASS/FAIL |
| 员工信息不存在 | personId 在员工表中不存在 | personName 为 null | | PASS/FAIL |
### 1.3 前端页面测试
| 测试项 | 测试场景 | 预期结果 | 实际结果 | 状态 |
|--------|----------|----------|----------|------|
| 员工姓名列显示 | 列表页面 | 显示"员工姓名"列 | | PASS/FAIL |
| 空值显示 | 员工信息不存在 | 显示为空 | | PASS/FAIL |
| 分页功能 | 切换页面 | 员工姓名持续显示 | | PASS/FAIL |
## 2. 性能测试
| 测试项 | 测试场景 | 预期结果 | 实际结果 | 状态 |
|--------|----------|----------|----------|------|
| 响应时间 | 1000 条数据查询 | < 100ms | ___ ms | PASS/FAIL |
| 大数据量 | 100 条/页 | 正常显示 | | PASS/FAIL |
## 3. 边界测试
| 测试项 | 测试场景 | 预期结果 | 实际结果 | 状态 |
|--------|----------|----------|----------|------|
| personId 为空 | person_id = NULL | 正常显示,姓名为空 | | PASS/FAIL |
| 特殊字符 | 姓名含特殊字符 | 正确显示无乱码 | | PASS/FAIL |
## 4. 测试结论
### 4.1 通过的功能
- [ ] 列表接口返回 personName 字段
- [ ] 详情接口返回 personName 字段
- [ ] 前端正确显示员工姓名
- [ ] 空值正确处理
- [ ] 性能满足要求
### 4.2 发现的问题
[记录测试中发现的问题]
### 4.3 建议
[记录优化建议]
### 4.4 总体评价
- 通过率: ___%
- 风险等级: 高/中/低
- 上线建议: 建议/不建议
```
**Step 2: 填写测试结果**
根据实际测试结果填写报告
**Step 3: 保存并提交**
```bash
git add doc/test-reports/2026-02-11-staff-enterprise-relation-person-name-test-report.md
git commit -m "test(staff-enterprise-relation): 添加员工姓名字段测试报告"
```
---
## 代码审查
### Task 16: 自我代码审查
**文件:**
- Review: 所有修改的文件
**Step 1: 检查 VO 类**
检查项:
- [ ] 字段命名符合规范(驼峰命名)
- [ ] 有正确的 Swagger 注解
- [ ] 字段类型正确(String)
- [ ] 实现了 Serializable 接口
**Step 2: 检查 Mapper XML**
检查项:
- [ ] SQL 语法正确
- [ ] LEFT JOIN 条件正确
- [ ] 字段别名正确(person_name)
- [ ] WHERE 条件不受影响
- [ ] 没有语法错误
**Step 3: 检查前端代码**
检查项:
- [ ] 列定义位置合理
- [ ] prop 名称与后端一致
- [ ] 列宽设置合理
- [ ] 没有 Vue 语法错误
**Step 4: 检查测试覆盖**
检查项:
- [ ] 接口测试覆盖列表和详情
- [ ] 前端测试覆盖显示和交互
- [ ] 边界测试覆盖异常场景
- [ ] 性能测试覆盖大数据量
**Step 5: 使用 code-reviewer 技能**
如果所有检查通过,调用 code-reviewer 技能进行正式审查:
```
/superpowers:requesting-code-review
```
---
## 最终提交和合并
### Task 17: 整合提交
**文件:**
- Git: Feature branch
**Step 1: 查看所有提交**
```bash
git log --oneline
```
确认所有任务都已提交
**Step 2: 确保分支是最新的**
```bash
git fetch origin
git rebase origin/dev_1
```
**Step 3: 推送到远程**
```bash
git push origin HEAD:feat/staff-enterprise-relation-person-name
```
**Step 4: 创建 Pull Request**
使用 gh 命令创建 PR:
```bash
gh pr create \
--title "feat: 员工实体关系添加员工姓名字段" \
--body "## 功能说明
在员工实体关系列表和详情中添加员工姓名字段,通过 LEFT JOIN 查询员工信息表获取。
## 实施方案
- 修改 CcdiStaffEnterpriseRelationVO,添加 personName 字段
- 修改 Mapper XML,添加 LEFT JOIN ccdi_base_staff
- 修改前端列表页,添加员工姓名列
- 不修改数据库表结构,通过关联查询获取
## 测试情况
- [x] 单元测试通过
- [x] 接口测试通过
- [x] 前端测试通过
- [x] 边界测试通过
- [x] 性能测试通过
## 相关文档
- 设计文档: doc/plans/2026-02-11-staff-enterprise-relation-person-name-design.md
- 实施计划: doc/plans/2026-02-11-staff-enterprise-relation-person-name-implementation.md
- 测试报告: doc/test-reports/2026-02-11-staff-enterprise-relation-person-name-test-report.md
" \
--base dev_1
```
**Step 6: 请求代码审查**
通知团队成员进行代码审查
---
## 任务清单总结
在开始实施前,确认以下任务清单:
- [x] Task 1: 检查数据库索引
- [ ] Task 2: 修改 VO 类
- [ ] Task 3: 修改 Mapper XML - 列表查询
- [ ] Task 4: 修改 Mapper XML - 详情查询
- [ ] Task 5: 编写接口测试脚本
- [ ] Task 6: 后端编译验证
- [ ] Task 7: 修改列表页面
- [ ] Task 8: 前端编译验证
- [ ] Task 9: 后端集成测试
- [ ] Task 10: 前端集成测试
- [ ] Task 11: 性能测试
- [ ] Task 12: 边界情况测试
- [ ] Task 13: 更新 API 文档
- [ ] Task 14: 更新数据库设计文档
- [ ] Task 15: 生成测试报告
- [ ] Task 16: 自我代码审查
- [ ] Task 17: 整合提交和 PR
**预计总时间:** 2-3 小时
**技术风险:**
**数据风险:** 无(仅查询改动,不影响数据写入)
---
## 实施笔记
在实施过程中记录遇到的问题和解决方案:
```markdown
## 问题记录
### 问题 1: [描述问题]
**时间:** [时间]
**现象:** [问题现象]
**原因:** [问题原因]
**解决:** [解决方案]
### 问题 2: ...
```

View File

@@ -1,428 +0,0 @@
# 员工关系导入身份证号校验设计文档
**日期**: 2026-02-11
**状态**: 设计完成
**优先级**: 中
---
## 1. 需求概述
### 1.1 背景
当前员工实体关系和员工亲属关系的导入功能在导入数据时,没有验证员工身份证号是否在员工信息表中存在。这可能导致导入的数据引用了不存在的员工,造成数据完整性问题。
### 1.2 目标
在员工实体关系和员工亲属关系的导入过程中,添加员工身份证号存在性校验:
- 验证员工身份证号是否在 `ccdi_base_staff` 表中存在
- 不存在的身份证号记录错误信息并跳过
- 继续处理其他有效数据
### 1.3 约束条件
- 仅验证员工身份证号(`person_id`)存在性,不验证关系人身份证号
- 不验证员工状态(在职/离职)
- 错误信息需要包含Excel行号
- 与现有的导入流程保持一致失败记录保存到Redis
### 1.4 优化范围
同时优化员工调动导入的身份证号验证逻辑从2次遍历优化为1次遍历。
---
## 2. 架构设计
### 2.1 整体架构
在两个导入服务中添加**员工身份证号批量预验证阶段**,流程如下:
```
导入流程:
1. 批量查询已存在的身份证号(新增)⭐
2. 数据处理循环(原有,修改)
└─ 循环开始时检查身份证号是否存在(新增)
3. 批量插入新数据(原有)
4. 保存失败记录到Redis原有
5. 更新导入状态(原有)
```
### 2.2 新增组件
#### 2.2.1 依赖注入
在三个导入服务中添加:
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
#### 2.2.2 核心逻辑
- **位置**: 在数据处理循环之前
- **功能**: 批量查询所有Excel中出现的身份证号构建存在性集合
- **输入**: Excel数据列表、任务ID
- **输出**: Set<String> 存在的身份证号集合
### 2.3 影响的服务
| 服务 | 文件 | 说明 |
|------|------|------|
| 员工实体关系导入 | `CcdiStaffEnterpriseRelationImportServiceImpl.java` | 添加身份证号验证 |
| 员工亲属关系导入 | `CcdiStaffFmyRelationImportServiceImpl.java` | 添加身份证号验证 |
| 员工调动导入 | `CcdiStaffTransferImportServiceImpl.java` | 优化为1次遍历 |
---
## 3. 数据流设计
### 3.1 详细流程
```
阶段1: 提取身份证号
├─ 从 excelList 提取所有 personId
├─ 过滤 null 值和空字符串
├─ HashSet 去重
└─ 得到 Set<String> allPersonIds
阶段2: 批量查询
├─ 如果 allPersonIds 为空,返回空集合
├─ 构建查询: SELECT id_card FROM ccdi_base_staff WHERE id_card IN (...)
├─ 执行: baseStaffMapper.selectList(wrapper)
├─ 提取结果中的 idCard
└─ 得到 Set<String> existingPersonIds
阶段3: 数据处理循环(原有循环增强)
├─ 遍历 excelList行号 1-basedi为索引
│ ├─ 【新增】首先检查: personId 是否在 existingPersonIds 中?
│ │ ├─ 如果不在:
│ │ │ ├─ 创建 ImportFailureVO 对象
│ │ │ ├─ 错误信息: "第{i+1}行: 身份证号[{personId}]不存在于员工信息表中"
│ │ │ ├─ 添加到 failures 列表
│ │ │ ├─ 记录验证失败日志
│ │ │ └─ continue跳过后续处理
│ │ └─ 如果在:继续执行原有逻辑
│ ├─ 转换并验证数据(原有)
│ ├─ 检查重复(原有)
│ └─ 添加到 newRecords原有
```
### 3.2 错误信息格式
```java
String errorMessage = String.format("第%d行: 身份证号[%s]不存在于员工信息表中",
rowNumber, personId);
```
### 3.3 日志记录
使用 `ImportLogUtils` 记录:
- 批量查询开始: `logBatchQueryStart(log, taskId, "员工身份证号", count)`
- 批量查询完成: `logBatchQueryComplete(log, taskId, "员工身份证号", count)`
- 验证失败: `logValidationError(log, taskId, rowNumber, errorMessage, keyData)`
---
## 4. 边界情况处理
### 4.1 personId 为 null 或空字符串
- 在提取阶段过滤掉: `.filter(StringUtils::isNotEmpty)`
- 这些记录会在原有的 `validateRelationData` 方法中报错
### 4.2 Excel 为空或所有 personId 为空
- `allPersonIds` 为空集合
- 直接返回空集合,跳过批量查询
- 所有记录会在后续验证中报错
### 4.3 所有身份证号都不存在
- `existingPersonIds` 为空集合
- 所有记录都会在第一次检查时抛出异常
- `newRecords` 保持为空
- 最终状态: `PARTIAL_SUCCESS`
### 4.4 Excel 中有重复身份证号
- 使用 HashSet 去重,只查询一次
- 每行独立检查,如果不存在则各自生成失败记录
### 4.5 数据库中没有员工记录
- `baseStaffMapper.selectList` 返回空列表
- 所有 Excel 行都会在检查时失败
### 4.6 身份证号格式错误
- 先检查身份证号是否存在
- 如果不存在,直接报错"身份证号不存在"
- 如果存在但格式错误,在后续的 `validateRelationData` 中会报错
---
## 5. 性能分析
### 5.1 时间复杂度
- 提取身份证号: O(n)n为Excel行数
- 数据库查询: O(m)m为不重复身份证号数量
- 数据处理循环: O(n)
- **总计: O(n)**,线性复杂度
### 5.2 空间复杂度
- `allPersonIds`: 约 20字节 × m
- `existingPersonIds`: 约 20字节 × m
- **总计: 约 40KB / 1000个不重复身份证号**
### 5.3 数据库查询
- 查询次数: **仅1次**
- 查询类型: `SELECT id_card FROM ccdi_base_staff WHERE id_card IN (...)`
- 索引: `id_card` 字段需要添加索引
### 5.4 性能对比
| 方案 | 数据库查询次数 | 1000行耗时 | 10000行耗时 |
|------|---------------|-----------|------------|
| 批量预验证(本设计) | 1次 | ~50ms | ~200ms |
| 逐条验证 | N次 | ~5000ms | ~50000ms |
**结论**: 批量预验证方案性能提升约**100倍**。
---
## 6. 代码实现
### 6.1 员工实体关系导入服务
**文件**: `CcdiStaffEnterpriseRelationImportServiceImpl.java`
#### 6.1.1 添加依赖注入第44行后
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
#### 6.1.2 在 importRelationAsync 方法中第55行后添加批量查询:
```java
// 【新增】批量验证员工身份证号是否存在
Set<String> excelPersonIds = excelList.stream()
.map(CcdiStaffEnterpriseRelationExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toSet());
Set<String> existingPersonIds = new HashSet<>();
if (!excelPersonIds.isEmpty()) {
ImportLogUtils.logBatchQueryStart(log, taskId, "员工身份证号", excelPersonIds.size());
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiBaseStaff::getIdCard)
.in(CcdiBaseStaff::getIdCard, excelPersonIds);
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
existingPersonIds = existingStaff.stream()
.map(CcdiBaseStaff::getIdCard)
.collect(Collectors.toSet());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工身份证号", existingPersonIds.size());
}
```
#### 6.1.3 在数据处理循环开始处第71行后添加检查:
```java
try {
// 【新增】身份证号存在性检查
if (!existingPersonIds.contains(excel.getPersonId())) {
throw new RuntimeException(String.format(
"第%d行: 身份证号[%s]不存在于员工信息表中",
i + 1, excel.getPersonId()));
}
// 原有逻辑继续...
CcdiStaffEnterpriseRelationAddDTO addDTO = new CcdiStaffEnterpriseRelationAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// ...
}
```
### 6.2 员工亲属关系导入服务
**文件**: `CcdiStaffFmyRelationImportServiceImpl.java`
相同的修改步骤:
1. 添加 `CcdiBaseStaffMapper` 依赖注入
2. 在第57行后添加批量查询身份证号逻辑
3. 在第96行后数据处理循环开始处添加身份证号检查
### 6.3 员工调动导入服务优化
**文件**: `CcdiStaffTransferImportServiceImpl.java`
**优化前**: 2次遍历预验证 + 主循环)
**优化后**: 1次遍历主循环中检查
**修改步骤**:
1. 移除 `batchValidateStaffIds` 方法
2. 移除 `isRowAlreadyFailed` 方法
3. 在主循环开始处添加员工ID存在性检查
4. 使用 `existingStaffIds` 替代原有的预验证逻辑
### 6.4 需要导入的类
```java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper;
import com.ruoyi.ccdi.domain.CcdiBaseStaff;
import java.util.HashSet;
import java.util.stream.Collectors;
```
---
## 7. 测试场景
### 7.1 功能测试用例
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| 正常导入 | 5条有效身份证号 | 全部成功failures为空 |
| 部分无效 | 3条有效 + 2条无效身份证号 | 3条成功2条失败 |
| 全部无效 | 5条全部无效身份证号 | 0条成功5条失败 |
| 身份证号为null | 包含null或空字符串 | 在后续验证中报错 |
| 大批量数据 | 1000条记录全部有效 | 仅1次查询全部成功 |
| 重复身份证号 | 10条记录3个不同身份证号 | 去重查询,正确验证 |
| 混合场景 | 有效、无效、null、重复 | 各自正确处理 |
### 7.2 员工实体关系导入专项测试
```
测试数据1: 有效身份证号
personId: "110101199001011234" (存在于ccdi_base_staff)
预期: 导入成功
测试数据2: 无效身份证号
personId: "999999999999999999" (不存在于ccdi_base_staff)
预期: 导入失败,错误信息: "第N行: 身份证号[xxx]不存在于员工信息表中"
```
### 7.3 员工亲属关系导入专项测试
```
测试数据1: 有效员工身份证号
personId: "110101199001011234" (存在)
relationCertNo: "110101199001011235" (可以不存在)
预期: 导入成功
测试数据2: 无效员工身份证号
personId: "999999999999999999" (不存在)
预期: 导入失败
```
### 7.4 性能测试
| 数据量 | 预期查询次数 | 预期耗时 | 内存占用 |
|--------|------------|---------|---------|
| 100条 | 1次 | < 20ms | < 10KB |
| 1000条 | 1次 | < 100ms | < 50KB |
| 10000条 | 1次 | < 500ms | < 500KB |
---
## 8. 影响范围和实施计划
### 8.1 影响的文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `CcdiStaffEnterpriseRelationImportServiceImpl.java` | 修改 | 添加员工身份证号验证 |
| `CcdiStaffFmyRelationImportServiceImpl.java` | 修改 | 添加员工身份证号验证 |
| `CcdiStaffTransferImportServiceImpl.java` | 优化 | 从2次遍历优化为1次遍历 |
### 8.2 不影响的组件
- ✅ Controller层无需修改
- ✅ 前端页面(无需修改)
- ✅ 数据库表结构(无需修改)
- ✅ Mapper接口无需修改
- ✅ VO/DTO/Excel类无需修改
### 8.3 数据库索引建议
```sql
-- 检查索引是否存在
SHOW INDEX FROM ccdi_base_staff WHERE Column_name = 'id_card';
-- 如果不存在,创建索引
CREATE INDEX idx_ccdi_base_staff_id_card ON ccdi_base_staff(id_card);
```
### 8.4 实施步骤
1. ✅ 完成设计方案
2. ⏳ 修改 `CcdiStaffEnterpriseRelationImportServiceImpl`
3. ⏳ 修改 `CcdiStaffFmyRelationImportServiceImpl`
4. ⏳ 优化 `CcdiStaffTransferImportServiceImpl`
5. ⏳ 检查并创建数据库索引(如需要)
6. ⏳ 编写单元测试
7. ⏳ 本地测试验证
8. ⏳ 更新API文档如需要
### 8.5 验收标准
- [ ] 不存在的员工身份证号被正确识别并记录错误
- [ ] 错误信息包含正确的行号和身份证号
- [ ] 有效数据正常导入
- [ ] 日志记录完整
- [ ] 性能无明显下降批量查询仅1次
- [ ] 与现有导入逻辑保持一致
- [ ] 三个导入服务功能一致
### 8.6 风险评估
- **风险等级**: 低
- **影响范围**: 仅影响导入功能,不影响其他模块
- **回滚方案**: 直接移除新增的验证代码即可
- **数据安全**: 只读查询,无数据风险
---
## 9. 设计总结
### 9.1 核心优势
1. **性能优异**: 批量查询仅1次数据库访问
2. **代码简洁**: 仅1次遍历逻辑清晰
3. **一致性高**: 三个导入服务使用相同的设计模式
4. **易于维护**: 遵循现有框架规范
### 9.2 与原员工调动导入设计的对比
| 对比项 | 原设计 | 新设计 |
|--------|--------|--------|
| 遍历次数 | 2次 | **1次** ⭐ |
| 代码复杂度 | 需要额外方法 | 更简洁 |
| 性能 | 优秀 | **更优** |
| 可维护性 | 良好 | **更好** |
### 9.3 设计亮点
-**批量预验证**: 充分利用数据库 IN 查询性能
-**单次遍历**: 减少不必要的循环,代码更清晰
-**统一模式**: 三个导入服务使用一致的验证逻辑
-**错误友好**: 详细的错误信息包含行号
-**性能监控**: 完整的日志记录便于排查问题
---
## 10. 附录
### 10.1 相关文档
- [员工调动导入员工ID校验设计文档](2026-02-11-staff-transfer-import-staff-id-validation-design.md)
- [若依框架导入功能说明](https://doc.ruoyi.vip/)
- [MyBatis Plus 官方文档](https://baomidou.com/)
### 10.2 设计决策记录
**Q1: 为什么选择批量预验证而非逐条验证?**
A: 批量验证只需1次数据库查询性能提升约100倍。
**Q2: 为什么优化为1次遍历**
A: 减少不必要的循环,代码更简洁,性能更好。
**Q3: 为什么不验证员工在职状态?**
A: 需求明确仅验证身份证号存在性,避免过度设计。
**Q4: 为什么不验证关系人身份证号?**
A: 关系人可能不是系统员工,验证会限制使用场景。
### 10.3 版本历史
- v1.0 (2026-02-11): 初始设计版本,包含三个导入服务的身份证号校验

View File

@@ -1,384 +0,0 @@
# 员工调动导入员工ID校验设计文档
**日期**: 2026-02-11
**状态**: 设计完成
**优先级**: 中
---
## 1. 需求概述
### 1.1 背景
当前员工调动导入功能(`CcdiStaffTransferImportServiceImpl`)在导入数据时没有验证员工ID是否在员工信息表中存在。这可能导致导入的数据引用了不存在的员工ID造成数据完整性问题。
### 1.2 目标
在员工调动导入过程中添加员工ID存在性校验
- 验证员工ID是否在 `ccdi_base_staff` 表中存在
- 不存在的员工ID记录错误信息并跳过
- 继续处理其他有效数据
### 1.3 约束条件
- 仅验证员工ID存在性不验证员工状态
- 错误信息需要包含Excel行号
- 与现有的导入流程保持一致失败记录保存到Redis
---
## 2. 架构设计
### 2.1 整体架构
在现有的 `CcdiStaffTransferImportServiceImpl` 中,在 `importTransferAsync` 方法的数据处理循环之前,添加一个**员工ID批量预验证阶段**。
```
导入流程:
1. 批量查询已存在的调动记录唯一键(原有)
2. 批量验证员工ID是否存在新增
3. 分类数据循环处理(原有,修改)
└─ 跳过已在预验证阶段失败的记录(新增)
4. 批量插入新数据(原有)
5. 保存失败记录到Redis原有
6. 更新导入状态(原有)
```
### 2.2 新增组件
#### 2.2.1 依赖注入
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
#### 2.2.2 核心方法
**方法1: batchValidateStaffIds**
- 功能: 批量验证员工ID是否存在
- 输入: Excel数据列表、任务ID、失败记录列表
- 输出: 存在的员工ID集合
- 位置: 第65行之前调用
**方法2: isRowAlreadyFailed**
- 功能: 检查某行数据是否已在失败列表中
- 输入: Excel数据、失败记录列表
- 输出: boolean
- 位置: 主循环中使用
---
## 3. 数据流设计
### 3.1 详细流程
```
阶段1: 提取员工ID新增
├─ 从 excelList 提取所有 staffId
├─ 过滤 null 值
├─ HashSet 去重
└─ 得到 Set<Long> allStaffIds
阶段2: 批量查询(新增)
├─ 如果 allStaffIds 为空,返回空集合
├─ 构建查询: WHERE staffId IN (...)
├─ 执行: baseStaffMapper.selectList(wrapper)
├─ 提取结果中的 staffId
└─ 得到 Set<Long> existingStaffIds
阶段3: 预验证(新增)
├─ 遍历 excelList行号 1-based
│ ├─ 提取当前行的 staffId
│ ├─ 如果 staffId 不在 existingStaffIds 中:
│ │ ├─ 创建 StaffTransferImportFailureVO
│ │ ├─ 错误信息: "第{行号}行: 员工ID {staffId} 不存在"
│ │ ├─ 添加到 failures 列表
│ │ └─ 记录验证失败日志
│ └─ 否则,继续处理
└─ 返回 existingStaffIds
阶段4: 原有数据处理循环(修改)
└─ 循环开始时检查:
└─ 如果当前行已在 failures 中,跳过
└─ 否则,执行原有处理逻辑
```
### 3.2 错误信息格式
```java
String errorMessage = String.format("第%d行: 员工ID %s 不存在",
rowNumber, staffId);
```
### 3.3 日志记录
使用 `ImportLogUtils` 记录:
- 批量查询开始: `logBatchQueryStart(log, taskId, "员工ID", count)`
- 批量查询完成: `logBatchQueryComplete(log, taskId, "员工ID", count)`
- 验证失败: `logValidationError(log, taskId, rowNumber, errorMessage, keyData)`
---
## 4. 代码实现
### 4.1 新增方法实现
#### 4.1.1 batchValidateStaffIds
```java
/**
* 批量验证员工ID是否存在
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param failures 失败记录列表(会追加验证失败的记录)
* @return 存在的员工ID集合
*/
private Set<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
String taskId,
List<StaffTransferImportFailureVO> failures) {
// 1. 提取并去重员工ID
Set<Long> allStaffIds = excelList.stream()
.map(CcdiStaffTransferExcel::getStaffId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (allStaffIds.isEmpty()) {
return Collections.emptySet();
}
// 2. 批量查询存在的员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID", allStaffIds.size());
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiBaseStaff::getStaffId)
.in(CcdiBaseStaff::getStaffId, allStaffIds);
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
Set<Long> existingStaffIds = existingStaff.stream()
.map(CcdiBaseStaff::getStaffId)
.collect(Collectors.toSet());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工ID", existingStaffIds.size());
// 3. 预验证并标记不存在的员工ID
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
Long staffId = excel.getStaffId();
if (staffId != null && !existingStaffIds.contains(staffId)) {
StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(String.format("第%d行: 员工ID %s 不存在", i + 1, staffId));
failures.add(failure);
String keyData = String.format("员工ID=%s", staffId);
ImportLogUtils.logValidationError(log, taskId, i + 1,
failure.getErrorMessage(), keyData);
}
}
return existingStaffIds;
}
```
#### 4.1.2 isRowAlreadyFailed
```java
/**
* 检查某行数据是否已在失败列表中
*
* @param excel Excel数据
* @param failures 失败记录列表
* @return true-已失败false-未失败
*/
private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel,
List<StaffTransferImportFailureVO> failures) {
return failures.stream()
.anyMatch(f -> f.getStaffId().equals(excel.getStaffId())
&& Objects.equals(f.getTransferDate(), excel.getTransferDate())
&& Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore())
&& Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter()));
}
```
### 4.2 主循环修改
`importTransferAsync` 方法的第 73 行开始:
```java
// 原有代码
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
try {
// ...原有处理逻辑
// 修改为
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
// 新增: 跳过已在预验证阶段失败的记录
if (isRowAlreadyFailed(excel, failures)) {
continue;
}
try {
// ...原有处理逻辑
```
### 4.3 调用位置
`importTransferAsync` 方法中,第 65 行之后插入:
```java
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
// 新增: 批量验证员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID预验证", excelList.size());
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);
// 原有代码继续
// 批量查询已存在的唯一键组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
Set<String> existingKeys = getExistingTransferKeys(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size());
```
---
## 5. 边界情况处理
### 5.1 员工ID为null
```java
// 在提取时过滤null
.filter(Objects::nonNull)
// 在预验证时跳过留给后续validateTransferData处理
if (staffId == null) {
continue;
}
```
### 5.2 Excel为空或所有员工ID为null
```java
if (allStaffIds.isEmpty()) {
return Collections.emptySet();
}
```
### 5.3 所有员工ID都不存在
- `existingStaffIds` 为空集合
- 所有记录都会被加入 `failures`
- `newRecords` 保持为空
- 最终状态: `PARTIAL_SUCCESS`
### 5.4 Excel中有重复员工ID
- 使用 HashSet 去重,只查询一次
- 预验证时每行都会独立检查并生成对应的失败记录
### 5.5 数据库中没有员工记录
- `baseStaffMapper.selectList` 返回空列表
- 所有Excel行都会标记为失败
---
## 6. 性能分析
### 6.1 时间复杂度
- 提取员工ID: O(n)n为Excel行数
- 数据库查询: O(m)m为不重复员工ID数量
- 预验证: O(n)
- **总计: O(n)**
### 6.2 空间复杂度
- `allStaffIds`: 约 8字节 × m
- `existingStaffIds`: 约 8字节 × m
- **总计: 约 16KB / 1000个不重复员工ID**
### 6.3 数据库查询
- 查询次数: **仅1次**
- 查询类型: `SELECT staffId FROM ccdi_base_staff WHERE staffId IN (...)`
- 索引: `staffId` 为主键,性能最优
---
## 7. 测试场景
### 7.1 功能测试
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| 正常导入 | 5条有效员工ID | 全部成功failures为空 |
| 部分无效 | 3条有效 + 2条无效 | 3条成功2条失败 |
| 全部无效 | 5条全部无效 | 0条成功5条失败 |
| 员工ID为null | 包含null记录 | 在后续验证中报错 |
| 大批量数据 | 1000条记录 | 仅1次查询性能良好 |
| 重复员工ID | 10条记录3个不同ID | 去重查询,正确验证 |
### 7.2 集成测试
- 验证Redis中失败记录格式正确
- 验证导入状态API返回正确
- 验证日志输出完整
- 验证事务回滚正常
---
## 8. 影响范围
### 8.1 影响的文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `CcdiStaffTransferImportServiceImpl.java` | 修改 | 添加员工ID验证逻辑 |
### 8.2 不影响的组件
- ✅ Controller层无需修改
- ✅ 前端页面(无需修改)
- ✅ 数据库表结构(无需修改)
- ✅ 其他导入服务(建议后续同步修改)
### 8.3 建议同步修改的服务
为了保持一致性建议对以下导入服务添加相同的员工ID验证
- `CcdiIntermediaryEntityImportServiceImpl` - 员工中介实体导入
- `CcdiIntermediaryPersonImportServiceImpl` - 员工中介人员导入
- `CcdiStaffRecruitmentImportServiceImpl` - 员工招聘导入
- `CcdiBaseStaffImportServiceImpl` - 员工信息导入
---
## 9. 实施计划
### 9.1 实施步骤
1. ✅ 完成设计方案
2. ⏳ 修改 `CcdiStaffTransferImportServiceImpl`
3. ⏳ 编写单元测试
4. ⏳ 本地测试验证
5. ⏳ 提交代码并生成API文档
6. ⏳ 同步修改其他导入服务(可选)
### 9.2 验收标准
- [x] 不存在的员工ID被正确识别并记录错误
- [x] 错误信息包含正确的行号
- [x] 有效数据正常导入
- [x] 日志记录完整
- [x] 性能无明显下降
- [x] 与现有导入逻辑保持一致
---
## 10. 附录
### 10.1 相关文档
- [若依框架导入功能说明](https://doc.ruoyi.vip/)
- [MyBatis Plus 官方文档](https://baomidou.com/)
### 10.2 设计决策记录
- **Q1: 为什么选择批量预验证而非逐条验证?**
- A: 批量验证只需1次数据库查询性能更好且符合现有部门验证的模式
- **Q2: 为什么不验证员工在职状态?**
- A: 需求明确仅验证员工ID存在性避免过度设计
- **Q3: 为什么选择跳过无效记录而非停止导入?**
- A: 与现有导入逻辑一致,最大化导入成功率
### 10.3 版本历史
- v1.0 (2026-02-11): 初始设计版本

View File

@@ -1,508 +0,0 @@
# 员工调动导入员工ID校验功能实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 在员工调动导入功能中添加员工ID存在性校验确保只导入有效员工的调动记录
**架构:** 采用批量预验证模式在数据处理循环前执行一次批量数据库查询验证所有员工ID不存在的记录提前标记为失败并跳过后续处理
**技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, Java 17, Redis
---
## Task 1: 添加 CcdiBaseStaffMapper 依赖注入
**文件:**
- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:48`
**Step 1: 添加依赖注入字段**
在第48行 `SysDeptMapper deptMapper` 之后添加:
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
**Step 2: 验证编译**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 3: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java
git commit -m "feat: 添加CcdiBaseStaffMapper依赖注入
为员工调动导入服务添加员工信息Mapper用于批量验证员工ID存在性"
```
---
## Task 2: 实现批量验证员工ID方法
**文件:**
- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java` (在文件末尾添加私有方法)
**Step 1: 编写批量验证方法**
`getImportFailures` 方法之后添加:
```java
/**
* 批量验证员工ID是否存在
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param failures 失败记录列表(会追加验证失败的记录)
* @return 存在的员工ID集合
*/
private Set<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
String taskId,
List<StaffTransferImportFailureVO> failures) {
// 1. 提取并去重员工ID
Set<Long> allStaffIds = excelList.stream()
.map(CcdiStaffTransferExcel::getStaffId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (allStaffIds.isEmpty()) {
return Collections.emptySet();
}
// 2. 批量查询存在的员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID", allStaffIds.size());
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiBaseStaff::getStaffId)
.in(CcdiBaseStaff::getStaffId, allStaffIds);
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
Set<Long> existingStaffIds = existingStaff.stream()
.map(CcdiBaseStaff::getStaffId)
.collect(Collectors.toSet());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工ID", existingStaffIds.size());
// 3. 预验证并标记不存在的员工ID
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
Long staffId = excel.getStaffId();
if (staffId != null && !existingStaffIds.contains(staffId)) {
StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(String.format("第%d行: 员工ID %s 不存在", i + 1, staffId));
failures.add(failure);
String keyData = String.format("员工ID=%s", staffId);
ImportLogUtils.logValidationError(log, taskId, i + 1,
failure.getErrorMessage(), keyData);
}
}
return existingStaffIds;
}
```
**Step 2: 验证编译**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 3: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java
git commit -m "feat: 实现批量验证员工ID方法
- 提取Excel中所有员工ID并去重
- 批量查询数据库中存在的员工ID
- 标记不存在的员工ID为失败记录
- 记录详细的验证日志"
```
---
## Task 3: 实现检查行是否已失败方法
**文件:**
- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java` (在 batchValidateStaffIds 方法之后)
**Step 1: 编写检查方法**
```java
/**
* 检查某行数据是否已在失败列表中
*
* @param excel Excel数据
* @param failures 失败记录列表
* @return true-已失败false-未失败
*/
private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel,
List<StaffTransferImportFailureVO> failures) {
return failures.stream()
.anyMatch(f -> f.getStaffId().equals(excel.getStaffId())
&& Objects.equals(f.getTransferDate(), excel.getTransferDate())
&& Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore())
&& Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter()));
}
```
**Step 2: 验证编译**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 3: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java
git commit -m "feat: 实现检查行是否已失败方法
通过比较员工ID、调动日期、调动前部门ID、调动后部门ID判断该行是否已在失败列表中"
```
---
## Task 4: 在导入方法中调用批量验证
**文件:**
- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:62-68`
**Step 1: 修改导入方法初始化部分**
在第62-68行将:
```java
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的唯一键组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
Set<String> existingKeys = getExistingTransferKeys(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size());
```
修改为:
```java
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
// 批量验证员工ID是否存在
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);
// 批量查询已存在的唯一键组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
Set<String> existingKeys = getExistingTransferKeys(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size());
```
**Step 2: 验证编译**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 3: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java
git commit -m "feat: 在导入流程中添加员工ID批量验证
在数据处理循环前添加员工ID存在性验证阶段提前标记无效员工ID的记录"
```
---
## Task 5: 在主循环中跳过已失败记录
**文件:**
- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:73-78`
**Step 1: 修改主循环开始部分**
在第73-78行将:
```java
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
try {
```
修改为:
```java
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
// 跳过已在预验证阶段失败的记录
if (isRowAlreadyFailed(excel, failures)) {
continue;
}
try {
```
**Step 2: 验证编译**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 3: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java
git commit -m "feat: 主循环跳过已失败的记录
在数据处理循环中添加检查逻辑,跳过已在预验证阶段标记为失败的记录"
```
---
## Task 6: 编写测试脚本
**文件:**
- 创建: `doc/test-data/staff-transfer-validation-test.http`
**Step 1: 创建HTTP测试文件**
```http
### ID
### 1. Token
POST http://localhost:8080/login/test
Content-Type: application/x-www-form-urlencoded
username=admin&password=admin123
> {%
client.global.set("token", response.body.token);
client.log("Token: " + response.body.token);
%}
### 2. ID
POST http://localhost:8080/ccdi/staffTransfer/import
Authorization: Bearer {{token}}
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="valid-staff-ids.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
< ./valid-staff-ids.xlsx
--boundary--
### 3. ID
POST http://localhost:8080/ccdi/staffTransfer/import
Authorization: Bearer {{token}}
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="partial-invalid-ids.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
< ./partial-invalid-ids.xlsx
--boundary--
### 4. ID
POST http://localhost:8080/ccdi/staffTransfer/import
Authorization: Bearer {{token}}
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="all-invalid-ids.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
< ./all-invalid-ids.xlsx
--boundary--
### 5.
GET http://localhost:8080/ccdi/staffTransfer/import/status/{{taskId}}
Authorization: Bearer {{token}}
### 6.
GET http://localhost:8080/ccdi/staffTransfer/import/failures/{{taskId}}
Authorization: Bearer {{token}}
```
**Step 2: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add doc/test-data/staff-transfer-validation-test.http
git commit -m "test: 添加员工ID验证测试脚本
包含正常导入、部分无效、全部无效等测试场景"
```
---
## Task 7: 生成本次修改的API文档
**文件:**
- 修改: `doc/interface-doc/ccdi/staff-transfer.md` (如果文件不存在则创建)
**Step 1: 更新API文档**
在现有的员工调动导入接口文档中,添加错误情况说明:
```markdown
### 员工调动导入
**接口地址:** `POST /ccdi/staffTransfer/import`
**请求参数:**
- file: Excel文件multipart/form-data
**响应格式:**
```json
{
"code": 200,
"msg": "导入任务已提交",
"data": {
"taskId": "uuid"
}
}
```
**错误情况:**
| 错误类型 | 错误信息示例 | 说明 |
|---------|-------------|------|
| 员工ID不存在 | 第3行: 员工ID 99999 不存在 | 该员工ID在员工信息表中不存在 |
| 员工ID为空 | 员工ID不能为空 | Excel中未填写员工ID |
| 调动类型无效 | 调动类型[xxx]无效 | 调动类型不在字典中 |
| 部门ID不存在 | 部门ID 999 不存在 | 调动前/后部门ID在部门表中不存在 |
| 记录重复 | 该员工在2026-01-01的调动记录已存在 | 数据库中已存在相同的调动记录 |
**导入状态查询:**
使用返回的 `taskId` 查询导入进度和结果。
**失败记录查询:**
导入失败或部分成功时,可通过 `taskId` 获取详细的失败记录列表。
```
**Step 2: 提交**
```bash
cd .worktrees/staff-transfer-validation
git add doc/interface-doc/ccdi/staff-transfer.md
git commit -m "docs: 更新员工调动导入API文档
添加员工ID验证相关的错误情况说明"
```
---
## Task 8: 最终验证和测试
**Step 1: 编译项目**
Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q`
Expected: 编译成功,无错误
**Step 2: 运行测试(如果有单元测试)**
Run: `cd .worktrees/staff-transfer-validation && mvn test -Dtest=*StaffTransferImport* -q`
Expected: 测试通过
**Step 3: 代码审查检查清单**
- [ ] 所有新增方法都有完整的JavaDoc注释
- [ ] 错误信息包含行号,便于用户定位
- [ ] 使用ImportLogUtils记录详细的验证日志
- [ ] 仅执行1次数据库查询批量验证所有员工ID
- [ ] 失败记录正确保存到Redis
- [ ] 与现有导入逻辑保持一致(跳过失败记录继续处理)
- [ ] 代码风格符合项目规范
- [ ] 无hardcode的字符串或数字
**Step 4: 最终提交**
```bash
cd .worktrees/staff-transfer-validation
git add -A
git commit -m "feat: 完成员工调动导入员工ID校验功能
功能实现:
- 批量预验证员工ID存在性1次数据库查询
- 不存在的员工ID记录错误并跳过
- 错误信息包含Excel行号
- 完整的日志记录
技术实现:
- 新增 batchValidateStaffIds() 方法
- 新增 isRowAlreadyFailed() 方法
- 修改 importTransferAsync() 主流程
- 添加 CcdiBaseStaffMapper 依赖
测试:
- 添加HTTP测试脚本
- 更新API文档
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
```
---
## 实施后任务
### 合并到主分支
**Step 1: 切换到dev_1分支**
```bash
cd D:\ccdi\ccdi
git checkout dev_1
git pull origin dev_1
```
**Step 2: 合并feature分支**
```bash
git merge feat/staff-transfer-staff-id-validation --no-ff
```
**Step 3: 推送到远程**
```bash
git push origin dev_1
```
**Step 4: 清理worktree**
```bash
git worktree remove .worktrees/staff-transfer-validation
git branch -d feat/staff-transfer-staff-id-validation
```
---
## 附录
### 相关文档
- 设计文档: `doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md`
- 员工调动接口文档: `doc/interface-doc/ccdi/staff-transfer.md`
- 导入服务代码: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java`
### 依赖服务
- 数据库: ccdi_intermediary_blacklist
- Redis: 用于存储导入状态和失败记录
### 测试数据准备
需要在 `doc/test-data/` 目录下准备测试Excel文件
- `valid-staff-ids.xlsx`: 包含有效员工ID的调动记录
- `partial-invalid-ids.xlsx`: 包含部分无效员工ID的调动记录
- `all-invalid-ids.xlsx`: 所有员工ID都无效的调动记录

View File

@@ -1,93 +0,0 @@
# ruoyi-ccdi 模块重命名设计文档
## 概述
`ruoyi-ccdi` 模块重命名为 `ruoyi-info-collection`,以更清晰地表达"信息采集"功能,同时保持与其他功能模块的命名一致性。
## 设计决策
### 方案选择:混合命名(方案 A
| 项目 | 当前命名 | 目标命名 |
|-----|---------|---------|
| Maven 模块 | `ruoyi-ccdi` | `ruoyi-info-collection` |
| Java 包名 | `com.ruoyi.ccdi` | `com.ruoyi.info.collection` |
| 数据库表 | `ccdi_*` | `ccdi_*` (保持不变) |
| API URL | `/ccdi/*` | `/ccdi/*` (保持不变) |
| 权限标识 | `ccdi:*:*` | `ccdi:*:*` (保持不变) |
| 前端文件 | `ccdi*` | `ccdi*` (保持不变) |
### 选择理由
1. **模块名和包名**:更清晰表达"信息采集"功能
2. **保留 ccdi 前缀**:在 URL、表名、前端避免破坏性变更
3. **数据库不变**:无需迁移数据,降低风险
4. **API 不变**:前端调用无需修改
## 修改清单
### 1. Maven 模块重命名
| 文件 | 修改内容 |
|-----|---------|
| `pom.xml` (根目录) | `<module>ruoyi-ccdi</module>``<module>ruoyi-info-collection</module>` |
| `pom.xml` (根目录) | `<artifactId>ruoyi-ccdi</artifactId>``<artifactId>ruoyi-info-collection</artifactId>` |
| `ruoyi-ccdi/pom.xml` | 目录重命名为 `ruoyi-info-collection/``<artifactId>` 同步修改 |
| `ruoyi-admin/pom.xml` | `<artifactId>ruoyi-ccdi</artifactId>``<artifactId>ruoyi-info-collection</artifactId>` |
### 2. Java 包名重命名
- **目录结构**`com/ruoyi/ccdi/``com/ruoyi/info/collection/`
- **涉及文件**:约 100+ 个 Java 文件
- **修改内容**
- 所有 `package com.ruoyi.ccdi``package com.ruoyi.info.collection`
- 所有 `import com.ruoyi.ccdi.*``import com.ruoyi.info.collection.*`
### 3. MyBatis XML 命名空间
- **涉及文件**11 个 Mapper XML 文件
- **修改内容**:命名空间从 `com.ruoyi.ccdi.mapper.*` 改为 `com.ruoyi.info.collection.mapper.*`
### 4. 项目文档修改
- **涉及文件**`doc/` 目录下约 135 个文件
- **修改内容**:将 `ruoyi-ccdi` 模块引用改为 `ruoyi-info-collection`
## 不修改的内容
- 数据库表名 (`ccdi_*`)
- 数据库名 (`ccdi`)
- API URL 路径 (`/ccdi/*`)
- 权限标识 (`ccdi:*:*`)
- 前端 API 文件和视图目录
- 菜单配置数据
## 执行步骤
1. 重命名模块目录 `ruoyi-ccdi/``ruoyi-info-collection/`
2. 修改 Maven 配置文件
3. 批量修改 Java 包名
4. 修改 MyBatis XML 命名空间
5. 更新项目文档
6. 验证编译 `mvn clean compile`
## 风险评估
- **风险等级**:中
- **主要风险**:包名修改涉及大量文件,可能遗漏
- **缓解措施**
- 使用 IDE 的重构功能
- 编译验证确保无遗漏
- 执行单元测试
## 验收标准
1. Maven 编译成功 (`mvn clean compile`)
2. 所有 Java 文件包名正确
3. MyBatis XML 命名空间正确
4. 文档中模块名称已更新
---
**设计日期**2026-02-24
**设计状态**:已批准

View File

@@ -1,331 +0,0 @@
# ruoyi-ccdi 模块重命名实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将 ruoyi-ccdi 模块重命名为 ruoyi-info-collection同时将 Java 包名从 com.ruoyi.ccdi 改为 com.ruoyi.info.collection
**Architecture:** Maven 模块重命名 + Java 包结构重组 + MyBatis XML 命名空间更新。保留数据库表名、API URL、权限标识和前端文件中的 ccdi 前缀不变。
**Tech Stack:** Maven, Java 17, MyBatis Plus, Spring Boot 3
---
## Task 1: 重命名模块目录
**Files:**
- Rename: `ruoyi-ccdi/``ruoyi-info-collection/`
**Step 1: 使用 git mv 重命名目录**
```bash
git mv ruoyi-ccdi ruoyi-info-collection
```
**Step 2: 验证目录已重命名**
Run: `ls -la | grep ruoyi-info-collection`
Expected: 显示 `ruoyi-info-collection` 目录
---
## Task 2: 修改根 pom.xml 模块声明
**Files:**
- Modify: `pom.xml`
**Step 1: 修改 module 声明**
找到 `<module>ruoyi-ccdi</module>` 并修改为:
```xml
<module>ruoyi-info-collection</module>
```
**Step 2: 修改 dependencyManagement 中的 artifactId**
找到 ruoyi-ccdi 的依赖声明并修改为:
```xml
<!-- 信息采集模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-info-collection</artifactId>
<version>${ruoyi.version}</version>
</dependency>
```
**Step 3: 验证修改**
Run: `grep -n "ruoyi-info-collection" pom.xml`
Expected: 显示 2 处匹配module 和 dependency
---
## Task 3: 修改 ruoyi-info-collection 模块 pom.xml
**Files:**
- Modify: `ruoyi-info-collection/pom.xml`
**Step 1: 修改 artifactId 和 description**
```xml
<artifactId>ruoyi-info-collection</artifactId>
<description>信息采集模块</description>
```
**Step 2: 验证修改**
Run: `grep -n "artifactId" ruoyi-info-collection/pom.xml | head -1`
Expected: `<artifactId>ruoyi-info-collection</artifactId>`
---
## Task 4: 修改 ruoyi-admin 的依赖声明
**Files:**
- Modify: `ruoyi-admin/pom.xml`
**Step 1: 修改依赖 artifactId**
找到 ruoyi-ccdi 依赖并修改为:
```xml
<!-- 信息采集模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-info-collection</artifactId>
</dependency>
```
**Step 2: 验证修改**
Run: `grep -n "ruoyi-info-collection" ruoyi-admin/pom.xml`
Expected: 显示 1 处匹配
---
## Task 5: 创建新的包目录结构
**Files:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/`
- Create: `ruoyi-info-collection/src/main/resources/mapper/info/collection/`
**Step 1: 创建 Java 包目录**
```bash
mkdir -p ruoyi-info-collection/src/main/java/com/ruoyi/info/collection
```
**Step 2: 创建 MyBatis mapper 目录**
```bash
mkdir -p ruoyi-info-collection/src/main/resources/mapper/info/collection
```
**Step 3: 验证目录创建**
Run: `ls -la ruoyi-info-collection/src/main/java/com/ruoyi/info/`
Expected: 显示 `collection` 目录
---
## Task 6: 移动 Java 源码到新包结构
**Files:**
- Move: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/*``ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/`
**Step 1: 移动所有子目录**
```bash
cd ruoyi-info-collection/src/main/java/com/ruoyi
mv ccdi/* info/collection/
```
**Step 2: 删除旧目录**
```bash
rm -rf ccdi
```
**Step 3: 验证新结构**
Run: `ls ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/`
Expected: 显示 controller, domain, enums, mapper, service, utils 等目录
---
## Task 7: 批量修改 Java 文件包名声明
**Files:**
- Modify: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/**/*.java` (约 100+ 文件)
**Step 1: 批量替换 package 声明**
```bash
find ruoyi-info-collection/src/main/java -name "*.java" -exec sed -i 's/package com\.ruoyi\.ccdi/package com.ruoyi.info.collection/g' {} +
```
**Step 2: 批量替换 import 语句**
```bash
find ruoyi-info-collection/src/main/java -name "*.java" -exec sed -i 's/import com\.ruoyi\.ccdi/import com.ruoyi.info.collection/g' {} +
```
**Step 3: 验证包名修改**
Run: `grep -r "package com.ruoyi.ccdi" ruoyi-info-collection/src/main/java/`
Expected: 无输出(所有旧的包名已替换)
---
## Task 8: 移动 MyBatis XML 文件
**Files:**
- Move: `ruoyi-info-collection/src/main/resources/mapper/ccdi/*``ruoyi-info-collection/src/main/resources/mapper/info/collection/`
**Step 1: 移动 XML 文件**
```bash
cd ruoyi-info-collection/src/main/resources/mapper
mkdir -p info/collection
mv ccdi/* info/collection/
rm -rf ccdi
```
**Step 2: 验证文件移动**
Run: `ls ruoyi-info-collection/src/main/resources/mapper/info/collection/`
Expected: 显示 11 个 XML 文件
---
## Task 9: 修改 MyBatis XML 命名空间
**Files:**
- Modify: `ruoyi-info-collection/src/main/resources/mapper/info/collection/*.xml` (11 文件)
**Step 1: 批量替换命名空间**
```bash
find ruoyi-info-collection/src/main/resources/mapper -name "*.xml" -exec sed -i 's/com\.ruoyi\.ccdi/com.ruoyi.info.collection/g' {} +
```
**Step 2: 验证命名空间修改**
Run: `grep -r "com.ruoyi.ccdi" ruoyi-info-collection/src/main/resources/mapper/`
Expected: 无输出(所有旧的命名空间已替换)
---
## Task 10: 更新 CLAUDE.md 项目文档
**Files:**
- Modify: `CLAUDE.md`
**Step 1: 更新模块架构描述**
将所有 `ruoyi-ccdi` 引用改为 `ruoyi-info-collection`,包括:
- 模块架构图
- 模块依赖关系
- ruoyi-ccdi 业务模块描述
- 重要文件路径
**Step 2: 验证修改**
Run: `grep "ruoyi-ccdi" CLAUDE.md`
Expected: 无输出(所有引用已更新)
---
## Task 11: 更新 doc 目录下的文档
**Files:**
- Modify: `doc/**/*.md` (约 135 文件)
**Step 1: 批量替换模块名引用**
```bash
find doc -name "*.md" -exec sed -i 's/ruoyi-ccdi/ruoyi-info-collection/g' {} +
```
**Step 2: 验证修改**
Run: `grep -r "ruoyi-ccdi" doc/`
Expected: 仅在设计文档中保留历史记录
---
## Task 12: 验证 Maven 编译
**Files:**
- None (验证步骤)
**Step 1: 清理并编译**
```bash
mvn clean compile
```
Expected: BUILD SUCCESS
**Step 2: 如果编译失败,检查错误**
常见的编译错误:
- 遗漏的 import 语句
- 遗漏的包名声明
- MyBatis XML 命名空间不匹配
---
## Task 13: 提交更改
**Files:**
- None (Git 操作)
**Step 1: 查看更改**
```bash
git status
git diff --stat
```
**Step 2: 添加所有更改**
```bash
git add -A
```
**Step 3: 提交**
```bash
git commit -m "$(cat <<'EOF'
refactor: 重命名 ruoyi-ccdi 模块为 ruoyi-info-collection
- Maven 模块从 ruoyi-ccdi 重命名为 ruoyi-info-collection
- Java 包名从 com.ruoyi.ccdi 改为 com.ruoyi.info.collection
- MyBatis XML 命名空间同步更新
- 保留数据库表名、API URL、权限标识中的 ccdi 前缀
- 更新项目文档中的模块引用
EOF
)"
```
---
## 验收清单
- [ ] 模块目录已重命名为 `ruoyi-info-collection`
- [ ] 所有 pom.xml 中的 artifactId 已更新
- [ ] Java 包结构已重组为 `com.ruoyi.info.collection`
- [ ] 所有 Java 文件的 package 声明已更新
- [ ] 所有 Java 文件的 import 语句已更新
- [ ] MyBatis XML 文件已移动到新目录
- [ ] MyBatis XML 命名空间已更新
- [ ] 项目文档已更新
- [ ] Maven 编译成功
- [ ] 更改已提交到 Git
---
**计划日期**: 2026-02-24
**预计任务数**: 13

View File

@@ -1,943 +0,0 @@
# 创建项目功能 - 后端实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 实现创建项目功能的后端接口包括数据库表、实体类、DTO/VO、Mapper、Service、Controller
**架构:** 基于若依框架 + MyBatis Plus采用分层架构Controller -> Service -> Mapper
**技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, MySQL 8.2.0, SpringDoc OpenAPI 2.8.14
---
## 前置条件
- MySQL 数据库已启动
- 后端项目已启动
- 已有 admin 账号和测试权限
- 数据库连接配置正确
---
## Task 1: 创建数据库表和字典数据
**文件:**
- Create: `sql/ccdi_project.sql`
**Step 1: 创建 SQL 脚本文件**
创建文件 `sql/ccdi_project.sql`,内容如下:
```sql
-- 创建项目表
CREATE TABLE `ccdi_project` (
`project_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '项目ID',
`project_name` VARCHAR(100) NOT NULL COMMENT '项目名称',
`project_desc` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
`config_type` VARCHAR(20) NOT NULL DEFAULT 'default' COMMENT '配置方式default-全局默认custom-自定义',
`project_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态0-进行中1-已完成2-已归档',
`target_count` INT NOT NULL DEFAULT 0 COMMENT '目标人数',
`high_risk_count` INT NOT NULL DEFAULT 0 COMMENT '高风险人数',
`medium_risk_count` INT NOT NULL DEFAULT 0 COMMENT '中风险人数',
`low_risk_count` INT NOT NULL DEFAULT 0 COMMENT '低风险人数',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`project_id`),
INDEX `idx_project_name` (`project_name`),
INDEX `idx_project_status` (`project_status`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='纪检初核项目表';
-- 插入项目状态字典
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('项目状态', 'ccdi_project_status', '0', 'admin', NOW(), '纪检初核项目状态');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time)
VALUES
(1, '进行中', '0', 'ccdi_project_status', '', 'primary', 'Y', '0', 'admin', NOW()),
(2, '已完成', '1', 'ccdi_project_status', '', 'success', 'N', '0', 'admin', NOW()),
(3, '已归档', '2', 'ccdi_project_status', '', 'info', 'N', '0', 'admin', NOW());
-- 插入配置方式字典
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('配置方式', 'ccdi_config_type', '0', 'admin', NOW(), '项目配置方式');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time)
VALUES
(1, '全局默认模型参数配置', 'default', 'ccdi_config_type', '', 'primary', 'Y', '0', 'admin', NOW()),
(2, '自定义项目规则参数配置', 'custom', 'ccdi_config_type', '', 'warning', 'N', '0', 'admin', NOW());
-- 插入菜单权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES ('纪检初核管理', 0, 1, 'ccdi', NULL, 'M', '0', '0', '', 'monitor', 'admin', NOW());
SET @parent_id = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES ('项目管理', @parent_id, 1, 'project', 'ccdiProject/index', 'C', '0', '0', 'ccdi:project:list', 'project', 'admin', NOW());
SET @menu_id = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time)
VALUES
('创建项目', @menu_id, 1, 'F', '0', '0', 'ccdi:project:add', 'admin', NOW()),
('编辑项目', @menu_id, 2, 'F', '0', '0', 'ccdi:project:edit', 'admin', NOW()),
('删除项目', @menu_id, 3, 'F', '0', '0', 'ccdi:project:remove', 'admin', NOW()),
('查询项目', @menu_id, 4, 'F', '0', '0', 'ccdi:project:query', 'admin', NOW());
-- 为管理员角色分配权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE perms LIKE 'ccdi:project:%' OR perms = 'ccdi:project:list';
```
**Step 2: 执行 SQL 脚本**
运行命令连接数据库并执行脚本:
```bash
mysql -h<host> -u<user> -p<password> ccdi < sql/ccdi_project.sql
```
预期输出:无错误,表创建成功
**Step 3: 验证数据库表**
连接数据库验证表是否创建成功:
```bash
mysql -h<host> -u<user> -p<password> -e "USE ccdi; SHOW TABLES LIKE 'ccdi_project'; DESC ccdi_project;"
```
预期输出:显示 `ccdi_project` 表及其字段结构
**Step 4: 验证字典数据**
验证字典数据是否插入成功:
```bash
mysql -h<host> -u<user> -p<password> -e "USE ccdi; SELECT * FROM sys_dict_type WHERE dict_type IN ('ccdi_project_status', 'ccdi_config_type'); SELECT * FROM sys_dict_data WHERE dict_type IN ('ccdi_project_status', 'ccdi_config_type');"
```
预期输出:显示新插入的字典类型和数据
**Step 5: 提交代码**
```bash
git add sql/ccdi_project.sql
git commit -m "feat: 添加项目表和字典数据SQL脚本"
```
---
## Task 2: 创建实体类 CcdiProject
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiProject.java`
**Step 1: 创建实体类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiProject.java`
```java
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 纪检初核项目实体类
*
* @author ruoyi
*/
@Data
@TableName("ccdi_project")
public class CcdiProject {
/** 项目ID */
@TableId(type = IdType.AUTO)
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目描述 */
private String projectDesc;
/** 配置方式default-全局默认custom-自定义 */
private String configType;
/** 项目状态0-进行中1-已完成2-已归档 */
private String projectStatus;
/** 目标人数 */
private Integer targetCount;
/** 高风险人数 */
private Integer highRiskCount;
/** 中风险人数 */
private Integer mediumRiskCount;
/** 低风险人数 */
private Integer lowRiskCount;
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
/** 备注 */
private String remark;
}
```
**Step 2: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 3: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiProject.java
git commit -m "feat: 添加项目实体类"
```
---
## Task 3: 创建 DTO - CcdiProjectSaveDTO
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectSaveDTO.java`
**Step 1: 创建 DTO 目录(如果不存在)**
```bash
mkdir -p ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto
```
**Step 2: 创建 DTO 类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectSaveDTO.java`
```java
package com.ruoyi.info.collection.domain.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.Length;
/**
* 项目保存DTO
*
* @author ruoyi
*/
@Data
public class CcdiProjectSaveDTO {
/** 项目名称(必填) */
@NotBlank(message = "项目名称不能为空")
@Length(max = 100, message = "项目名称长度不能超过100个字符")
private String projectName;
/** 项目描述(可选) */
@Length(max = 500, message = "项目描述长度不能超过500个字符")
private String projectDesc;
/** 配置方式必填default-全局默认custom-自定义 */
@NotBlank(message = "配置方式不能为空")
private String configType;
}
```
**Step 3: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 4: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectSaveDTO.java
git commit -m "feat: 添加项目保存DTO"
```
---
## Task 4: 创建 VO - CcdiProjectVO
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiProjectVO.java`
**Step 1: 创建 VO 目录(如果不存在)**
```bash
mkdir -p ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo
```
**Step 2: 创建 VO 类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiProjectVO.java`
```java
package com.ruoyi.info.collection.domain.vo;
import lombok.Data;
import java.util.Date;
/**
* 项目VO
*
* @author ruoyi
*/
@Data
public class CcdiProjectVO {
/** 项目ID */
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目描述 */
private String projectDesc;
/** 配置方式 */
private String configType;
/** 项目状态 */
private String projectStatus;
/** 目标人数 */
private Integer targetCount;
/** 高风险人数 */
private Integer highRiskCount;
/** 中风险人数 */
private Integer mediumRiskCount;
/** 低风险人数 */
private Integer lowRiskCount;
/** 创建时间 */
private Date createTime;
/** 创建者 */
private String createBy;
}
```
**Step 3: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 4: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiProjectVO.java
git commit -m "feat: 添加项目VO"
```
---
## Task 5: 创建查询 DTO - CcdiProjectQueryDTO
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectQueryDTO.java`
**Step 1: 创建查询 DTO 类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectQueryDTO.java`
```java
package com.ruoyi.info.collection.domain.dto;
import lombok.Data;
/**
* 项目查询DTO
*
* @author ruoyi
*/
@Data
public class CcdiProjectQueryDTO {
/** 项目名称 */
private String projectName;
/** 项目状态 */
private String projectStatus;
}
```
**Step 2: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 3: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiProjectQueryDTO.java
git commit -m "feat: 添加项目查询DTO"
```
---
## Task 6: 创建 Mapper 接口
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiProjectMapper.java`
**Step 1: 创建 Mapper 接口**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiProjectMapper.java`
```java
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiProject;
import com.ruoyi.info.collection.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.info.collection.domain.vo.CcdiProjectVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 项目Mapper接口
*
* @author ruoyi
*/
@Mapper
public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
/**
* 分页查询项目列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO);
}
```
**Step 2: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 3: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiProjectMapper.java
git commit -m "feat: 添加项目Mapper接口"
```
---
## Task 7: 创建 Mapper XML 文件
**文件:**
- Create: `ruoyi-info-collection/src/main/resources/mapper/info/collection/CcdiProjectMapper.xml`
**Step 1: 创建 Mapper 目录(如果不存在)**
```bash
mkdir -p ruoyi-info-collection/src/main/resources/mapper/info/collection
```
**Step 2: 创建 XML 文件**
创建文件 `ruoyi-info-collection/src/main/resources/mapper/info/collection/CcdiProjectMapper.xml`
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiProjectMapper">
<resultMap id="ProjectVOResultMap" type="com.ruoyi.info.collection.domain.vo.CcdiProjectVO">
<id property="projectId" column="project_id"/>
<result property="projectName" column="project_name"/>
<result property="projectDesc" column="project_desc"/>
<result property="configType" column="config_type"/>
<result property="projectStatus" column="project_status"/>
<result property="targetCount" column="target_count"/>
<result property="highRiskCount" column="high_risk_count"/>
<result property="mediumRiskCount" column="medium_risk_count"/>
<result property="lowRiskCount" column="low_risk_count"/>
<result property="createTime" column="create_time"/>
<result property="createBy" column="create_by"/>
</resultMap>
<!-- 分页查询项目列表 -->
<select id="selectProjectPage" resultMap="ProjectVOResultMap">
SELECT
project_id, project_name, project_desc, config_type,
project_status, target_count, high_risk_count,
medium_risk_count, low_risk_count, create_time, create_by
FROM ccdi_project
<where>
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
AND project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
</if>
<if test="queryDTO.projectStatus != null and queryDTO.projectStatus != ''">
AND project_status = #{queryDTO.projectStatus}
</if>
</where>
ORDER BY create_time DESC
</select>
</mapper>
```
**Step 3: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 4: 提交代码**
```bash
git add ruoyi-info-collection/src/main/resources/mapper/info/collection/CcdiProjectMapper.xml
git commit -m "feat: 添加项目Mapper XML配置"
```
---
## Task 8: 创建 Service 接口
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiProjectService.java`
**Step 1: 创建 Service 接口**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiProjectService.java`
```java
package com.ruoyi.info.collection.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.info.collection.domain.vo.CcdiProjectVO;
/**
* 项目Service接口
*
* @author ruoyi
*/
public interface ICcdiProjectService {
/**
* 创建项目
*
* @param dto 项目保存DTO
* @return 项目VO
*/
CcdiProjectVO createProject(CcdiProjectSaveDTO dto);
/**
* 更新项目
*
* @param dto 项目更新DTO
* @return 项目VO
*/
CcdiProjectVO updateProject(CcdiProjectSaveDTO dto);
/**
* 删除项目
*
* @param projectId 项目ID
* @return 是否成功
*/
boolean deleteProject(Long projectId);
/**
* 查询项目详情
*
* @param projectId 项目ID
* @return 项目VO
*/
CcdiProjectVO getProjectById(Long projectId);
/**
* 分页查询项目列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO);
}
```
**Step 2: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 3: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiProjectService.java
git commit -m "feat: 添加项目Service接口"
```
---
## Task 9: 创建 Service 实现类
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 创建 Service 实现目录(如果不存在)**
```bash
mkdir -p ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl
```
**Step 2: 创建 Service 实现类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiProjectServiceImpl.java`
```java
package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiProject;
import com.ruoyi.info.collection.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.info.collection.domain.vo.CcdiProjectVO;
import com.ruoyi.info.collection.mapper.CcdiProjectMapper;
import com.ruoyi.info.collection.service.ICcdiProjectService;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
/**
* 项目Service实现类
*
* @author ruoyi
*/
@Service
public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private CcdiProjectMapper projectMapper;
@Override
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
CcdiProject project = new CcdiProject();
BeanUtils.copyProperties(dto, project);
// 设置默认值
project.setProjectStatus("0"); // 进行中
project.setTargetCount(0);
project.setHighRiskCount(0);
project.setMediumRiskCount(0);
project.setLowRiskCount(0);
projectMapper.insert(project);
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
@Override
public CcdiProjectVO updateProject(CcdiProjectSaveDTO dto) {
// TODO: 实现更新逻辑
return null;
}
@Override
public boolean deleteProject(Long projectId) {
return projectMapper.deleteById(projectId) > 0;
}
@Override
public CcdiProjectVO getProjectById(Long projectId) {
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
return null;
}
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
@Override
public Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO) {
return projectMapper.selectProjectPage(page, queryDTO);
}
}
```
**Step 3: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 4: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: 添加项目Service实现类"
```
---
## Task 10: 创建 Controller
**文件:**
- Create: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiProjectController.java`
**Step 1: 创建 Controller 类**
创建文件 `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiProjectController.java`
```java
package com.ruoyi.info.collection.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.info.collection.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.info.collection.domain.vo.CcdiProjectVO;
import com.ruoyi.info.collection.service.ICcdiProjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 纪检初核项目管理Controller
*
* @author ruoyi
*/
@RestController
@RequestMapping("/ccdi/project")
@Tag(name = "纪检初核项目管理")
public class CcdiProjectController extends BaseController {
@Resource
private ICcdiProjectService projectService;
/**
* 创建项目
*/
@PostMapping
@Operation(summary = "创建项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
public AjaxResult createProject(@Validated @RequestBody CcdiProjectSaveDTO dto) {
CcdiProjectVO vo = projectService.createProject(dto);
return AjaxResult.success("项目创建成功", vo);
}
/**
* 更新项目
*/
@PutMapping
@Operation(summary = "更新项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult updateProject(@Validated @RequestBody CcdiProjectSaveDTO dto) {
CcdiProjectVO vo = projectService.updateProject(dto);
return AjaxResult.success("项目更新成功", vo);
}
/**
* 删除项目
*/
@DeleteMapping("/{projectId}")
@Operation(summary = "删除项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:remove')")
public AjaxResult deleteProject(@PathVariable Long projectId) {
boolean success = projectService.deleteProject(projectId);
return success ? AjaxResult.success("项目删除成功") : AjaxResult.error("项目删除失败");
}
/**
* 查询项目详情
*/
@GetMapping("/{projectId}")
@Operation(summary = "查询项目详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getProject(@PathVariable Long projectId) {
CcdiProjectVO vo = projectService.getProjectById(projectId);
return AjaxResult.success(vo);
}
/**
* 查询项目列表(分页)
*/
@GetMapping("/list")
@Operation(summary = "查询项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public TableDataInfo listProject(CcdiProjectQueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiProjectVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiProjectVO> result = projectService.selectProjectPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
}
```
**Step 2: 验证编译**
```bash
cd ruoyi-info-collection && mvn clean compile
```
预期输出BUILD SUCCESS
**Step 3: 提交代码**
```bash
git add ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiProjectController.java
git commit -m "feat: 添加项目Controller"
```
---
## Task 11: 启动后端并测试接口
**Step 1: 启动后端服务**
```bash
cd ruoyi-admin && mvn spring-boot:run
```
预期输出Spring Boot 启动成功日志,端口 8080
**Step 2: 获取测试 Token**
使用测试接口获取 Token
```bash
curl -X POST "http://localhost:8080/login/test?username=admin&password=admin123"
```
预期输出:返回包含 token 的 JSON 响应
**Step 3: 测试创建项目接口**
使用 Token 测试创建项目接口:
```bash
curl -X POST "http://localhost:8080/ccdi/project" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"projectName": "测试项目1",
"projectDesc": "这是测试项目描述",
"configType": "default"
}'
```
预期输出:返回成功响应,包含项目 ID 和创建的项目信息
**Step 4: 测试查询项目列表接口**
```bash
curl -X GET "http://localhost:8080/ccdi/project/list?pageNum=1&pageSize=10" \
-H "Authorization: Bearer <token>"
```
预期输出:返回分页数据,包含刚才创建的项目
**Step 5: 使用 Swagger 测试**
访问 Swagger UI 进行接口测试:
```bash
# 浏览器打开
http://localhost:8080/swagger-ui/index.html
```
预期结果:在 Swagger UI 中可以看到项目管理的所有接口,并进行测试
---
## Task 12: 提交最终代码
**Step 1: 检查所有文件**
```bash
git status
```
预期输出:所有后端文件已提交
**Step 2: 推送到远程仓库**
```bash
git push origin dev
```
预期输出:推送成功
---
## 完成检查清单
- [ ] 数据库表 `ccdi_project` 创建成功
- [ ] 字典数据 `ccdi_project_status``ccdi_config_type` 插入成功
- [ ] 菜单权限配置成功
- [ ] 实体类 `CcdiProject` 创建并编译通过
- [ ] DTO `CcdiProjectSaveDTO` 创建并编译通过
- [ ] VO `CcdiProjectVO` 创建并编译通过
- [ ] Mapper 接口和 XML 创建并编译通过
- [ ] Service 接口和实现类创建并编译通过
- [ ] Controller 创建并编译通过
- [ ] 后端服务启动成功
- [ ] 创建项目接口测试通过
- [ ] 查询项目列表接口测试通过
- [ ] Swagger 文档显示正确
- [ ] 所有代码已提交到 git
---
**后端实施计划完成!**

View File

@@ -1,902 +0,0 @@
# 创建项目功能设计文档
**文档版本:** v1.0
**创建日期:** 2026-02-26
**设计人员:** Claude Code
---
## 1. 概述
### 1.1 功能描述
新增"创建项目"功能,允许用户在首页点击"新建项目"按钮后,通过弹窗表单创建新的纪检初核项目。
### 1.2 核心需求
- 弹窗包含3个字段项目名称、项目描述、配置方式
- 配置方式为单选按钮:全局默认模型参数配置 / 自定义项目规则参数配置
- 项目列表展示项目名称和描述(上下排列)、状态、目标人数、预警人数、创建人、创建时间
- 预警人数为各级别风险人数之和,悬停显示详细分布
---
## 2. 数据库设计
### 2.1 表结构
**表名:** `ccdi_project`
**字段列表:**
| 字段名 | 类型 | 长度 | 必填 | 默认值 | 说明 |
|--------|------|------|------|--------|------|
| project_id | BIGINT | - | 是 | 自增 | 项目ID主键 |
| project_name | VARCHAR | 100 | 是 | - | 项目名称 |
| project_desc | VARCHAR | 500 | 否 | NULL | 项目描述 |
| config_type | VARCHAR | 20 | 是 | 'default' | 配置方式default-全局默认custom-自定义 |
| project_status | CHAR | 1 | 是 | '0' | 项目状态0-进行中1-已完成2-已归档 |
| target_count | INT | - | 是 | 0 | 目标人数 |
| high_risk_count | INT | - | 是 | 0 | 高风险人数 |
| medium_risk_count | INT | - | 是 | 0 | 中风险人数 |
| low_risk_count | INT | - | 是 | 0 | 低风险人数 |
| create_by | VARCHAR | 64 | 否 | '' | 创建者 |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| update_by | VARCHAR | 64 | 否 | '' | 更新者 |
| update_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 更新时间 |
| remark | VARCHAR | 500 | 否 | NULL | 备注 |
**索引设计:**
- 主键索引:`PRIMARY KEY (project_id)`
- 项目名称索引:`INDEX idx_project_name (project_name)`
- 项目状态索引:`INDEX idx_project_status (project_status)`
- 创建时间索引:`INDEX idx_create_time (create_time)`
### 2.2 SQL 脚本
```sql
-- 创建项目表
CREATE TABLE `ccdi_project` (
`project_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '项目ID',
`project_name` VARCHAR(100) NOT NULL COMMENT '项目名称',
`project_desc` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
`config_type` VARCHAR(20) NOT NULL DEFAULT 'default' COMMENT '配置方式default-全局默认custom-自定义',
`project_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态0-进行中1-已完成2-已归档',
`target_count` INT NOT NULL DEFAULT 0 COMMENT '目标人数',
`high_risk_count` INT NOT NULL DEFAULT 0 COMMENT '高风险人数',
`medium_risk_count` INT NOT NULL DEFAULT 0 COMMENT '中风险人数',
`low_risk_count` INT NOT NULL DEFAULT 0 COMMENT '低风险人数',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`project_id`),
INDEX `idx_project_name` (`project_name`),
INDEX `idx_project_status` (`project_status`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='纪检初核项目表';
-- 插入项目状态字典
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('项目状态', 'ccdi_project_status', '0', 'admin', NOW(), '纪检初核项目状态');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time)
VALUES
(1, '进行中', '0', 'ccdi_project_status', '', 'primary', 'Y', '0', 'admin', NOW()),
(2, '已完成', '1', 'ccdi_project_status', '', 'success', 'N', '0', 'admin', NOW()),
(3, '已归档', '2', 'ccdi_project_status', '', 'info', 'N', '0', 'admin', NOW());
-- 插入配置方式字典
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('配置方式', 'ccdi_config_type', '0', 'admin', NOW(), '项目配置方式');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time)
VALUES
(1, '全局默认模型参数配置', 'default', 'ccdi_config_type', '', 'primary', 'Y', '0', 'admin', NOW()),
(2, '自定义项目规则参数配置', 'custom', 'ccdi_config_type', '', 'warning', 'N', '0', 'admin', NOW());
-- 插入菜单权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES ('纪检初核管理', 0, 1, 'ccdi', NULL, 'M', '0', '0', '', 'monitor', 'admin', NOW());
SET @parent_id = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES ('项目管理', @parent_id, 1, 'project', 'ccdiProject/index', 'C', '0', '0', 'ccdi:project:list', 'project', 'admin', NOW());
SET @menu_id = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, visible, status, perms, create_by, create_time)
VALUES
('创建项目', @menu_id, 1, 'F', '0', '0', 'ccdi:project:add', 'admin', NOW()),
('编辑项目', @menu_id, 2, 'F', '0', '0', 'ccdi:project:edit', 'admin', NOW()),
('删除项目', @menu_id, 3, 'F', '0', '0', 'ccdi:project:remove', 'admin', NOW()),
('查询项目', @menu_id, 4, 'F', '0', '0', 'ccdi:project:query', 'admin', NOW());
-- 为管理员角色分配权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE perms LIKE 'ccdi:project:%' OR perms = 'ccdi:project:list';
```
---
## 3. 后端架构设计
### 3.1 实体类
**类名:** `CcdiProject`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/`
```java
@Data
public class CcdiProject {
/** 项目ID */
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目描述 */
private String projectDesc;
/** 配置方式default-全局默认custom-自定义 */
private String configType;
/** 项目状态0-进行中1-已完成2-已归档 */
private String projectStatus;
/** 目标人数 */
private Integer targetCount;
/** 高风险人数 */
private Integer highRiskCount;
/** 中风险人数 */
private Integer mediumRiskCount;
/** 低风险人数 */
private Integer lowRiskCount;
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
/** 备注 */
private String remark;
}
```
### 3.2 DTO 设计
**类名:** `CcdiProjectSaveDTO`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/`
```java
@Data
public class CcdiProjectSaveDTO {
/** 项目名称(必填) */
@NotBlank(message = "项目名称不能为空")
@Length(max = 100, message = "项目名称长度不能超过100个字符")
private String projectName;
/** 项目描述(可选) */
@Length(max = 500, message = "项目描述长度不能超过500个字符")
private String projectDesc;
/** 配置方式必填default-全局默认custom-自定义 */
@NotBlank(message = "配置方式不能为空")
private String configType;
}
```
### 3.3 VO 设计
**类名:** `CcdiProjectVO`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/`
```java
@Data
public class CcdiProjectVO {
/** 项目ID */
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目描述 */
private String projectDesc;
/** 配置方式 */
private String configType;
/** 项目状态 */
private String projectStatus;
/** 目标人数 */
private Integer targetCount;
/** 高风险人数 */
private Integer highRiskCount;
/** 中风险人数 */
private Integer mediumRiskCount;
/** 低风险人数 */
private Integer lowRiskCount;
/** 创建时间 */
private Date createTime;
/** 创建者 */
private String createBy;
}
```
### 3.4 Controller 接口
**类名:** `CcdiProjectController`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
**接口列表:**
| 接口路径 | 方法 | 说明 | 权限标识 |
|---------|------|------|---------|
| `/ccdi/project` | POST | 创建项目 | `ccdi:project:add` |
| `/ccdi/project` | PUT | 更新项目 | `ccdi:project:edit` |
| `/ccdi/project/{projectId}` | DELETE | 删除项目 | `ccdi:project:remove` |
| `/ccdi/project/{projectId}` | GET | 查询项目详情 | `ccdi:project:query` |
| `/ccdi/project/list` | GET | 查询项目列表(分页) | `ccdi:project:list` |
**示例代码:**
```java
@RestController
@RequestMapping("/ccdi/project")
@Api(tags = "纪检初核项目管理")
public class CcdiProjectController extends BaseController {
@Resource
private ICcdiProjectService projectService;
@PostMapping
@ApiOperation("创建项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
public AjaxResult createProject(@Validated @RequestBody CcdiProjectSaveDTO dto) {
CcdiProjectVO vo = projectService.createProject(dto);
return AjaxResult.success("项目创建成功", vo);
}
@GetMapping("/list")
@ApiOperation("查询项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public TableDataInfo listProject(CcdiProjectQueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiProjectVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiProjectVO> result = projectService.selectProjectPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
}
```
### 3.5 Service 层
**接口名:** `ICcdiProjectService`
**实现类名:** `CcdiProjectServiceImpl`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/`
```java
public interface ICcdiProjectService {
/**
* 创建项目
* @param dto 项目保存DTO
* @return 项目VO
*/
CcdiProjectVO createProject(CcdiProjectSaveDTO dto);
/**
* 更新项目
* @param dto 项目更新DTO
* @return 项目VO
*/
CcdiProjectVO updateProject(CcdiProjectSaveDTO dto);
/**
* 删除项目
* @param projectId 项目ID
* @return 是否成功
*/
boolean deleteProject(Long projectId);
/**
* 查询项目详情
* @param projectId 项目ID
* @return 项目VO
*/
CcdiProjectVO getProjectById(Long projectId);
/**
* 分页查询项目列表
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO);
}
```
**实现类示例:**
```java
@Service
public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private CcdiProjectMapper projectMapper;
@Override
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
CcdiProject project = new CcdiProject();
BeanUtils.copyProperties(dto, project);
// 设置默认值
project.setProjectStatus("0"); // 进行中
project.setTargetCount(0);
project.setHighRiskCount(0);
project.setMediumRiskCount(0);
project.setLowRiskCount(0);
projectMapper.insert(project);
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
}
```
### 3.6 Mapper 层
**接口名:** `CcdiProjectMapper`
**位置:** `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/`
```java
public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
/**
* 分页查询项目列表
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO);
}
```
**XML 文件:** `CcdiProjectMapper.xml`
**位置:** `ruoyi-info-collection/src/main/resources/mapper/info/collection/`
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiProjectMapper">
<resultMap id="ProjectVOResultMap" type="com.ruoyi.info.collection.domain.vo.CcdiProjectVO">
<id property="projectId" column="project_id"/>
<result property="projectName" column="project_name"/>
<result property="projectDesc" column="project_desc"/>
<result property="configType" column="config_type"/>
<result property="projectStatus" column="project_status"/>
<result property="targetCount" column="target_count"/>
<result property="highRiskCount" column="high_risk_count"/>
<result property="mediumRiskCount" column="medium_risk_count"/>
<result property="lowRiskCount" column="low_risk_count"/>
<result property="createTime" column="create_time"/>
<result property="createBy" column="create_by"/>
</resultMap>
<!-- 分页查询项目列表 -->
<select id="selectProjectPage" resultMap="ProjectVOResultMap">
SELECT
project_id, project_name, project_desc, config_type,
project_status, target_count, high_risk_count,
medium_risk_count, low_risk_count, create_time, create_by
FROM ccdi_project
<where>
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
AND project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
</if>
<if test="queryDTO.projectStatus != null and queryDTO.projectStatus != ''">
AND project_status = #{queryDTO.projectStatus}
</if>
</where>
ORDER BY create_time DESC
</select>
</mapper>
```
---
## 4. 前端架构设计
### 4.1 组件修改
**组件名称:** `AddProjectDialog.vue`
**位置:** `ruoyi-ui/src/views/ccdiProject/components/`
**修改内容:**
1. **简化表单字段**只保留项目名称、项目描述、配置方式3个字段
2. **移除字段**:目标人员、开始日期、结束日期、目标人数、高级设置
3. **默认值**:配置方式默认为 `'default'`
**关键代码:**
```vue
<template>
<el-dialog
:visible.sync="dialogVisible"
:title="title"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="handleClose"
>
<el-form
ref="projectForm"
:model="formData"
:rules="rules"
label-width="100px"
label-position="right"
>
<!-- 项目名称 -->
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="formData.projectName"
placeholder="请输入项目名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<!-- 项目描述 -->
<el-form-item label="项目描述" prop="projectDesc">
<el-input
v-model="formData.projectDesc"
type="textarea"
:rows="4"
placeholder="请输入项目描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
<!-- 配置方式 -->
<el-form-item label="配置方式" prop="configType">
<el-radio-group v-model="formData.configType">
<el-radio label="default">全局默认模型参数配置</el-radio>
<el-radio label="custom">自定义项目规则参数配置</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
创建项目
</el-button>
</div>
</el-dialog>
</template>
<script>
import { createProject } from '@/api/ccdiProject'
export default {
name: 'AddProjectDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '新建项目'
}
},
data() {
return {
submitting: false,
formData: {
projectName: '',
projectDesc: '',
configType: 'default'
},
rules: {
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
configType: [
{ required: true, message: '请选择配置方式', trigger: 'change' }
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
if (!val) {
this.handleClose()
}
}
}
},
methods: {
handleSubmit() {
this.$refs.projectForm.validate(valid => {
if (valid) {
this.submitting = true
createProject(this.formData).then(response => {
this.$message.success('项目创建成功')
this.submitting = false
this.$emit('submit', response.data)
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
})
},
handleClose() {
this.$emit('close')
this.$refs.projectForm.resetFields()
this.formData = {
projectName: '',
projectDesc: '',
configType: 'default'
}
}
}
}
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: right;
.el-button + .el-button {
margin-left: 8px;
}
}
:deep(.el-radio-group) {
display: flex;
flex-direction: column;
gap: 12px;
.el-radio {
line-height: 32px;
}
}
</style>
```
### 4.2 项目列表表格
**组件名称:** `ProjectTable.vue`
**位置:** `ruoyi-ui/src/views/ccdiProject/components/`
**关键特性:**
1. **项目名称和描述上下排列**:同一单元格内,项目名称加粗深色,项目描述常规浅色
2. **预警人数悬停提示**:显示高、中、低风险人数详细分布
3. **预警人数样式**:根据风险级别自动调整颜色
**表格列配置:**
| 列名 | 宽度 | 对齐方式 | 说明 |
|------|------|---------|------|
| 项目名称 | 最小300px | 左对齐 | 包含项目名称(上)+项目描述(下),自适应 |
| 项目状态 | 100px | 居中对齐 | 固定宽度 |
| 目标人数 | 100px | 居中对齐 | 固定宽度 |
| 预警人数 | 120px | 居中对齐 | 悬停显示详细风险分布 |
| 创建人 | 120px | 居中对齐 | 固定宽度 |
| 创建时间 | 160px | 居中对齐 | 格式化显示 |
| 操作 | 280px | 居中对齐 | 固定在右侧 |
**关键代码:**
```vue
<!-- 项目名称含描述 -->
<el-table-column label="项目名称" min-width="300" align="left">
<template slot-scope="scope">
<div class="project-info-cell">
<div class="project-name">{{ scope.row.projectName }}</div>
<div class="project-desc">{{ scope.row.projectDesc || '暂无描述' }}</div>
</div>
</template>
</el-table-column>
<!-- 预警人数带悬停详情 -->
<el-table-column label="预警人数" width="120" align="center">
<template slot-scope="scope">
<el-tooltip placement="top" effect="light">
<div slot="content">
<div style="padding: 8px;">
<div style="margin-bottom: 8px; font-weight: bold; color: #303133;">
风险人数统计
</div>
<div style="margin-bottom: 6px;">
<span style="color: #f56c6c;"> 高风险</span>
<span style="font-weight: bold;">{{ scope.row.highRiskCount }} </span>
</div>
<div style="margin-bottom: 6px;">
<span style="color: #e6a23c;"> 中风险</span>
<span style="font-weight: bold;">{{ scope.row.mediumRiskCount }} </span>
</div>
<div>
<span style="color: #909399;"> 低风险</span>
<span style="font-weight: bold;">{{ scope.row.lowRiskCount }} </span>
</div>
</div>
</div>
<div class="warning-count-wrapper">
<span :class="getWarningClass(scope.row)" style="cursor: pointer;">
{{ scope.row.highRiskCount + scope.row.mediumRiskCount + scope.row.lowRiskCount }}
</span>
</div>
</el-tooltip>
</template>
</el-table-column>
```
**样式代码:**
```scss
.project-info-cell {
padding: 8px 0;
line-height: 1.5;
.project-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-desc {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.text-info {
color: #909399;
}
.text-bold {
font-weight: bold;
}
```
**预警人数样式规则:**
- 高风险 > 0红色加粗
- 中风险 > 0橙色加粗
- 低风险 > 0灰色
- 无预警:普通黑色
### 4.3 API 接口
**文件名:** `ccdiProject.js`
**位置:** `ruoyi-ui/src/api/`
```javascript
import request from '@/utils/request'
// 创建初核项目
export function createProject(data) {
return request({
url: '/ccdi/project',
method: 'post',
data: data
})
}
// 查询初核项目列表(分页)
export function listProject(query) {
return request({
url: '/ccdi/project/list',
method: 'get',
params: query
})
}
// 查询初核项目详细
export function getProject(projectId) {
return request({
url: '/ccdi/project/' + projectId,
method: 'get'
})
}
// 修改初核项目
export function updateProject(data) {
return request({
url: '/ccdi/project',
method: 'put',
data: data
})
}
// 删除初核项目
export function delProject(projectId) {
return request({
url: '/ccdi/project/' + projectId,
method: 'delete'
})
}
```
---
## 5. 实施计划
### 5.1 实施步骤
#### 阶段一:数据库与后端开发(预计 2.5 小时)
1. **创建数据库表**15 分钟)
- 执行 `ccdi_project` 表创建脚本
- 插入字典数据和菜单数据
2. **后端开发**2 小时)
- 创建实体类 `CcdiProject`
- 创建 DTO `CcdiProjectSaveDTO`
- 创建 VO `CcdiProjectVO`
- 创建 Mapper 接口和 XML
- 创建 Service 接口和实现类
- 创建 Controller 接口
- 添加 Swagger 注解
3. **后端测试**30 分钟)
- 使用 Swagger 测试创建项目接口
- 使用 Swagger 测试查询项目列表接口
- 验证数据字典显示
#### 阶段二:前端开发(预计 2.5 小时)
4. **前端组件开发**1.5 小时)
- 修改 `AddProjectDialog.vue` 组件
- 修改 `ProjectTable.vue` 组件
- 更新 API 接口文件 `ccdiProject.js`
- 修改父组件调用逻辑
5. **前端联调**1 小时)
- 测试创建项目功能
- 测试项目列表显示
- 测试预警人数悬停提示
- 测试字典数据展示
---
## 6. 注意事项
### 6.1 数据完整性
- 创建项目时,`project_status` 默认为 `'0'`(进行中)
- 创建项目时,风险计数字段默认为 `0`
- `config_type` 默认为 `'default'`
- 项目名称和描述不能为空
### 6.2 权限控制
- 创建项目需要 `ccdi:project:add` 权限
- 编辑项目需要 `ccdi:project:edit` 权限
- 删除项目需要 `ccdi:project:remove` 权限
- 查询项目需要 `ccdi:project:list` 权限
### 6.3 前端验证
- 项目名称必填2-100字符
- 项目描述可选最多500字符
- 配置方式:必填,只能选择 `default``custom`
### 6.4 后端验证
- 使用 `@Validated` 注解进行参数校验
- 项目名称长度校验
- 配置方式枚举值校验
### 6.5 性能优化
- 项目列表分页查询
- 项目名称和状态字段添加索引
- 字典数据使用缓存
### 6.6 用户体验
- 提交按钮显示 loading 状态
- 创建成功后自动刷新列表
- 预警人数悬停提示详细信息
- 项目名称和描述上下排列,层次分明
---
## 7. 测试清单
### 7.1 后端测试
- [ ] 创建项目成功(必填字段)
- [ ] 创建项目失败(缺少必填字段)
- [ ] 创建项目失败(字段长度超限)
- [ ] 查询项目列表(分页)
- [ ] 查询项目详情
- [ ] 更新项目
- [ ] 删除项目
- [ ] 字典数据正确返回
- [ ] 权限验证正确
### 7.2 前端测试
- [ ] 弹窗显示正确3个字段
- [ ] 表单验证正常(必填项)
- [ ] 表单验证正常(长度限制)
- [ ] 项目名称和描述上下排列
- [ ] 项目名称样式正确(加粗深色)
- [ ] 项目描述样式正确(常规浅色)
- [ ] 项目状态标签正确显示
- [ ] 预警人数计算正确(高+中+低)
- [ ] 预警人数悬停提示显示
- [ ] 预警人数颜色根据风险级别变化
- [ ] 创建人正确显示
- [ ] 创建时间格式化正确
- [ ] 操作按钮权限控制
- [ ] 提交按钮 loading 状态
- [ ] 创建成功后列表刷新
---
## 8. 附录
### 8.1 参考文档
- 若依框架官方文档
- Element UI 组件库文档
- MyBatis Plus 官方文档
### 8.2 相关文件
- 数据库脚本:`sql/ccdi_project.sql`
- 设计截图:`doc/创建项目功能/ScreenShot_2026-02-26_153149_900.png`
- 设计截图:`doc/创建项目功能/ScreenShot_2026-02-26_162233_965.png`
---
**文档结束**

View File

@@ -1,881 +0,0 @@
# 创建项目功能 - 前端实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 实现创建项目功能的前端界面包括弹窗表单、项目列表展示、API调用
**架构:** 基于 Vue 2.6.12 + Element UI 2.15.14,采用组件化开发
**技术栈:** Vue.js 2.6.12, Element UI 2.15.14, Axios 0.28.1
---
## 前置条件
- 后端接口已部署并测试通过
- 前端项目依赖已安装
- 已有测试账号admin/admin123
- 后端服务运行在 http://localhost:8080
---
## Task 1: 更新 API 接口文件
**文件:**
- Modify: `ruoyi-ui/src/api/ccdiProject.js`
**Step 1: 备份原文件**
```bash
cp ruoyi-ui/src/api/ccdiProject.js ruoyi-ui/src/api/ccdiProject.js.bak
```
**Step 2: 修改 API 文件**
`ruoyi-ui/src/api/ccdiProject.js` 修改为以下内容:
```javascript
import request from '@/utils/request'
// 创建初核项目
export function createProject(data) {
return request({
url: '/ccdi/project',
method: 'post',
data: data
})
}
// 查询初核项目列表(分页)
export function listProject(query) {
return request({
url: '/ccdi/project/list',
method: 'get',
params: query
})
}
// 查询初核项目详细
export function getProject(projectId) {
return request({
url: '/ccdi/project/' + projectId,
method: 'get'
})
}
// 修改初核项目
export function updateProject(data) {
return request({
url: '/ccdi/project',
method: 'put',
data: data
})
}
// 删除初核项目
export function delProject(projectId) {
return request({
url: '/ccdi/project/' + projectId,
method: 'delete'
})
}
// Mock数据获取项目列表保留用于测试
export function getMockProjectList() {
return Promise.resolve({
code: 200,
total: 3,
rows: [
{
projectId: 1,
projectName: '2024年Q1初核',
projectDesc: '2024年第一季度纪检初核排查工作',
createTime: '2024-01-01',
projectStatus: '0',
configType: 'default',
targetCount: 500,
highRiskCount: 5,
mediumRiskCount: 10,
lowRiskCount: 0
},
{
projectId: 2,
projectName: '2023年Q4初核',
projectDesc: '2023年第四季度纪检初核排查工作',
createTime: '2023-10-01',
projectStatus: '1',
configType: 'custom',
targetCount: 480,
highRiskCount: 8,
mediumRiskCount: 15,
lowRiskCount: 0
},
{
projectId: 3,
projectName: '2023年Q3初核',
projectDesc: '2023年第三季度纪检初核排查工作',
createTime: '2023-07-01',
projectStatus: '2',
configType: 'default',
targetCount: 450,
highRiskCount: 0,
mediumRiskCount: 18,
lowRiskCount: 5
}
]
})
}
```
**Step 3: 验证语法**
```bash
cd ruoyi-ui && npm run lint -- --fix src/api/ccdiProject.js
```
预期输出:无 ESLint 错误
**Step 4: 提交代码**
```bash
git add ruoyi-ui/src/api/ccdiProject.js
git commit -m "feat: 更新项目API接口添加创建项目接口"
```
---
## Task 2: 修改 AddProjectDialog 组件
**文件:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/AddProjectDialog.vue`
**Step 1: 备份原文件**
```bash
cp ruoyi-ui/src/views/ccdiProject/components/AddProjectDialog.vue ruoyi-ui/src/views/ccdiProject/components/AddProjectDialog.vue.bak
```
**Step 2: 重写组件**
`ruoyi-ui/src/views/ccdiProject/components/AddProjectDialog.vue` 重写为以下内容:
```vue
<template>
<el-dialog
:visible.sync="dialogVisible"
:title="title"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="handleClose"
>
<el-form
ref="projectForm"
:model="formData"
:rules="rules"
label-width="100px"
label-position="right"
>
<!-- 项目名称 -->
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="formData.projectName"
placeholder="请输入项目名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<!-- 项目描述 -->
<el-form-item label="项目描述" prop="projectDesc">
<el-input
v-model="formData.projectDesc"
type="textarea"
:rows="4"
placeholder="请输入项目描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
<!-- 配置方式 -->
<el-form-item label="配置方式" prop="configType">
<el-radio-group v-model="formData.configType">
<el-radio label="default">全局默认模型参数配置</el-radio>
<el-radio label="custom">自定义项目规则参数配置</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
创建项目
</el-button>
</div>
</el-dialog>
</template>
<script>
import { createProject } from '@/api/ccdiProject'
export default {
name: 'AddProjectDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '新建项目'
},
form: {
type: Object,
default: () => ({})
}
},
data() {
return {
submitting: false,
formData: {
projectName: '',
projectDesc: '',
configType: 'default'
},
rules: {
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
configType: [
{ required: true, message: '请选择配置方式', trigger: 'change' }
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
if (!val) {
this.handleClose()
}
}
}
},
watch: {
form: {
handler(newVal) {
if (newVal && Object.keys(newVal).length > 0) {
this.formData = { ...this.formData, ...newVal }
}
},
immediate: true,
deep: true
},
visible(val) {
if (val) {
this.$nextTick(() => {
if (this.$refs.projectForm) {
this.$refs.projectForm.clearValidate()
}
})
}
}
},
methods: {
/** 提交表单 */
handleSubmit() {
this.$refs.projectForm.validate(valid => {
if (valid) {
this.submitting = true
createProject(this.formData).then(response => {
this.$message.success('项目创建成功')
this.submitting = false
this.$emit('submit', response.data)
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
})
},
/** 关闭对话框 */
handleClose() {
this.$emit('close')
this.$refs.projectForm.resetFields()
this.formData = {
projectName: '',
projectDesc: '',
configType: 'default'
}
}
}
}
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: right;
.el-button + .el-button {
margin-left: 8px;
}
}
:deep(.el-radio-group) {
display: flex;
flex-direction: column;
gap: 12px;
.el-radio {
line-height: 32px;
}
}
</style>
```
**Step 3: 验证语法**
```bash
cd ruoyi-ui && npm run lint -- --fix src/views/ccdiProject/components/AddProjectDialog.vue
```
预期输出:无 ESLint 错误
**Step 4: 提交代码**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/AddProjectDialog.vue
git commit -m "feat: 简化项目创建弹窗只保留3个核心字段"
```
---
## Task 3: 修改 ProjectTable 组件
**文件:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 备份原文件**
```bash
cp ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue.bak
```
**Step 2: 重写组件**
`ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue` 重写为以下内容:
```vue
<template>
<div class="project-table-container">
<el-table
:data="dataList"
:loading="loading"
border
style="width: 100%"
>
<!-- 项目名称含描述 -->
<el-table-column
label="项目名称"
min-width="300"
align="left"
>
<template slot-scope="scope">
<div class="project-info-cell">
<div class="project-name">{{ scope.row.projectName }}</div>
<div class="project-desc">{{ scope.row.projectDesc || '暂无描述' }}</div>
</div>
</template>
</el-table-column>
<!-- 项目状态 -->
<el-table-column
prop="projectStatus"
label="项目状态"
width="100"
align="center"
>
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.projectStatus)">
<dict-tag :options="dict.type.ccdi_project_status" :value="scope.row.projectStatus"/>
</el-tag>
</template>
</el-table-column>
<!-- 目标人数 -->
<el-table-column
prop="targetCount"
label="目标人数"
width="100"
align="center"
/>
<!-- 预警人数带悬停详情 -->
<el-table-column
label="预警人数"
width="120"
align="center"
>
<template slot-scope="scope">
<el-tooltip placement="top" effect="light">
<div slot="content">
<div style="padding: 8px;">
<div style="margin-bottom: 8px; font-weight: bold; color: #303133;">
风险人数统计
</div>
<div style="margin-bottom: 6px;">
<span style="color: #f56c6c;"> 高风险</span>
<span style="font-weight: bold;">{{ scope.row.highRiskCount }} </span>
</div>
<div style="margin-bottom: 6px;">
<span style="color: #e6a23c;"> 中风险</span>
<span style="font-weight: bold;">{{ scope.row.mediumRiskCount }} </span>
</div>
<div>
<span style="color: #909399;"> 低风险</span>
<span style="font-weight: bold;">{{ scope.row.lowRiskCount }} </span>
</div>
</div>
</div>
<div class="warning-count-wrapper">
<span :class="getWarningClass(scope.row)" style="cursor: pointer;">
{{ scope.row.highRiskCount + scope.row.mediumRiskCount + scope.row.lowRiskCount }}
</span>
</div>
</el-tooltip>
</template>
</el-table-column>
<!-- 创建人 -->
<el-table-column
prop="createBy"
label="创建人"
width="120"
align="center"
/>
<!-- 创建时间 -->
<el-table-column
prop="createTime"
label="创建时间"
width="160"
align="center"
>
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
label="操作"
width="200"
align="center"
fixed="right"
>
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
>详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleEdit(scope.row)"
>编辑</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
:current-page="pageParams.pageNum"
:page-size="pageParams.pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
style="margin-top: 16px; text-align: right;"
/>
</div>
</template>
<script>
export default {
name: 'ProjectTable',
dicts: ['ccdi_project_status', 'ccdi_config_type'],
props: {
dataList: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
},
total: {
type: Number,
default: 0
},
pageParams: {
type: Object,
default: () => ({
pageNum: 1,
pageSize: 10
})
}
},
methods: {
getStatusType(status) {
const statusMap = {
'0': 'primary', // 进行中
'1': 'success', // 已完成
'2': 'info' // 已归档
}
return statusMap[status] || 'info'
},
getWarningClass(row) {
const total = row.highRiskCount + row.mediumRiskCount + row.lowRiskCount
if (row.highRiskCount > 0) {
return 'text-danger text-bold'
} else if (row.mediumRiskCount > 0) {
return 'text-warning text-bold'
} else if (total > 0) {
return 'text-info'
}
return ''
},
handleDetail(row) {
this.$emit('detail', row)
},
handleEdit(row) {
this.$emit('edit', row)
},
handleDelete(row) {
this.$emit('delete', row)
},
handleSizeChange(val) {
this.$emit('pagination', { pageNum: this.pageParams.pageNum, pageSize: val })
},
handleCurrentChange(val) {
this.$emit('pagination', { pageNum: val, pageSize: this.pageParams.pageSize })
}
}
}
</script>
<style lang="scss" scoped>
.project-table-container {
margin-top: 16px;
}
.project-info-cell {
padding: 8px 0;
line-height: 1.5;
.project-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-desc {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.warning-count-wrapper {
display: inline-block;
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.text-info {
color: #909399;
}
.text-bold {
font-weight: bold;
}
</style>
```
**Step 3: 验证语法**
```bash
cd ruoyi-ui && npm run lint -- --fix src/views/ccdiProject/components/ProjectTable.vue
```
预期输出:无 ESLint 错误
**Step 4: 提交代码**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "feat: 优化项目列表表格,添加预警人数悬停提示"
```
---
## Task 4: 修改父组件 index.vue
**文件:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
**Step 1: 备份原文件**
```bash
cp ruoyi-ui/src/views/ccdiProject/index.vue ruoyi-ui/src/views/ccdiProject/index.vue.bak
```
**Step 2: 修改父组件**
`ruoyi-ui/src/views/ccdiProject/index.vue``getList``handleSubmitProject` 方法修改为:
```javascript
/** 查询项目列表 */
getList() {
this.loading = true
// 使用真实API
listProject(this.queryParams).then(response => {
this.projectList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 提交项目表单 */
handleSubmitProject(data) {
// 不需要再次调用API因为AddProjectDialog已经处理了
this.addDialogVisible = false
this.getList() // 刷新列表
}
```
**Step 3: 验证语法**
```bash
cd ruoyi-ui && npm run lint -- --fix src/views/ccdiProject/index.vue
```
预期输出:无 ESLint 错误
**Step 4: 提交代码**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 修改父组件切换为真实API调用"
```
---
## Task 5: 启动前端并测试
**Step 1: 启动前端开发服务器**
```bash
cd ruoyi-ui && npm run dev
```
预期输出:前端服务启动成功,访问地址 http://localhost:80
**Step 2: 测试登录**
浏览器访问 http://localhost:80使用测试账号登录
- 用户名admin
- 密码admin123
预期结果:登录成功,进入首页
**Step 3: 测试项目列表**
导航到"纪检初核管理 > 项目管理"菜单:
预期结果:
- 项目列表正常显示
- 项目名称和描述上下排列
- 项目状态标签显示正确
- 预警人数悬停提示显示风险详情
**Step 4: 测试创建项目**
点击"新建项目"按钮:
预期结果:
- 弹窗正常打开
- 显示3个字段项目名称、项目描述、配置方式
- 配置方式默认选中"全局默认模型参数配置"
填写表单:
- 项目名称测试项目001
- 项目描述:这是测试项目的描述
- 配置方式:选择"自定义项目规则参数配置"
点击"创建项目"按钮:
预期结果:
- 按钮显示 loading 状态
- 创建成功,提示"项目创建成功"
- 弹窗关闭
- 项目列表自动刷新,显示新创建的项目
**Step 5: 测试预警人数悬停**
在项目列表中,将鼠标悬停在预警人数上:
预期结果:
- 显示风险人数统计提示框
- 显示高风险、中风险、低风险人数
- 预警人数颜色根据风险级别变化
**Step 6: 测试表单验证**
不填写项目名称,直接点击"创建项目"
预期结果:
- 提示"请输入项目名称"
- 表单不提交
**Step 7: 测试取消按钮**
点击"新建项目",然后点击"取消"
预期结果:
- 弹窗关闭
- 表单数据清空
---
## Task 6: 跨浏览器测试
**Step 1: Chrome 测试**
在 Chrome 浏览器中重复 Task 5 的所有测试:
预期结果:所有功能正常
**Step 2: Edge 测试**
在 Edge 浏览器中重复 Task 5 的所有测试:
预期结果:所有功能正常
**Step 3: Firefox 测试(可选)**
在 Firefox 浏览器中重复 Task 5 的所有测试:
预期结果:所有功能正常
---
## Task 7: 响应式测试
**Step 1: 测试不同分辨率**
调整浏览器窗口大小,测试以下分辨率:
- 1920x1080桌面
- 1366x768笔记本
- 768x1024平板
预期结果:
- 表格自适应宽度
- 弹窗居中显示
- 所有功能正常使用
**Step 2: 测试表格横向滚动**
缩小浏览器窗口,使表格宽度小于内容宽度:
预期结果:
- 表格出现横向滚动条
- 操作列固定在右侧
- 可以横向滚动查看所有列
---
## Task 8: 提交最终代码
**Step 1: 检查所有文件**
```bash
git status
```
预期输出:所有前端文件已提交
**Step 2: 推送到远程仓库**
```bash
git push origin dev
```
预期输出:推送成功
---
## 完成检查清单
- [ ] API 接口文件更新完成
- [ ] AddProjectDialog 组件简化完成3个字段
- [ ] ProjectTable 组件优化完成(上下排列、预警悬停)
- [ ] 父组件切换为真实API
- [ ] 前端服务启动成功
- [ ] 登录功能正常
- [ ] 项目列表显示正常
- [ ] 项目名称和描述上下排列正确
- [ ] 项目状态标签显示正确
- [ ] 预警人数悬停提示显示正常
- [ ] 预警人数颜色根据风险级别变化
- [ ] 创建项目弹窗打开正常
- [ ] 配置方式默认值正确
- [ ] 创建项目功能正常
- [ ] 创建成功后列表刷新
- [ ] 表单验证正常
- [ ] 取消按钮功能正常
- [ ] 跨浏览器测试通过
- [ ] 响应式测试通过
- [ ] 所有代码已提交到 git
---
**前端实施计划完成!**

View File

@@ -1,105 +0,0 @@
# 模型参数阈值更新接口优化设计
## 1. 背景
当前 `ModelParamSaveDTO` 存在参数冗余问题:
- 外层包含不必要的 `modelName` 字段
- 内层 `ParamItem` 包含 6 个字段,但 Service 层只使用 `paramCode``paramValue`
- 前端请求体包含大量无用字段,增加网络传输开销
## 2. 优化目标
- 简化 DTO 结构,减少冗余字段
- 减少前端请求数据量
- 提升代码可读性
## 3. 设计方案
### 3.1 后端 DTO 简化
**文件:** `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamSaveDTO.java`
**改动:**
- 移除 `modelName` 字段
- 将内部类 `ParamItem` 重命名为 `ParamValueItem`
- 内部类只保留 `paramCode``paramValue` 两个字段
**优化后结构:**
```java
@Data
public class ModelParamSaveDTO {
private Long projectId;
@NotBlank(message = "模型编码不能为空")
private String modelCode;
@NotNull(message = "参数列表不能为空")
private List<ParamValueItem> params;
@Data
public static class ParamValueItem {
@NotBlank(message = "参数编码不能为空")
private String paramCode;
@NotBlank(message = "参数值不能为空")
private String paramValue;
}
}
```
### 3.2 Service 层微调
**文件:** `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**改动:** 更新循环中的类型引用
```java
// 改动前
for (ModelParamSaveDTO.ParamItem item : saveDTO.getParams())
// 改动后
for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams())
```
### 3.3 前端请求简化
**文件:** `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
**改动:** 简化 `handleSave` 方法中的请求参数
**优化后:**
```javascript
const saveDTO = {
projectId: this.queryParams.projectId,
modelCode: this.queryParams.modelCode,
params: modifiedParams.map((item) => ({
paramCode: item.paramCode,
paramValue: item.paramValue,
})),
};
```
## 4. 改动文件清单
| 文件 | 改动类型 |
|------|---------|
| `ModelParamSaveDTO.java` | 简化字段 |
| `CcdiModelParamServiceImpl.java` | 类型引用更新 |
| `index.vue` | 请求参数简化 |
## 5. 优化效果
| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| DTO 外层字段数 | 3 | 2 |
| DTO 内层字段数 | 6 | 2 |
| 前端请求体字段数 | 8 | 4 |
## 6. 风险评估
- **风险等级:** 低
- **向后兼容:** 是(后端忽略多余字段)
- **测试要求:** 验证保存功能正常
---
**创建日期:** 2026-02-26

View File

@@ -1,238 +0,0 @@
# 模型参数阈值更新接口优化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 简化模型参数保存接口的 DTO 结构,减少冗余字段
**Architecture:** 纯重构,不改变业务逻辑。简化 DTO 字段,同步更新 Service 和前端调用
**Tech Stack:** Java 17, Spring Boot, Vue.js
---
## Task 1: 简化后端 DTO
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamSaveDTO.java`
**Step 1: 读取当前 DTO 文件**
检查现有代码结构。
**Step 2: 重写 DTO 文件**
将整个文件替换为简化后的版本:
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 模型参数保存DTO
*/
@Data
public class ModelParamSaveDTO {
/** 项目ID */
private Long projectId;
/** 模型编码 */
@NotBlank(message = "模型编码不能为空")
private String modelCode;
/** 参数列表 */
@NotNull(message = "参数列表不能为空")
private List<ParamValueItem> params;
@Data
public static class ParamValueItem {
/** 参数编码 */
@NotBlank(message = "参数编码不能为空")
private String paramCode;
/** 参数值 - 唯一可修改字段 */
@NotBlank(message = "参数值不能为空")
private String paramValue;
}
}
```
**Step 3: 保存文件**
确保文件保存成功。
---
## Task 2: 更新 Service 层类型引用
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**Step 1: 更新 for 循环中的类型引用**
找到第 105 行附近的代码,将:
```java
for (ModelParamSaveDTO.ParamItem item : saveDTO.getParams()) {
```
改为:
```java
for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) {
```
---
## Task 3: 简化前端请求参数
**Files:**
- Modify: `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
**Step 1: 找到 handleSave 方法中的 saveDTO 构建**
定位到第 119-133 行。
**Step 2: 简化请求参数**
将原有的 saveDTO 构建代码:
```javascript
const saveDTO = {
projectId: this.queryParams.projectId,
modelCode: this.queryParams.modelCode,
modelName: this.modelList.find(
(m) => m.modelCode === this.queryParams.modelCode
)?.modelName,
params: modifiedParams.map((item) => ({
paramCode: item.paramCode,
paramName: item.paramName,
paramDesc: item.paramDesc,
paramValue: item.paramValue,
paramUnit: item.paramUnit,
sortOrder: item.sortOrder,
})),
};
```
替换为简化版本:
```javascript
const saveDTO = {
projectId: this.queryParams.projectId,
modelCode: this.queryParams.modelCode,
params: modifiedParams.map((item) => ({
paramCode: item.paramCode,
paramValue: item.paramValue,
})),
};
```
---
## Task 4: 编译后端验证
**Files:**
- 无文件修改,仅验证
**Step 1: 编译后端项目**
```bash
cd D:/ccdi/ccdi && mvn clean compile -DskipTests
```
**Expected:** BUILD SUCCESS
**Step 2: 如有编译错误,检查类型引用**
确保所有 `ParamItem` 都已改为 `ParamValueItem`
---
## Task 5: 功能测试验证
**Files:**
- 无文件修改,仅验证
**Step 1: 启动后端服务**
```bash
cd D:/ccdi/ccdi && mvn spring-boot:run
```
等待服务启动完成。
**Step 2: 通过 Swagger 测试保存接口**
1. 访问 `http://localhost:8080/swagger-ui/index.html`
2. 找到 `模型参数配置` 分组
3. 测试 `/ccdi/modelParam/save` 接口
4. 使用简化的请求体:
```json
{
"projectId": 0,
"modelCode": "LARGE_TRANSACTION",
"params": [
{
"paramCode": "SINGLE_AMOUNT",
"paramValue": "50000"
}
]
}
```
**Expected:** 返回 `{"code": 200, "msg": "保存成功"}`
**Step 3: 验证参数已更新**
调用 `/ccdi/modelParam/list?projectId=0&modelCode=LARGE_TRANSACTION`
**Expected:** 返回的参数中 `SINGLE_AMOUNT``paramValue` 已更新为 `50000`
---
## Task 6: 提交代码
**Step 1: 查看变更**
```bash
git status
git diff
```
**Step 2: 提交后端改动**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamSaveDTO.java
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "refactor: 简化 ModelParamSaveDTO移除冗余字段"
```
**Step 3: 提交前端改动**
```bash
git add ruoyi-ui/src/views/ccdi/modelParam/index.vue
git commit -m "refactor: 简化模型参数保存请求参数"
```
---
## 改动摘要
| 文件 | 改动 |
|------|------|
| `ModelParamSaveDTO.java` | 移除 modelNameParamItem 简化为 ParamValueItem2字段 |
| `CcdiModelParamServiceImpl.java` | 类型引用 ParamItem → ParamValueItem |
| `index.vue` | 请求参数只保留 paramCode 和 paramValue |
**风险等级:** 低(向后兼容,纯简化重构)
---
**创建日期:** 2026-02-26

View File

@@ -1,587 +0,0 @@
# Material Design 表格样式优化设计文档
**日期**: 2026-02-27
**状态**: 已批准
**方案**: 纯扁平卡片式(方案 1
## 概述
本文档描述项目管理表格的 Material Design 风格优化设计。通过移除边框、使用阴影和留白来分隔内容,实现现代、简洁的视觉体验。
## 设计目标
1. **全面 Material Design 改版**:采用 Material Design 的核心设计语言
2. **扁平化表头**:移除表头背景色,使用排版和留白区分
3. **阴影和留白**:用视觉层次代替边框分隔
4. **中等阴影效果**`box-shadow: 0 2px 8px rgba(0,0,0,0.1)`
## 设计方案
### 整体设计理念
采用 **纯扁平卡片式** 设计,核心特征:
- 表格整体作为一张浮动卡片
- 使用阴影创造视觉层次
- 移除所有边框和分隔线
- 通过留白分隔行与行
- 表头扁平化,无背景色
---
## 详细设计
### 1. 整体卡片容器和阴影
**样式定义:**
```scss
.project-table-container {
margin-top: 16px;
:deep(.el-table) {
// 移除边框,使用阴影
border: none;
border-radius: 8px; // 从 4px 增加到 8px更圆润
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 中等阴影
// 悬停时卡片阴影加深
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
```
**视觉效果:**
- 表格作为浮动的独立卡片
- 圆角8px更柔和
- 默认阴影:`0 2px 8px rgba(0,0,0,0.1)`
- 悬停阴影:`0 4px 12px rgba(0,0,0,0.15)`
- 完全移除边框
**变更对比:**
| 属性 | 旧值 | 新值 |
|------|------|------|
| border | `1px solid #eee` | `none` |
| border-radius | `4px` | `8px` |
| box-shadow | 无 | `0 2px 8px rgba(0,0,0,0.1)` |
---
### 2. 扁平化表头设计
**样式定义:**
```scss
:deep(.el-table) {
// 表头样式 - 扁平化,无背景色
th {
background-color: transparent; // 移除背景色
color: #333;
font-weight: 600; // 加粗字体
font-size: 14px;
height: 56px; // 从 48px 增加到 56px
padding: 16px 12px; // 从 12px 增加到 16px
// 只保留底部一条分隔线
border-bottom: 2px solid #e0e0e0;
}
// 表头单元格内部
.cell {
border-right: none; // 移除垂直分隔线
}
}
```
**设计理念:**
- 通过字体粗细、留白和底线区分表头
- 不依赖背景色
- 简洁、现代
**变更对比:**
| 属性 | 旧值 | 新值 |
|------|------|------|
| background-color | `#f5f5f5` | `transparent` |
| height | `48px` | `56px` |
| padding | `12px` | `16px 12px` |
| border-bottom | 无 | `2px solid #e0e0e0` |
---
### 3. 数据行设计(留白和悬停)
**样式定义:**
```scss
:deep(.el-table) {
// 数据行样式 - 增加留白,移除分隔线
td {
color: #333;
font-size: 14px;
height: 64px; // 从 50px 增加到 64px
padding: 20px 12px; // 从 12px 增加到 20px
border-bottom: none; // 完全移除行分隔线
}
// 悬停效果
.el-table__row {
transition: background-color 0.2s ease;
&:hover > td {
background-color: #fafafa !important; // 浅灰色背景
}
}
// 移除表格内容的额外边框
.el-table__body-wrapper {
.cell {
border-right: none;
}
}
&::before,
&::after {
display: none; // 完全移除伪元素边框
}
}
```
**关键变化:**
1. **行高增加**50px → 64px+28%
2. **垂直内边距**12px → 20px+67%
3. **移除行分隔线**`border-bottom: none`
4. **悬停效果**:浅灰色背景 `#fafafa` + 过渡 0.2s
**变更对比:**
| 属性 | 旧值 | 新值 |
|------|------|------|
| height | `50px` | `64px` |
| padding | `12px` | `20px 12px` |
| border-bottom | `1px solid #f0f0f0` | `none` |
| 悬停背景 | `#f5f5f5` | `#fafafa` |
---
### 4. 操作按钮样式
**样式定义:**
```scss
// 操作按钮样式 - Material Design 风格
:deep(.el-button--text) {
color: #1890ff;
padding: 8px 12px; // 从 0 8px 增加到 8px 12px
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
color: #096dd9;
background-color: rgba(24, 144, 255, 0.08); // 添加浅蓝色背景
text-decoration: none; // 移除下划线
}
&:first-child {
padding-left: 0;
}
// 按钮间距
& + .el-button--text {
margin-left: 4px; // 从 8px 减少到 4px
}
}
```
**改进点:**
1. **增加内边距**:更符合 Material Design 的"点击区域"理念
2. **悬停背景色**:用浅蓝色背景代替下划线
3. **减少间距**:背景色会在视觉上分隔按钮
**变更对比:**
| 属性 | 旧值 | 新值 |
|------|------|------|
| padding | `0 8px` | `8px 12px` |
| border-radius | 无 | `4px` |
| hover background | 无 | `rgba(24, 144, 255, 0.08)` |
| hover text-decoration | `underline` | `none` |
---
### 5. 分页组件样式
**样式定义:**
```scss
// 分页样式优化
:deep(.el-pagination) {
margin-top: 24px; // 从 16px 增加到 24px
text-align: right;
// 扁平化按钮
.btn-prev,
.btn-next,
.el-pager li {
border: none;
background-color: transparent;
&:hover {
background-color: #f5f5f5;
}
}
.el-pager li.active {
background-color: #1890ff;
color: white;
border-radius: 4px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
color: #666; // 从 #606266 改为 #666
}
}
```
**改进点:**
1. **移除边框**:扁平化所有按钮
2. **激活页码**:蓝色背景 + 圆角
3. **增加上边距**24px原 16px
---
### 6. 空状态设计
**样式定义:**
```scss
// 空状态(无数据时)
:deep(.el-table__empty-block) {
padding: 48px 0; // 增加垂直留白
.el-table__empty-text {
color: #999;
font-size: 14px;
}
}
```
---
## 完整样式代码
```scss
<style lang="scss" scoped>
.project-table-container {
margin-top: 16px;
:deep(.el-table) {
// 卡片容器
border: none;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
// 表头样式
th {
background-color: transparent;
color: #333;
font-weight: 600;
font-size: 14px;
height: 56px;
padding: 16px 12px;
border-bottom: 2px solid #e0e0e0;
}
// 数据行样式
td {
color: #333;
font-size: 14px;
height: 64px;
padding: 20px 12px;
border-bottom: none;
}
// 移除列分隔线
.el-table__body-wrapper {
.cell {
border-right: none;
}
}
// 悬停效果
.el-table__row {
transition: background-color 0.2s ease;
&:hover > td {
background-color: #fafafa !important;
}
}
// 移除额外边框
&::before,
&::after {
display: none;
}
}
}
// 操作按钮样式
:deep(.el-button--text) {
color: #1890ff;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
color: #096dd9;
background-color: rgba(24, 144, 255, 0.08);
text-decoration: none;
}
&:first-child {
padding-left: 0;
}
& + .el-button--text {
margin-left: 4px;
}
}
// 分页样式
:deep(.el-pagination) {
margin-top: 24px;
text-align: right;
.btn-prev,
.btn-next,
.el-pager li {
border: none;
background-color: transparent;
&:hover {
background-color: #f5f5f5;
}
}
.el-pager li.active {
background-color: #1890ff;
color: white;
border-radius: 4px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
color: #666;
}
}
// 空状态
:deep(.el-table__empty-block) {
padding: 48px 0;
.el-table__empty-text {
color: #999;
font-size: 14px;
}
}
// 保留现有样式
.project-info-cell {
padding: 8px 0;
line-height: 1.5;
.project-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-desc {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.warning-count-wrapper {
display: inline-block;
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.text-info {
color: #909399;
}
.text-bold {
font-weight: bold;
}
</style>
```
---
## 视觉对比
### 旧设计 vs 新设计
| 元素 | 旧设计 | 新设计 | 改进 |
|------|--------|--------|------|
| **表格边框** | `1px solid #eee` | 无边框 + 阴影 | 更轻盈 |
| **圆角** | 4px | 8px | 更柔和 |
| **表头背景** | `#f5f5f5` | 透明 | 扁平化 |
| **表头高度** | 48px | 56px | 更舒适 |
| **行高** | 50px | 64px | 更透气 |
| **行内边距** | 12px | 20px | 留白充足 |
| **行分隔线** | `1px solid #f0f0f0` | 无 | 纯留白 |
| **悬停背景** | `#f5f5f5` | `#fafafa` | 更微妙 |
| **按钮悬停** | 下划线 | 背景色 | Material 风格 |
---
## 设计规范
### 阴影层级
- **默认卡片阴影**`0 2px 8px rgba(0, 0, 0, 0.1)`Elevation 2
- **悬停卡片阴影**`0 4px 12px rgba(0, 0, 0, 0.15)`Elevation 4
### 间距规范
- **卡片上边距**16px
- **表头高度**56px
- **表头内边距**16px 12px
- **数据行高度**64px
- **数据行内边距**20px 12px
- **按钮内边距**8px 12px
- **分页上边距**24px
### 颜色规范
- **卡片背景**#ffffff
- **表头文字**#333
- **表头底线**#e0e0e0
- **数据行文字**#333
- **悬停背景**#fafafa
- **操作按钮**#1890ff
- **按钮悬停**#096dd9
- **按钮悬停背景**`rgba(24, 144, 255, 0.08)`
- **激活页码**#1890ff
- **空状态文字**#999
### 圆角规范
- **卡片圆角**8px
- **按钮圆角**4px
- **页码圆角**4px
---
## 响应式考虑
### 大屏幕≥1920px
- 保持设计不变
- 可以考虑增加卡片间距
### 中等屏幕1366px - 1919px
- 当前设计最佳适配
### 小屏幕(<1366px
- 表格可能需要横向滚动
- 考虑固定关键列(如操作列)
---
## 浏览器兼容性
### 现代浏览器
- ✅ Chrome 80+
- ✅ Firefox 75+
- ✅ Safari 13+
- ✅ Edge 80+
### 潜在问题
- `box-shadow` 在所有现代浏览器中都支持良好
- `border-radius` 无兼容性问题
- `transition` 在现代浏览器中完全支持
---
## 实现步骤
1. 修改 `ProjectTable.vue``<style>` 部分
2. 替换所有边框样式为阴影
3. 调整表头、数据行的高度和内边距
4. 更新操作按钮和分页样式
5. 测试视觉效果和交互体验
6. 提交代码
---
## 文件修改
**修改文件:**
- `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**影响范围:**
- 仅影响样式,不影响功能
- 不影响其他组件
- 向后兼容
---
## 风险评估
**风险等级:** 🟢 **低风险**
- ✅ 纯样式优化,无业务逻辑变更
- ✅ 组件职责单一,影响范围可控
- ✅ 样式使用 scoped不污染其他组件
- ✅ 可以快速回滚(只需恢复旧样式)
---
## 后续优化建议
**可选增强(非必需):**
1. **添加 Ripple 效果**:操作按钮点击时的水波纹动画
2. **暗色模式**:提供暗色主题支持
3. **动画效果**:行展开/折叠的平滑动画
4. **可访问性**:添加高对比度模式支持
5. **响应式优化**:移动端的特殊处理
---
## 参考资源
- **Material Design 官方文档**: https://material.io/design
- **Element UI 文档**: https://element.eleme.cn/
- **当前设计文档**: `doc/plans/2026-02-27-项目管理首页优化-design.md`
- **当前实现计划**: `doc/plans/2026-02-27-项目管理首页优化.md`
---
**设计完成日期**: 2026-02-27
**设计状态**: ✅ 已批准
**下一步**: 创建实现计划

View File

@@ -1,654 +0,0 @@
# Material Design 表格样式优化实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将项目管理表格优化为 Material Design 风格,移除边框,使用阴影和留白分隔内容
**Architecture:** 纯样式优化,修改 ProjectTable 组件的 SCSS采用扁平卡片式设计
**Tech Stack:** Vue.js 2.6.12, Element UI 2.15.14, SCSS
---
## Task 1: 修改表格容器样式 - 添加阴影和圆角
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 定位表格样式部分**
找到 `<style lang="scss" scoped>` 中的 `.project-table-container` 部分(约第 245 行开始)。
**Step 2: 修改表格容器样式**
替换现有的 `:deep(.el-table)` 样式:
```scss
.project-table-container {
margin-top: 16px;
// 表格整体样式 - Material Design 卡片式
:deep(.el-table) {
// 移除边框,使用阴影
border: none; // 从 1px solid #eee 改为 none
border-radius: 8px; // 从 4px 增加到 8px
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 新增中等阴影
// 悬停时卡片阴影加深
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
```
**变更说明:**
- `border`: `1px solid #eee``none`
- `border-radius`: `4px``8px`
- 新增 `box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)`
- 新增悬停效果 `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15)`
- 新增过渡动画 `transition: box-shadow 0.3s ease`
**Step 3: 验证样式应用**
1. 启动前端开发服务器:
```bash
cd ruoyi-ui && npm run dev
```
2. 访问项目管理页面http://localhost:80
3. 使用浏览器开发者工具检查表格:
- 确认 `border` 为 `none`
- 确认 `border-radius` 为 `8px`
- 确认有阴影效果
- 鼠标悬停时阴影加深
**Step 4: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: Material Design - 表格容器添加阴影和圆角"
```
---
## Task 2: 扁平化表头样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 修改表头样式**
在 `:deep(.el-table)` 内修改 `th` 样式:
```scss
// 表头样式 - 扁平化,无背景色
th {
background-color: transparent; // 从 #f5f5f5 改为 transparent
color: #333;
font-weight: 600;
font-size: 14px;
height: 56px; // 从 48px 增加到 56px
padding: 16px 12px; // 从 12px 增加到 16px 垂直内边距
// 只保留底部一条分隔线
border-bottom: 2px solid #e0e0e0; // 新增
}
```
**变更说明:**
- `background-color`: `#f5f5f5` → `transparent`
- `height`: `48px` → `56px`
- `padding`: `12px` → `16px 12px`
- 新增 `border-bottom: 2px solid #e0e0e0`
**Step 2: 移除表头单元格的垂直分隔线**
确认 `.cell` 样式中已有:
```scss
.cell {
border-right: none; // 确认这行存在
}
```
**Step 3: 验证表头样式**
在浏览器中检查表头:
- 表头背景应为透明(白色)
- 表头文字应加粗
- 底部应有一条 2px 的灰色线
- 高度应增加到 56px
**Step 4: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: Material Design - 扁平化表头,移除背景色"
```
---
## Task 3: 优化数据行样式 - 移除分隔线,增加留白
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 修改数据行样式**
在 `:deep(.el-table)` 内修改 `td` 样式:
```scss
// 数据行样式 - 增加留白,移除分隔线
td {
color: #333;
font-size: 14px;
height: 64px; // 从 50px 增加到 64px
padding: 20px 12px; // 从 12px 增加到 20px 垂直内边距
border-bottom: none; // 从 1px solid #f0f0f0 改为 none
}
```
**变更说明:**
- `height`: `50px` → `64px` (+28%)
- `padding`: `12px` → `20px 12px` (+67% 垂直内边距)
- `border-bottom`: `1px solid #f0f0f0` → `none`
**Step 2: 优化悬停效果**
修改 `.el-table__row:hover > td` 样式:
```scss
// 悬停效果
.el-table__row {
transition: background-color 0.2s ease; // 新增过渡
&:hover > td {
background-color: #fafafa !important; // 从 #f5f5f5 改为 #fafafa
}
}
```
**变更说明:**
- 悬停背景色:`#f5f5f5` → `#fafafa`(更浅)
- 新增 `transition: background-color 0.2s ease`
**Step 3: 确认移除额外边框**
确认已有以下样式(如果不存在则添加):
```scss
// 移除表格内容的额外边框
.el-table__body-wrapper {
.cell {
border-right: none;
}
}
&::before,
&::after {
display: none; // 或 height: 0; width: 0;
}
```
**Step 4: 验证数据行样式**
在浏览器中检查:
- 行高应增加到 64px
- 行之间应无分隔线(纯留白)
- 悬停时背景应为浅灰色 `#fafafa`
- 过渡动画应平滑
**Step 5: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: Material Design - 移除行分隔线,增加留白"
```
---
## Task 4: 优化操作按钮样式 - Material Design 风格
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 修改操作按钮样式**
找到或添加 `:deep(.el-button--text)` 样式部分(约第 338 行),修改为:
```scss
// 操作按钮样式 - Material Design 风格
:deep(.el-button--text) {
color: #1890ff;
padding: 8px 12px; // 从 0 8px 增加到 8px 12px
border-radius: 4px; // 新增圆角
transition: all 0.2s ease; // 从只过渡颜色改为过渡所有属性
&:hover {
color: #096dd9;
background-color: rgba(24, 144, 255, 0.08); // 新增浅蓝色背景
text-decoration: none; // 移除下划线
}
&:first-child {
padding-left: 0; // 第一个按钮无左内边距
}
// 按钮间距
& + .el-button--text {
margin-left: 4px; // 从 8px 减少到 4px
}
}
```
**变更说明:**
- `padding`: `0 8px` → `8px 12px`(增加点击区域)
- 新增 `border-radius: 4px`
- 新增悬停背景色 `rgba(24, 144, 255, 0.08)`
- 移除悬停下划线 `text-decoration: none`
- `transition`: 只过渡颜色 → 过渡所有属性
- 按钮间距:`8px` → `4px`
**Step 2: 验证按钮样式**
在浏览器中测试:
- 按钮内边距应增加
- 悬停时应显示浅蓝色背景,无下划线
- 过渡动画应平滑
- 按钮之间应有适当间距
**Step 3: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: Material Design - 操作按钮添加悬停背景"
```
---
## Task 5: 优化分页组件样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 修改分页样式**
找到或添加 `:deep(.el-pagination)` 样式部分(约第 352 行),修改为:
```scss
// 分页样式优化 - Material Design 风格
:deep(.el-pagination) {
margin-top: 24px; // 从 16px 增加到 24px
text-align: right;
// 扁平化按钮
.btn-prev,
.btn-next,
.el-pager li {
border: none; // 移除边框
background-color: transparent; // 透明背景
&:hover {
background-color: #f5f5f5; // 悬停时浅灰背景
}
}
.el-pager li.active {
background-color: #1890ff; // 激活页码蓝色背景
color: white;
border-radius: 4px; // 添加圆角
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
color: #666; // 从 #606266 改为 #666
}
}
```
**变更说明:**
- `margin-top`: `16px` → `24px`
- 移除分页按钮边框
- 激活页码添加圆角 `border-radius: 4px`
- 统一文字颜色为 `#666`
**Step 2: 验证分页样式**
在浏览器中检查分页组件:
- 上边距应增加到 24px
- 所有按钮应无边框
- 激活页码应有蓝色背景 + 圆角
- 悬停时按钮应有浅灰背景
**Step 3: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: Material Design - 扁平化分页组件"
```
---
## Task 6: 全面测试和文档更新
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 视觉测试清单**
在浏览器中逐项检查:
**卡片容器:**
- [ ] 表格有圆角8px
- [ ] 表格有阴影(`0 2px 8px rgba(0,0,0,0.1)`
- [ ] 鼠标悬停时阴影加深
- [ ] 完全无边框
**表头:**
- [ ] 表头背景透明
- [ ] 表头文字加粗
- [ ] 底部有 2px 灰色分隔线
- [ ] 高度为 56px
**数据行:**
- [ ] 行高增加到 64px
- [ ] 行之间无分隔线
- [ ] 悬停时背景为 `#fafafa`
- [ ] 过渡动画平滑
**操作按钮:**
- [ ] 按钮内边距增加
- [ ] 悬停时显示浅蓝色背景
- [ ] 悬停时无下划线
- [ ] 按钮之间有适当间距
**分页组件:**
- [ ] 所有按钮无边框
- [ ] 激活页码有蓝色背景 + 圆角
- [ ] 悬停时按钮有浅灰背景
- [ ] 上边距为 24px
**Step 2: 交互测试**
测试以下交互:
- [ ] 鼠标悬停在表格上,阴影加深
- [ ] 鼠标悬停在行上,背景变化
- [ ] 点击操作按钮,检查事件是否正常触发
- [ ] 点击分页按钮,检查翻页功能
- [ ] 改变每页条数,检查表格刷新
- [ ] 表格横向滚动(如果内容超出)
**Step 3: 响应式测试**
在不同分辨率下测试:
- [ ] 1920x1080 - 表格正常显示
- [ ] 1366x768 - 表格正常显示
- [ ] 小于 1366px - 表格应可横向滚动
**Step 4: 浏览器兼容性测试**
在以下浏览器中测试:
- [ ] Chrome主要浏览器
- [ ] Edge
- [ ] Firefox可选
**Step 5: 截图对比(可选)**
拍摄优化前后的对比截图,保存到:
```
doc/screenshots/material-design-table-before.png
doc/screenshots/material-design-table-after.png
```
**Step 6: 更新最终验收报告**
创建或更新验收报告,记录所有改进:
```bash
# 如果需要更新验收报告
git add doc/implementation/final_acceptance_report.md
git commit -m "docs: 更新验收报告 - Material Design 样式优化"
```
**Step 7: 最终提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "feat: 完成 Material Design 表格样式优化
- 移除表格边框,使用阴影和圆角
- 扁平化表头,移除背景色
- 移除行分隔线,增加留白
- 优化操作按钮悬停效果
- 扁平化分页组件
- 全面视觉测试通过
"
```
---
## 完整样式代码参考
以下是完整的 `<style>` 部分代码,供参考:
```scss
<style lang="scss" scoped>
.project-table-container {
margin-top: 16px;
// 表格整体样式 - Material Design 卡片式
:deep(.el-table) {
border: none;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
// 表头样式 - 扁平化
th {
background-color: transparent;
color: #333;
font-weight: 600;
font-size: 14px;
height: 56px;
padding: 16px 12px;
border-bottom: 2px solid #e0e0e0;
}
// 数据行样式 - 增加留白
td {
color: #333;
font-size: 14px;
height: 64px;
padding: 20px 12px;
border-bottom: none;
}
// 移除列分隔线
.el-table__body-wrapper .cell {
border-right: none;
}
// 悬停效果
.el-table__row {
transition: background-color 0.2s ease;
&:hover > td {
background-color: #fafafa !important;
}
}
// 移除额外边框
&::before,
&::after {
display: none;
}
}
}
// 项目信息单元格
.project-info-cell {
padding: 8px 0;
line-height: 1.5;
.project-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-desc {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// 预警人数包装器
.warning-count-wrapper {
display: inline-block;
}
// 文字颜色类
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.text-info {
color: #909399;
}
.text-bold {
font-weight: bold;
}
// 操作按钮样式 - Material Design
:deep(.el-button--text) {
color: #1890ff;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
color: #096dd9;
background-color: rgba(24, 144, 255, 0.08);
text-decoration: none;
}
&:first-child {
padding-left: 0;
}
& + .el-button--text {
margin-left: 4px;
}
}
// 分页样式 - Material Design
:deep(.el-pagination) {
margin-top: 24px;
text-align: right;
.btn-prev,
.btn-next,
.el-pager li {
border: none;
background-color: transparent;
&:hover {
background-color: #f5f5f5;
}
}
.el-pager li.active {
background-color: #1890ff;
color: white;
border-radius: 4px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
color: #666;
}
}
// 空状态
:deep(.el-table__empty-block) {
padding: 48px 0;
.el-table__empty-text {
color: #999;
font-size: 14px;
}
}
</style>
```
---
## 验收标准
完成所有任务后,验证以下内容:
### 视觉验收
- [x] 表格作为浮动卡片,有阴影效果
- [x] 表格圆角为 8px
- [x] 鼠标悬停时阴影加深
- [x] 表头扁平化,无背景色
- [x] 表头高度 56px
- [x] 数据行高度 64px
- [x] 行之间无分隔线,纯留白
- [x] 悬停时行背景为 #fafafa
- [x] 操作按钮悬停有浅蓝色背景
- [x] 分页组件扁平化,激活页码有圆角
### 交互验收
- [x] 悬停效果平滑
- [x] 所有操作按钮点击正常
- [x] 分页功能正常
- [x] 表格滚动正常
### 代码质量验收
- [x] 样式使用 scoped
- [x] 无冗余代码
- [x] 遵循 Material Design 规范
- [x] 每个改进有独立提交
---
## 风险与注意事项
1. **视觉冲击**:变化较大,用户可能需要适应时间
2. **数据密集场景**:留白增加可能需要更多滚动
3. **浏览器兼容**:现代浏览器都支持,无兼容性问题
4. **回滚方案**:如有问题,可以通过 git revert 快速回滚
---
## 参考资源
- 设计文档:`doc/plans/2026-02-27-Material-Design-表格样式优化-design.md`
- Material Design 官方文档https://material.io/design
- Element UI 文档https://element.eleme.cn/
- 当前实现:`doc/plans/2026-02-27-项目管理首页优化.md`

View File

@@ -1,358 +0,0 @@
# 项目管理首页优化设计文档
**日期**: 2026-02-27
**状态**: 已批准
**方案**: 混合方案方案3
## 概述
本文档描述项目管理首页的用户界面优化设计,包括搜索栏、表格样式和操作按钮的改进。目标是提升用户体验,使界面更符合现代设计标准,并增强功能性。
## 需求总结
1. **搜索栏优化**:添加独立的重置按钮,调整布局
2. **状态列优化**:增加宽度至 160px添加图标
3. **操作按钮条件显示**:根据项目状态显示不同操作
4. **表格视觉优化**:按照参考截图实现现代化样式
## 设计方案
### 1. 搜索栏设计
**布局结构**
```
┌────────────────────────────────────────────────────────────────┐
│ [🔍 项目名称] [状态选择] [搜索] [重置] [新建项目] [导入历史] │
└────────────────────────────────────────────────────────────────┘
```
**具体实现**
| 元素 | 说明 |
|------|------|
| 项目名称输入框 | 宽度约占25%,带搜索图标前缀,支持回车搜索 |
| 状态下拉框 | 宽度约占15%,选项:全部/进行中/已完成/已归档 |
| 搜索按钮 | 蓝色主按钮(#1890ff),从输入框内移出独立显示 |
| 重置按钮 | 默认按钮样式(白底灰边),点击清空所有搜索条件并刷新 |
| 新建项目 | 蓝色主按钮,右对齐 |
| 导入历史项目 | 默认按钮,右对齐 |
### 2. 表格设计
#### 2.1 状态列设计(宽度 160px
**视觉效果**:圆点图标 + 文字标签
| 状态 | 图标颜色 | 标签颜色 | 文字 |
|------|----------|----------|------|
| 进行中 | 蓝色圆点 | type="primary" (#1890ff) | 进行中 |
| 已完成 | 绿色圆点 | type="success" (#52c41a) | 已完成 |
| 已归档 | 灰色圆点 | type="info" (#909399) | 已归档 |
#### 2.2 操作列设计(宽度 200px
**条件渲染逻辑**
| 项目状态 | 显示的按钮 |
|----------|------------|
| 进行中('0' | 进入项目 |
| 已完成('1' | 查看结果、重新分析、归档 |
| 已归档('2' | 查看结果 |
**按钮样式**
- 类型文字按钮type="text"
- 颜色:蓝色(#1890ff
- 悬停:深蓝色(#096dd9+ 下划线
- 间距8px
#### 2.3 表格整体样式
**表头**
- 背景色:#f5f5f5
- 文字:深灰色粗体(#333
- 字号14px
- 高度48px
**数据行**
- 高度50-60px根据内容自动调整
- 背景色:#fff
- 文字颜色:#333
- 内边距12px
- 悬停背景:#f5f5f5
- 过渡时间0.3s
**边框**
- 表格外边框1px solid #eee
- 行分隔线1px solid #f0f0f0
- 列分隔线:无或极浅(#fafafa
**列宽分布**
- 项目名称300px左对齐
- 项目状态160px居中
- 目标人数100px居中
- 预警人数120px居中保留悬停详情
- 创建人120px居中
- 创建时间160px居中
- 操作200px居中
### 3. 样式规范
#### 3.1 配色方案
| 用途 | 颜色 | 色值 |
|------|------|------|
| 主色调 | 蓝色 | #1890ff |
| 成功色 | 绿色 | #52c41a |
| 警告色 | 红色 | #f5222d |
| 主要文字 | 深灰色 | #333333 |
| 次要文字 | 中灰色 | #909399 |
| 背景色 | 浅灰色 | #f5f5f5 |
| 卡片背景 | 白色 | #ffffff |
#### 3.2 间距规范
- 页面边距16px
- 卡片内边距12px - 20px
- 元素间距12px
- 按钮间距8px
- 表格单元格内边距12px
#### 3.3 字体规范
- 标题18pxfont-weight: 500
- 副标题13pxfont-weight: 400
- 表头14pxfont-weight: 600
- 正文14pxfont-weight: 400
- 小文字12px
#### 3.4 圆角与阴影
- 卡片圆角4px
- 按钮圆角4px
- 标签圆角4px
- 阴影:`0 1px 4px rgba(0, 0, 0, 0.08)`
#### 3.5 交互效果
**按钮悬停**
- 蓝色按钮:背景色 → #096dd9
- 文字链接:添加下划线,颜色 → #096dd9
**表格行悬停**
- 背景色 → #f5f5f5
- 过渡时间0.3s
## 技术实现方案
### 需要修改的文件
1. **SearchBar.vue**
- 路径:`ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
- 修改内容:
- 添加重置按钮
- 调整布局结构(将搜索按钮移出输入框)
- 优化样式和间距
2. **ProjectTable.vue**
- 路径:`ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
- 修改内容:
- 状态列宽度调整为 160px
- 状态列添加图标渲染
- 操作列实现条件渲染逻辑
- 优化表格样式(表头、行高、悬停效果)
3. **index.vue**
- 路径:`ruoyi-ui/src/views/ccdiProject/index.vue`
- 修改内容:
- 添加重置功能的处理方法(如果需要)
- 确认所有操作按钮的事件处理已实现
### 关键代码逻辑
#### 1. 搜索栏重置功能
```javascript
// SearchBar.vue
handleReset() {
this.searchKeyword = ''
this.selectedStatus = ''
this.$emit('query', {
projectName: null,
status: null
})
}
```
#### 2. 操作按钮条件渲染
```vue
<!-- ProjectTable.vue -->
<template slot-scope="scope">
<!-- 进行中状态 -->
<el-button
v-if="scope.row.status === '0'"
size="mini"
type="text"
icon="el-icon-right"
@click="handleEnter(scope.row)"
>进入项目</el-button>
<!-- 已完成状态 -->
<template v-if="scope.row.status === '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>查看结果</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-refresh"
@click="handleReAnalyze(scope.row)"
>重新分析</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-folder"
@click="handleArchive(scope.row)"
>归档</el-button>
</template>
<!-- 已归档状态 -->
<el-button
v-if="scope.row.status === '2'"
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>查看结果</el-button>
</template>
```
#### 3. 状态列图标渲染
```vue
<!-- ProjectTable.vue -->
<el-table-column
prop="status"
label="项目状态"
width="160"
align="center"
>
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
<dict-tag :options="dict.type.ccdi_project_status" :value="scope.row.status"/>
</el-tag>
</template>
</el-table-column>
```
#### 4. 表格样式优化
```scss
// ProjectTable.vue - scoped styles
.project-table-container {
:deep(.el-table) {
// 表头样式
th {
background-color: #f5f5f5;
color: #333;
font-weight: 600;
font-size: 14px;
height: 48px;
padding: 12px;
}
// 数据行样式
td {
color: #333;
font-size: 14px;
height: 50px;
padding: 12px;
}
// 悬停效果
.el-table__row:hover > td {
background-color: #f5f5f5 !important;
transition: background-color 0.3s;
}
}
}
// 操作按钮样式
:deep(.el-button--text) {
color: #1890ff;
&:hover {
color: #096dd9;
text-decoration: underline;
}
}
```
### 实现步骤
1. **修改 SearchBar 组件**
- 添加重置按钮的模板和事件处理
- 调整布局,将搜索按钮移出输入框
- 优化样式和间距
2. **修改 ProjectTable 组件**
- 调整状态列宽度为 160px
- 实现操作按钮的条件渲染逻辑
- 优化表格样式(表头、行高、悬停效果)
3. **更新 index.vue**
- 确认所有操作按钮的事件处理方法已实现
- 测试重置功能
4. **统一调整样式**
- 确保所有组件的配色、间距、字体一致
- 测试视觉效果是否匹配参考截图
## 测试要点
### 功能测试
- [ ] 搜索功能正常(项目名称、状态筛选)
- [ ] 重置按钮清空所有条件并刷新列表
- [ ] 操作按钮根据状态正确显示
- [ ] 所有操作按钮的点击事件正常触发
### 视觉测试
- [ ] 表格行高、间距符合设计
- [ ] 表头样式正确(背景色、字体、高度)
- [ ] 悬停效果正常
- [ ] 状态列图标和颜色正确
- [ ] 操作按钮颜色、间距、悬停效果正确
- [ ] 整体配色、圆角、阴影符合设计规范
### 兼容性测试
- [ ] Chrome 浏览器测试
- [ ] Edge 浏览器测试
- [ ] 不同屏幕分辨率测试1366x768、1920x1080
## 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 样式冲突 | 中 | 使用 scoped style避免全局样式污染 |
| 现有功能受影响 | 低 | 只修改样式和条件渲染,不改变数据逻辑 |
| 浏览器兼容性 | 低 | 使用 Element UI 标准组件,兼容性好 |
## 后续优化建议
1. **性能优化**:如果项目列表数据量大,考虑添加虚拟滚动
2. **用户体验**:添加加载动画和空状态提示
3. **响应式设计**:适配移动端设备(如有需求)
4. **无障碍访问**:添加 ARIA 标签,提升可访问性
## 参考资源
- 参考截图:`doc/创建项目功能/ScreenShot_2026-02-27_091429_733.png`
- Element UI 文档https://element.eleme.cn/
- 项目 CLAUDE.md 文件

View File

@@ -1,690 +0,0 @@
# 项目管理首页优化实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 优化项目管理首页的搜索栏、表格样式和操作按钮,提升用户体验和视觉效果
**Architecture:** 采用混合方案,在现有组件结构基础上优化布局、样式和交互逻辑,不进行大规模重构
**Tech Stack:** Vue.js 2.6.12, Element UI 2.15.14, SCSS
---
## Task 1: 优化 SearchBar 组件
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
**Step 1: 添加重置按钮到模板**
在搜索按钮后添加重置按钮:
```vue
<!-- el-col :span="11" 的按钮组之前先调整搜索和重置按钮 -->
<el-col :span="8">
<el-input
v-model="searchKeyword"
placeholder="请输入项目名称"
prefix-icon="el-icon-search"
clearable
size="medium"
@keyup.enter.native="handleSearch"
/>
</el-col>
<el-col :span="5">
<el-select
v-model="selectedStatus"
placeholder="项目状态"
clearable
size="medium"
style="width: 100%"
@change="handleStatusChange"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="4">
<el-button
type="primary"
icon="el-icon-search"
size="medium"
@click="handleSearch"
>搜索</el-button>
<el-button
icon="el-icon-refresh"
size="medium"
@click="handleReset"
>重置</el-button>
</el-col>
<el-col :span="7" style="text-align: right">
<el-button
type="primary"
icon="el-icon-plus"
size="medium"
@click="handleAdd"
>新建项目</el-button>
<el-button
icon="el-icon-folder-opened"
size="medium"
@click="handleImport"
>导入历史项目</el-button>
</el-col>
```
**Step 2: 添加重置方法**
`methods` 中添加:
```javascript
/** 重置 */
handleReset() {
this.searchKeyword = ''
this.selectedStatus = ''
this.emitQuery()
}
```
**Step 3: 移除 watch 中的自动重置逻辑**
删除或注释掉 watch 中的 `searchKeyword` 监听:
```javascript
// watch: {
// searchKeyword(newVal) {
// if (newVal === '') {
// this.emitQuery()
// }
// }
// }
```
**Step 4: 更新样式**
调整按钮间距:
```scss
:deep(.el-button--medium) {
padding: 10px 16px;
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
```
移除输入框内的搜索按钮样式(因为已移出):
```scss
// 删除这段样式
// :deep(.el-input-group__append) {
// background-color: #409EFF;
// color: white;
// border-color: #409EFF;
// cursor: pointer;
//
// &:hover {
// background-color: #66b1ff;
// }
// }
```
**Step 5: 测试搜索和重置功能**
1. 启动前端开发服务器:
```bash
cd ruoyi-ui && npm run dev
```
2. 访问项目管理页面http://localhost:80
3. 测试搜索功能:
- 输入项目名称,点击搜索按钮
- 验证列表正确过滤
- 选择状态,验证列表正确过滤
4. 测试重置功能:
- 点击重置按钮
- 验证输入框和下拉框被清空
- 验证列表显示全部项目
**Step 6: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue
git commit -m "feat: SearchBar 组件添加重置按钮并优化布局"
```
---
## Task 2: 优化 ProjectTable 状态列
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 调整状态列宽度**
将状态列宽度从 100px 改为 160px
```vue
<!-- 项目状态 -->
<el-table-column
prop="status"
label="项目状态"
width="160"
align="center"
>
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
<dict-tag :options="dict.type.ccdi_project_status" :value="scope.row.status"/>
</el-tag>
</template>
</el-table-column>
```
**Step 2: 测试状态列显示**
1. 访问项目管理页面
2. 验证状态列宽度足够显示标签
3. 验证不同状态的标签颜色正确:
- 进行中:蓝色
- 已完成:绿色
- 已归档:灰色
**Step 3: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "feat: 项目状态列宽度调整为 160px"
```
---
## Task 3: 实现操作按钮条件渲染
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 修改操作列模板**
替换操作列的模板:
```vue
<!-- 操作列 -->
<el-table-column
label="操作"
width="200"
align="center"
fixed="right"
>
<template slot-scope="scope">
<!-- 进行中状态 (status = '0') -->
<el-button
v-if="scope.row.status === '0'"
size="mini"
type="text"
icon="el-icon-right"
@click="handleEnter(scope.row)"
>进入项目</el-button>
<!-- 已完成状态 (status = '1') -->
<template v-if="scope.row.status === '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>查看结果</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-refresh"
@click="handleReAnalyze(scope.row)"
>重新分析</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-folder"
@click="handleArchive(scope.row)"
>归档</el-button>
</template>
<!-- 已归档状态 (status = '2') -->
<el-button
v-if="scope.row.status === '2'"
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>查看结果</el-button>
</template>
</el-table-column>
```
**Step 2: 添加新的事件发射方法**
在 `methods` 中添加:
```javascript
handleEnter(row) {
this.$emit('enter', row)
},
handleViewResult(row) {
this.$emit('view-result', row)
},
handleReAnalyze(row) {
this.$emit('re-analyze', row)
},
handleArchive(row) {
this.$emit('archive', row)
}
```
**Step 3: 删除旧的 handleDetail, handleEdit, handleDelete 方法**
移除不再需要的方法:
```javascript
// 删除以下方法
// handleDetail(row) {
// this.$emit('detail', row)
// },
// handleEdit(row) {
// this.$emit('edit', row)
// },
// handleDelete(row) {
// this.$emit('delete', row)
// }
```
**Step 4: 测试条件渲染**
1. 确保数据库中有不同状态的项目数据
2. 访问项目管理页面
3. 验证按钮根据状态正确显示:
- 进行中项目:只显示"进入项目"
- 已完成项目:显示"查看结果"、"重新分析"、"归档"
- 已归档项目:只显示"查看结果"
4. 点击各个按钮,验证点击事件正常触发(可在浏览器控制台查看)
**Step 5: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "feat: 操作按钮根据项目状态条件渲染"
```
---
## Task 4: 优化表格样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 优化表格样式**
更新 `<style>` 部分:
```scss
<style lang="scss" scoped>
.project-table-container {
margin-top: 16px;
// 表格整体样式
:deep(.el-table) {
border: 1px solid #eee;
border-radius: 4px;
// 表头样式
th {
background-color: #f5f5f5;
color: #333;
font-weight: 600;
font-size: 14px;
height: 48px;
padding: 12px;
}
// 数据行样式
td {
color: #333;
font-size: 14px;
height: 50px;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
}
// 移除列分隔线
.el-table__body-wrapper {
.cell {
border-right: none;
}
}
// 悬停效果
.el-table__row:hover > td {
background-color: #f5f5f5 !important;
transition: background-color 0.3s;
}
// 表格内容无额外边框
&::before {
height: 0;
}
&::after {
width: 0;
}
}
}
.project-info-cell {
padding: 8px 0;
line-height: 1.5;
.project-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-desc {
font-size: 12px;
color: #909399;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.warning-count-wrapper {
display: inline-block;
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.text-info {
color: #909399;
}
.text-bold {
font-weight: bold;
}
// 操作按钮样式
:deep(.el-button--text) {
color: #1890ff;
padding: 0 8px;
&:hover {
color: #096dd9;
text-decoration: underline;
}
&:first-child {
padding-left: 0;
}
}
// 分页样式优化
:deep(.el-pagination) {
margin-top: 16px;
text-align: right;
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
color: #606266;
}
}
</style>
```
**Step 2: 测试表格视觉效果**
1. 访问项目管理页面
2. 验证表格样式:
- 表头背景为浅灰色(#f5f5f5
- 表头文字为深灰色粗体
- 数据行高度约 50px
- 鼠标悬停时行背景变为浅灰色
- 列之间无分隔线或极浅
- 行分隔线为浅灰色
3. 验证操作按钮样式:
- 按钮文字为蓝色(#1890ff
- 悬停时变为深蓝色(#096dd9并显示下划线
- 按钮间距为 8px
**Step 3: 提交更改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: 优化表格样式,匹配参考设计"
```
---
## Task 5: 更新 index.vue 并全面测试
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
**Step 1: 验证事件处理方法**
确认 `index.vue` 中已有以下方法(从代码审查看已经存在):
```javascript
/** 进入项目 */
handleEnter(row) {
console.log('进入项目:', row)
this.$modal.msgSuccess('进入项目: ' + row.projectName)
},
/** 查看结果 */
handleViewResult(row) {
console.log('查看结果:', row)
this.$modal.msgInfo('查看项目结果: ' + row.projectName)
},
/** 重新分析 */
handleReAnalyze(row) {
console.log('重新分析:', row)
this.$modal.msgSuccess('正在重新分析项目: ' + row.projectName)
},
/** 归档项目 */
handleArchive(row) {
this.currentArchiveProject = row
this.archiveDialogVisible = true
}
```
**Step 2: 移除不需要的事件监听**
从 `project-table` 组件中移除不再使用的事件:
```vue
<!-- 项目列表表格 -->
<project-table
:loading="loading"
:data-list="projectList"
:total="total"
:page-params="queryParams"
@pagination="getList"
@enter="handleEnter"
@view-result="handleViewResult"
@re-analyze="handleReAnalyze"
@archive="handleArchive"
/>
```
移除:
- `@detail`
- `@edit`
- `@delete`
**Step 3: 全面功能测试**
1. **搜索功能测试**
```
- 输入项目名称 → 点击搜索 → 验证过滤结果
- 选择状态筛选 → 验证过滤结果
- 点击重置 → 验证所有条件清空,显示全部项目
```
2. **操作按钮测试**
```
- 找到"进行中"项目 → 验证只显示"进入项目"按钮 → 点击测试
- 找到"已完成"项目 → 验证显示三个按钮 → 逐一点击测试
- 找到"已归档"项目 → 验证只显示"查看结果"按钮 → 点击测试
```
3. **视觉测试**
```
- 检查表头样式(背景色、字体)
- 检查行高和间距
- 检查悬停效果
- 检查操作按钮颜色和悬停效果
- 检查状态列宽度和标签样式
```
4. **响应式测试**
```
- 在不同分辨率下测试1366x768, 1920x1080
- 测试表格滚动是否正常
```
**Step 4: 修复发现的问题**
如果测试中发现任何问题,记录并修复:
```bash
# 修复后提交
git add <modified-files>
git commit -m "fix: 修复[具体问题描述]"
```
**Step 5: 最终提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 完成项目管理首页优化
- SearchBar 添加重置按钮
- 状态列宽度调整为 160px
- 操作按钮根据状态条件显示
- 表格样式优化以匹配参考设计
"
```
---
## Task 6: 代码审查与文档更新
**Step 1: 代码审查清单**
检查以下内容:
- [ ] 所有文件路径正确
- [ ] 样式使用 scoped不影响其他组件
- [ ] 颜色使用标准值(#1890ff 等)
- [ ] 按钮间距和边距符合设计规范
- [ ] 事件命名遵循 kebab-caseview-result, re-analyze
- [ ] 删除了不再使用的代码和注释
**Step 2: 更新 CLAUDE.md如有必要**
如果修改了重要功能或添加了新的规范,更新项目文档:
```bash
# 如果有更新
git add CLAUDE.md
git commit -m "docs: 更新项目管理模块文档"
```
**Step 3: 生成变更总结**
```bash
git log --oneline --decorate --graph -10
```
记录所有提交,确保每个功能点都有对应的提交。
**Step 4: 推送到远程(如需要)**
```bash
git push origin dev
```
---
## 验收标准
完成所有任务后,验证以下内容:
### 功能验收
- [x] 搜索栏有独立的重置按钮
- [x] 重置按钮清空所有搜索条件并刷新列表
- [x] 状态列宽度为 160px
- [x] 进行中项目只显示"进入项目"按钮
- [x] 已完成项目显示"查看结果"、"重新分析"、"归档"按钮
- [x] 已归档项目只显示"查看结果"按钮
- [x] 所有按钮点击事件正常触发
### 视觉验收
- [x] 表头背景为浅灰色(#f5f5f5
- [x] 表头文字为深灰色粗体
- [x] 数据行高度约 50px
- [x] 悬停效果正常(背景 #f5f5f5
- [x] 状态标签颜色正确
- [x] 操作按钮为蓝色(#1890ff
- [x] 悬停时按钮变为深蓝色并显示下划线
### 代码质量验收
- [x] 代码使用 scoped style
- [x] 无冗余代码和注释
- [x] 遵循项目编码规范
- [x] 每个功能点有独立提交
---
## 风险与注意事项
1. **样式冲突**:使用 scoped style 和深度选择器避免影响其他组件
2. **现有功能**:只修改样式和条件渲染,不改变数据逻辑
3. **测试覆盖**:手动测试所有操作按钮和搜索功能
4. **浏览器兼容**:在 Chrome 和 Edge 中测试
---
## 参考资源
- 设计文档:`doc/plans/2026-02-27-项目管理首页优化-design.md`
- 参考截图:`doc/创建项目功能/ScreenShot_2026-02-27_091429_733.png`
- Element UI 文档https://element.eleme.cn/

View File

@@ -1,245 +0,0 @@
# 项目列表页面UI优化设计文档
**文档版本**: 1.0
**创建日期**: 2026-02-28
**创建人**: Claude Code
**状态**: 已确认
---
## 1. 概述
### 1.1 背景
根据原型图 `ScreenShot_2026-02-27_111611_994.png`,对项目列表页面(`ccdiProject/index.vue`)进行 UI 优化,使其更符合扁平化设计风格。
### 1.2 目标
- 简化页面标题样式,去掉卡片式装饰
- 优化搜索区域,添加独立的搜索按钮
- 保持表格表头现有样式
### 1.3 影响范围
- 页面:`ruoyi-ui/src/views/ccdiProject/index.vue`
- 组件:`ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
---
## 2. 设计方案
### 2.1 方案选择
采用**最小改动方案**,只修改必要的样式和结构,降低风险。
### 2.2 详细设计
#### 2.2.1 页面标题修改
**当前实现:**
- 标题区域使用卡片式设计(白色背景、圆角、阴影)
- 字体大小20px
- 字体粗细500
**修改内容:**
- 移除白色背景
- 移除圆角border-radius
- 移除阴影box-shadow
- 保留字体大小和粗细
- 保留 flex 布局和间距
**样式对比:**
修改前:
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
background: #ffffff; // 移除
border-radius: 8px; // 移除
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); // 移除
}
```
修改后:
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
// 移除 background、border-radius、box-shadow
}
```
#### 2.2.2 搜索按钮修改
**当前实现:**
- 搜索图标位于输入框内部suffix slot
- 通过点击图标或回车触发搜索
**修改内容:**
- 移除输入框内的搜索图标
- 在输入框外部添加独立的搜索按钮
- 按钮与输入框使用 flex 布局组合
- 按钮高度与输入框一致40px
**结构对比:**
修改前:
```vue
<el-input v-model="searchKeyword" placeholder="请输入关键词搜索项目" clearable>
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch" />
</el-input>
```
修改后:
```vue
<div class="search-input-wrapper">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
</div>
```
**样式调整:**
```scss
.search-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
width: 240px;
height: 40px;
}
```
#### 2.2.3 表格表头样式
**当前实现:**
- 透明背景background-color: transparent
- 深色加粗文字font-weight: 600, color: #333
- 底部 2px 分隔线
**修改内容:**
- 保持不变,已符合需求
---
## 3. 用户体验改进
### 3.1 视觉层次
- 页面标题扁平化,减少视觉干扰
- 搜索按钮独立显示,操作更明确
### 3.2 交互优化
- 搜索按钮支持点击触发搜索
- 保留回车和清空触发搜索的功能
---
## 4. 技术实现
### 4.1 文件修改清单
| 文件路径 | 修改类型 | 修改内容 |
|---------|---------|---------|
| `ccdiProject/index.vue` | 样式修改 | 移除 `.page-header` 的背景、圆角、阴影 |
| `ccdiProject/components/SearchBar.vue` | 结构+样式修改 | 移除搜索图标,添加独立搜索按钮 |
### 4.2 关键代码
#### index.vue 样式修改
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
// 移除背景、圆角、阴影
}
```
#### SearchBar.vue 结构修改
```vue
<div class="search-filter-bar">
<div class="search-input-wrapper">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
</div>
<!-- 标签页筛选 -->
<div class="tab-filters">
<!-- ... -->
</div>
</div>
```
---
## 5. 测试要点
### 5.1 功能测试
- [ ] 搜索按钮点击触发搜索
- [ ] 输入框回车触发搜索
- [ ] 输入框清空触发搜索
- [ ] 标签页切换正常工作
### 5.2 样式测试
- [ ] 页面标题扁平化,无背景、圆角、阴影
- [ ] 搜索按钮与输入框同高40px
- [ ] 搜索按钮与输入框间距 8px
- [ ] 表格表头样式保持不变
### 5.3 兼容性测试
- [ ] Chrome 浏览器
- [ ] Firefox 浏览器
- [ ] Edge 浏览器
---
## 6. 风险评估
### 6.1 技术风险
- **低风险**:只修改样式和少量 HTML 结构
- **无后端影响**:不涉及 API 调用
### 6.2 兼容性风险
- **低风险**:使用标准的 Element UI 组件和 CSS flex 布局
---
## 7. 实施计划
### 7.1 开发任务
1. 修改 `index.vue` 的页面标题样式
2. 修改 `SearchBar.vue` 的搜索区域结构和样式
3. 本地测试验证
### 7.2 预计工作量
- 开发时间0.5 小时
- 测试时间0.5 小时
---
## 8. 参考资料
- 原型图:`doc/创建项目功能/ScreenShot_2026-02-27_111611_994.png`
- 当前代码:`ruoyi-ui/src/views/ccdiProject/index.vue`
- 搜索组件:`ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
- 表格组件:`ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`

View File

@@ -1,474 +0,0 @@
# 项目列表页面UI优化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 优化项目列表页面 UI实现扁平化设计添加独立搜索按钮
**Architecture:** 修改两个 Vue 组件index.vue 和 SearchBar.vue移除页面标题的卡片式装饰将搜索图标改为独立按钮
**Tech Stack:** Vue 2.6.12, Element UI 2.15.14, SCSS
---
## Task 1: 修改页面标题样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue:266-282` (样式部分)
**Step 1: 读取当前文件**
读取文件:`ruoyi-ui/src/views/ccdiProject/index.vue`
查看 `.page-header` 样式块(第 266-282 行)
**Step 2: 移除页面标题的卡片样式**
`<style lang="scss" scoped>` 部分,找到 `.page-header` 样式块,移除以下三行:
- `padding: 16px 20px;`
- `background: #ffffff;`
- `border-radius: 8px;`
- `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);`
修改后的样式:
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.page-title {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #303133;
}
}
```
**Step 3: 保存文件**
保存修改后的文件
**Step 4: 提交修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "refactor: 移除页面标题的卡片式样式"
```
---
## Task 2: 修改搜索区域结构和样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue` (模板和样式)
**Step 1: 读取当前 SearchBar 组件**
读取文件:`ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
**Step 2: 修改模板结构**
`<template>` 部分(第 2-28 行),将搜索输入框和标签页筛选分离。
修改前的结构(第 2-17 行):
```vue
<div class="search-filter-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
>
<i
slot="suffix"
class="el-icon-search search-icon"
@click="handleSearch"
/>
</el-input>
<div class="tab-filters">
<!-- 标签页内容 -->
</div>
</div>
```
修改后的结构:
```vue
<div class="search-filter-bar">
<div class="search-input-wrapper">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
/>
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
</div>
<div class="tab-filters">
<div
v-for="tab in tabs"
:key="tab.value"
:class="['tab-item', { active: activeTab === tab.value }]"
@click="handleTabChange(tab.value)"
>
{{ tab.label }}({{ tab.count }})
</div>
</div>
</div>
```
**Step 3: 修改样式**
`<style lang="scss" scoped>` 部分(第 94-146 行),更新样式:
移除以下样式:
```scss
.search-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
margin-right: 8px;
&:hover {
color: #3B82F6;
}
}
```
添加新的样式:
```scss
.search-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
```
完整的样式部分应该是:
```scss
.search-filter-bar {
display: flex;
align-items: center;
gap: 24px;
padding: 16px 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
width: 240px;
height: 40px;
}
.tab-filters {
display: flex;
align-items: center;
gap: 24px;
}
.tab-item {
font-size: 14px;
color: #6B7280;
cursor: pointer;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.2s ease;
user-select: none;
&:hover {
color: #3B82F6;
}
&.active {
color: #3B82F6;
background: #EFF6FF;
font-weight: 500;
}
}
```
**Step 4: 保存文件**
保存修改后的文件
**Step 5: 提交修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue
git commit -m "feat: 添加独立搜索按钮,移除输入框内搜索图标"
```
---
## Task 3: 启动前端服务进行测试
**Files:**
- Test: 浏览器手动测试
**Step 1: 启动前端开发服务器**
```bash
cd ruoyi-ui
npm run dev
```
Expected: 前端服务启动在 http://localhost:80
**Step 2: 在浏览器中打开项目列表页面**
1. 打开浏览器访问 http://localhost:80
2. 使用测试账号登录(用户名: admin, 密码: admin123
3. 导航到"初核项目管理"页面
Expected: 页面正常加载
**Step 3: 验证页面标题样式**
检查页面标题"初核项目管理"区域:
- [ ] 无白色背景
- [ ] 无圆角
- [ ] 无阴影
- [ ] 字体大小为 20px
- [ ] 字体粗细为 500
**Step 4: 验证搜索按钮功能**
检查搜索区域:
- [ ] 输入框右侧有独立的"搜索"按钮
- [ ] 输入框内无搜索图标
- [ ] 按钮与输入框高度一致40px
- [ ] 按钮与输入框间距为 8px
- [ ] 点击搜索按钮触发搜索
- [ ] 输入框回车触发搜索
- [ ] 输入框清空触发搜索
**Step 5: 验证标签页功能**
检查标签页切换:
- [ ] 点击"全部项目"显示所有项目
- [ ] 点击"进行中"显示进行中的项目
- [ ] 点击"已完成"显示已完成的项目
- [ ] 点击"已归档"显示已归档的项目
**Step 6: 验证表格表头样式**
检查表格表头:
- [ ] 透明背景
- [ ] 深色加粗文字
- [ ] 底部有分隔线
---
## Task 4: 创建测试报告
**Files:**
- Create: `doc/test-scripts/2026-02-28-project-list-ui-test-report.md`
**Step 1: 创建测试报告文档**
创建文件:`doc/test-scripts/2026-02-28-project-list-ui-test-report.md`
**Step 2: 编写测试报告内容**
```markdown
# 项目列表页面 UI 优化测试报告
**测试日期**: 2026-02-28
**测试环境**: Chrome/Firefox/Edge
**测试人员**: [姓名]
---
## 1. 测试环境
- 前端地址: http://localhost:80
- 后端地址: http://localhost:8080
- 测试账号: admin/admin123
- 测试页面: 初核项目管理
---
## 2. 样式测试
### 2.1 页面标题
- [x] 无白色背景
- [x] 无圆角
- [x] 无阴影
- [x] 字体大小为 20px
- [x] 字体粗细为 500
**结果**: 通过
### 2.2 搜索按钮
- [x] 输入框右侧有独立的"搜索"按钮
- [x] 输入框内无搜索图标
- [x] 按钮与输入框高度一致40px
- [x] 按钮与输入框间距为 8px
**结果**: 通过
### 2.3 表格表头
- [x] 透明背景
- [x] 深色加粗文字
- [x] 底部有分隔线
**结果**: 通过
---
## 3. 功能测试
### 3.1 搜索功能
- [x] 点击搜索按钮触发搜索
- [x] 输入框回车触发搜索
- [x] 输入框清空触发搜索
**测试步骤**:
1. 在搜索框输入关键词"测试"
2. 点击"搜索"按钮
3. 验证列表只显示包含"测试"的项目
**结果**: 通过
### 3.2 标签页切换
- [x] 点击"全部项目"显示所有项目
- [x] 点击"进行中"显示进行中的项目
- [x] 点击"已完成"显示已完成的项目
- [x] 点击"已归档"显示已归档的项目
**测试步骤**:
1. 点击"进行中"标签
2. 验证列表只显示状态为"进行中"的项目
3. 验证标签计数与实际数量一致
**结果**: 通过
---
## 4. 兼容性测试
### 4.1 Chrome 浏览器
- [x] 页面正常显示
- [x] 功能正常工作
**结果**: 通过
### 4.2 Firefox 浏览器
- [x] 页面正常显示
- [x] 功能正常工作
**结果**: 通过
### 4.3 Edge 浏览器
- [x] 页面正常显示
- [x] 功能正常工作
**结果**: 通过
---
## 5. 浏览器截图
### 5.1 页面整体效果
[截图:显示页面标题和搜索区域]
### 5.2 搜索按钮特写
[截图:显示搜索按钮与输入框的布局]
### 5.3 表格表头效果
[截图:显示表格表头样式]
---
## 6. 测试总结
**测试结果**: 全部通过
**发现问题**: 无
**改进建议**: 无
---
## 7. 附录
### 7.1 测试数据
- 测试项目数量: 25
- 进行中项目: 10
- 已完成项目: 8
- 已归档项目: 7
### 7.2 参考文档
- 设计文档: `doc/plans/2026-02-28-project-list-ui-optimization-design.md`
- 原型图: `doc/创建项目功能/ScreenShot_2026-02-27_111611_994.png`
```
**Step 3: 保存测试报告**
保存文件
**Step 4: 提交测试报告**
```bash
git add doc/test-scripts/2026-02-28-project-list-ui-test-report.md
git commit -m "docs: 添加项目列表页面UI优化测试报告"
```
---
## Task 5: 推送代码到远程仓库
**Step 1: 推送所有提交**
```bash
git push origin dev
```
Expected: 所有提交成功推送到远程 dev 分支
**Step 2: 验证远程提交**
访问 Git 仓库,确认所有提交已成功推送:
- `refactor: 移除页面标题的卡片式样式`
- `feat: 添加独立搜索按钮,移除输入框内搜索图标`
- `docs: 添加项目列表页面UI优化测试报告`
Expected: 所有提交都存在于远程 dev 分支
---
## 实施完成
所有任务完成后,项目列表页面 UI 优化实施完毕。
**关键变更:**
1. 页面标题扁平化,移除卡片式装饰
2. 搜索区域添加独立搜索按钮
3. 保留输入框回车和清空触发搜索功能
4. 表格表头样式保持不变
**测试验证:**
- 功能测试通过
- 样式测试通过
- 兼容性测试通过
**文档输出:**
- 测试报告: `doc/test-scripts/2026-02-28-project-list-ui-test-report.md`