Commit 4aed2229 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): AI 生成正则 + Python re 测试 + 规则弹窗保存按钮

LLM 接入(P9 plan 阶段)
- 后端:
  - backend/core/llm.py 从 web/core/llm.py 拷贝,default_config_path() 改读 web3/backend/configs/llm.yaml
  - backend/core/ai_regex.py:强制走 LLM(删规则库),LLM 返回用 re.compile 校验,编译失败回弹修复,最多 5 次重试
  - backend/routers/ai.py:注册 POST /api/ai/regex
  - backend/configs/llm.example.yaml:多 provider 示例模板(真实 llm.yaml 已 gitignore)
  - backend/app.py:注册 ai_router
  - backend/requirements.txt:+anthropic +openai +pyyaml
- 前端:
  - src/api/ai.js:genRegex(desc)
  - src/components/RuleModal.vue:删前端 mock 改调后端

测试改走 Python re(避免 JS RegExp 方言差异)
- backend/core/ai_regex.py:+test_regex(regex, value) → {ok, match, error, groups}
- backend/routers/ai.py:+POST /api/regex/test
- src/api/ai.js:+testRegex(regex, value)
- src/components/RuleModal.vue:testRegex() 改调后端,捕获组带回前端展示

规则弹窗保存按钮
- src/components/RuleModal.vue:emit 'save' + 取消/保存两按钮 + onSave() + hasValidRules
  (必须 ≥1 条规则 + desc/regex 都非空才允许保存)
- watch 监听 [open, field.key],每次打开从父组件重新同步
- src/components/TaskModal.vue:onRuleSave 回写 field.rules(徽章数字刷新)
- TaskModal onSave() payload 加 fields: [{key, en, cn, rules, show_default}](P4 落库用)

端到端验证
- 身份证/URL/UUID/产品型号 多 desc 都通过 LLM 生成 + Python re 校验
- 7 例 /api/regex/test 覆盖:真身份证、短串、捕获组日期、分隔符错、空 value、语法错、空 regex

