feat: refactor monolithic App.vue into modular Vue component architecture

- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
This commit is contained in:
2026-04-28 17:20:52 +08:00
commit 7141e1d11a
40 changed files with 10133 additions and 0 deletions

View File

@@ -0,0 +1,553 @@
# Phase 6: 测试与打磨W7-W8
> **目标:** 完善集成测试、E2E 测试、修复 Bug、UI 打磨、编写部署文档,准备 Demo 演示。
> **周期:** 第 7 ~ 8 周
> **任务数:** 4 个
> **可并行:** Task 6.1 / 6.2 / 6.3 可并行
> **前置依赖:** Phase 5
---
## 本阶段交付物
| 交付物 | 说明 |
|---|---|
| 后端集成测试 | 完整报销流程的自动化测试 |
| 前端 E2E 测试 | Playwright 自动化测试(可选) |
| Bug 修复 + UI 打磨 | 视觉和交互优化 |
| 部署文档 | README + 部署指南 + API 文档 |
---
## 任务清单
### Task 6.1: 后端集成测试
**负责人:** 后端工程师 A
**预计工时:** 3 天
**前置依赖:** Phase 5
**Files:**
- Create: `backend/tests/test_integration_flow.py`
- Create: `backend/tests/helpers.py`(测试辅助函数)
- Modify: `backend/tests/conftest.py`(添加测试数据库 fixture
- [ ] **Step 1: 更新 conftest.py 添加测试数据库 fixture**
`backend/tests/conftest.py`:
```python
import pytest
import asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.models.base import Base
from app.main import app
from app.core.database import get_db
# 测试数据库 URL
TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/x_financial_test"
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
test_session = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture(autouse=True)
async def setup_database():
"""每个测试前创建表,测试后清理"""
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
async def db():
async with test_session() as session:
yield session
@pytest.fixture
async def client(db):
async def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
```
- [ ] **Step 2: 编写测试辅助函数**
`backend/tests/helpers.py`:
```python
from httpx import AsyncClient
async def create_task(client: AsyncClient, user_id="U001", company_id="C001", intent="报北京出差费用") -> dict:
resp = await client.post("/api/v1/reimbursement/tasks", json={
"user_id": user_id, "company_id": company_id, "user_intent": intent
})
assert resp.status_code == 201
return resp.json()
async def upload_document(client: AsyncClient, task_id: str, document_type: str, filename: str = "test.pdf") -> dict:
files = {"file": (filename, b"fake file content", "application/pdf")}
resp = await client.post(
f"/api/v1/reimbursement/tasks/{task_id}/documents",
files=files,
data={"document_type": document_type}
)
assert resp.status_code == 201
return resp.json()
async def run_agent(client: AsyncClient, task_id: str, start_from="intake") -> dict:
resp = await client.post(
f"/api/v1/reimbursement/tasks/{task_id}/agent/run",
json={"start_from": start_from, "mode": "precheck"}
)
assert resp.status_code == 200
return resp.json()
async def get_draft(client: AsyncClient, task_id: str) -> dict:
resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/draft")
assert resp.status_code == 200
return resp.json()
async def get_precheck_result(client: AsyncClient, task_id: str) -> dict:
resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/precheck-result")
assert resp.status_code == 200
return resp.json()
async def respond_supplement(client: AsyncClient, task_id: str, supplement_id: str, text: str) -> dict:
resp = await client.post(
f"/api/v1/reimbursement/tasks/{task_id}/supplements",
json={"supplement_request_id": supplement_id, "response_text": text}
)
assert resp.status_code == 200
return resp.json()
async def submit_task(client: AsyncClient, task_id: str) -> dict:
resp = await client.post(
f"/api/v1/reimbursement/tasks/{task_id}/submit",
json={"confirmed": True, "submit_to": "expense_system"}
)
assert resp.status_code == 200
return resp.json()
async def get_sync_status(client: AsyncClient, task_id: str) -> dict:
resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/sync-status")
assert resp.status_code == 200
return resp.json()
```
- [ ] **Step 3: 编写完整流程集成测试**
`backend/tests/test_integration_flow.py`:
```python
import pytest
from tests.helpers import *
@pytest.mark.asyncio
async def test_full_reimbursement_flow(client):
"""完整报销流程:创建→上传→识别→草稿→预审→补件→提交→同步"""
# 1. 创建任务
task = await create_task(client, intent="我要报这次北京出差的费用")
task_id = task["task_id"]
assert task["status"] == "material_collecting"
# 2. 上传票据
doc1 = await upload_document(client, task_id, "vat_invoice", "invoice.pdf")
assert doc1["ocr_status"] == "pending"
doc2 = await upload_document(client, task_id, "train_ticket", "train.pdf")
assert doc2["ocr_status"] == "pending"
doc3 = await upload_document(client, task_id, "hotel_bill", "hotel.pdf")
# 3. 启动 Agent使用 mock OCR
result = await run_agent(client, task_id, start_from="intake")
assert result["status"] in ["draft_generated", "prechecking", "need_supplement", "pending_user_confirm"]
# 4. 获取草稿
draft = await get_draft(client, task_id)
assert draft["reimbursement_id"] is not None
assert len(draft["items"]) > 0
assert draft["total_amount"] > 0
# 5. 获取预审结果
precheck = await get_precheck_result(client, task_id)
assert "risk_level" in precheck
assert "precheck_status" in precheck
assert "rule_hits" in precheck
# 6. 如果需要补件
if precheck["precheck_status"] == "need_supplement":
# 找到需要补件的规则
for hit in precheck["rule_hits"]:
if hit["action"] == "require_attachment":
# 补充附件
await upload_document(client, task_id, "hotel_bill", "hotel_supplement.pdf")
await respond_supplement(client, task_id, hit.get("id", "S001"), "已补充酒店流水")
# 重新预审
await run_agent(client, task_id, start_from="precheck")
precheck2 = await get_precheck_result(client, task_id)
# 7. 确认提交
submit = await submit_task(client, task_id)
assert submit["status"] == "submitting"
# 8. 检查同步状态
sync = await get_sync_status(client, task_id)
assert sync["sync_status"] in ["success", "pending"]
@pytest.mark.asyncio
async def test_create_task_without_intent(client):
"""测试不提供意图时创建任务"""
resp = await client.post("/api/v1/reimbursement/tasks", json={
"user_id": "U001", "company_id": "C001"
})
assert resp.status_code == 422 # Validation error
@pytest.mark.asyncio
async def test_get_nonexistent_task(client):
"""测试查询不存在的任务"""
resp = await client.get("/api/v1/reimbursement/tasks/nonexistent-id")
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_list_tasks_pagination(client):
"""测试任务列表分页"""
# 创建多个任务
for i in range(5):
await create_task(client, intent=f"test task {i}")
# 测试分页
resp = await client.get("/api/v1/reimbursement/tasks?page=1&size=3")
assert resp.status_code == 200
data = resp.json()
assert data["total"] >= 5
assert len(data["items"]) <= 3
```
- [ ] **Step 4: 编写规则引擎集成测试**
测试每条规则对真实报销数据的命中情况:
- 住宿费超标 → 命中 `TRAVEL_HOTEL_LIMIT`
- 缺少酒店流水 → 命中 `HOTEL_BILL_REQUIRED`
- 重复发票 → 命中 `DUPLICATE_INVOICE_CHECK`
- 合规报销 → 无命中
- [ ] **Step 5: 确保所有测试通过**
Run: `cd backend && pytest tests/ -v --tb=short`
Expected: All PASS
- [ ] **Step 6: Commit**
```bash
git add backend/
git commit -m "test: 添加完整报销流程集成测试"
```
---
### Task 6.2: 前端 E2E 测试(可选)
**负责人:** 前端工程师
**预计工时:** 2 天
**前置依赖:** Phase 5
**可并行于:** Task 6.1、6.3
**Files:**
- Create: `frontend/e2e/reimbursement.spec.ts`
- Create: `frontend/playwright.config.ts`
- [ ] **Step 1: 安装 Playwright**
```bash
cd frontend
npm install -D @playwright/test
npx playwright install chromium
```
- [ ] **Step 2: 配置 Playwright**
`frontend/playwright.config.ts`:
```typescript
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
baseURL: 'http://localhost:5173',
use: {
headless: true,
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: true,
},
})
```
- [ ] **Step 3: 编写核心流程 E2E 测试**
`frontend/e2e/reimbursement.spec.ts`:
```typescript
import { test, expect } from '@playwright/test'
test('完整报销流程', async ({ page }) => {
// 1. 访问首页
await page.goto('/')
await expect(page.locator('h1')).toContainText('报销')
// 2. 输入报销意图
await page.fill('input[placeholder*="报销"]', '我要报这次北京出差的费用')
await page.click('button:has-text("提交")')
// 3. 跳转到上传页
await expect(page).toHaveURL(/\/task\/.*\/upload/)
// 4. 上传文件
await page.setInputFiles('input[type="file"]', 'tests/fixtures/invoice.pdf')
// 5. 选择票据类型
await page.selectOption('select', 'vat_invoice')
// 6. 开始识别
await page.click('button:has-text("开始识别")')
// 7. 跳转到草稿页
await expect(page).toHaveURL(/\/draft/, { timeout: 30000 })
// 8. 执行预审
await page.click('button:has-text("执行预审")')
// 9. 跳转到预审结果页
await expect(page).toHaveURL(/\/precheck/, { timeout: 30000 })
})
```
- [ ] **Step 4: 运行 E2E 测试**
Run: `cd frontend && npx playwright test`
Expected: All PASS
- [ ] **Step 5: Commit**
```bash
git add frontend/
git commit -m "test: 添加前端 E2E 测试"
```
---
### Task 6.3: Bug 修复与 UI 打磨
**负责人:** 全员参与
**预计工时:** 3 天
**前置依赖:** Phase 5
**可并行于:** Task 6.1、6.2
- [ ] **Step 1: UI 走查清单**
逐页面检查:
| 页面 | 检查项 |
|---|---|
| 首页 | 布局、输入框交互、快捷按钮、最近任务列表 |
| 上传页 | 拖拽上传、文件预览、票据类型选择、进度条 |
| 草稿页 | 表格编辑、金额汇总、附件预览、预审按钮 |
| 预审结果页 | 结论卡片、风险项展示、规则命中详情 |
| 补件页 | 补件清单、上传/回复交互、提交反馈 |
| 确认页 | 摘要展示、同步状态轮询、成功/失败状态 |
| 审计日志页 | 时间线展示、筛选功能 |
- [ ] **Step 2: 修复共性问题**
- [ ] 响应式布局适配1280px / 1024px / 768px 断点)
- [ ] Loading 状态:所有异步操作加 loading 指示器
- [ ] 错误提示API 错误统一使用 Ant Design Message 提示
- [ ] 空状态:无数据时展示空状态插画和文案
- [ ] 表单校验:必填项红框提示 + 校验文案
- [ ] 金额格式化:千分位 + 两位小数 + ¥ 前缀
- [ ] 日期格式化YYYY-MM-DD
- [ ] 确认弹窗:删除、提交等危险操作二次确认
- [ ] **Step 3: 添加 Demo 展示数据**
在首页添加"体验 Demo"按钮,一键生成演示数据:
- 创建一个已完成全流程的报销任务
- 包含 3 条费用明细
- 有规则命中记录
- 有审计日志
- [ ] **Step 4: 性能优化**
- [ ] 路由懒加载(已配置)
- [ ] 表格虚拟滚动(如果明细很多)
- [ ] 图片懒加载
- [ ] API 请求去重/缓存
- [ ] **Step 5: Commit**
```bash
git add .
git commit -m "fix: UI 打磨和 Bug 修复"
```
---
### Task 6.4: 部署与文档
**负责人:** 后端工程师 B + 前端工程师
**预计工时:** 2 天
**前置依赖:** Task 6.3
**Files:**
- Create: `docker-compose.prod.yml`
- Create: `nginx.conf`
- Modify: `README.md`
- Create: `docs/deployment.md`
- [ ] **Step 1: 编写生产 Docker Compose**
`docker-compose.prod.yml`:
```yaml
version: "3.8"
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./frontend/dist:/usr/share/nginx/html
depends_on:
- backend
backend:
build: ./backend
environment:
- DATABASE_URL=postgresql+asyncpg://postgres:${DB_PASSWORD}@postgres:5432/x_financial
- REDIS_URL=redis://redis:6379/0
- MINIO_ENDPOINT=minio:9000
depends_on:
- postgres
- redis
- minio
postgres:
image: postgres:15
environment:
POSTGRES_DB: x_financial
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
minio:
image: minio/minio
command: server /data
environment:
MINIO_ROOT_USER: ${MINIO_USER}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
volumes:
- minio_data:/data
volumes:
pgdata:
minio_data:
```
- [ ] **Step 2: 编写 Nginx 配置**
`nginx.conf`:
```nginx
server {
listen 80;
server_name localhost;
# 前端静态文件
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# 后端 API 代理
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
}
}
```
- [ ] **Step 3: 编写部署文档**
`docs/deployment.md` — 包含:
- 环境要求Docker、Docker Compose
- 配置说明(.env 文件)
- 启动步骤
- 停止和重启
- 数据库迁移
- 种子数据初始化
- 日志查看
- 常见问题排查
- [ ] **Step 4: 更新 README**
项目 README 包含:
- 项目简介和架构图
- 快速启动(开发环境)
- 技术栈说明
- 目录结构
- 开发指南
- API 文档链接
- [ ] **Step 5: 确认 Swagger 文档完整**
访问 http://localhost:8000/docs确认
- 所有 API 端点都有描述
- 请求/响应示例完整
- 错误码说明完整
- [ ] **Step 6: Commit**
```bash
git add .
git commit -m "docs: 添加部署文档、Nginx 配置、生产 Docker Compose"
```
---
## 本阶段完成检查
- [ ] `cd backend && pytest tests/ -v` 全部通过
- [ ] `cd frontend && npx playwright test` 全部通过(如配置)
- [ ] `cd frontend && npm run build` 无报错
- [ ] 完整报销流程在浏览器中手动测试无问题
- [ ] 所有页面响应式布局正常
- [ ] `docker-compose -f docker-compose.prod.yml up -d` 能启动
- [ ] README 和部署文档完整
- [ ] Swagger API 文档完整
- [ ] Demo 数据展示正常