refactor: simplify risk rules to KILL/WARN actions only
- Replace complex type system (DEMOTE/TASK) with simple KILL/WARN action model - Each rule now has: type (monitoring category) + action (KILL=封号 / WARN=告警) - Type options: KILL/TRAFFIC/LOGIN/HD/RATIO (no DEMOTE/TASK) - Card left border color by action: red=KILL, orange=WARN - 12 preset templates with alert-based rules (60-day no-traffic warn, 140-day login warn, HnR warn, ratio warn) - Remove requirements sub-table and downloadThreshold fields
This commit is contained in:
@@ -62,7 +62,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="p in presets" :key="p.name" :command="p">
|
||||
<div style="font-size:12px">
|
||||
<span :style="{color:levelColor(p.level),fontWeight:600}">{{ p.name }}</span>
|
||||
<span :style="{color:actionColor(p.action),fontWeight:600}">{{ p.name }}</span>
|
||||
<span style="color:#909399;margin-left:4px">{{ typeLabel(p.type) }}</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
@@ -75,7 +75,7 @@
|
||||
<!-- 规则卡片列表 -->
|
||||
<div v-if="parsed.length" style="display:flex;flex-direction:column;gap:10px">
|
||||
<div v-for="(rule, idx) in parsed" :key="idx"
|
||||
:class="['rule-card', rule.level.toLowerCase()]"
|
||||
:class="['rule-card', rule.action.toLowerCase()]"
|
||||
:draggable="true"
|
||||
@dragstart="onDragStart($event, idx)"
|
||||
@dragover.prevent="onDragOver($event, idx)"
|
||||
@@ -86,15 +86,18 @@
|
||||
<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="onTypeChange(rule)" style="width:90px">
|
||||
<el-option v-for="x in typeOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<el-select v-model="rule.level" size="small" @change="sync" style="width:85px">
|
||||
<el-option v-for="x in levelOptions" :key="x.value" :label="x.label" :value="x.value">
|
||||
<span :style="{color:levelColor(x.value),fontWeight:600}">{{ x.label }}</span>
|
||||
<!-- 操作:封号 / 告警 -->
|
||||
<el-select v-model="rule.action" size="small" @change="sync" style="width:80px">
|
||||
<el-option v-for="x in actionOptions" :key="x.value" :label="x.label" :value="x.value">
|
||||
<span :style="{color:actionColor(x.value),fontWeight:600}">{{ x.label }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input v-model="rule.name" size="small" placeholder="规则名称" @input="sync" style="width:180px" />
|
||||
<!-- 类型 -->
|
||||
<el-select v-model="rule.type" size="small" @change="sync" style="width:90px">
|
||||
<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:200px" />
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<span class="rule-index">#{{ idx + 1 }}</span>
|
||||
@@ -108,7 +111,7 @@
|
||||
<!-- 卡片正文(可折叠) -->
|
||||
<div v-show="!rule._fold" class="card-body">
|
||||
|
||||
<!-- 适用条件(高级) -->
|
||||
<!-- 适用条件 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">适用条件</span>
|
||||
<el-select v-model="rule.applicable" size="small" @change="sync" clearable placeholder="无条件(始终适用)" style="width:200px">
|
||||
@@ -117,7 +120,7 @@
|
||||
<span class="field-hint">{{ applicableHint(rule.applicable) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 触发条件:指标 + 比较 + 阈值 + 单位 -->
|
||||
<!-- 触发条件 -->
|
||||
<div class="field-row">
|
||||
<span class="field-label">触发条件</span>
|
||||
<span class="condition-text">当</span>
|
||||
@@ -130,7 +133,7 @@
|
||||
<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:levelColor(rule.level),fontWeight:600}">{{ levelText(rule.level) }}</span>
|
||||
<span :style="{color:actionColor(rule.action),fontWeight:600}">{{ actionText(rule.action) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 规则描述 -->
|
||||
@@ -138,33 +141,6 @@
|
||||
<span class="field-label">规则说明</span>
|
||||
<el-input v-model="rule.description" size="small" placeholder="可选说明" @input="sync" style="flex:1;max-width:500px" />
|
||||
</div>
|
||||
|
||||
<!-- TASK 类型:多要求子项 -->
|
||||
<div v-if="rule.type === 'TASK'" class="task-section">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span class="field-label" style="margin:0">考核要求</span>
|
||||
<el-button size="small" text type="primary" @click="addRequirement(rule)">+ 添加</el-button>
|
||||
</div>
|
||||
<div v-for="(req, ri) in (rule.requirements || [])" :key="ri" class="req-row">
|
||||
<el-input v-model="req.label" size="small" placeholder="项目名" @input="sync" style="width:90px" />
|
||||
<el-select v-model="req.metric" size="small" @change="sync" filterable allow-create style="width:120px">
|
||||
<el-option v-for="x in metricOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<el-select v-model="req.condition" size="small" @change="sync" style="width:80px">
|
||||
<el-option v-for="x in condOptions" :key="x.value" :label="x.label" :value="x.value" />
|
||||
</el-select>
|
||||
<el-input v-model.number="req.threshold" size="small" type="number" placeholder="阈值" @input="sync" style="width:80px" />
|
||||
<el-input v-model="req.unit" size="small" placeholder="单位" @input="sync" style="width:60px" />
|
||||
<el-button size="small" type="danger" text @click="removeRequirement(rule, ri)" :icon="Delete" circle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEMOTE 类型:动态分享率配置 -->
|
||||
<div v-if="rule.type === 'DEMOTE'" class="field-row">
|
||||
<span class="field-label">下载量阈值</span>
|
||||
<el-input v-model.number="rule.downloadThreshold" size="small" type="number" placeholder="达到此下载量后生效" @input="sync" style="width:100px" />
|
||||
<span style="color:#909399;font-size:12px">GB,达到后要求分享率不低于阈值</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,10 +148,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 预设规则导入对话框 -->
|
||||
<el-dialog v-model="showPresets" title="PT 站点核心管理规则模板" width="750px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="showPresets" title="PT 站点管理规则模板" width="700px" :close-on-click-modal="false">
|
||||
<div style="margin-bottom:12px;padding:10px 14px;background:#f0f9ff;border-radius:6px;font-size:13px;color:#606266">
|
||||
<p style="margin:0 0 4px;font-weight:500">规则优先级:等级豁免 → 新手考核 → 动态分享率 → 存活策略 → 零活跃清理 → H&R 惩罚</p>
|
||||
<p style="margin:0;font-size:12px;color:#909399">满足豁免条件的用户不受任何清理规则影响。各参数值可在导入后按需调整。</p>
|
||||
<p style="margin:0 0 4px;font-weight:500">规则体系:等级豁免 → 账号存活 → 流量监控 → H&R 惩罚 → 分享率检查</p>
|
||||
<p style="margin:0;font-size:12px;color:#909399">满足豁免条件的用户不受任何规则影响。导入后可调整参数和操作类型(封号/告警)。</p>
|
||||
</div>
|
||||
<div v-for="cat in presetCategories" :key="cat.title" style="margin-bottom:12px">
|
||||
<div style="font-weight:600;font-size:13px;margin-bottom:6px;color:#303133">{{ cat.title }}</div>
|
||||
@@ -185,7 +161,7 @@
|
||||
<el-checkbox :value="p.name" :label="p.name" style="margin-right:0;height:auto">
|
||||
<span style="font-weight:500;font-size:13px">{{ p.name }}</span>
|
||||
</el-checkbox>
|
||||
<span :style="{color:levelColor(p.level),fontSize:'11px',fontWeight:500}">{{ levelText(p.level) }}</span>
|
||||
<span :style="{color:actionColor(p.action),fontSize:'11px',fontWeight:500}">{{ actionText(p.action) }}</span>
|
||||
<span style="color:#909399;font-size:11px;flex:1">{{ presetDesc(p) }}</span>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
@@ -204,19 +180,16 @@ import { Plus, ArrowDown, ArrowUp, Rank, Delete } from '@element-plus/icons-vue'
|
||||
import api from '../api'
|
||||
|
||||
// ============ 选项配置 ============
|
||||
const levelOptions = [
|
||||
{ value: 'CRITICAL', label: '严重' },
|
||||
{ value: 'WARN', label: '关注' },
|
||||
{ value: 'INFO', label: '提示' },
|
||||
const actionOptions = [
|
||||
{ value: 'KILL', label: '封号' },
|
||||
{ value: 'WARN', label: '告警' },
|
||||
]
|
||||
const typeOptions = [
|
||||
{ value: 'KILL', label: '封号/删号' },
|
||||
{ value: 'DEMOTE', label: '降级' },
|
||||
{ value: 'TASK', label: '考核' },
|
||||
{ value: 'TRAFFIC',label: '流量' },
|
||||
{ value: 'LOGIN', label: '登录' },
|
||||
{ value: 'HD', label: 'H&R' },
|
||||
{ value: 'RATIO', label: '分享率' },
|
||||
{ value: 'KILL', label: '封号规则' },
|
||||
{ value: 'TRAFFIC', label: '流量监控' },
|
||||
{ value: 'LOGIN', label: '登录监控' },
|
||||
{ value: 'HD', label: 'H&R' },
|
||||
{ value: 'RATIO', label: '分享率' },
|
||||
]
|
||||
const condOptions = [
|
||||
{ value: 'GT', label: '大于' },
|
||||
@@ -244,65 +217,46 @@ const metricOptions = [
|
||||
]
|
||||
|
||||
const typeLabel = (t) => typeOptions.find(x => x.value === t)?.label || t
|
||||
const levelColor = (lv) => ({ CRITICAL: '#f56c6c', WARN: '#e6a23c', INFO: '#409eff' }[lv] || '#909399')
|
||||
const levelText = (lv) => ({ CRITICAL: '严重', WARN: '关注', INFO: '提示' }[lv] || lv)
|
||||
const actionColor = (a) => ({ KILL: '#f56c6c', WARN: '#e6a23c' }[a] || '#909399')
|
||||
const actionText = (a) => ({ KILL: '封号', WARN: '告警' }[a] || a)
|
||||
const applicableHint = (a) => {
|
||||
const m = { '!sealed':'未封存用户', 'sealed':'已封存用户', 'zero_traffic':'上传/下载均为 0', 'new_account_30d':'从未完成种子且无流量增量' }
|
||||
return a ? `— ${m[a] || ''}` : ''
|
||||
}
|
||||
|
||||
// ============ 预设规则模板(按 PT 管理规范 6 大类) ============
|
||||
// ============ 预设规则模板 ============
|
||||
// 每条模板包含 type(监控类型)+ action(KILL/WARN 操作)+ 触发条件
|
||||
const presets = [
|
||||
// 三、动态分享率
|
||||
{ name:'动态分享率-降级', type:'DEMOTE', level:'WARN', metric:'ratio', threshold:0.95, unit:'倍', condition:'LT', downloadThreshold:100, applicable:'!sealed', description:'下载量达100GB后分享率低于0.95时降级' },
|
||||
{ name:'动态分享率-严重', type:'DEMOTE', level:'CRITICAL', metric:'ratio', threshold:0.5, unit:'倍', condition:'LT', downloadThreshold:500, applicable:'!sealed', description:'下载量达500GB后分享率低于0.5时严重警告' },
|
||||
// 四、账号存活策略
|
||||
{ name:'未封存不登录清理', type:'KILL', level:'CRITICAL', metric:'days_since_last_login', threshold:150, unit:'天', condition:'GT', applicable:'!sealed', description:'未封存账号连续150天不登录删除' },
|
||||
{ name:'封存账号不登录清理', type:'KILL', level:'CRITICAL', metric:'days_since_last_login', threshold:365, unit:'天', condition:'GT', applicable:'sealed', description:'封存账号连续365天不登录删除' },
|
||||
{ name:'零流量账号清理', type:'KILL', level:'CRITICAL', metric:'days_since_last_login', threshold:30, unit:'天', condition:'GT', applicable:'zero_traffic', description:'零流量用户连续30天不登录删除' },
|
||||
{ name:'未封存不登录封号', type:'KILL', action:'KILL', metric:'days_since_last_login', threshold:150, unit:'天', condition:'GT', applicable:'!sealed', description:'未封存账号连续150天不登录即封号' },
|
||||
{ name:'封存账号不登录封号', type:'KILL', action:'KILL', metric:'days_since_last_login', threshold:365, unit:'天', condition:'GT', applicable:'sealed', description:'封存账号连续365天不登录即封号' },
|
||||
{ name:'零流量账号清理', type:'KILL', action:'KILL', metric:'days_since_last_login', threshold:30, unit:'天', condition:'GT', applicable:'zero_traffic', description:'零流量用户连续30天不登录即封号' },
|
||||
// 五、零活跃新用户清理
|
||||
{ name:'零活跃不登录清理', type:'KILL', level:'CRITICAL', metric:'days_since_last_login', threshold:7, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户连续7天不登录删除' },
|
||||
{ name:'零活跃注册天数清理', type:'KILL', level:'CRITICAL', metric:'days_since_registration', threshold:30, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户注册满30天删除' },
|
||||
// 六、H&R 惩罚
|
||||
{ name:'H&R 封号', type:'HD', level:'CRITICAL', metric:'hnr_count', threshold:5, unit:'次', condition:'GTE', description:'H&R违规次数达到5次即封号' },
|
||||
{ name:'H&R 警告', type:'HD', level:'WARN', metric:'hnr_count', threshold:3, unit:'次', condition:'GTE', description:'H&R违规次数达到3次告警' },
|
||||
// 补充:流量异常
|
||||
{ name:'连续无流量封号', type:'TRAFFIC', level:'CRITICAL', metric:'days_since_last_traffic', threshold:90, unit:'天', condition:'GT', description:'连续90天无上传/下载流量封号' },
|
||||
{ name:'长期未登录提醒', type:'LOGIN', level:'WARN', metric:'days_since_last_login', threshold:30, unit:'天', condition:'GT', description:'超过30天未登录提醒' },
|
||||
{ name:'零活跃不登录封号', type:'KILL', action:'KILL', metric:'days_since_last_login', threshold:7, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户连续7天不登录即封号' },
|
||||
{ name:'零活跃注册天数封号', type:'KILL', action:'KILL', metric:'days_since_registration', threshold:30, unit:'天', condition:'GT', applicable:'new_account_30d', description:'零活跃新用户注册满30天即封号' },
|
||||
// 流量监控
|
||||
{ name:'连续无流量封号', type:'TRAFFIC', action:'KILL', metric:'days_since_last_traffic', threshold:90, unit:'天', condition:'GT', description:'连续90天无上传/下载流量即封号' },
|
||||
{ name:'无流量告警', type:'TRAFFIC', action:'WARN', metric:'days_since_last_traffic', threshold:60, unit:'天', condition:'GT', description:'连续60天无流量时发出告警提醒' },
|
||||
// 登录监控
|
||||
{ name:'长期未登录告警', type:'LOGIN', action:'WARN', metric:'days_since_last_login', threshold:30, unit:'天', condition:'GT', description:'超过30天未登录时发出告警提醒' },
|
||||
{ name:'即将触发封号提醒', type:'LOGIN', action:'WARN', metric:'days_since_last_login', threshold:140, unit:'天', condition:'GT', applicable:'!sealed', description:'未封存用户即将达到150天封号线时提前告警' },
|
||||
// H&R
|
||||
{ name:'H&R 封号', type:'HD', action:'KILL', metric:'hnr_count', threshold:5, unit:'次', condition:'GTE', description:'H&R违规次数达到5次即封号' },
|
||||
{ name:'H&R 告警', type:'HD', action:'WARN', metric:'hnr_count', threshold:3, unit:'次', condition:'GTE', description:'H&R违规次数达到3次发出告警' },
|
||||
// 分享率
|
||||
{ name:'分享率过低告警', type:'RATIO', action:'WARN', metric:'ratio', threshold:1.0, unit:'倍', condition:'LT', description:'分享率低于1.0时发出告警提醒' },
|
||||
]
|
||||
|
||||
// 新手考核模板(TASK 类型,含多要求)
|
||||
const taskPresets = [
|
||||
{
|
||||
name:'新手考核', type:'TASK', level:'CRITICAL', metric:'task', threshold:0, unit:'', condition:'GT',
|
||||
applicable:'new_account_30d',
|
||||
description:'新用户需在30天内完成考核:上传≥50GB、下载≥20GB、魔力≥10000、做种积分≥5000、平均做种≥24h',
|
||||
requirements: [
|
||||
{ label:'上传量', metric:'upload_gb', threshold:50, condition:'LT', unit:'GB' },
|
||||
{ label:'下载量', metric:'download_gb', threshold:20, condition:'LT', unit:'GB' },
|
||||
{ label:'魔力值', metric:'bonus', threshold:10000,condition:'LT', unit:'' },
|
||||
{ label:'做种积分',metric:'seeding', threshold:5000, condition:'LT', unit:'' },
|
||||
{ label:'平均做种',metric:'avg_seed_time', threshold:24, condition:'LT', unit:'h' },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const allPresets = [...presets, ...taskPresets]
|
||||
|
||||
const presetCategories = [
|
||||
{ title:'三、动态分享率', items: presets.filter(p => p.type === 'DEMOTE') },
|
||||
{ title:'四、账号存活策略', items: presets.filter(p => p.name.includes('不登录清理') || p.name.includes('账号清理')) },
|
||||
{ title:'四、账号存活策略', items: presets.filter(p => p.name.includes('不登录') || p.name.includes('账号清理')) },
|
||||
{ title:'五、零活跃新用户清理', items: presets.filter(p => p.name.includes('零活跃')) },
|
||||
{ title:'二、新手考核', items: taskPresets },
|
||||
{ title:'六、H&R 惩罚', items: presets.filter(p => p.type === 'HD') },
|
||||
{ title:'补充规则', items: presets.filter(p => !['DEMOTE','HD'].includes(p.type) && !p.name.includes('不登录') && !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 === 'LOGIN') },
|
||||
{ title:'分享率监控', items: presets.filter(p => p.type === 'RATIO') },
|
||||
]
|
||||
|
||||
const presetDesc = (p) => {
|
||||
if (p.type === 'TASK' && p.requirements) {
|
||||
return p.requirements.map(r => `${r.label}≥${r.threshold}${r.unit||''}`).join(',')
|
||||
}
|
||||
if (p.type === 'DEMOTE') return `下载≥${p.downloadThreshold}GB后分享率${condOptions.find(c=>c.value===p.condition)?.label||p.condition}${p.threshold}`
|
||||
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+')':''}`
|
||||
@@ -358,8 +312,8 @@ const sync = () => { editorJson.value = JSON.stringify(parsed.value, null, 2) }
|
||||
|
||||
const addRule = () => {
|
||||
parsed.value = [...parsed.value, {
|
||||
type:'KILL', name:'', level:'CRITICAL', metric:'days_since_last_login', threshold:0, unit:'天', condition:'GT',
|
||||
applicable:null, description:'', requirements:null, _fold:false
|
||||
type:'KILL', action:'KILL', name:'', metric:'days_since_last_login', threshold:0, unit:'天', condition:'GT',
|
||||
applicable:null, description:'', _fold:false
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -368,28 +322,10 @@ const confirmRemove = async (idx) => {
|
||||
const arr = [...parsed.value]; arr.splice(idx, 1); parsed.value = arr
|
||||
}
|
||||
|
||||
const onTypeChange = (rule) => {
|
||||
if (rule.type === 'TASK' && !rule.requirements) rule.requirements = []
|
||||
if (rule.type === 'DEMOTE' && rule.downloadThreshold == null) rule.downloadThreshold = 100
|
||||
sync()
|
||||
}
|
||||
|
||||
const addRequirement = (rule) => {
|
||||
if (!rule.requirements) rule.requirements = []
|
||||
rule.requirements.push({ label:'', metric:'upload_gb', threshold:0, condition:'LT', unit:'GB' })
|
||||
sync()
|
||||
}
|
||||
|
||||
const removeRequirement = (rule, ri) => {
|
||||
rule.requirements.splice(ri, 1)
|
||||
if (!rule.requirements.length) rule.requirements = null
|
||||
sync()
|
||||
}
|
||||
|
||||
const addPreset = (p) => { parsed.value = [...parsed.value, { ...p, _fold: false }] }
|
||||
|
||||
const importPresets = () => {
|
||||
const items = allPresets.filter(p => selectedPresets.value.includes(p.name)).map(p => ({ ...p, _fold: false }))
|
||||
const items = presets.filter(p => selectedPresets.value.includes(p.name)).map(p => ({ ...p, _fold: false }))
|
||||
parsed.value = [...parsed.value, ...items]
|
||||
selectedPresets.value = []
|
||||
showPresets.value = false
|
||||
@@ -435,9 +371,8 @@ onMounted(fetchAll)
|
||||
cursor: default;
|
||||
}
|
||||
.rule-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.rule-card.critical { border-left: 4px solid #f56c6c; }
|
||||
.rule-card.warn { border-left: 4px solid #e6a23c; }
|
||||
.rule-card.info { border-left: 4px solid #409eff; }
|
||||
.rule-card.kill { border-left: 4px solid #f56c6c; }
|
||||
.rule-card.warn { border-left: 4px solid #e6a23c; }
|
||||
|
||||
.card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
@@ -456,10 +391,4 @@ onMounted(fetchAll)
|
||||
.field-hint { color: #909399; font-size: 11px; }
|
||||
.condition-text { color: #909399; font-size: 12px; }
|
||||
.then-arrow { color: #409eff; font-size: 16px; font-weight: 700; margin: 0 2px; }
|
||||
|
||||
.task-section {
|
||||
margin-top: 2px; padding: 8px 10px;
|
||||
border: 1px dashed #d9ecff; border-radius: 6px; background: #f5f9ff;
|
||||
}
|
||||
.req-row { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user