Commit 5f6fe045 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 规则设置弹窗 · 从其他任务导入规则 + 字段中文标签

动机
1) 每建一个新任务都要从零手写规则,重复劳动严重。
   任务间的规则往往通用(手机号格式、身份证校验、年龄范围),希望能从已有任务复制。
2) 导入弹框只显示 ZJHM 这种英文列名,业务人员看不懂字段是啥意思。
   需要在字段标题后补字段中文标签(数据库列注释)。

改动
src/components/ImportRulesDialog.vue (新)
- 左右两栏布局(960px 宽):
  · 左列:所有非自身的任务列表(listTasks(),过滤掉 currentTaskId)
  · 右列:选中任务的所有字段+规则(getTask(id) 拿详情,按字段分组),每条规则 checkbox
- 字段标题:field_key + field_comment 横排显示(comment 为空时「(无字段注释)」灰斜体兜底)
- 顶部「全选 / 反选」;底部「已选 N 条 + 取消 + 导入 N 条」

src/components/RuleModal.vue
- 在 + 添加规则 下方加「📥 从其他任务导入规则」按钮(绿色边区分)
- 新增 taskId prop + importOpen ref + onImportRules()
- 去掉后端 id(避免保存时按 id 当 update 走错路径),用 Date.now()+count 生成临时占位 id
- 清空 testVal/testResult(让用户在新字段下重新测试)

src/components/TaskModal.vue
- <RuleModal> 加 :task-id="props.task?.id || 0"

backend/routers/tasks.py
- FieldOut 加 comment: Optional[str] = None
- 新增 _fetch_field_comments(t, conn):用任务自身的 conn_json 连一次数据源,
  调 db_adapter.list_columns() 拿 {field_key: column_comment}
  · 连接信息不全 / 连不上 / 表不存在 → 降级返回空 dict,不报错(comment 是辅助信息)
  · 复用既有 db_adapter,无新依赖;schema/table_name 走绑定参数无 SQL 注入
- _row_to_out(include_field_list=True) 时拉一次 comment_map 注入每个 FieldOut

关键决策
- 后端主动拉 comment 而非前端:前端拿不到密码
- 失败不报错:comment 缺失不影响主流程
- 不持久化到 Field 表:跟 plan §4 决策一致——数据字典不存,每次按 task 重新拉

