feat: TRAFFIC 规则增加连续未登录天数输入,预设默认90天
This commit is contained in:
@@ -94,6 +94,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==='TRAFFIC'" class="rule-fixed-text">无流量 > {{ rule.threshold }}天<span v-if="rule.loginDays>0">,且未登录 > {{ rule.loginDays }}天</span></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>
|
||||
@@ -158,6 +159,20 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TRAFFIC 连续无流量 -->
|
||||
<template v-else-if="rule.type==='TRAFFIC'">
|
||||
<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="field-row">
|
||||
<span class="field-label">且未登录天数</span>
|
||||
<el-input v-model.number="rule.loginDays" size="small" type="number" placeholder="0=不限制" @input="sync" style="width:100px" />
|
||||
<span class="field-hint">天内无登录活动时触发(0 表示不限制登录条件)</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 非 TASK:普通规则 -->
|
||||
<template v-else>
|
||||
<div class="field-row">
|
||||
@@ -267,7 +282,7 @@ const presets = [
|
||||
{ name:'零活跃不登录封号', type:'KILL', metric:'days_since_last_login', threshold:7, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户连续7天不登录即封号' },
|
||||
{ name:'零活跃注册天数封号', type:'KILL', metric:'days_since_registration', threshold:30, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户注册满30天即封号' },
|
||||
// 流量监控
|
||||
{ name:'连续无流量封号', type:'TRAFFIC', metric:'days_since_last_traffic', threshold:90, unit:'天', condition:'GT', description:'连续90天无上传/下载流量即封号' },
|
||||
{ name:'连续无流量封号', type:'TRAFFIC', metric:'days_since_last_traffic', threshold:90, unit:'天', loginDays:90, condition:'GT', description:'连续90天无流量且90天未登录即封号' },
|
||||
// H&R
|
||||
{ name:'H&R 封号', type:'HD', metric:'hnr_count', threshold:5, unit:'次', condition:'GTE', description:'H&R违规次数达到5次即封号' },
|
||||
// 新人考核
|
||||
@@ -337,7 +352,10 @@ 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') {
|
||||
if (r.type === 'TRAFFIC') {
|
||||
const extra = r.loginDays > 0 ? `(且${r.loginDays}天未登录)` : ''
|
||||
return `无流量:${val}${extra} → <span style="color:#f56c6c;font-weight:500">封号/删除账号</span>${appText}`
|
||||
}
|
||||
const levels = (r.levels||[]).map(l => `下载量≥${l.threshold}GB时分享率<${l.ratio}`).join(',')
|
||||
return `分享率:${levels} → <span style="color:#e6a23c;font-weight:500">封号(缓刑7天)</span>${appText}`
|
||||
}
|
||||
|
||||
@@ -104,29 +104,45 @@
|
||||
<div v-if="!parsed.length" style="text-align:center;padding:40px;color:#909399">暂无,点击添加</div>
|
||||
<div v-for="(entry, idx) in parsed" :key="idx" style="margin-bottom:12px;padding:12px;border:1px solid #ebeef5;border-radius:4px;background:#fafafa">
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
|
||||
<el-tag size="small" :type="entry.compose ? 'warning' : 'primary'">{{ entry.key }} - {{ keyLabel(entry.key) }}</el-tag>
|
||||
<template v-if="entry.compose">
|
||||
<span style="font-size:12px;color:#b88230;margin:0 4px">→</span>
|
||||
<el-tag v-for="k in entry.compose" :key="k" size="small" type="warning" effect="plain" style="margin-right:4px">{{ k }}</el-tag>
|
||||
</template>
|
||||
<!-- compose 主 key 可编辑下拉框 -->
|
||||
<el-select v-if="entry.compose" v-model="entry.key" size="small" @change="syncToJson" style="width:150px">
|
||||
<el-option v-for="pk in allPageKeys" :key="pk.key" :label="`${pk.key} - ${pk.label}`" :value="pk.key" />
|
||||
</el-select>
|
||||
<el-tag v-else size="small" type="primary">{{ entry.key }} - {{ keyLabel(entry.key) }}</el-tag>
|
||||
<el-select v-if="!entry.compose" v-model="entry.method" size="small" @change="syncToJson" style="width:80px">
|
||||
<el-option label="GET" value="GET"/><el-option label="POST" value="POST"/>
|
||||
</el-select>
|
||||
<div style="flex:1"></div>
|
||||
<el-button size="small" type="danger" text @click="remove(idx)">删除</el-button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:6px;max-width:500px">
|
||||
<el-input v-model="entry.url" size="small" placeholder="URL路径,如 /torrents.php" @input="syncToJson" style="flex:1" />
|
||||
<el-button size="small" @click="verifyEntry(entry, idx)" :loading="verifying[idx]" :type="verifyResults[idx]?.success===true ? 'success' : verifyResults[idx]?.success===false ? 'danger' : ''">
|
||||
{{ verifyResults[idx]?.success===true ? '✓' : verifyResults[idx]?.success===false ? '✗' : '验证' }}
|
||||
<!-- compose 子页面编辑区 -->
|
||||
<template v-if="entry.compose">
|
||||
<div style="font-size:12px;color:#606266;font-weight:500;margin-bottom:4px">子页面({uid} 自动替换为用户UID):</div>
|
||||
<div v-for="(sk, si) in entry.compose" :key="si" style="display:flex;gap:6px;align-items:center;margin-bottom:4px">
|
||||
<el-select v-model="entry.compose[si]" size="small" @change="syncToJson" style="width:160px">
|
||||
<el-option v-for="pk in allPageKeys" :key="pk.key" :label="`${pk.key} - ${pk.label}`" :value="pk.key" />
|
||||
</el-select>
|
||||
<el-button size="small" type="danger" text @click="removeComposeSub(entry, si)" :icon="Delete" circle />
|
||||
</div>
|
||||
<el-button size="small" text type="primary" @click="addComposeSub(entry)" style="align-self:flex-start">
|
||||
<el-icon style="margin-right:2px"><Plus /></el-icon>添加子页面
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="verifyResults[idx]?.message" style="margin-bottom:6px;font-size:12px;padding:4px 8px;border-radius:4px" :style="{background:verifyResults[idx]?.success? '#f0f9eb':'#fef0f0',color:verifyResults[idx]?.success? '#67c23a':'#f56c6c'}">
|
||||
{{ verifyResults[idx].message }}
|
||||
<span v-if="verifyResults[idx].responseTimeMs != null" style="margin-left:8px;opacity:0.7">({{ verifyResults[idx].responseTimeMs }}ms)</span>
|
||||
</div>
|
||||
<el-input v-model="entry.headers" size="small" type="textarea" :rows="2" placeholder="自定义Header,每行一个 key: value" @input="syncToJson" style="margin-bottom:6px;max-width:500px" />
|
||||
<el-input v-if="entry.method==='POST'" v-model="entry.body" size="small" type="textarea" :rows="2" placeholder="POST Body(可选)" @input="syncToJson" />
|
||||
</template>
|
||||
<!-- 非 compose:URL / 验证 / headers -->
|
||||
<template v-if="!entry.compose">
|
||||
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:6px;max-width:500px">
|
||||
<el-input v-model="entry.url" size="small" placeholder="URL路径,如 /torrents.php" @input="syncToJson" style="flex:1" />
|
||||
<el-button size="small" @click="verifyEntry(entry, idx)" :loading="verifying[idx]" :type="verifyResults[idx]?.success===true ? 'success' : verifyResults[idx]?.success===false ? 'danger' : ''">
|
||||
{{ verifyResults[idx]?.success===true ? '✓' : verifyResults[idx]?.success===false ? '✗' : '验证' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="verifyResults[idx]?.message" style="margin-bottom:6px;font-size:12px;padding:4px 8px;border-radius:4px" :style="{background:verifyResults[idx]?.success? '#f0f9eb':'#fef0f0',color:verifyResults[idx]?.success? '#67c23a':'#f56c6c'}">
|
||||
{{ verifyResults[idx].message }}
|
||||
<span v-if="verifyResults[idx].responseTimeMs != null" style="margin-left:8px;opacity:0.7">({{ verifyResults[idx].responseTimeMs }}ms)</span>
|
||||
</div>
|
||||
<el-input v-model="entry.headers" size="small" type="textarea" :rows="2" placeholder="自定义Header,每行一个 key: value" @input="syncToJson" style="margin-bottom:6px;max-width:500px" />
|
||||
<el-input v-if="entry.method==='POST'" v-model="entry.body" size="small" type="textarea" :rows="2" placeholder="POST Body(可选)" @input="syncToJson" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +150,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../api'; import { Refresh } from '@element-plus/icons-vue'
|
||||
import api from '../api'; import { Refresh, Plus, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false), saving = ref(false), deleting = ref(false), savingPreview = ref(false), publishing = ref(false), discardingDraft = ref(false)
|
||||
const sites = ref([]), pagesMap = ref({}), sitesList = ref([])
|
||||
@@ -171,6 +187,7 @@ const availableKeys = computed(() => {
|
||||
const used = new Set(parsed.value.map(e => e.key).filter(Boolean))
|
||||
return pageKeys.value.filter(k => !used.has(k.key))
|
||||
})
|
||||
const allPageKeys = computed(() => pageKeys.value)
|
||||
|
||||
const onKeyChange = (row) => {
|
||||
const pk = pageKeys.value.find(k => k.key === row.key)
|
||||
@@ -315,7 +332,16 @@ const addEntryByKey = (k) => {
|
||||
syncToJson()
|
||||
}
|
||||
const addComposeEntry = () => {
|
||||
parsed.value.push({ key: 'index', compose: ['profile', 'peerStats'] })
|
||||
parsed.value.push({ key: 'index', compose: ['userdetail', 'peerStats'] })
|
||||
syncToJson()
|
||||
}
|
||||
const addComposeSub = (entry) => {
|
||||
if (!entry.compose) entry.compose = []
|
||||
entry.compose.push('userdetail')
|
||||
syncToJson()
|
||||
}
|
||||
const removeComposeSub = (entry, si) => {
|
||||
entry.compose.splice(si, 1)
|
||||
syncToJson()
|
||||
}
|
||||
const deletePages = async () => {
|
||||
|
||||
Reference in New Issue
Block a user