Files
InsightReply/web/src/views/Radar.vue
zs 8cf6cb944b
Some checks failed
Extension Build & Release / build (push) Failing after 1m5s
Backend Deploy (Go + Docker) / deploy (push) Failing after 1m40s
Web Console Deploy (Vue 3 + Vite) / deploy (push) Has been cancelled
feat: 部署初版测试
2026-03-02 21:25:21 +08:00

191 lines
7.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Monitor {
id: string
brand_name: string
x_handle: string
is_active: boolean
created_at: string
}
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'
const monitors = ref<Monitor[]>([])
const loading = ref(true)
const error = ref('')
const newKeyword = ref('')
const newHandle = ref('')
const adding = ref(false)
const loadMonitors = async () => {
const token = localStorage.getItem('jwt_token')
if (!token) return
loading.value = true
try {
const res = await fetch(`${API_BASE}/monitors`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (!res.ok) throw new Error('Failed to fetch monitors')
monitors.value = await res.json()
} catch (err: any) {
error.value = err.message
} finally {
loading.value = false
}
}
const addMonitor = async () => {
if (!newKeyword.value && !newHandle.value) return
const token = localStorage.getItem('jwt_token')
adding.value = true
try {
const res = await fetch(`${API_BASE}/monitors`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
brand_name: newKeyword.value,
x_handle: newHandle.value.replace('@', '')
})
})
if (!res.ok) throw new Error('Failed to add target')
newKeyword.value = ''
newHandle.value = ''
await loadMonitors()
} catch (err: any) {
alert(err.message)
} finally {
adding.value = false
}
}
const toggleMonitor = async (m: Monitor) => {
const token = localStorage.getItem('jwt_token')
try {
await fetch(`${API_BASE}/monitors/${m.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ is_active: !m.is_active })
})
m.is_active = !m.is_active
} catch (err) {
console.error(err)
}
}
const deleteMonitor = async (id: string) => {
if (!confirm('Remove this radar target?')) return
const token = localStorage.getItem('jwt_token')
try {
await fetch(`${API_BASE}/monitors/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
monitors.value = monitors.value.filter(m => m.id !== id)
} catch (err) {
console.error(err)
}
}
onMounted(() => {
loadMonitors()
})
</script>
<template>
<div class="max-w-[1400px] mx-auto space-y-8 animate-fade-in-up">
<!-- Header & Add Form -->
<header class="flex flex-col md:flex-row md:justify-between items-start md:items-end gap-6 pb-6 border-b border-white/5">
<div>
<h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
📡 X Radar Targets
<span class="text-sm font-medium px-2.5 py-1 bg-indigo-500/20 text-indigo-400 rounded-full">{{ monitors.length }} Active</span>
</h1>
<p class="text-zinc-500 mt-2 text-sm max-w-lg">
Configure Nitter scraper targets. The engine will autonomously monitor these accounts and keywords for high-potential engagement opportunities.
</p>
</div>
<form @submit.prevent="addMonitor" class="flex items-center gap-3 bg-white/5 p-2 rounded-xl border border-white/10 w-full md:w-auto">
<input
v-model="newHandle"
placeholder="X Handle (e.g. elonmusk)"
class="bg-black/20 text-white text-sm rounded-lg px-3 py-2 border border-white/10 w-40 focus:outline-none focus:border-brand-primary/50 pointer-events-auto" style="pointer-events: auto;"
/>
<span class="text-zinc-600 text-sm font-bold">+</span>
<input
v-model="newKeyword"
placeholder="Keyword Combo (Optional)"
class="bg-black/20 text-white text-sm rounded-lg px-3 py-2 border border-white/10 w-48 focus:outline-none focus:border-brand-primary/50 pointer-events-auto" style="pointer-events: auto;"
/>
<button
type="submit"
:disabled="adding || (!newKeyword && !newHandle)"
class="bg-brand-primary hover:bg-blue-500 disabled:opacity-50 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap"
>
Add Target
</button>
</form>
</header>
<div v-if="loading" class="text-zinc-500 py-12 text-center">Loading radar grid...</div>
<!-- Active Grid -->
<div v-else-if="monitors.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div
v-for="m in monitors"
:key="m.id"
class="glass-panel p-5 border border-white/10 relative group transition-all"
:class="m.is_active ? 'hover:border-indigo-500/30' : 'opacity-60 grayscale'"
>
<div class="flex justify-between items-start mb-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500/20 to-purple-600/20 border border-indigo-500/20 flex items-center justify-center font-bold text-indigo-400">
{{ m.x_handle ? m.x_handle.charAt(0).toUpperCase() : '#' }}
</div>
<div class="flex flex-col">
<span class="font-bold text-white tracking-wide text-sm truncate max-w-[120px]" :title="m.x_handle || 'Global Search'">
{{ m.x_handle ? '@' + m.x_handle : 'Global Search' }}
</span>
<span class="text-xs text-zinc-500">Target</span>
</div>
</div>
<!-- Toggle -->
<button @click="toggleMonitor(m)" class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none" :class="m.is_active ? 'bg-indigo-500' : 'bg-white/10'">
<span aria-hidden="true" class="pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" :class="m.is_active ? 'translate-x-4' : 'translate-x-0'"></span>
</button>
</div>
<div class="bg-black/20 rounded-lg p-3 border border-white/5 mb-4">
<span class="text-xs font-semibold text-zinc-500 uppercase tracking-widest block mb-1">Intersection Keyword</span>
<span class="text-sm text-zinc-300 font-mono">{{ m.brand_name || '— (All Posts)' }}</span>
</div>
<div class="flex justify-between items-center border-t border-white/5 pt-4 mt-auto">
<span class="text-[10px] text-zinc-500 font-mono">{{ new Date(m.created_at).toLocaleDateString() }}</span>
<button @click="deleteMonitor(m.id)" class="text-xs text-red-400/50 hover:text-red-400 transition-colors pointer-events-auto" style="pointer-events: auto;">Delete</button>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center py-20 bg-white/5 border border-white/10 rounded-2xl border-dashed">
<span class="text-4xl mb-4 opacity-50">🛰</span>
<h3 class="text-lg font-medium text-white mb-2">Radar is offline</h3>
<p class="text-zinc-400 text-sm max-w-sm text-center">Add a competitor's X handle or an intersection keyword to initialize the autonomous scraping engine.</p>
</div>
</div>
</template>