BrianChuan010393's picture
在本地端已經嘗試成功「防護中心」、「事件分析」、「基準管理」的相關API串接
72390b3 verified
Raw
History Blame Contribute Delete
70.9 kB
<script setup lang="ts">
import { ref, onMounted, computed, watch, nextTick } from 'vue'
import { Client } from "@gradio/client"
import { gradioService } from './services/gradioService'
import {
ShieldCheck, Activity, AlertTriangle, ShieldAlert, Search, FileText,
Settings, Bell, User, Globe, Clock, X, Terminal, BrainCircuit,
ShieldX, CheckCircle2, ChevronRight, Loader2, Zap, Target, Lock,
LogOut, ChevronDown, ChevronUp, GraduationCap, BookOpen, Users,
Info, Mail, Cloud
} from 'lucide-vue-next'
import { Chart, registerables } from 'chart.js'
// --- 🌟 API 路由對照表 ---
const ACTION_ENDPOINTS: Record<string, string> = {
'期末教學評量': 'lambda',
'預選系統': 'lambda1',
'請假系統': 'lambda7',
'宿舍登錄': 'lambda8',
'網路郵局': 'lambda10',
'軟體雲': 'lambda11',
'嘗試偷改成績 (SQL Injection)': 'lambda12'
}
Chart.register(...registerables)
// Navigation State
const activeTab = ref('防護中心')
const userRole = ref<string | null>(null) // null, 'admin', 'student'
// Loading states
const isLoggingIn = ref(false)
const isLoggingOut = ref(false)
const isUpdatingWeights = ref(false)
// Login State
const loginForm = ref({
account: '',
password: '',
captcha: ''
})
const loginError = ref('')
// Simulator State
const simulator = ref({
ip: '114.32.1.45',
sessionId: 'sess_9a2b4c6d8e0f',
timeMode: 'real', // real, custom
customTime: '2026-03-17 14:00:00'
})
const ips = ['114.32.1.45', '203.104.1.12', '192.168.1.1', '82.1.4.156', '111.65.1.22']
const simLogs = ref([
{ timestamp: '2026-03-17 13:55:21', ip: '114.32.1.45', cookieId: 'sess_9a2b4c6d8e0f', account: 'admin', result: '正常' },
{ timestamp: '2026-03-17 13:56:05', ip: '82.1.4.156', cookieId: 'sess_unknown', account: 'guest', result: '異常' },
])
const addLog = (account: string, result: string, actionDesc?: string) => {
const now = new Date()
simLogs.value.unshift({
timestamp: simulator.value.timeMode === 'real' ? now.toLocaleString() : simulator.value.customTime,
ip: simulator.value.ip,
cookieId: simulator.value.sessionId,
account: account || 'unknown',
result: result,
action: actionDesc
})
}
const syncLogData = async () => {
try {
const result = await gradioService.updateDataframes()
// Use the first dataframe from the result
const df = result.dataframe1 || []
if (!Array.isArray(df)) {
console.warn("⚠️ 同步到的資料不是陣列,嘗試直接解析", df)
}
// Check if data exists
if (df && df.length > 0) {
// Transform data to match frontend format
simLogs.value = df.map((item: any) => ({
timestamp: item.timestamp || item.time || item.datetime || '',
ip: item.ip_address || item.ip || item.src_ip || '',
cookieId: item.cookie_id || item.cookieId || item.sessionId || '',
account: item.account || '',
result: item.status || item.result || item.action || '正常'
}))
console.log("✅ 成功更新 Logs 顯示:", simLogs.value)
} else {
console.warn("⚠️ 資料為空")
simLogs.value = []
}
} catch (e) {
console.error("❌ 資料同步失敗:", e)
}
}
// Sync stats from backend data
const syncStats = async () => {
try {
const result = await gradioService.getStats()
// Backend returns: { status, ai_ready, data: { total_logs, abnormal_logs, anomaly_rate_percent, current_weights } }
if (result && result.data) {
const { total_logs, abnormal_logs, anomaly_rate_percent } = result.data
// 防呆:如果後端返回 undefined/null,給預設值
let totalCount = total_logs || 0
let abnormalRate = anomaly_rate_percent || 0
let systemStatus = result.ai_ready ? '正常執行' : 'AI 模組錯誤'
if (typeof totalCount !== 'number' || isNaN(totalCount)) totalCount = 0
if (typeof abnormalRate !== 'number' || isNaN(abnormalRate)) abnormalRate = 0
if (!systemStatus) systemStatus = '未知'
stats.value = [
{
title: '總監測量',
value: totalCount.toLocaleString(),
change: `+${Math.round(totalCount * 0.125)}`,
icon: Activity,
color: 'text-blue-600'
},
{
title: '異常率',
value: `${abnormalRate.toFixed(2)}%`,
change: abnormalRate > 0.04 ? `+${(abnormalRate - 0.04).toFixed(2)}%` : `-${(0.04 - abnormalRate).toFixed(2)}%`,
icon: AlertTriangle,
color: abnormalRate > 5 ? 'text-red-600' : 'text-amber-600'
},
{
title: '系統狀態',
value: systemStatus,
change: '99.9% Uptime',
icon: ShieldCheck,
color: 'text-emerald-600'
},
]
console.log("✅ 成功更新 Stats:", stats.value)
}
} catch (e) {
console.error("❌ Stats 同步失敗:", e)
// Fallback to old method if getStats fails
try {
const result = await gradioService.updateDataframes()
const df = result.dataframe1 || []
if (df && df.length > 0) {
const totalCount = df.length
const abnormalCount = df.filter((item: any) =>
(item.status || item.result || '').includes('異常') ||
(item.status || item.result || '').includes('錯誤') ||
(item.status || item.result || '').includes('受限')
).length
const abnormalRate = totalCount > 0 ? (abnormalCount / totalCount * 100) : 0
// Determine system status based on abnormal rate
let systemStatus = '穩定'
let statusChange = '99.9% Uptime'
let statusColor = 'text-emerald-600'
if (abnormalRate > 10) {
systemStatus = '高風險'
statusChange = '需要關注'
statusColor = 'text-red-600'
} else if (abnormalRate > 5) {
systemStatus = '中等風險'
statusChange = '監控中'
statusColor = 'text-amber-600'
}
stats.value = [
{
title: '總監測量',
value: totalCount.toLocaleString(), // 只顯示總筆數
change: `+${Math.round(totalCount * 0.125)}`,
icon: Activity,
color: 'text-blue-600'
},
{
title: '異常率',
value: `${abnormalRate.toFixed(2)}%`,
change: abnormalRate > 0.04 ? `+${(abnormalRate - 0.04).toFixed(2)}%` : `-${(0.04 - abnormalRate).toFixed(2)}%`,
icon: AlertTriangle,
color: abnormalRate > 5 ? 'text-red-600' : 'text-amber-600'
},
{
title: '系統狀態',
value: systemStatus,
change: statusChange,
icon: ShieldCheck,
color: statusColor
},
]
console.log("✅ 成功更新 Stats (fallback):", stats.value)
}
} catch (fallbackError) {
console.error("❌ Stats fallback 也失敗:", fallbackError)
}
}
}
// Real-time log update timer
let logUpdateTimer: NodeJS.Timeout | null = null
const startRealTimeLogUpdates = () => {
if (logUpdateTimer) return
logUpdateTimer = setInterval(async () => {
await syncLogData()
await syncStats() // Also sync stats in real-time
await syncAnomalies() // Also sync anomalies in real-time
}, 5000) // Update every 5 seconds
}
const stopRealTimeLogUpdates = () => {
if (logUpdateTimer) {
clearInterval(logUpdateTimer)
logUpdateTimer = null
}
}
// --- 使用者行為密度分析 (API 圖片) ---
const plotKeys = [
'教務系統: 期末網路教學評量',
'教務系統: 期末網路預選系統',
'教務系統: 開學後加退選系統',
'教務系統: 北科i學園PLUS',
'教務系統: 學生證掛失及補發系統',
'教務系統: 課程系統',
'教務系統: 學業成績查詢系統',
'學務系統: 學生請假系統',
'學務系統: 學生宿舍登錄(抽籤)系統',
'學務系統: 獎助學金申請系統',
'資訊服務: 網路郵局 WebMail',
'資訊服務: 北科軟體雲',
'惡意測試: 嘗試偷改成績 (SQL Injection)'
]
const selectedPlotKey = ref(plotKeys[0])
const plotImg = ref<string | null>(null)
async function fetchPlotData() {
plotImg.value = null
if (!selectedPlotKey.value) return
try {
const result = await gradioService.getPlotData(selectedPlotKey.value)
// 假設API回傳為Base64圖片字串
if (result && typeof result === 'string' && result.startsWith('data:image')) {
plotImg.value = result
} else if (result && typeof result === 'string') {
// 若API只回傳Base64不含data:image頭
plotImg.value = 'data:image/png;base64,' + result
} else {
plotImg.value = null
}
} catch (e) {
plotImg.value = null
console.error("getPlotData failed:", e)
}
}
// --- 登入邏輯 ---
const handleLogin = async () => {
loginError.value = ''
isLoggingIn.value = true
try {
const timeMode = simulator.value.timeMode === 'real' ? "真實時間 (目前時間)" : "自訂時間 (模擬過去/未來)"
const result = await gradioService.processLogin(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
loginForm.value.password,
loginForm.value.captcha,
timeMode,
simulator.value.customTime
)
// Check for server response/error message
if (result.abnormalLogText && result.abnormalLogText.trim() !== '') {
loginError.value = result.abnormalLogText
} else {
userRole.value = loginForm.value.account === 'admin' ? 'admin' : 'student'
await syncLogData()
await syncStats() // Sync stats after login
await syncAnomalies() // Sync anomalies after login
startRealTimeLogUpdates() // Start real-time updates after login
}
} catch (err: any) {
console.error("Login API Error:", err)
loginError.value = "登入發生異常,請確認參數是否正確"
} finally {
isLoggingIn.value = false
}
}
// ---登出邏輯 ---
const handleLogout = async () => {
isLoggingOut.value = true
try {
const timeMode = simulator.value.timeMode === 'real' ? "真實時間 (目前時間)" : "自訂時間 (模擬過去/未來)"
await gradioService.logout(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
} catch (err) {
console.error("Logout API Error:", err)
} finally {
stopRealTimeLogUpdates() // Stop real-time updates on logout
userRole.value = null
loginForm.value = { account: '', password: '', captcha: '' }
isLoggingOut.value = false
}
}
// --- 學生行為連動 ---
const studentAction = async (name: string) => {
const endpoint = ACTION_ENDPOINTS[name]
if (!endpoint) return
try {
const timeMode = simulator.value.timeMode === 'real' ? "真實時間 (目前時間)" : "自訂時間 (模擬過去/未來)"
let result: string
// Call the appropriate lambda function based on the endpoint
switch (endpoint) {
case 'lambda':
result = await gradioService.lambda(
simulator.value.ip,
simulator.value.sessionId,
timeMode,
simulator.value.customTime
)
break
case 'lambda1':
result = await gradioService.lambda1(
simulator.value.ip,
simulator.value.sessionId,
timeMode,
simulator.value.customTime
)
break
case 'lambda7':
result = await gradioService.lambda7(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
break
case 'lambda8':
result = await gradioService.lambda8(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
break
case 'lambda10':
result = await gradioService.lambda10(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
break
case 'lambda11':
result = await gradioService.lambda11(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
break
case 'lambda12':
result = await gradioService.lambda12(
simulator.value.ip,
simulator.value.sessionId,
loginForm.value.account,
timeMode,
simulator.value.customTime
)
break
default:
console.warn(`Unknown endpoint: ${endpoint}`)
return
}
// Check for abnormal behavior alerts
if (result && result.includes('⚠️異常')) {
alert(`【AI 防護警報】偵測到異常行為:${result}`)
}
await syncLogData()
} catch (err) {
console.error("行為紀錄失敗:", err)
}
}
// Student Dashboard State
const openAccordions = ref({
academic: true,
affairs: false,
services: false
})
const toggleAccordion = (key: 'academic' | 'affairs' | 'services') => {
openAccordions.value[key] = !openAccordions.value[key]
}
// Sidebar Menu
const menuItems = [
{ name: '防護中心', icon: ShieldCheck },
{ name: '事件分析', icon: Activity },
{ name: '紅隊演練', icon: ShieldAlert },
{ name: '基準管理', icon: FileText },
]
// Stats Cards
const stats = ref([
{ title: '總監測量', value: '1,284,592', change: '+12.5%', icon: Activity, color: 'text-blue-600' },
{ title: '異常率', value: '0.04%', change: '-2.1%', icon: AlertTriangle, color: 'text-amber-600' },
{ title: '系統狀態', value: '穩定', change: '99.9% Uptime', icon: ShieldCheck, color: 'text-emerald-600' },
])
// All Events Data (Mock)
const allEvents = ref([
{ id: 1, time: '2026-03-17 13:02:15', location: '台北, 台灣', ip: '114.32.1.45', score: 85, status: 'High', desc: '暴力破解嘗試', rawLog: '{"event_id": "auth_fail_001", "src_ip": "114.32.1.45", "method": "POST", "path": "/api/login", "attempts": 42, "user_agent": "python-requests/2.25.1"}', llmExplain: '' },
{ id: 2, time: '2026-03-17 13:01:42', location: '東京, 日本', ip: '203.104.1.12', score: 12, status: 'Low', desc: '正常 API 調用', rawLog: '{"event_id": "api_002", "src_ip": "203.104.1.12", "method": "GET", "path": "/v1/products", "status": 200}', llmExplain: '' },
{ id: 3, time: '2026-03-17 12:58:10', location: '舊金山, 美國', ip: '192.168.1.1', score: 45, status: 'Medium', desc: '內網端口掃描', rawLog: '{"event_id": "scan_003", "src_ip": "192.168.1.1", "method": "TCP_SYN", "ports": [80, 443, 8080, 22]}', llmExplain: '' },
{ id: 4, time: '2026-03-17 12:55:33', location: '倫敦, 英國', ip: '82.1.4.156', score: 92, status: 'Critical', desc: 'SQL 注入攻擊', rawLog: '{"event_id": "sqli_004", "src_ip": "82.1.4.156", "method": "GET", "path": "/search?q=\' OR 1=1--", "payload": "UNION SELECT password FROM users"}', llmExplain: '' },
{ id: 5, time: '2026-03-17 12:52:01', location: '新加坡', ip: '111.65.1.22', score: 8, status: 'Low', desc: '靜態資源存取', rawLog: '{"event_id": "static_005", "src_ip": "111.65.1.22", "method": "GET", "path": "/assets/logo.png"}', llmExplain: '' },
{ id: 6, time: '2026-03-17 12:45:12', location: '首爾, 韓國', ip: '211.234.1.99', score: 78, status: 'High', desc: '敏感資料下載', rawLog: '{"event_id": "data_exfil_006", "src_ip": "211.234.1.99", "method": "GET", "path": "/admin/export/users.csv", "size": "500MB"}', llmExplain: '' },
])
// Sync anomalies from backend logs
const syncAnomalies = async () => {
try {
const result = await gradioService.getAbnormalLogs() // Get all abnormal logs
// Backend returns: { status, count, data: [logs] }
if (result && result.data && Array.isArray(result.data)) {
// Transform logs to event format
allEvents.value = result.data.map((log: any, index: number) => ({
id: index + 1,
time: log.timestamp || new Date().toISOString(),
location: '未知', // Backend doesn't provide location
ip: log.ip_address || '未知',
score: 85, // All abnormal logs are high risk
status: 'High',
desc: `${log.action} - ${log.status}`,
rawLog: JSON.stringify(log),
llmExplain: ''
}))
console.log("✅ 成功更新 Anomalies:", allEvents.value.length)
}
} catch (e) {
console.error("❌ Anomalies 同步失敗:", e)
}
}
// Filtered anomalies for Event Analysis (Score >= 50)
const anomalies = computed(() => allEvents.value.filter(e => e.score >= 50))
// Truncate description to 10 chars
const truncateDesc = (desc: string) => {
return desc.length > 10 ? desc.substring(0, 10) + '...' : desc
}
// Red Team Exercise State
const simulationStatus = ref('idle') // idle, running, completed
const manualParams = ref({
country: '全球',
timeRange: 60,
intensity: 50
})
const startSimulation = () => {
simulationStatus.value = 'running'
setTimeout(() => {
simulationStatus.value = 'completed'
}, 3000)
}
// Baseline Management State
const baselineWeights = ref({
time: 80,
ip: 65,
device: 45,
behavior: 70,
geo: 55
})
const protectionStrength = ref(75)
const radarCanvas = ref<HTMLCanvasElement | null>(null)
let radarChart: Chart | null = null
const initRadarChart = () => {
if (!radarCanvas.value) return
const ctx = radarCanvas.value.getContext('2d')
if (!ctx) return
if (radarChart) {
radarChart.destroy()
}
radarChart = new Chart(ctx, {
type: 'radar',
data: {
labels: ['時間特徵', 'IP 聲譽', '裝置指紋', '行為頻率', '地理偏移'],
datasets: [{
label: '當前權重',
data: [
baselineWeights.value.time,
baselineWeights.value.ip,
baselineWeights.value.device,
baselineWeights.value.behavior,
baselineWeights.value.geo
],
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: '#3b82f6',
pointBackgroundColor: '#3b82f6',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#3b82f6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 1500,
easing: 'easeOutQuart'
},
scales: {
r: {
angleLines: { display: true, color: '#f1f5f9' },
grid: { color: '#f1f5f9' },
suggestedMin: 0,
suggestedMax: 100,
ticks: { display: false }
}
},
plugins: {
legend: { display: false }
}
}
})
}
// Drawer State
const selectedEvent = ref<any>(null)
const isDrawerOpen = ref(false)
const isGenerating = ref(false)
const generateExplanation = async (event: any) => {
if (event.llmExplain) return
isGenerating.value = true
try {
const explanation = await gradioService.explainAbnormalLog(event.rawLog)
event.llmExplain = explanation || '後端 AI 未回傳分析結果。'
} catch (error) {
console.error('AI generation error:', error)
event.llmExplain = 'AI 解析服務暫時無法使用。'
} finally {
isGenerating.value = false
}
}
const openDrawer = async (event: any) => {
selectedEvent.value = event
isDrawerOpen.value = true
await generateExplanation(event)
}
const closeDrawer = () => {
isDrawerOpen.value = false
}
const chartCanvas = ref<HTMLCanvasElement | null>(null)
let behaviorChart: Chart | null = null
const initChart = () => {
if (!chartCanvas.value) return
const ctx = chartCanvas.value.getContext('2d')
if (!ctx) return
if (behaviorChart) {
behaviorChart.destroy()
}
const generateCluster = (centerX: number, centerY: number, count: number, spread: number, label: string, color: string) => {
return Array.from({ length: count }, () => ({
x: centerX + (Math.random() - 0.5) * spread,
y: centerY + (Math.random() - 0.5) * spread,
label,
color
}))
}
const cluster1 = generateCluster(20, 30, 40, 15, 'Cluster A (Normal)', '#10b981')
const cluster2 = generateCluster(70, 60, 35, 20, 'Cluster B (Admin)', '#3b82f6')
const cluster3 = generateCluster(40, 80, 25, 10, 'Cluster C (API)', '#8b5cf6')
const noise = generateCluster(50, 50, 20, 100, 'Noise (Outliers)', '#94a3b8')
const allData = [...cluster1, ...cluster2, ...cluster3, ...noise]
behaviorChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: '行為聚類',
data: allData.map(d => ({ x: d.x, y: d.y })),
backgroundColor: allData.map(d => d.color),
pointRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 2000,
easing: 'easeInOutQuart'
},
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: '#f1f5f9' } },
y: { grid: { color: '#f1f5f9' } }
}
}
})
}
// --- 修正後的初始同步 ---
onMounted(async () => {
if (activeTab.value === '防護中心') initChart()
if (activeTab.value === '基準管理') initRadarChart()
const saved = localStorage.getItem('guard_settings')
if (saved) {
const { weights, strength } = JSON.parse(saved)
baselineWeights.value = weights
protectionStrength.value = strength
}
// 改用專門的更新接口,避免 index 錯亂
await syncLogData()
fetchPlotData()
await syncAnomalies() // Sync anomalies on mount
})
watch(activeTab, async (newTab) => {
await nextTick()
if (newTab === '防護中心') {
initChart()
await syncStats() // Sync stats when switching to protection center
} else if (newTab === '基準管理') {
initRadarChart()
}
})
watch([baselineWeights, protectionStrength], async () => {
localStorage.setItem('guard_settings', JSON.stringify({
weights: baselineWeights.value,
strength: protectionStrength.value
}))
if (activeTab.value !== '基準管理') {
return
}
// Update system weights via API
const updateResult = await gradioService.updateSystemWeights(
protectionStrength.value, // ow - overall weight
baselineWeights.value.time, // tw - time weight
baselineWeights.value.ip, // iw - IP weight
baselineWeights.value.device, // dw - device weight
baselineWeights.value.geo, // gw - geo weight
baselineWeights.value.behavior // fw - frequency weight
)
if (updateResult !== null) {
console.log("✅ System weights updated successfully")
} else {
console.warn("⚠️ System weights backend endpoint unavailable; local settings saved only.")
}
initRadarChart()
}, { deep: true })
</script>
<template>
<div v-if="!userRole" class="flex h-screen bg-slate-100 font-sans text-slate-900 overflow-hidden">
<div class="w-[40%] bg-white border-r border-slate-200 p-10 flex flex-col gap-8 overflow-y-auto">
<div class="flex items-center gap-3 mb-2">
<div class="bg-blue-600 p-2 rounded-lg">
<Terminal class="w-6 h-6 text-white" />
</div>
<h2 class="text-xl font-bold tracking-tight">環境變數模擬器</h2>
</div>
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-200 space-y-6 shadow-sm">
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">模擬來源 IP</label>
<select v-model="simulator.ip" class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all">
<option v-for="ip in ips" :key="ip" :value="ip">{{ ip }}</option>
</select>
</div>
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">Cookie Session ID</label>
<input
type="text"
v-model="simulator.sessionId"
class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono text-sm"
placeholder="sess_..."
/>
</div>
<div class="space-y-4 pt-2">
<label class="block text-sm font-bold text-slate-700">時間設定</label>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="simulator.timeMode" value="real" class="w-4 h-4 text-blue-600 focus:ring-blue-500">
<span class="text-sm font-medium group-hover:text-blue-600 transition-colors">真實時間</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="simulator.timeMode" value="custom" class="w-4 h-4 text-blue-600 focus:ring-blue-500">
<span class="text-sm font-medium group-hover:text-blue-600 transition-colors">自訂時間</span>
</label>
</div>
<input
v-if="simulator.timeMode === 'custom'"
type="text"
v-model="simulator.customTime"
class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono text-sm"
/>
</div>
</div>
<div class="flex-1 flex flex-col min-h-0">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider">即時 Log 監控區</h3>
<span class="text-[10px] bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded font-bold animate-pulse">LIVE</span>
</div>
<div class="flex-1 bg-slate-900 rounded-2xl border border-slate-800 overflow-hidden flex flex-col shadow-xl">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-slate-800 bg-slate-800/50">
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Timestamp</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">IP</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Cookie ID</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Account</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">AI Result</th>
</tr>
</thead>
<tbody class="font-mono text-[11px] divide-y divide-slate-800">
<tr v-for="(log, idx) in simLogs" :key="idx" class="hover:bg-slate-800/30 transition-colors">
<td class="p-3 text-slate-300 truncate max-w-[100px]">{{ log.timestamp }}</td>
<td class="p-3 text-blue-400">{{ log.ip }}</td>
<td class="p-3 text-slate-500 truncate max-w-[80px]">{{ log.cookieId }}</td>
<td class="p-3 text-slate-300">{{ log.account }}</td>
<td class="p-3">
<span
:class="log.result === 'success' ? 'text-emerald-500' : 'text-red-500'"
class="font-bold"
>
{{ log.result }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="w-[60%] bg-slate-100 flex items-center justify-center p-10">
<div class="w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden border border-slate-200">
<div class="bg-blue-600 p-8 text-white text-center relative overflow-hidden">
<div class="relative z-10">
<h2 class="text-2xl font-bold mb-1">校園入口網站</h2>
<p class="text-blue-100 text-sm font-medium tracking-wide">Taipei Tech Portal</p>
</div>
<div class="absolute -right-10 -bottom-10 opacity-10">
<ShieldCheck class="w-48 h-48" />
</div>
</div>
<div class="p-10 space-y-8">
<div v-if="loginError" class="bg-red-50 text-red-600 p-4 rounded-xl text-sm font-bold border border-red-100 flex items-center gap-2 animate-shake">
<AlertTriangle class="w-4 h-4" />
{{ loginError }}
</div>
<div class="space-y-6">
<div class="space-y-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider ml-1">帳號 Account</label>
<div class="relative">
<User class="w-5 h-5 text-slate-300 absolute left-4 top-1/2 -translate-y-1/2" />
<input
type="text"
v-model="loginForm.account"
class="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="請輸入帳號"
/>
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider ml-1">密碼 Password</label>
<div class="relative">
<Lock class="w-5 h-5 text-slate-300 absolute left-4 top-1/2 -translate-y-1/2" />
<input
type="password"
v-model="loginForm.password"
class="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="請輸入密碼"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider ml-1">驗證碼 Captcha</label>
<input
type="text"
v-model="loginForm.captcha"
class="w-full px-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
placeholder="驗證碼"
/>
</div>
<div class="flex items-end">
<div class="w-full h-[58px] bg-slate-100 rounded-2xl border border-slate-200 flex items-center justify-center gap-2 cursor-pointer hover:bg-slate-200 transition-colors group">
<div class="font-mono font-bold text-xl tracking-widest text-slate-600 group-hover:scale-110 transition-transform select-none italic">
<span class="text-blue-600">F</span>
<span class="text-red-500">E</span>
<span class="text-emerald-600">I</span>
<span class="text-amber-500">K</span>
</div>
<Clock class="w-4 h-4 text-slate-400" />
</div>
</div>
</div>
</div>
<button
@click="handleLogin"
:disabled="isLoggingIn"
class="w-full bg-orange-500 hover:bg-orange-600 disabled:bg-slate-300 text-white font-bold py-5 rounded-2xl transition-all shadow-xl shadow-orange-100 flex items-center justify-center gap-3 group active:scale-[0.98]"
>
<Loader2 v-if="isLoggingIn" class="w-5 h-5 animate-spin" />
<span>{{ isLoggingIn ? '登入中...' : '登入 Login' }}</span>
<ChevronRight v-if="!isLoggingIn" class="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</button>
<div class="flex justify-between text-xs font-bold text-slate-400 px-2">
<a href="#" class="hover:text-blue-600 transition-colors">忘記密碼?</a>
<a href="#" class="hover:text-blue-600 transition-colors">帳號申請</a>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="userRole === 'admin'" class="flex h-screen bg-slate-50 font-sans text-slate-900 overflow-hidden">
<aside class="w-64 bg-white border-r border-slate-200 flex flex-col shrink-0">
<div class="p-6 border-b border-slate-100 flex items-center gap-3">
<div class="bg-blue-600 p-2 rounded-lg">
<ShieldCheck class="w-6 h-6 text-white" />
</div>
<h1 class="font-bold text-xl tracking-tight text-slate-800">數位保鑣</h1>
</div>
<nav class="flex-1 p-4 space-y-2">
<button
v-for="item in menuItems"
:key="item.name"
@click="activeTab = item.name"
class="w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 text-left"
:class="activeTab === item.name ? 'bg-blue-50 text-blue-700 font-semibold shadow-sm' : 'text-slate-500 hover:bg-slate-50 hover:text-slate-800'"
>
<component :is="item.icon" class="w-5 h-5" />
<span>{{ item.name }}</span>
</button>
</nav>
<div class="p-4 border-t border-slate-100">
<div class="bg-slate-50 rounded-2xl p-4 flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center">
<User class="w-5 h-5 text-slate-500" />
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold truncate">管理員 0103</p>
<p class="text-xs text-slate-500 truncate">系統核心權限</p>
</div>
<LogOut
@click="handleLogout"
:class="isLoggingOut ? 'animate-spin' : ''"
class="w-5 h-5 text-slate-400 cursor-pointer hover:text-red-600 transition-colors"
title="登出"
/>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col overflow-hidden relative">
<header class="h-16 bg-white border-b border-slate-200 px-8 flex items-center justify-between shrink-0 z-10">
<div class="flex items-center gap-4">
<h2 class="text-lg font-semibold text-slate-800">{{ activeTab }}概覽</h2>
<span class="px-2 py-1 bg-emerald-100 text-emerald-700 text-xs font-bold rounded-md flex items-center gap-1">
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
即時監控中
</span>
</div>
<div class="flex items-center gap-6">
<div class="relative">
<Search class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="搜尋事件..."
class="pl-10 pr-4 py-2 bg-slate-100 border-none rounded-full text-sm w-64 focus:ring-2 focus:ring-blue-500 outline-none transition-all"
/>
</div>
<button class="relative p-2 text-slate-500 hover:bg-slate-100 rounded-full transition-colors">
<Bell class="w-5 h-5" />
<span class="absolute top-2 right-2 w-2 h-2 bg-red-500 border-2 border-white rounded-full"></span>
</button>
</div>
</header>
<div class="flex-1 overflow-y-auto p-8 space-y-8">
<div v-if="activeTab === '防護中心'" class="space-y-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div v-for="stat in stats" :key="stat.title"
class="relative bg-white p-8 rounded-2xl border border-slate-200 shadow-sm flex flex-col justify-between min-h-[180px] overflow-hidden">
<div :class="[
'absolute left-0 top-0 h-full w-2 rounded-l-2xl',
stat.title.includes('總監測量') ? 'bg-emerald-400' :
stat.title.includes('異常率') ? 'bg-red-400' :
stat.title.includes('系統狀態') ? 'bg-blue-400' : 'bg-slate-200']"></div>
<div class="flex items-center gap-4 mb-4">
<div class="flex-shrink-0 w-14 h-14 rounded-xl flex items-center justify-center bg-slate-50 border border-slate-100">
<component :is="stat.icon" :class="[
'w-10 h-10',
stat.title.includes('總監測量') ? 'text-blue-500' :
stat.title.includes('異常率') ? 'text-red-500' :
stat.title.includes('系統狀態') ? 'text-emerald-500' : 'text-slate-400']" />
</div>
<div>
<p class="text-slate-700 font-bold text-lg leading-tight">
<span v-if="stat.title.includes('總監測量')">總監測量 <span class="text-xs text-slate-500">(本地歷史資料庫)</span></span>
<span v-else-if="stat.title.includes('異常率')">異常率 <span class="text-xs text-slate-500">(平均數值)</span></span>
<span v-else>{{ stat.title }}</span>
</p>
<p v-if="stat.title.includes('總監測量')" class="text-slate-500 text-xs mt-1">自系統啟動起統計 (+今日即時)</p>
<p v-else-if="stat.title.includes('異常率')" class="text-slate-500 text-xs mt-1">共 {{ (stat as any).abnormalCount || 1 }} 筆監測到異常行為</p>
<p v-else-if="stat.title.includes('系統狀態')" class="text-slate-500 text-xs mt-1">HDBSCANAgent 服務運作正常。</p>
</div>
</div>
<div class="flex items-end justify-between flex-1">
<div>
<span v-if="stat.title.includes('總監測量')" class="text-4xl font-extrabold text-slate-900">{{ stat.value }}</span>
<span v-else-if="stat.title.includes('異常率')" class="text-4xl font-extrabold" :class="stat.value === '0.00%' ? 'text-red-500' : 'text-amber-500'">{{ stat.value }}</span>
<span v-else-if="stat.title.includes('系統狀態')" class="text-4xl font-extrabold text-emerald-500">{{ stat.value }}</span>
<span v-if="stat.title.includes('總監測量')" class="ml-2 text-lg text-slate-500 font-bold">筆 Log</span>
<span v-else-if="stat.title.includes('異常率')" class="ml-2 text-lg text-slate-500 font-bold"></span>
</div>
<span v-if="stat.title.includes('異常率')" class="ml-2 px-2 py-1 rounded bg-red-100 text-red-500 text-sm font-bold">↑即時</span>
<span v-else-if="stat.title.includes('系統狀態')" class="ml-2 px-2 py-1 rounded bg-emerald-100 text-emerald-600 text-sm font-bold">● 正常執行</span>
</div>
</div>
</div>
<div class="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm">
<h3 class="text-lg font-bold text-slate-800 mb-8">使用者行為密度分析 (HDBSCAN)</h3>
<div class="mb-4">
<select v-model="selectedPlotKey" @change="fetchPlotData" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all">
<option v-for="key in plotKeys" :key="key" :value="key">{{ key }}</option>
</select>
</div>
<div class="h-[400px] w-full flex items-center justify-center bg-slate-100 rounded-xl border border-slate-200">
<img v-if="plotImg" :src="plotImg" alt="行為密度分析圖" class="max-h-[380px] max-w-full object-contain" />
<span v-else class="text-slate-400">請選擇資料類型以顯示圖表</span>
</div>
</div>
</div>
<template v-else-if="activeTab === '事件分析'">
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-6 border-b border-slate-100">
<h3 class="text-lg font-bold text-slate-800">所有異常事件</h3>
</div>
<div class="divide-y divide-slate-100">
<div
v-for="event in anomalies"
:key="event.id"
@click="openDrawer(event)"
class="flex items-center justify-between p-6 hover:bg-slate-50 transition-colors cursor-pointer group"
>
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center"
:class="event.score > 80 ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'"
>
<AlertTriangle class="w-6 h-6" />
</div>
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-slate-800">{{ event.ip }}</span>
<span
class="px-2 py-0.5 rounded text-[10px] font-bold uppercase"
:class="event.score > 80 ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'"
>
{{ event.status }}
</span>
</div>
<p class="text-sm text-slate-500 mt-0.5">{{ truncateDesc(event.desc) }}</p>
</div>
</div>
<div class="flex items-center gap-6">
<div class="text-right">
<p class="text-xs text-slate-400">{{ event.time }}</p>
<p class="text-sm font-semibold text-slate-700">{{ event.location }}</p>
</div>
<ChevronRight class="w-5 h-5 text-slate-300 group-hover:text-slate-500 transition-colors" />
</div>
</div>
</div>
</div>
</template>
<template v-else-if="activeTab === '紅隊演練'">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-8">
<div class="bg-slate-900 rounded-2xl p-6 border border-slate-800 shadow-xl overflow-hidden">
<div class="flex items-center justify-between mb-4 border-b border-slate-800 pb-4">
<div class="flex items-center gap-2 text-emerald-500">
<Terminal class="w-5 h-5" />
<span class="font-mono font-bold">ATTACK_SIMULATOR_V2.0</span>
</div>
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-amber-500"></div>
<div class="w-3 h-3 rounded-full bg-emerald-500"></div>
</div>
</div>
<div class="font-mono text-sm space-y-2 h-64 overflow-y-auto text-slate-300">
<template v-if="simulationStatus === 'idle'">
<p class="text-slate-500 italic">等待指令中...</p>
</template>
<template v-else-if="simulationStatus === 'running'">
<p class="text-emerald-500">root@digital-bodyguard:~$ ./simulate_attack.sh --target=internal_db</p>
<p class="animate-pulse">[INFO] Starting reconnaissance on 10.0.4.12...</p>
<p class="animate-pulse">[INFO] Port 3306 (MySQL) detected as OPEN.</p>
<p class="text-amber-500">[WARN] Weak password policy detected on service account 'readonly_user'.</p>
<p>[INFO] Attempting brute-force attack (Dictionary: common_passwords.txt)...</p>
</template>
<template v-else-if="simulationStatus === 'completed'">
<p class="text-emerald-500">root@digital-bodyguard:~$ ./simulate_attack.sh --target=internal_db</p>
<p>[INFO] Starting reconnaissance on 10.0.4.12...</p>
<p>[INFO] Port 3306 (MySQL) detected as OPEN.</p>
<p class="text-amber-500">[WARN] Weak password policy detected on service account 'readonly_user'.</p>
<p>[INFO] Attempting brute-force attack (Dictionary: common_passwords.txt)...</p>
<p class="text-red-500 font-bold">[CRITICAL] Access granted! Session ID: 0xAF42E9</p>
<p class="text-emerald-500">root@digital-bodyguard:~$ _</p>
</template>
</div>
</div>
<div class="bg-white rounded-2xl border border-slate-200 p-8 shadow-sm">
<h3 class="text-lg font-bold text-slate-800 mb-6">手動參數區</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">攻擊來源國家</label>
<select v-model="manualParams.country" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all">
<option>全球</option>
<option>台灣</option>
<option>美國</option>
<option>中國</option>
<option>俄羅斯</option>
<option>北韓</option>
</select>
</div>
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">演練持續時間 ({{ manualParams.timeRange }} 分鐘)</label>
<input type="range" v-model="manualParams.timeRange" min="10" max="180" class="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="flex justify-between text-xs text-slate-400">
<span>10m</span>
<span>180m</span>
</div>
</div>
<div class="space-y-4 md:col-span-2">
<label class="block text-sm font-bold text-slate-700">攻擊強度 ({{ manualParams.intensity }}%)</label>
<input type="range" v-model="manualParams.intensity" min="1" max="100" class="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-red-600">
<div class="flex justify-between text-xs text-slate-400">
<span>低</span>
<span>高</span>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-8">
<div class="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<h3 class="text-lg font-bold text-slate-800 mb-4">演練劇本</h3>
<div class="space-y-4">
<div class="p-4 bg-slate-50 rounded-xl border border-slate-100">
<div class="flex items-center gap-3 mb-2">
<Target class="w-5 h-5 text-red-600" />
<span class="font-bold text-sm">核心資料庫滲透</span>
</div>
<p class="text-xs text-slate-500">模擬外部攻擊者試圖獲取 PII 資料。</p>
</div>
<div class="p-4 bg-slate-50 rounded-xl border border-slate-100">
<div class="flex items-center gap-3 mb-2">
<Lock class="w-5 h-5 text-blue-600" />
<span class="font-bold text-sm">MFA 繞過測試</span>
</div>
<p class="text-xs text-slate-500">測試身分驗證服務的 Session 固定攻擊。</p>
</div>
</div>
<button
@click="startSimulation"
:disabled="simulationStatus === 'running'"
class="w-full mt-6 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-blue-100 flex items-center justify-center gap-2"
>
<Loader2 v-if="simulationStatus === 'running'" class="w-5 h-5 animate-spin" />
<Zap v-else class="w-5 h-5" />
{{ simulationStatus === 'running' ? '演練進行中...' : '一鍵模擬劇本' }}
</button>
</div>
</div>
</div>
</template>
<template v-else-if="activeTab === '基準管理'">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-white rounded-2xl border border-slate-200 p-8 shadow-sm">
<h3 class="text-lg font-bold text-slate-800 mb-8">特徵權重分析</h3>
<div class="h-[400px] w-full">
<canvas ref="radarCanvas"></canvas>
</div>
</div>
<div class="space-y-8">
<div class="bg-white rounded-2xl border border-slate-200 p-8 shadow-sm">
<h3 class="text-lg font-bold text-slate-800 mb-6">防護強度微調</h3>
<div class="space-y-8">
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-sm font-bold text-slate-700">總體防護強度</label>
<span class="text-blue-600 font-bold">{{ protectionStrength }}%</span>
</div>
<input type="range" v-model="protectionStrength" min="0" max="100" class="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600">
<p class="text-xs text-slate-400">提高強度會增加誤報率,但能更有效攔截新型攻擊。</p>
</div>
<div class="border-t border-slate-100 pt-6 space-y-6">
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-slate-500">時間特徵權重</label>
<span class="text-xs font-bold">{{ baselineWeights.time }}%</span>
</div>
<input type="range" v-model="baselineWeights.time" min="0" max="100" class="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-slate-400">
</div>
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-slate-500">IP 聲譽權重</label>
<span class="text-xs font-bold">{{ baselineWeights.ip }}%</span>
</div>
<input type="range" v-model="baselineWeights.ip" min="0" max="100" class="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-slate-400">
</div>
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-slate-500">裝置指紋權重</label>
<span class="text-xs font-bold">{{ baselineWeights.device }}%</span>
</div>
<input type="range" v-model="baselineWeights.device" min="0" max="100" class="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-slate-400">
</div>
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-slate-500">行為頻率權重</label>
<span class="text-xs font-bold">{{ baselineWeights.behavior }}%</span>
</div>
<input type="range" v-model="baselineWeights.behavior" min="0" max="100" class="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-slate-400">
</div>
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-slate-500">地理偏移權重</label>
<span class="text-xs font-bold">{{ baselineWeights.geo }}%</span>
</div>
<input type="range" v-model="baselineWeights.geo" min="0" max="100" class="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-slate-400">
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<div
v-if="isDrawerOpen"
class="absolute inset-0 bg-slate-900/20 backdrop-blur-sm z-20 transition-opacity duration-300"
@click="closeDrawer"
></div>
<div
class="absolute top-0 right-0 h-full w-[500px] bg-white shadow-2xl z-30 transform transition-transform duration-300 ease-out flex flex-col"
:class="isDrawerOpen ? 'translate-x-0' : 'translate-x-full'"
>
<div class="p-6 border-b border-slate-100 flex items-center justify-between shrink-0">
<div class="flex items-center gap-3">
<div class="p-2 bg-slate-100 rounded-lg">
<Activity class="w-5 h-5 text-slate-600" />
</div>
<h3 class="font-bold text-lg">事件分析詳情</h3>
</div>
<button @click="closeDrawer" class="p-2 hover:bg-slate-100 rounded-full transition-colors">
<X class="w-5 h-5 text-slate-400" />
</button>
</div>
<div v-if="selectedEvent" class="flex-1 overflow-y-auto p-8 space-y-8">
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400 uppercase tracking-wider">風險評級</span>
<span
class="px-3 py-1 rounded-full text-xs font-bold"
:class="selectedEvent.score > 80 ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'"
>
{{ selectedEvent.status }} ({{ selectedEvent.score }}/100)
</span>
</div>
<h4 class="text-xl font-bold text-slate-800">{{ selectedEvent.desc }}</h4>
<div class="grid grid-cols-2 gap-4">
<div class="bg-slate-50 p-4 rounded-xl">
<p class="text-xs text-slate-500 mb-1">來源 IP</p>
<p class="font-mono font-semibold">{{ selectedEvent.ip }}</p>
</div>
<div class="bg-slate-50 p-4 rounded-xl">
<p class="text-xs text-slate-500 mb-1">地理位置</p>
<p class="font-semibold">{{ selectedEvent.location }}</p>
</div>
</div>
</div>
<div class="bg-blue-50/50 border border-blue-100 rounded-2xl p-6 space-y-3 relative overflow-hidden">
<div class="flex items-center gap-2 text-blue-700">
<BrainCircuit class="w-5 h-5" />
<span class="font-bold">AI 智慧解析</span>
</div>
<div v-if="isGenerating" class="flex items-center gap-3 py-4 text-blue-600">
<Loader2 class="w-5 h-5 animate-spin" />
<span class="text-sm font-medium animate-pulse">正在分析 Log 數據並生成解釋...</span>
</div>
<p v-else class="text-slate-700 leading-relaxed text-sm">
{{ selectedEvent.llmExplain || '點擊以生成 AI 解析。' }}
</p>
<div class="absolute -right-4 -bottom-4 opacity-5">
<BrainCircuit class="w-24 h-24 text-blue-900" />
</div>
</div>
<div class="space-y-3">
<div class="flex items-center gap-2 text-slate-500">
<Terminal class="w-5 h-5" />
<span class="font-bold text-sm">原始 Log 數據</span>
</div>
<div class="bg-slate-900 rounded-xl p-4 overflow-x-auto">
<pre class="text-emerald-400 font-mono text-xs leading-relaxed">{{ JSON.stringify(JSON.parse(selectedEvent.rawLog), null, 2) }}</pre>
</div>
</div>
</div>
<div class="p-6 border-t border-slate-100 bg-slate-50 flex gap-4 shrink-0">
<button @click="closeDrawer" class="flex-1 flex items-center justify-center gap-2 bg-red-600 hover:bg-red-700 text-white font-bold py-3 rounded-xl transition-colors shadow-lg shadow-red-200">
<ShieldX class="w-5 h-5" />
立即阻斷
</button>
<button @click="closeDrawer" class="flex-1 flex items-center justify-center gap-2 bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 font-bold py-3 rounded-xl transition-colors">
<CheckCircle2 class="w-5 h-5 text-emerald-500" />
標記為信任
</button>
</div>
</div>
</main>
</div>
<div v-else-if="userRole === 'student'" class="flex h-screen bg-slate-100 font-sans text-slate-900 overflow-hidden">
<div class="w-[40%] bg-white border-r border-slate-200 p-10 flex flex-col gap-8 overflow-y-auto">
<div class="flex items-center gap-3 mb-2">
<div class="bg-blue-600 p-2 rounded-lg">
<Terminal class="w-6 h-6 text-white" />
</div>
<h2 class="text-xl font-bold tracking-tight">環境變數模擬器</h2>
</div>
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-200 space-y-6 shadow-sm">
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">模擬來源 IP</label>
<select v-model="simulator.ip" class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all">
<option v-for="ip in ips" :key="ip" :value="ip">{{ ip }}</option>
</select>
</div>
<div class="space-y-4">
<label class="block text-sm font-bold text-slate-700">Cookie Session ID</label>
<input
type="text"
v-model="simulator.sessionId"
class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono text-sm"
placeholder="sess_..."
/>
</div>
<div class="space-y-4 pt-2">
<label class="block text-sm font-bold text-slate-700">時間設定</label>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="simulator.timeMode" value="real" class="w-4 h-4 text-blue-600 focus:ring-blue-500">
<span class="text-sm font-medium group-hover:text-blue-600 transition-colors">真實時間</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="simulator.timeMode" value="custom" class="w-4 h-4 text-blue-600 focus:ring-blue-500">
<span class="text-sm font-medium group-hover:text-blue-600 transition-colors">自訂時間</span>
</label>
</div>
<input
v-if="simulator.timeMode === 'custom'"
type="text"
v-model="simulator.customTime"
class="w-full bg-white border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono text-sm"
/>
</div>
</div>
<div class="flex-1 flex flex-col min-h-0">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider">即時 Log 監控區</h3>
<span class="text-[10px] bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded font-bold animate-pulse">LIVE</span>
</div>
<div class="flex-1 bg-slate-900 rounded-2xl border border-slate-800 overflow-hidden flex flex-col shadow-xl">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-slate-800 bg-slate-800/50">
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Timestamp</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">IP</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Cookie ID</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">Account</th>
<th class="p-3 text-[10px] font-bold text-slate-400 uppercase">AI Result</th>
</tr>
</thead>
<tbody class="font-mono text-[11px] divide-y divide-slate-800">
<tr v-for="(log, idx) in simLogs" :key="idx" class="hover:bg-slate-800/30 transition-colors">
<td class="p-3 text-slate-300 truncate max-w-[100px]">{{ log.timestamp }}</td>
<td class="p-3 text-blue-400">{{ log.ip }}</td>
<td class="p-3 text-slate-500 truncate max-w-[80px]">{{ log.cookieId }}</td>
<td class="p-3 text-slate-300">{{ log.account }}</td>
<td class="p-3">
<span
:class="log.result === '正常' ? 'text-emerald-500' : 'text-red-500'"
class="font-bold"
>
{{ log.result }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="w-[60%] flex flex-col overflow-hidden">
<header class="h-20 bg-white border-b border-slate-200 px-10 flex items-center justify-between shrink-0 shadow-sm z-10">
<div class="flex items-center gap-6">
<div class="flex items-center gap-3">
<div class="bg-blue-600 p-2 rounded-lg">
<GraduationCap class="w-6 h-6 text-white" />
</div>
<div>
<h2 class="text-lg font-bold text-slate-800">學生資訊系統</h2>
<p class="text-[10px] text-slate-400 font-bold tracking-widest uppercase">Student Information System</p>
</div>
</div>
<div class="h-8 w-px bg-slate-200 mx-2"></div>
<div class="flex items-center gap-2 text-slate-600">
<User class="w-5 h-5" />
<span class="font-bold text-sm">王小明 同學 歡迎您</span>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2 text-slate-500">
<Users class="w-4 h-4" />
<span class="text-xs font-bold">線上人數: 1,284</span>
</div>
<button
@click="handleLogout"
:disabled="isLoggingOut"
class="flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-red-50 hover:text-red-600 disabled:bg-slate-200 text-slate-600 rounded-xl font-bold text-sm transition-all"
>
<LogOut class="w-4 h-4" />
{{ isLoggingOut ? '登出中...' : '登出 Logout' }}
</button>
</div>
</header>
<div class="flex-1 overflow-y-auto p-10 space-y-8">
<div class="space-y-4">
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<button
@click="toggleAccordion('academic')"
class="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
>
<div class="flex items-center gap-3">
<BookOpen class="w-5 h-5 text-blue-600" />
<span class="font-bold text-slate-800">教務系統 Academic Affairs</span>
</div>
<component :is="openAccordions.academic ? ChevronUp : ChevronDown" class="w-5 h-5 text-slate-400" />
</button>
<div v-show="openAccordions.academic" class="p-6 bg-slate-50/50 border-t border-slate-100 grid grid-cols-2 gap-4">
<button @click="studentAction('期末教學評量')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-blue-500 hover:shadow-md transition-all group">
<div class="p-2 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
<FileText class="w-5 h-5 text-blue-600" />
</div>
<span class="font-bold text-sm text-slate-700">期末教學評量</span>
</button>
<button @click="studentAction('預選系統')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-blue-500 hover:shadow-md transition-all group">
<div class="p-2 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
<Clock class="w-5 h-5 text-blue-600" />
</div>
<span class="font-bold text-sm text-slate-700">預選系統</span>
</button>
</div>
</div>
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<button
@click="toggleAccordion('affairs')"
class="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
>
<div class="flex items-center gap-3">
<Users class="w-5 h-5 text-emerald-600" />
<span class="font-bold text-slate-800">學務系統 Student Affairs</span>
</div>
<component :is="openAccordions.affairs ? ChevronUp : ChevronDown" class="w-5 h-5 text-slate-400" />
</button>
<div v-show="openAccordions.affairs" class="p-6 bg-slate-50/50 border-t border-slate-100 grid grid-cols-2 gap-4">
<button @click="studentAction('請假系統')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-emerald-500 hover:shadow-md transition-all group">
<div class="p-2 bg-emerald-50 rounded-lg group-hover:bg-emerald-100 transition-colors">
<Activity class="w-5 h-5 text-emerald-600" />
</div>
<span class="font-bold text-sm text-slate-700">請假系統</span>
</button>
<button @click="studentAction('宿舍登錄')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-emerald-500 hover:shadow-md transition-all group">
<div class="p-2 bg-emerald-50 rounded-lg group-hover:bg-emerald-100 transition-colors">
<Target class="w-5 h-5 text-emerald-600" />
</div>
<span class="font-bold text-sm text-slate-700">宿舍登錄</span>
</button>
</div>
</div>
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<button
@click="toggleAccordion('services')"
class="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
>
<div class="flex items-center gap-3">
<Info class="w-5 h-5 text-amber-600" />
<span class="font-bold text-slate-800">資訊服務 Information Services</span>
</div>
<component :is="openAccordions.services ? ChevronUp : ChevronDown" class="w-5 h-5 text-slate-400" />
</button>
<div v-show="openAccordions.services" class="p-6 bg-slate-50/50 border-t border-slate-100 grid grid-cols-2 gap-4">
<button @click="studentAction('網路郵局')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-amber-500 hover:shadow-md transition-all group">
<div class="p-2 bg-amber-50 rounded-lg group-hover:bg-amber-100 transition-colors">
<Mail class="w-5 h-5 text-amber-600" />
</div>
<span class="font-bold text-sm text-slate-700">網路郵局</span>
</button>
<button @click="studentAction('軟體雲')" class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl hover:border-amber-500 hover:shadow-md transition-all group">
<div class="p-2 bg-amber-50 rounded-lg group-hover:bg-amber-100 transition-colors">
<Cloud class="w-5 h-5 text-amber-600" />
</div>
<span class="font-bold text-sm text-slate-700">軟體雲</span>
</button>
</div>
</div>
</div>
<div class="bg-red-50 rounded-2xl border border-red-100 p-8 space-y-6 shadow-sm">
<div class="flex items-center gap-3 text-red-700">
<ShieldAlert class="w-6 h-6" />
<h3 class="text-lg font-bold">惡意操作測試區 (Security Test)</h3>
</div>
<p class="text-sm text-red-600/80 leading-relaxed">
此區域僅供資安演練使用。點擊下方按鈕將模擬 SQL Injection 攻擊嘗試,系統將即時偵測並記錄此異常行為。
</p>
<button
@click="studentAction('嘗試偷改成績 (SQL Injection)')"
class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-red-100 flex items-center justify-center gap-3 group active:scale-[0.98]"
>
<Zap class="w-5 h-5 group-hover:animate-pulse" />
嘗試偷改成績 (SQL Injection)
</button>
</div>
</div>
</div>
</div>
</template>
<style>
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
.translate-x-full {
transform: translateX(100%);
}
.translate-x-0 {
transform: translateX(0);
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.2s ease-in-out 0s 2;
}
</style>