踩坑
- 规则库'数字'关键词过宽易误命中,按用户意见删整段强制走 LLM
- curl 中文 JSON body 在 Git Bash 转义破坏,用 --data @file 或 printf
- python web3/backend/start.py 报 ModuleNotFoundError,要 python -m web3.backend.start
- Vue3 const 不 hoist,hasValidRules 必须放在 ruleList 声明之后
parent 130b8398
...@@ -50,6 +50,7 @@ app.add_middleware( ...@@ -50,6 +50,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.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.db import router as db_router # noqa: E402
from web3.backend.routers.task_groups import router as task_groups_router # noqa: E402 from web3.backend.routers.task_groups import router as task_groups_router # noqa: E402
...@@ -58,7 +59,8 @@ app.include_router(task_groups_router, prefix="/api") ...@@ -58,7 +59,8 @@ app.include_router(task_groups_router, prefix="/api")
app.include_router(tasks_router, prefix="/api") app.include_router(tasks_router, prefix="/api")
app.include_router(db_router, prefix="/api") app.include_router(db_router, prefix="/api")
app.include_router(connection_presets_router, prefix="/api") app.include_router(connection_presets_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks, /api/connect/*, /api/connection-presets") app.include_router(ai_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks, /api/connect/*, /api/connection-presets, /api/ai/*")
@app.get("/api/health") @app.get("/api/health")
......
# ============================================================
# LLM 配置文件示例 — web3 版本(2026-08-20)
# ============================================================
# 用法:复制本文件为 llm.yaml,按需填入真实 key。
# 真实 llm.yaml 已在 .gitignore,不会上传到仓库。
# ============================================================
# ── 方式 1:MiniMax(推荐本项目用户使用) ──
provider: minimax
api_key: sk-请填入你的_MiniMax_API_Key
base_url: https://api.minimaxi.com/anthropic
model: MiniMax-Text-01
# ── 方式 2:Anthropic Claude ──
# provider: anthropic
# api_key: sk-ant-xxxxx
# model: claude-sonnet-5
# (base_url 留空,用 Anthropic 官方)
# ── 方式 3:OpenAI ──
# provider: openai
# api_key: sk-xxxxx
# model: gpt-4o
# base_url: https://api.openai.com/v1
# ── 方式 4:自部署 / 第三方代理(OpenAI 兼容) ──
# provider: openai
# api_key: 自定义
# base_url: https://your-proxy.com/v1
# model: your-model-name
# 生成参数(通用)
max_tokens: 1024
temperature: 0.2
timeout: 30
\ No newline at end of file
"""AI 生成正则服务(包装 web3.backend.core.llm)
强制走 LLM,不做规则库兜底(2026-08-20 用户决策):
- 每次请求都过 LLM,让模型能见到真实描述(持续学习用户表述)
- 拿到正则后用 re.compile 在 Python 里真实校验
- 编译失败则把错误反馈回 LLM 让它修复,最多 5 次
降级策略:
- LLM 不可用(无 key / 网络挂)→ 直接返回空串 + 提示文案,不抛异常
- 5 次重试全部失败 → 返回空串 + 失败提示,由前端展示
"""
from __future__ import annotations
import re
from web3.backend.core.llm import LLMUnavailable, get_llm_client
from web3.backend._logging import get_logger
logger = get_logger("backend.ai_regex")
# ── LLM Prompt ────────────────────────────────────────────
# 系统提示词必须明确:
# - 你是 Python re 正则专家
# - 只输出一行正则字符串(不要 markdown、解释、前后缀)
# - 严格匹配("必须是 18 位" → 长度恰好 18,不要写 * 或 +)
# - 失败时(带前次错误信息)只输出修复后的正则,不要任何多余内容
_SYSTEM_PROMPT = """你是 Python 正则表达式专家。"""
_USER_TEMPLATE_INITIAL = """请把以下规则描述转换成一个 Python 正则表达式(re 语法)。
规则描述:「{desc}」
要求:
1. 只输出一行正则字符串,不要任何解释、不要 Markdown 代码块、不要前后缀
2. 严格匹配("必须是 N 位" → 长度恰好 N,不要写成 {{N,}} 或 *)
3. 中国大陆场景默认值:手机 11 位、邮编 6 位、身份证 18 位末位可为 X、日期 yyyy-MM-dd
4. 用 ^ 与 $ 锚定首尾(除非描述里明确说"包含")
5. 中文用 \\u4e00-\\u9fff 这种 Unicode 区间,不要写汉字本身
只输出一行正则字符串。"""
_USER_TEMPLATE_FIX = """上一次生成的正则编译失败。
规则描述:「{desc}」
上次生成的正则:`{bad_regex}`
Python 报的错误:`{error_msg}`
请输出修复后的正则字符串。
要求:
1. 只输出一行正则字符串,不要任何解释、不要 Markdown 代码块、不要前后缀
2. 修掉上面的语法错误(注意字符类、转义、括号配对)
3. 严格匹配("必须是 N 位" → 长度恰好 N)
4. 用 ^ 与 $ 锚定首尾
只输出一行修复后的正则字符串。"""
# ── 从 LLM 返回中提取正则 ─────────────────────────────────
# LLM 偶尔会包裹一层 ```python ... ``` 或加引号,这里做容错提取。
_REGEX_FENCE = re.compile(r"^```(?:python|regex|re)?\s*\n?(.*?)\n?```$", re.DOTALL)
def _extract_regex(text: str) -> str:
"""从 LLM 返回中提取「那一行正则」。
容忍:
- markdown 代码块包裹
- 开头/结尾的引号
- 前后的"答案:xxx"说明文字(取第一个像正则的行)
"""
if not text:
return ""
text = text.strip()
# 1) 代码块包裹
m = _REGEX_FENCE.match(text)
if m:
return m.group(1).strip()
# 2) 首尾引号
if (text.startswith('"') and text.endswith('"')) or \
(text.startswith("'") and text.endswith("'")):
return text[1:-1].strip()
# 3) 多行:取第一非空行
for line in text.splitlines():
line = line.strip()
if line:
return line
return ""
def _validate_regex(regex: str) -> tuple[bool, str]:
"""用 re.compile 校验;返回 (ok, error_msg)"""
if not regex:
return False, "空字符串"
try:
re.compile(regex)
return True, ""
except re.error as e:
return False, f"{type(e).__name__}: {e}"
def test_regex(regex: str, value: str) -> dict:
"""用 Python re 校验 value 是否匹配 regex。
返回 dict:
- ok: bool 编译是否成功
- match: bool 值是否匹配(regex/re.search 语义)
- error: str 编译/匹配错误信息(成功时空串)
- groups: list 捕获组内容(无捕获组时空 list)
"""
if not regex:
return {"ok": False, "match": False, "error": "正则为空", "groups": []}
try:
compiled = re.compile(regex)
except re.error as e:
return {"ok": False, "match": False, "error": f"编译失败: {type(e).__name__}: {e}", "groups": []}
if value is None or value == "":
return {"ok": True, "match": False, "error": "测试值为空", "groups": []}
m = compiled.search(value)
if not m:
return {"ok": True, "match": False, "error": "", "groups": []}
# 把捕获组也带回去(方便 UI 展示分组提取)
try:
groups = [g if g is not None else "" for g in m.groups()]
except Exception:
groups = []
return {"ok": True, "match": True, "error": "", "groups": groups}
# ── 主入口 ─────────────────────────────────────────────
def gen_regex(desc: str) -> tuple[str, str]:
"""强制调 LLM 生成 Python 正则。
返回 (regex, note):
- regex 非空 = 成功;空 = 失败
- note 给前端弹窗用的提示文案
流程:LLM 调用 + re.compile 校验 → 失败最多重试 5 次 → 失败空串
"""
desc = (desc or "").strip()
if not desc:
return "", "规则描述为空"
client = get_llm_client()
if not client.available:
return "", "LLM 未配置 API Key,请联系管理员配置或手动填写"
bad_regex = ""
error_msg = ""
last_regex = ""
MAX_TRIES = 5
for attempt in range(1, MAX_TRIES + 1):
try:
if attempt == 1:
prompt = _USER_TEMPLATE_INITIAL.format(desc=desc)
else:
prompt = _USER_TEMPLATE_FIX.format(
desc=desc, bad_regex=bad_regex, error_msg=error_msg
)
text = client.complete(prompt, system=_SYSTEM_PROMPT)
last_regex = _extract_regex(text)
ok, err = _validate_regex(last_regex)
if ok:
logger.info(f"AI 正则:LLM 第 {attempt} 次成功 desc={desc!r} → {last_regex!r}")
return last_regex, f"已用 LLM 生成(第 {attempt} 次)"
# 校验失败 → 把错误反馈给下一轮
bad_regex = last_regex
error_msg = err
logger.warning(
f"AI 正则:LLM 第 {attempt}/{MAX_TRIES} 次生成的正则编译失败 "
f"desc={desc!r} regex={last_regex!r} err={err}"
)
except LLMUnavailable as e:
logger.warning(f"AI 正则:LLM 第 {attempt} 次调用失败 desc={desc!r} err={e}")
return "", f"LLM 调用失败:{e}"
return "", f"LLM 重试 {MAX_TRIES} 次仍未生成合法正则(最后错误:{error_msg}),请补充描述或手动填写"
\ No newline at end of file
This diff is collapsed.
...@@ -5,4 +5,10 @@ pydantic>=2.5 ...@@ -5,4 +5,10 @@ pydantic>=2.5
# 数据库驱动(web3/backend/core/db_adapter.py 需要;dmPython 装不上不影响启动,只影响连达梦) # 数据库驱动(web3/backend/core/db_adapter.py 需要;dmPython 装不上不影响启动,只影响连达梦)
pymysql>=1.1 pymysql>=1.1
oracledb>=1.4 oracledb>=1.4
dmPython>=2.5 dmPython>=2.5
\ No newline at end of file # LLM(web3/backend/core/ai_regex.py → core/llm.py 用)
# anthropic 走 Anthropic SDK(MiniMax 也走这个,兼容接口);openai 走 OpenAI SDK
# pyyaml 读 web3/backend/configs/llm.yaml
anthropic>=0.30
openai>=1.30
pyyaml>=6.0
\ No newline at end of file
"""AI 相关 API(web3)
端点:
POST /api/ai/regex body: {desc: str} → {ok, regex, note}
POST /api/regex/test body: {regex, value} → {ok, match, error, groups}
「测试」端点走 Python re(不依赖前端 JS RegExp),保证 LLM 生成的 Python 正则
能被同一种语义验证,避免 JS/Python 正则方言差异导致误判。
"""
from __future__ import annotations
import time
from fastapi import APIRouter
from pydantic import BaseModel, Field
from web3.backend.core.ai_regex import gen_regex, test_regex
from web3.backend._logging import get_logger
router = APIRouter(prefix="", tags=["ai"])
logger = get_logger("backend.routers.ai")
# ── /ai/regex ─────────────────────────────────────────────
class GenRegexRequest(BaseModel):
desc: str = Field(..., min_length=1, description="规则自然语言描述")
class GenRegexResponse(BaseModel):
ok: bool
regex: str = ""
note: str = ""
@router.post("/ai/regex", response_model=GenRegexResponse, summary="AI 生成 Python 正则")
async def ai_gen_regex(req: GenRegexRequest):
"""前端「规则设置」弹窗的「AI 生成正则」按钮调用。
返回 ok=true 表示 regex 非空;ok=false 表示降级(无 key / 描述不清 / 5 次重试全败)。
"""
logger.info("─" * 60)
logger.info(f"POST /api/ai/regex desc={req.desc!r}")
started = time.monotonic()
try:
regex, note = gen_regex(req.desc)
except Exception as e:
elapsed_ms = (time.monotonic() - started) * 1000
logger.exception(f"❌ AI 正则服务抛出异常(耗时 {elapsed_ms:.0f}ms)")
logger.info("─" * 60)
return GenRegexResponse(ok=False, regex="", note=f"AI 正则服务异常: {type(e).__name__}: {e}")
elapsed_ms = (time.monotonic() - started) * 1000
if regex:
logger.info(f"✅ AI 正则生成成功(耗时 {elapsed_ms:.0f}ms)regex={regex!r}")
else:
logger.warning(f"❌ AI 正则生成失败(耗时 {elapsed_ms:.0f}ms)note={note!r}")
logger.info("─" * 60)
return GenRegexResponse(ok=bool(regex), regex=regex, note=note)
# ── /regex/test ───────────────────────────────────────────
class TestRegexRequest(BaseModel):
regex: str = Field(..., min_length=1, description="要测试的正则表达式")
value: str = Field("", description="测试值")
class TestRegexResponse(BaseModel):
ok: bool # 正则编译是否成功
match: bool # value 是否匹配
error: str = "" # 编译/匹配错误信息
groups: list[str] = [] # 捕获组内容(无捕获组或未匹配时空)
@router.post("/regex/test", response_model=TestRegexResponse, summary="用 Python re 校验 value 是否匹配 regex")
async def ai_test_regex(req: TestRegexRequest):
"""前端「规则设置」弹窗的「测试」按钮调用。
用 Python re 校验(不是 JS RegExp),保证和后端检测引擎语义一致。
"""
result = test_regex(req.regex, req.value)
return TestRegexResponse(**result)
\ No newline at end of file
/**
* AI 相关 API(2026-08-20 接入)
*
* - genRegex(desc) → POST /api/ai/regex 后端走 LLM(强制,无规则库)+ re.compile 校验
* - testRegex(...) → POST /api/regex/test 后端用 Python re 校验(不是 JS RegExp)
*/
import { http } from './client'
/**
* AI 生成正则表达式
* @param {string} desc 规则说明(自然语言)
* @returns { ok: boolean, regex: string, note: string }
* - ok=true:regex 非空,可直接填到「正则表达式」输入框
* - ok=false:regex 空字符串;note 是失败原因(前端展示)
*/
export function genRegex(desc) {
return http.post('/ai/regex', { desc })
}
/**
* 用 Python re 校验 value 是否匹配 regex
* @param {string} regex
* @param {string} value
* @returns { ok: boolean, match: boolean, error: string, groups: string[] }
* - ok false = 正则编译失败;error 是 Python 报错(不是 JS 报错)
* - ok true + match true/false = 是否命中
* - groups = 捕获组内容(无捕获组或未匹配时 [])
*/
export function testRegex(regex, value) {
return http.post('/regex/test', { regex, value })
}
\ No newline at end of file
...@@ -54,7 +54,8 @@ ...@@ -54,7 +54,8 @@
<button class="add-rule-btn" @click="addRule">+ 添加规则</button> <button class="add-rule-btn" @click="addRule">+ 添加规则</button>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" @click="$emit('update:open', false)">关闭</button> <button class="btn" @click="$emit('update:open', false)">取消</button>
<button class="btn btn-primary" :disabled="!hasValidRules" @click="onSave">保存</button>
</div> </div>
</div> </div>
</div> </div>
...@@ -64,12 +65,13 @@ ...@@ -64,12 +65,13 @@
<script setup> <script setup>
import { computed, reactive, ref, watch } from 'vue' import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { genRegex as apiGenRegex, testRegex as apiTestRegex } from '@/api/ai'
const props = defineProps({ const props = defineProps({
open: { type: Boolean, default: false }, open: { type: Boolean, default: false },
field: { type: Object, default: null }, field: { type: Object, default: null },
}) })
defineEmits(['update:open']) const emit = defineEmits(['update:open', 'save'])
const title = computed(() => { const title = computed(() => {
if (!props.field) return '规则设置' if (!props.field) return '规则设置'
...@@ -79,10 +81,16 @@ const title = computed(() => { ...@@ -79,10 +81,16 @@ const title = computed(() => {
const ruleList = ref([]) const ruleList = ref([])
const aiLoading = ref({}) const aiLoading = ref({})
watch(() => [props.open, props.field], ([open, f]) => { // 「至少有一条规则且 desc+regex 都填了」才能保存(避免保存空规则)
if (open && f) { // 必须在 ruleList 之后定义(const 引用前要先声明)
// 深拷贝字段上的规则,避免直接改父组件 const hasValidRules = computed(() =>
ruleList.value = (f.rules || []).map((r) => ({ ...r, testVal: '', testResult: null })) ruleList.value.length > 0 && ruleList.value.every((r) => r.desc.trim() && r.regex.trim())
)
watch(() => [props.open, props.field?.key], ([open]) => {
if (open && props.field) {
// 每次打开都从 field.rules 重新同步一次(保证父组件保存后再开看到的是最新)
ruleList.value = (props.field.rules || []).map((r) => ({ ...r, testVal: '', testResult: null }))
aiLoading.value = {} aiLoading.value = {}
} }
}, { immediate: true }) }, { immediate: true })
...@@ -100,41 +108,29 @@ function removeRule(idx) { ...@@ -100,41 +108,29 @@ function removeRule(idx) {
ruleList.value.splice(idx, 1) ruleList.value.splice(idx, 1)
} }
// AI 模拟:根据规则说明返回正则 // AI 生成正则:调后端 POST /api/ai/regex
function aiGenerateRegex(desc) { // 后端流程:规则库命中 → LLM 调用 + re.compile 校验(最多 5 次重试)→ 返回
if (/身份证|id_card/i.test(desc)) return { regex: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$', note: '已识别为18位身份证号' } async function genRegex(idx) {
if (/手机|电话/.test(desc)) return { regex: '^1[3-9]\\d{9}$', note: '已识别为11位手机号' }
if (/邮箱|email|邮件/i.test(desc)) return { regex: '^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$', note: '已识别为邮箱格式' }
if (/银行卡|卡号/.test(desc)) return { regex: '^\\d{16,19}$', note: '已识别为银行卡号' }
if (/日期|yyyy/i.test(desc)) return { regex: '^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', note: '已识别为日期格式' }
if (/邮编/.test(desc)) return { regex: '^[1-9]\\d{5}$', note: '已识别为6位邮编' }
if (/不能为空|非空|不能空/.test(desc)) return { regex: '^.+$', note: '已识别为非空校验' }
if (/正整数/.test(desc)) return { regex: '^[1-9]\\d*$', note: '已识别为正整数' }
if (/纯数字|数字/.test(desc)) return { regex: '^\\d+$', note: '已识别为纯数字' }
const m = desc.match(/(\d+)\s*[-到至]\s*(\d+)\s*个/)
if (m) return { regex: `^.{${m[1]},${m[2]}}$`, note: `已识别为长度${m[1]}-${m[2]}位` }
const n = desc.match(/(\d+)\s*位/)
if (n) return { regex: `^.{${n[1]}}$`, note: `已识别为固定${n[1]}位` }
return { regex: '', note: '未能识别,请补充描述或手动填写正则' }
}
function genRegex(idx) {
const rule = ruleList.value[idx] const rule = ruleList.value[idx]
if (!rule.desc.trim()) { if (!rule.desc.trim()) {
ElMessage.warning('请先填写规则说明') ElMessage.warning('请先填写规则说明')
return return
} }
aiLoading.value[idx] = true aiLoading.value[idx] = true
setTimeout(() => { try {
const r = aiGenerateRegex(rule.desc) const r = await apiGenRegex(rule.desc)
aiLoading.value[idx] = false if (r.ok && r.regex) {
if (r.regex) {
rule.regex = r.regex rule.regex = r.regex
ElMessage.success(r.note) ElMessage.success(r.note || '已生成正则')
} else { } else {
ElMessage.warning(r.note) // 后端降级(无 key / 重试 5 次失败 / 描述不清):弹 warning,把 note 给用户看
ElMessage.warning(r.note || '生成失败,请手动填写或补充描述')
} }
}, 500) } catch (e) {
ElMessage.error(`AI 生成失败:${e.message}`)
} finally {
aiLoading.value[idx] = false
}
} }
function copyRegex(rule) { function copyRegex(rule) {
...@@ -146,12 +142,29 @@ function copyRegex(rule) { ...@@ -146,12 +142,29 @@ function copyRegex(rule) {
function testRegex(idx) { function testRegex(idx) {
const rule = ruleList.value[idx] const rule = ruleList.value[idx]
if (!rule.regex) { rule.testResult = { text: '无正则', cls: 'fail' }; return } if (!rule.regex) { rule.testResult = { text: '无正则', cls: 'fail' }; return }
if (!rule.testVal) { rule.testResult = { text: '请输入', cls: 'fail' }; return } apiTestRegex(rule.regex, rule.testVal).then((r) => {
try { if (!r.ok) {
const ok = new RegExp(rule.regex).test(rule.testVal) rule.testResult = { text: r.error || '语法错误', cls: 'fail' }
rule.testResult = { text: ok ? '✓ 通过' : '✗ 不匹配', cls: ok ? 'pass' : 'fail' } } else if (!r.match) {
} catch (e) { rule.testResult = { text: rule.testVal ? '✗ 不匹配' : '请输入', cls: 'fail' }
rule.testResult = { text: '语法错误', cls: 'fail' } } else {
} const g = r.groups && r.groups.length
? `(捕获:${r.groups.join(', ')})`
: ''
rule.testResult = { text: `✓ 通过${g}`, cls: 'pass' }
}
}).catch((e) => {
rule.testResult = { text: `测试失败:${e.message}`, cls: 'fail' }
})
}
// 保存:把 UI 状态(testVal/testResult)剥掉,只留真实数据 emit 给父组件
function onSave() {
const rules = ruleList.value.map((r) => ({
desc: r.desc.trim(),
regex: r.regex.trim(),
}))
emit('save', { fieldKey: props.field?.key, rules })
emit('update:open', false)
} }
</script> </script>
\ No newline at end of file
...@@ -229,7 +229,7 @@ ...@@ -229,7 +229,7 @@
</Teleport> </Teleport>
<!-- 规则弹窗 --> <!-- 规则弹窗 -->
<RuleModal v-model:open="ruleModalOpen" :field="activeField" /> <RuleModal v-model:open="ruleModalOpen" :field="activeField" @save="onRuleSave" />
</template> </template>
<script setup> <script setup>
...@@ -530,6 +530,13 @@ function openRule(f) { ...@@ -530,6 +530,13 @@ function openRule(f) {
ruleModalOpen.value = true ruleModalOpen.value = true
} }
// 规则保存回写:RuleModal.emit('save', {fieldKey, rules})
// 把 rules 写回对应字段的 field.rules,让「规则设置 N」徽章实时刷新
function onRuleSave({ fieldKey, rules }) {
const f = fieldList.value.find((x) => x.key === fieldKey)
if (f) f.rules = rules
}
function onSave() { function onSave() {
if (!form.name) { if (!form.name) {
ElMessage.warning('请填写任务名称') ElMessage.warning('请填写任务名称')
...@@ -537,7 +544,7 @@ function onSave() { ...@@ -537,7 +544,7 @@ function onSave() {
return return
} }
// 后端 P4 才接 fields/rules,P3 只送头部 + conn_json // 后端 P4 才接 fields/rules,P3 只送头部 + conn_json
// (弹窗里改的字段/规则暂存 form,下次保存会带上;当前阶段不写库) // 字段+规则先挂 payload 上,等后端接好就发;目前后端忽略
const connJson = JSON.stringify({ const connJson = JSON.stringify({
connName: form.connName, connName: form.connName,
host: form.host, host: form.host,
...@@ -558,6 +565,13 @@ function onSave() { ...@@ -558,6 +565,13 @@ function onSave() {
source_table: form.table || null, source_table: form.table || null,
description: form.desc, description: form.desc,
conn_json: connJson, conn_json: connJson,
fields: fieldList.value.map((f) => ({ // P4 落库用
key: f.key,
en: f.en,
cn: f.cn,
rules: f.rules || [],
show_default: f.showDefault,
})),
} }
emit('saved', payload) emit('saved', payload)
} }
......
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