Files
nexus-terminal/packages/frontend/src/components/settings/IpBlacklistSettings.vue
T
2025-05-11 21:53:43 +08:00

124 lines
7.9 KiB
Vue

<template>
<div class="bg-background border border-border rounded-lg shadow-sm overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-border bg-header/50">
<h2 class="text-lg font-semibold text-foreground">{{ $t('settings.ipBlacklist.title') }}</h2>
<!-- IP Blacklist Enable/Disable Switch -->
<button
type="button"
@click="handleUpdateIpBlacklistEnabled"
:class="[
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary',
ipBlacklistEnabled ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
]"
role="switch"
:aria-checked="ipBlacklistEnabled"
>
<span
aria-hidden="true"
:class="[
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
ipBlacklistEnabled ? 'translate-x-5' : 'translate-x-0'
]"
></span>
</button>
</div>
<div class="p-6 space-y-6">
<!-- Existing Blacklist Content (Conditional Rendering) -->
<div v-if="ipBlacklistEnabled" class="space-y-6 pt-4">
<p class="text-sm text-text-secondary">{{ $t('settings.ipBlacklist.description') }}</p>
<!-- Blacklist config form -->
<form @submit.prevent="handleUpdateBlacklistSettings" class="flex flex-wrap items-end gap-4">
<div class="flex-grow min-w-[150px]">
<label for="maxLoginAttempts" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.ipBlacklist.maxAttemptsLabel') }}</label>
<input type="number" id="maxLoginAttempts" v-model="blacklistSettingsForm.maxLoginAttempts" min="1" required
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
</div>
<div class="flex-grow min-w-[150px]">
<label for="loginBanDuration" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.ipBlacklist.banDurationLabel') }}</label>
<input type="number" id="loginBanDuration" v-model="blacklistSettingsForm.loginBanDuration" min="1" required
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
</div>
<div class="flex-shrink-0">
<button type="submit"
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-150 ease-in-out text-sm font-medium">
{{ $t('settings.ipBlacklist.saveConfigButton') }}
</button>
</div>
<p v-if="blacklistSettingsMessage" :class="['w-full mt-2 text-sm', blacklistSettingsSuccess ? 'text-success' : 'text-error']">{{ blacklistSettingsMessage }}</p>
</form>
<hr class="border-border/50">
<!-- Blacklist table -->
<h3 class="text-base font-semibold text-foreground">{{ $t('settings.ipBlacklist.currentBannedTitle') }}</h3>
<!-- Error state -->
<div v-if="ipBlacklist.error" class="p-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ ipBlacklist.error }}</div>
<!-- Loading state (Only show if loading AND no entries are displayed yet) -->
<div v-else-if="ipBlacklist.loading && ipBlacklist.entries.length === 0" class="p-4 text-center text-text-secondary italic">{{ $t('settings.ipBlacklist.loadingList') }}</div>
<!-- Empty state (Show only if not loading, no error, and entries empty) -->
<p v-else-if="!ipBlacklist.loading && !ipBlacklist.error && ipBlacklist.entries.length === 0" class="p-4 text-center text-text-secondary italic">{{ $t('settings.ipBlacklist.noBannedIps') }}</p>
<!-- Table (Show if not loading, no error, and has entries) -->
<div v-else-if="!ipBlacklist.loading && !ipBlacklist.error && ipBlacklist.entries.length > 0" class="overflow-x-auto border border-border rounded-lg shadow-sm bg-background">
<table class="min-w-full divide-y divide-border text-sm">
<thead class="bg-header">
<tr>
<th scope="col" class="px-4 py-2 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('settings.ipBlacklist.table.ipAddress') }}</th>
<th scope="col" class="px-4 py-2 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('settings.ipBlacklist.table.attempts') }}</th>
<th scope="col" class="px-4 py-2 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('settings.ipBlacklist.table.lastAttempt') }}</th>
<th scope="col" class="px-4 py-2 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('settings.ipBlacklist.table.bannedUntil') }}</th>
<th scope="col" class="px-4 py-2 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('settings.ipBlacklist.table.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr v-for="entry in ipBlacklist.entries" :key="entry.ip" class="hover:bg-header/50">
<td class="px-4 py-2 whitespace-nowrap">{{ entry.ip }}</td>
<td class="px-4 py-2 whitespace-nowrap">{{ entry.attempts }}</td>
<td class="px-4 py-2 whitespace-nowrap">{{ new Date(entry.last_attempt_at * 1000).toLocaleString() }}</td>
<td class="px-4 py-2 whitespace-nowrap">{{ entry.blocked_until ? new Date(entry.blocked_until * 1000).toLocaleString() : $t('statusMonitor.notAvailable') }}</td>
<td class="px-4 py-2 whitespace-nowrap">
<button
@click="handleDeleteIp(entry.ip)"
:disabled="blacklistDeleteLoading && blacklistToDeleteIp === entry.ip"
class="px-2 py-1 bg-error text-error-text rounded text-xs font-medium hover:bg-error/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-error disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out"
>
{{ (blacklistDeleteLoading && blacklistToDeleteIp === entry.ip) ? $t('settings.ipBlacklist.table.deleting') : $t('settings.ipBlacklist.table.removeButton') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Delete Error (Show regardless of loading state if present) -->
<p v-if="blacklistDeleteError" class="mt-3 text-sm text-error">{{ blacklistDeleteError }}</p>
</div> <!-- End v-if="ipBlacklistEnabled" -->
<!-- Message when disabled -->
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
{{ $t('settings.ipBlacklist.disabledMessage', 'IP 黑名单功能当前已禁用。') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useIpBlacklist } from '../../composables/settings/useIpBlacklist';
// const { t } = useI18n(); // $t is globally available in template
const {
ipBlacklistEnabled,
handleUpdateIpBlacklistEnabled,
blacklistSettingsForm,
// blacklistSettingsLoading, // Not used directly in template, handled by form submit button state
blacklistSettingsMessage,
blacklistSettingsSuccess,
handleUpdateBlacklistSettings,
ipBlacklist,
blacklistToDeleteIp,
blacklistDeleteLoading,
blacklistDeleteError,
handleDeleteIp,
} = useIpBlacklist();
</script>
<style scoped>
/* Styles specific to IpBlacklistSettings if any */
</style>