Bug 修复
第一次实现时 computed 返 {fieldKey, fieldComment, rules},但模板里写了 v-if="group.comment",
comment 没定义 → 永远 falsy → 显示「(无字段注释」兜底。不报错只静默渲染空。
改成 v-if="group.fieldComment"。已记到 memory(vue-computed-template-prop-name-mismatch)。

验证
- 后端 TestClient: GET /api/tasks/24 → field_list[ZJHM].comment = '证件号码 | 数据标准: 01010007' ✓
- npx vite build → ✓ built in 7.09s(1640 modules,无报错)

注
- 4 个 web3/tests/*.png modified 未提交(与本功能无关)
- 后端有改动需用户重启进程
parent 5cb07f5a
......@@ -27,13 +27,17 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from web3.backend.db.database import get_session
from web3.backend.core.db_adapter import DBConfig, DBConnection
from web3.backend.models.field import Field
from web3.backend.models.rule import Rule
from web3.backend.models.task import Task
from web3.backend.models.task_group import TaskGroup
from web3.backend._logging import get_logger
router = APIRouter(prefix="/tasks", tags=["tasks"])
logger = get_logger("backend.routers.tasks")
# ── Pydantic schemas ──────────────────────────────────────────────
......@@ -95,6 +99,10 @@ class FieldOut(BaseModel):
show_default: bool
ord: int
rules: list["RuleOut"] = []
# 2026-08-24:字段中文标签 / 数据库列注释(从数据源 info_schema 实时拉,不持久化)
# 详情接口附带返回,ImportRulesDialog 用它来展示「字段是啥意思」
# 没拉到(连不上 / 没源表)时为 None —— 前端按 None 处理:只显示 field_key
comment: Optional[str] = None
class RuleOut(BaseModel):
......@@ -157,6 +165,70 @@ def _parse_conn_json(conn_json: Optional[str]) -> dict:
return {}
# 2026-08-24:db_type 中文 label → 内部小写值(与 db.py::_LABEL_TO_DB_TYPE 保持一致)
_TASK_DB_TYPE_LABEL_TO_INTERNAL = {
"MySQL": "mysql",
"Oracle": "oracle",
"达梦 DM": "dameng",
}
def _fetch_field_comments(t: Task, conn: dict) -> dict[str, str]:
"""从任务自身的数据源拉一次字段中文注释,返回 {field_key: comment} dict。
用于详情接口:让 ImportRulesDialog 能展示「这个字段是啥意思」,
不让前端单独再连一次库(拿不到密码)。
失败 / 没 source_table / 没密码:返回空 dict —— 前端按 None 处理。
"""
source_table = t.data_source_path or t.source_table
# 优先用 source_table 字段(任务保存了「sources/customer_db/customer.sqlite」形式)
# 拿不到表名 → 直接放弃
if not source_table:
return {}
table_name = source_table.split("/")[-1] if "/" in source_table else source_table
schema = conn.get("schema") or conn.get("database") or ""
db_type_label = t.db_type or "MySQL"
db_type_internal = _TASK_DB_TYPE_LABEL_TO_INTERNAL.get(db_type_label, db_type_label.lower())
host = conn.get("host")
port = conn.get("port")
user = conn.get("user")
password = conn.get("password")
database = conn.get("db")
if not (host and port and user and database):
# 连接信息不全(典型:密码缺失 / 任务用预设建但预设不存在)—— 不报错,直接降级
return {}
try:
cfg = DBConfig(
db_type=db_type_internal,
host=host,
port=int(port),
user=user,
password=password or "",
database=database,
oracle_client_dir=conn.get("oracleClientDir"),
)
with DBConnection(cfg) as db:
rows = db.list_columns(schema, table_name)
except Exception as e:
# 连不上 / 权限不够 / 表不存在 —— 都不致命,只是不显示 comment
logger.warning(
f"[tasks] _fetch_field_comments 失败(task_id={t.id} table={table_name!r}):"
f"{type(e).__name__}: {e}"
)
return {}
out: dict[str, str] = {}
for r in rows:
key = r.get("column_name") or r.get("COLUMN_NAME")
comment = r.get("column_comment") or r.get("COLUMN_COMMENT")
if key and comment:
out[key] = str(comment)
return out
def _replace_fields(db: Session, task: Task, fields_payload: list[FieldPayload]) -> None:
"""整体替换 task 下的 fields + rules(先清空再插入)。
......@@ -242,6 +314,8 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta
field_list: list[FieldOut] = []
if include_field_list:
# 2026-08-24:详情接口顺带从数据源拉一次列注释(连不上 / 没 source_table 时降级为空 dict)
comment_map = _fetch_field_comments(t, conn)
for f in db.query(Field).filter(Field.task_id == t.id).order_by(Field.ord).all():
rules = [
RuleOut(
......@@ -261,6 +335,7 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta
show_default=bool(f.show_default),
ord=f.ord,
rules=rules,
comment=comment_map.get(f.field_key), # 2026-08-24:字段中文标签(连不上时 None)
))
return TaskOut(
......
This diff is collapsed.
......@@ -134,6 +134,10 @@
<div v-if="ruleList.length === 0" class="rule-empty-tip">还没有规则,点击下方按钮添加</div>
<button class="add-rule-btn" @click="addRule">+ 添加规则</button>
<!-- 2026-08-24:从其他任务导入规则 -->
<button class="add-rule-btn import-rule-btn" @click="importOpen = true">
📥 从其他任务导入规则
</button>
</div>
<div class="modal-footer">
<button class="btn" @click="$emit('update:open', false)">取消</button>
......@@ -141,6 +145,13 @@
</div>
</div>
</div>
<!-- 从其他任务导入规则弹框 -->
<ImportRulesDialog
v-model:open="importOpen"
:current-task-id="props.taskId || 0"
@import="onImportRules"
/>
</Teleport>
</template>
......@@ -148,10 +159,13 @@
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { genRule as apiGenRule, testRule as apiTestRule } from '@/api/ai'
import ImportRulesDialog from '@/components/ImportRulesDialog.vue'
const props = defineProps({
open: { type: Boolean, default: false },
field: { type: Object, default: null },
// 2026-08-24:当前编辑的任务 id —— 传给 ImportRulesDialog 用于过滤自身
taskId: { type: Number, default: 0 },
})
const emit = defineEmits(['update:open', 'save'])
......@@ -172,6 +186,9 @@ const title = computed(() => {
const ruleList = ref([])
const aiLoading = ref({})
// 2026-08-24:从其他任务导入规则弹框开关
const importOpen = ref(false)
// 「每条规则的 name + desc + code/regex 都填了」才能保存(避免保存半成品规则)
// 注意:ruleList 可以为空 —— 用户明确「删空所有规则」就该能保存,让该字段回到无规则状态
// 2026-08-24:name 也是必填(前端展示「违反哪条规则」用)
......@@ -254,6 +271,31 @@ function removeRule(idx) {
ruleList.value.splice(idx, 1)
}
// 2026-08-24:从其他任务导入规则 —— 把 ImportRulesDialog 选中的规则 push 到 ruleList
// 注意:去掉后端 id(避免保存时按 id 当成已有规则 update),用临时 id 占位(保存时统一按数组顺序重建)
function onImportRules(rules) {
if (!Array.isArray(rules) || rules.length === 0) return
let count = 0
for (const r of rules) {
ruleList.value.push({
// 用 Date.now() + count 防同毫秒内导入多条时 id 冲突
id: Date.now() + count,
name: r.name || '',
desc: r.desc || '',
rule_type: r.ruleType || DEFAULT_RULE_TYPE,
regex: r.regex || '',
code: r.code || '',
skip_null: !!r.skipNull,
// 导入后让用户重新测试 —— 不同字段下原值可能通不过
testVal: '',
testResult: null,
})
count++
}
// 滚动到新导入的规则区域(让用户立刻看到)
// 这里只 push,不主动滚动;父容器可滚动自然能看到
}
// AI 生成:按当前规则类型调后端 /api/ai/rule
async function genAI(idx) {
const rule = ruleList.value[idx]
......@@ -409,6 +451,20 @@ function onSave() {
margin: 0;
}
/* 2026-08-24:从其他任务导入规则按钮 —— 复用 .add-rule-btn(global.css)样式,
跟 + 添加规则 视觉上拉开一点距离,用绿色边表示「导入」语义 */
.import-rule-btn {
margin-top: 8px;
color: #67c23a;
border-color: #c2e7b0;
background: #f0f9eb;
}
.import-rule-btn:hover {
border-color: #67c23a;
color: #fff;
background: #67c23a;
}
/* Python 函数编辑框:等宽字体 + 稍高 + 灰底 */
.code-line {
display: flex;
......
......@@ -235,7 +235,12 @@
</Teleport>
<!-- 规则弹窗 -->
<RuleModal v-model:open="ruleModalOpen" :field="activeField" @save="onRuleSave" />
<RuleModal
v-model:open="ruleModalOpen"
:field="activeField"
:task-id="props.task?.id || 0"
@save="onRuleSave"
/>
</template>
<script setup>
......
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