23 Commits

Author SHA1 Message Date
wkc
a5a3e36d48 refactor(ccdiProject): 将下拉菜单项提升为顶层菜单项实现扁平化导航 2026-03-04 11:03:09 +08:00
wkc
9ffcb22929 fix(ccdiProject): 统一菜单项高度为 40px 实现垂直对齐 2026-03-04 10:57:02 +08:00
wkc
5ac8d0bb99 fix(ccdiProject): 修复导航菜单垂直居中对齐问题 2026-03-04 10:52:34 +08:00
wkc
5e85533062 style(ccdiProject): 添加导航菜单响应式布局支持 2026-03-04 10:41:14 +08:00
wkc
4678f2cd44 style(ccdiProject): 添加导航菜单简洁链接风格样式 2026-03-04 10:40:30 +08:00
wkc
9f2a2b7c17 feat(ccdiProject): 添加菜单选择处理方法并清理废弃代码 2026-03-04 10:39:27 +08:00
wkc
6d322ea7da feat(ccdiProject): 替换按钮组为导航菜单并使用动态组件 2026-03-04 10:38:18 +08:00
wkc
38adbaed90 feat(ccdiProject): 导入子组件并添加菜单状态数据 2026-03-04 10:36:58 +08:00
wkc
b0f5422593 feat(ccdiProject): 添加流水明细查询占位组件 2026-03-04 10:35:23 +08:00
wkc
bf68f5e7ee feat(ccdiProject): 添加专项排查占位组件 2026-03-04 10:32:48 +08:00
wkc
bd2d7b80dc feat(ccdiProject): 添加结果总览占位组件 2026-03-04 10:31:59 +08:00
wkc
1feb295a93 feat(ccdiProject): 添加参数配置占位组件 2026-03-04 10:31:15 +08:00
wkc
c7b140c5db chore: 清理 Python 缓存文件 2026-03-04 10:28:20 +08:00
wkc
6e30a0ccf4 docs: 添加项目详情页面导航菜单改造实施计划 2026-03-04 10:22:53 +08:00
wkc
33994531b0 docs: 添加项目详情页面导航菜单改造设计文档 2026-03-04 10:19:25 +08:00
wkc
e43d2ac0f6 feat: CcdiProjectVO添加lsfxProjectId字段 2026-03-04 09:55:38 +08:00
wkc
4a2d993a91 feat: CcdiProject实体类添加lsfxProjectId字段 2026-03-04 09:55:10 +08:00
wkc
301fa6c85c 文件上传 2026-03-04 09:47:42 +08:00
wkc
3f71217dfc docs: 添加创建项目集成流水分析平台实施计划 2026-03-04 09:30:51 +08:00
wkc
5571e85363 docs: 添加创建项目集成流水分析平台设计文档 2026-03-04 09:28:05 +08:00
mengke
812defdfc6 Merge branch 'dev-lgw' into dev 2026-03-04 09:24:51 +08:00
mengke
990fb8ec4f Merge branch 'dev' into dev-lgw 2026-03-02 19:20:51 +08:00
mengke
c6d5063c8d feat: 完成上传数据页面 2026-03-02 19:18:45 +08:00
31 changed files with 4729 additions and 66 deletions

View File

@@ -115,7 +115,5 @@
"mcp__chrome-devtools-mcp__take_screenshot" "mcp__chrome-devtools-mcp__take_screenshot"
] ]
}, },
"enabledMcpjsonServers": [ "enabledMcpjsonServers": ["mysql"]
"mysql"
]
} }

163
CLAUDE.md
View File

@@ -34,7 +34,7 @@ POST http://localhost:8080/login/test?username=admin&password=admin123
| 后端技术 | 版本 | 前端技术 | 版本 | | 后端技术 | 版本 | 前端技术 | 版本 |
|-----------------------------|--------|------------|---------| |-----------------------------|--------|------------|---------|
| Spring Boot | 3.5.8 | Vue.js | 2.6.12 | | Spring Boot | 3.5.8 | Vue.js | 2.6.12 |
| Java | 17 | Element UI | 2.15.14 | | Java | 21 | Element UI | 2.15.14 |
| MyBatis Spring Boot Starter | 3.0.5 | Vuex | 3.6.0 | | MyBatis Spring Boot Starter | 3.0.5 | Vuex | 3.6.0 |
| MySQL Connector | 8.2.0 | Vue Router | 3.4.9 | | MySQL Connector | 8.2.0 | Vue Router | 3.4.9 |
| SpringDoc OpenAPI | 2.8.14 | Axios | 0.28.1 | | SpringDoc OpenAPI | 2.8.14 | Axios | 0.28.1 |
@@ -114,7 +114,10 @@ ccdi/
├── ruoyi-common/ # 通用工具 (annotations, utils, constants) ├── ruoyi-common/ # 通用工具 (annotations, utils, constants)
├── ruoyi-quartz/ # 定时任务 ├── ruoyi-quartz/ # 定时任务
├── ruoyi-generator/ # 代码生成器 ├── ruoyi-generator/ # 代码生成器
├── ruoyi-info-collection/ # 【核心业务模块】信息采集 ├── ccdi-info-collection/ # 【核心业务模块】信息采集
├── ccdi-project/ # 【核心业务模块】项目管理
├── ccdi-lsfx/ # 【核心业务模块】流水分析对接
├── lsfx-mock-server/ # 流水分析模拟服务器 (Python)
├── ruoyi-ui/ # 前端 Vue 应用 ├── ruoyi-ui/ # 前端 Vue 应用
├── sql/ # 数据库脚本 ├── sql/ # 数据库脚本
├── bin/ # 启动脚本 ├── bin/ # 启动脚本
@@ -130,7 +133,11 @@ ruoyi-admin (启动模块)
├── ruoyi-common (共享工具) ├── ruoyi-common (共享工具)
├── ruoyi-quartz (定时任务) ├── ruoyi-quartz (定时任务)
├── ruoyi-generator (代码生成) ├── ruoyi-generator (代码生成)
── ruoyi-info-collection (信息采集模块) ── ccdi-info-collection (信息采集模块)
│ └── 依赖 ruoyi-common
├── ccdi-project (项目管理模块)
│ └── 依赖 ruoyi-common
└── ccdi-lsfx (流水分析对接模块)
└── 依赖 ruoyi-common └── 依赖 ruoyi-common
``` ```
@@ -140,7 +147,7 @@ ruoyi-admin (启动模块)
3.`ruoyi-admin/pom.xml` 中添加对新模块的依赖 3.`ruoyi-admin/pom.xml` 中添加对新模块的依赖
4. 在新模块中按照分层规范创建 controller/service/mapper/domain 包 4. 在新模块中按照分层规范创建 controller/service/mapper/domain 包
### ruoyi-info-collection 业务模块 (核心) ### ccdi-info-collection 业务模块 (核心)
自定义业务模块,包含以下核心功能: 自定义业务模块,包含以下核心功能:
@@ -158,14 +165,87 @@ ruoyi-admin (启动模块)
**分层结构:** **分层结构:**
- Controller: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` - Controller: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
- Service: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/` - Service: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/`
- Mapper: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/` - Mapper: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/`
- Domain: `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/` - Domain: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/`
- dto/: 数据传输对象 - dto/: 数据传输对象
- vo/: 视图对象 - vo/: 视图对象
- excel/: Excel导入导出实体 - excel/: Excel导入导出实体
- XML映射: `ruoyi-info-collection/src/main/resources/mapper/info/collection/` - XML映射: `ccdi-info-collection/src/main/resources/mapper/info/collection/`
### ccdi-project 业务模块 (核心)
项目管理模块,用于管理纪检初核项目的全生命周期:
**核心功能:**
- 项目创建、更新、删除、查询
- 项目状态管理 (进行中、已完成、已归档)
- 项目统计(按状态统计数量)
- 模型参数配置管理
**主要 Controller:**
- CcdiProjectController: 项目管理
- CcdiModelParamController: 模型参数配置
**分层结构:**
- Controller: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/`
- Service: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/`
- Mapper: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/`
- Domain: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/`
- XML映射: `ccdi-project/src/main/resources/mapper/ccdi/project/`
### ccdi-lsfx 业务模块 (核心)
流水分析平台对接模块,用于与外部流水分析系统交互:
**核心功能:**
- 获取访问令牌 (Token)
- 上传流水文件并解析
- 拉取行内流水数据
- 查询解析状态和结果
- 获取银行流水明细
**主要组件:**
- LsfxAnalysisClient: 流水分析平台客户端
- LsfxTestController: 测试接口
**配置项 (application-dev.yml):**
```yaml
lsfx:
api:
base-url: http://localhost:8000 # 流水分析平台地址
app-id: your-app-id
app-secret: your-app-secret
client-id: your-client-id
endpoints:
get-token: /api/auth/token
upload-file: /api/files/upload
fetch-inner-flow: /api/flow/inner
```
**分层结构:**
- Client: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/`
- Controller: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/`
- Domain: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/`
- request/: 请求对象
- response/: 响应对象
- Config: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/`
### lsfx-mock-server (开发测试工具)
Python 实现的流水分析平台模拟服务器,用于本地开发和测试:
**用途:**
- 模拟流水分析平台的 API 接口
- 提供测试数据和模拟响应
- 支持错误场景模拟
**启动方式:**
```bash
cd lsfx-mock-server
python app.py # 默认监听 http://localhost:8000
```
--- ---
@@ -389,6 +469,55 @@ POST /login/test?username=admin&password=admin123
- **数据库索引**: 0 - **数据库索引**: 0
- **连接超时**: 10s - **连接超时**: 10s
### 流水分析平台配置
项目集成了外部流水分析平台,配置项位于 `application-dev.yml`:
```yaml
lsfx:
api:
base-url: http://localhost:8000 # 流水分析平台基础地址
app-id: ccdi-app # 应用ID
app-secret: ccdi-secret-2024 # 应用密钥
client-id: ccdi-client # 客户端ID
endpoints:
get-token: /api/auth/token # 获取令牌接口
upload-file: /api/files/upload # 文件上传接口
fetch-inner-flow: /api/flow/inner # 拉取行内流水接口
```
**开发环境使用 Mock 服务器:**
- 本地开发时,将 `base-url` 设置为 `http://localhost:8000`
- 启动 `lsfx-mock-server` 提供模拟接口
- 生产环境替换为真实的流水分析平台地址
### MCP 配置
项目使用 MCP (Model Context Protocol) 连接数据库,配置文件: `.mcp.json`
```json
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "@fhuang/mcp-mysql-server"],
"env": {
"MYSQL_HOST": "116.62.17.81",
"MYSQL_PORT": "3306",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "Kfcx@1234",
"MYSQL_DATABASE": "ccdi"
}
}
}
}
```
**使用场景:**
- 通过 MCP 工具直接查询和操作数据库
- 在开发过程中快速验证数据
- 生成测试数据和调试 SQL
### Druid 监控台 ### Druid 监控台
访问地址: `http://localhost:8080/druid/` 访问地址: `http://localhost:8080/druid/`
@@ -405,8 +534,11 @@ POST /login/test?username=admin&password=admin123
|---------------|--------------------------------------------------------------------------------| |---------------|--------------------------------------------------------------------------------|
| 应用入口 | `ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java` | | 应用入口 | `ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java` |
| 安全配置 | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` | | 安全配置 | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` |
| 业务 Controller | `ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` | | 信息采集 Controller | `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` |
| 业务 Mapper XML | `ruoyi-info-collection/src/main/resources/mapper/info/collection/` | | 信息采集 Mapper XML | `ccdi-info-collection/src/main/resources/mapper/info/collection/` |
| 项目管理 Controller | `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/` |
| 项目管理 Mapper XML | `ccdi-project/src/main/resources/mapper/ccdi/project/` |
| 流水分析 Client | `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java` |
| Vue 路由 | `ruoyi-ui/src/router/index.js` | | Vue 路由 | `ruoyi-ui/src/router/index.js` |
| Vuex Store | `ruoyi-ui/src/store/` | | Vuex Store | `ruoyi-ui/src/store/` |
| 前端 API | `ruoyi-ui/src/api/` | | 前端 API | `ruoyi-ui/src/api/` |
@@ -499,6 +631,15 @@ doc/
3. 确认 Excel 模板格式正确 3. 确认 Excel 模板格式正确
4. 检查必填字段是否为空 4. 检查必填字段是否为空
### 流水分析平台连接失败
**检查项:**
1. 确认 `lsfx-mock-server` 已启动(开发环境)
2. 检查 `application-dev.yml` 中的 `lsfx.api.base-url` 配置
3. 验证 app-id、app-secret、client-id 是否正确
4. 检查网络连接和防火墙设置
5. 查看后端日志中的 HTTP 请求错误信息
--- ---
## MyBatis Plus 分页使用 ## MyBatis Plus 分页使用

Binary file not shown.

View File

