| <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' |
|
|
|
|
| |
| const ACTION_ENDPOINTS: Record<string, string> = { |
| '期末教學評量': 'lambda', |
| '預選系統': 'lambda1', |
| '請假系統': 'lambda7', |
| '宿舍登錄': 'lambda8', |
| '網路郵局': 'lambda10', |
| '軟體雲': 'lambda11', |
| '嘗試偷改成績 (SQL Injection)': 'lambda12' |
| } |
|
|
| Chart.register(...registerables) |
|
|
| |
| const activeTab = ref('防護中心') |
| const userRole = ref<string | null>(null) |
|
|
| |
| const isLoggingIn = ref(false) |
| const isLoggingOut = ref(false) |
| const isUpdatingWeights = ref(false) |
|
|
| |
| const loginForm = ref({ |
| account: '', |
| password: '', |
| captcha: '' |
| }) |
| const loginError = ref('') |
|
|
| |
| 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() |
|
|
| |
| const df = result.dataframe1 || [] |
|
|
| if (!Array.isArray(df)) { |
| console.warn("⚠️ 同步到的資料不是陣列,嘗試直接解析", df) |
| } |
|
|
| |
| if (df && df.length > 0) { |
| |
| 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) |
| } |
| } |
|
|
| |
| const syncStats = async () => { |
| try { |
| const result = await gradioService.getStats() |
| |
| if (result && result.data) { |
| const { total_logs, abnormal_logs, anomaly_rate_percent } = result.data |
| |
| |
| 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) |
| |
| 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 |
| |
| |
| 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) |
| } |
| } |
| } |
|
|
| |
| 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) |
| } |
|
|
| const stopRealTimeLogUpdates = () => { |
| if (logUpdateTimer) { |
| clearInterval(logUpdateTimer) |
| logUpdateTimer = null |
| } |
| } |
|
|
| |
| 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) |
| |
| if (result && typeof result === 'string' && result.startsWith('data:image')) { |
| plotImg.value = result |
| } else if (result && typeof result === 'string') { |
| |
| 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 |
| ) |
|
|
| |
| if (result.abnormalLogText && result.abnormalLogText.trim() !== '') { |
| loginError.value = result.abnormalLogText |
| } else { |
| userRole.value = loginForm.value.account === 'admin' ? 'admin' : 'student' |
| await syncLogData() |
| await syncStats() |
| await syncAnomalies() |
| startRealTimeLogUpdates() |
| } |
| } 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() |
| 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 |
|
|
| |
| 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 |
| } |
|
|
| |
| if (result && result.includes('⚠️異常')) { |
| alert(`【AI 防護警報】偵測到異常行為:${result}`) |
| } |
| await syncLogData() |
| } catch (err) { |
| console.error("行為紀錄失敗:", err) |
| } |
| } |
|
|
| |
| const openAccordions = ref({ |
| academic: true, |
| affairs: false, |
| services: false |
| }) |
|
|
| const toggleAccordion = (key: 'academic' | 'affairs' | 'services') => { |
| openAccordions.value[key] = !openAccordions.value[key] |
| } |
|
|
|
|
| |
| const menuItems = [ |
| { name: '防護中心', icon: ShieldCheck }, |
| { name: '事件分析', icon: Activity }, |
| { name: '紅隊演練', icon: ShieldAlert }, |
| { name: '基準管理', icon: FileText }, |
| ] |
|
|
| |
| 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' }, |
| ]) |
|
|
| |
| 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: '' }, |
| ]) |
|
|
| |
| const syncAnomalies = async () => { |
| try { |
| const result = await gradioService.getAbnormalLogs() |
| |
| if (result && result.data && Array.isArray(result.data)) { |
| |
| allEvents.value = result.data.map((log: any, index: number) => ({ |
| id: index + 1, |
| time: log.timestamp || new Date().toISOString(), |
| location: '未知', |
| ip: log.ip_address || '未知', |
| score: 85, |
| status: 'High', |
| desc: `${log.action} - ${log.status}`, |
| rawLog: JSON.stringify(log), |
| llmExplain: '' |
| })) |
| console.log("✅ 成功更新 Anomalies:", allEvents.value.length) |
| } |
| } catch (e) { |
| console.error("❌ Anomalies 同步失敗:", e) |
| } |
| } |
|
|
| |
| const anomalies = computed(() => allEvents.value.filter(e => e.score >= 50)) |
|
|
| |
| const truncateDesc = (desc: string) => { |
| return desc.length > 10 ? desc.substring(0, 10) + '...' : desc |
| } |
|
|
| |
| const simulationStatus = ref('idle') |
| const manualParams = ref({ |
| country: '全球', |
| timeRange: 60, |
| intensity: 50 |
| }) |
|
|
| const startSimulation = () => { |
| simulationStatus.value = 'running' |
| setTimeout(() => { |
| simulationStatus.value = 'completed' |
| }, 3000) |
| } |
|
|
| |
| 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 } |
| } |
| } |
| }) |
| } |
|
|
| |
| 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 |
| } |
| |
| await syncLogData() |
| fetchPlotData() |
| await syncAnomalies() |
| }) |
|
|
| watch(activeTab, async (newTab) => { |
| await nextTick() |
| if (newTab === '防護中心') { |
| initChart() |
| await syncStats() |
| } else if (newTab === '基準管理') { |
| initRadarChart() |
| } |
| }) |
|
|
| watch([baselineWeights, protectionStrength], async () => { |
| localStorage.setItem('guard_settings', JSON.stringify({ |
| weights: baselineWeights.value, |
| strength: protectionStrength.value |
| })) |
|
|
| if (activeTab.value !== '基準管理') { |
| return |
| } |
|
|
| |
| 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">HDBSCAN 與 Agent 服務運作正常。</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: |
| border-radius: 10px; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: |
| } |
|
|
| .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> |