Commit a07af9cc authored by Data Governance Dev's avatar Data Governance Dev

fix(web3): RuleModal 测试按钮「未填写」提示优化

之前 code 为空只笼统提示「未填写」,用户难定位是哪个框没填;
现在分别提示「请先填写正则表达式」/「请先填写 Python 函数」/
「请输入测试值」,避免和 textarea 的 placeholder 灰色示例混淆。
parent 8ea2935f
......@@ -13,35 +13,81 @@
<span class="rnum">规则{{ idx + 1 }}</span>
<button class="btn-text danger" @click="removeRule(idx)">删除</button>
</div>
<!-- 规则类型切换:regex / number / date -->
<div class="rule-item-row">
<span class="rule-item-label">规则类型</span>
<div class="rule-item-ctrl">
<div class="rule-type-tabs">
<div
v-for="t in RULE_TYPES"
:key="t.value"
class="rule-type-tab"
:class="{ active: rule.rule_type === t.value }"
:title="t.hint"
@click="rule.rule_type = t.value"
>{{ t.label }}</div>
</div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl">
<textarea v-model="rule.desc" placeholder="用自然语言描述规则,如:必须是18位中国大陆身份证号,最后一位可为X"></textarea>
<textarea v-model="rule.desc" :placeholder="descPlaceholder(rule.rule_type)"></textarea>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl">
<button class="btn ai-btn" :disabled="aiLoading[idx]" @click="genRegex(idx)">
{{ aiLoading[idx] ? '✨ AI 分析中…' : '✨ AI 生成正则' }}
<button class="btn ai-btn" :disabled="aiLoading[idx]" @click="genAI(idx)">
{{ aiLoading[idx] ? '✨ AI 分析中…' : aiBtnLabel(rule.rule_type) }}
</button>
</div>
</div>
<div class="rule-item-row">
<!-- regex 类型:单行输入框 -->
<div v-if="rule.rule_type === 'regex'" class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line">
<input type="text" class="text-input" v-model="rule.regex" placeholder="点击AI生成或手动填写" />
<button class="btn" @click="copyRegex(rule)">复制</button>
<button class="btn" @click="copyText(rule.regex, '已复制正则')">复制</button>
</div>
</div>
</div>
<!-- number / date 类型:多行 Python 函数编辑框 -->
<div v-else class="rule-item-row">
<span class="rule-item-label">Python 函数</span>
<div class="rule-item-ctrl">
<div class="code-line">
<textarea
class="code-input"
v-model="rule.code"
:placeholder="codePlaceholder(rule.rule_type)"
spellcheck="false"
></textarea>
<button class="btn" @click="copyText(rule.code, '已复制代码')">复制</button>
</div>
<div class="form-hint">
<span v-if="rule.rule_type === 'date'">
datetime 模块已预注入,直接用 datetime.strptime(v, '%Y-%m-%d') 即可;不需要 import。
</span>
<span v-else>
需要数学常量时用 math 模块;不需要 import。
</span>
</div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line">
<input type="text" class="text-input" v-model="rule.testVal" placeholder="输入测试值" />
<button class="btn" @click="testRegex(idx)">测试</button>
<button class="btn" @click="testRule(idx)">测试</button>
<span class="test-result" :class="rule.testResult?.cls" v-if="rule.testResult">{{ rule.testResult.text }}</span>
</div>
</div>
......@@ -65,7 +111,7 @@
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { genRegex as apiGenRegex, testRegex as apiTestRegex } from '@/api/ai'
import { genRule as apiGenRule, testRule as apiTestRule } from '@/api/ai'
const props = defineProps({
open: { type: Boolean, default: false },
......@@ -73,6 +119,14 @@ const props = defineProps({
})
const emit = defineEmits(['update:open', 'save'])
// 规则类型定义集中在一处:tab 标签、AI 按钮文案、占位符都引用
const RULE_TYPES = [
{ value: 'regex', label: '正则', hint: '用 Python 正则表达式校验文本(身份证/手机号/邮箱 等)' },
{ value: 'number', label: '数值', hint: '用 Python 函数 def check(value) -> bool 校验数值' },
{ value: 'date', label: '日期', hint: '用 Python 函数 + datetime.strptime 校验日期' },
]
const DEFAULT_RULE_TYPE = 'regex'
const title = computed(() => {
if (!props.field) return '规则设置'
return `规则设置 - ${props.field.en}(${props.field.cnName || props.field.cn})`
......@@ -81,16 +135,45 @@ const title = computed(() => {
const ruleList = ref([])
const aiLoading = ref({})
// 「至少有一条规则且 desc+regex 都填了」才能保存(避免保存空规则)
// 必须在 ruleList 之后定义(const 引用前要先声明)
// 「至少有一条规则且 desc + code/regex 都填了」才能保存(避免保存空规则)
const hasValidRules = computed(() =>
ruleList.value.length > 0 && ruleList.value.every((r) => r.desc.trim() && r.regex.trim())
ruleList.value.length > 0 && ruleList.value.every((r) => {
if (!r.desc.trim()) return false
if ((r.rule_type || 'regex') === 'regex') return r.regex.trim()
return r.code.trim() // number / date
})
)
// 规则说明 / 函数 / AI 按钮文案,按 rule_type 切换
function descPlaceholder(rt) {
if (rt === 'number') return '如:年龄必须在 0 到 150 之间'
if (rt === 'date') return '如:日期必须是 yyyy-MM-dd 格式且在 2020-01-01 之后'
return '用自然语言描述规则,如:必须是18位中国大陆身份证号,最后一位可为X'
}
function codePlaceholder(rt) {
if (rt === 'date') {
return 'def check(value) -> bool:\n try:\n d = datetime.strptime(value, "%Y-%m-%d")\n return d >= datetime.datetime(2020, 1, 1)\n except Exception:\n return False'
}
return 'def check(value) -> bool:\n try:\n n = float(value)\n return 0 <= n <= 150\n except Exception:\n return False'
}
function aiBtnLabel(rt) {
if (rt === 'number') return '✨ AI 生成数值校验函数'
if (rt === 'date') return '✨ AI 生成日期校验函数'
return '✨ AI 生成正则'
}
watch(() => [props.open, props.field?.key], ([open]) => {
if (open && props.field) {
// 每次打开都从 field.rules 重新同步一次(保证父组件保存后再开看到的是最新)
ruleList.value = (props.field.rules || []).map((r) => ({ ...r, testVal: '', testResult: null }))
// 后端 Pydantic 序列化用 snake_case(rule_type),前端也用同名
ruleList.value = (props.field.rules || []).map((r) => ({
...r,
rule_type: r.rule_type || DEFAULT_RULE_TYPE,
code: r.code || '',
regex: r.regex || '',
testVal: '',
testResult: null,
}))
aiLoading.value = {}
}
}, { immediate: true })
......@@ -99,7 +182,9 @@ function addRule() {
ruleList.value.push({
id: Date.now(),
desc: '',
rule_type: DEFAULT_RULE_TYPE,
regex: '',
code: '',
testVal: '',
testResult: null,
})
......@@ -108,9 +193,8 @@ function removeRule(idx) {
ruleList.value.splice(idx, 1)
}
// AI 生成正则:调后端 POST /api/ai/regex
// 后端流程:规则库命中 → LLM 调用 + re.compile 校验(最多 5 次重试)→ 返回
async function genRegex(idx) {
// AI 生成:按当前规则类型调后端 /api/ai/rule
async function genAI(idx) {
const rule = ruleList.value[idx]
if (!rule.desc.trim()) {
ElMessage.warning('请先填写规则说明')
......@@ -118,12 +202,16 @@ async function genRegex(idx) {
}
aiLoading.value[idx] = true
try {
const r = await apiGenRegex(rule.desc)
if (r.ok && r.regex) {
rule.regex = r.regex
ElMessage.success(r.note || '已生成正则')
const r = await apiGenRule(rule.desc, rule.rule_type)
if (r.ok && r.code) {
if (rule.rule_type === 'regex') {
rule.regex = r.code
} else {
rule.code = r.code
}
ElMessage.success(r.note || '已生成')
} else {
// 后端降级(无 key / 重试 5 次失败 / 描述不清):弹 warning,把 note 给用户看
// 后端降级(无 key / 重试 5 次失败 / 描述不清):弹弹警告,把 note 给用户看
ElMessage.warning(r.note || '生成失败,请手动填写或补充描述')
}
} catch (e) {
......@@ -133,25 +221,31 @@ async function genRegex(idx) {
}
}
function copyRegex(rule) {
if (!rule.regex) return
navigator.clipboard?.writeText(rule.regex).catch(() => {})
ElMessage.success('已复制')
function copyText(text, msg) {
if (!text) return
navigator.clipboard?.writeText(text).catch(() => {})
ElMessage.success(msg)
}
function testRegex(idx) {
// 测试:按当前规则类型调后端 /api/rule/test
function testRule(idx) {
const rule = ruleList.value[idx]
if (!rule.regex) { rule.testResult = { text: '无正则', cls: 'fail' }; return }
apiTestRegex(rule.regex, rule.testVal).then((r) => {
const code = rule.rule_type === 'regex' ? rule.regex : rule.code
if (!code || !code.trim()) {
rule.testResult = { text: rule.rule_type === 'regex' ? '请先填写正则表达式' : '请先填写 Python 函数', cls: 'fail' }
return
}
if (!rule.testVal) {
rule.testResult = { text: '请输入测试值', cls: 'fail' }
return
}
apiTestRule(rule.rule_type, code, rule.testVal).then((r) => {
if (!r.ok) {
rule.testResult = { text: r.error || '语法错误', cls: 'fail' }
} else if (!r.match) {
rule.testResult = { text: rule.testVal ? '✗ 不匹配' : '请输入', cls: 'fail' }
} else if (!r.pass) {
rule.testResult = { text: rule.testVal ? '✗ 不通过' : '请输入', cls: 'fail' }
} else {
const g = r.groups && r.groups.length
? `(捕获:${r.groups.join(', ')})`
: ''
rule.testResult = { text: `✓ 通过${g}`, cls: 'pass' }
rule.testResult = { text: '✓ 通过', cls: 'pass' }
}
}).catch((e) => {
rule.testResult = { text: `测试失败:${e.message}`, cls: 'fail' }
......@@ -160,11 +254,80 @@ function testRegex(idx) {
// 保存:把 UI 状态(testVal/testResult)剥掉,只留真实数据 emit 给父组件
function onSave() {
const rules = ruleList.value.map((r) => ({
desc: r.desc.trim(),
regex: r.regex.trim(),
}))
const rules = ruleList.value.map((r) => {
const base = {
desc: r.desc.trim(),
rule_type: r.rule_type || DEFAULT_RULE_TYPE,
}
if (r.rule_type === 'regex') {
base.regex = r.regex.trim()
base.code = ''
} else {
base.code = r.code.trim()
base.regex = ''
}
return base
})
emit('save', { fieldKey: props.field?.key, rules })
emit('update:open', false)
}
</script>
\ No newline at end of file
</script>
<style scoped>
/* 规则类型 tab —— 用 inline-block 撑开,pill 风格;与 .conn-tab 风格统一 */
.rule-type-tabs {
display: inline-flex;
gap: 0;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.rule-type-tab {
padding: 5px 14px;
font-size: 13px;
cursor: pointer;
color: #606266;
border-right: 1px solid #dcdfe6;
user-select: none;
transition: background 0.15s, color 0.15s;
}
.rule-type-tab:last-child {
border-right: 0;
}
.rule-type-tab:hover {
background: #f5f7fa;
}
.rule-type-tab.active {
background: #409eff;
color: #fff;
}
/* Python 函数编辑框:等宽字体 + 稍高 + 灰底 */
.code-line {
display: flex;
gap: 8px;
align-items: stretch;
}
.code-input {
flex: 1;
min-height: 96px;
padding: 8px 10px;
font-family: ui-monospace, 'Cascadia Code', 'Consolas', 'Menlo', monospace;
font-size: 12.5px;
line-height: 1.55;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fafafa;
color: #303133;
resize: vertical;
outline: none;
}
.code-input:focus {
border-color: #409eff;
background: #fff;
}
.code-line .btn {
align-self: flex-start;
}
</style>
\ No newline at end of file
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