feat: DRATIO supports multiple threshold-ratio levels

Replace single threshold+minRatio with levels array

Card header: \u4e0b\u8f7d\u91cf\u5206\u7ea7\u63a7\u5236\uff082\u7ea7\uff09

Card body: editable rows with add/delete buttons per level

Default preset: 100GB\u21921.0, 500GB\u21922.0, 1000GB\u21923.0

Auto-migrate old single-value format on load
This commit is contained in:
mediabot-pt
2026-07-06 23:07:27 +08:00
parent 1cea882d9d
commit 1c06be991d

View File

@@ -87,7 +87,7 @@
<el-icon class="drag-handle"><Rank /></el-icon>
<span class="type-tag">{{ typeLabel(rule.type) }}</span>
<span v-if="rule.type==='TASK'" class="rule-fixed-text">考核周期 {{ rule.threshold }}天</span>
<span v-else-if="rule.type==='DRATIO'" class="rule-fixed-text">下载量 ≥ {{ rule.threshold }}GB 时分享率 ≥ {{ rule.minRatio }}</span>
<span v-else-if="rule.type==='DRATIO'" class="rule-fixed-text">下载量分级控制({{ (rule.levels||[]).length }} 级)</span>
<span v-else-if="rule.type==='EXEMPT'" class="rule-fixed-text">永久保留 ≥ {{ rankDisplay(rule.permRank) }} / 封存豁免 ≥ {{ rankDisplay(rule.sealExemptRank) }}</span>
<span v-else class="rule-fixed-text">{{ metricLabel(rule.metric) }} {{ condLabel(rule.condition) }} {{ rule.threshold }}{{ rule.unit || '' }}</span>
<span class="then-arrow" :style="rule.type==='EXEMPT'?'color:#67c23a':rule.type==='RATIO'||rule.type==='DRATIO'?'color:#e6a23c':''">→</span>
@@ -125,16 +125,16 @@
<!-- DRATIO 动态分享率 -->
<template v-else-if="rule.type==='DRATIO'">
<div class="field-row">
<span class="field-label">下载量阈值</span>
<el-input v-model.number="rule.threshold" size="small" type="number" placeholder="GB" @input="sync" style="width:100px" />
<span class="field-hint">GB(达到此下载量后检查分享率)</span>
</div>
<div class="field-row">
<span class="field-label">要求分享率</span>
<el-input v-model.number="rule.minRatio" size="small" type="number" :step="0.1" placeholder="分享率" @input="sync" style="width:100px" />
<span class="field-hint">(分享率低于此值则降级至Peasant,7天缓刑后封号/删除账号)</span>
<div v-for="(lv, li) in (rule.levels||[])" :key="li" class="field-row">
<span class="field-label">下载量 ≥</span>
<el-input v-model.number="lv.threshold" size="small" type="number" placeholder="GB" @input="sync" style="width:90px" />
<span class="field-hint">GB 时,分享率 ≥</span>
<el-input v-model.number="lv.ratio" size="small" type="number" :step="0.1" placeholder="分享率" @input="sync" style="width:80px" />
<el-button size="small" type="danger" text :icon="Delete" @click="removeDRatioLevel(rule, li)" />
</div>
<el-button size="small" text type="primary" @click="addDRatioLevel(rule)" style="align-self:flex-start">
<el-icon style="margin-right:2px"><Plus /></el-icon>添加等级
</el-button>
</template>
<!-- EXEMPT 等级豁免 -->
@@ -182,7 +182,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ArrowDown, ArrowUp, Rank, Delete, Refresh } from '@element-plus/icons-vue'
import { ArrowDown, ArrowUp, Rank, Delete, Refresh, Plus } from '@element-plus/icons-vue'
import api from '../api'
// ============ 选项配置 ============
@@ -270,7 +270,7 @@ const presets = [
// 新人考核
{ name:'新人考核', type:'TASK', threshold:30, unit:'天', description:'新注册用户需在考核周期内完成各项要求' },
// 动态分享率
{ name:'动态分享率', type:'DRATIO', threshold:100, minRatio:1.0, unit:'GB', description:'下载量达到阈值时分享率不低于要求值' },
{ name:'动态分享率', type:'DRATIO', levels:[{threshold:100,ratio:1.0},{threshold:500,ratio:2.0},{threshold:1000,ratio:3.0}], description:'下载量分级控制分享率' },
// 等级豁免
{ name:'等级豁免', type:'EXEMPT', permRank:'Elite User', sealExemptRank:'Power User', description:'达到等级的账号可豁免清理' },
]
@@ -284,7 +284,20 @@ const dragIdx = ref(null)
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const parsed = computed({
get() { try { const arr = JSON.parse(editorJson.value); return Array.isArray(arr) ? arr : [] } catch { return [] } },
get() {
try {
const arr = JSON.parse(editorJson.value)
if (!Array.isArray(arr)) return []
// migrate old DRATIO format
for (const r of arr) {
if (r.type === 'DRATIO' && !r.levels && r.threshold !== undefined) {
r.levels = [{ threshold: r.threshold || 100, ratio: r.minRatio || 1.0 }]
delete r.threshold; delete r.minRatio
}
}
return arr
} catch { return [] }
},
set(v) { editorJson.value = JSON.stringify(v, null, 2) }
})
@@ -314,9 +327,12 @@ const ruleText = (r) => {
const metric = metricLabel(r.metric)
const cond = condLabel(r.condition)
const val = `${metric}${cond}${r.threshold}${r.unit||''}`
if (r.type === 'DRATIO' || r.type === 'RATIO') {
const prefix = r.type === 'DRATIO' ? `动态分享率:下载量 ≥ ${r.threshold}GB 时分享率 ≥ ${r.minRatio}` : `分享率:${val}`
return `${prefix} → <span style="color:#e6a23c;font-weight:500">封号(缓刑7天)</span>${appText}`
if (r.type === 'DRATIO') {
const levels = (r.levels||[]).map(l => `下载量≥${l.threshold}GB时分享率≥${l.ratio}`).join(',')
return `动态分享率:${levels} → <span style="color:#e6a23c;font-weight:500">封号(缓刑7天)</span>${appText}`
}
if (r.type === 'RATIO') {
return `分享率:${val} → <span style="color:#e6a23c;font-weight:500">封号(缓刑7天)</span>${appText}`
}
return `${typeLabel(r.type)}:${val} → <span style="color:#f56c6c;font-weight:500">封号/删除账号</span>${appText}`
}
@@ -364,6 +380,24 @@ const ensureTaskReqs = (rule) => {
}
}
const addDRatioLevel = (rule) => {
if (!rule.levels) rule.levels = []
rule.levels.push({ threshold: 100, ratio: 1.0 })
sync()
}
const removeDRatioLevel = (rule, li) => {
rule.levels.splice(li, 1)
sync()
}
const ensureDRatioLevels = (rule) => {
if (rule.type === 'DRATIO') {
if (rule.levels && rule.levels.length) return
// migrate old single-value format
rule.levels = [{ threshold: rule.threshold || 100, ratio: rule.minRatio || 1.0 }]
delete rule.threshold; delete rule.minRatio
}
}
const ruleKey = (r) => `${r.type}|${r.applicable||''}`
const existsRule = (rule) => {
@@ -373,6 +407,7 @@ const existsRule = (rule) => {
const addPreset = (p) => {
const r = { ...p, _fold: false }
ensureTaskReqs(r)
ensureDRatioLevels(r)
if (existsRule(r)) {
ElMessage.warning(`规则「${p.name}」已存在,同类型+适用条件只能添加一次`)
return