@@ -0,0 +1,92 @@
# 兰溪存储的流水表的表结构
```sql
CREATE TABLE `ccdi_bank_statement` (
`bank_statement_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`LE_ID` int(10) unsigned DEFAULT '0' COMMENT '企业ID',
`ACCOUNT_ID` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账号ID',
`LE_ACCOUNT_NAME` varchar(240) DEFAULT 'NONE' COMMENT '企业账号名称',
`LE_ACCOUNT_NO` varchar(240) DEFAULT NULL COMMENT '企业银行账号',
`ACCOUNTING_DATE_ID` int(11) DEFAULT NULL COMMENT '账号日期ID',
`ACCOUNTING_DATE` varchar(10) DEFAULT '0000-00-00' COMMENT '账号日期',
`TRX_DATE` varchar(20) NOT NULL COMMENT '交易日期',
`CURRENCY` varchar(10) DEFAULT NULL COMMENT '币种',
`AMOUNT_DR` decimal(19,2) NOT NULL DEFAULT '0.00' COMMENT '付款金额',
`AMOUNT_CR` decimal(19,2) NOT NULL DEFAULT '0.00' COMMENT '收款金额',
`AMOUNT_BALANCE` decimal(19,2) NOT NULL COMMENT '余额',
`CASH_TYPE` varchar(500) DEFAULT NULL COMMENT '交易类型',
`CUSTOMER_LE_ID` int(11) DEFAULT '-1' COMMENT '对手方企业ID',
`CUSTOMER_ACCOUNT_NAME` varchar(240) DEFAULT NULL COMMENT '对手方企业名称',
`CUSTOMER_ACCOUNT_NO` varchar(240) DEFAULT NULL COMMENT '对手方账号',
`customer_bank` varchar(300) DEFAULT NULL COMMENT '对手方银行',
`customer_reference` varchar(500) DEFAULT NULL COMMENT '对手方备注',
`USER_MEMO` varchar(1000) DEFAULT NULL COMMENT '用户交易摘要',
`BANK_COMMENTS` varchar(240) DEFAULT NULL COMMENT '银行交易摘要',
`BANK_TRX_NUMBER` varchar(240) DEFAULT NULL COMMENT '银行交易号',
`BANK` varchar(250) NOT NULL DEFAULT '' COMMENT '所属银行缩写',
`TRX_FLAG` varchar(2) DEFAULT '0' COMMENT '交易标志位',
`TRX_TYPE` int(11) NOT NULL DEFAULT '0' COMMENT '分类ID',
`EXCEPTION_TYPE` varchar(50) NOT NULL DEFAULT '' COMMENT '异常类型',
`internal_flag` tinyint(1) DEFAULT '0' COMMENT '"是否为内部交易1 是 0 否"',
`batch_id` int(11) NOT NULL DEFAULT '0' COMMENT '上传logId对应upload_log',
`batch_sequence` int(11) NOT NULL COMMENT '每次上传在文件中的line',
`CREATE_DATE` datetime DEFAULT NULL COMMENT '创建时间内',
`created_by` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '创建者',
`meta_json` text COMMENT '"meta json"',
`no_balance` tinyint(1) DEFAULT '0' COMMENT '是否包含余额',
`begin_balance` tinyint(1) DEFAULT '0' COMMENT '初始余额',
`end_balance` tinyint(1) DEFAULT '0' COMMENT '结束余额',
`group_id` int(11) DEFAULT '0' COMMENT '项目id',
`override_bs_id` bigint(20) DEFAULT '0' COMMENT '=0表示该数据未覆盖主表>0表示覆盖主表<0表示被主表覆盖',
`payment_method` varchar(500) DEFAULT NULL COMMENT '微信、支付宝流水字段,交易方式',
`cret_no` varchar(20) COMMENT '身份证号',
PRIMARY KEY (`bank_statement_id`),
KEY `idx_batch_id_account` (`batch_id`,`LE_ACCOUNT_NO`,`ACCOUNTING_DATE_ID`),
KEY `GROUP_ID` (`group_id`),
KEY `c4c_bank_statement_stg_batch_id_IDX` (`batch_id`,`LE_ACCOUNT_NO`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='银行流水的中间处理表';
```
流水表和返回值的对应关系
| 序号 | ccdi_bank_statement | 返回值 |
| --- | --- | --- |
| 1 | bank_statement_id | bankStatementId |
| 2 | LE_ID | leId |
| 3 | ACCOUNT_ID | accountId |
| 4 | LE_ACCOUNT_NAME | leName |
| 5 | LE_ACCOUNT_NO | accountNo |
| 6 | ACCOUNTING_DATE_ID | accountingDateId |
| 7 | ACCOUNTING_DATE | accountingDate |
| 8 | TRX_DATE | trxDate |
| 9 | CURRENCY | currency |
| 10 | AMOUNT_DR | drAmount |
| 11 | AMOUNT_CR | crAmount |
| 12 | AMOUNT_BALANCE | balanceAmount |
| 13 | CASH_TYPE | cashType |
| 14 | CUSTOMER_LE_ID | customerId |
| 15 | CUSTOMER_ACCOUNT_NAME | customerName |
| 16 | CUSTOMER_ACCOUNT_NO | customerAccountNo |
| 17 | customer_bank | customerBank |
| 18 | customer_reference | customerReference |
| 19 | USER_MEMO | userMemo |
| 20 | BANK_COMMENTS | bankComments |
| 21 | BANK_TRX_NUMBER | bankTrxNumber |
| 22 | BANK | bank |
| 23 | TRX_FLAG | transFlag |
| 24 | TRX_TYPE | transTypeId |
| 25 | EXCEPTION_TYPE | exceptionType |
| 26 | internal_flag | internalFlag |
| 27 | batch_id | batchId |
| 28 | batch_sequence | uploadSequnceNumber |
| 29 | CREATE_DATE | createDate |
| 30 | created_by | createdBy |
| 31 | meta_json | 设置为null |
| 32 | no_balance | isNoBalance |
| 33 | begin_balance | isBeginBalance |
| 34 | end_balance | isEndBalance |
| 35 | override_bs_id | overrideBsId |
| 36 | payment_method | paymentMethod |
| 37 | cret_no | cretNo |
| 38 | group_id | groupId |

View File

@@ -82,8 +82,7 @@ public class LsfxAnalysisClient {
long elapsed = System.currentTimeMillis() - startTime; long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) { if (response != null && response.getData() != null) {
log.info("【流水分析】获取Token成功: projectId={}, 耗时={}ms", log.info("【流水分析】获取Token成功: projectId={}, 耗时={}ms", response.getData().getProjectId(), elapsed);
response.getData().getProjectId(), elapsed);
} else { } else {
log.warn("【流水分析】获取Token响应异常: 耗时={}ms", elapsed); log.warn("【流水分析】获取Token响应异常: 耗时={}ms", elapsed);
} }

View File

@@ -50,6 +50,9 @@ public class CcdiProject implements Serializable {
/** 低风险人数 */ /** 低风险人数 */
private Integer lowRiskCount; private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 删除标志0-存在2-删除 */ /** 删除标志0-存在2-删除 */
@TableLogic @TableLogic
private String delFlag; private String delFlag;

View File

@@ -41,6 +41,9 @@ public class CcdiProjectVO {
/** 低风险人数 */ /** 低风险人数 */
private Integer lowRiskCount; private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 创建时间 */ /** 创建时间 */
private Date createTime; private Date createTime;

View File

@@ -0,0 +1,30 @@
-- ====================================
-- 功能ccdi_project 表新增流水分析平台项目ID字段
-- 日期2026-03-04
-- 作者Claude Code
-- 说明为支持创建项目时集成流水分析平台需要新增字段存储流水分析平台返回的projectId
-- ====================================
USE ccdi;
-- 新增字段
ALTER TABLE `ccdi_project`
ADD COLUMN `lsfx_project_id` INT(11) DEFAULT NULL COMMENT '流水分析平台项目ID'
AFTER `low_risk_count`;
-- 验证字段是否添加成功
SELECT
COLUMN_NAME,
COLUMN_TYPE,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_COMMENT
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = 'ccdi'
AND TABLE_NAME = 'ccdi_project'
AND COLUMN_NAME = 'lsfx_project_id';
-- 提示信息
SELECT '字段 lsfx_project_id 添加成功!' AS message;

View File

@@ -0,0 +1,552 @@
# 创建项目时集成流水分析平台设计方案
**文档版本**: 1.0
**创建日期**: 2026-03-04
**作者**: Claude Code
**状态**: 待实施
---
## 1. 需求概述
### 1.1 背景
在纪检初核系统中,创建项目时需要同步在流水分析平台创建对应的项目,以便后续进行流水分析操作。
### 1.2 目标
- 创建项目时自动调用流水分析平台的 `getToken` 接口
- 获取返回的 `projectId` 并保存到项目表
- 确保数据一致性,调用失败时项目创建也失败
### 1.3 参考文档
- 《兰溪-流水分析对接-新版.md》
- 项目现有代码:`ccdi-project` 模块、`ccdi-lsfx` 模块
---
## 2. 设计方案
### 2.1 总体架构
#### 业务流程
```
用户创建项目
Controller接收请求
Service层处理
├─→ 生成projectNo (902000_时间戳)
├─→ 调用流水分析平台getToken接口
│ ├─→ 成功获取projectId
│ └─→ 失败:抛出异常,事务回滚
├─→ 保存项目包含projectId
└─→ 返回项目信息
```
#### 技术方案
- **调用方式**: 同步调用
- **集成位置**: Service层`CcdiProjectServiceImpl`
- **事务管理**: Spring声明式事务失败自动回滚
- **异常处理**: 依赖 `LsfxAnalysisClient` 的异常处理
### 2.2 核心设计决策
| 决策点 | 选择 | 理由 |
|-------|------|------|
| 调用方式 | 同步调用 | 数据一致性强,用户体验清晰 |
| 保存策略 | 仅保存projectId | token 使用一次后失效,无需保存 |
| 集成位置 | Service层 | 业务逻辑集中,事务管理简单 |
| 异常处理 | 依赖客户端 | 避免重复日志Service层只做业务校验 |
---
## 3. 详细设计
### 3.1 数据库设计
#### ccdi_project 表新增字段
```sql
ALTER TABLE `ccdi_project`
ADD COLUMN `lsfx_project_id` INT(11) DEFAULT NULL COMMENT '流水分析平台项目ID'
AFTER `low_risk_count`;
```
**字段说明:**
- 字段名: `lsfx_project_id`
- 类型: `INT(11)`(与流水分析平台保持一致)
- 允许为空: 是(理论上不会为空,因为失败会回滚)
- 索引: 不设置唯一索引(流水分析平台的 projectId 可能重复)
### 3.2 参数映射规则
根据《兰溪-流水分析对接-新版.md》文档GetTokenRequest 参数配置如下:
#### 必填参数(固定值)
| 参数名 | 值 | 说明 |
|-------|-----|------|
| `userId` | `"902001"` | 操作人员编号(固定值) |
| `userName` | `"902001"` | 操作人员姓名(固定值) |
| `role` | `"VIEWER"` | 人员角色(固定值) |
| `orgCode` | `"902000"` | 行社机构号(固定值) |
| `analysisType` | `"-1"` | 分析类型(固定值) |
| `departmentCode` | `"902000"` | 客户经理所属营业部机构编码(固定值) |
#### 必填参数(动态生成)
| 参数名 | 生成规则 | 说明 |
|-------|---------|------|
| `projectNo` | `"902000_" + System.currentTimeMillis()` | 项目编号 |
| `entityName` | 使用前端传入的 `projectName` | 项目名称 |
#### 自动生成参数
| 参数名 | 生成位置 | 说明 |
|-------|---------|------|
| `appId` | `LsfxAnalysisClient` 配置 | 固定值:`remote_app` |
| `appSecretCode` | `LsfxAnalysisClient.getToken()` | MD5(projectNo + "_" + entityName + "_" + appSecret) |
#### 可选参数
以下参数不传递(暂不需要):
- `entityId` (企业统信码或个人身份证号)
- `xdRelatedPersons` (信贷关联人信息)
- `jzDataDateId` (金综链流水日期ID)
- `innerBSStartDateId` (行内流水开始日期)
- `innerBSEndDateId` (行内流水结束日期)
### 3.3 代码实现
#### 3.3.1 实体类修改
**CcdiProject.java**
```java
@Data
@TableName("ccdi_project")
public class CcdiProject implements Serializable {
// ... 现有字段 ...
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId; // 新增字段
/** 删除标志 */
@TableLogic
private String delFlag;
// ... 审计字段 ...
}
```
**CcdiProjectVO.java**
```java
@Data
public class CcdiProjectVO implements Serializable {
// ... 现有字段 ...
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId; // 新增字段
/** 创建时间 */
private Date createTime;
// ... 其他字段 ...
}
```
#### 3.3.2 Service实现
**CcdiProjectServiceImpl.java**
```java
@Service
public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private CcdiProjectMapper projectMapper;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient; // 新增依赖注入
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// 1. 调用流水分析平台获取projectId
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
// 2. 创建项目实体
CcdiProject project = new CcdiProject();
BeanUtils.copyProperties(dto, project);
// 3. 设置默认值和流水分析平台ID
project.setStatus("0"); // 进行中
project.setIsArchived(0); // 未归档
project.setTargetCount(0);
project.setHighRiskCount(0);
project.setMediumRiskCount(0);
project.setLowRiskCount(0);
project.setLsfxProjectId(lsfxProjectId); // 设置流水分析平台ID
// 4. 保存到数据库
projectMapper.insert(project);
// 5. 返回VO
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
/**
* 调用流水分析平台获取projectId
*
* @param projectName 项目名称
* @return 流水分析平台项目ID
* @throws ServiceException 调用失败或响应无效时抛出
*/
private Integer callLsfxPlatform(String projectName) {
// 构建请求参数
GetTokenRequest request = new GetTokenRequest();
request.setProjectNo("902000_" + System.currentTimeMillis());
request.setEntityName(projectName);
request.setUserId("902001");
request.setUserName("902001");
request.setRole("VIEWER");
request.setOrgCode("902000");
request.setAnalysisType("-1");
request.setDepartmentCode("902000");
// 调用流水分析平台(异常处理和日志已在 LsfxAnalysisClient 中完成)
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
// 业务层校验:确保响应有效
if (response == null || response.getData() == null) {
throw new ServiceException("流水分析平台响应数据为空");
}
if (response.getData().getProjectId() == null) {
throw new ServiceException("流水分析平台返回的projectId为空");
}
// 校验返回码
if (!"200".equals(response.getCode())) {
throw new ServiceException("流水分析平台返回错误: " + response.getMessage());
}
return response.getData().getProjectId();
}
}
```
### 3.4 异常处理与事务管理
#### 3.4.1 异常处理策略
**场景1流水分析平台调用失败**
- `LsfxAnalysisClient` 抛出 `LsfxApiException`
- Service 层捕获后转换为 `ServiceException`
- Spring 事务自动回滚
- 用户收到明确错误提示
**场景2网络超时**
- `LsfxAnalysisClient` 配置了超时时间连接30秒读取60秒
- 超时后抛出异常,事务回滚
**场景3响应数据无效**
- Service 层进行业务校验
- 发现无效响应抛出 `ServiceException`
- 事务回滚
#### 3.4.2 事务管理
```java
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// 任何一步失败,整个事务自动回滚
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
// ... 数据库操作 ...
projectMapper.insert(project);
return vo;
}
```
**特性:**
- 声明式事务管理
- 任何异常都触发回滚
- 保证数据一致性
#### 3.4.3 日志记录
日志记录已在 `LsfxAnalysisClient.getToken()` 中实现:
```java
// LsfxAnalysisClient 中的日志
log.info("【流水分析】获取Token请求: projectNo={}, entityName={}", ...);
log.info("【流水分析】获取Token成功: projectId={}, 耗时={}ms", ...);
log.error("【流水分析】获取Token失败: projectNo={}, error={}", ...);
```
Service 层无需重复记录日志。
---
## 4. 实施步骤
### 4.1 实施顺序
**步骤1数据库变更**
```bash
# 执行SQL脚本
mysql -u root -p ccdi < doc/design/2026-03-04-add-lsfx-project-id.sql
```
**步骤2修改实体类和VO**
- `CcdiProject` 实体类添加 `lsfxProjectId` 字段
- `CcdiProjectVO` 视图对象添加 `lsfxProjectId` 字段
**步骤3修改 Service 实现**
- `CcdiProjectServiceImpl` 注入 `LsfxAnalysisClient`
- 添加 `callLsfxPlatform()` 私有方法
- 修改 `createProject()` 方法
**步骤4单元测试**
- 测试正常创建项目流程
- 测试流水分析平台调用失败场景
- 测试网络超时场景
**步骤5集成测试**
- 使用 Swagger 测试完整流程
- 验证数据库中 `lsfx_project_id` 是否正确保存
- 验证流水分析平台是否成功创建项目
**步骤6前端适配可选**
- 如果前端需要展示 `lsfxProjectId`,修改前端页面
### 4.2 数据库迁移脚本
**文件路径**: `doc/design/2026-03-04-add-lsfx-project-id.sql`
```sql
-- ====================================
-- 功能ccdi_project 表新增流水分析平台项目ID字段
-- 日期2026-03-04
-- 作者Claude Code
-- ====================================
USE ccdi;
-- 新增字段
ALTER TABLE `ccdi_project`
ADD COLUMN `lsfx_project_id` INT(11) DEFAULT NULL COMMENT '流水分析平台项目ID'
AFTER `low_risk_count`;
-- 验证
SELECT * FROM `ccdi_project` LIMIT 1;
```
---
## 5. 测试方案
### 5.1 单元测试
**测试类**: `CcdiProjectServiceImplTest.java`
```java
@SpringBootTest
public class CcdiProjectServiceImplTest {
@Resource
private ICcdiProjectService projectService;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient;
@Test
public void testCreateProject_Success() {
// 准备数据
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName("测试项目");
dto.setDescription("测试描述");
dto.setConfigType("default");
// 执行
CcdiProjectVO result = projectService.createProject(dto);
// 验证
assertNotNull(result);
assertNotNull(result.getProjectId());
assertNotNull(result.getLsfxProjectId());
assertEquals("测试项目", result.getProjectName());
}
@Test(expected = ServiceException.class)
public void testCreateProject_LsfxFailed() {
// 模拟流水分析平台失败
// 需要使用 Mock 或关闭 Mock Server
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName("测试项目");
projectService.createProject(dto);
}
}
```
### 5.2 集成测试
**测试步骤:**
1. **启动 Mock Server**
```bash
cd lsfx-mock-server
python app.py
```
2. **访问 Swagger**
```
http://localhost:8080/swagger-ui/index.html
```
3. **测试创建项目接口**
- 接口: `POST /ccdi/project`
- 请求体:
```json
{
"projectName": "测试项目001",
"description": "测试项目描述",
"configType": "default"
}
```
4. **验证响应**
```json
{
"code": 200,
"msg": "项目创建成功",
"data": {
"projectId": 1,
"projectName": "测试项目001",
"lsfxProjectId": 77, // 应该有值
...
}
}
```
5. **验证数据库**
```sql
SELECT project_id, project_name, lsfx_project_id
FROM ccdi_project
WHERE project_id = 1;
```
### 5.3 异常测试
**测试场景:**
1. **流水分析平台不可用**
- 关闭 Mock Server
- 创建项目应该失败
- 数据库不应该有新记录
2. **网络超时**
- 修改 Mock Server 延迟超过60秒
- 创建项目应该超时失败
3. **返回错误码**
- 修改 Mock Server 返回错误码如40104
- 创建项目应该失败并显示对应错误信息
---
## 6. 风险与注意事项
### 6.1 风险分析
| 风险 | 影响 | 缓解措施 |
|-----|------|---------|
| 流水分析平台不可用 | 无法创建项目 | 提供明确的错误提示,用户可稍后重试 |
| 网络延迟 | 创建项目缓慢 | 已配置合理超时时间60秒 |
| projectId 重复 | 无影响 | 不设置唯一索引 |
| 事务回滚失败 | 数据不一致 | 依赖 Spring 事务管理,经过充分验证 |
### 6.2 注意事项
1. **依赖外部服务**
- 流水分析平台必须可用
- 建议在生产环境做好监控和告警
2. **无重试机制**
- 失败需要用户手动重试
- 可以考虑在 UI 层提供重试按钮
3. **项目编号唯一性**
- 使用时间戳保证唯一性
- 理论上不会重复(毫秒级时间戳)
4. **前端适配**
- 当前设计不要求前端传 `lsfxProjectId`
- 如果需要展示,修改前端页面即可
---
## 7. 变更清单
| 类型 | 文件 | 变更内容 | 状态 |
|-----|------|---------|------|
| 数据库 | `ccdi_project` 表 | 新增 `lsfx_project_id` 字段 | 待执行 |
| SQL | `2026-03-04-add-lsfx-project-id.sql` | 数据库迁移脚本 | 待创建 |
| 实体类 | `CcdiProject.java` | 新增 `lsfxProjectId` 属性 | 待修改 |
| VO | `CcdiProjectVO.java` | 新增 `lsfxProjectId` 属性 | 待修改 |
| Service | `CcdiProjectServiceImpl.java` | 注入 `LsfxAnalysisClient`,添加调用逻辑 | 待修改 |
---
## 8. 后续优化建议
### 8.1 短期优化
1. **添加重试机制**
- 在 Service 层添加自动重试最多3次
- 使用 Spring Retry 框架
2. **异步调用(可选)**
- 如果创建速度成为瓶颈,可改为异步调用
- 需要增加状态管理和轮询接口
### 8.2 长期优化
1. **批量创建支持**
- 如果需要批量创建项目,考虑批量调用流水分析平台
2. **缓存机制**
- 如果 token 需要重复使用,考虑缓存机制
- 需要处理 token 过期问题
---
## 9. 参考资料
- 《兰溪-流水分析对接-新版.md》
- 项目 CLAUDE.md 文档
- Spring Boot 事务管理文档
- MyBatis Plus 使用指南
---
**文档结束**

View File

@@ -0,0 +1,786 @@
# 创建项目集成流水分析平台实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在创建项目时同步调用流水分析平台获取projectId并保存到数据库
**Architecture:** Service层集成LsfxAnalysisClient同步调用getToken接口使用Spring事务管理确保数据一致性
**Tech Stack:** Spring Boot 3.5.8, MyBatis Plus 3.0.5, Lombok, JUnit 5
---
## 前置条件
- [ ] 流水分析平台 Mock Server 可用http://localhost:8000
- [ ] 数据库连接正常
- [ ] 后端项目可正常启动
---
## Task 1: 执行数据库变更
**Files:**
- Execute: `doc/design/2026-03-04-add-lsfx-project-id.sql`
**Step 1: 连接数据库执行SQL脚本**
使用MCP工具连接数据库并执行SQL
```bash
# 或者使用MySQL客户端
mysql -h 116.62.17.81 -u root -p ccdi < doc/design/2026-03-04-add-lsfx-project-id.sql
```
**Step 2: 验证字段已添加**
```sql
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'ccdi'
AND TABLE_NAME = 'ccdi_project'
AND COLUMN_NAME = 'lsfx_project_id';
```
预期结果:应返回字段信息,类型为 `INT(11)`,允许为空
**Step 3: 提交数据库变更**
```bash
git add doc/design/2026-03-04-add-lsfx-project-id.sql
git commit -m "chore: 添加流水分析平台项目ID字段到ccdi_project表"
```
---
## Task 2: 修改CcdiProject实体类
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java`
**Step 1: 打开实体类文件**
定位到 `lowRiskCount` 字段的位置约第50行
**Step 2: 添加新字段**
`lowRiskCount` 字段之后,`delFlag` 字段之前添加:
```java
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 删除标志0-存在2-删除 */
@TableLogic
private String delFlag;
```
**Step 3: 验证代码**
确保:
- Lombok 的 `@Data` 注解存在(会自动生成 getter/setter
- 字段顺序与数据库表结构一致
- 注释清晰
**Step 4: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java
git commit -m "feat: CcdiProject实体类添加lsfxProjectId字段"
```
---
## Task 3: 修改CcdiProjectVO视图对象
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectVO.java`
**Step 1: 打开VO文件**
定位到 `lowRiskCount` 字段的位置
**Step 2: 添加新字段**
`lowRiskCount` 字段之后添加:
```java
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 创建时间 */
private Date createTime;
```
**Step 3: 验证代码**
确保字段顺序与实体类一致
**Step 4: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectVO.java
git commit -m "feat: CcdiProjectVO添加lsfxProjectId字段"
```
---
## Task 4: 准备测试环境
**Files:**
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 创建测试目录(如果不存在)**
```bash
mkdir -p ccdi-project/src/test/java/com/ruoyi/ccdi/project/service
```
**Step 2: 创建测试类**
```java
package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
/**
* 项目Service测试类
*/
@SpringBootTest
@Transactional
public class CcdiProjectServiceImplTest {
@Resource
private ICcdiProjectService projectService;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient;
@Test
public void testCreateProject_Success() {
// 准备数据
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName("测试项目");
dto.setDescription("测试描述");
dto.setConfigType("default");
// 执行
CcdiProjectVO result = projectService.createProject(dto);
// 验证
assertNotNull(result);
assertNotNull(result.getProjectId());
assertNotNull(result.getLsfxProjectId(), "流水分析平台项目ID不应为空");
assertEquals("测试项目", result.getProjectName());
}
}
```
**Step 3: 运行测试(预期失败)**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_Success
```
预期结果:测试失败,因为 `CcdiProjectServiceImpl` 尚未注入 `LsfxAnalysisClient`
**Step 4: 提交测试代码**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java
git commit -m "test: 添加项目创建集成流水分析平台的测试用例"
```
---
## Task 5: 修改CcdiProjectServiceImpl - 注入依赖
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 打开Service实现类**
找到依赖注入区域约第25行
**Step 2: 添加LsfxAnalysisClient依赖**
```java
@Service
public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private CcdiProjectMapper projectMapper;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient; // 新增依赖注入
// ... 方法实现 ...
}
```
**Step 3: 添加必要的import**
```java
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
```
**Step 4: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 5: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: CcdiProjectServiceImpl注入LsfxAnalysisClient依赖"
```
---
## Task 6: 实现callLsfxPlatform私有方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 在类末尾添加私有方法**
`getStatusCounts()` 方法之后添加:
```java
/**
* 调用流水分析平台获取projectId
*
* @param projectName 项目名称
* @return 流水分析平台项目ID
* @throws ServiceException 调用失败或响应无效时抛出
*/
private Integer callLsfxPlatform(String projectName) {
// 构建请求参数
GetTokenRequest request = new GetTokenRequest();
request.setProjectNo("902000_" + System.currentTimeMillis());
request.setEntityName(projectName);
request.setUserId("902001");
request.setUserName("902001");
request.setRole("VIEWER");
request.setOrgCode("902000");
request.setAnalysisType("-1");
request.setDepartmentCode("902000");
// 调用流水分析平台(异常处理和日志已在 LsfxAnalysisClient 中完成)
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
// 业务层校验:确保响应有效
if (response == null || response.getData() == null) {
throw new ServiceException("流水分析平台响应数据为空");
}
if (response.getData().getProjectId() == null) {
throw new ServiceException("流水分析平台返回的projectId为空");
}
// 校验返回码
if (!"200".equals(response.getCode())) {
throw new ServiceException("流水分析平台返回错误: " + response.getMessage());
}
return response.getData().getProjectId();
}
```
**Step 2: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 3: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: 实现callLsfxPlatform方法调用流水分析平台"
```
---
## Task 7: 修改createProject方法集成流水分析
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 定位createProject方法**
找到 `createProject` 方法约第29行
**Step 2: 修改方法实现**
将现有实现修改为:
```java
@Override
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// 1. 调用流水分析平台获取projectId
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
// 2. 创建项目实体
CcdiProject project = new CcdiProject();
BeanUtils.copyProperties(dto, project);
// 3. 设置默认值和流水分析平台ID
project.setStatus("0"); // 进行中
project.setIsArchived(0); // 未归档
project.setTargetCount(0);
project.setHighRiskCount(0);
project.setMediumRiskCount(0);
project.setLowRiskCount(0);
project.setLsfxProjectId(lsfxProjectId); // 设置流水分析平台ID
// 4. 保存到数据库
projectMapper.insert(project);
// 5. 返回VO
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
```
**Step 3: 确认事务注解存在**
确保方法上有 `@Transactional` 注解(如果没有则添加):
```java
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// ...
}
```
需要添加import
```java
import org.springframework.transaction.annotation.Transactional;
```
**Step 4: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 5: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: createProject方法集成流水分析平台调用"
```
---
## Task 8: 运行单元测试验证功能
**Files:**
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 确保Mock Server运行**
```bash
cd lsfx-mock-server
python app.py
```
确认输出:`Running on http://localhost:8000`
**Step 2: 运行测试**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_Success
```
预期结果:
```
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
```
**Step 3: 如果测试失败,检查日志**
常见问题:
- Mock Server 未启动:启动 Mock Server
- 数据库连接失败:检查数据库配置
- 字段映射错误:检查 BeanUtils.copyProperties 是否正常
**Step 4: 提交测试结果**
如果测试通过,无需额外提交(测试代码已提交)
---
## Task 9: 添加异常测试用例
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 添加测试方法**
在测试类中添加:
```java
@Test
public void testCreateProject_WithNullProjectName() {
// 准备数据 - 项目名称为null
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName(null);
dto.setDescription("测试描述");
dto.setConfigType("default");
// 执行并验证异常
assertThrows(ServiceException.class, () -> {
projectService.createProject(dto);
});
}
```
**Step 2: 运行新测试**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_WithNullProjectName
```
预期结果:测试可能失败(因为没有验证项目名称),这是正常的
**Step 3: 提交测试代码**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java
git commit -m "test: 添加项目名称为null的异常测试用例"
```
---
## Task 10: 使用Swagger进行集成测试
**Files:**
- 无需修改文件
**Step 1: 启动后端应用**
```bash
cd ruoyi-admin
mvn spring-boot:run
```
等待启动完成,确认无错误
**Step 2: 访问Swagger UI**
浏览器打开:`http://localhost:8080/swagger-ui/index.html`
**Step 3: 找到创建项目接口**
导航到:`纪检初核项目管理``POST /ccdi/project`
**Step 4: 准备测试数据**
点击 "Try it out",输入请求体:
```json
{
"projectName": "集成测试项目001",
"description": "测试集成流水分析平台",
"configType": "default"
}
```
**Step 5: 执行请求**
点击 "Execute"
**Step 6: 验证响应**
检查响应体:
```json
{
"code": 200,
"msg": "项目创建成功",
"data": {
"projectId": 1,
"projectName": "集成测试项目001",
"lsfxProjectId": 77, // 必须有值
"description": "测试集成流水分析平台",
"configType": "default",
"status": "0",
...
}
}
```
**Step 7: 验证数据库**
使用MCP工具或MySQL客户端查询
```sql
SELECT project_id, project_name, lsfx_project_id, create_time
FROM ccdi_project
WHERE project_name = '集成测试项目001';
```
预期结果:`lsfx_project_id` 字段有值如77
**Step 8: 记录测试结果**
无代码提交,手动记录测试通过
---
## Task 11: 测试异常场景
**Files:**
- 无需修改文件
**Step 1: 停止Mock Server**
在 Mock Server 运行的终端按 `Ctrl+C` 停止
**Step 2: 尝试创建项目**
使用 Swagger 或 curl
```bash
curl -X POST http://localhost:8080/ccdi/project \
-H "Content-Type: application/json" \
-d '{
"projectName": "异常测试项目",
"description": "测试流水分析平台不可用",
"configType": "default"
}'
```
**Step 3: 验证响应**
预期结果:
```json
{
"code": 500,
"msg": "调用流水分析平台失败: ..." // 包含错误信息
}
```
**Step 4: 验证数据库没有脏数据**
```sql
SELECT COUNT(*) FROM ccdi_project WHERE project_name = '异常测试项目';
```
预期结果:`0`(事务已回滚)
**Step 5: 重启Mock Server**
```bash
cd lsfx-mock-server
python app.py
```
**Step 6: 验证功能恢复**
再次创建项目,确认功能正常
---
## Task 12: 清理和文档更新
**Files:**
- Modify: `doc/design/2026-03-04-create-project-integrate-lsfx-design.md`
**Step 1: 更新设计文档状态**
修改设计文档开头:
```markdown
**状态**: 已实施 ✅
```
**Step 2: 更新变更清单**
将变更清单中的状态更新为"已完成"
```markdown
| 类型 | 文件 | 变更内容 | 状态 |
|-----|------|---------|------|
| 数据库 | `ccdi_project` 表 | 新增 `lsfx_project_id` 字段 | ✅ 已完成 |
| SQL | `2026-03-04-add-lsfx-project-id.sql` | 数据库迁移脚本 | ✅ 已执行 |
| 实体类 | `CcdiProject.java` | 新增 `lsfxProjectId` 属性 | ✅ 已修改 |
| VO | `CcdiProjectVO.java` | 新增 `lsfxProjectId` 属性 | ✅ 已修改 |
| Service | `CcdiProjectServiceImpl.java` | 注入 `LsfxAnalysisClient`,添加调用逻辑 | ✅ 已修改 |
```
**Step 3: 提交文档更新**
```bash
git add doc/design/2026-03-04-create-project-integrate-lsfx-design.md
git commit -m "docs: 更新设计文档状态为已实施"
```
**Step 4: 创建实施总结**
```bash
git log --oneline --graph > doc/design/2026-03-04-implementation-summary.txt
```
提交总结:
```bash
git add doc/design/2026-03-04-implementation-summary.txt
git commit -m "docs: 添加实施总结"
```
---
## Task 13: 最终验收
**Files:**
- 无需修改文件
**Step 1: 运行所有测试**
```bash
cd ccdi-project
mvn test
```
预期结果:所有测试通过
**Step 2: 检查代码质量**
```bash
mvn checkstyle:check
```
如果失败,根据提示修复代码风格问题
**Step 3: 验证数据库字段**
```sql
DESC ccdi_project;
```
确认 `lsfx_project_id` 字段存在
**Step 4: 端到端测试**
通过前端或 Swagger 完整测试创建项目流程:
1. 创建项目成功
2. 查询项目列表,确认 `lsfxProjectId` 显示
3. 查询项目详情,确认 `lsfxProjectId` 正确
**Step 5: 记录验收结果**
在项目文档中记录:
- 功能验收通过 ✅
- 测试覆盖率100%
- 性能测试创建项目耗时约1-2秒取决于网络
- 异常处理:已验证
---
## 后续任务(可选)
### Task 14: 添加前端展示(可选)
如果需要在项目列表页面展示 `lsfxProjectId`
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
- Modify: `ruoyi-ui/src/api/ccdi/project.js`
**Step 1: 修改表格列定义**
`index.vue` 的表格列中添加:
```javascript
{
label: '流水分析项目ID',
prop: 'lsfxProjectId',
width: '120'
}
```
**Step 2: 提交前端变更**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 前端项目列表展示流水分析平台项目ID"
```
---
## 实施注意事项
### 关键点
1. **事务一致性**:确保 `@Transactional` 注解存在,任何异常都会回滚
2. **参数正确性**:严格按照《兰溪-流水分析对接-新版.md》配置固定参数
3. **错误提示**:给用户清晰的错误提示,便于排查问题
4. **测试覆盖**:必须测试成功和失败两种场景
### 常见问题
**Q1: 测试时提示找不到 LsfxAnalysisClient**
- 检查 ccdi-project 模块是否依赖 ccdi-lsfx 模块
- 检查 Spring 组件扫描是否包含 `com.ruoyi.lsfx`
**Q2: 数据库字段添加失败**
- 确认数据库连接正常
- 检查是否有权限修改表结构
**Q3: Mock Server 无法启动**
- 检查 8000 端口是否被占用
- 确认 Python 环境正常
**Q4: 事务回滚不生效**
- 确认方法上有 `@Transactional` 注解
- 检查异常是否被捕获后未重新抛出
---
## 参考资料
- 设计文档: `doc/design/2026-03-04-create-project-integrate-lsfx-design.md`
- 流水分析对接文档: `assets/对接流水分析/兰溪-流水分析对接-新版.md`
- 项目规范: `CLAUDE.md`
---
**计划创建完成!**

View File

@@ -0,0 +1,334 @@
# 项目详情页面导航菜单改造设计文档
## 概述
将项目详情页面detail.vue右侧的按钮组改为水平导航菜单使用 Element UI Menu 组件实现简洁链接风格。
## 当前问题
项目详情页面右侧的操作按钮(上传数据、参数配置、初核结果)占用空间较大,视觉层级不够清晰,交互方式不够统一。
## 解决方案
### 核心设计
- 使用 Element UI 的 `el-menu` 组件(水平模式)
- 菜单放在标题右侧,右对齐
- "上传数据"和"参数配置"作为普通菜单项
- "初核结果"保留下拉菜单结构,包含三个子项:结果总览、专项排查、流水明细查询
- 采用简洁链接风格:透明背景 + 底部下划线激活效果
### 视觉风格
- 默认状态:灰色文字(#606266),透明背景
- Hover 状态:浅灰背景(#f5f7fa),深色文字(#303133
- 激活状态:蓝色文字(#1890ff+ 底部 2px 蓝色下划线
- 下拉菜单:白色背景,激活项浅蓝背景(#e6f7ff
## 技术设计
### 1. 组件结构
#### detail.vue 模板改造
```vue
<div class="header-right">
<el-menu
:default-active="activeTab"
mode="horizontal"
@select="handleMenuSelect"
class="nav-menu"
>
<el-menu-item index="upload">上传数据</el-menu-item>
<el-menu-item index="config">参数配置</el-menu-item>
<el-submenu index="result">
<template slot="title">初核结果</template>
<el-menu-item index="overview">结果总览</el-menu-item>
<el-menu-item index="special">专项排查</el-menu-item>
<el-menu-item index="detail">流水明细查询</el-menu-item>
</el-submenu>
</el-menu>
</div>
<!-- 动态组件渲染区域 -->
<component
:is="currentComponent"
:project-id="projectId"
:project-info="projectInfo"
@menu-change="handleMenuChange"
@data-uploaded="handleDataUploaded"
@name-selected="handleNameSelected"
@generate-report="handleGenerateReport"
@fetch-bank-info="handleFetchBankInfo"
/>
```
### 2. 数据结构
```javascript
data() {
return {
activeTab: 'upload', // 当前激活的菜单项索引
currentComponent: 'UploadData', // 当前显示的组件名称
// ... 其他现有数据
}
}
```
### 3. 交互逻辑
```javascript
methods: {
/** 菜单选择事件 */
handleMenuSelect(index) {
this.activeTab = index;
// 组件映射
const componentMap = {
'upload': 'UploadData',
'config': 'ParamConfig',
'overview': 'PreliminaryCheck',
'special': 'SpecialCheck',
'detail': 'DetailQuery'
};
this.currentComponent = componentMap[index] || 'UploadData';
},
// ... 其他现有方法
}
```
### 4. 组件导入
```javascript
import UploadData from "./components/detail/UploadData";
import ParamConfig from "./components/detail/ParamConfig";
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
import SpecialCheck from "./components/detail/SpecialCheck";
import DetailQuery from "./components/detail/DetailQuery";
export default {
name: "ProjectDetail",
components: {
UploadData,
ParamConfig,
PreliminaryCheck,
SpecialCheck,
DetailQuery,
},
// ...
}
```
## 样式设计
### 1. 导航菜单样式
```scss
.header-right {
.nav-menu {
// 移除默认背景色和边框
background-color: transparent;
border-bottom: none;
// 菜单项基础样式
.el-menu-item,
.el-submenu__title {
font-size: 14px;
color: #606266;
padding: 0 16px;
height: 40px;
line-height: 40px;
border-bottom: 2px solid transparent;
&:hover {
background-color: #f5f7fa;
color: #303133;
}
}
// 激活状态:底部下划线 + 蓝色文字
.el-menu-item.is-active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
background-color: transparent;
}
// 下拉菜单激活状态
.el-submenu.is-active > .el-submenu__title {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
// 下拉菜单图标
.el-submenu__icon-arrow {
margin-left: 4px;
}
}
}
```
### 2. 下拉菜单弹窗样式
```scss
::v-deep .el-menu--popup {
min-width: 140px;
.el-menu-item {
font-size: 14px;
&:hover {
background-color: #f5f7fa;
}
&.is-active {
color: #1890ff;
background-color: #e6f7ff;
}
}
}
```
### 3. 响应式适配
```scss
@media (max-width: 768px) {
.detail-header {
flex-direction: column;
align-items: flex-start;
.header-right {
width: 100%;
margin-top: 12px;
.nav-menu {
width: 100%;
display: flex;
justify-content: flex-start;
.el-menu-item,
.el-submenu {
flex: 1;
text-align: center;
padding: 0 8px;
font-size: 13px;
}
}
}
}
}
```
## 组件规范
### Props 接口
所有子组件应接收相同的 props
```javascript
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
```
### Events 接口
```javascript
this.$emit('data-uploaded', { type: 'xxx' });
this.$emit('generate-report');
this.$emit('fetch-bank-info');
this.$emit('menu-change', { key, route });
this.$emit('name-selected', nameList);
```
## 实施步骤
### 第一步:修改 detail.vue 文件
1. 替换 header-right 中的按钮为 el-menu 组件
2. 添加 activeTab 和 currentComponent 数据字段
3. 实现 handleMenuSelect 方法
4. 添加动态组件渲染区域
5. 导入所有子组件
### 第二步:添加样式
1. 添加导航菜单的自定义样式
2. 添加下拉菜单样式
3. 添加响应式样式
### 第三步:创建占位组件
为未实现的功能创建占位组件:
- ParamConfig.vue
- PreliminaryCheck.vue
- SpecialCheck.vue
- DetailQuery.vue
### 第四步:测试验证
- 功能测试:菜单切换、下拉菜单交互
- 视觉测试:样式符合设计要求
- 响应式测试:移动端布局正常
## 技术栈
- Element UI Menu 组件(`el-menu`, `el-menu-item`, `el-submenu`
- Vue 动态组件(`<component :is="...">`
- Scoped CSS 样式覆盖
- Vue 2.6.12
- Element UI 2.15.14
## 预期效果
### 视觉效果
- 菜单项横向排列在标题右侧,右对齐
- 简洁链接风格,无背景色和边框
- 激活项显示蓝色文字和底部下划线
- 下拉菜单样式统一
### 交互效果
- 点击菜单项切换组件URL 不变
- 下拉菜单点击外部区域可关闭
- 组件切换流畅,数据正确传递
- 响应式布局在移动端自适应
## 代码改动量估算
- detail.vue 文件改动:约 80-100 行(模板 + 脚本 + 样式)
- 占位组件创建4 个文件,每个约 20 行
- 总代码量:约 150-180 行
## 风险与注意事项
1. **组件文件不存在**ParamConfig、PreliminaryCheck 等组件需要创建占位组件
2. **样式覆盖**Element UI 默认样式覆盖需要使用 `::v-deep``/deep/`
3. **事件传递**:确保所有子组件的事件正确向上传递
4. **路由监听**:移除原有路由相关的逻辑,改为组件状态管理
## 后续优化建议
1. 添加菜单切换动画效果
2. 为占位组件实现完整功能
3. 添加面包屑导航支持
4. 支持菜单项权限控制
5. 添加快捷键支持Ctrl+Tab 切换)
## 测试清单
- [ ] 点击"上传数据"菜单,显示 UploadData 组件
- [ ] 点击"参数配置"菜单,显示占位组件或 ParamConfig 组件
- [ ] 点击"初核结果"菜单,展开下拉菜单
- [ ] 点击下拉菜单子项,切换到对应组件
- [ ] 激活菜单项显示底部下划线
- [ ] Hover 菜单项显示浅灰背景
- [ ] 下拉菜单点击外部区域关闭
- [ ] 组件切换时 projectId 和 projectInfo 正确传递
- [ ] 移动端菜单响应式布局正常
- [ ] 现有功能不受影响(返回按钮、项目信息显示等)

View File

@@ -0,0 +1,960 @@
# 项目详情页面导航菜单改造实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将项目详情页面右侧的按钮组改为水平导航菜单,使用 Element UI Menu 组件实现简洁链接风格,支持菜单切换组件内容。
**Architecture:** 使用 Element UI 的 `el-menu` 组件(水平模式)替换现有的按钮组,通过 Vue 动态组件(`<component :is="...">`)实现内容切换。菜单项包括"上传数据"、"参数配置"和"初核结果"下拉菜单(含三个子项)。采用简洁链接风格,激活状态显示底部下划线。
**Tech Stack:** Vue 2.6.12, Element UI 2.15.14, Scoped CSS
---
## 前置检查
**验证当前项目状态:**
```bash
cd D:/ccdi/ccdi
git status
```
预期:工作目录干净,或只有 CLAUDE.md 修改
**验证文件存在:**
```bash
ls ruoyi-ui/src/views/ccdiProject/detail.vue
ls ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
```
预期:两个文件都存在
---
## Task 1: 创建占位组件 ParamConfig
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**Step 1: 创建 ParamConfig 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`:
```vue
<template>
<div class="param-config-container">
<div class="placeholder-content">
<i class="el-icon-setting"></i>
<p>参数配置功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "ParamConfig",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.param-config-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 ParamConfig 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
git commit -m "feat(ccdiProject): 添加参数配置占位组件"
```
---
## Task 2: 创建占位组件 PreliminaryCheck
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
**Step 1: 创建 PreliminaryCheck 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`:
```vue
<template>
<div class="preliminary-check-container">
<div class="placeholder-content">
<i class="el-icon-data-analysis"></i>
<p>结果总览功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "PreliminaryCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.preliminary-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 PreliminaryCheck 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue
git commit -m "feat(ccdiProject): 添加结果总览占位组件"
```
---
## Task 3: 创建占位组件 SpecialCheck
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
**Step 1: 创建 SpecialCheck 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`:
```vue
<template>
<div class="special-check-container">
<div class="placeholder-content">
<i class="el-icon-search"></i>
<p>专项排查功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "SpecialCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.special-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 SpecialCheck 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue
git commit -m "feat(ccdiProject): 添加专项排查占位组件"
```
---
## Task 4: 创建占位组件 DetailQuery
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
**Step 1: 创建 DetailQuery 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`:
```vue
<template>
<div class="detail-query-container">
<div class="placeholder-content">
<i class="el-icon-document"></i>
<p>流水明细查询功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "DetailQuery",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.detail-query-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 DetailQuery 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
git commit -m "feat(ccdiProject): 添加流水明细查询占位组件"
```
---
## Task 5: 修改 detail.vue - 添加数据字段和导入组件
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加组件导入**
`detail.vue``<script>` 部分,找到 import 语句(第 72 行附近),替换为:
```javascript
import UploadData from "./components/detail/UploadData";
import ParamConfig from "./components/detail/ParamConfig";
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
import SpecialCheck from "./components/detail/SpecialCheck";
import DetailQuery from "./components/detail/DetailQuery";
```
**Step 2: 注册组件**
`components` 对象中(第 81-88 行),替换为:
```javascript
components: {
UploadData,
ParamConfig,
PreliminaryCheck,
SpecialCheck,
DetailQuery,
},
```
**Step 3: 添加数据字段**
`data()` 函数中(第 89-110 行),在 `activeTab: "data"` 之后添加:
```javascript
data() {
return {
// 当前激活的菜单项索引
activeTab: "upload",
// 当前显示的组件名称
currentComponent: "UploadData",
// 项目ID
projectId: this.$route.params.projectId,
// ... 其他现有数据保持不变
projectInfo: {
projectId: this.$route.params.projectId,
projectName: "",
projectDesc: "",
createTime: "",
updateTime: "",
startDate: "",
endDate: "",
targetCount: 0,
warningCount: 0,
warningThreshold: 60,
projectStatus: "0",
},
};
},
```
**Step 4: 验证文件语法正确**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 5: 提交组件导入修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 导入子组件并添加菜单状态数据"
```
---
## Task 6: 修改 detail.vue - 替换模板中的按钮为菜单
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 替换 header-right 中的按钮组**
找到 `<div class="header-right">` 部分(第 27-55 行),替换为:
```vue
<div class="header-right">
<el-menu
:default-active="activeTab"
mode="horizontal"
@select="handleMenuSelect"
class="nav-menu"
>
<el-menu-item index="upload">上传数据</el-menu-item>
<el-menu-item index="config">参数配置</el-menu-item>
<el-submenu index="result">
<template slot="title">初核结果</template>
<el-menu-item index="overview">结果总览</el-menu-item>
<el-menu-item index="special">专项排查</el-menu-item>
<el-menu-item index="detail">流水明细查询</el-menu-item>
</el-submenu>
</el-menu>
</div>
```
**Step 2: 替换 UploadData 为动态组件**
找到 `<UploadData>` 组件(第 59-67 行),替换为:
```vue
<!-- 动态组件渲染区域 -->
<component
:is="currentComponent"
:project-id="projectId"
:project-info="projectInfo"
@menu-change="handleMenuChange"
@data-uploaded="handleDataUploaded"
@name-selected="handleNameSelected"
@generate-report="handleGenerateReport"
@fetch-bank-info="handleFetchBankInfo"
/>
```
**Step 3: 验证模板语法**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 4: 提交模板修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 替换按钮组为导航菜单并使用动态组件"
```
---
## Task 7: 修改 detail.vue - 添加菜单选择处理方法
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加菜单选择处理方法**
`methods` 对象中(第 124 行),在 `handleBack()` 方法之后添加新方法:
```javascript
/** 菜单选择事件 */
handleMenuSelect(index) {
console.log("菜单选择:", index);
this.activeTab = index;
// 组件映射
const componentMap = {
upload: "UploadData",
config: "ParamConfig",
overview: "PreliminaryCheck",
special: "SpecialCheck",
detail: "DetailQuery",
};
this.currentComponent = componentMap[index] || "UploadData";
},
```
**Step 2: 删除废弃的方法**
删除以下不再使用的方法(第 226-251 行):
- `handleUploadData()`
- `handleParamConfig()`
- `handleCheckResultCommand()`
**Step 3: 更新 handleMenuChange 方法**
修改 `handleMenuChange` 方法(第 185-205 行),简化为:
```javascript
/** UploadData 组件:菜单切换 */
handleMenuChange({ key, route }) {
console.log("切换到菜单:", key, route);
// 直接触发菜单选择
this.handleMenuSelect(route);
},
```
**Step 4: 验证方法逻辑**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误,未使用的方法已删除
**Step 5: 提交方法修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 添加菜单选择处理方法并清理废弃代码"
```
---
## Task 8: 修改 detail.vue - 添加导航菜单样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加导航菜单样式**
`<style lang="scss" scoped>` 部分(第 306 行之后),在 `.header-right` 样式块内部添加:
```scss
.header-right {
.nav-menu {
// 移除默认背景色和边框
background-color: transparent;
border-bottom: none;
// 菜单项基础样式
.el-menu-item,
.el-submenu__title {
font-size: 14px;
color: #606266;
padding: 0 16px;
height: 40px;
line-height: 40px;
border-bottom: 2px solid transparent;
&:hover {
background-color: #f5f7fa;
color: #303133;
}
}
// 激活状态:底部下划线 + 蓝色文字
.el-menu-item.is-active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
background-color: transparent;
}
// 下拉菜单激活状态
.el-submenu.is-active > .el-submenu__title {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
// 下拉菜单图标
.el-submenu__icon-arrow {
margin-left: 4px;
}
}
}
```
**Step 2: 添加下拉菜单弹窗样式**
在样式末尾(第 496 行之后),添加深度选择器样式:
```scss
// 下拉菜单弹窗样式
::v-deep .el-menu--popup {
min-width: 140px;
.el-menu-item {
font-size: 14px;
&:hover {
background-color: #f5f7fa;
}
&.is-active {
color: #1890ff;
background-color: #e6f7ff;
}
}
}
```
**Step 3: 验证样式语法**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 4: 提交导航菜单样式**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "style(ccdiProject): 添加导航菜单简洁链接风格样式"
```
---
## Task 9: 修改 detail.vue - 添加响应式样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加响应式样式**
在现有的 `@media (max-width: 768px)` 媒体查询中(第 464 行),找到 `.detail-header` 样式块,添加响应式菜单样式:
```scss
@media (max-width: 768px) {
.dpc-detail-container {
padding: 8px;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.header-right {
width: 100%;
margin-top: 12px;
.nav-menu {
width: 100%;
display: flex;
justify-content: flex-start;
.el-menu-item,
.el-submenu {
flex: 1;
text-align: center;
padding: 0 8px;
font-size: 13px;
}
}
}
}
// ... 其他现有响应式样式保持不变
}
```
**Step 2: 验证响应式样式**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 3: 提交响应式样式**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "style(ccdiProject): 添加导航菜单响应式布局支持"
```
---
## Task 10: 手动测试验证
**Files:**
- Test: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 启动前端开发服务器**
```bash
cd ruoyi-ui
npm run dev
```
预期:服务启动成功,监听 http://localhost:80
**Step 2: 访问项目详情页面**
在浏览器中访问:`http://localhost/ccdiProject`,点击任意项目进入详情页面
预期:页面正常加载,右侧显示导航菜单
**Step 3: 测试菜单切换功能**
测试步骤:
1. 点击"上传数据"菜单项
- 预期:激活状态显示蓝色文字和底部下划线,显示 UploadData 组件
2. 点击"参数配置"菜单项
- 预期:激活状态切换,显示 ParamConfig 占位组件("参数配置功能开发中..."
3. 点击"初核结果"菜单,展开下拉菜单
- 预期:下拉菜单展开,显示三个子项
4. 点击"结果总览"子菜单项
- 预期:激活状态切换,显示 PreliminaryCheck 占位组件
5. 点击"专项排查"子菜单项
- 预期:显示 SpecialCheck 占位组件
6. 点击"流水明细查询"子菜单项
- 预期:显示 DetailQuery 占位组件
**Step 4: 测试样式效果**
测试步骤:
1. Hover 菜单项
- 预期:显示浅灰背景(#f5f7fa
2. 检查激活菜单项
- 预期:蓝色文字(#1890ff+ 底部 2px 蓝色下划线
3. 点击下拉菜单外部区域
- 预期:下拉菜单关闭
4. 调整浏览器窗口宽度至 768px 以下
- 预期:菜单项平均分配宽度,布局自适应
**Step 5: 测试数据传递**
测试步骤:
1. 切换到"参数配置"组件
2. 在浏览器控制台检查组件 props
- 预期projectId 和 projectInfo 正确传递
3. 点击"上传数据"中的功能按钮
- 预期:事件正常触发,原有功能不受影响
**Step 6: 记录测试结果**
创建测试报告文件 `docs/test-reports/2026-03-04-navigation-menu-test.md`:
```markdown
# 项目详情页面导航菜单改造测试报告
## 测试环境
- 浏览器: [记录浏览器名称和版本]
- 测试时间: 2026-03-04
- 测试人员: [你的名字]
## 功能测试
### 菜单切换
- [x] 点击"上传数据",显示 UploadData 组件
- [x] 点击"参数配置",显示 ParamConfig 占位组件
- [x] 点击"初核结果",下拉菜单展开
- [x] 点击"结果总览",显示 PreliminaryCheck 占位组件
- [x] 点击"专项排查",显示 SpecialCheck 占位组件
- [x] 点击"流水明细查询",显示 DetailQuery 占位组件
### 样式测试
- [x] 默认状态:灰色文字,透明背景
- [x] Hover 状态:浅灰背景,深色文字
- [x] 激活状态:蓝色文字 + 底部下划线
- [x] 下拉菜单样式统一
### 交互测试
- [x] 下拉菜单点击外部区域关闭
- [x] 菜单切换流畅无闪烁
- [x] 组件切换数据正确传递
### 响应式测试
- [x] 移动端菜单布局正常
- [x] 菜单项平均分配宽度
## 问题记录
[记录发现的任何问题]
## 测试结论
[通过/需要修复]
```
**Step 7: 提交测试报告**
```bash
git add docs/test-reports/2026-03-04-navigation-menu-test.md
git commit -m "test(ccdiProject): 添加导航菜单改造测试报告"
```
---
## Task 11: 清理和最终提交
**Step 1: 检查所有修改文件**
```bash
git status
```
预期:所有修改已提交
**Step 2: 查看提交历史**
```bash
git log --oneline -10
```
预期:看到 10 个新提交:
1. feat(ccdiProject): 添加参数配置占位组件
2. feat(ccdiProject): 添加结果总览占位组件
3. feat(ccdiProject): 添加专项排查占位组件
4. feat(ccdiProject): 添加流水明细查询占位组件
5. feat(ccdiProject): 导入子组件并添加菜单状态数据
6. feat(ccdiProject): 替换按钮组为导航菜单并使用动态组件
7. feat(ccdiProject): 添加菜单选择处理方法并清理废弃代码
8. style(ccdiProject): 添加导航菜单简洁链接风格样式
9. style(ccdiProject): 添加导航菜单响应式布局支持
10. test(ccdiProject): 添加导航菜单改造测试报告
**Step 3: 验证无遗留问题**
```bash
cd ruoyi-ui
npm run lint
```
预期:无 lint 错误
**Step 4: 推送到远程分支**
```bash
git push origin dev
```
预期:推送成功
---
## 实施后检查清单
- [ ] 所有 4 个占位组件已创建
- [ ] detail.vue 已修改完成(模板、脚本、样式)
- [ ] 导航菜单样式符合简洁链接风格
- [ ] 菜单切换功能正常
- [ ] 下拉菜单交互正常
- [ ] 响应式布局正常
- [ ] 所有修改已提交到 git
- [ ] 测试报告已完成
- [ ] 代码已推送到远程仓库
---
## 预期成果
### 文件创建
- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
- `docs/test-reports/2026-03-04-navigation-menu-test.md`
### 文件修改
- `ruoyi-ui/src/views/ccdiProject/detail.vue`(模板、脚本、样式)
### Git 提交
- 10 个功能清晰的提交记录
### 功能实现
- ✅ 水平导航菜单替代按钮组
- ✅ 简洁链接风格样式
- ✅ 菜单切换组件内容
- ✅ 下拉菜单支持
- ✅ 响应式布局
- ✅ 原有功能不受影响
---
## 潜在问题和解决方案
### 问题 1: 组件切换时状态丢失
**解决方案:** 使用 `<keep-alive>` 包裹动态组件(可选优化)
```vue
<keep-alive>
<component :is="currentComponent" ... />
</keep-alive>
```
### 问题 2: 下拉菜单样式不生效
**解决方案:** 检查 `::v-deep` 是否被正确编译,可能需要使用 `/deep/``>>>`
### 问题 3: 移动端菜单换行
**解决方案:** 调整响应式断点或使用折叠菜单el-menu 的 collapse 模式)
### 问题 4: 原有事件未触发
**解决方案:** 检查动态组件的事件绑定是否完整,确保所有事件都有对应的处理方法
---
## 后续优化建议
1. **添加组件切换动画**
```vue
<transition name="fade" mode="out-in">
<component :is="currentComponent" ... />
</transition>
```
2. **实现占位组件的完整功能**
- ParamConfig: 模型参数配置界面
- PreliminaryCheck: 结果总览数据展示
- SpecialCheck: 专项排查功能
- DetailQuery: 流水明细查询和筛选
3. **添加菜单权限控制**
- 根据用户权限显示/隐藏菜单项
- 使用 `v-if` 或动态生成菜单配置
4. **添加面包屑导航**
- 在页面顶部显示当前位置
- 支持快速返回上级页面
5. **添加快捷键支持**
- Ctrl+Tab: 切换到下一个菜单
- Ctrl+Shift+Tab: 切换到上一个菜单
---
## 相关技能参考
- @superpowers:brainstorming - 需求分析和设计
- @superpowers:test-driven-development - TDD 开发流程
- @superpowers:verification-before-completion - 完成前验证
- @superpowers:requesting-code-review - 代码审查
---
## 文档参考
- 设计文档: `docs/plans/2026-03-04-project-detail-navigation-menu-design.md`
- Element UI Menu 文档: https://element.eleme.cn/#/zh-CN/component/menu
- Vue 动态组件: https://cn.vuejs.org/v2/guide/components.html#动态组件

View File

@@ -0,0 +1,81 @@
import request from '@/utils/request'
// 获取项目上传数据状态
export function getUploadStatus(projectId) {
return request({
url: '/ccdi/project/' + projectId + '/upload-status',
method: 'get'
})
}
// 上传文件
export function uploadFile(projectId, uploadType, file) {
const formData = new FormData()
formData.append('file', file)
formData.append('uploadType', uploadType)
return request({
url: '/ccdi/project/' + projectId + '/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 删除文件
export function deleteFile(projectId, uploadType) {
return request({
url: '/ccdi/project/' + projectId + '/file/' + uploadType,
method: 'delete'
})
}
// 获取名单库选项
export function getNameListOptions() {
return request({
url: '/ccdi/project/name-list/options',
method: 'get'
})
}
// 更新名单库选择
export function updateNameListSelection(projectId, data) {
return request({
url: '/ccdi/project/' + projectId + '/name-lists',
method: 'put',
data: data
})
}
// 执行数据质量检查
export function executeQualityCheck(projectId) {
return request({
url: '/ccdi/project/' + projectId + '/quality-check',
method: 'post'
})
}
// 拉取本行信息
export function pullBankInfo(projectId) {
return request({
url: '/ccdi/project/' + projectId + '/pull-bank-info',
method: 'post'
})
}
// 生成报告
export function generateReport(projectId) {
return request({
url: '/ccdi/project/' + projectId + '/generate-report',
method: 'post'
})
}
// 查询导入状态
export function getImportStatus(taskId) {
return request({
url: '/ccdi/project/upload-status/' + taskId,
method: 'get'
})
}

View File

@@ -70,6 +70,13 @@ export const constantRoutes = [
component: () => import('@/views/ccdiProject/index'), component: () => import('@/views/ccdiProject/index'),
name: 'CcdiProject', name: 'CcdiProject',
meta: { title: '初核项目管理', icon: 'dashboard', affix: true } meta: { title: '初核项目管理', icon: 'dashboard', affix: true }
},
{
path: 'ccdiProject/detail/:projectId',
component: () => import('@/views/ccdiProject/detail'),
name: 'ProjectDetail',
hidden: true,
meta: { title: '项目详情', noCache: true }
} }
] ]
}, },

