屏蔽表已创建
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import {
|
||||
createForbiddenWordBlockingRule,
|
||||
createIPBlockingRule,
|
||||
createNodeBlockingRule,
|
||||
deleteForbiddenWordBlockingRule,
|
||||
deleteIPBlockingRule,
|
||||
deleteNodeBlockingRule,
|
||||
getForbiddenWordBlockingRules,
|
||||
getIPBlockingRules,
|
||||
getNodeBlockingRules,
|
||||
updateForbiddenWordBlockingRule,
|
||||
updateIPBlockingRule,
|
||||
updateNodeBlockingRule,
|
||||
} from '../api'
|
||||
import type {
|
||||
ForbiddenWordBlockingRule,
|
||||
ForbiddenWordBlockingRulePayload,
|
||||
IPBlockingRule,
|
||||
IPBlockingRulePayload,
|
||||
NodeBlockingRule,
|
||||
NodeBlockingRulePayload,
|
||||
} from '../types'
|
||||
|
||||
const pageSize = 25
|
||||
|
||||
const nodeRules = ref<NodeBlockingRule[]>([])
|
||||
const nodeLoading = ref(false)
|
||||
const nodeError = ref('')
|
||||
const nodeMessage = ref('')
|
||||
const nodePage = ref(1)
|
||||
const nodeTotal = ref(0)
|
||||
const newNodeId = ref('')
|
||||
const newNodeNum = ref('')
|
||||
const newNodeReason = ref('')
|
||||
const newNodeEnabled = ref(true)
|
||||
const nodeEdits = ref<Record<number, { node_id: string; node_num: string; reason: string; enabled: boolean }>>({})
|
||||
|
||||
const ipRules = ref<IPBlockingRule[]>([])
|
||||
const ipLoading = ref(false)
|
||||
const ipError = ref('')
|
||||
const ipMessage = ref('')
|
||||
const ipPage = ref(1)
|
||||
const ipTotal = ref(0)
|
||||
const newIPValue = ref('')
|
||||
const newIPReason = ref('')
|
||||
const newIPEnabled = ref(true)
|
||||
const ipEdits = ref<Record<number, IPBlockingRulePayload>>({})
|
||||
|
||||
const wordRules = ref<ForbiddenWordBlockingRule[]>([])
|
||||
const wordLoading = ref(false)
|
||||
const wordError = ref('')
|
||||
const wordMessage = ref('')
|
||||
const wordPage = ref(1)
|
||||
const wordTotal = ref(0)
|
||||
const newWord = ref('')
|
||||
const newWordMatchType = ref('contains')
|
||||
const newWordCaseSensitive = ref(false)
|
||||
const newWordReason = ref('')
|
||||
const newWordEnabled = ref(true)
|
||||
const wordEdits = ref<Record<number, ForbiddenWordBlockingRulePayload>>({})
|
||||
|
||||
function formatTime(value: string): string {
|
||||
return new Date(value).toLocaleString()
|
||||
}
|
||||
|
||||
function canPrev(page: number): boolean {
|
||||
return page > 1
|
||||
}
|
||||
|
||||
function canNext(page: number, total: number, count: number): boolean {
|
||||
return page * pageSize < total || count === pageSize
|
||||
}
|
||||
|
||||
function parseOptionalInt(value: string): number | null {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10)
|
||||
if (!Number.isFinite(parsed) || String(parsed) !== trimmed) {
|
||||
throw new Error('节点数字 ID 必须是整数')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function nodePayload(nodeId: string, nodeNum: string, reason: string, enabled: boolean): NodeBlockingRulePayload {
|
||||
if (!nodeId.trim()) {
|
||||
throw new Error('节点 ID 不能为空')
|
||||
}
|
||||
return {
|
||||
node_id: nodeId.trim(),
|
||||
node_num: parseOptionalInt(nodeNum),
|
||||
reason: reason.trim(),
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
|
||||
function ipPayload(ipValue: string, reason: string, enabled: boolean): IPBlockingRulePayload {
|
||||
if (!ipValue.trim()) {
|
||||
throw new Error('IP 或 CIDR 不能为空')
|
||||
}
|
||||
return { ip_value: ipValue.trim(), reason: reason.trim(), enabled }
|
||||
}
|
||||
|
||||
function wordPayload(word: string, matchType: string, caseSensitive: boolean, reason: string, enabled: boolean): ForbiddenWordBlockingRulePayload {
|
||||
if (!word.trim()) {
|
||||
throw new Error('违禁词不能为空')
|
||||
}
|
||||
return {
|
||||
word: word.trim(),
|
||||
match_type: matchType || 'contains',
|
||||
case_sensitive: caseSensitive,
|
||||
reason: reason.trim(),
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
|
||||
function resetNodeEdits(items: NodeBlockingRule[]) {
|
||||
nodeEdits.value = Object.fromEntries(
|
||||
items.map((item) => [
|
||||
item.id,
|
||||
{
|
||||
node_id: item.node_id,
|
||||
node_num: item.node_num == null ? '' : String(item.node_num),
|
||||
reason: item.reason,
|
||||
enabled: item.enabled,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function resetIPEdits(items: IPBlockingRule[]) {
|
||||
ipEdits.value = Object.fromEntries(items.map((item) => [item.id, { ip_value: item.ip_value, reason: item.reason, enabled: item.enabled }]))
|
||||
}
|
||||
|
||||
function resetWordEdits(items: ForbiddenWordBlockingRule[]) {
|
||||
wordEdits.value = Object.fromEntries(
|
||||
items.map((item) => [
|
||||
item.id,
|
||||
{
|
||||
word: item.word,
|
||||
match_type: item.match_type,
|
||||
case_sensitive: item.case_sensitive,
|
||||
reason: item.reason,
|
||||
enabled: item.enabled,
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
async function refreshNodeRules(page = nodePage.value) {
|
||||
nodeLoading.value = true
|
||||
nodeError.value = ''
|
||||
try {
|
||||
const safePage = Math.max(1, page)
|
||||
const response = await getNodeBlockingRules(pageSize, (safePage - 1) * pageSize)
|
||||
nodeRules.value = response.items
|
||||
nodeTotal.value = response.total ?? response.offset + response.items.length
|
||||
nodePage.value = safePage
|
||||
resetNodeEdits(response.items)
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
nodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createNodeRule() {
|
||||
nodeError.value = ''
|
||||
nodeMessage.value = ''
|
||||
let payload: NodeBlockingRulePayload
|
||||
try {
|
||||
payload = nodePayload(newNodeId.value, newNodeNum.value, newNodeReason.value, newNodeEnabled.value)
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
nodeLoading.value = true
|
||||
try {
|
||||
await createNodeBlockingRule(payload)
|
||||
newNodeId.value = ''
|
||||
newNodeNum.value = ''
|
||||
newNodeReason.value = ''
|
||||
newNodeEnabled.value = true
|
||||
nodeMessage.value = '节点屏蔽规则已新增'
|
||||
await refreshNodeRules(1)
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
nodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNodeRule(rule: NodeBlockingRule) {
|
||||
nodeError.value = ''
|
||||
nodeMessage.value = ''
|
||||
const edit = nodeEdits.value[rule.id]
|
||||
if (!edit) {
|
||||
return
|
||||
}
|
||||
let payload: NodeBlockingRulePayload
|
||||
try {
|
||||
payload = nodePayload(edit.node_id, edit.node_num, edit.reason, edit.enabled)
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
nodeLoading.value = true
|
||||
try {
|
||||
await updateNodeBlockingRule(rule.id, payload)
|
||||
nodeMessage.value = '节点屏蔽规则已保存'
|
||||
await refreshNodeRules()
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
nodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNodeRule(rule: NodeBlockingRule) {
|
||||
nodeError.value = ''
|
||||
nodeMessage.value = ''
|
||||
nodeLoading.value = true
|
||||
try {
|
||||
await deleteNodeBlockingRule(rule.id)
|
||||
nodeMessage.value = '节点屏蔽规则已删除'
|
||||
await refreshNodeRules(nodeRules.value.length === 1 ? nodePage.value - 1 : nodePage.value)
|
||||
} catch (err) {
|
||||
nodeError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
nodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshIPRules(page = ipPage.value) {
|
||||
ipLoading.value = true
|
||||
ipError.value = ''
|
||||
try {
|
||||
const safePage = Math.max(1, page)
|
||||
const response = await getIPBlockingRules(pageSize, (safePage - 1) * pageSize)
|
||||
ipRules.value = response.items
|
||||
ipTotal.value = response.total ?? response.offset + response.items.length
|
||||
ipPage.value = safePage
|
||||
resetIPEdits(response.items)
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
ipLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createIPRule() {
|
||||
ipError.value = ''
|
||||
ipMessage.value = ''
|
||||
let payload: IPBlockingRulePayload
|
||||
try {
|
||||
payload = ipPayload(newIPValue.value, newIPReason.value, newIPEnabled.value)
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
ipLoading.value = true
|
||||
try {
|
||||
await createIPBlockingRule(payload)
|
||||
newIPValue.value = ''
|
||||
newIPReason.value = ''
|
||||
newIPEnabled.value = true
|
||||
ipMessage.value = 'IP 屏蔽规则已新增'
|
||||
await refreshIPRules(1)
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
ipLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIPRule(rule: IPBlockingRule) {
|
||||
ipError.value = ''
|
||||
ipMessage.value = ''
|
||||
const edit = ipEdits.value[rule.id]
|
||||
if (!edit) {
|
||||
return
|
||||
}
|
||||
let payload: IPBlockingRulePayload
|
||||
try {
|
||||
payload = ipPayload(edit.ip_value, edit.reason, edit.enabled)
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
ipLoading.value = true
|
||||
try {
|
||||
await updateIPBlockingRule(rule.id, payload)
|
||||
ipMessage.value = 'IP 屏蔽规则已保存'
|
||||
await refreshIPRules()
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
ipLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeIPRule(rule: IPBlockingRule) {
|
||||
ipError.value = ''
|
||||
ipMessage.value = ''
|
||||
ipLoading.value = true
|
||||
try {
|
||||
await deleteIPBlockingRule(rule.id)
|
||||
ipMessage.value = 'IP 屏蔽规则已删除'
|
||||
await refreshIPRules(ipRules.value.length === 1 ? ipPage.value - 1 : ipPage.value)
|
||||
} catch (err) {
|
||||
ipError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
ipLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWordRules(page = wordPage.value) {
|
||||
wordLoading.value = true
|
||||
wordError.value = ''
|
||||
try {
|
||||
const safePage = Math.max(1, page)
|
||||
const response = await getForbiddenWordBlockingRules(pageSize, (safePage - 1) * pageSize)
|
||||
wordRules.value = response.items
|
||||
wordTotal.value = response.total ?? response.offset + response.items.length
|
||||
wordPage.value = safePage
|
||||
resetWordEdits(response.items)
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
wordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createWordRule() {
|
||||
wordError.value = ''
|
||||
wordMessage.value = ''
|
||||
let payload: ForbiddenWordBlockingRulePayload
|
||||
try {
|
||||
payload = wordPayload(newWord.value, newWordMatchType.value, newWordCaseSensitive.value, newWordReason.value, newWordEnabled.value)
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
wordLoading.value = true
|
||||
try {
|
||||
await createForbiddenWordBlockingRule(payload)
|
||||
newWord.value = ''
|
||||
newWordMatchType.value = 'contains'
|
||||
newWordCaseSensitive.value = false
|
||||
newWordReason.value = ''
|
||||
newWordEnabled.value = true
|
||||
wordMessage.value = '违禁词屏蔽规则已新增'
|
||||
await refreshWordRules(1)
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
wordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWordRule(rule: ForbiddenWordBlockingRule) {
|
||||
wordError.value = ''
|
||||
wordMessage.value = ''
|
||||
const edit = wordEdits.value[rule.id]
|
||||
if (!edit) {
|
||||
return
|
||||
}
|
||||
let payload: ForbiddenWordBlockingRulePayload
|
||||
try {
|
||||
payload = wordPayload(edit.word, edit.match_type, edit.case_sensitive, edit.reason, edit.enabled)
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
return
|
||||
}
|
||||
wordLoading.value = true
|
||||
try {
|
||||
await updateForbiddenWordBlockingRule(rule.id, payload)
|
||||
wordMessage.value = '违禁词屏蔽规则已保存'
|
||||
await refreshWordRules()
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
wordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeWordRule(rule: ForbiddenWordBlockingRule) {
|
||||
wordError.value = ''
|
||||
wordMessage.value = ''
|
||||
wordLoading.value = true
|
||||
try {
|
||||
await deleteForbiddenWordBlockingRule(rule.id)
|
||||
wordMessage.value = '违禁词屏蔽规则已删除'
|
||||
await refreshWordRules(wordRules.value.length === 1 ? wordPage.value - 1 : wordPage.value)
|
||||
} catch (err) {
|
||||
wordError.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
wordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshNodeRules()
|
||||
refreshIPRules()
|
||||
refreshWordRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-dashboard">
|
||||
<div class="panel admin-status-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Blocking</p>
|
||||
<h2>屏蔽管理</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="empty">管理节点、IP/CIDR、违禁词三类屏蔽规则。当前页面只维护规则,不改变 MQTT 转发行为。</p>
|
||||
</div>
|
||||
|
||||
<div class="panel admin-status-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Forbidden Words</p>
|
||||
<h2>违禁词屏蔽</h2>
|
||||
</div>
|
||||
<button class="admin-button" @click="refreshWordRules()" :disabled="wordLoading">{{ wordLoading ? '刷新中...' : '刷新' }}</button>
|
||||
</div>
|
||||
|
||||
<form class="admin-form admin-user-form" @submit.prevent="createWordRule">
|
||||
<label>
|
||||
<span>违禁词</span>
|
||||
<input v-model="newWord" autocomplete="off" placeholder="spam" />
|
||||
</label>
|
||||
<label>
|
||||
<span>匹配类型</span>
|
||||
<select v-model="newWordMatchType" class="admin-table-input">
|
||||
<option value="contains">包含</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>区分大小写</span>
|
||||
<input v-model="newWordCaseSensitive" type="checkbox" />
|
||||
</label>
|
||||
<label>
|
||||
<span>原因</span>
|
||||
<input v-model="newWordReason" autocomplete="off" placeholder="policy" />
|
||||
</label>
|
||||
<label>
|
||||
<span>启用</span>
|
||||
<input v-model="newWordEnabled" type="checkbox" />
|
||||
</label>
|
||||
<button class="admin-button" :disabled="wordLoading" type="submit">新增违禁词规则</button>
|
||||
</form>
|
||||
<p v-if="wordError" class="error">{{ wordError }}</p>
|
||||
<p v-if="wordMessage" class="success">{{ wordMessage }}</p>
|
||||
|
||||
<div class="node-table-wrap">
|
||||
<table class="node-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>违禁词</th>
|
||||
<th>匹配类型</th>
|
||||
<th>区分大小写</th>
|
||||
<th>原因</th>
|
||||
<th>启用</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in wordRules" :key="rule.id">
|
||||
<td>{{ rule.id }}</td>
|
||||
<td><input v-model="wordEdits[rule.id].word" class="admin-table-input" /></td>
|
||||
<td>
|
||||
<select v-model="wordEdits[rule.id].match_type" class="admin-table-input">
|
||||
<option value="contains">包含</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input v-model="wordEdits[rule.id].case_sensitive" type="checkbox" /></td>
|
||||
<td><input v-model="wordEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||
<td><input v-model="wordEdits[rule.id].enabled" type="checkbox" /></td>
|
||||
<td>{{ formatTime(rule.created_at) }}</td>
|
||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
||||
<td>
|
||||
<button class="admin-button" :disabled="wordLoading" @click="saveWordRule(rule)">保存</button>
|
||||
<button class="admin-button" :disabled="wordLoading" @click="removeWordRule(rule)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="wordRules.length === 0" class="empty">暂无违禁词屏蔽规则</div>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button class="admin-button" :disabled="wordLoading || !canPrev(wordPage)" @click="refreshWordRules(wordPage - 1)">上一页</button>
|
||||
<span>第 {{ wordPage }} 页 · 共 {{ wordTotal }} 条</span>
|
||||
<button class="admin-button" :disabled="wordLoading || !canNext(wordPage, wordTotal, wordRules.length)" @click="refreshWordRules(wordPage + 1)">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel admin-status-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Nodes</p>
|
||||
<h2>节点屏蔽</h2>
|
||||
</div>
|
||||
<button class="admin-button" @click="refreshNodeRules()" :disabled="nodeLoading">{{ nodeLoading ? '刷新中...' : '刷新' }}</button>
|
||||
</div>
|
||||
|
||||
<form class="admin-form admin-user-form" @submit.prevent="createNodeRule">
|
||||
<label>
|
||||
<span>节点 ID</span>
|
||||
<input v-model="newNodeId" autocomplete="off" placeholder="!12345678" />
|
||||
</label>
|
||||
<label>
|
||||
<span>节点数字 ID</span>
|
||||
<input v-model="newNodeNum" autocomplete="off" placeholder="可选" />
|
||||
</label>
|
||||
<label>
|
||||
<span>原因</span>
|
||||
<input v-model="newNodeReason" autocomplete="off" placeholder="spam / abuse" />
|
||||
</label>
|
||||
<label>
|
||||
<span>启用</span>
|
||||
<input v-model="newNodeEnabled" type="checkbox" />
|
||||
</label>
|
||||
<button class="admin-button" :disabled="nodeLoading" type="submit">新增节点规则</button>
|
||||
</form>
|
||||
<p v-if="nodeError" class="error">{{ nodeError }}</p>
|
||||
<p v-if="nodeMessage" class="success">{{ nodeMessage }}</p>
|
||||
|
||||
<div class="node-table-wrap">
|
||||
<table class="node-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>节点 ID</th>
|
||||
<th>数字 ID</th>
|
||||
<th>原因</th>
|
||||
<th>启用</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in nodeRules" :key="rule.id">
|
||||
<td>{{ rule.id }}</td>
|
||||
<td><input v-model="nodeEdits[rule.id].node_id" class="admin-table-input" /></td>
|
||||
<td><input v-model="nodeEdits[rule.id].node_num" class="admin-table-input" placeholder="可选" /></td>
|
||||
<td><input v-model="nodeEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||
<td><input v-model="nodeEdits[rule.id].enabled" type="checkbox" /></td>
|
||||
<td>{{ formatTime(rule.created_at) }}</td>
|
||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
||||
<td>
|
||||
<button class="admin-button" :disabled="nodeLoading" @click="saveNodeRule(rule)">保存</button>
|
||||
<button class="admin-button" :disabled="nodeLoading" @click="removeNodeRule(rule)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="nodeRules.length === 0" class="empty">暂无节点屏蔽规则</div>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button class="admin-button" :disabled="nodeLoading || !canPrev(nodePage)" @click="refreshNodeRules(nodePage - 1)">上一页</button>
|
||||
<span>第 {{ nodePage }} 页 · 共 {{ nodeTotal }} 条</span>
|
||||
<button class="admin-button" :disabled="nodeLoading || !canNext(nodePage, nodeTotal, nodeRules.length)" @click="refreshNodeRules(nodePage + 1)">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel admin-status-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">IP / CIDR</p>
|
||||
<h2>IP 屏蔽</h2>
|
||||
</div>
|
||||
<button class="admin-button" @click="refreshIPRules()" :disabled="ipLoading">{{ ipLoading ? '刷新中...' : '刷新' }}</button>
|
||||
</div>
|
||||
|
||||
<form class="admin-form admin-user-form" @submit.prevent="createIPRule">
|
||||
<label>
|
||||
<span>IP 或 CIDR</span>
|
||||
<input v-model="newIPValue" autocomplete="off" placeholder="127.0.0.1 或 192.168.1.0/24" />
|
||||
</label>
|
||||
<label>
|
||||
<span>原因</span>
|
||||
<input v-model="newIPReason" autocomplete="off" placeholder="abuse" />
|
||||
</label>
|
||||
<label>
|
||||
<span>启用</span>
|
||||
<input v-model="newIPEnabled" type="checkbox" />
|
||||
</label>
|
||||
<button class="admin-button" :disabled="ipLoading" type="submit">新增 IP 规则</button>
|
||||
</form>
|
||||
<p v-if="ipError" class="error">{{ ipError }}</p>
|
||||
<p v-if="ipMessage" class="success">{{ ipMessage }}</p>
|
||||
|
||||
<div class="node-table-wrap">
|
||||
<table class="node-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>IP/CIDR</th>
|
||||
<th>原因</th>
|
||||
<th>启用</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in ipRules" :key="rule.id">
|
||||
<td>{{ rule.id }}</td>
|
||||
<td><input v-model="ipEdits[rule.id].ip_value" class="admin-table-input" /></td>
|
||||
<td><input v-model="ipEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||
<td><input v-model="ipEdits[rule.id].enabled" type="checkbox" /></td>
|
||||
<td>{{ formatTime(rule.created_at) }}</td>
|
||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
||||
<td>
|
||||
<button class="admin-button" :disabled="ipLoading" @click="saveIPRule(rule)">保存</button>
|
||||
<button class="admin-button" :disabled="ipLoading" @click="removeIPRule(rule)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="ipRules.length === 0" class="empty">暂无 IP 屏蔽规则</div>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button class="admin-button" :disabled="ipLoading || !canPrev(ipPage)" @click="refreshIPRules(ipPage - 1)">上一页</button>
|
||||
<span>第 {{ ipPage }} 页 · 共 {{ ipTotal }} 条</span>
|
||||
<button class="admin-button" :disabled="ipLoading || !canNext(ipPage, ipTotal, ipRules.length)" @click="refreshIPRules(ipPage + 1)">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user