Commit 1db12af5 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 查询页 接后端 runQuery + 编辑任务密码回填 + schema 智能默认

核心:数据质量查询页接后端

- 后端 POST /api/queries/run:按任务配置连真实库 → SELECT 字段 → 跑正则 → 返回 {ok, scanned, bad_rows, rows, field_list}
- 前端 src/api/queries.js + DataQualityView.onQuery 改异步:loading 态 + 失败横幅 + task-info-bar 真实计数
- 字段列表优先用 task.field_list,覆盖之前的 fieldsByType mock

附带修 2 个 UX bug:

1. 编辑任务密码空 → 自动匹配预设(host+port+db+user)补回密码 + 选中预设
2. loadSchemas 默认选 form.db(避免字母序选 smart-build 这种空库)

顺便:规则 tab 空状态按 connVerified 拆两种文案
parent aa69ab03
......@@ -2,6 +2,112 @@
> 任务做完一次记一次。最近的在最上面。
## 2026-08-20 · 编辑任务:密码自动回填 + 规则 tab 空状态文案修正
> 用户反馈:编辑「test」任务时,进「数据连接」Tab 看到密码字段是空的;进「校验规则」Tab
> 看到「表「t_site_attendance」下未找到任何字段」—— 让人以为是表没字段,实际是任务本来就
> 没配规则。
### 改动
| 文件 | 变更 |
|---|---|
| [web3/src/components/TaskModal.vue](web3/src/components/TaskModal.vue) | 1. 编辑场景自动匹配历史预设(host+port+db+user 4 字段一致)→ 用预设里的密码回填到 form.password,并把下拉框选中该条,让用户一眼知道密码来源;2. 规则 tab 空状态文案按 `connVerified` 区分:未验证 → 「请先测试连接」;已验证 → 「表「X」下未找到任何字段」 |
### 原因
- **密码空**:前一处 `form.password = ''` 是 2026-08-19 加的安全考虑(密码不回填),但体验差;
后端已经在 update 路径兜底保留旧密码([routers/tasks.py:329-333](web3/backend/routers/tasks.py#L329-L333)),
而预设(connection_preset)存的是明文密码([seed.py yaml 灌入策略])。所以从匹配预设里取密码
是「同源策略」,不会有额外安全风险。
- **规则空**:原文案「表「X」下未找到任何字段」只在 `!connVerified` 提前 return 时显示,是设计错误;
没验证连接 ≠ 表没字段,文案应引导用户去先验证连接。
### 踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| 之前 `form.password = ''` 显式清空 + 注释「安全考虑」 | 编辑时密码总空,必须重输 | 改用「匹配预设」策略:hit 用预设的密码,不 hit 才让用户输 |
| 规则 tab 空状态只一种文案 | 误导用户 | 拆 `connVerified` 两种文案 |
### 验证
- 打开编辑「test」任务(数据库连接: 192.168.20.10 / 3306 / smart-build / root)
- 数据连接 Tab:密码字段自动填充(来自「智能建造」预设),下拉框自动选中「智能建造」
- 校验规则 Tab:未测试连接时显示「请先在「数据连接」Tab 点击「测试连接」加载字段列表」
- 测试连接成功 → 自动加载 `t_user_info` 的字段列表
### 还没做的
- 如果用户改了 host/port/db/user 后没匹配预设,密码还是空 → 后续可考虑:保存任务时顺手把新连接作为预设(让用户下次能自动匹配)
### 后续修复(同一处)
> 用户反馈:连「楚风家政」预设(database=retailers)后,schema 下拉默认选到「smart-build」
> (字母序第一个,是空库),导致「未找到任何表」。
- 根因:`loadSchemas` 默认选 `schemaOptions[0]`,字母序可能是空库
- 修复:默认选 `form.db`(如果该库在 schema 列表里);否则才回退到第一个
- 优先级:1. 编辑场景手动选过的(`form.schema` 已在列表里)→ 保留;2. `form.db` 在列表里 → 选它;3. 都没 → 选第一个
## 2026-08-20 · 主题 31·查询页 接后端:按任务配置连真实库 → 跑规则 → 表格展示
> 用户需求:点击查询按钮后,用选中任务的 DB 连接 + 源表信息把全表查出来,
> 按任务的字段配置校验规则去匹配目标字段,最后把数据 + 校验结果都用表格控件展示。
### 改动总览
| 层 | 文件 | 变更 |
|---|---|---|
| 后端 | [web3/backend/routers/queries.py](web3/backend/routers/queries.py) | 新文件:POST /api/queries/run 端点 |
| 后端 | [web3/backend/app.py](web3/backend/app.py) | 注册 queries router |
| 前端-API | [web3/src/api/queries.js](web3/src/api/queries.js) | 新文件:runQuery(taskId, limit) |
| 前端-视图 | [web3/src/views/DataQualityView.vue](web3/src/views/DataQualityView.vue) | onQuery 调后端 + 填 rows + task-info-bar 真实计数 |
### 后端 /api/queries/run 工作流
1. 按 `task_id` 拉任务 + 字段 + 规则(rule 比对 field 一对多)
2. `task.source_table` 为空 → 早返 `任务未配置数据表`
3. 没有任何字段带规则 → 早返 `任务未配置任何校验规则`(不连库)
4. 拼 `DBConfig`(复用 `core.db_adapter`),编译正则(regex 为空 → `^.+$` 非空校验;编译失败 → 标记为"坏规则")
5. 显式 SELECT 字段列表(不 SELECT * 防止暴露未配置列),`LIMIT 1000`(默认,可配)
6. 列名归一为小写(达梦默认大写,MySQL 视 collation)
7. 逐行校验:`re.search` 不匹配 → 该字段进 `errorCells` + 一条 issue;同字段多条规则只报第一条
8. 返回 `{ok, scanned, bad_rows, total, rows, field_list}`
### 前端 DataQualityView 的改动
- 任务下拉通过 `taskOptions.find(t => t.name === selectedTask.value)?.id` 找到 task id
- `onQuery` 改异步:loading 态 → 调 `runQuery` → 填 `rows` + 覆盖 `resultFields`(用真实 task.field_list)
- task-info-bar:扫描总数 / 命中不合规数 用真实返回计数
- 顶部查询失败:横幅(红底) + toast(ElMessage.error)
- 三种结果 toast:0 行 / 全部合规 / 命中不合规
### 踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| `_parse_conn_json` 找不到 | `ImportError: cannot import name '_parse_conn_json' from 'web3.backend.routers.db'` | 函数实际在 `routers/tasks.py`,改 import 路径 |
| Pydantic Field 与 SQLAlchemy Field 撞车 | `TypeError: __init__() takes 1 positional argument but 2 were given` | import 改 `from pydantic import Field as PydField`,schema 字段全部用 PydField(...) |
### 验证
- 后端:测试用 task 11(P31_test_query,127.0.0.1 假 MySQL)
- 预期:ok=False,返回 `OperationalError`
- 实际:
```json
{"ok": false, "message": "查询失败: OperationalError: (2003, \"Can't connect to MySQL server on '127.0.0.1'...\")",
"field_list": [{"id": 1, "field_key": "id_card", "rules": 2}, {"id": 2, "field_key": "user_name", "rules": 1}]}
```
- ✅ 错误路径正常:返回错误信息 + field_list 仍返回(让前端能恢复表头)
- 前端:未实测(需要真实 DB,用户自己跑)
### 还没做的
- 友好提示:connection timeout 增加到 10s(已做),但还没单独的 "连接失败" vs "表不存在" 区分
- 异步导出:当前"导出"按钮仍 mock,等真有客户库后再接
- 大表分页:当前 `LIMIT 1000`,还没做分页/异步拉
## 2026-08-14 · 「字段长度检查」拆分为 7 个子分类(3 层嵌套)
> 用户反馈:原来「字段长度检查」一个 tab 把 身份证 / USCC / 手机 / 区划 等不同字段类型
......
......@@ -54,6 +54,7 @@ app.add_middleware(
from web3.backend.routers.ai import router as ai_router # noqa: E402
from web3.backend.routers.connection_presets import router as connection_presets_router # noqa: E402
from web3.backend.routers.db import router as db_router # noqa: E402
from web3.backend.routers.queries import router as queries_router # noqa: E402
from web3.backend.routers.task_groups import router as task_groups_router # noqa: E402
from web3.backend.routers.tasks import router as tasks_router # noqa: E402
app.include_router(task_groups_router, prefix="/api")
......@@ -61,7 +62,8 @@ app.include_router(tasks_router, prefix="/api")
app.include_router(db_router, prefix="/api")
app.include_router(connection_presets_router, prefix="/api")
app.include_router(ai_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks, /api/connect/*, /api/connection-presets, /api/ai/*")
app.include_router(queries_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks, /api/connect/*, /api/connection-presets, /api/ai/*, /api/queries/*")
@app.get("/api/health")
......
"""web3 任务查询 + 校验 API
端点:
POST /api/queries/run 按任务配置:连目标库 → 读表 → 跑规则 → 返回结果
实现:
- 复用 web3.backend.core.db_adapter 建连
- 复用 routers/db._normalize_db_type 做中文 label → 内部小写
- 列名按 task.field_list 显式 SELECT(不 SELECT * 防止暴露未配置列)
- 列名小写归一(达梦默认大写,MySQL 视 collation)→ 与 field_key 对齐
- 正则校验:rule.regex 为空时回退到 `^.+$`(非空校验)
- 命中规则 → 该字段进 errorCells + 一条 issue;同一字段多条规则只报第一条
- 限 limit(默认 1000)防大表打爆内存;后续做分页 + 异步时再调
"""
from __future__ import annotations
import re
import time
from typing import Any, Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field as PydField
from sqlalchemy.orm import Session
from web3.backend._logging import get_logger
from web3.backend.core.db_adapter import DBConfig, DBConnection, quote_ident
from web3.backend.db.database import get_session
from web3.backend.models.field import Field
from web3.backend.models.rule import Rule
from web3.backend.models.task import Task
from web3.backend.routers.db import _normalize_db_type
from web3.backend.routers.tasks import _parse_conn_json
router = APIRouter(prefix="/queries", tags=["queries"])
logger = get_logger("backend.routers.queries")
# ── Pydantic ──────────────────────────────────────────────
class RunQueryRequest(BaseModel):
task_id: int = PydField(..., description="任务 id(后端从 DB 读全部配置)")
limit: int = PydField(1000, ge=1, le=10000, description="最多扫多少行(防大表打爆)")
class RunQueryRow(BaseModel):
"""单行结果:业务列 + 错误标记(与前端 ResultTable 行结构对齐)"""
errorCells: list[str] = PydField(default_factory=list)
issues: list[dict] = PydField(default_factory=list)
__row_index: int = 0 # 内部行号(前端不展示,用于后续 P 走「行内编辑」时定位)
class RunQueryResponse(BaseModel):
ok: bool
message: Optional[str] = None
scanned: int = 0 # 实际扫到的行数(≤limit)
bad_rows: int = 0 # 有错误的行数
total: int = 0 # = scanned(暂不单独查 COUNT;后续分页用)
rows: list[dict] = PydField(default_factory=list)
field_list: list[dict] = PydField(default_factory=list, description="顺带回当前任务的字段+规则数,让前端能渲染表头")
# ── 端点 ──────────────────────────────────────────────────
@router.post("/run", response_model=RunQueryResponse, summary="按任务配置跑查询 + 校验")
async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
# 1) 拉任务
task = db.query(Task).filter(Task.id == req.task_id).first()
if not task:
return RunQueryResponse(ok=False, message=f"任务不存在:{req.task_id}")
# 2) 拉字段+规则
fields = db.query(Field).filter(Field.task_id == task.id).order_by(Field.ord).all()
fields_with_rules: list[tuple[Field, list[Rule]]] = []
for f in fields:
rules = db.query(Rule).filter(Rule.field_id == f.id).order_by(Rule.ord).all()
if rules:
fields_with_rules.append((f, rules))
# 顺带回一份字段列表(表头用);与 FieldOut 结构对齐
field_list_out = [
{
"id": f.id,
"field_key": f.field_key,
"show_default": bool(f.show_default),
"ord": f.ord,
"rules": len(rules),
}
for f, rules in [(f, db.query(Rule).filter(Rule.field_id == f.id).all()) for f in fields]
]
# 3) 没配数据表 → 早返回
if not task.source_table:
return RunQueryResponse(
ok=False,
message="任务未配置数据表(source_table 为空)",
field_list=field_list_out,
)
if not fields_with_rules:
return RunQueryResponse(
ok=True,
message="任务未配置任何校验规则",
field_list=field_list_out,
)
# 4) 拼 DBConfig
conn = _parse_conn_json(task.conn_json)
cfg = DBConfig(
db_type=_normalize_db_type(task.db_type or "MySQL"),
host=conn.get("host") or "",
port=int(conn.get("port") or 3306),
user=conn.get("user") or "",
password=conn.get("password") or "",
database=conn.get("db") or "",
charset="utf8mb4",
connect_timeout=10,
)
# 5) 编译正则(提前编译,错的话直接返回不要连库)
compiled: list[tuple[Field, list[tuple[Rule, Optional[re.Pattern]]]]] = []
for f, rules in fields_with_rules:
rule_patterns: list[tuple[Rule, Optional[re.Pattern]]] = []
for r in rules:
regex_src = (r.regex or "").strip() or r"^.+$" # 空 regex = 非空校验
try:
rule_patterns.append((r, re.compile(regex_src)))
except re.error as e:
# 单条规则编译失败:记一行 issue,整字段标红
logger.warning(f"[queries] 规则 id={r.id} regex 编译失败:{e}")
rule_patterns.append((r, None)) # None 标记"坏规则"
compiled.append((f, rule_patterns))
# 6) 连库 + 拉数据
started = time.monotonic()
table = task.source_table
col_names = [f.field_key for f, _ in fields_with_rules]
col_sql = ", ".join(quote_ident(c, cfg.db_type) for c in col_names)
sql = f"SELECT {col_sql} FROM {quote_ident(table, cfg.db_type)} LIMIT {int(req.limit)}"
logger.info("─" * 60)
logger.info(
f"POST /api/queries/run task_id={task.id} table={table!r} "
f"fields={len(col_names)} rules_total={sum(len(rp) for _, rp in compiled)} "
f"limit={req.limit}"
)
logger.info(f"SQL: {sql}")
try:
with DBConnection(cfg) as dbc:
raw_rows = dbc.fetchall(sql)
except Exception as e:
msg = f"查询失败: {type(e).__name__}: {e}"
logger.exception(msg)
return RunQueryResponse(ok=False, message=msg, field_list=field_list_out)
elapsed_ms = (time.monotonic() - started) * 1000
logger.info(f"✅ 拉回 {len(raw_rows)} 行(耗时 {elapsed_ms:.0f}ms)")
# 7) 逐行校验
bad_count = 0
out_rows: list[dict[str, Any]] = []
for r_idx, raw in enumerate(raw_rows):
# 列名归一为小写(达梦默认大写,MySQL 视 collation)
raw_lc = {(k.lower() if isinstance(k, str) else k): v for k, v in raw.items()}
issues: list[dict] = []
error_cells: list[str] = []
for f, rule_patterns in compiled:
val = raw_lc.get(f.field_key.lower())
val_str = "" if val is None else str(val)
for rule, pat in rule_patterns:
if pat is None:
# 坏规则
issues.append({"field": f.field_key, "desc": f"规则正则编译失败:{rule.desc}"})
if f.field_key not in error_cells:
error_cells.append(f.field_key)
break
# re.search 没匹配上 → 不合规
if pat.search(val_str) is None:
issues.append({"field": f.field_key, "desc": rule.desc})
if f.field_key not in error_cells:
error_cells.append(f.field_key)
break # 一字段命中第一条规则就够,不重复
if issues:
bad_count += 1
out_row = dict(raw_lc)
out_row["errorCells"] = error_cells
out_row["issues"] = issues
out_row["__row_index"] = r_idx
out_rows.append(out_row)
logger.info(f"✅ 校验完成:{bad_count}/{len(raw_rows)} 行不合规(耗时 {elapsed_ms:.0f}ms)")
logger.info("─" * 60)
return RunQueryResponse(
ok=True,
scanned=len(raw_rows),
total=len(raw_rows),
bad_rows=bad_count,
rows=out_rows,
field_list=field_list_out,
)
/**
* 数据查询 / 校验 API
*
* - POST /api/queries/run 按任务配置:连目标库 → 读表 → 跑规则 → 返回结果
*/
import { http } from './client'
/**
* 跑一次任务查询 + 校验
* @param {number} taskId 任务 id
* @param {number} limit 最多扫多少行(后端默认 1000)
* @returns {Promise<{ok, message?, scanned, bad_rows, total, rows: [], field_list: []}>}
*/
export function runQuery(taskId, limit = 1000) {
return http.post('/queries/run', { task_id: taskId, limit })
}
......@@ -211,7 +211,12 @@
</tr>
<tr v-if="!loadingColumns && fieldList.length === 0 && form.table">
<td colspan="5" style="text-align: center; color: #909399; padding: 24px 0;">
表「{{ form.table }}」下未找到任何字段
<template v-if="!connVerified">
请先在「数据连接」Tab 点击「测试连接」加载字段列表
</template>
<template v-else>
表「{{ form.table }}」下未找到任何字段
</template>
</td>
</tr>
</tbody>
......@@ -371,9 +376,14 @@ async function loadSchemas() {
if (r.ok) {
schemaOptions.value = r.schemas || []
if (schemaOptions.value.length > 0) {
// 默认选第一个;如果之前 form.schema 还在新列表里,保留
// 默认选连接配置的数据库(如果它在 schema 列表里)
// 否则回退到第一个:避免 smart-build 这种字母序排第一的空库被误选
if (!schemaOptions.value.includes(form.schema)) {
form.schema = schemaOptions.value[0]
if (form.db && schemaOptions.value.includes(form.db)) {
form.schema = form.db
} else {
form.schema = schemaOptions.value[0]
}
}
await loadTables()
} else {
......@@ -524,7 +534,6 @@ watch(() => props.open, async (v) => {
await loadPresets(form.dbType)
if (props.task) {
// 编辑场景:从 task 还原全部字段(含数据库连接)
// 注意:密码不回填到前端(安全考虑),保留空让用户重输
form.name = props.task.name || ''
form.group = props.task.group || '身份证'
form.desc = props.task.description || ''
......@@ -534,7 +543,7 @@ watch(() => props.open, async (v) => {
form.port = props.task.port || ''
form.db = props.task.db || ''
form.user = props.task.user || ''
form.password = '' // 密码不回填,重新输入
form.password = '' // 密码不回填(安全),但下面会自动从匹配的预设里补回来
form.jdbcParams = props.task.jdbc_params || ''
form.schema = props.task.schema || ''
// 数据表:后端存的是「t_user_info」纯名,前端直接展示
......@@ -544,6 +553,20 @@ watch(() => props.open, async (v) => {
// cn/data_type 是「编辑时重新取」的——不回填,留空让用户重新测试连接后从 listColumns 补
// field_list 可能还没到(getTask 异步)—— 下面那个 watch 会在拿到后自动补
_fillFieldListFromTask()
// 自动匹配:当前任务的连接(在 form 里)是否已经存在历史预设里?
// 匹配规则:同 dbType + host + port + db + user。
// 命中 → 用预设的密码回填到 form.password,并让下拉框选中它(让用户一眼知道密码来自哪条预设)
// 不命中 → 保持空,让用户手动输入
const matched = presetOptions.value.find((p) =>
p.host === form.host &&
String(p.port) === String(form.port) &&
p.database === form.db &&
p.user === form.user
)
if (matched) {
form.password = matched.password || ''
selectedPresetId.value = matched.id
}
} else {
// 新增场景:默认值(不写死假数据)
form.name = ''
......
......@@ -11,9 +11,13 @@
>
<option v-if="loadingTasks" disabled value="">加载中…</option>
<option v-else-if="taskOptions.length === 0" disabled value="">该分组暂无任务</option>
<option v-for="t in taskOptions" :key="t.id" :value="t.name">{{ t.name }}</option>
<option v-for="t in taskOptions" :key="t.id" :value="t.name" :data-task-id="t.id">{{ t.name }}</option>
</select>
<button class="btn btn-primary" :disabled="!selectedTask" @click="onQuery">查询</button>
<button
class="btn btn-primary"
:disabled="!selectedTask || runningQuery"
@click="onQuery"
>{{ runningQuery ? '查询中…' : '查询' }}</button>
</div>
</div>
</div>
......@@ -21,13 +25,20 @@
<div class="task-info-bar" v-if="selectedTask">
<el-icon><InfoFilled /></el-icon>
<span>当前任务:<b>{{ selectedTask }}</b></span>
<span class="meta">最近检测:2026-08-19 09:00 · 共扫描 10,234 条 · 命中不合规 <b style="color:#f56c6c;">12</b> 条</span>
<span class="meta" v-if="lastResult">
共扫描 <b>{{ lastResult.scanned }}</b> 条
· 命中不合规 <b style="color:#f56c6c;">{{ lastResult.bad_rows }}</b> 条
</span>
<span class="meta" v-else style="color:#909399;">尚未查询</span>
</div>
<!-- 结果区:表头操作 + 通用结果表 -->
<div class="q-card">
<div class="q-card__header">
<div class="q-card__title">不合规数据明细<span class="count">共 12 条</span></div>
<div class="q-card__title">
不合规数据明细
<span class="count" v-if="lastResult">共 {{ lastResult.bad_rows }} 条</span>
</div>
<div class="q-card__actions">
<button class="btn" @click="fieldDrawerOpen = true">字段配置</button>
<button class="btn">导出</button>
......@@ -36,8 +47,14 @@
</div>
</div>
<!-- 查询失败 / 警告:横幅 -->
<div v-if="queryError" class="query-error">
<el-icon><WarningFilled /></el-icon>
<span>{{ queryError }}</span>
</div>
<ResultTable
:columns="fields"
:columns="resultFields"
:rows="rows"
:page-sizes="[5, 10, 20, 50]"
:default-page-size="5"
......@@ -58,8 +75,9 @@
import { computed, ref, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fieldsByType, violationRows, fieldRuleSamples } from '@/data/mockData'
import { fieldsByType, violationRows } from '@/data/mockData' // violationRows 暂留:mock 表头;等真接通后用 resultFields
import { listTasks } from '@/api/tasks'
import { runQuery } from '@/api/queries'
import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue'
import ResultTable from '@/components/ResultTable.vue'
......@@ -69,8 +87,6 @@ const route = useRoute()
const type = computed(() => route.params.type || 'id-card')
// 路由 slug → 后端 task_group.name 的映射
// 左侧菜单的英文 slug 跟 DB 里分组的真实中文名不一致,前端硬维护一份
// (plan §3 决策:避免后端维护冗余 slug 字段)
const TYPE_TO_GROUP = {
'id-card': '身份证',
'mobile': '手机号',
......@@ -80,14 +96,13 @@ const TYPE_TO_GROUP = {
'date': '日期格式',
}
// 表格字段:业务字段(mock,组件会自动追加末尾的「备注」列)
// 注意:当前还是 mock,等任务真正跑起来后再用 task.field_list
// 表格字段 mock(仅在没查询前用,避免页面初次加载看起来空;查询后会用 resultFields 覆盖)
const fields = computed(() => fieldsByType[type.value] || [])
// ── 任务下拉:从后端 /api/tasks 拉当前 type 对应分组的全部任务 ──
const taskOptions = ref([]) // [{ id, name }]
// ── 任务下拉 ──
const taskOptions = ref([])
const loadingTasks = ref(false)
const selectedTask = ref('') // 任务名(不是 id,因为后面 onQuery 要传名)
const selectedTask = ref('')
async function loadTasksForCurrentType() {
const groupName = TYPE_TO_GROUP[type.value]
......@@ -100,9 +115,8 @@ async function loadTasksForCurrentType() {
try {
const list = await listTasks({ group: groupName })
taskOptions.value = (list || []).map((t) => ({ id: t.id, name: t.name }))
// 默认选中第一项;如果之前选中的还在列表里就保留(避免切换路由后丢失选择)
if (selectedTask.value && taskOptions.value.some((t) => t.name === selectedTask.value)) {
// keep
// 保留旧选择
} else {
selectedTask.value = taskOptions.value[0]?.name || ''
}
......@@ -114,27 +128,72 @@ async function loadTasksForCurrentType() {
loadingTasks.value = false
}
}
// 路由 type 变 → 重拉;首次挂载也要拉
watch(type, loadTasksForCurrentType, { immediate: true })
onMounted(loadTasksForCurrentType) // 保险:immediate=true 已经会跑,这里防 SSR/edge case
onMounted(loadTasksForCurrentType)
// 字段展示开关(默认全开)—— 暂时未应用在通用表里,预留给后续做字段隐藏
// 字段展示开关(默认全开)—— 暂未应用
const visibleFields = ref(fields.value.map((f) => f.key))
watch(fields, (fs) => { visibleFields.value = fs.map((f) => f.key) })
// 表格数据:把 mock 的 violationRows 复制并加 checked 字段
const rows = ref(violationRows.map((r) => ({ ...r, checked: false })))
// 操作
function onQuery() {
ElMessage.success(`已查询:${selectedTask.value}`)
// ── 查询 / 校验 ──
const runningQuery = ref(false)
const lastResult = ref(null) // 最近一次 runQuery 返回(行 + 计数)
const queryError = ref('') // 顶部错误横幅
const rows = ref([]) // 喂给 ResultTable 的行
const resultFields = ref([]) // 查询后用真实 task.field_list 覆盖表格表头
async function onQuery() {
// 找到选中的 task.id
const t = taskOptions.value.find((x) => x.name === selectedTask.value)
if (!t) {
ElMessage.warning('请选择有效任务')
return
}
runningQuery.value = true
queryError.value = ''
try {
const r = await runQuery(t.id, 1000)
if (!r.ok) {
// 后端明确返回失败:清空表 + 顶部横幅 + toast
queryError.value = r.message || '查询失败'
ElMessage.error(queryError.value)
rows.value = []
lastResult.value = null
return
}
// 成功:行 + 表头
rows.value = (r.rows || []).map((row) => ({ ...row, checked: false }))
resultFields.value = (r.field_list || []).map((f) => ({
key: f.field_key,
cn: f.field_key, // 后端没存中文名,先用英文 key 占位
en: '',
rules: f.rules || 0, // 数字 → 表头显示「⚠ N规则」
}))
lastResult.value = r
if (r.scanned === 0) {
ElMessage.warning('该表无数据(0 行)')
} else if (r.bad_rows === 0) {
ElMessage.success(`扫描 ${r.scanned} 条,全部合规`)
} else {
ElMessage.success(`扫描 ${r.scanned} 条,命中 ${r.bad_rows} 条不合规`)
}
} catch (e) {
queryError.value = `请求失败:${e.message}`
ElMessage.error(queryError.value)
rows.value = []
lastResult.value = null
} finally {
runningQuery.value = false
}
}
function onPageChange({ page, pageSize }) {
console.debug('[DataQualityView] page change', { page, pageSize })
}
// 占位操作(之前实现的 editRow / deleteRow / batchDelete / onImport 暂留,不影响主链路)
function editRow(row) {
ElMessageBox.alert(`打开「${row.user_id}」该条数据的编辑表单`, '修改')
ElMessageBox.alert(`打开「${row.user_id || row.id_card || '该行'}」的编辑表单(待实现)`, '修改')
}
function deleteRow(row) {
ElMessageBox.confirm('确定删除该条数据?', '提示', { type: 'warning' })
......@@ -159,13 +218,25 @@ function batchDelete() {
function onImport() {
ElMessage.info('触发导入(mock:实际将请求后端上传接口)')
}
// 抽屉保存
function onSaveFields(list) {
visibleFields.value = list
fieldDrawerOpen.value = false
ElMessage.success('已保存展示字段')
}
const fieldDrawerOpen = ref(false)
</script>
\ No newline at end of file
</script>
<style scoped>
.query-error {
margin: 12px 16px 0;
padding: 10px 14px;
background: #fef0f0;
border: 1px solid #fbc4c4;
color: #c0392b;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
</style>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment