Spaces:
Sleeping
Sleeping
| <script setup lang="ts"> | |
| import { ref, onMounted } from 'vue' | |
| import { Shield, Clock, Globe, Laptop, Activity, RefreshCcw, AlertCircle } from 'lucide-vue-next' | |
| const props = defineProps<{ | |
| sessionId: string | |
| }>() | |
| interface UserLog { | |
| time: string | |
| action: string | |
| ip: string | |
| location: string | |
| device: string | |
| ai_status: string | |
| status_detail?: string | |
| } | |
| const logs = ref<UserLog[]>([]) | |
| const loading = ref(false) | |
| const error = ref('') | |
| const fetchLogs = async () => { | |
| if (!props.sessionId) { | |
| error.value = '未偵測到 Session ID,無法讀取日誌。' | |
| return | |
| } | |
| loading.value = true | |
| error.value = '' | |
| try { | |
| // 使用目前 host 的 7860 端口(開發環境慣例)或指向後端 Space 網域 | |
| // 🌐 【HF 部署模式】指向 Hugging Face Space 後端 | |
| // 💻 【本地開發模式】指向本地後端 | |
| const baseUrl = window.location.port === '5173' ? 'http://localhost:7860' : 'https://my-project-team-infosecai-finalproj.hf.space' | |
| const response = await fetch(`${baseUrl}/api/get_user_logs?session_id=${props.sessionId}`) | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`) | |
| } | |
| const data = await response.json() | |
| if (data.status === 'success') { | |
| logs.value = data.data | |
| } else { | |
| error.value = data.message || '後端回傳資料格式錯誤' | |
| } | |
| } catch (err) { | |
| console.error('Fetch logs error:', err) | |
| error.value = '無法連接至 AI 後端安全引擎,請確認伺服器是否正常運作。' | |
| } finally { | |
| loading.value = false | |
| } | |
| } | |
| onMounted(() => { | |
| fetchLogs() | |
| }) | |
| </script> | |
| <template> | |
| <div class="user-log-view-root space-y-6"> | |
| <!-- Header Card --> | |
| <div class="bg-slate-900 rounded-3xl border border-slate-800 p-8 shadow-2xl relative overflow-hidden group"> | |
| <div class="absolute top-0 right-0 p-12 opacity-5 pointer-events-none group-hover:scale-110 transition-transform duration-700"> | |
| <Shield class="w-48 h-48 text-blue-500" /> | |
| </div> | |
| <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10"> | |
| <div class="flex items-center gap-5"> | |
| <div class="bg-gradient-to-br from-blue-600 to-indigo-700 p-4 rounded-2xl shadow-xl shadow-blue-900/40"> | |
| <Shield class="w-8 h-8 text-white" /> | |
| </div> | |
| <div> | |
| <h2 class="text-2xl font-black text-white tracking-tight">數位保鑣|個人安全日誌</h2> | |
| <div class="flex items-center gap-2 mt-1"> | |
| <span class="text-[10px] bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded font-bold tracking-widest uppercase border border-blue-500/30">Live Monitor</span> | |
| <p class="text-xs text-slate-500 font-medium italic">目前監控 Session: {{ props.sessionId }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| @click="fetchLogs" | |
| :disabled="loading" | |
| class="flex items-center justify-center gap-3 px-6 py-3 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-2xl text-sm font-bold transition-all border border-slate-700 active:scale-95 disabled:opacity-50 hover:shadow-lg hover:shadow-blue-900/10" | |
| > | |
| <RefreshCcw class="w-4 h-4" :class="{ 'animate-spin': loading }" /> | |
| <span>同步最新紀錄</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Error State --> | |
| <div v-if="error" class="bg-red-500/10 border border-red-500/30 text-red-400 p-6 rounded-3xl flex items-start gap-4 animate-in fade-in slide-in-from-top-4 duration-300"> | |
| <AlertCircle class="w-6 h-6 shrink-0 mt-1" /> | |
| <div> | |
| <h4 class="font-bold text-lg">系統連線異常</h4> | |
| <p class="text-sm opacity-80 leading-relaxed">{{ error }}</p> | |
| </div> | |
| </div> | |
| <!-- Data Table Card --> | |
| <div class="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden"> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-left border-collapse"> | |
| <thead> | |
| <tr class="bg-slate-50/80 border-b border-slate-100"> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest"> | |
| <div class="flex items-center gap-2"><Clock class="w-4 h-4 text-slate-400" /> 紀錄時間</div> | |
| </th> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest"> | |
| <div class="flex items-center gap-2"><Activity class="w-4 h-4 text-slate-400" /> 行為名稱</div> | |
| </th> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest"> | |
| <div class="flex items-center gap-2"><Globe class="w-4 h-4 text-slate-400" /> 來源 IP</div> | |
| </th> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest"> | |
| 地理位置 | |
| </th> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest"> | |
| <div class="flex items-center gap-2"><Laptop class="w-4 h-4 text-slate-400" /> 存取設備</div> | |
| </th> | |
| <th class="p-5 text-xs font-bold text-slate-500 uppercase tracking-widest text-center"> | |
| AI 防護判定 | |
| </th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-50"> | |
| <tr v-if="loading" class="animate-pulse"> | |
| <td colspan="6" class="p-20 text-center text-slate-400 font-medium"> | |
| 正在從雲端 Dataset 同步資料... | |
| </td> | |
| </tr> | |
| <tr v-else-if="logs.length === 0" class="fade-in"> | |
| <td colspan="6" class="p-20 text-center"> | |
| <div class="flex flex-col items-center gap-3"> | |
| <div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center"> | |
| <Activity class="w-8 h-8 text-slate-300" /> | |
| </div> | |
| <p class="text-slate-400 font-medium">目前尚無相關行為日誌紀錄</p> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr v-for="(log, idx) in logs" :key="idx" class="hover:bg-blue-50/30 transition-all group duration-300"> | |
| <td class="p-5 text-sm font-mono text-slate-500">{{ log.time }}</td> | |
| <td class="p-5"> | |
| <span class="text-sm font-bold text-slate-800 group-hover:text-blue-600 transition-colors">{{ log.action }}</span> | |
| </td> | |
| <td class="p-5"> | |
| <span class="text-sm font-mono text-blue-600 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100">{{ log.ip }}</span> | |
| </td> | |
| <td class="p-5"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-sm text-slate-600">{{ log.location }}</span> | |
| </div> | |
| </td> | |
| <td class="p-5 text-sm text-slate-500 italic">{{ log.device }}</td> | |
| <td class="p-5"> | |
| <div class="flex justify-center"> | |
| <span | |
| :class="log.ai_status === '正常' | |
| ? 'bg-emerald-50 text-emerald-600 border-emerald-200' | |
| : 'bg-red-50 text-red-600 border-red-200 animate-pulse font-black'" | |
| class="px-4 py-1.5 rounded-full text-[11px] font-bold border shadow-sm flex items-center gap-1.5" | |
| > | |
| <div class="w-1.5 h-1.5 rounded-full" :class="log.ai_status === '正常' ? 'bg-emerald-500' : 'bg-red-500'"></div> | |
| {{ log.ai_status }} | |
| </span> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <style scoped> | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* 讓表格在窄螢幕也能看 */ | |
| .overflow-x-auto { | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| </style> | |