立项配置页初步优化
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "9f9bb57a-6de8-4e31-a7a4-e0c0e7c0d119",
|
||||
"name": "CTMS Local",
|
||||
"values": [
|
||||
{ "key": "base_url", "value": "http://localhost", "type": "default", "enabled": true },
|
||||
{ "key": "email", "value": "admin@example.com", "type": "default", "enabled": true },
|
||||
{ "key": "password", "value": "admin123", "type": "secret", "enabled": true },
|
||||
{ "key": "study_id", "value": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "type": "default", "enabled": true },
|
||||
{ "key": "token", "value": "", "type": "default", "enabled": true },
|
||||
{ "key": "setup_version", "value": "1", "type": "default", "enabled": true },
|
||||
{ "key": "target_total", "value": "123", "type": "default", "enabled": true },
|
||||
{ "key": "plan_start_date", "value": "2026-02-01", "type": "default", "enabled": true },
|
||||
{ "key": "plan_end_date", "value": "2026-12-31", "type": "default", "enabled": true },
|
||||
{
|
||||
"key": "exported_data",
|
||||
"value": "{\"projectMilestones\":[],\"enrollmentPlan\":{\"totalTarget\":0,\"startDate\":\"\",\"endDate\":\"\",\"monthlyGoalNote\":\"\",\"stageBreakdown\":\"\"},\"siteMilestones\":[],\"siteEnrollmentPlans\":[],\"monitoringStrategies\":[],\"centerConfirm\":[]}",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment",
|
||||
"_postman_exported_at": "2026-02-09T00:00:00.000Z",
|
||||
"_postman_exported_using": "Codex"
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "CTMS Setup Config API (Excel)",
|
||||
"description": "立项配置接口联调集合(登录 -> 获取 -> 保存 -> 发布 -> 导出Excel -> 导入Excel)",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 登录(获取 Token)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"{{email}}\",\n \"password\": \"{{password}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/login",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.collectionVariables.set('token', json.access_token || '');"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. 获取立项配置",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/studies/{{study_id}}/setup-config",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "studies", "{{study_id}}", "setup-config"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.collectionVariables.set('setup_version', String(json.version || 1));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. 保存草稿",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" },
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"expected_version\": {{setup_version}},\n \"data\": {\n \"projectMilestones\": [],\n \"enrollmentPlan\": {\n \"totalTarget\": {{target_total}},\n \"startDate\": \"{{plan_start_date}}\",\n \"endDate\": \"{{plan_end_date}}\",\n \"monthlyGoalNote\": \"Postman save draft\",\n \"stageBreakdown\": \"phase-1\"\n },\n \"siteMilestones\": [],\n \"siteEnrollmentPlans\": [],\n \"monitoringStrategies\": [],\n \"centerConfirm\": []\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/studies/{{study_id}}/setup-config",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "studies", "{{study_id}}", "setup-config"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.collectionVariables.set('setup_version', String(json.version || pm.collectionVariables.get('setup_version') || '1'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. 发布配置",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" },
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"expected_version\": {{setup_version}}\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/studies/{{study_id}}/setup-config/publish",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "studies", "{{study_id}}", "setup-config", "publish"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.test('publish status is PUBLISHED', function () { pm.expect(json.publish_status).to.eql('PUBLISHED'); });",
|
||||
"pm.collectionVariables.set('setup_version', String(json.version || pm.collectionVariables.get('setup_version') || '1'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "5. 导出配置 Excel",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/studies/{{study_id}}/setup-config/export-excel",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "studies", "{{study_id}}", "setup-config", "export-excel"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "6. 导入配置 Excel",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{ "key": "expected_version", "value": "{{setup_version}}", "type": "text" },
|
||||
{ "key": "file", "type": "file", "src": "{{excel_file_path}}" }
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/studies/{{study_id}}/setup-config/import-excel",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "studies", "{{study_id}}", "setup-config", "import-excel"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.collectionVariables.set('setup_version', String(json.version || pm.collectionVariables.get('setup_version') || '1'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{ "key": "base_url", "value": "http://localhost" },
|
||||
{ "key": "email", "value": "admin@example.com" },
|
||||
{ "key": "password", "value": "admin123" },
|
||||
{ "key": "study_id", "value": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
|
||||
{ "key": "token", "value": "" },
|
||||
{ "key": "setup_version", "value": "1" },
|
||||
{ "key": "target_total", "value": "123" },
|
||||
{ "key": "plan_start_date", "value": "2026-02-01" },
|
||||
{ "key": "plan_end_date", "value": "2026-12-31" },
|
||||
{ "key": "excel_file_path", "value": "" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Release Checklist(立项配置)
|
||||
|
||||
## 1. 构建与迁移
|
||||
- [ ] `cd backend && docker compose run --rm backend python -m alembic upgrade head`
|
||||
- [ ] `cd backend && python3 -m compileall app scripts`
|
||||
- [ ] `cd frontend && npm run build`
|
||||
|
||||
## 2. 接口与权限回归
|
||||
- [ ] ADMIN 账号可 `GET/PUT/PUBLISH/IMPORT-EXCEL/EXPORT-EXCEL`
|
||||
- [ ] PM 账号可 `GET/PUT/PUBLISH/IMPORT-EXCEL/EXPORT-EXCEL`
|
||||
- [ ] CRA 账号可 `GET/EXPORT-EXCEL`,`PUT/PUBLISH/IMPORT-EXCEL` 返回 `403`
|
||||
- [ ] 非项目成员访问返回 `403`
|
||||
|
||||
## 3. 并发冲突回归
|
||||
- [ ] 双开两个窗口同项目配置
|
||||
- [ ] A 保存后,B 再保存触发 `409`
|
||||
- [ ] 前端显示冲突对比弹窗(可读 diff + JSON)
|
||||
- [ ] 选择“使用服务器版本”后版本同步
|
||||
- [ ] 选择“用本地草稿覆盖”后保存成功且版本递增
|
||||
- [ ] `409` 响应结构包含 `detail.code=SETUP_CONFIG_VERSION_CONFLICT`
|
||||
|
||||
## 3.1 业务校验回归(422)
|
||||
- [ ] `enrollmentPlan.startDate > endDate` 返回 `422`
|
||||
- [ ] 非本项目 `siteId` 返回 `422`
|
||||
- [ ] `centerConfirm` 状态为“已确认”但无 `confirmDate` 返回 `422`
|
||||
- [ ] `422` 响应包含 `detail.errors[].field` 与 `detail.errors[].message`
|
||||
|
||||
## 4. 发布快照回归
|
||||
- [ ] 草稿保存后 `publish_status=DRAFT`
|
||||
- [ ] 发布后 `publish_status=PUBLISHED`
|
||||
- [ ] 发布后可切换“已发布快照”并只读
|
||||
- [ ] 再修改草稿后状态回到 `DRAFT`
|
||||
|
||||
## 5. 导入导出回归
|
||||
- [ ] 导出 Excel 文件可打开且结构完整
|
||||
- [ ] 导入有效 Excel 成功,页面数据刷新
|
||||
- [ ] 导入非法 Excel 给出错误提示
|
||||
- [ ] 导入冲突时触发 `409` 提示并刷新版本
|
||||
|
||||
## 6. 锁定与审计回归
|
||||
- [ ] 项目锁定后 `PUT/PUBLISH/IMPORT-EXCEL` 返回 `403`
|
||||
- [ ] 页面进入只读态,编辑按钮不可用
|
||||
- [ ] 审计日志可按对象类型筛选 `立项配置`
|
||||
- [ ] 审计事件包含:
|
||||
- [ ] `CREATE_SETUP_CONFIG`
|
||||
- [ ] `UPDATE_SETUP_CONFIG`
|
||||
- [ ] `PUBLISH_SETUP_CONFIG`
|
||||
- [ ] `IMPORT_SETUP_CONFIG_EXCEL`
|
||||
|
||||
## 7. 监控埋点回归
|
||||
- [ ] 访问 `GET /health/setup-config-stats` 可返回 JSON
|
||||
- [ ] `by_endpoint` 中可看到 `setup-config` 各接口 `2xx/4xx/5xx` 计数
|
||||
- [ ] 人为制造一次冲突或锁定写入后,`4xx` 计数增加
|
||||
|
||||
## 8. 冒烟脚本
|
||||
- [ ] `cd backend && docker compose exec -T backend python scripts/smoke_setup_config.py`
|
||||
- [ ] 使用 `BASE_URL/EMAIL/PASSWORD/STUDY_ID` 环境变量执行一次(覆盖指定项目)
|
||||
- [ ] `BASE_URL=http://localhost EMAIL=... PASSWORD=... STUDY_ID=... bash docs/setup-config-curl-smoke.sh`
|
||||
- [ ] 导入并执行 Postman 集合 `docs/postman/setup-config.postman_collection.json`
|
||||
|
||||
## 9. 发布前确认
|
||||
- [ ] 文档已更新:`docs/setup-config-api.md`
|
||||
- [ ] 本清单已全量勾选
|
||||
- [ ] 发布说明已包含新增接口与权限规则
|
||||
@@ -0,0 +1,180 @@
|
||||
# 立项配置接口说明(Excel 导入导出版)
|
||||
|
||||
## 1. 概览
|
||||
- 基础路径: `/api/v1/studies/{study_id}/setup-config`
|
||||
- 数据作用域: 单项目唯一配置(`study_setup_configs.study_id` 唯一约束)
|
||||
- 存储格式: JSONB(字段结构与前端 `SetupConfigDraft` 对齐)
|
||||
- 并发控制: 乐观锁(`expected_version`)
|
||||
- 发布机制: 草稿/已发布(`publish_status`)
|
||||
- 前端视图语义:
|
||||
- `草稿视图`: 当前草稿可编辑
|
||||
- `发布预览`: 当前草稿只读预览(不直接读取 `published_data`)
|
||||
- `published_data`: 用于发布快照、差异比较与版本能力
|
||||
|
||||
## 2. 权限矩阵
|
||||
- `GET /setup-config`
|
||||
- 允许: 项目成员、管理员
|
||||
- `PUT /setup-config`
|
||||
- 允许: PM、ADMIN
|
||||
- 限制: 项目锁定时返回 `403`
|
||||
- `POST /setup-config/publish`
|
||||
- 允许: PM、ADMIN
|
||||
- 限制: 项目锁定时返回 `403`
|
||||
- `GET /setup-config/export-excel`
|
||||
- 允许: 项目成员、管理员
|
||||
- `POST /setup-config/import-excel`
|
||||
- 允许: PM、ADMIN
|
||||
- 限制: 项目锁定时返回 `403`
|
||||
|
||||
## 3. 数据结构
|
||||
|
||||
### 3.1 请求体(保存)
|
||||
```json
|
||||
{
|
||||
"expected_version": 3,
|
||||
"data": {
|
||||
"projectMilestones": [
|
||||
{
|
||||
"id": "m1",
|
||||
"name": "首例入组",
|
||||
"planDate": "2026-03-01",
|
||||
"owner": "项目经理",
|
||||
"remark": "",
|
||||
"status": "未开始"
|
||||
}
|
||||
],
|
||||
"enrollmentPlan": {
|
||||
"totalTarget": 100,
|
||||
"startDate": "2026-02-01",
|
||||
"endDate": "2026-12-31",
|
||||
"monthlyGoalNote": "按月拆解",
|
||||
"stageBreakdown": "阶段1/2/3"
|
||||
},
|
||||
"siteMilestones": [],
|
||||
"siteEnrollmentPlans": [],
|
||||
"monitoringStrategies": [],
|
||||
"centerConfirm": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 响应体(GET/PUT/PUBLISH/IMPORT-EXCEL)
|
||||
```json
|
||||
{
|
||||
"id": "f0ac2fca-5fd3-470e-8e7e-2ded2b8f0f77",
|
||||
"study_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"version": 6,
|
||||
"publish_status": "PUBLISHED",
|
||||
"data": {
|
||||
"projectMilestones": [],
|
||||
"enrollmentPlan": {
|
||||
"totalTarget": 120,
|
||||
"startDate": "2026-02-01",
|
||||
"endDate": "2026-12-31",
|
||||
"monthlyGoalNote": "",
|
||||
"stageBreakdown": ""
|
||||
},
|
||||
"siteMilestones": [],
|
||||
"siteEnrollmentPlans": [],
|
||||
"monitoringStrategies": [],
|
||||
"centerConfirm": []
|
||||
},
|
||||
"published_data": null,
|
||||
"saved_by": "11111111-1111-1111-1111-111111111111",
|
||||
"saved_by_name": "System Admin",
|
||||
"published_by": null,
|
||||
"published_by_name": null,
|
||||
"published_at": null,
|
||||
"created_at": "2026-02-09T01:30:00.000000Z",
|
||||
"updated_at": "2026-02-09T02:11:13.000000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 接口定义
|
||||
|
||||
### 4.1 GET 获取配置
|
||||
```http
|
||||
GET /api/v1/studies/{study_id}/setup-config
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 4.2 PUT 保存配置(草稿)
|
||||
```http
|
||||
PUT /api/v1/studies/{study_id}/setup-config
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 4.3 POST 发布配置
|
||||
```http
|
||||
POST /api/v1/studies/{study_id}/setup-config/publish
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 4.4 GET 导出 Excel
|
||||
```http
|
||||
GET /api/v1/studies/{study_id}/setup-config/export-excel
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
说明:
|
||||
- 返回 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
||||
- 文件可直接作为导入模板继续回灌
|
||||
|
||||
### 4.5 POST 导入 Excel
|
||||
```http
|
||||
POST /api/v1/studies/{study_id}/setup-config/import-excel
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
表单字段:
|
||||
- `file`: `.xlsx` 文件(必填)
|
||||
- `expected_version`: 当前配置版本(可选,建议携带)
|
||||
|
||||
说明:
|
||||
- 导入会覆盖当前草稿并自增版本。
|
||||
- 写入审计事件: `IMPORT_SETUP_CONFIG_EXCEL`。
|
||||
|
||||
## 5. 错误码与典型场景
|
||||
- `400 BAD REQUEST`
|
||||
- 上传文件非 `.xlsx`
|
||||
- Excel 解析失败
|
||||
- `403 FORBIDDEN`
|
||||
- 非 PM/ADMIN 导入/保存/发布
|
||||
- 项目已锁定
|
||||
- `404 NOT FOUND`
|
||||
- study 不存在
|
||||
- `409 CONFLICT`
|
||||
- `expected_version` 与服务端不一致
|
||||
- `422 UNPROCESSABLE ENTITY`
|
||||
- Excel 数据通过解析后,业务字段校验失败
|
||||
|
||||
## 6. 联调脚本(curl)
|
||||
- 脚本路径: `docs/setup-config-curl-smoke.sh`
|
||||
- 使用方式:
|
||||
```bash
|
||||
chmod +x docs/setup-config-curl-smoke.sh
|
||||
BASE_URL=http://localhost \
|
||||
EMAIL=admin@example.com \
|
||||
PASSWORD=admin123 \
|
||||
STUDY_ID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \
|
||||
bash docs/setup-config-curl-smoke.sh
|
||||
```
|
||||
|
||||
## 7. Postman 联调集合
|
||||
- Collection: `docs/postman/setup-config.postman_collection.json`
|
||||
- Environment: `docs/postman/local.postman_environment.json`
|
||||
|
||||
## 8. 后端烟雾测试(非 pytest)
|
||||
```bash
|
||||
docker compose up -d db backend
|
||||
docker compose exec -T backend python scripts/smoke_setup_config.py
|
||||
```
|
||||
|
||||
## 9. 监控埋点(4xx/5xx)
|
||||
- 中间件统计以下接口:
|
||||
- `GET /api/v1/studies/{study_id}/setup-config`
|
||||
- `PUT /api/v1/studies/{study_id}/setup-config`
|
||||
- `POST /api/v1/studies/{study_id}/setup-config/publish`
|
||||
- `GET /api/v1/studies/{study_id}/setup-config/export-excel`
|
||||
- `POST /api/v1/studies/{study_id}/setup-config/import-excel`
|
||||
@@ -0,0 +1,74 @@
|
||||
# 立项配置代码审计报告(2026-02)
|
||||
|
||||
## 范围
|
||||
- 前端:`frontend/src/views/admin/ProjectDetail.vue`、`frontend/src/api/studies.ts`、`frontend/src/types/setupConfig.ts`、`frontend/src/utils/setupFieldLocator.ts`
|
||||
- 后端:`backend/app/api/v1/studies.py`、`backend/app/crud/study_setup_config.py`、`backend/app/schemas/study_setup_config.py`、`backend/app/services/setup_config_excel.py`
|
||||
- 文档:`docs/setup-config-api.md`、`docs/setup-config-curl-smoke.sh`、`docs/postman/setup-config.postman_collection.json`、`backend/scripts/smoke_setup_config.py`
|
||||
|
||||
## 发现清单
|
||||
|
||||
### 严重(会导致错误或明显行为偏差)
|
||||
- 无新增严重问题。
|
||||
|
||||
### 中等(会导致交互不一致或维护风险)
|
||||
1. `ProjectDetail.vue` 中存在大量 `v-if="false"` 历史分支(Step4-7),增加阅读成本并容易引发误改。
|
||||
2. 步骤表格从“输入态表格”转“展示态 + 抽屉编辑”后,存在未清理的旧方法与旧样式残留,导致逻辑路径复杂。
|
||||
3. “发布预览”语义已切换为“当前草稿只读预览”,但文档未明确,容易与后端 `published_data` 概念混淆。
|
||||
|
||||
### 低(代码异味/重复/可读性)
|
||||
1. `ProjectDetail.vue` 文件过长(约 3800+ 行),后续改动冲突概率高。
|
||||
2. 多个步骤编辑抽屉逻辑重复,适合后续抽象为通用 `RowEditorDrawer`。
|
||||
3. 表格列模板重复,适合后续改为列配置驱动。
|
||||
|
||||
## 本轮已处理
|
||||
1. 删除 Step4-7 全部 `v-if="false"` 模板分支。
|
||||
2. 删除上述分支遗留的无用方法:
|
||||
- `handleSiteMilestoneSiteChange`
|
||||
- `handleSiteEnrollmentSiteChange`
|
||||
- `handleCenterConfirmSiteChange`
|
||||
- `touchStrategy`
|
||||
- `applyStrategy`
|
||||
3. 清理无用样式:
|
||||
- `.section-toolbar`
|
||||
- `.setup-content.is-published-preview .section-toolbar` 相关规则
|
||||
4. 保持现有交互行为不变并完成构建验证。
|
||||
5. 修复步骤标题行按钮保护不一致问题:
|
||||
- 第2/4/5/6/7步标题行功能键增加 `:disabled="!canEditSetup"`,避免锁定项目或无权限时误触发。
|
||||
- 对应新增/删除/抽屉打开与保存方法统一加守卫,防止外部调用绕过前端可见状态。
|
||||
6. 收敛重复的草稿可编辑判定,新增 `canMutateDraft()` 供步骤行编辑方法复用(不改变界面行为)。
|
||||
7. 抽取步骤行编辑通用控制器(`IndexedEditorController`)与通用函数:
|
||||
- `openIndexedEditor`
|
||||
- `getEditingIndex`
|
||||
- `closeIndexedEditor`
|
||||
复用到 Step2/4/5/6/7 的单行抽屉编辑逻辑,减少重复代码。
|
||||
8. 合并重复 `onMounted` 注册逻辑,统一在单一入口完成 `loadProject` 与 `beforeunload` 监听初始化。
|
||||
9. 新增 `frontend/src/composables/useSetupConfig.ts`:
|
||||
- 将 setup-config 相关 API 编排从页面中收口到 composable(获取/保存/发布/版本列表/回滚/版本删除/Excel 导入导出)。
|
||||
- `ProjectDetail.vue` 改为依赖 composable 暴露的方法,降低页面对 API 层的直接耦合。
|
||||
10. 新增 `frontend/src/utils/setupDiffRows.ts`:
|
||||
- 将“步骤差异明细”计算与字段路径可读化逻辑从 `ProjectDetail.vue` 抽离为工具函数。
|
||||
- 页面保留项目主字段差异拼装,步骤差异统一由工具输出,降低页面认知负担。
|
||||
11. 将步骤标题行按钮显隐规则收敛为 `stepActionMode` 计算属性:
|
||||
- 去除模板中多段 `activeStep === n && !isPublishedView` 分支。
|
||||
- 保持按钮行为与权限控制不变,降低后续步骤扩展的修改面。
|
||||
|
||||
## 验证结果
|
||||
1. `frontend npm run build` 通过。
|
||||
2. 后端 setup-config 核心文件通过 AST 语法解析检查。
|
||||
3. 当前环境 `npx tsc --noEmit` 失败主要来自项目既有依赖类型版本不匹配(`vue`/`element-plus`/`csstype`),非本轮立项配置改动引入。
|
||||
|
||||
## 一致性核对结果
|
||||
1. 前后端 setup-config 路由一致:
|
||||
- `GET/PUT /setup-config`
|
||||
- `POST /setup-config/publish`
|
||||
- `GET /setup-config/versions`
|
||||
- `POST /setup-config/rollback`
|
||||
- `DELETE /setup-config/versions/{target_version}`
|
||||
- `GET /setup-config/export-excel`
|
||||
- `POST /setup-config/import-excel`
|
||||
2. 前端 `setupConfig` 类型与后端 schema 字段一致(`published_data` 仍保留用于发布快照与差异比较)。
|
||||
|
||||
## 下一步建议(不改行为)
|
||||
1. 拆分 `ProjectDetail.vue` 为步骤子组件(先拆 Step4-7)。
|
||||
2. 抽象通用行编辑抽屉组件,统一校验和保存流程。
|
||||
3. 使用列配置对象复用 `el-table-column` 渲染,降低重复模板。
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost}"
|
||||
EMAIL="${EMAIL:-admin@example.com}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
STUDY_ID="${STUDY_ID:-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}"
|
||||
|
||||
echo "[1/6] login: $EMAIL"
|
||||
TOKEN=$(curl -sS -X POST "$BASE_URL/api/v1/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | \
|
||||
python3 -c 'import json,sys; print(json.load(sys.stdin).get("access_token",""))')
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "login failed: access_token empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUTH=(-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
echo "[2/6] get setup-config"
|
||||
GET_RESP=$(curl -sS "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config" "${AUTH[@]}")
|
||||
VERSION=$(echo "$GET_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "get setup-config failed"
|
||||
echo "$GET_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION"
|
||||
|
||||
echo "[3/6] save draft"
|
||||
PUT_PAYLOAD=$(cat <<JSON
|
||||
{
|
||||
"expected_version": $VERSION,
|
||||
"data": {
|
||||
"projectMilestones": [],
|
||||
"enrollmentPlan": {
|
||||
"totalTarget": 123,
|
||||
"startDate": "2026-02-01",
|
||||
"endDate": "2026-12-31",
|
||||
"monthlyGoalNote": "curl smoke",
|
||||
"stageBreakdown": "phase-1"
|
||||
},
|
||||
"siteMilestones": [],
|
||||
"siteEnrollmentPlans": [],
|
||||
"monitoringStrategies": [],
|
||||
"centerConfirm": []
|
||||
}
|
||||
}
|
||||
JSON
|
||||
)
|
||||
PUT_RESP=$(curl -sS -X PUT "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config" \
|
||||
"${AUTH[@]}" -H 'Content-Type: application/json' -d "$PUT_PAYLOAD")
|
||||
VERSION=$(echo "$PUT_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "save draft failed"
|
||||
echo "$PUT_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "saved version=$VERSION"
|
||||
|
||||
echo "[4/6] publish"
|
||||
PUB_RESP=$(curl -sS -X POST "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/publish" \
|
||||
"${AUTH[@]}" -H 'Content-Type: application/json' -d "{\"expected_version\":$VERSION}")
|
||||
PUB_STATUS=$(echo "$PUB_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("publish_status",""))')
|
||||
VERSION=$(echo "$PUB_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
|
||||
if [[ "$PUB_STATUS" != "PUBLISHED" ]]; then
|
||||
echo "publish failed"
|
||||
echo "$PUB_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "publish_status=$PUB_STATUS version=$VERSION"
|
||||
|
||||
echo "[5/6] export excel"
|
||||
EXCEL_FILE="/tmp/setup-config-export.xlsx"
|
||||
curl -sS "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/export-excel" "${AUTH[@]}" -o "$EXCEL_FILE"
|
||||
if [[ ! -s "$EXCEL_FILE" ]]; then
|
||||
echo "export excel failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "export saved: $EXCEL_FILE"
|
||||
|
||||
echo "[6/6] import excel (re-import export file)"
|
||||
IMP_RESP=$(curl -sS -X POST "$BASE_URL/api/v1/studies/$STUDY_ID/setup-config/import-excel" \
|
||||
"${AUTH[@]}" \
|
||||
-F "expected_version=$VERSION" \
|
||||
-F "file=@$EXCEL_FILE;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
IMP_VER=$(echo "$IMP_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))')
|
||||
if [[ -z "$IMP_VER" ]]; then
|
||||
echo "import failed"
|
||||
echo "$IMP_RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo "import version=$IMP_VER"
|
||||
|
||||
echo "setup-config curl smoke passed"
|
||||
Reference in New Issue
Block a user