refactor: simplify rule card UI - hide metric/condition/unit
- Replace editable name/metric/condition/unit with plain text in card header - Card header shows: [type] [metric label] [condition label] [threshold][unit] \u2192 \u5c01\u53f7 - Type change auto-updates metric/condition/unit via typeDefaults mapping - Card body only shows: threshold, applicable, description - Fix edit title to show site name instead of raw siteType - Remove unused styles and functions
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
<!-- 顶部工具栏 -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-shrink:0">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<h2 style="margin:0">编辑: {{ editType }}</h2>
|
||||
<h2 style="margin:0">编辑: {{ editSiteName }}</h2>
|
||||
<el-tag size="small" type="info">{{ parsed.length }} 条规则</el-tag>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
@@ -86,12 +86,12 @@
|
||||
<div class="card-header" @dblclick="rule._fold = !rule._fold">
|
||||
<div class="card-header-left">
|
||||
<el-icon class="drag-handle"><Rank /></el-icon>
|
||||
<!-- 类型 -->
|
||||
<el-select v-model="rule.type" size="small" @change="sync" style="width:100px">
|
||||
<el-select v-model="rule.type" size="small" @change="onTypeChange(rule)" style="width:100px">
|
||||
<el-option v-for="x in typeOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<!-- 规则名称 -->
|
||||
<el-input v-model="rule.name" size="small" placeholder="规则名称" @input="sync" style="width:220px" />
|
||||
<span class="rule-fixed-text">{{ metricLabel(rule.metric) }} {{ condLabel(rule.condition) }} {{ rule.threshold }}{{ rule.unit || '' }}</span>
|
||||
<span class="then-arrow">→</span>
|
||||
<span style="color:#f56c6c;font-weight:600;font-size:13px">封号</span>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<span class="rule-index">#{{ idx + 1 }}</span>
|
||||
@@ -105,34 +105,24 @@
|
||||
<!-- 卡片正文(可折叠) -->
|
||||
<div v-show="!rule._fold" class="card-body">
|
||||
|
||||
<!-- 阈值 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">阈值</span>
|
||||
<el-input v-model.number="rule.threshold" size="small" type="number" placeholder="阈值" @input="sync" style="width:120px" />
|
||||
<span class="field-hint">{{ rule.unit || '' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 适用条件 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">适用条件</span>
|
||||
<el-select v-model="rule.applicable" size="small" @change="sync" clearable placeholder="无条件(始终适用)" style="width:200px">
|
||||
<el-option v-for="x in applicableOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<span class="field-hint">{{ applicableHint(rule.applicable) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 触发条件:当 [指标] [条件] [阈值] [单位] → 封号 -->
|
||||
<!-- 规则说明 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">触发条件</span>
|
||||
<span class="condition-text">当</span>
|
||||
<el-select v-model="rule.metric" size="small" @change="sync" filterable allow-create placeholder="指标" style="width:160px">
|
||||
<el-option v-for="x in metricOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<el-select v-model="rule.condition" size="small" @change="sync" style="width:105px">
|
||||
<el-option v-for="x in condOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<el-input v-model.number="rule.threshold" size="small" type="number" placeholder="阈值" @input="sync" style="width:90px" />
|
||||
<el-input v-model="rule.unit" size="small" placeholder="单位" @input="sync" style="width:70px" />
|
||||
<span class="then-arrow">→</span>
|
||||
<span style="color:#f56c6c;font-weight:600">封号</span>
|
||||
</div>
|
||||
|
||||
<!-- 规则描述 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">规则说明</span>
|
||||
<span class="field-label">说明</span>
|
||||
<el-input v-model="rule.description" size="small" placeholder="可选说明" @input="sync" style="flex:1;max-width:500px" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,9 +197,21 @@ const metricOptions = [
|
||||
]
|
||||
|
||||
const typeLabel = (t) => typeOptions.find(x => x.value === t)?.label || t
|
||||
const applicableHint = (a) => {
|
||||
const m = { '!sealed':'未封存用户', 'sealed':'已封存用户', 'zero_traffic':'上传/下载均为 0', 'new_account_30d':'从未完成种子且无流量增量' }
|
||||
return a ? `— ${m[a] || ''}` : ''
|
||||
|
||||
const typeDefaults = {
|
||||
KILL: { metric:'days_since_last_login', condition:'GT', unit:'天' },
|
||||
TRAFFIC: { metric:'days_since_last_traffic', condition:'GT', unit:'天' },
|
||||
LOGIN: { metric:'days_since_last_login', condition:'GT', unit:'天' },
|
||||
HD: { metric:'hnr_count', condition:'GTE', unit:'次' },
|
||||
RATIO: { metric:'ratio', condition:'LT', unit:'' },
|
||||
}
|
||||
const metricLabel = (m) => metricOptions.find(x => x.value === m)?.label || m || ''
|
||||
const condLabel = (c) => condOptions.find(x => x.value === c)?.label || c || ''
|
||||
|
||||
const onTypeChange = (rule) => {
|
||||
const d = typeDefaults[rule.type]
|
||||
if (d) { rule.metric = d.metric; rule.condition = d.condition; rule.unit = d.unit }
|
||||
sync()
|
||||
}
|
||||
|
||||
// ============ 预设规则模板 ============
|
||||
@@ -255,6 +257,11 @@ const parsed = computed({
|
||||
set(v) { editorJson.value = JSON.stringify(v, null, 2) }
|
||||
})
|
||||
|
||||
const editSiteName = computed(() => {
|
||||
if (!editType.value) return ''
|
||||
const site = (sites.value||[]).find(s => s.siteType === editType.value)
|
||||
return site ? (site.names||[]).join('/') || site.siteId || editType.value : editType.value
|
||||
})
|
||||
const merged = computed(() => (Array.isArray(sites.value)?sites.value:[]).map(s => ({
|
||||
siteId: s.siteId, names: s.names, siteType: s.siteType,
|
||||
ruleCount: rulesMap.value[s.siteType]?.ruleCount || 0, version: rulesMap.value[s.siteType]?.version || null,
|
||||
@@ -289,8 +296,9 @@ const editRule = async (row) => {
|
||||
const sync = () => { editorJson.value = JSON.stringify(parsed.value, null, 2) }
|
||||
|
||||
const addRule = () => {
|
||||
const d = typeDefaults['KILL']
|
||||
parsed.value = [...parsed.value, {
|
||||
type:'KILL', name:'', metric:'days_since_last_login', threshold:0, unit:'天', condition:'GT',
|
||||
type:'KILL', metric:d.metric, threshold:0, unit:d.unit, condition:d.condition,
|
||||
applicable:null, description:'', _fold:false
|
||||
}]
|
||||
}
|
||||
@@ -361,11 +369,11 @@ onMounted(fetchAll)
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
.rule-index { font-size: 11px; color: #c0c4cc; font-weight: 500; min-width: 24px; text-align: right; }
|
||||
|
||||
.card-body { padding: 4px 12px 10px 34px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.card-body { padding: 6px 12px 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.rule-fixed-text { font-size: 13px; color: #303133; }
|
||||
|
||||
.field-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; }
|
||||
.field-label { font-weight: 500; color: #606266; font-size: 12px; min-width: 56px; }
|
||||
.field-hint { color: #909399; font-size: 11px; }
|
||||
.condition-text { color: #909399; font-size: 12px; }
|
||||
.then-arrow { color: #f56c6c; font-size: 16px; font-weight: 700; margin: 0 2px; }
|
||||
.field-hint { color: #909399; font-size: 12px; }
|
||||
.then-arrow { color: #f56c6c; font-size: 14px; font-weight: 700; margin: 0 2px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user