View File

@@ -0,0 +1,51 @@
<template>
<div class="detail-query-container">
<div class="placeholder-content">
<i class="el-icon-document"></i>
<p>流水明细查询功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "DetailQuery",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.detail-query-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="param-config-container">
<div class="placeholder-content">
<i class="el-icon-setting"></i>
<p>参数配置功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "ParamConfig",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.param-config-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="preliminary-check-container">
<div class="placeholder-content">
<i class="el-icon-data-analysis"></i>
<p>结果总览功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "PreliminaryCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.preliminary-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="special-check-container">
<div class="placeholder-content">
<i class="el-icon-search"></i>
<p>专项排查功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "SpecialCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.special-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,947 @@
<template>
<div class="upload-data-container">
<!-- 主内容区 -->
<div class="main-content">
<!-- 页面头部 -->
<div class="content-header">
<h2 class="content-title">{{ currentMenuTitle }}</h2>
<div class="header-actions">
<el-button
size="small"
type="primary"
icon="el-icon-document"
@click="handleGenerateReport"
>
生成报告
</el-button>
<el-button
size="small"
icon="el-icon-download"
@click="handleFetchBankInfo"
>
拉取本行信息
</el-button>
</div>
</div>
<!-- 上传模块 -->
<div class="upload-section">
<div class="upload-cards">
<div v-for="card in uploadCards" :key="card.key" class="upload-card">
<div class="card-icon">
<i :class="card.icon"></i>
</div>
<div class="card-title">{{ card.title }}</div>
<div class="card-desc">{{ card.desc }}</div>
<el-button
size="small"
:type="card.uploaded ? 'primary' : ''"
:icon="card.uploaded ? 'el-icon-view' : 'el-icon-upload2'"
:plain="!card.uploaded"
@click="handleUploadClick(card.key)"
>
{{ card.btnText }}
</el-button>
</div>
</div>
</div>
<!-- 数据质量检查
<div class="quality-check-section">
<div class="section-header">
<i class="el-icon-warning-outline warning-icon"></i>
<span>检查结果</span>
</div>
<div class="metrics">
<div v-for="metric in metrics" :key="metric.key" class="metric-card">
<div class="metric-title">{{ metric.title }}</div>
<div class="metric-value" :class="`value-${metric.level}`">
{{ metric.value }}
</div>
<div class="progress-ring-container">
<svg class="progress-ring" viewBox="0 0 32 32">
<circle
class="progress-ring-bg"
cx="16"
cy="16"
r="14"
fill="none"
stroke="#f0f0f0"
stroke-width="4"
/>
<circle
class="progress-ring-progress"
:stroke="getProgressColor(metric.level)"
cx="16"
cy="16"
r="14"
fill="none"
stroke-width="4"
:stroke-dasharray="circumference"
:stroke-dashoffset="getProgressOffset(metric.value)"
stroke-linecap="round"
/>
</svg>
</div>
</div>
</div>
</div> -->
</div>
<!-- 上传弹窗 -->
<el-dialog
v-if="showUploadDialog"
:title="uploadDialogTitle"
:visible.sync="showUploadDialog"
:close-on-click-modal="false"
width="500px"
>
<el-upload
class="upload-area"
drag
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
支持 {{ uploadFileTypes }} 格式文件
</div>
</el-upload>
<span slot="footer">
<el-button @click="showUploadDialog = false">取消</el-button>
<el-button
type="primary"
@click="handleConfirmUpload"
:loading="uploading"
>确定</el-button
>
</span>
</el-dialog>
<!-- 名单选择弹窗 -->
<el-dialog
:title="'名单库选择'"
:visible.sync="showNameListDialog"
width="600px"
>
<el-form :model="nameListForm" label-width="100px">
<el-form-item label="名单类型">
<el-select v-model="nameListForm.type" placeholder="请选择名单类型">
<el-option label="黑名单" value="blacklist"></el-option>
<el-option label="灰名单" value="graylist"></el-option>
<el-option label="白名单" value="whitelist"></el-option>
</el-select>
</el-form-item>
<el-form-item label="名单来源">
<el-select v-model="nameListForm.source" placeholder="请选择名单来源">
<el-option label="中台管理系统" value="platform"></el-option>
<el-option label="本地上传" value="local"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showNameListDialog = false">取消</el-button>
<el-button type="primary" @click="handleConfirmNameList"
>确定</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
import {
getUploadStatus,
uploadFile,
deleteFile,
getNameListOptions,
updateNameListSelection,
executeQualityCheck,
pullBankInfo,
generateReport,
getImportStatus,
} from "@/api/ccdiProjectUpload";
export default {
name: "UploadData",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
data() {
return {
// 加载状态
loading: false,
// 当前选中的菜单
activeMenu: "upload",
// 当前菜单标题
currentMenuTitle: "上传数据",
// 圆环周长
circumference: 2 * Math.PI * 14,
// 上传弹窗
showUploadDialog: false,
uploadDialogTitle: "",
uploadFileType: "",
uploadFileTypes: "",
fileList: [],
uploading: false,
// 名单选择弹窗
showNameListDialog: false,
nameListForm: {
type: "",
source: "platform",
},
// 上传状态列表
uploadStatusList: [],
// 名单库选项列表
nameListOptions: [],
// 侧边栏菜单项
menuItems: [
{ key: "upload", label: "上传数据", route: "upload" },
{ key: "config", label: "参数配置", route: "config" },
{ key: "overview", label: "结果总览", route: "overview" },
{ key: "special", label: "专项排查", route: "special" },
{ key: "detail", label: "流水明细查询", route: "detail" },
],
// 上传卡片
uploadCards: [
{
key: "transaction",
title: "流水导入",
desc: "支持 Excel、PDF 格式文件上传",
icon: "el-icon-document",
btnText: "上传流水",
uploaded: false,
},
{
key: "credit",
title: "征信导入",
desc: "支持 HTML 格式征信数据解析",
icon: "el-icon-s-data",
btnText: "上传征信",
uploaded: false,
},
{
key: "employee",
title: "员工关系导入",
desc: "Excel 表格上传员工家庭关系信息",
icon: "el-icon-user",
btnText: "上传员工关系",
uploaded: false,
},
{
key: "namelist",
title: "名单库选择",
desc: "选择中台管理系统的名单",
icon: "el-icon-s-order",
btnText: "选择名单",
uploaded: false,
},
],
// 质量指标
metrics: [
{
key: "completeness",
title: "数据完整性",
value: "98.5%",
level: "success",
},
{
key: "consistency",
title: "格式一致性",
value: "95.2%",
level: "info",
},
{
key: "continuity",
title: "余额连续性",
value: "92.8%",
level: "info",
},
],
};
},
created() {
// 加载初始数据
// this.loadInitialData();
// 监听路由变化更新选中菜单
this.updateActiveMenu();
},
mounted() {
// 组件挂载后监听项目ID变化
this.$watch("projectId", this.loadInitialData);
},
methods: {
/** 加载初始数据 */
async loadInitialData() {
if (!this.projectId) return;
try {
this.loading = true;
// 并行加载上传状态和名单库选项
const [uploadStatusRes, nameListRes] = await Promise.all([
getUploadStatus(this.projectId),
getNameListOptions(),
]);
this.uploadStatusList = uploadStatusRes.data || [];
this.nameListOptions = nameListRes.data || [];
// 更新上传卡片状态
this.updateUploadCards();
// 模拟更新质量指标实际应从API获取
this.updateQualityMetrics();
} catch (error) {
console.error("加载初始数据失败:", error);
this.$message.error("加载数据失败");
} finally {
this.loading = false;
}
},
/** 更新上传卡片状态 */
updateUploadCards() {
const statusMap = {};
this.uploadStatusList.forEach((item) => {
statusMap[item.uploadType] = item;
});
this.uploadCards.forEach((card) => {
const status = statusMap[card.key.toUpperCase()];
if (status) {
card.uploaded = status.uploaded;
card.btnText = status.uploaded
? "已上传" + card.title.replace("导入", "").replace("上传", "")
: card.btnText;
}
});
},
/** 更新质量指标 */
updateQualityMetrics() {
// 模拟更新质量指标
this.metrics.forEach((metric) => {
if (metric.key === "completeness") {
metric.value = "98.5%";
metric.level = "success";
} else if (metric.key === "consistency") {
metric.value = "95.2%";
metric.level = "info";
} else if (metric.key === "continuity") {
metric.value = "92.8%";
metric.level = "info";
}
});
},
/** 菜单点击 */
handleMenuClick(key, route) {
const menuItem = this.menuItems.find((m) => m.key === key);
if (menuItem) {
this.currentMenuTitle = menuItem.label;
}
if (key === "upload") {
this.activeMenu = key;
} else {
// 其他菜单项通知父组件跳转
this.$emit("menu-change", { key, route });
}
},
/** 更新当前选中菜单 */
updateActiveMenu() {
this.activeMenu = "upload";
this.currentMenuTitle = "上传数据";
},
/** 上传卡片点击 */
handleUploadClick(key) {
const card = this.uploadCards.find((c) => c.key === key);
if (!card) return;
if (key === "namelist") {
this.showNameListDialog = true;
} else {
this.uploadFileType = key;
this.uploadDialogTitle = `上传${card.title}`;
this.uploadFileTypes = card.desc.replace(/.*支持|上传/g, "").trim();
this.showUploadDialog = true;
}
},
/** 文件选择变化 */
handleFileChange(file, fileList) {
this.fileList = fileList.slice(-1); // 只保留最后一个文件
},
/** 确认上传 */
async handleConfirmUpload() {
if (this.fileList.length === 0) {
this.$message.warning("请选择要上传的文件");
return;
}
this.uploading = true;
try {
// 调用上传API
const res = await uploadFile(
this.projectId,
this.uploadFileType.toUpperCase(),
this.fileList[0].raw
);
this.uploading = false;
this.showUploadDialog = false;
this.$message.success("文件上传成功,正在处理中...");
this.$emit("data-uploaded", { type: this.uploadFileType });
// 刷新上传状态
await this.loadUploadStatus();
// 开始轮询任务状态
this.startPolling(res.data);
} catch (error) {
this.uploading = false;
this.$message.error("上传失败:" + (error.msg || "未知错误"));
} finally {
this.fileList = [];
}
},
/** 轮询任务状态 */
startPolling(taskId) {
const maxAttempts = 20; // 最多轮询20次约10分钟
let attempts = 0;
const poll = async () => {
if (attempts >= maxAttempts) {
this.$message.warning("文件处理超时,请稍后查看");
return;
}
try {
const res = await getImportStatus(taskId);
const status = res.data;
if (!status) {
attempts++;
setTimeout(poll, 30000); // 30秒后再次查询
return;
}
if (status.uploadStatus === "SUCCESS") {
// 处理完成,刷新状态
await this.loadUploadStatus();
this.$message.success("文件处理完成");
return;
} else if (status.uploadStatus === "FAILED") {
// 处理失败
await this.loadUploadStatus();
this.$message.error(
"文件处理失败:" + (status.errorMessage || "未知错误")
);
return;
}
// 继续处理中
attempts++;
setTimeout(poll, 30000);
} catch (error) {
console.error("轮询任务状态失败:", error);
attempts++;
setTimeout(poll, 30000);
}
};
poll();
},
/** 加载上传状态 */
async loadUploadStatus() {
try {
const res = await getUploadStatus(this.projectId);
this.uploadStatusList = res.data || [];
this.updateUploadCards();
} catch (error) {
console.error("加载上传状态失败:", error);
}
},
/** 确认选择名单 */
async handleConfirmNameList() {
if (!this.nameListForm.type || !this.nameListForm.source) {
this.$message.warning("请完善名单选择信息");
return;
}
try {
// 调用更新名单API
await updateNameListSelection(this.projectId, {
nameLists: [
{
type: this.nameListForm.type,
source: this.nameListForm.source,
},
],
});
const card = this.uploadCards.find((c) => c.key === "namelist");
if (card) {
card.uploaded = true;
card.btnText = "已选择名单";
}
this.showNameListDialog = false;
this.$message.success("名单选择成功");
this.$emit("name-selected", this.nameListForm);
} catch (error) {
this.$message.error("名单选择失败:" + (error.msg || "未知错误"));
}
},
/** 生成报告 */
async handleGenerateReport() {
this.$confirm("确认生成报告吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
const loading = this.$loading({
lock: true,
text: "正在生成报告...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
// await generateReport(this.projectId);
loading.close();
this.$message.success("报告生成成功");
this.$emit("generate-report");
} catch (error) {
this.$message.error("生成报告失败:" + (error.msg || "未知错误"));
}
})
.catch(() => {});
},
/** 拉取本行信息 */
async handleFetchBankInfo() {
this.$confirm("确认拉取本行信息吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
const loading = this.$loading({
lock: true,
text: "正在拉取本行信息...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
await pullBankInfo(this.projectId);
loading.close();
this.$message.success("本行信息拉取成功");
this.$emit("fetch-bank-info");
// 刷新质量指标
this.updateQualityMetrics();
} catch (error) {
this.$message.error(
"拉取本行信息失败:" + (error.msg || "未知错误")
);
}
})
.catch(() => {});
},
/** 获取进度条偏移 */
getProgressOffset(value) {
const percentage = parseFloat(value);
return this.circumference * (1 - percentage / 100);
},
/** 获取进度条颜色 */
getProgressColor(level) {
const colorMap = {
success: "#52c41a",
info: "#1890ff",
warning: "#fa8c16",
danger: "#f5222d",
};
return colorMap[level] || "#1890ff";
},
/** 格式化更新时间 */
formatUpdateTime(time) {
if (!time) return "-";
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
/** 获取状态样式类 */
getStatusClass() {
const status = String(this.projectInfo.projectStatus);
const statusMap = {
0: "processing",
1: "success",
2: "archived",
};
return statusMap[status] || "processing";
},
/** 获取状态标签 */
getStatusLabel() {
const status = String(this.projectInfo.projectStatus);
const statusMap = {
0: "进行中",
1: "已完成",
2: "已归档",
};
return statusMap[status] || "未知";
},
},
};
</script>
<style lang="scss" scoped>
.upload-data-container {
padding: 16px;
background: #fff;
min-height: 100%;
}
// 侧边栏
.sidebar {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
.sidebar-header {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.sidebar-menu {
list-style: none;
padding: 0;
margin: 0 0 auto 0;
flex: 1;
.menu-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
color: #606266;
transition: all 0.3s;
position: relative;
&:hover {
background: #f5f7fa;
color: #303133;
}
&.active {
background: #1890ff;
color: #ffffff;
.menu-dot {
display: block;
}
}
.menu-dot {
display: none;
width: 4px;
height: 4px;
background: #ffffff;
border-radius: 50%;
margin-right: 8px;
}
}
}
.sidebar-footer {
border-top: 1px solid #ebeef5;
padding-top: 16px;
margin-top: 16px;
.status {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
.status-processing {
color: #1890ff;
font-weight: 500;
}
.status-success {
color: #52c41a;
font-weight: 500;
}
.status-archived {
color: #909399;
font-weight: 500;
}
}
.update-time {
font-size: 12px;
color: #909399;
}
}
}
// 主内容区
.main-content {
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.content-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 12px;
}
}
// 上传模块
.upload-section {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 20px;
margin-bottom: 16px;
.upload-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.upload-card {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 20px 16px;
text-align: center;
transition: all 0.3s;
background-color: #fff;
&:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
}
.card-icon {
font-size: 32px;
color: #1890ff;
margin-bottom: 12px;
i {
font-size: 32px;
}
}
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 13px;
color: #909399;
margin-bottom: 16px;
min-height: 36px;
line-height: 1.4;
}
.el-upload {
width: 100%;
}
}
}
}
// 数据质量检查
.quality-check-section {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 20px;
.section-header {
display: flex;
align-items: center;
margin-bottom: 20px;
.warning-icon {
font-size: 18px;
color: #fa8c16;
margin-right: 8px;
}
span {
font-size: 15px;
font-weight: 500;
color: #303133;
}
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
.metric-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafafa;
.metric-title {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
}
.metric-value {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
&.value-success {
color: #52c41a;
}
&.value-info {
color: #1890ff;
}
&.value-warning {
color: #fa8c16;
}
&.value-danger {
color: #f5222d;
}
}
.progress-ring-container {
width: 48px;
height: 48px;
.progress-ring {
width: 48px;
height: 48px;
transform: rotate(-90deg);
.progress-ring-bg {
stroke: #ebeef5;
}
.progress-ring-progress {
transition: stroke-dashoffset 0.5s ease;
}
}
}
}
}
}
}
// 上传弹窗样式
::v-deep .el-dialog__wrapper {
.upload-area {
width: 100%;
}
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
.el-upload__tip {
margin-top: 8px;
color: #909399;
font-size: 12px;
}
}
// 响应式
@media (max-width: 1200px) {
.upload-section .upload-cards {
grid-template-columns: repeat(2, 1fr);
}
.quality-check-section .metrics {
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
}
@media (max-width: 768px) {
.upload-data-container {
padding: 8px;
}
.sidebar {
margin-bottom: 16px;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.upload-section .upload-cards {
grid-template-columns: 1fr;
}
.quality-check-section .metrics {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,524 @@
<template>
<div class="app-container dpc-detail-container">
<!-- 原页面头部 (已隐藏使用UploadData组件的头部) -->
<div class="detail-header">
<div class="header-left">
<el-button size="small" icon="el-icon-back" @click="handleBack"
>返回</el-button
>
<div class="title-section">
<div class="page-title">
<h2>
{{ projectInfo.projectName }}
</h2>
<el-tag
:type="getStatusType(projectInfo.projectStatus)"
size="small"
>
{{ getStatusLabel(projectInfo.projectStatus) }}
</el-tag>
</div>
<p class="update-time">
最后更新时间{{ formatUpdateTime(projectInfo.updateTime) }}
</p>
</div>
</div>
<div class="header-right">
<el-menu
:default-active="activeTab"
mode="horizontal"
@select="handleMenuSelect"
class="nav-menu"
>
<el-menu-item index="upload">上传数据</el-menu-item>
<el-menu-item index="config">参数配置</el-menu-item>
<el-menu-item index="overview">结果总览</el-menu-item>
<el-menu-item index="special">专项排查</el-menu-item>
<el-menu-item index="detail">流水明细查询</el-menu-item>
</el-menu>
</div>
</div>
<!-- 动态组件渲染区域 -->
<component
:is="currentComponent"
:project-id="projectId"
:project-info="projectInfo"
@menu-change="handleMenuChange"
@data-uploaded="handleDataUploaded"
@name-selected="handleNameSelected"
@generate-report="handleGenerateReport"
@fetch-bank-info="handleFetchBankInfo"
/>
</div>
</template>
<script>
import UploadData from "./components/detail/UploadData";
import ParamConfig from "./components/detail/ParamConfig";
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
import SpecialCheck from "./components/detail/SpecialCheck";
import DetailQuery from "./components/detail/DetailQuery";
export default {
name: "ProjectDetail",
components: {
UploadData,
ParamConfig,
PreliminaryCheck,
SpecialCheck,
DetailQuery,
},
data() {
return {
// 当前激活的菜单项索引
activeTab: "upload",
// 当前显示的组件名称
currentComponent: "UploadData",
// 项目ID
projectId: this.$route.params.projectId,
// 项目信息
projectInfo: {
projectId: this.$route.params.projectId,
projectName: "",
projectDesc: "",
createTime: "",
updateTime: "",
startDate: "",
endDate: "",
targetCount: 0,
warningCount: 0,
warningThreshold: 60,
projectStatus: "0",
},
};
},
watch: {
"$route.params.projectId"(newId) {
if (newId) {
this.projectId = newId;
this.projectInfo.projectId = newId;
this.initPageData();
}
},
},
created() {
// 初始化页面数据
this.initPageData();
},
methods: {
/** 初始化页面数据 */
initPageData() {
// 这里应该从API获取项目详细信息
this.mockProjectInfo();
},
/** 格式化更新时间 */
formatUpdateTime(time) {
if (!time) return "-";
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
/** 模拟项目信息 */
mockProjectInfo() {
// 模拟数据实际应该调用API
this.projectInfo = {
projectId: this.projectId,
projectName: "2024年Q1初核项目",
projectDesc: "第一季度员工异常行为排查",
createTime: "2024-01-01 10:00:00",
updateTime: new Date().toISOString(),
startDate: "2024-01-01",
endDate: "2024-03-31",
targetCount: 500,
warningCount: 15,
warningThreshold: 60,
projectStatus: "0",
};
},
/** 获取状态类型 */
getStatusType(status) {
const statusMap = {
0: "primary", // 进行中
1: "success", // 已完成
2: "info", // 已归档
};
return statusMap[status] || "info";
},
/** 获取状态标签 */
getStatusLabel(status) {
const statusMap = {
0: "进行中",
1: "已完成",
2: "已归档",
};
return statusMap[status] || "未知";
},
/** 标签页切换 */
handleTabChange(tab) {
console.log("切换到标签页:", tab.name);
},
/** 返回列表页 */
handleBack() {
this.$router.push("/ccdiProject");
},
/** 菜单选择事件 */
handleMenuSelect(index) {
console.log("菜单选择:", index);
this.activeTab = index;
// 组件映射
const componentMap = {
upload: "UploadData",
config: "ParamConfig",
overview: "PreliminaryCheck",
special: "SpecialCheck",
detail: "DetailQuery",
};
this.currentComponent = componentMap[index] || "UploadData";
},
/** UploadData 组件:菜单切换 */
handleMenuChange({ key, route }) {
console.log("切换到菜单:", key, route);
// 直接触发菜单选择
this.handleMenuSelect(route);
},
/** UploadData 组件:数据上传完成 */
handleDataUploaded({ type }) {
console.log("数据上传完成:", type);
this.$message.success(`${type} 数据上传成功`);
},
/** UploadData 组件:名单选择完成 */
handleNameSelected(nameList) {
console.log("名单选择完成:", nameList);
this.$message.success("名单选择成功");
},
/** UploadData 组件:生成报告 */
handleGenerateReport() {
console.log("生成报告");
// this.$message.info("生成报告功能开发中");
},
/** UploadData 组件:拉取本行信息 */
handleFetchBankInfo() {
console.log("拉取本行信息");
this.$message.info("拉取本行信息功能开发中");
},
/** 数据上传完成 */
handleDataUploaded() {
console.log("数据上传完成");
this.$message.success("数据上传成功");
},
/** 刷新页面 */
handleRefresh() {
this.mockProjectInfo();
this.$message.success("刷新成功");
},
/** 导出报告 */
handleExport() {
console.log("导出报告");
this.$message.info("报告导出功能开发中");
},
/** 完成项目 */
handleComplete() {
this.$confirm("确定要完成当前项目吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$message.success("项目已完成");
this.projectInfo.projectStatus = "1";
})
.catch(() => {
this.$message.info("已取消");
});
},
/** 归档项目 */
handleArchive() {
this.$confirm(
"确定要归档当前项目吗?归档后将不能进行修改操作。",
"警告",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
this.$message.success("项目已归档");
this.projectInfo.projectStatus = "2";
})
.catch(() => {
this.$message.info("已取消");
});
},
},
};
</script>
<style lang="scss" scoped>
.dpc-detail-container {
padding: 16px;
background: #f0f2f5;
min-height: calc(100vh - 84px);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
.header-left {
display: flex;
align-items: flex-start;
gap: 12px;
.el-button {
padding: 8px 12px;
font-size: 13px;
margin-top: 4px;
}
.title-section {
.page-title {
display: flex;
align-items: center;
h2 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 500;
color: #303133;
margin-right: 5px;
}
}
.update-time {
margin: 0;
font-size: 13px;
color: #909399;
}
}
}
.header-right {
display: flex;
align-items: center;
.nav-menu {
// 移除默认背景色和边框
background-color: transparent;
border-bottom: none;
// 菜单项基础样式
.el-menu-item,
.el-submenu__title {
font-size: 14px;
color: #606266;
padding: 0 16px;
height: 40px;
line-height: 40px;
border-bottom: 2px solid transparent;
&:hover {
background-color: #f5f7fa;
color: #303133;
}
}
// 子菜单容器高度统一
.el-submenu {
height: 40px;
line-height: 40px;
}
// 激活状态:底部下划线 + 蓝色文字
.el-menu-item.is-active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
background-color: transparent;
}
// 下拉菜单激活状态
.el-submenu.is-active > .el-submenu__title {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
// 下拉菜单图标
.el-submenu__icon-arrow {
margin-left: 4px;
}
}
}
}
.info-card {
margin-bottom: 16px;
:deep(.el-card__body) {
padding: 20px;
}
}
.info-header {
margin-bottom: 16px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.info-content {
.info-row {
display: flex;
gap: 32px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.info-item {
display: flex;
align-items: center;
font-size: 14px;
.label {
color: #606266;
min-width: 100px;
font-weight: 500;
}
.value {
color: #303133;
font-weight: 400;
&.warning-count {
color: #e6a23c;
font-weight: 600;
}
}
}
}
.content-card {
margin-bottom: 16px;
:deep(.el-card__body) {
padding: 0;
.el-tabs {
height: 100%;
.el-tabs__content {
padding: 20px;
}
}
}
}
.action-bar {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
.el-button {
padding: 10px 20px;
i {
margin-right: 4px;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.dpc-detail-container {
padding: 8px;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.header-right {
width: 100%;
margin-top: 12px;
.nav-menu {
width: 100%;
display: flex;
justify-content: flex-start;
.el-menu-item,
.el-submenu {
flex: 1;
text-align: center;
padding: 0 8px;
font-size: 13px;
}
}
}
}
.info-content {
.info-row {
flex-direction: column;
gap: 8px;
.info-item {
.label {
min-width: auto;
}
}
}
}
.action-bar {
flex-wrap: wrap;
.el-button {
width: 100%;
}
}
}
// 下拉菜单弹窗样式
::v-deep .el-menu--popup {
min-width: 140px;
.el-menu-item {
font-size: 14px;
&:hover {
background-color: #f5f7fa;
}
&.is-active {
color: #1890ff;
background-color: #e6f7ff;
}
}
}
</style>

View File

@@ -70,14 +70,14 @@ import ImportHistoryDialog from './components/ImportHistoryDialog'
import ArchiveConfirmDialog from './components/ArchiveConfirmDialog' import ArchiveConfirmDialog from './components/ArchiveConfirmDialog'
export default { export default {
name: 'DpcProject', name: "DpcProject",
components: { components: {
SearchBar, SearchBar,
ProjectTable, ProjectTable,
QuickEntry, QuickEntry,
AddProjectDialog, AddProjectDialog,
ImportHistoryDialog, ImportHistoryDialog,
ArchiveConfirmDialog ArchiveConfirmDialog,
}, },
data() { data() {
return { return {
@@ -105,17 +105,17 @@ export default {
}, },
// 新增/编辑弹窗 // 新增/编辑弹窗
addDialogVisible: false, addDialogVisible: false,
addDialogTitle: '新建项目', addDialogTitle: "新建项目",
projectForm: {}, projectForm: {},
// 导入历史项目弹窗 // 导入历史项目弹窗
importDialogVisible: false, importDialogVisible: false,
// 归档确认弹窗 // 归档确认弹窗
archiveDialogVisible: false, archiveDialogVisible: false,
currentArchiveProject: null currentArchiveProject: null,
} };
}, },
created() { created() {
this.getList() this.getList();
}, },
methods: { methods: {
/** 查询项目列表 */ /** 查询项目列表 */
@@ -150,10 +150,10 @@ export default {
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery(queryParams) { handleQuery(queryParams) {
if (queryParams) { if (queryParams) {
this.queryParams = { ...this.queryParams, ...queryParams } this.queryParams = { ...this.queryParams, ...queryParams };
} }
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1;
this.getList() this.getList();
}, },
/** 分页事件处理 */ /** 分页事件处理 */
handlePagination(pagination) { handlePagination(pagination) {
@@ -165,26 +165,26 @@ export default {
}, },
/** 新增按钮操作 */ /** 新增按钮操作 */
handleAdd() { handleAdd() {
this.projectForm = this.getEmptyForm() this.projectForm = this.getEmptyForm();
this.addDialogTitle = '新建项目' this.addDialogTitle = "新建项目";
this.addDialogVisible = true this.addDialogVisible = true;
}, },
/** 获取空表单 */ /** 获取空表单 */
getEmptyForm() { getEmptyForm() {
return { return {
projectId: null, projectId: null,
projectName: '', projectName: "",
projectDesc: '', projectDesc: "",
startDate: '', startDate: "",
endDate: '', endDate: "",
targetCount: 0, targetCount: 0,
targetPersons: [] targetPersons: [],
} };
}, },
/** 关闭新增弹窗 */ /** 关闭新增弹窗 */
handleCloseAddDialog() { handleCloseAddDialog() {
this.addDialogVisible = false this.addDialogVisible = false;
this.projectForm = {} this.projectForm = {};
}, },
/** 提交项目表单 */ /** 提交项目表单 */
handleSubmitProject(data) { handleSubmitProject(data) {
@@ -194,66 +194,68 @@ export default {
}, },
/** 导入历史项目 */ /** 导入历史项目 */
handleImport() { handleImport() {
this.importDialogVisible = true this.importDialogVisible = true;
}, },
/** 关闭导入弹窗 */ /** 关闭导入弹窗 */
handleCloseImportDialog() { handleCloseImportDialog() {
this.importDialogVisible = false this.importDialogVisible = false;
}, },
/** 提交导入 */ /** 提交导入 */
handleSubmitImport(data) { handleSubmitImport(data) {
console.log('导入历史项目:', data) console.log("导入历史项目:", data);
this.$modal.msgSuccess('项目导入成功') this.$modal.msgSuccess("项目导入成功");
this.importDialogVisible = false this.importDialogVisible = false;
this.getList() this.getList();
}, },
/** 创建季度初核 */ /** 创建季度初核 */
handleCreateQuarterly() { handleCreateQuarterly() {
this.projectForm = this.getEmptyForm() this.projectForm = this.getEmptyForm();
this.addDialogTitle = '创建季度初核项目' this.addDialogTitle = "创建季度初核项目";
this.addDialogVisible = true this.addDialogVisible = true;
}, },
/** 创建新员工排查 */ /** 创建新员工排查 */
handleCreateEmployee() { handleCreateEmployee() {
this.projectForm = this.getEmptyForm() this.projectForm = this.getEmptyForm();
this.addDialogTitle = '创建新员工排查项目' this.addDialogTitle = "创建新员工排查项目";
this.addDialogVisible = true this.addDialogVisible = true;
}, },
/** 创建高风险专项 */ /** 创建高风险专项 */
handleCreateHighRisk() { handleCreateHighRisk() {
this.projectForm = this.getEmptyForm() this.projectForm = this.getEmptyForm();
this.addDialogTitle = '创建高风险专项项目' this.addDialogTitle = "创建高风险专项项目";
this.addDialogVisible = true this.addDialogVisible = true;
}, },
/** 进入项目 */ /** 进入项目 */
handleEnter(row) { handleEnter(row) {
console.log('进入项目:', row) this.$router.push({
this.$modal.msgSuccess('进入项目: ' + row.projectName) path: `ccdiProject/detail/${row.projectId}`,
});
// this.$modal.msgSuccess("进入项目: " + row.projectName);
}, },
/** 查看结果 */ /** 查看结果 */
handleViewResult(row) { handleViewResult(row) {
console.log('查看结果:', row) console.log("查看结果:", row);
this.$modal.msgInfo('查看项目结果: ' + row.projectName) this.$modal.msgInfo("查看项目结果: " + row.projectName);
}, },
/** 重新分析 */ /** 重新分析 */
handleReAnalyze(row) { handleReAnalyze(row) {
console.log('重新分析:', row) console.log("重新分析:", row);
this.$modal.msgSuccess('正在重新分析项目: ' + row.projectName) this.$modal.msgSuccess("正在重新分析项目: " + row.projectName);
}, },
/** 归档项目 */ /** 归档项目 */
handleArchive(row) { handleArchive(row) {
this.currentArchiveProject = row this.currentArchiveProject = row;
this.archiveDialogVisible = true this.archiveDialogVisible = true;
}, },
/** 确认归档 */ /** 确认归档 */
handleConfirmArchive(data) { handleConfirmArchive(data) {
console.log('确认归档:', data) console.log("确认归档:", data);
this.$modal.msgSuccess('项目已归档') this.$modal.msgSuccess("项目已归档");
this.archiveDialogVisible = false this.archiveDialogVisible = false;
this.getList() this.getList();
} },
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>