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:
@@ -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">
|
||||
|
||||
<!-- 阈值 -->
|
||||
<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>
|
||||
<!-- 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) => {
|
||||
const d = typeDefaults[rule.type]
|
||||
if (d) { rule.metric = d.metric; rule.condition = d.condition; rule.unit = d.unit }
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user