feat: 扩展优化重构
Some checks failed
Extension Build & Release / build (push) Failing after 56s

This commit is contained in:
zs
2026-03-03 02:19:14 +08:00
parent c686d81d30
commit 1d17ac03e0
15 changed files with 904 additions and 692 deletions

View File

@@ -51,7 +51,6 @@ const createCompetitor = async () => {
throw new Error(data.message || 'Failed to add competitor')
}
// Reset form & reload
form.value = { competitor_name: '', platform: 'twitter', target_handle: '', keywords: '' }
showForm.value = false
await fetchCompetitors()
@@ -85,108 +84,140 @@ onMounted(() => {
</script>
<template>
<div class="space-y-6">
<div class="space-y-12">
<div v-if="isLoading" class="p-10 flex justify-center">
<div class="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
<header class="flex justify-between items-end">
<div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">Competitor Radar</h2>
<p class="text-sm text-zinc-500 font-medium">Identify and engage in conversations happening around your alternatives.</p>
</div>
<button
v-if="!showForm"
@click="showForm = true"
class="px-5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-[14px] text-sm font-bold transition-apple shadow-lg shadow-blue-900/20 active:scale-95 flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
Add Target
</button>
</header>
<div v-if="isLoading" class="py-20 flex justify-center">
<div class="w-8 h-8 border-2 border-white/10 border-t-blue-500 rounded-full animate-spin"></div>
</div>
<div v-else>
<div class="flex justify-between items-center mb-6">
<p class="text-sm text-zinc-400">Track competitors tweets & mentions to find opportunistic conversations.</p>
<button
v-if="!showForm"
@click="showForm = true"
class="px-4 py-2 bg-brand-primary/20 text-brand-primary border border-brand-primary/30 rounded-lg text-xs font-semibold hover:bg-brand-primary hover:text-white transition-colors flex items-center gap-1"
>
+ Add Competitor
</button>
</div>
<!-- Create Form -->
<form v-if="showForm" @submit.prevent="createCompetitor" class="bg-[#171717] border border-white/10 rounded-xl p-6 space-y-4 mb-8">
<div class="flex justify-between items-center mb-2 border-b border-white/10 pb-2">
<h3 class="text-sm font-semibold text-white">Add Target Radar</h3>
<button type="button" @click="showForm = false" class="text-zinc-500 hover:text-white"></button>
</div>
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded-lg">
{{ errorMsg }}
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Competitor Product / Name</label>
<input v-model="form.competitor_name" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. Acme Corp" />
<div v-else class="space-y-8">
<!-- Create Form Modal View -->
<transition enter-active-class="transition duration-500 ease-out transform" enter-from-class="opacity-0 scale-95 -translate-y-4" enter-to-class="opacity-100 scale-100 translate-y-0">
<form v-if="showForm" @submit.prevent="createCompetitor" class="bg-white/[0.03] border border-white/5 rounded-[32px] p-8 space-y-8 shadow-2xl relative">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">New Radar Target</h3>
<button type="button" @click="showForm = false" class="p-2 rounded-full hover:bg-white/5 transition-apple">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Platform</label>
<select v-model="form.platform" class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50 appearance-none">
<option value="twitter">Twitter / X</option>
<option value="reddit">Reddit (Coming Soon)</option>
</select>
<div v-if="errorMsg" class="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-bold rounded-xl">
{{ errorMsg }}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Target Handle</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-zinc-500 pointer-events-none text-sm">@</span>
<input v-model="form.target_handle" class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg pl-8 pr-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="acmecorp" />
<div class="grid grid-cols-2 gap-8">
<div class="space-y-3">
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Competitor Name</label>
<input v-model="form.competitor_name" required class="dashboard-input" placeholder="e.g. Acme Corp" />
</div>
<div class="space-y-3">
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Platform</label>
<div class="relative">
<select v-model="form.platform" class="dashboard-input appearance-none cursor-pointer">
<option value="twitter">Twitter / X</option>
<option value="reddit">Reddit (Coming Soon)</option>
</select>
<div class="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-600">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</div>
</div>
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Track Keywords (Comma separated)</label>
<input v-model="form.keywords" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. acme sucks, alternative to acme" />
</div>
<div class="grid grid-cols-2 gap-8">
<div class="space-y-3">
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">X Handle <span class="text-zinc-700">(optional)</span></label>
<div class="relative">
<span class="absolute inset-y-0 left-5 flex items-center text-zinc-600 pointer-events-none text-sm font-bold">@</span>
<input v-model="form.target_handle" class="dashboard-input pl-10" placeholder="handle" />
</div>
</div>
</div>
<div class="space-y-3">
<label class="text-[10px] font-bold text-zinc-500 uppercase tracking-[0.2em] ml-1">Monitor Keywords</label>
<input v-model="form.keywords" required class="dashboard-input" placeholder="e.g. acme sucks, alternative to acme" />
</div>
</div>
<div class="flex justify-end pt-4">
<button type="submit" :disabled="isSubmitting" class="px-5 py-2.5 bg-brand-primary hover:bg-brand-primary/90 text-white rounded-lg text-sm font-medium transition-all shadow flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed">
<span v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></span>
{{ isSubmitting ? 'Saving...' : 'Start Tracking' }}
</button>
</div>
</form>
<div class="flex justify-end">
<button type="submit" :disabled="isSubmitting" class="px-8 py-3.5 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-[15px] font-bold transition-apple shadow-xl shadow-blue-900/30 flex items-center gap-3 active:scale-95 disabled:opacity-50">
<div v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
{{ isSubmitting ? 'Calibrating...' : 'Initialize Radar' }}
</button>
</div>
</form>
</transition>
<!-- List -->
<div v-if="competitors.length === 0 && !showForm" class="text-center p-12 border border-dashed border-white/10 rounded-xl text-zinc-500">
You are not tracking any competitors.
<!-- Radar List -->
<div v-if="competitors.length === 0 && !showForm" class="text-center py-20 bg-white/[0.01] border-2 border-dashed border-white/5 rounded-[40px] space-y-4">
<div class="w-16 h-16 bg-white/[0.03] rounded-3xl mx-auto flex items-center justify-center border border-white/5">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-zinc-700"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M2 12h2"/><path d="M20 12h2"/></svg>
</div>
<p class="text-zinc-500 font-bold tracking-tight">Radar is currently silent. No competitors added.</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div v-for="c in competitors" :key="c.id" class="bg-[#0A0A0A] border border-white/10 rounded-xl p-5 relative group hover:border-white/20 transition-colors">
<div class="grid grid-cols-2 gap-6">
<div v-for="c in competitors" :key="c.id" class="group bg-white/[0.02] border border-white/5 rounded-[32px] p-6 flex flex-col justify-between transition-apple hover:bg-white/[0.04] hover:border-white/10 hover:shadow-2xl relative overflow-hidden">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-brand-primary">
<svg v-if="c.platform === 'twitter'" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>
<div class="flex justify-between items-start mb-6 z-10">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-500 group-hover:scale-110 transition-apple">
<svg v-if="c.platform === 'twitter'" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M2 12h2"/><path d="M20 12h2"/></svg>
</div>
<div>
<h4 class="text-sm font-semibold text-white">{{ c.competitor_name }}</h4>
<p v-if="c.target_handle" class="text-xs text-zinc-500">@{{ c.target_handle }}</p>
<h4 class="text-[17px] font-black tracking-tight group-hover:text-blue-400 transition-apple">{{ c.competitor_name }}</h4>
<p v-if="c.target_handle" class="text-[11px] font-bold text-zinc-600 mt-0.5 tracking-tight font-mono">@{{ c.target_handle }}</p>
</div>
</div>
<span class="px-2.5 py-1 bg-green-500/10 text-green-400 text-[10px] font-bold rounded-full border border-green-500/20 uppercase tracking-wide">Active</span>
<div class="group/pulse flex items-center gap-2 px-3 py-1 bg-emerald-500/10 rounded-full border border-emerald-500/20">
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
<span class="text-[9px] font-black text-emerald-400 uppercase tracking-widest">Tracking</span>
</div>
</div>
<div class="space-y-1">
<p class="text-[10px] text-zinc-500 uppercase tracking-wider font-semibold">Keywords Targeted</p>
<div class="flex flex-wrap gap-1.5">
<span v-for="kw in c.keywords.split(',')" :key="kw" class="px-2 py-0.5 bg-zinc-800 text-zinc-300 text-[11px] rounded flex border border-zinc-700">
<div class="space-y-3 z-10">
<span class="text-[9px] font-black text-zinc-700 uppercase tracking-[0.2em] flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
Search Logic
</span>
<div class="flex flex-wrap gap-2">
<span v-for="kw in c.keywords.split(',')" :key="kw" class="px-3 py-1 bg-black/40 text-zinc-400 text-[11px] font-bold rounded-lg border border-white/5 transition-apple group-hover:border-blue-500/30 group-hover:text-zinc-200">
{{ kw.trim() }}
</span>
</div>
</div>
<button @click="deleteCompetitor(c.id)" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-red-500 hover:text-red-400 p-2 rounded-lg hover:bg-red-500/10">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
<button @click="deleteCompetitor(c.id)" class="absolute top-2 right-2 p-3 text-zinc-800 hover:text-rose-500 transition-apple opacity-0 group-hover:opacity-100 z-20 active:scale-90">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
<!-- Abstract Decor -->
<div class="absolute -right-10 -bottom-10 w-32 h-32 bg-blue-500/5 blur-3xl rounded-full group-hover:bg-blue-500/10 transition-apple"></div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dashboard-input {
@apply w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-3.5 text-sm font-semibold;
@apply transition-apple focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/40 placeholder:text-zinc-700;
}
</style>