214 lines
8.4 KiB
Vue
214 lines
8.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
|
|
interface LeadRecord {
|
|
id: string
|
|
name: string
|
|
phone: string
|
|
email: string
|
|
campaign: string
|
|
page: string
|
|
channel: string
|
|
status: '새리드' | '검토중' | '완료'
|
|
createdAt: string
|
|
}
|
|
|
|
const leads = ref<LeadRecord[]>([
|
|
{
|
|
id: 'ld-1001',
|
|
name: '김하늘',
|
|
phone: '010-1111-2222',
|
|
email: 'haneul@example.com',
|
|
campaign: 'Q3 마케팅 캠페인',
|
|
page: '메인 랜딩',
|
|
channel: 'Google',
|
|
status: '새리드',
|
|
createdAt: '2026-03-04T09:12:00.000Z'
|
|
},
|
|
{
|
|
id: 'ld-1002',
|
|
name: '이준수',
|
|
phone: '010-2222-3333',
|
|
email: 'junsoo@example.com',
|
|
campaign: '블랙프라이데이 2023',
|
|
page: '구글 전용 랜딩',
|
|
channel: 'Meta',
|
|
status: '검토중',
|
|
createdAt: '2026-03-03T17:44:00.000Z'
|
|
},
|
|
{
|
|
id: 'ld-1003',
|
|
name: '박소영',
|
|
phone: '010-3333-4444',
|
|
email: 'soyoung@example.com',
|
|
campaign: 'Product Hunt 런칭',
|
|
page: '리타겟 랜딩',
|
|
channel: 'Instagram',
|
|
status: '완료',
|
|
createdAt: '2026-03-02T21:13:00.000Z'
|
|
},
|
|
{
|
|
id: 'ld-1004',
|
|
name: '최민재',
|
|
phone: '010-4444-5555',
|
|
email: 'minjae@example.com',
|
|
campaign: 'SaaS 웨비나 시리즈',
|
|
page: '오퍼 페이지 B',
|
|
channel: 'Naver',
|
|
status: '새리드',
|
|
createdAt: '2026-03-01T11:08:00.000Z'
|
|
},
|
|
{
|
|
id: 'ld-1005',
|
|
name: '정연우',
|
|
phone: '010-5555-6666',
|
|
email: 'yeonwoo@example.com',
|
|
campaign: 'Q3 마케팅 캠페인',
|
|
page: '메인 랜딩',
|
|
channel: 'Google',
|
|
status: '검토중',
|
|
createdAt: '2026-03-01T03:03:00.000Z'
|
|
}
|
|
])
|
|
|
|
const searchQuery = ref('')
|
|
const statusFilter = ref<'all' | LeadRecord['status']>('all')
|
|
const channelFilter = ref<'all' | string>('all')
|
|
const campaignFilter = ref<'all' | string>('all')
|
|
|
|
const channelOptions = ['Google', 'Meta', 'Instagram', 'Naver']
|
|
const campaigns = computed(() => {
|
|
const names = [...new Set(leads.value.map((lead) => lead.campaign))]
|
|
return names
|
|
})
|
|
|
|
const filteredLeads = computed(() => {
|
|
const query = searchQuery.value.trim().toLowerCase()
|
|
|
|
return leads.value
|
|
.filter((lead) => {
|
|
const target = `${lead.name} ${lead.phone} ${lead.email} ${lead.campaign} ${lead.page} ${lead.channel} ${lead.status}`.toLowerCase()
|
|
const matchesQuery = query === '' || target.includes(query)
|
|
const matchesStatus = statusFilter.value === 'all' || lead.status === statusFilter.value
|
|
const matchesChannel = channelFilter.value === 'all' || lead.channel === channelFilter.value
|
|
const matchesCampaign = campaignFilter.value === 'all' || lead.campaign === campaignFilter.value
|
|
|
|
return matchesQuery && matchesStatus && matchesChannel && matchesCampaign
|
|
})
|
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
})
|
|
|
|
const statusLabel = (status: LeadRecord['status']) => {
|
|
if (status === '완료') return '완료'
|
|
if (status === '검토중') return '검토중'
|
|
return '새 리드'
|
|
}
|
|
|
|
const statusClass = (status: LeadRecord['status']) => {
|
|
if (status === '완료') {
|
|
return 'text-emerald-200 bg-emerald-400/10'
|
|
}
|
|
|
|
if (status === '검토중') {
|
|
return 'text-amber-200 bg-amber-400/10'
|
|
}
|
|
|
|
return 'text-indigo-200 bg-indigo-400/10'
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-5">
|
|
<header class="rounded-xl border border-slate-700/80 bg-gradient-to-r from-slate-900 via-slate-900 to-indigo-950/70 px-5 py-4 shadow-[0_16px_40px_rgba(0,0,0,0.35)]">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<p class="text-sm text-slate-400">홈 <span class="px-2">></span> 리드 조회</p>
|
|
<h1 class="mt-1 text-3xl font-black tracking-tight text-slate-100">리드 조회</h1>
|
|
<p class="mt-1 text-sm text-slate-400">캠페인/페이지 기반으로 리드를 확인하고 상태를 관리하세요.</p>
|
|
</div>
|
|
<button class="rounded-xl bg-gradient-to-r from-indigo-500 to-cyan-500 px-5 py-2.5 text-base font-semibold text-white shadow-lg shadow-cyan-950/40 transition hover:from-indigo-400 hover:to-cyan-400">
|
|
리드 엑셀 내보내기
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="rounded-2xl border border-indigo-400/20 bg-gradient-to-br from-slate-900/80 via-slate-950 to-slate-900/90 p-5 shadow-[0_12px_30px_rgba(0,0,0,0.3)]">
|
|
<div class="flex flex-nowrap items-end justify-between gap-4">
|
|
<label class="min-w-0 space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">검색</span>
|
|
<div class="relative">
|
|
<span class="pointer-events-none absolute inset-y-0 left-3 flex items-center text-cyan-300">⌕</span>
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="이름, 전화번호, 이메일, 캠페인, 페이지"
|
|
class="w-full min-w-[420px] rounded-xl border border-indigo-500/20 bg-slate-950/80 px-9 py-3 text-sm text-slate-100 outline-none ring-0 transition focus:border-cyan-400 focus:shadow-[0_0_0_1px_rgba(103,232,249,0.35)]"
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<div class="ml-auto flex shrink-0 items-end gap-3">
|
|
<label class="flex flex-col space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">상태</span>
|
|
<select v-model="statusFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
|
|
<option value="all">전체</option>
|
|
<option value="새리드">새 리드</option>
|
|
<option value="검토중">검토중</option>
|
|
<option value="완료">완료</option>
|
|
</select>
|
|
</label>
|
|
<label class="flex flex-col space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">채널</span>
|
|
<select v-model="channelFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
|
|
<option value="all">전체</option>
|
|
<option v-for="ch in channelOptions" :key="ch" :value="ch">
|
|
{{ ch }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label class="flex flex-col space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">캠페인</span>
|
|
<select v-model="campaignFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
|
|
<option value="all">전체 캠페인</option>
|
|
<option v-for="name in campaigns" :key="name" :value="name">{{ name }}</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950/80">
|
|
<table class="w-full text-left text-sm">
|
|
<thead class="bg-slate-900 text-slate-300">
|
|
<tr>
|
|
<th class="px-4 py-3">이름</th>
|
|
<th class="px-4 py-3">연락처</th>
|
|
<th class="px-4 py-3">이메일</th>
|
|
<th class="px-4 py-3">캠페인</th>
|
|
<th class="px-4 py-3">페이지</th>
|
|
<th class="px-4 py-3">채널</th>
|
|
<th class="px-4 py-3">상태</th>
|
|
<th class="px-4 py-3">수집일</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="lead in filteredLeads" :key="lead.id" class="border-t border-slate-800 text-slate-200">
|
|
<td class="px-4 py-3">{{ lead.name }}</td>
|
|
<td class="px-4 py-3">{{ lead.phone }}</td>
|
|
<td class="px-4 py-3 text-cyan-200">{{ lead.email }}</td>
|
|
<td class="px-4 py-3">{{ lead.campaign }}</td>
|
|
<td class="px-4 py-3">{{ lead.page }}</td>
|
|
<td class="px-4 py-3">{{ lead.channel }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold tracking-[0.07em]" :class="statusClass(lead.status)">
|
|
{{ statusLabel(lead.status) }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3">{{ new Date(lead.createdAt).toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</div>
|
|
</template>
|