petstore-admin/src/views/CustomersView.vue

155 lines
5.7 KiB
Vue

<template>
<div class="page-card">
<h2 class="page-title">服务客户</h2>
<div class="summary-bar" v-if="summary">
客户数 {{ summary.customerCount }} · 待回访 {{ summary.pendingLeadCount }} · 本月到店 {{ summary.monthVisitCount }}
</div>
<div class="filter-row">
<el-select v-model="source" clearable placeholder="来源" style="width: 140px">
<el-option label="预约" value="appointment" />
<el-option label="报告留资" value="lead" />
</el-select>
<el-checkbox v-model="hasPendingLead">有待回访</el-checkbox>
<el-input v-model="q" clearable placeholder="姓名 / 手机 / 宠物名" style="width: 220px" />
<el-button type="primary" @click="load">查询</el-button>
</div>
<el-table :data="rows" v-loading="loading" row-key="storeCustomerId" stripe>
<el-table-column prop="displayName" label="客户" width="120" />
<el-table-column prop="phoneMasked" label="手机" width="140" />
<el-table-column label="宠物" min-width="160">
<template #default="{ row }">
<el-tag v-for="p in row.pets || []" :key="p.name" size="small" class="pet-tag">{{ p.name }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastVisitAt" label="最近到店" min-width="160" />
<el-table-column prop="leadStatus" label="留资状态" width="100" />
<el-table-column label="来源" width="140">
<template #default="{ row }">{{ formatSource(row.source) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button v-if="row.lastReportId" link @click="goReport(row)">报告</el-button>
<el-button v-if="row.hasPendingLead" link @click="goLead">回访</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer v-model="drawer" title="客户详情" size="420px">
<template v-if="current">
<el-descriptions :column="1" border>
<el-descriptions-item label="姓名">{{ current.displayName }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ current.phoneMasked }}</el-descriptions-item>
<el-descriptions-item label="首次来源">{{ formatOriginSource(current.originSource) }}</el-descriptions-item>
<el-descriptions-item label="首次接触">{{ current.firstContactAt || '—' }}</el-descriptions-item>
<el-descriptions-item label="最近接触">{{ current.lastContactAt || '—' }}</el-descriptions-item>
<el-descriptions-item label="最近到店">{{ current.lastVisitAt || '—' }}</el-descriptions-item>
<el-descriptions-item label="留资">{{ current.leadStatus || '—' }}</el-descriptions-item>
<el-descriptions-item label="宠物">
<el-tag v-for="p in current.pets || []" :key="p.name" size="small" class="pet-tag">{{ p.name }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="drawer-actions">
<el-button v-if="current.lastReportId" type="primary" @click="goReport(current)">最近报告</el-button>
<el-button v-if="current.hasPendingLead" @click="goLead">去回访</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getServiceCustomers } from '@/api'
type CustomerRow = {
storeCustomerId: number
userId?: number | null
displayName?: string
phoneMasked?: string
pets?: Array<{ name: string }>
lastVisitAt?: string
lastReportId?: number | null
leadStatus?: string
hasPendingLead?: boolean
source?: string
originSource?: string
firstContactAt?: string
lastContactAt?: string
}
const router = useRouter()
const loading = ref(false)
const rows = ref<CustomerRow[]>([])
const summary = ref<{ customerCount: number; pendingLeadCount: number; monthVisitCount: number } | null>(null)
const source = ref('')
const hasPendingLead = ref(false)
const q = ref('')
const drawer = ref(false)
const current = ref<CustomerRow | null>(null)
async function load() {
loading.value = true
try {
const res = await getServiceCustomers({
source: source.value || undefined,
hasPendingLead: hasPendingLead.value || undefined,
q: q.value || undefined,
page: 1,
pageSize: 50,
})
const data = res.data as {
list?: CustomerRow[]
summary?: { customerCount: number; pendingLeadCount: number; monthVisitCount: number }
} | undefined
rows.value = data?.list || []
summary.value = data?.summary || null
} finally {
loading.value = false
}
}
function openDetail(row: CustomerRow) {
current.value = row
drawer.value = true
}
function goReport(row: CustomerRow) {
if (!row.lastReportId) return
router.push({ path: '/reports', query: { q: String(row.lastReportId) } })
}
function goLead() {
router.push({ path: '/leads', query: { due: 'today' } })
}
function formatSource(value?: string) {
if (value === 'appointment+lead') return '预约 + 报告留资'
if (value === 'appointment') return '预约'
if (value === 'lead') return '报告留资'
return '—'
}
function formatOriginSource(value?: string) {
if (value === 'customer_booking') return '宠主自助预约'
if (value === 'assisted_booking') return '门店代客预约'
if (value === 'report_lead') return '报告留资'
if (value === 'appointment') return '历史预约'
return '—'
}
onMounted(load)
</script>
<style scoped>
.pet-tag {
margin-right: 4px;
}
.drawer-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
</style>