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 @@ ...@@ -13,35 +13,81 @@
<span class="rnum">规则{{ idx + 1 }}</span> <span class="rnum">规则{{ idx + 1 }}</span>
<button class="btn-text danger" @click="removeRule(idx)">删除</button> <button class="btn-text danger" @click="removeRule(idx)">删除</button>
</div> </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"> <div class="rule-item-row">
<span class="rule-item-label">规则说明</span> <span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl"> <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> </div>
<div class="rule-item-row"> <div class="rule-item-row">
<span class="rule-item-label"></span> <span class="rule-item-label"></span>
<div class="rule-item-ctrl"> <div class="rule-item-ctrl">
<button class="btn ai-btn" :disabled="aiLoading[idx]" @click="genRegex(idx)"> <button class="btn ai-btn" :disabled="aiLoading[idx]" @click="genAI(idx)">
{{ aiLoading[idx] ? '✨ AI 分析中…' : '✨ AI 生成正则' }} {{ aiLoading[idx] ? '✨ AI 分析中…' : aiBtnLabel(rule.rule_type) }}
</button> </button>
</div> </div>
</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> <span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl"> <div class="rule-item-ctrl">
<div class="regex-line"> <div class="regex-line">
<input type="text" class="text-input" v-model="rule.regex" placeholder="点击AI生成或手动填写" /> <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> </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"> <div class="rule-item-row">
<span class="rule-item-label">测试</span> <span class="rule-item-label">测试</span>
<div class="rule-item-ctrl"> <div class="rule-item-ctrl">
<div class="test-line"> <div class="test-line">
<input type="text" class="text-input" v-model="rule.testVal" placeholder="输入测试值" /> <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> <span class="test-result" :class="rule.testResult?.cls" v-if="rule.testResult">{{ rule.testResult.text }}</span>
</div> </div>
</div> </div>
...@@ -65,7 +111,7 @@ ...@@ -65,7 +111,7 @@
<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' import { genRule as apiGenRule, testRule as apiTestRule } from '@/api/ai'
const props = defineProps({ const props = defineProps({
open: { type: Boolean, default: false }, open: { type: Boolean, default: false },
...@@ -73,6 +119,14 @@ const props = defineProps({ ...@@ -73,6 +119,14 @@ const props = defineProps({
}) })
const emit = defineEmits(['update:open', 'save']) 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(() => { const title = computed(() => {
if (!props.field) return '规则设置' if (!props.field) return '规则设置'
return `规则设置 - ${props.field.en}(${props.field.cnName || props.field.cn})` return `规则设置 - ${props.field.en}(${props.field.cnName || props.field.cn})`
...@@ -81,16 +135,45 @@ const title = computed(() => { ...@@ -81,16 +135,45 @@ const title = computed(() => {
const ruleList = ref([]) const ruleList = ref([])
const aiLoading = ref({}) const aiLoading = ref({})
// 「至少有一条规则且 desc+regex 都填了」才能保存(避免保存空规则) // 「至少有一条规则且 desc + code/regex 都填了」才能保存(避免保存空规则)
// 必须在 ruleList 之后定义(const 引用前要先声明)
const hasValidRules = computed(() => 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]) => { watch(() => [props.open, props.field?.key], ([open]) => {
if (open && props.field) { if (open && props.field) {
// 每次打开都从 field.rules 重新同步一次(保证父组件保存后再开看到的是最新) // 每次打开都从 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 = {} aiLoading.value = {}
} }
}, { immediate: true }) }, { immediate: true })
...@@ -99,7 +182,9 @@ function addRule() { ...@@ -99,7 +182,9 @@ function addRule() {
ruleList.value.push({ ruleList.value.push({
id: Date.now(), id: Date.now(),
desc: '', desc: '',
rule_type: DEFAULT_RULE_TYPE,
regex: '', regex: '',
code: '',
testVal: '', testVal: '',
testResult: null, testResult: null,
}) })
...@@ -108,9 +193,8 @@ function removeRule(idx) { ...@@ -108,9 +193,8 @@ function removeRule(idx) {
ruleList.value.splice(idx, 1) ruleList.value.splice(idx, 1)
} }
// AI 生成正则:调后端 POST /api/ai/regex // AI 生成:按当前规则类型调后端 /api/ai/rule
// 后端流程:规则库命中 → LLM 调用 + re.compile 校验(最多 5 次重试)→ 返回 async function genAI(idx) {
async 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('请先填写规则说明')
...@@ -118,12 +202,16 @@ async function genRegex(idx) { ...@@ -118,12 +202,16 @@ async function genRegex(idx) {
} }
aiLoading.value[idx] = true aiLoading.value[idx] = true
try { try {
const r = await apiGenRegex(rule.desc) const r = await apiGenRule(rule.desc, rule.rule_type)
if (r.ok && r.regex) { if (r.ok && r.code) {
rule.regex = r.regex if (rule.rule_type === 'regex') {
ElMessage.success(r.note || '已生成正则') rule.regex = r.code
} else {
rule.code = r.code
}
ElMessage.success(r.note || '已生成')
} else { } else {
// 后端降级(无 key / 重试 5 次失败 / 描述不清):弹 warning,把 note 给用户看 // 后端降级(无 key / 重试 5 次失败 / 描述不清):弹弹警告,把 note 给用户看
ElMessage.warning(r.note || '生成失败,请手动填写或补充描述') ElMessage.warning(r.note || '生成失败,请手动填写或补充描述')
} }
} catch (e) { } catch (e) {
...@@ -133,25 +221,31 @@ async function genRegex(idx) { ...@@ -133,25 +221,31 @@ async function genRegex(idx) {
} }
} }
function copyRegex(rule) { function copyText(text, msg) {
if (!rule.regex) return if (!text) return
navigator.clipboard?.writeText(rule.regex).catch(() => {}) navigator.clipboard?.writeText(text).catch(() => {})
ElMessage.success('已复制') ElMessage.success(msg)
} }
function testRegex(idx) { // 测试:按当前规则类型调后端 /api/rule/test
function testRule(idx) {
const rule = ruleList.value[idx] const rule = ruleList.value[idx]
if (!rule.regex) { rule.testResult = { text: '无正则', cls: 'fail' }; return } const code = rule.rule_type === 'regex' ? rule.regex : rule.code
apiTestRegex(rule.regex, rule.testVal).then((r) => { 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) { if (!r.ok) {
rule.testResult = { text: r.error || '语法错误', cls: 'fail' } rule.testResult = { text: r.error || '语法错误', cls: 'fail' }
} else if (!r.match) { } else if (!r.pass) {
rule.testResult = { text: rule.testVal ? '✗ 不匹配' : '请输入', cls: 'fail' } rule.testResult = { text: rule.testVal ? '✗ 不通过' : '请输入', cls: 'fail' }
} else { } else {
const g = r.groups && r.groups.length rule.testResult = { text: '✓ 通过', cls: 'pass' }
? `(捕获:${r.groups.join(', ')})`
: ''
rule.testResult = { text: `✓ 通过${g}`, cls: 'pass' }
} }
}).catch((e) => { }).catch((e) => {
rule.testResult = { text: `测试失败:${e.message}`, cls: 'fail' } rule.testResult = { text: `测试失败:${e.message}`, cls: 'fail' }
...@@ -160,11 +254,80 @@ function testRegex(idx) { ...@@ -160,11 +254,80 @@ function testRegex(idx) {
// 保存:把 UI 状态(testVal/testResult)剥掉,只留真实数据 emit 给父组件 // 保存:把 UI 状态(testVal/testResult)剥掉,只留真实数据 emit 给父组件
function onSave() { function onSave() {
const rules = ruleList.value.map((r) => ({ const rules = ruleList.value.map((r) => {
const base = {
desc: r.desc.trim(), desc: r.desc.trim(),
regex: r.regex.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('save', { fieldKey: props.field?.key, rules })
emit('update:open', false) emit('update:open', false)
} }
</script> </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