feat: add TASK type for new user assessment rules

- New TASK type with requirements array: upload_gb, download_gb, bonus, seeding_score, avg_seed_time

- Card header shows: \u8003\u6838\u5468\u671f N\u5929 \u2192 \u5c01\u53f7/\u5220\u9664\u8d26\u53f7

- Card body: editable period + 5 requirement rows (each with editable threshold)

- Type switch to TASK auto-initializes requirements from defaults

- Preset import auto-fills requirements for TASK rules

- Add seeding_score metric option
This commit is contained in:
mediabot-pt
2026-07-06 22:13:04 +08:00
parent 51bf25912b
commit b5a8dfa3b8

View File

@@ -89,7 +89,8 @@
<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>
<span class="rule-fixed-text">{{ metricLabel(rule.metric) }} {{ condLabel(rule.condition) }} {{ rule.threshold }}{{ rule.unit || '' }}</span>
<span v-if="rule.type==='TASK'" class="rule-fixed-text">考核周期 {{ rule.threshold }}天</span>
<span v-else 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>
@@ -105,14 +106,32 @@
<!-- 卡片正文(可折叠) -->
<div v-show="!rule._fold" class="card-body">
<!-- 阈值 -->
<!-- TASK 考核规则 -->
<template v-if="rule.type==='TASK'">
<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:100px" />
<span class="field-hint">天内完成以下要求,否则封号/删除账号</span>
</div>
<div class="req-divider">考核要求</div>
<div v-for="(r, ri) in (rule.requirements||[])" :key="ri" class="field-row">
<span class="field-label">{{ r.label }}</span>
<span class="condition-text">不低于</span>
<el-input v-model.number="r.threshold" size="small" type="number" @input="sync" style="width:100px" />
<span class="field-hint">{{ r.unit }}</span>
</div>
</template>
<!-- 非 TASK:普通规则 -->
<template v-else>
<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>
</template>
<!-- 适用条件 -->
<!-- 适用条件(通用) -->
<div class="field-row">
<span class="field-label">适用条件</span>
<el-select v-model="rule.applicable" size="small" @change="sync" placeholder="无条件(始终适用)" style="width:200px">
@@ -164,6 +183,7 @@ const typeOptions = [
{ value: 'TRAFFIC', label: '流量' },
{ value: 'HD', label: 'H&R' },
{ value: 'RATIO', label: '分享率' },
{ value: 'TASK', label: '新人考核' },
]
const condOptions = [
{ value: 'GT', label: '大于' },
@@ -186,6 +206,7 @@ const metricOptions = [
{ value: 'download_gb', label: '下载量(GB)' },
{ value: 'bonus', label: '魔力值' },
{ value: 'seeding', label: '做种数' },
{ value: 'seeding_score', label: '做种积分' },
{ value: 'hnr_count', label: 'H&R 违规次数' },
{ value: 'avg_seed_time', label: '平均做种时间(小时)' },
]
@@ -201,9 +222,25 @@ const typeDefaults = {
const metricLabel = (m) => metricOptions.find(x => x.value === m)?.label || m || ''
const condLabel = (c) => condOptions.find(x => x.value === c)?.label || c || ''
const taskReqDefaults = [
{ metric:'upload_gb', label:'上传量', threshold:10, unit:'GB', condition:'GTE' },
{ metric:'download_gb', label:'下载量', threshold:10, unit:'GB', condition:'GTE' },
{ metric:'bonus', label:'魔力值', threshold:1000, unit:'', condition:'GTE' },
{ metric:'seeding_score', label:'做种积分', threshold:100, unit:'', condition:'GTE' },
{ metric:'avg_seed_time', label:'平均做种时间', threshold:24, unit:'小时', condition:'GTE' },
]
const onTypeChange = (rule) => {
if (rule.type === 'TASK') {
if (!rule.requirements || !rule.requirements.length) {
rule.requirements = JSON.parse(JSON.stringify(taskReqDefaults))
}
rule.threshold = rule.threshold || 30
rule.unit = '天'
} else {
const d = typeDefaults[rule.type]
if (d) { rule.metric = d.metric; rule.condition = d.condition; rule.unit = d.unit }
}
sync()
}
@@ -220,6 +257,8 @@ const presets = [
{ name:'连续无流量封号', type:'TRAFFIC', metric:'days_since_last_traffic', threshold:90, unit:'天', condition:'GT', description:'连续90天无上传/下载流量即封号' },
// H&R
{ name:'H&R 封号', type:'HD', metric:'hnr_count', threshold:5, unit:'次', condition:'GTE', description:'H&R违规次数达到5次即封号' },
// 新人考核
{ name:'新人考核', type:'TASK', threshold:30, unit:'天', description:'新注册用户需在考核周期内完成各项要求' },
]
const presetCategories = [
@@ -227,9 +266,11 @@ const presetCategories = [
{ title:'五、零活跃新用户清理', items: presets.filter(p => p.name.includes('零活跃')) },
{ title:'流量监控', items: presets.filter(p => p.type === 'TRAFFIC') },
{ title:'H&R 惩罚', items: presets.filter(p => p.type === 'HD') },
{ title:'新人考核', items: presets.filter(p => p.type === 'TASK') },
]
const presetDesc = (p) => {
if (p.type === 'TASK') return `考核周期 ${p.threshold||30} 天,需满足上传/下载/魔力值/做种积分/做种时间要求`
const condLabel = condOptions.find(c => c.value === p.condition)?.label || p.condition
const appLabel = applicableOptions.find(a => a.value === p.applicable)?.label || ''
return `${metricOptions.find(m=>m.value===p.metric)?.label||p.metric} ${condLabel} ${p.threshold}${p.unit||''}${appLabel?' ('+appLabel+')':''}`
@@ -301,10 +342,24 @@ const confirmRemove = async (idx) => {
const arr = [...parsed.value]; arr.splice(idx, 1); parsed.value = arr
}
const addPreset = (p) => { parsed.value = [...parsed.value, { ...p, _fold: false }] }
const ensureTaskReqs = (rule) => {
if (rule.type === 'TASK' && !rule.requirements) {
rule.requirements = JSON.parse(JSON.stringify(taskReqDefaults))
}
}
const addPreset = (p) => {
const r = { ...p, _fold: false }
ensureTaskReqs(r)
parsed.value = [...parsed.value, r]
}
const importPresets = () => {
const items = presets.filter(p => selectedPresets.value.includes(p.name)).map(p => ({ ...p, _fold: false }))
const items = presets.filter(p => selectedPresets.value.includes(p.name)).map(p => {
const r = { ...p, _fold: false }
ensureTaskReqs(r)
return r
})
parsed.value = [...parsed.value, ...items]
selectedPresets.value = []
showPresets.value = false
@@ -368,5 +423,7 @@ onMounted(fetchAll)
.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: 12px; }
.condition-text { color: #909399; font-size: 12px; }
.req-divider { font-size: 12px; font-weight: 500; color: #303133; padding-top: 4px; border-top: 1px dashed #dcdfe6; }
.then-arrow { color: #f56c6c; font-size: 14px; font-weight: 700; margin: 0 2px; }
</style>