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:
mediabot-pt
2026-07-06 21:52:48 +08:00
parent 842c01d79a
commit c9889a4aab

View File

@@ -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>