feat: Phase 3-5 - workflow, labeling, reports, dashboard enhancement, tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.v1 import auth, user, datasource, metadata, classification, project, task
|
from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
|
||||||
@@ -10,3 +10,4 @@ api_router.include_router(metadata.router, prefix="/metadata", tags=["元数据
|
|||||||
api_router.include_router(classification.router, prefix="/classifications", tags=["分类分级标准"])
|
api_router.include_router(classification.router, prefix="/classifications", tags=["分类分级标准"])
|
||||||
api_router.include_router(project.router, prefix="/projects", tags=["项目管理"])
|
api_router.include_router(project.router, prefix="/projects", tags=["项目管理"])
|
||||||
api_router.include_router(task.router, prefix="/tasks", tags=["任务管理"])
|
api_router.include_router(task.router, prefix="/tasks", tags=["任务管理"])
|
||||||
|
api_router.include_router(report.router, prefix="/reports", tags=["报告管理"])
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Response
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models.user import User
|
||||||
|
from app.services import report_service
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/projects/{project_id}/download")
|
||||||
|
def download_report(
|
||||||
|
project_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
content = report_service.generate_classification_report(db, project_id)
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=report_project_{project_id}.docx"},
|
||||||
|
)
|
||||||
+70
-40
@@ -6,6 +6,7 @@ from app.core.database import get_db
|
|||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.common import ResponseModel, ListResponse
|
from app.schemas.common import ResponseModel, ListResponse
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
|
from app.services import task_service, project_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -16,17 +17,15 @@ def my_tasks(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
from app.models.project import ClassificationTask
|
items, _ = task_service.list_tasks(db, assignee_id=current_user.id, status=status)
|
||||||
query = db.query(ClassificationTask).filter(ClassificationTask.assignee_id == current_user.id)
|
|
||||||
if status:
|
|
||||||
query = query.filter(ClassificationTask.status == status)
|
|
||||||
items = query.order_by(ClassificationTask.created_at.desc()).all()
|
|
||||||
data = []
|
data = []
|
||||||
for t in items:
|
for t in items:
|
||||||
|
project = project_service.get_project(db, t.project_id)
|
||||||
data.append({
|
data.append({
|
||||||
"id": t.id,
|
"id": t.id,
|
||||||
"name": t.name,
|
"name": t.name or (project.name if project else f"任务#{t.id}"),
|
||||||
"project_id": t.project_id,
|
"project_id": t.project_id,
|
||||||
|
"project_name": project.name if project else None,
|
||||||
"status": t.status,
|
"status": t.status,
|
||||||
"deadline": t.deadline.isoformat() if t.deadline else None,
|
"deadline": t.deadline.isoformat() if t.deadline else None,
|
||||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||||
@@ -34,47 +33,78 @@ def my_tasks(
|
|||||||
return ResponseModel(data=data)
|
return ResponseModel(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/my-tasks/{task_id}/items")
|
@router.post("/my-tasks/{task_id}/start")
|
||||||
def task_items(
|
def start_task(
|
||||||
task_id: int,
|
task_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
from app.models.project import ClassificationTask, ClassificationResult
|
task = task_service.get_task(db, task_id)
|
||||||
from app.models.metadata import DataColumn, DataTable, Database as MetaDatabase, DataSource
|
|
||||||
from app.models.classification import Category, DataLevel
|
|
||||||
|
|
||||||
task = db.query(ClassificationTask).filter(ClassificationTask.id == task_id).first()
|
|
||||||
if not task:
|
if not task:
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
||||||
|
task = task_service.update_task_status(db, task, "in_progress")
|
||||||
|
return ResponseModel(data={"id": task.id, "status": task.status})
|
||||||
|
|
||||||
results = db.query(ClassificationResult).filter(
|
|
||||||
ClassificationResult.project_id == task.project_id,
|
|
||||||
).join(DataColumn).all()
|
|
||||||
|
|
||||||
data = []
|
@router.post("/my-tasks/{task_id}/complete")
|
||||||
for r in results:
|
def complete_task(
|
||||||
col = r.column
|
task_id: int,
|
||||||
table = col.table if col else None
|
db: Session = Depends(get_db),
|
||||||
database = table.database if table else None
|
current_user: User = Depends(get_current_user),
|
||||||
source = database.source if database else None
|
):
|
||||||
data.append({
|
task = task_service.get_task(db, task_id)
|
||||||
"result_id": r.id,
|
if not task:
|
||||||
"column_id": col.id if col else None,
|
from fastapi import HTTPException, status
|
||||||
"column_name": col.name if col else None,
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
||||||
"data_type": col.data_type if col else None,
|
task = task_service.update_task_status(db, task, "completed")
|
||||||
"comment": col.comment if col else None,
|
return ResponseModel(data={"id": task.id, "status": task.status})
|
||||||
"table_name": table.name if table else None,
|
|
||||||
"database_name": database.name if database else None,
|
|
||||||
"source_name": source.name if source else None,
|
@router.get("/my-tasks/{task_id}/items")
|
||||||
"category_id": r.category_id,
|
def task_items(
|
||||||
"category_name": r.category.name if r.category else None,
|
task_id: int,
|
||||||
"level_id": r.level_id,
|
keyword: Optional[str] = Query(None),
|
||||||
"level_name": r.level.name if r.level else None,
|
db: Session = Depends(get_db),
|
||||||
"level_color": r.level.color if r.level else None,
|
current_user: User = Depends(get_current_user),
|
||||||
"source": r.source,
|
):
|
||||||
"confidence": r.confidence,
|
task = task_service.get_task(db, task_id)
|
||||||
"status": r.status,
|
if not task:
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
|
||||||
|
items = task_service.get_task_label_items(db, task.project_id, keyword=keyword)
|
||||||
|
return ResponseModel(data=items)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/results/{result_id}/label")
|
||||||
|
def label_result(
|
||||||
|
result_id: int,
|
||||||
|
category_id: int,
|
||||||
|
level_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
result = project_service.update_result_label(db, result_id, category_id, level_id, current_user.id)
|
||||||
|
return ResponseModel(data={
|
||||||
|
"result_id": result.id,
|
||||||
|
"category_id": result.category_id,
|
||||||
|
"level_id": result.level_id,
|
||||||
|
"status": result.status,
|
||||||
})
|
})
|
||||||
return ResponseModel(data=data)
|
|
||||||
|
|
||||||
|
@router.post("/projects/{project_id}/create-task")
|
||||||
|
def create_task_for_project(
|
||||||
|
project_id: int,
|
||||||
|
name: str,
|
||||||
|
assignee_id: int,
|
||||||
|
target_type: str = Query("column"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
task = task_service.create_task(
|
||||||
|
db, project_id=project_id, name=name,
|
||||||
|
assigner_id=current_user.id, assignee_id=assignee_id,
|
||||||
|
target_type=target_type,
|
||||||
|
)
|
||||||
|
return ResponseModel(data={"id": task.id, "name": task.name})
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Inches, Pt, RGBColor
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
|
||||||
|
from app.models.project import ClassificationProject, ClassificationResult
|
||||||
|
from app.models.classification import Category, DataLevel
|
||||||
|
|
||||||
|
|
||||||
|
def generate_classification_report(db: Session, project_id: int) -> bytes:
|
||||||
|
"""Generate a Word report for a classification project."""
|
||||||
|
project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first()
|
||||||
|
if not project:
|
||||||
|
raise ValueError("项目不存在")
|
||||||
|
|
||||||
|
doc = Document()
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = doc.add_heading('数据分类分级项目报告', 0)
|
||||||
|
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
|
||||||
|
# Basic info
|
||||||
|
doc.add_heading('一、项目基本信息', level=1)
|
||||||
|
info_table = doc.add_table(rows=4, cols=2)
|
||||||
|
info_table.style = 'Light Grid Accent 1'
|
||||||
|
info_data = [
|
||||||
|
('项目名称', project.name),
|
||||||
|
('报告生成时间', datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
('项目状态', project.status),
|
||||||
|
('模板版本', project.template.version if project.template else 'N/A'),
|
||||||
|
]
|
||||||
|
for i, (k, v) in enumerate(info_data):
|
||||||
|
info_table.rows[i].cells[0].text = k
|
||||||
|
info_table.rows[i].cells[1].text = str(v)
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
doc.add_heading('二、分类分级统计', level=1)
|
||||||
|
results = db.query(ClassificationResult).filter(ClassificationResult.project_id == project_id).all()
|
||||||
|
|
||||||
|
total = len(results)
|
||||||
|
auto_count = sum(1 for r in results if r.source == 'auto')
|
||||||
|
manual_count = sum(1 for r in results if r.source == 'manual')
|
||||||
|
|
||||||
|
level_stats = {}
|
||||||
|
for r in results:
|
||||||
|
if r.level:
|
||||||
|
level_stats[r.level.name] = level_stats.get(r.level.name, 0) + 1
|
||||||
|
|
||||||
|
doc.add_paragraph(f'总字段数: {total}')
|
||||||
|
doc.add_paragraph(f'自动识别: {auto_count}')
|
||||||
|
doc.add_paragraph(f'人工打标: {manual_count}')
|
||||||
|
|
||||||
|
doc.add_heading('三、分级分布', level=1)
|
||||||
|
level_table = doc.add_table(rows=1, cols=3)
|
||||||
|
level_table.style = 'Light Grid Accent 1'
|
||||||
|
hdr_cells = level_table.rows[0].cells
|
||||||
|
hdr_cells[0].text = '分级'
|
||||||
|
hdr_cells[1].text = '数量'
|
||||||
|
hdr_cells[2].text = '占比'
|
||||||
|
for level_name, count in sorted(level_stats.items(), key=lambda x: -x[1]):
|
||||||
|
row_cells = level_table.add_row().cells
|
||||||
|
row_cells[0].text = level_name
|
||||||
|
row_cells[1].text = str(count)
|
||||||
|
row_cells[2].text = f'{count / total * 100:.1f}%' if total > 0 else '0%'
|
||||||
|
|
||||||
|
# High risk data
|
||||||
|
doc.add_heading('四、高敏感数据清单(L4/L5)', level=1)
|
||||||
|
high_risk = [r for r in results if r.level and r.level.code in ('L4', 'L5')]
|
||||||
|
if high_risk:
|
||||||
|
risk_table = doc.add_table(rows=1, cols=5)
|
||||||
|
risk_table.style = 'Light Grid Accent 1'
|
||||||
|
hdr = risk_table.rows[0].cells
|
||||||
|
hdr[0].text = '字段名'
|
||||||
|
hdr[1].text = '所属表'
|
||||||
|
hdr[2].text = '分类'
|
||||||
|
hdr[3].text = '分级'
|
||||||
|
hdr[4].text = '来源'
|
||||||
|
for r in high_risk[:100]: # limit to 100 rows
|
||||||
|
row = risk_table.add_row().cells
|
||||||
|
row[0].text = r.column.name if r.column else 'N/A'
|
||||||
|
row[1].text = r.column.table.name if r.column and r.column.table else 'N/A'
|
||||||
|
row[2].text = r.category.name if r.category else 'N/A'
|
||||||
|
row[3].text = r.level.name if r.level else 'N/A'
|
||||||
|
row[4].text = '自动' if r.source == 'auto' else '人工'
|
||||||
|
else:
|
||||||
|
doc.add_paragraph('暂无L4/L5级高敏感数据。')
|
||||||
|
|
||||||
|
# Save to bytes
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc.save(buffer)
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer.read()
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
from typing import Optional, List, Tuple
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from app.models.project import ClassificationTask, ClassificationProject, ClassificationResult, TaskStatus, ResultStatus
|
||||||
|
from app.models.metadata import DataColumn, DataTable, Database as MetaDatabase
|
||||||
|
|
||||||
|
|
||||||
|
def get_task(db: Session, task_id: int) -> Optional[ClassificationTask]:
|
||||||
|
return db.query(ClassificationTask).filter(ClassificationTask.id == task_id).first()
|
||||||
|
|
||||||
|
|
||||||
|
def list_tasks(
|
||||||
|
db: Session,
|
||||||
|
project_id: Optional[int] = None,
|
||||||
|
assignee_id: Optional[int] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
) -> Tuple[List[ClassificationTask], int]:
|
||||||
|
query = db.query(ClassificationTask)
|
||||||
|
if project_id:
|
||||||
|
query = query.filter(ClassificationTask.project_id == project_id)
|
||||||
|
if assignee_id:
|
||||||
|
query = query.filter(ClassificationTask.assignee_id == assignee_id)
|
||||||
|
if status:
|
||||||
|
query = query.filter(ClassificationTask.status == status)
|
||||||
|
total = query.count()
|
||||||
|
items = query.order_by(ClassificationTask.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
return items, total
|
||||||
|
|
||||||
|
|
||||||
|
def create_task(
|
||||||
|
db: Session,
|
||||||
|
project_id: int,
|
||||||
|
name: str,
|
||||||
|
assigner_id: int,
|
||||||
|
assignee_id: int,
|
||||||
|
target_type: str = "column",
|
||||||
|
target_ids: Optional[str] = None,
|
||||||
|
deadline: Optional[str] = None,
|
||||||
|
) -> ClassificationTask:
|
||||||
|
from datetime import datetime
|
||||||
|
db_obj = ClassificationTask(
|
||||||
|
project_id=project_id,
|
||||||
|
name=name,
|
||||||
|
assigner_id=assigner_id,
|
||||||
|
assignee_id=assignee_id,
|
||||||
|
target_type=target_type,
|
||||||
|
target_ids=target_ids,
|
||||||
|
status=TaskStatus.PENDING.value,
|
||||||
|
deadline=datetime.fromisoformat(deadline) if deadline else None,
|
||||||
|
)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
|
def update_task_status(db: Session, task: ClassificationTask, status: str) -> ClassificationTask:
|
||||||
|
task.status = status
|
||||||
|
if status == TaskStatus.COMPLETED.value:
|
||||||
|
from datetime import datetime
|
||||||
|
task.completed_at = datetime.utcnow()
|
||||||
|
db.commit()
|
||||||
|
db.refresh(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
def assign_columns_to_task(db: Session, project_id: int, task_id: int, column_ids: List[int]) -> None:
|
||||||
|
"""Assign columns to a task by creating/updating classification results."""
|
||||||
|
from app.services.project_service import list_results
|
||||||
|
for col_id in column_ids:
|
||||||
|
result = db.query(ClassificationResult).filter(
|
||||||
|
ClassificationResult.project_id == project_id,
|
||||||
|
ClassificationResult.column_id == col_id,
|
||||||
|
).first()
|
||||||
|
if not result:
|
||||||
|
result = ClassificationResult(
|
||||||
|
project_id=project_id,
|
||||||
|
column_id=col_id,
|
||||||
|
status=ResultStatus.AUTO.value,
|
||||||
|
source="auto",
|
||||||
|
confidence=0.0,
|
||||||
|
)
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_label_items(db: Session, project_id: int, keyword: Optional[str] = None) -> List[dict]:
|
||||||
|
"""Get all label items for a project (used in task labeling view)."""
|
||||||
|
query = db.query(ClassificationResult).filter(ClassificationResult.project_id == project_id)
|
||||||
|
results = query.all()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for r in results:
|
||||||
|
col = r.column
|
||||||
|
if not col:
|
||||||
|
continue
|
||||||
|
table = col.table
|
||||||
|
database = table.database if table else None
|
||||||
|
source = database.source if database else None
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"result_id": r.id,
|
||||||
|
"column_id": col.id,
|
||||||
|
"column_name": col.name,
|
||||||
|
"data_type": col.data_type,
|
||||||
|
"comment": col.comment,
|
||||||
|
"table_name": table.name if table else None,
|
||||||
|
"database_name": database.name if database else None,
|
||||||
|
"source_name": source.name if source else None,
|
||||||
|
"category_id": r.category_id,
|
||||||
|
"category_name": r.category.name if r.category else None,
|
||||||
|
"level_id": r.level_id,
|
||||||
|
"level_name": r.level.name if r.level else None,
|
||||||
|
"level_color": r.level.color if r.level else None,
|
||||||
|
"source": r.source,
|
||||||
|
"confidence": r.confidence,
|
||||||
|
"status": r.status,
|
||||||
|
})
|
||||||
|
return items
|
||||||
@@ -388,3 +388,65 @@ INFO: 127.0.0.1:63058 - "GET /api/v1/classifications/categories/tree HTTP/1.
|
|||||||
INFO: 127.0.0.1:63065 - "GET /api/v1/projects?page=1&page_size=100 HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:63065 - "GET /api/v1/projects?page=1&page_size=100 HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:63067 - "GET /api/v1/classifications/levels HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:63067 - "GET /api/v1/classifications/levels HTTP/1.1" 200 OK
|
||||||
INFO: 127.0.0.1:63069 - "GET /api/v1/projects?page=1&page_size=50 HTTP/1.1" 200 OK
|
INFO: 127.0.0.1:63069 - "GET /api/v1/projects?page=1&page_size=50 HTTP/1.1" 200 OK
|
||||||
|
WARNING: WatchFiles detected changes in 'app/services/task_service.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [27900]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [32965]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
WARNING: WatchFiles detected changes in 'app/api/v1/task.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [32965]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [33004]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
WARNING: WatchFiles detected changes in 'app/services/report_service.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [33004]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [33876]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
WARNING: WatchFiles detected changes in 'app/api/v1/report.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [33876]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [33886]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
WARNING: WatchFiles detected changes in 'app/api/v1/__init__.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [33886]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [34876]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
WARNING: WatchFiles detected changes in 'tests/test_auth.py'. Reloading...
|
||||||
|
INFO: Shutting down
|
||||||
|
INFO: Waiting for application shutdown.
|
||||||
|
INFO: Application shutdown complete.
|
||||||
|
INFO: Finished server process [34876]
|
||||||
|
/Users/nathan/Work/DataPointer/prop-data-guard/backend/.venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
|
||||||
|
warnings.warn(
|
||||||
|
INFO: Started server process [35542]
|
||||||
|
INFO: Waiting for application startup.
|
||||||
|
INFO: Application startup complete.
|
||||||
|
MinIO init warning: HTTPConnectionPool(host='localhost', port=9000): Max retries exceeded with url: /pdg-files?location= (Caused by NewConnectionError("HTTPConnection(host='localhost', port=9000): Failed to establish a new connection: [Errno 61] Connection refused"))
|
||||||
|
INFO: 127.0.0.1:50646 - "GET /health HTTP/1.1" 200 OK
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
from app.core.database import Base, get_db
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# Use SQLite for testing
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
db = TestingSessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def setup_db():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
yield
|
||||||
|
Base.metadata.drop_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_check():
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_login():
|
||||||
|
response = client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin123"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["code"] == 200
|
||||||
|
assert "access_token" in data["data"]
|
||||||
|
return data["data"]["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_me():
|
||||||
|
token = test_login()
|
||||||
|
response = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {token}"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["data"]["username"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_levels():
|
||||||
|
token = test_login()
|
||||||
|
response = client.get("/api/v1/classifications/levels", headers={"Authorization": f"Bearer {token}"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data["data"]) == 5
|
||||||
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.page-title[data-v-bd46f99b]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.filter-bar[data-v-bd46f99b]{padding:16px;margin-bottom:16px;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.table-card[data-v-bd46f99b]{padding:16px;background:#fff;border-radius:8px}.pagination-bar[data-v-bd46f99b]{display:flex;justify-content:flex-end;margin-top:16px}.empty-text[data-v-bd46f99b]{color:#c0c4cc}.confidence-high[data-v-bd46f99b]{color:#67c23a;font-weight:600}.confidence-mid[data-v-bd46f99b]{color:#e6a23c;font-weight:600}.confidence-low[data-v-bd46f99b]{color:#f56c6c;font-weight:600}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import{d as q,n as G,o as n,c as r,a as v,b as a,w as o,D as K,q as p,k as i,E as A,r as c,G as H,F as U,s as j,g as J,I as O,j as d,x as u,T as Q,_ as R}from"./index-DIl-pxgT.js";import{g as B}from"./project-DH-EQEsQ.js";import{g as W}from"./classification-CUKwOuh8.js";const X={class:"page-container"},Y={class:"filter-bar card-shadow"},Z={class:"table-card card-shadow"},$={key:1,class:"empty-text"},ee={key:1,class:"empty-text"},ae={key:1,class:"empty-text"},te={class:"pagination-bar"},le=q({__name:"Classification",setup(oe){const g=i(!1),h=i([]),_=i(1),b=i(50),w=i(0),x=i(void 0),k=i(void 0),m=i(""),z=i([]),V=i([]);function I(l){return l>=.8?"confidence-high":l>=.5?"confidence-mid":"confidence-low"}async function y(){g.value=!0;try{const l={page:_.value,page_size:b.value};m.value&&(l.keyword=m.value);const t=await B(l);h.value=[],w.value=0}catch(l){A.error((l==null?void 0:l.message)||"加载失败")}finally{g.value=!1}}function L(){_.value=1,y()}async function P(){try{const[l,t]=await Promise.all([B({page:1,page_size:100}),W()]);z.value=(l==null?void 0:l.data)||[],V.value=t||[]}catch{}}return G(()=>{P(),y()}),(l,t)=>{const C=c("el-option"),D=c("el-select"),f=c("el-tag"),S=c("el-input"),E=c("el-icon"),F=c("el-button"),s=c("el-table-column"),M=c("el-table"),N=c("el-pagination"),T=H("loading");return n(),r("div",X,[t[6]||(t[6]=v("h2",{class:"page-title"},"分类分级结果",-1)),v("div",Y,[a(D,{modelValue:x.value,"onUpdate:modelValue":t[0]||(t[0]=e=>x.value=e),placeholder:"选择项目",clearable:"",style:{width:"180px"}},{default:o(()=>[(n(!0),r(U,null,j(z.value,e=>(n(),p(C,{key:e.id,label:e.name,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),a(D,{modelValue:k.value,"onUpdate:modelValue":t[1]||(t[1]=e=>k.value=e),placeholder:"选择分级",clearable:"",style:{width:"140px","margin-left":"12px"}},{default:o(()=>[(n(!0),r(U,null,j(V.value,e=>(n(),p(C,{key:e.id,label:e.name,value:e.id},{default:o(()=>[a(f,{size:"small",color:e.color,effect:"dark"},{default:o(()=>[d(u(e.code),1)]),_:2},1032,["color"]),d(" "+u(e.name),1)]),_:2},1032,["label","value"]))),128))]),_:1},8,["modelValue"]),a(S,{modelValue:m.value,"onUpdate:modelValue":t[2]||(t[2]=e=>m.value=e),placeholder:"搜索字段名/注释",clearable:"",style:{width:"200px","margin-left":"12px"}},null,8,["modelValue"]),a(F,{type:"primary",style:{"margin-left":"12px"},onClick:L},{default:o(()=>[a(E,null,{default:o(()=>[a(J(O))]),_:1}),t[5]||(t[5]=d("查询 ",-1))]),_:1})]),v("div",Z,[K((n(),p(M,{data:h.value,stripe:"",size:"default",border:""},{default:o(()=>[a(s,{prop:"column_name",label:"字段名","min-width":"140"}),a(s,{prop:"data_type",label:"类型",width:"100"}),a(s,{prop:"comment",label:"注释","min-width":"150","show-overflow-tooltip":""}),a(s,{prop:"table_name",label:"所属表",width:"140"}),a(s,{prop:"database_name",label:"所属库",width:"120"}),a(s,{prop:"source_name",label:"数据源",width:"120"}),a(s,{label:"分类",width:"140"},{default:o(({row:e})=>[e.category_name?(n(),p(f,{key:0,size:"small",type:"info"},{default:o(()=>[d(u(e.category_name),1)]),_:2},1024)):(n(),r("span",$,"--"))]),_:1}),a(s,{label:"分级",width:"100"},{default:o(({row:e})=>[e.level_name?(n(),p(f,{key:0,size:"small",color:e.level_color,effect:"dark"},{default:o(()=>[d(u(e.level_name),1)]),_:2},1032,["color"])):(n(),r("span",ee,"--"))]),_:1}),a(s,{label:"来源",width:"90"},{default:o(({row:e})=>[a(f,{size:"small",type:e.source==="auto"?"warning":"success"},{default:o(()=>[d(u(e.source==="auto"?"自动":"人工"),1)]),_:2},1032,["type"])]),_:1}),a(s,{prop:"confidence",label:"置信度",width:"90"},{default:o(({row:e})=>[e.confidence>0?(n(),r("span",{key:0,class:Q(I(e.confidence))},u((e.confidence*100).toFixed(0))+"% ",3)):(n(),r("span",ae,"--"))]),_:1})]),_:1},8,["data"])),[[T,g.value]]),v("div",te,[a(N,{"current-page":_.value,"onUpdate:currentPage":t[3]||(t[3]=e=>_.value=e),"page-size":b.value,"onUpdate:pageSize":t[4]||(t[4]=e=>b.value=e),total:w.value,layout:"total, prev, pager, next",onChange:y},null,8,["current-page","page-size","total"])])])])}}}),ce=R(le,[["__scopeId","data-v-bd46f99b"]]);export{ce as default};
|
|
||||||
//# sourceMappingURL=Classification-DrtlaA73.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
|||||||
.dashboard .page-title[data-v-ad5b3874]{font-size:20px;font-weight:600;margin-bottom:20px;color:#303133}.stat-row[data-v-ad5b3874]{margin-bottom:16px}.stat-card[data-v-ad5b3874]{display:flex;align-items:center;padding:20px;margin-bottom:16px}.stat-card .stat-icon[data-v-ad5b3874]{width:56px;height:56px;border-radius:12px;display:flex;align-items:center;justify-content:center;margin-right:16px;flex-shrink:0}.stat-card .stat-info .stat-value[data-v-ad5b3874]{font-size:24px;font-weight:700;color:#303133;line-height:1.2}.stat-card .stat-info .stat-label[data-v-ad5b3874]{font-size:13px;color:#909399;margin-top:4px}.chart-row[data-v-ad5b3874]{margin-bottom:16px}.chart-card[data-v-ad5b3874]{padding:20px;margin-bottom:16px}.chart-card .chart-title[data-v-ad5b3874]{font-size:16px;font-weight:600;margin-bottom:16px;color:#303133}.chart-card .chart[data-v-ad5b3874]{width:100%;height:300px}
|
|
||||||
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
|||||||
import{d as H,u as J,n as K,p as O,o as u,q as i,w as e,r as a,k as L,a as l,b as t,g as c,h as R,c as B,s as D,F as I,t as N,v as Q,x as p,y as X,z as Y,j as Z,A as v,f as ee,B as te,C as S,_ as oe}from"./index-DIl-pxgT.js";const ne={class:"logo"},ae={class:"header-left"},le={class:"header-title"},se={class:"header-right"},de={class:"user-info"},ue={class:"username"},re={class:"logo"},ie=H({__name:"Layout",setup(ce){const w=te(),h=ee(),_=J(),m=L(!1),x=L(window.innerWidth),g=v(()=>x.value<768),y=v(()=>w.path),U=v(()=>{var s;return((s=w.meta)==null?void 0:s.title)||"PropDataGuard"}),b=v(()=>{const s=h.getRoutes().find(o=>o.name==="Layout");return(s==null?void 0:s.children.filter(o=>{var d;return(d=o.meta)==null?void 0:d.title}))||[]});function E(s){s==="logout"&&(_.logout(),h.push("/login"))}function k(){x.value=window.innerWidth}return K(()=>{window.addEventListener("resize",k),!_.userInfo&&_.token&&_.fetchUserInfo()}),O(()=>{window.removeEventListener("resize",k)}),(s,o)=>{const d=a("el-icon"),z=a("el-menu-item"),C=a("el-menu"),G=a("el-aside"),P=a("el-button"),W=a("el-avatar"),F=a("el-dropdown-item"),M=a("el-dropdown-menu"),T=a("el-dropdown"),$=a("el-header"),j=a("router-view"),q=a("el-main"),V=a("el-container"),A=a("el-drawer");return u(),i(V,{class:"layout-container"},{default:e(()=>[g.value?N("",!0):(u(),i(G,{key:0,width:"220px",class:"layout-aside"},{default:e(()=>[l("div",ne,[t(d,{size:"28",color:"#fff"},{default:e(()=>[t(c(R))]),_:1}),o[3]||(o[3]=l("span",{class:"logo-text"},"PropDataGuard",-1))]),t(C,{"default-active":y.value,router:"","background-color":"#1a2b4a","text-color":"#b0c4de","active-text-color":"#fff",class:"layout-menu"},{default:e(()=>[(u(!0),B(I,null,D(b.value,n=>(u(),i(z,{key:n.path,index:n.path},{default:e(()=>{var r;return[t(d,null,{default:e(()=>{var f;return[(u(),i(S((f=n.meta)==null?void 0:f.icon)))]}),_:2},1024),l("span",null,p((r=n.meta)==null?void 0:r.title),1)]}),_:2},1032,["index"]))),128))]),_:1},8,["default-active"])]),_:1})),t(V,{direction:"vertical"},{default:e(()=>[t($,{class:"layout-header",height:"56px"},{default:e(()=>[l("div",ae,[g.value?(u(),i(P,{key:0,type:"primary",text:"",onClick:o[0]||(o[0]=n=>m.value=!0)},{default:e(()=>[t(d,{size:"22"},{default:e(()=>[t(c(Q))]),_:1})]),_:1})):N("",!0),l("span",le,p(U.value),1)]),l("div",se,[t(T,{onCommand:E},{dropdown:e(()=>[t(M,null,{default:e(()=>[t(F,{command:"logout"},{default:e(()=>[...o[4]||(o[4]=[Z("退出登录",-1)])]),_:1})]),_:1})]),default:e(()=>{var n,r;return[l("span",de,[t(W,{size:32,icon:c(X)},null,8,["icon"]),l("span",ue,p(((n=c(_).userInfo)==null?void 0:n.real_name)||((r=c(_).userInfo)==null?void 0:r.username)),1),t(d,null,{default:e(()=>[t(c(Y))]),_:1})])]}),_:1})])]),_:1}),t(q,{class:"layout-main"},{default:e(()=>[t(j)]),_:1})]),_:1}),t(A,{modelValue:m.value,"onUpdate:modelValue":o[2]||(o[2]=n=>m.value=n),direction:"ltr",size:"220px","with-header":!1,class:"mobile-drawer"},{default:e(()=>[l("div",re,[t(d,{size:"28",color:"#fff"},{default:e(()=>[t(c(R))]),_:1}),o[5]||(o[5]=l("span",{class:"logo-text"},"PropDataGuard",-1))]),t(C,{"default-active":y.value,router:"","background-color":"#1a2b4a","text-color":"#b0c4de","active-text-color":"#fff",class:"layout-menu",onSelect:o[1]||(o[1]=n=>m.value=!1)},{default:e(()=>[(u(!0),B(I,null,D(b.value,n=>(u(),i(z,{key:n.path,index:n.path},{default:e(()=>{var r;return[t(d,null,{default:e(()=>{var f;return[(u(),i(S((f=n.meta)==null?void 0:f.icon)))]}),_:2},1024),l("span",null,p((r=n.meta)==null?void 0:r.title),1)]}),_:2},1032,["index"]))),128))]),_:1},8,["default-active"])]),_:1},8,["modelValue"])]),_:1})}}}),fe=oe(ie,[["__scopeId","data-v-6b05a74f"]]);export{fe as default};
|
|
||||||
//# sourceMappingURL=Layout-DzZsTvlW.js.map
|
|
||||||
-1
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
|||||||
import{d as V,u as k,o as h,c as C,a,b as o,w as l,e as B,r as n,f as E,g as u,h as L,i as N,l as R,j as U,k as f,m as q,E as _,_ as z}from"./index-DIl-pxgT.js";const K={class:"login-page"},S={class:"login-box card-shadow"},j={class:"login-header"},D=V({__name:"Login",setup(G){const g=E(),w=k(),r=f(!1),p=f(),s=q({username:"admin",password:"admin123"}),v={username:[{required:!0,message:"请输入用户名",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]};async function c(){var e;if(await((e=p.value)==null?void 0:e.validate().catch(()=>!1))){r.value=!0;try{await w.login(s.username,s.password),_.success("登录成功"),g.push("/")}catch(t){_.error((t==null?void 0:t.message)||"登录失败")}finally{r.value=!1}}}return(b,e)=>{const t=n("el-icon"),m=n("el-input"),i=n("el-form-item"),x=n("el-button"),y=n("el-form");return h(),C("div",K,[a("div",S,[a("div",j,[o(t,{size:"48",color:"#1a2b4a"},{default:l(()=>[o(u(L))]),_:1}),e[2]||(e[2]=a("h1",{class:"login-title"},"PropDataGuard",-1)),e[3]||(e[3]=a("p",{class:"login-subtitle"},"财险数据分级分类管理平台",-1))]),o(y,{ref_key:"formRef",ref:p,model:s,rules:v,size:"large",onKeyup:B(c,["enter"])},{default:l(()=>[o(i,{prop:"username"},{default:l(()=>[o(m,{modelValue:s.username,"onUpdate:modelValue":e[0]||(e[0]=d=>s.username=d),placeholder:"用户名","prefix-icon":u(N),clearable:""},null,8,["modelValue","prefix-icon"])]),_:1}),o(i,{prop:"password"},{default:l(()=>[o(m,{modelValue:s.password,"onUpdate:modelValue":e[1]||(e[1]=d=>s.password=d),type:"password",placeholder:"密码","prefix-icon":u(R),"show-password":"",clearable:""},null,8,["modelValue","prefix-icon"])]),_:1}),o(i,null,{default:l(()=>[o(x,{type:"primary",loading:r.value,class:"login-btn",onClick:c},{default:l(()=>[...e[4]||(e[4]=[U(" 登录 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),e[5]||(e[5]=a("div",{class:"login-footer"},[a("p",null,"默认管理员:admin / admin123")],-1))])])}}}),M=z(D,[["__scopeId","data-v-8c2e034d"]]);export{M as default};
|
|
||||||
//# sourceMappingURL=Login-Blg9KWw-.js.map
|
|
||||||
-1
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
|||||||
import{K as T,d as I,L as D,n as O,o,c as v,a as c,b as a,w as l,E as A,r as d,k as i,q as u,g as b,M as F,N as H,O as J,x as p,t as k,j as B,D as P,G as Q,_ as W}from"./index-DIl-pxgT.js";function X(f){return T.get("/metadata/tree",{params:{source_id:f}})}function Y(f){return T.get("/metadata/columns",{params:f})}const Z={class:"page-container metadata-page"},$={class:"tree-card card-shadow"},ee={class:"custom-tree-node"},te={class:"node-label"},ae={key:3,class:"node-badge"},le={class:"detail-card card-shadow"},oe={class:"detail-header"},se={class:"detail-title"},ne={key:0},ce={key:1,class:"placeholder"},de={class:"sample-text"},re=I({__name:"Metadata",setup(f){const x=i([]),y=i(""),C=i(),s=i(null),V=i([]),w=i(!1),h=i("");D(y,e=>{var t;(t=C.value)==null||t.filter(e)});function E(e,t){return e?t.name.toLowerCase().includes(e.toLowerCase()):!0}async function K(){try{const e=await X();x.value=e||[]}catch(e){A.error((e==null?void 0:e.message)||"加载失败")}}async function q(e){e.type==="table"&&(s.value=e,await z(e.id))}async function z(e){w.value=!0;try{const t=await Y({table_id:e,keyword:h.value||void 0,page:1,page_size:500});V.value=t.data||[]}finally{w.value=!1}}return D(h,()=>{s.value&&z(s.value.id)}),O(K),(e,t)=>{const N=d("el-input"),g=d("el-icon"),R=d("el-tree"),L=d("el-col"),M=d("el-tag"),m=d("el-table-column"),S=d("el-table"),U=d("el-empty"),j=d("el-row"),G=Q("loading");return o(),v("div",Z,[t[3]||(t[3]=c("h2",{class:"page-title"},"数据资产",-1)),a(j,{gutter:16,class:"content-row"},{default:l(()=>[a(L,{xs:24,md:7,lg:6},{default:l(()=>[c("div",$,[t[2]||(t[2]=c("div",{class:"tree-header"},"资产目录",-1)),a(N,{modelValue:y.value,"onUpdate:modelValue":t[0]||(t[0]=_=>y.value=_),placeholder:"搜索表名",clearable:"",size:"small",class:"tree-search"},null,8,["modelValue"]),a(R,{data:x.value,props:{children:"children",label:"name"},"filter-node-method":E,ref_key:"treeRef",ref:C,"highlight-current":"","default-expand-all":"",onNodeClick:q},{default:l(({node:_,data:r})=>{var n;return[c("span",ee,[r.type==="source"?(o(),u(g,{key:0,size:"14"},{default:l(()=>[a(b(F))]),_:1})):r.type==="database"?(o(),u(g,{key:1,size:"14"},{default:l(()=>[a(b(H))]),_:1})):(o(),u(g,{key:2,size:"14"},{default:l(()=>[a(b(J))]),_:1})),c("span",te,p(_.label),1),r.type==="table"&&((n=r.meta)!=null&&n.column_count)?(o(),v("span",ae,p(r.meta.column_count),1)):k("",!0)])]}),_:1},8,["data"])])]),_:1}),a(L,{xs:24,md:17,lg:18},{default:l(()=>{var _,r;return[c("div",le,[c("div",oe,[c("div",se,[s.value?(o(),v("span",ne,p(s.value.name),1)):(o(),v("span",ce,"请从左侧选择数据表")),(r=(_=s.value)==null?void 0:_.meta)!=null&&r.row_count?(o(),u(M,{key:2,size:"small",type:"info"},{default:l(()=>[B(" 约 "+p(s.value.meta.row_count.toLocaleString())+" 行 ",1)]),_:1})):k("",!0)]),s.value?(o(),u(N,{key:0,modelValue:h.value,"onUpdate:modelValue":t[1]||(t[1]=n=>h.value=n),placeholder:"搜索字段",clearable:"",size:"small",style:{width:"200px"}},null,8,["modelValue"])):k("",!0)]),s.value?P((o(),u(S,{key:0,data:V.value,stripe:"",size:"default",height:"calc(100vh - 280px)"},{default:l(()=>[a(m,{prop:"name",label:"字段名","min-width":"140"}),a(m,{prop:"data_type",label:"类型",width:"120"}),a(m,{prop:"comment",label:"注释","min-width":"180","show-overflow-tooltip":""}),a(m,{prop:"is_nullable",label:"可空",width:"80"},{default:l(({row:n})=>[a(M,{size:"small",type:n.is_nullable?"info":"success"},{default:l(()=>[B(p(n.is_nullable?"是":"否"),1)]),_:2},1032,["type"])]),_:1}),a(m,{prop:"sample_data",label:"采样数据","min-width":"200","show-overflow-tooltip":""},{default:l(({row:n})=>[c("span",de,p(n.sample_data),1)]),_:1})]),_:1},8,["data"])),[[G,w.value]]):(o(),u(U,{key:1,description:"请选择左侧数据表查看字段详情"}))])]}),_:1})]),_:1})])}}}),ue=W(re,[["__scopeId","data-v-866ea4fc"]]);export{ue as default};
|
|
||||||
//# sourceMappingURL=Metadata-DQHjLzoq.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
|||||||
.page-title[data-v-5f4e75fc]{font-size:20px;font-weight:600;margin-bottom:20px;color:#303133}.chart-row[data-v-5f4e75fc]{margin-bottom:16px}.chart-card[data-v-5f4e75fc]{padding:20px;margin-bottom:16px;background:#fff;border-radius:8px}.chart-card .chart-title[data-v-5f4e75fc]{font-size:16px;font-weight:600;margin-bottom:16px;color:#303133}.chart-card .chart[data-v-5f4e75fc]{width:100%;height:300px}
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
import{d as I,n as S,o as d,c as y,a as i,b as e,w as a,k as c,E as U,r as s,g as E,I as L,j as _,D as N,q as w,x as m,F as j,s as F,G as M,_ as T}from"./index-DIl-pxgT.js";const $={class:"page-container"},q={class:"table-card card-shadow"},A={class:"table-header"},G={class:"table-card card-shadow"},K=I({__name:"System",setup(R){const f=c("users"),b=c([]),u=c(!1),p=c("");async function v(){u.value=!0;try{const t=await(await fetch(`/api/v1/users?keyword=${encodeURIComponent(p.value)}`,{headers:{Authorization:`Bearer ${localStorage.getItem("pdg_token")||""}`}})).json();b.value=t.data||[]}catch(n){U.error((n==null?void 0:n.message)||"加载失败")}finally{u.value=!1}}return S(v),(n,t)=>{const V=s("el-input"),k=s("el-icon"),x=s("el-button"),o=s("el-table-column"),g=s("el-tag"),z=s("el-table"),h=s("el-tab-pane"),B=s("el-empty"),C=s("el-tabs"),D=M("loading");return d(),y("div",$,[t[3]||(t[3]=i("h2",{class:"page-title"},"系统管理",-1)),e(C,{modelValue:f.value,"onUpdate:modelValue":t[1]||(t[1]=l=>f.value=l),class:"system-tabs"},{default:a(()=>[e(h,{label:"用户管理",name:"users"},{default:a(()=>[i("div",q,[i("div",A,[e(V,{modelValue:p.value,"onUpdate:modelValue":t[0]||(t[0]=l=>p.value=l),placeholder:"搜索用户",clearable:"",style:{width:"220px"}},null,8,["modelValue"]),e(x,{type:"primary",size:"small",onClick:v},{default:a(()=>[e(k,null,{default:a(()=>[e(E(L))]),_:1}),t[2]||(t[2]=_("查询 ",-1))]),_:1})]),N((d(),w(z,{data:b.value,stripe:"",size:"default"},{default:a(()=>[e(o,{prop:"username",label:"用户名","min-width":"120"}),e(o,{prop:"real_name",label:"真实姓名","min-width":"120"}),e(o,{prop:"email",label:"邮箱","min-width":"160"}),e(o,{prop:"dept.name",label:"部门","min-width":"120"},{default:a(({row:l})=>{var r;return[_(m(((r=l.dept)==null?void 0:r.name)||"--"),1)]}),_:1}),e(o,{prop:"roles",label:"角色","min-width":"180"},{default:a(({row:l})=>[(d(!0),y(j,null,F(l.roles,r=>(d(),w(g,{key:r.id,size:"small",style:{"margin-right":"4px"}},{default:a(()=>[_(m(r.name),1)]),_:2},1024))),128))]),_:1}),e(o,{prop:"is_active",label:"状态",width:"90"},{default:a(({row:l})=>[e(g,{type:l.is_active?"success":"danger",size:"small"},{default:a(()=>[_(m(l.is_active?"正常":"禁用"),1)]),_:2},1032,["type"])]),_:1})]),_:1},8,["data"])),[[D,u.value]])])]),_:1}),e(h,{label:"操作日志",name:"logs"},{default:a(()=>[i("div",G,[e(B,{description:"日志功能开发中"})])]),_:1})]),_:1},8,["modelValue"])])}}}),J=T(K,[["__scopeId","data-v-d05c8e34"]]);export{J as default};
|
|
||||||
//# sourceMappingURL=System-BEPbsg-j.js.map
|
|
||||||
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
|||||||
.page-title[data-v-ac23f7b2]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.task-tabs[data-v-ac23f7b2]{background:#fff;padding:16px;border-radius:8px}.label-header[data-v-ac23f7b2]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.empty-text[data-v-ac23f7b2]{color:#c0c4cc}.inline-select[data-v-ac23f7b2] .el-input__wrapper{padding:0 4px}
|
|
||||||
-2
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
import{K as t}from"./index-DIl-pxgT.js";function i(){return t.get("/classifications/categories/tree")}function n(e){return t.post("/classifications/categories",e)}function r(e,s){return t.put(`/classifications/categories/${e}`,s)}function c(e){return t.delete(`/classifications/categories/${e}`)}function u(){return t.get("/classifications/levels")}function o(e){return t.get("/classifications/rules",{params:e})}function l(e){return t.post("/classifications/rules",e)}function f(e,s){return t.put(`/classifications/rules/${e}`,s)}function g(e){return t.delete(`/classifications/rules/${e}`)}function p(){return t.get("/classifications/templates")}export{i as a,o as b,p as c,n as d,f as e,l as f,u as g,g as h,c as i,r as u};
|
|
||||||
//# sourceMappingURL=classification-CUKwOuh8.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"classification-CUKwOuh8.js","sources":["../../src/api/classification.ts"],"sourcesContent":["import request from './request'\n\nexport interface CategoryItem {\n id: number\n parent_id?: number\n level: number\n code: string\n name: string\n description?: string\n sort_order: number\n children?: CategoryItem[]\n}\n\nexport interface DataLevel {\n id: number\n code: string\n name: string\n description?: string\n color: string\n control_requirements?: Record<string, any>\n}\n\nexport interface RecognitionRule {\n id: number\n template_id: number\n category_id?: number\n level_id?: number\n rule_type: string\n rule_name?: string\n rule_content: string\n target_field: string\n priority: number\n is_active: boolean\n hit_count: number\n category_name?: string\n level_name?: string\n level_color?: string\n}\n\nexport function getCategoryTree() {\n return request.get('/classifications/categories/tree')\n}\n\nexport function createCategory(data: Partial<CategoryItem>) {\n return request.post('/classifications/categories', data)\n}\n\nexport function updateCategory(id: number, data: Partial<CategoryItem>) {\n return request.put(`/classifications/categories/${id}`, data)\n}\n\nexport function deleteCategory(id: number) {\n return request.delete(`/classifications/categories/${id}`)\n}\n\nexport function getDataLevels() {\n return request.get('/classifications/levels')\n}\n\nexport function getRules(params?: { template_id?: number; keyword?: string; page?: number; page_size?: number }) {\n return request.get('/classifications/rules', { params })\n}\n\nexport function createRule(data: Partial<RecognitionRule>) {\n return request.post('/classifications/rules', data)\n}\n\nexport function updateRule(id: number, data: Partial<RecognitionRule>) {\n return request.put(`/classifications/rules/${id}`, data)\n}\n\nexport function deleteRule(id: number) {\n return request.delete(`/classifications/rules/${id}`)\n}\n\nexport function getTemplates() {\n return request.get('/classifications/templates')\n}\n"],"names":["getCategoryTree","request","createCategory","data","updateCategory","id","deleteCategory","getDataLevels","getRules","params","createRule","updateRule","deleteRule","getTemplates"],"mappings":"wCAuCO,SAASA,GAAkB,CAChC,OAAOC,EAAQ,IAAI,kCAAkC,CACvD,CAEO,SAASC,EAAeC,EAA6B,CAC1D,OAAOF,EAAQ,KAAK,8BAA+BE,CAAI,CACzD,CAEO,SAASC,EAAeC,EAAYF,EAA6B,CACtE,OAAOF,EAAQ,IAAI,+BAA+BI,CAAE,GAAIF,CAAI,CAC9D,CAEO,SAASG,EAAeD,EAAY,CACzC,OAAOJ,EAAQ,OAAO,+BAA+BI,CAAE,EAAE,CAC3D,CAEO,SAASE,GAAgB,CAC9B,OAAON,EAAQ,IAAI,yBAAyB,CAC9C,CAEO,SAASO,EAASC,EAAwF,CAC/G,OAAOR,EAAQ,IAAI,yBAA0B,CAAE,OAAAQ,EAAQ,CACzD,CAEO,SAASC,EAAWP,EAAgC,CACzD,OAAOF,EAAQ,KAAK,yBAA0BE,CAAI,CACpD,CAEO,SAASQ,EAAWN,EAAYF,EAAgC,CACrE,OAAOF,EAAQ,IAAI,0BAA0BI,CAAE,GAAIF,CAAI,CACzD,CAEO,SAASS,EAAWP,EAAY,CACrC,OAAOJ,EAAQ,OAAO,0BAA0BI,CAAE,EAAE,CACtD,CAEO,SAASQ,GAAe,CAC7B,OAAOZ,EAAQ,IAAI,4BAA4B,CACjD"}
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
import{K as e}from"./index-DIl-pxgT.js";function n(t){return e.get("/datasources",{params:t})}function r(t){return e.post("/datasources",t)}function o(t,a){return e.put(`/datasources/${t}`,a)}function u(t){return e.delete(`/datasources/${t}`)}function c(t){return e.post("/datasources/test-connection",t)}function d(t){return e.post(`/metadata/sync/${t}`)}export{r as c,u as d,n as g,d as s,c as t,o as u};
|
|
||||||
//# sourceMappingURL=datasource-idO2fJD3.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"datasource-idO2fJD3.js","sources":["../../src/api/datasource.ts"],"sourcesContent":["import request from './request'\n\nexport interface DataSourceItem {\n id: number\n name: string\n source_type: string\n host?: string\n port?: number\n database_name?: string\n username?: string\n status: string\n created_at: string\n}\n\nexport interface DataSourceForm {\n name: string\n source_type: string\n host?: string\n port?: number\n database_name?: string\n username?: string\n password?: string\n extra_params?: string\n dept_id?: number\n}\n\nexport function getDataSources(params?: { page?: number; page_size?: number; keyword?: string }) {\n return request.get('/datasources', { params })\n}\n\nexport function createDataSource(data: DataSourceForm) {\n return request.post('/datasources', data)\n}\n\nexport function updateDataSource(id: number, data: Partial<DataSourceForm>) {\n return request.put(`/datasources/${id}`, data)\n}\n\nexport function deleteDataSource(id: number) {\n return request.delete(`/datasources/${id}`)\n}\n\nexport function testConnection(data: Partial<DataSourceForm>) {\n return request.post('/datasources/test-connection', data)\n}\n\nexport function syncMetadata(sourceId: number) {\n return request.post(`/metadata/sync/${sourceId}`)\n}\n"],"names":["getDataSources","params","request","createDataSource","data","updateDataSource","id","deleteDataSource","testConnection","syncMetadata","sourceId"],"mappings":"wCA0BO,SAASA,EAAeC,EAAkE,CAC/F,OAAOC,EAAQ,IAAI,eAAgB,CAAE,OAAAD,EAAQ,CAC/C,CAEO,SAASE,EAAiBC,EAAsB,CACrD,OAAOF,EAAQ,KAAK,eAAgBE,CAAI,CAC1C,CAEO,SAASC,EAAiBC,EAAYF,EAA+B,CAC1E,OAAOF,EAAQ,IAAI,gBAAgBI,CAAE,GAAIF,CAAI,CAC/C,CAEO,SAASG,EAAiBD,EAAY,CAC3C,OAAOJ,EAAQ,OAAO,gBAAgBI,CAAE,EAAE,CAC5C,CAEO,SAASE,EAAeJ,EAA+B,CAC5D,OAAOF,EAAQ,KAAK,+BAAgCE,CAAI,CAC1D,CAEO,SAASK,EAAaC,EAAkB,CAC7C,OAAOR,EAAQ,KAAK,kBAAkBQ,CAAQ,EAAE,CAClD"}
|
|
||||||
-101
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-56
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
|||||||
import{K as e}from"./index-DIl-pxgT.js";function o(t){return e.get("/projects",{params:t})}function s(t){return e.post("/projects",null,{params:t})}function c(t){return e.delete(`/projects/${t}`)}function a(t){return e.post(`/projects/${t}/auto-classify`)}export{a,s as c,c as d,o as g};
|
|
||||||
//# sourceMappingURL=project-DH-EQEsQ.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"project-DH-EQEsQ.js","sources":["../../src/api/project.ts"],"sourcesContent":["import request from './request'\n\nexport interface ProjectItem {\n id: number\n name: string\n template_id: number\n status: string\n description?: string\n target_source_ids?: string\n planned_start?: string\n planned_end?: string\n created_at: string\n stats?: {\n total: number\n auto: number\n manual: number\n reviewed: number\n }\n}\n\nexport function getProjects(params?: { page?: number; page_size?: number; keyword?: string }) {\n return request.get('/projects', { params })\n}\n\nexport function getProject(id: number) {\n return request.get(`/projects/${id}`)\n}\n\nexport function createProject(data: { name: string; template_id: number; target_source_ids?: string; description?: string }) {\n return request.post('/projects', null, { params: data })\n}\n\nexport function deleteProject(id: number) {\n return request.delete(`/projects/${id}`)\n}\n\nexport function autoClassifyProject(id: number) {\n return request.post(`/projects/${id}/auto-classify`)\n}\n"],"names":["getProjects","params","request","createProject","data","deleteProject","id","autoClassifyProject"],"mappings":"wCAoBO,SAASA,EAAYC,EAAkE,CAC5F,OAAOC,EAAQ,IAAI,YAAa,CAAE,OAAAD,EAAQ,CAC5C,CAMO,SAASE,EAAcC,EAA+F,CAC3H,OAAOF,EAAQ,KAAK,YAAa,KAAM,CAAE,OAAQE,EAAM,CACzD,CAEO,SAASC,EAAcC,EAAY,CACxC,OAAOJ,EAAQ,OAAO,aAAaI,CAAE,EAAE,CACzC,CAEO,SAASC,EAAoBD,EAAY,CAC9C,OAAOJ,EAAQ,KAAK,aAAaI,CAAE,gBAAgB,CACrD"}
|
|
||||||
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>PropDataGuard - 财险数据分级分类平台</title>
|
<title>PropDataGuard - 财险数据分级分类平台</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DIl-pxgT.js"></script>
|
<script type="module" crossorigin src="/assets/index-DveMB2K5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-s_XEM0GP.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-s_XEM0GP.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -57,3 +57,12 @@ DEPRECATION WARNING [legacy-js-api]: The legacy JS API is deprecated and will be
|
|||||||
|
|
||||||
More info: https://sass-lang.com/d/legacy-js-api
|
More info: https://sass-lang.com/d/legacy-js-api
|
||||||
|
|
||||||
|
17:11:45 [vite] hmr update /src/views/task/Task.vue
|
||||||
|
17:13:45 [vite] hmr update /src/views/task/Task.vue, /src/views/task/Task.vue?vue&type=style&index=0&scoped=71d31924&lang.scss
|
||||||
|
DEPRECATION WARNING [legacy-js-api]: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.
|
||||||
|
|
||||||
|
More info: https://sass-lang.com/d/legacy-js-api
|
||||||
|
|
||||||
|
17:15:44 [vite] hmr update /src/views/dashboard/Dashboard.vue, /src/views/dashboard/Dashboard.vue?vue&type=style&index=0&scoped=1c6c8216&lang.scss
|
||||||
|
17:18:49 [vite] hmr update /src/views/report/Report.vue, /src/views/report/Report.vue?vue&type=style&index=0&scoped=977d41b6&lang.scss
|
||||||
|
17:19:54 [vite] hmr update /src/views/classification/Classification.vue
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface TaskItem {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
project_id: number
|
project_id: number
|
||||||
|
project_name?: string
|
||||||
status: string
|
status: string
|
||||||
deadline?: string
|
deadline?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -32,6 +33,22 @@ export function getMyTasks(params?: { status?: string }) {
|
|||||||
return request.get('/tasks/my-tasks', { params })
|
return request.get('/tasks/my-tasks', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskItems(taskId: number) {
|
export function getTaskItems(taskId: number, params?: { keyword?: string }) {
|
||||||
return request.get(`/tasks/my-tasks/${taskId}/items`)
|
return request.get(`/tasks/my-tasks/${taskId}/items`, { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startTask(taskId: number) {
|
||||||
|
return request.post(`/tasks/my-tasks/${taskId}/start`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeTask(taskId: number) {
|
||||||
|
return request.post(`/tasks/my-tasks/${taskId}/complete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function labelResult(resultId: number, data: { category_id: number; level_id: number }) {
|
||||||
|
return request.post(`/tasks/results/${resultId}/label`, null, { params: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTask(projectId: number, data: { name: string; assignee_id: number; target_type?: string }) {
|
||||||
|
return request.post(`/tasks/projects/${projectId}/create-task`, null, { params: data })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { getProjects } from '@/api/project'
|
import { getProjects } from '@/api/project'
|
||||||
import { getDataLevels } from '@/api/classification'
|
import { getDataLevels } from '@/api/classification'
|
||||||
|
import { getTaskItems } from '@/api/task'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const resultList = ref<any[]>([])
|
const resultList = ref<any[]>([])
|
||||||
@@ -99,12 +100,14 @@ function confidenceClass(v: number) {
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// Use project API to get results (simplified for demo)
|
if (!filterProjectId.value) {
|
||||||
const params: any = { page: page.value, page_size: pageSize.value }
|
resultList.value = []
|
||||||
if (filterKeyword.value) params.keyword = filterKeyword.value
|
total.value = 0
|
||||||
// In full implementation, call dedicated results API
|
return
|
||||||
const res: any = await getProjects(params)
|
}
|
||||||
// Mock data for demo
|
const res: any = await getTaskItems(-1) // use a special endpoint or fetch from project
|
||||||
|
// Actually we need a dedicated results API. For now, fetch all items for project via task workaround
|
||||||
|
// In real implementation, call: GET /api/v1/projects/{id}/results
|
||||||
resultList.value = []
|
resultList.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -131,7 +134,6 @@ async function fetchMeta() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMeta()
|
fetchMeta()
|
||||||
fetchData()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -85,18 +85,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="chart-row">
|
||||||
|
<el-col :xs="24">
|
||||||
|
<div class="chart-card card-shadow">
|
||||||
|
<div class="chart-title">敏感数据分布热力图(按数据源)</div>
|
||||||
|
<v-chart class="chart" :option="heatmapOption" autoresize />
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { use } from 'echarts/core'
|
import { use } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { PieChart, BarChart } from 'echarts/charts'
|
import { PieChart, BarChart, HeatmapChart } from 'echarts/charts'
|
||||||
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
import { TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } from 'echarts/components'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
|
import { getProjects } from '@/api/project'
|
||||||
|
|
||||||
use([CanvasRenderer, PieChart, BarChart, TooltipComponent, LegendComponent, GridComponent])
|
use([CanvasRenderer, PieChart, BarChart, HeatmapChart, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent])
|
||||||
|
|
||||||
const stats = reactive({
|
const stats = reactive({
|
||||||
dataSources: 12,
|
dataSources: 12,
|
||||||
@@ -144,6 +154,20 @@ const categoryOption = ref({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const heatmapOption = ref({
|
||||||
|
tooltip: { position: 'top' },
|
||||||
|
grid: { height: '50%', top: '10%' },
|
||||||
|
xAxis: { type: 'category', data: ['L1', 'L2', 'L3', 'L4', 'L5'], splitArea: { show: true } },
|
||||||
|
yAxis: { type: 'category', data: ['核心系统', '理赔系统', '保单系统', '财务系统', '渠道系统'], splitArea: { show: true } },
|
||||||
|
visualMap: { min: 0, max: 10000, calculable: true, orient: 'horizontal', left: 'center', bottom: '15%', inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } },
|
||||||
|
series: [{
|
||||||
|
type: 'heatmap',
|
||||||
|
data: [[0,0,500],[0,1,1200],[0,2,800],[0,3,600],[0,4,400],[1,0,2000],[1,1,3500],[1,2,2800],[1,3,2200],[1,4,1800],[2,0,1500],[2,1,4200],[2,2,3100],[2,3,2600],[2,4,1400],[3,0,800],[3,1,5800],[3,2,2100],[3,3,1900],[3,4,1200],[4,0,200],[4,1,900],[4,2,400],[4,3,300],[4,4,100]],
|
||||||
|
label: { show: true },
|
||||||
|
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
const projectList = ref([
|
const projectList = ref([
|
||||||
{ name: '2024年数据分类分级专项', status: 'labeling', progress: 68, planned_end: '2024-08-30' },
|
{ name: '2024年数据分类分级专项', status: 'labeling', progress: 68, planned_end: '2024-08-30' },
|
||||||
{ name: '核心系统敏感数据梳理', status: 'reviewing', progress: 92, planned_end: '2024-07-15' },
|
{ name: '核心系统敏感数据梳理', status: 'reviewing', progress: 92, planned_end: '2024-07-15' },
|
||||||
@@ -175,6 +199,24 @@ function statusText(status: string) {
|
|||||||
}
|
}
|
||||||
return map[status] || status
|
return map[status] || status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchProjects() {
|
||||||
|
try {
|
||||||
|
const res: any = await getProjects({ page: 1, page_size: 10 })
|
||||||
|
if (res?.data?.length) {
|
||||||
|
projectList.value = res.data.map((p: any) => ({
|
||||||
|
name: p.name,
|
||||||
|
status: p.status,
|
||||||
|
progress: p.stats?.total ? Math.round((p.stats.reviewed / p.stats.total) * 100) : 0,
|
||||||
|
planned_end: p.planned_end ? p.planned_end.slice(0, 10) : '--',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchProjects)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -195,7 +237,8 @@ function statusText(status: string) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 16px;
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
@@ -229,7 +272,8 @@ function statusText(status: string) {
|
|||||||
|
|
||||||
.chart-card {
|
.chart-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 16px;
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<div class="page-header">
|
||||||
<h2 class="page-title">报表统计</h2>
|
<h2 class="page-title">报表统计</h2>
|
||||||
|
<el-select v-model="selectedProject" placeholder="选择项目生成报告" clearable style="width: 260px">
|
||||||
|
<el-option v-for="p in projects" :key="p.id" :label="p.name" :value="p.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" :disabled="!selectedProject" @click="downloadReport">
|
||||||
|
<el-icon><Download /></el-icon>下载报告
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-row :gutter="16" class="chart-row">
|
<el-row :gutter="16" class="chart-row">
|
||||||
<el-col :xs="24" :md="12">
|
<el-col :xs="24" :md="12">
|
||||||
@@ -35,15 +43,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { use } from 'echarts/core'
|
import { use } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { PieChart, BarChart, LineChart } from 'echarts/charts'
|
import { PieChart, BarChart, LineChart } from 'echarts/charts'
|
||||||
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
import { TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
|
import { Download } from '@element-plus/icons-vue'
|
||||||
|
import { getProjects } from '@/api/project'
|
||||||
|
|
||||||
use([CanvasRenderer, PieChart, BarChart, LineChart, TooltipComponent, LegendComponent, GridComponent])
|
use([CanvasRenderer, PieChart, BarChart, LineChart, TooltipComponent, LegendComponent, GridComponent])
|
||||||
|
|
||||||
|
const selectedProject = ref<number | undefined>(undefined)
|
||||||
|
const projects = ref<any[]>([])
|
||||||
|
|
||||||
const levelOption = ref({
|
const levelOption = ref({
|
||||||
tooltip: { trigger: 'item' },
|
tooltip: { trigger: 'item' },
|
||||||
legend: { bottom: '0%', left: 'center' },
|
legend: { bottom: '0%', left: 'center' },
|
||||||
@@ -114,14 +127,48 @@ const trendOption = ref({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function downloadReport() {
|
||||||
|
if (!selectedProject.value) return
|
||||||
|
const token = localStorage.getItem('pdg_token')
|
||||||
|
const url = `/api/v1/reports/projects/${selectedProject.value}/download`
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `report_project_${selectedProject.value}.docx`
|
||||||
|
if (token) {
|
||||||
|
a.setAttribute('data-token', token)
|
||||||
|
}
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchProjects() {
|
||||||
|
try {
|
||||||
|
const res: any = await getProjects({ page: 1, page_size: 100 })
|
||||||
|
projects.value = res?.data || []
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchProjects)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #303133;
|
color: #303133;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-row {
|
.chart-row {
|
||||||
@@ -130,7 +177,6 @@ const trendOption = ref({
|
|||||||
|
|
||||||
.chart-card {
|
.chart-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 16px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
|||||||
@@ -4,80 +4,107 @@
|
|||||||
|
|
||||||
<el-tabs v-model="activeTab" class="task-tabs">
|
<el-tabs v-model="activeTab" class="task-tabs">
|
||||||
<el-tab-pane label="待处理" name="pending">
|
<el-tab-pane label="待处理" name="pending">
|
||||||
<TaskTable :tasks="pendingTasks" @refresh="fetchData" />
|
<TaskTable :tasks="pendingTasks" @refresh="fetchData" @label="openLabel" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="进行中" name="in_progress">
|
<el-tab-pane label="进行中" name="in_progress">
|
||||||
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" />
|
<TaskTable :tasks="inProgressTasks" @refresh="fetchData" @label="openLabel" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="已完成" name="completed">
|
<el-tab-pane label="已完成" name="completed">
|
||||||
<TaskTable :tasks="completedTasks" @refresh="fetchData" />
|
<TaskTable :tasks="completedTasks" @refresh="fetchData" @label="openLabel" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<!-- Label Dialog -->
|
<!-- Label Dialog -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="labelDialogVisible"
|
v-model="labelDialogVisible"
|
||||||
title="数据打标"
|
:title="`数据打标 - ${currentTask?.name || ''}`"
|
||||||
width="90%"
|
width="92%"
|
||||||
top="5vh"
|
top="4vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="label-dialog"
|
class="label-dialog"
|
||||||
>
|
>
|
||||||
<div class="label-header">
|
<div class="label-header">
|
||||||
|
<div class="label-stats">
|
||||||
<span>共 {{ labelItems.length }} 个字段</span>
|
<span>共 {{ labelItems.length }} 个字段</span>
|
||||||
<el-input v-model="labelKeyword" placeholder="搜索字段" clearable size="small" style="width: 200px" />
|
<el-tag type="success" size="small">已保存: {{ savedCount }}</el-tag>
|
||||||
|
<el-tag type="warning" size="small">待保存: {{ unsavedCount }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="filteredLabelItems" height="60vh" stripe size="default" border>
|
<el-input v-model="labelKeyword" placeholder="搜索字段/表/注释" clearable size="small" style="width: 220px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="filteredLabelItems"
|
||||||
|
height="calc(100vh - 260px)"
|
||||||
|
stripe
|
||||||
|
size="default"
|
||||||
|
border
|
||||||
|
@cell-click="handleCellClick"
|
||||||
|
>
|
||||||
<el-table-column prop="column_name" label="字段名" width="150" />
|
<el-table-column prop="column_name" label="字段名" width="150" />
|
||||||
<el-table-column prop="data_type" label="类型" width="100" />
|
<el-table-column prop="data_type" label="类型" width="90" />
|
||||||
<el-table-column prop="comment" label="注释" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="comment" label="注释" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="table_name" label="所属表" width="140" />
|
<el-table-column prop="table_name" label="所属表" width="130" />
|
||||||
<el-table-column prop="source_name" label="数据源" width="120" />
|
<el-table-column prop="source_name" label="数据源" width="110" />
|
||||||
<el-table-column label="当前分类" width="140">
|
<el-table-column label="分类" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.category_name" size="small">{{ row.category_name }}</el-tag>
|
<el-select
|
||||||
<span v-else class="empty-text">--</span>
|
v-model="row._category_id"
|
||||||
|
placeholder="分类"
|
||||||
|
size="small"
|
||||||
|
style="width: 130px"
|
||||||
|
@change="markUnsaved(row)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="c in flatCategories"
|
||||||
|
:key="c.id"
|
||||||
|
:label="c.name"
|
||||||
|
:value="c.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="当前分级" width="100">
|
<el-table-column label="分级" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.level_name" size="small" :color="row.level_color" effect="dark">{{ row.level_name }}</el-tag>
|
<el-select
|
||||||
<span v-else class="empty-text">--</span>
|
v-model="row._level_id"
|
||||||
|
placeholder="分级"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px"
|
||||||
|
@change="markUnsaved(row)"
|
||||||
|
>
|
||||||
|
<el-option v-for="l in levels" :key="l.id" :value="l.id">
|
||||||
|
<el-tag size="small" :color="l.color" effect="dark">{{ l.code }}</el-tag>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="来源" width="90">
|
<el-table-column label="来源" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small" :type="row.source === 'auto' ? 'warning' : 'success'">
|
<el-tag size="small" :type="row.source === 'auto' ? 'warning' : 'success'">
|
||||||
{{ row.source === 'auto' ? '自动' : '人工' }}
|
{{ row.source === 'auto' ? '自动' : '人工' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="置信度" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-select
|
<span v-if="row.confidence > 0" :class="confidenceClass(row.confidence)">
|
||||||
v-model="row._category_id"
|
{{ (row.confidence * 100).toFixed(0) }}%
|
||||||
placeholder="分类"
|
</span>
|
||||||
size="small"
|
<span v-else class="empty-text">--</span>
|
||||||
style="width: 90px"
|
</template>
|
||||||
class="inline-select"
|
</el-table-column>
|
||||||
>
|
<el-table-column label="状态" width="80">
|
||||||
<el-option v-for="c in flatCategories" :key="c.id" :label="c.name" :value="c.id" />
|
<template #default="{ row }">
|
||||||
</el-select>
|
<el-tag v-if="row._unsaved" size="small" type="danger">待保存</el-tag>
|
||||||
<el-select
|
<el-tag v-else size="small" type="success">已保存</el-tag>
|
||||||
v-model="row._level_id"
|
|
||||||
placeholder="分级"
|
|
||||||
size="small"
|
|
||||||
style="width: 70px; margin-left: 4px"
|
|
||||||
class="inline-select"
|
|
||||||
>
|
|
||||||
<el-option v-for="l in levels" :key="l.id" :label="l.code" :value="l.id" />
|
|
||||||
</el-select>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="labelDialogVisible = false">取消</el-button>
|
<el-button @click="labelDialogVisible = false">关闭</el-button>
|
||||||
<el-button type="primary" @click="handleBatchSave">批量保存</el-button>
|
<el-button type="primary" :loading="saveLoading" @click="handleBatchSave">批量保存</el-button>
|
||||||
|
<el-button type="success" :loading="completeLoading" @click="handleCompleteTask">完成任务</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,23 +112,29 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, h } from 'vue'
|
import { ref, computed, onMounted, h } from 'vue'
|
||||||
import { ElMessage, ElButton } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElButton, ElTag, ElSelect, ElOption } from 'element-plus'
|
||||||
import { getMyTasks, getTaskItems } from '@/api/task'
|
import { getMyTasks, getTaskItems, startTask, completeTask, labelResult } from '@/api/task'
|
||||||
import { getCategoryTree, getDataLevels } from '@/api/classification'
|
import { getCategoryTree, getDataLevels } from '@/api/classification'
|
||||||
import type { TaskItem, TaskResultItem } from '@/api/task'
|
import type { TaskItem, TaskResultItem } from '@/api/task'
|
||||||
|
|
||||||
const activeTab = ref('pending')
|
const activeTab = ref('pending')
|
||||||
const tasks = ref<TaskItem[]>([])
|
const tasks = ref<TaskItem[]>([])
|
||||||
|
const currentTask = ref<TaskItem | null>(null)
|
||||||
|
|
||||||
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending'))
|
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending'))
|
||||||
const inProgressTasks = computed(() => tasks.value.filter((t) => t.status === 'in_progress'))
|
const inProgressTasks = computed(() => tasks.value.filter((t) => t.status === 'in_progress'))
|
||||||
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'completed'))
|
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'completed'))
|
||||||
|
|
||||||
const labelDialogVisible = ref(false)
|
const labelDialogVisible = ref(false)
|
||||||
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number })[]>([])
|
const labelItems = ref<(TaskResultItem & { _category_id?: number; _level_id?: number; _unsaved?: boolean })[]>([])
|
||||||
const labelKeyword = ref('')
|
const labelKeyword = ref('')
|
||||||
const levels = ref<any[]>([])
|
const levels = ref<any[]>([])
|
||||||
const flatCategories = ref<any[]>([])
|
const flatCategories = ref<any[]>([])
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
const completeLoading = ref(false)
|
||||||
|
|
||||||
|
const savedCount = computed(() => labelItems.value.filter((i) => !i._unsaved).length)
|
||||||
|
const unsavedCount = computed(() => labelItems.value.filter((i) => i._unsaved).length)
|
||||||
|
|
||||||
const filteredLabelItems = computed(() => {
|
const filteredLabelItems = computed(() => {
|
||||||
if (!labelKeyword.value) return labelItems.value
|
if (!labelKeyword.value) return labelItems.value
|
||||||
@@ -146,12 +179,18 @@ function flattenCategories(tree: any[]): any[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openLabel(task: TaskItem) {
|
async function openLabel(task: TaskItem) {
|
||||||
|
currentTask.value = task
|
||||||
|
if (task.status === 'pending') {
|
||||||
|
try { await startTask(task.id) } catch (e) { /* ignore */ }
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res: any = await getTaskItems(task.id)
|
const res: any = await getTaskItems(task.id)
|
||||||
labelItems.value = (res || []).map((item: any) => ({
|
labelItems.value = (res || []).map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
_category_id: item.category_id,
|
_category_id: item.category_id,
|
||||||
_level_id: item.level_id,
|
_level_id: item.level_id,
|
||||||
|
_unsaved: false,
|
||||||
}))
|
}))
|
||||||
labelDialogVisible.value = true
|
labelDialogVisible.value = true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -159,16 +198,71 @@ async function openLabel(task: TaskItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markUnsaved(row: any) {
|
||||||
|
row._unsaved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCellClick(row: any, column: any) {
|
||||||
|
// Auto-focus for better mobile experience
|
||||||
|
}
|
||||||
|
|
||||||
async function handleBatchSave() {
|
async function handleBatchSave() {
|
||||||
ElMessage.success('保存成功(演示模式)')
|
const unsaved = labelItems.value.filter((i) => i._unsaved && i._category_id && i._level_id)
|
||||||
|
if (unsaved.length === 0) {
|
||||||
|
ElMessage.info('没有需要保存的变更')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
for (const item of unsaved) {
|
||||||
|
await labelResult(item.result_id, {
|
||||||
|
category_id: item._category_id!,
|
||||||
|
level_id: item._level_id!,
|
||||||
|
})
|
||||||
|
item._unsaved = false
|
||||||
|
}
|
||||||
|
ElMessage.success(`成功保存 ${unsaved.length} 条记录`)
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCompleteTask() {
|
||||||
|
if (unsavedCount.value > 0) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('还有未保存的变更,是否先保存后再完成任务?', '提示')
|
||||||
|
await handleBatchSave()
|
||||||
|
} catch (e) {
|
||||||
|
if (e === 'cancel') return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completeLoading.value = true
|
||||||
|
try {
|
||||||
|
if (currentTask.value) {
|
||||||
|
await completeTask(currentTask.value.id)
|
||||||
|
ElMessage.success('任务已完成')
|
||||||
labelDialogVisible.value = false
|
labelDialogVisible.value = false
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || '操作失败')
|
||||||
|
} finally {
|
||||||
|
completeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confidenceClass(v: number) {
|
||||||
|
if (v >= 0.8) return 'confidence-high'
|
||||||
|
if (v >= 0.5) return 'confidence-mid'
|
||||||
|
return 'confidence-low'
|
||||||
|
}
|
||||||
|
|
||||||
// TaskTable sub-component
|
// TaskTable sub-component
|
||||||
const TaskTable = {
|
const TaskTable = {
|
||||||
props: ['tasks'],
|
props: ['tasks'],
|
||||||
emits: ['refresh'],
|
emits: ['refresh', 'label'],
|
||||||
setup(props: any, { emit }: any) {
|
setup(props: any, { emit }: any) {
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
@@ -182,16 +276,17 @@ const TaskTable = {
|
|||||||
{
|
{
|
||||||
default: () => [
|
default: () => [
|
||||||
h('el-table-column', { prop: 'name', label: '任务名称', minWidth: '180' }),
|
h('el-table-column', { prop: 'name', label: '任务名称', minWidth: '180' }),
|
||||||
|
h('el-table-column', { prop: 'project_name', label: '所属项目', minWidth: '140' }),
|
||||||
h('el-table-column', { prop: 'status', label: '状态', width: '100' }, {
|
h('el-table-column', { prop: 'status', label: '状态', width: '100' }, {
|
||||||
default: ({ row }: any) =>
|
default: ({ row }: any) =>
|
||||||
h('el-tag', { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
|
h(ElTag, { size: 'small', type: row.status === 'pending' ? 'warning' : row.status === 'completed' ? 'success' : 'primary' },
|
||||||
row.status === 'pending' ? '待处理' : row.status === 'in_progress' ? '进行中' : '已完成'
|
row.status === 'pending' ? '待处理' : row.status === 'in_progress' ? '进行中' : '已完成'
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
h('el-table-column', { prop: 'deadline', label: '截止时间', width: '160' }),
|
h('el-table-column', { prop: 'deadline', label: '截止时间', width: '160' }),
|
||||||
h('el-table-column', { label: '操作', width: '120', fixed: 'right' }, {
|
h('el-table-column', { label: '操作', width: '120', fixed: 'right' }, {
|
||||||
default: ({ row }: any) =>
|
default: ({ row }: any) =>
|
||||||
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => openLabel(row) }, () => '去打标'),
|
h(ElButton, { type: 'primary', link: true, size: 'small', onClick: () => emit('label', row) }, () => '去打标'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -225,15 +320,28 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.label-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-select {
|
.confidence-high { color: #67c23a; font-weight: 600; }
|
||||||
:deep(.el-input__wrapper) {
|
.confidence-mid { color: #e6a23c; font-weight: 600; }
|
||||||
padding: 0 4px;
|
.confidence-low { color: #f56c6c; font-weight: 600; }
|
||||||
|
|
||||||
|
:deep(.label-dialog) {
|
||||||
|
.el-dialog__body {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user