Files
WonderQ-Project/WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue
T
duanshuwen efab6d1398 feat(admin): 新增全套运营管理功能,修复核心路由bug及后端seed问题
修复Vue Admin布局中RouterView未绑定唯一key的问题,解决RBAC系统资源页面切换后内容不刷新的ISSUE-001故障,新增routeViewKey工具函数区分路由实例
完善路由配置,新增admin空路径路由,修复路由命名空路径告警
新增媒体管理、运维发布/重置页面,补全全站路由注册与动态路由过滤逻辑
新增玩法、线索、管家、系统资源管理的全套工具函数、业务组件及单元测试用例
修复后端app/seed.py中媒体资源重复检测未考虑会话内pending资源的bug,新增对应测试验证修复逻辑
修正alembic迁移版本0015的依赖关系,删除废弃的旧迁移文件
优化后端系统接口的字典序列化逻辑,简化重复代码
添加QA验收报告与相关测试截图,完善项目测试覆盖
2026-08-26 18:48:48 +08:00

72 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. 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 { computed, onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { getLeads, updateLeadStatus } from "@/api/client";
import { leadStatusLabel, leadStatusOptions, leadTypeOptions } from "@/lib/leads";
import type { Lead, LeadStatus, LeadType } from "@/types";
const leads = ref<Lead[]>([]);
const selectedId = ref("");
const leadType = ref<LeadType | "">("");
const status = ref<LeadStatus | "">("");
const keyword = ref("");
const loading = ref(true);
const error = ref("");
const updatingId = ref("");
const selectedLead = computed(() => leads.value.find((lead) => lead.id === selectedId.value) ?? null);
async function load() {
loading.value = true;
error.value = "";
try {
const result = await getLeads({ leadType: leadType.value, status: status.value, keyword: keyword.value, take: 50 });
leads.value = result.items;
selectedId.value = result.items.some((item) => item.id === selectedId.value) ? selectedId.value : result.items[0]?.id ?? "";
} catch (loadError) {
error.value = loadError instanceof Error ? loadError.message : "线索加载失败";
} finally {
loading.value = false;
}
}
onMounted(load);
async function changeStatus(lead: Lead, nextStatus: LeadStatus) {
if (updatingId.value) return;
const previous = lead.status;
lead.status = nextStatus;
updatingId.value = lead.id;
try {
const saved = await updateLeadStatus(lead.id, nextStatus);
const index = leads.value.findIndex((item) => item.id === saved.id);
if (index >= 0) leads.value[index] = saved;
ElMessage.success(`线索已更新为${leadStatusLabel(nextStatus)}`);
} catch (updateError) {
lead.status = previous;
ElMessage.error(updateError instanceof Error ? updateError.message : "线索状态更新失败");
} finally {
updatingId.value = "";
}
}
function formatDate(value?: string | null) {
if (!value) return "未填写";
const date = new Date(value);
return Number.isNaN(date.valueOf()) ? value : new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short" }).format(date);
}
function leadTitle(lead: Lead) {
return lead.vehicleDemand?.vehicleOptionTitle || lead.destination || (lead.leadType === "vehicle" ? "用车需求" : "一般需求");
}
</script>
<template>
<div class="page-stack">
<div class="page-intro"><div><p class="eyebrow">INBOX / LEADS</p><h2>需求线索</h2><p>按类型、状态和关键词筛选需求,并记录跟进状态。</p></div><el-button :loading="loading" @click="load">刷新</el-button></div>
<el-card shadow="never" class="panel-card lead-filter-card"><div class="lead-filters"><el-select v-model="leadType" aria-label="线索类型" placeholder="全部类型"><el-option v-for="option in leadTypeOptions" :key="option.value || 'all'" :label="option.label" :value="option.value" /></el-select><el-select v-model="status" aria-label="线索状态" placeholder="全部状态" clearable><el-option label="全部状态" value="" /><el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select><el-input v-model="keyword" clearable placeholder="搜索地点、手机号或备注" aria-label="搜索线索" @keyup.enter="load" /><el-button type="primary" :loading="loading" @click="load">查询</el-button></div></el-card>
<el-alert v-if="error" :title="error" type="error" show-icon :closable="false"><template #default><el-button link type="primary" @click="load">重试</el-button></template></el-alert>
<el-card shadow="never" class="panel-card"><el-skeleton v-if="loading" :rows="8" animated /><el-empty v-else-if="!leads.length" description="暂无符合条件的需求线索" /><el-table v-else :data="leads" stripe highlight-current-row :current-row-key="selectedId" row-key="id" @row-click="(row: Lead) => selectedId = row.id"><el-table-column label="需求" min-width="210"><template #default="{ row }"><div class="lead-summary"><strong>{{ row.contactName || "未填写联系人" }}</strong><span>{{ leadTitle(row) }} · {{ row.phone }}</span></div></template></el-table-column><el-table-column prop="destination" label="目的地" min-width="130" /><el-table-column label="类型" width="100"><template #default="{ row }"><el-tag effect="plain">{{ row.leadType === "vehicle" ? "用车" : "一般" }}</el-tag></template></el-table-column><el-table-column label="状态" width="160"><template #default="{ row }"><el-select v-permission="'admin:leads:update'" :model-value="row.status" size="small" :loading="updatingId === row.id" @click.stop @update:model-value="changeStatus(row, $event)"><el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></template></el-table-column><el-table-column label="提交时间" min-width="170"><template #default="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column></el-table></el-card>
<el-card v-if="selectedLead" shadow="never" class="panel-card lead-detail-card"><template #header><div class="panel-heading"><span>线索详情</span><el-tag :type="selectedLead.status === 'invalid' ? 'info' : selectedLead.status === 'won' ? 'success' : 'warning'" effect="plain">{{ leadStatusLabel(selectedLead.status) }}</el-tag></div></template><dl class="lead-detail-grid"><div><dt>联系人</dt><dd>{{ selectedLead.contactName || "未填写" }}</dd></div><div><dt>联系方式</dt><dd>{{ selectedLead.phone }}</dd></div><div><dt>需求类型</dt><dd>{{ selectedLead.leadType === "vehicle" ? "用车需求" : "一般需求" }}</dd></div><div><dt>提交时间</dt><dd>{{ formatDate(selectedLead.createdAt) }}</dd></div><div><dt>目的地</dt><dd>{{ selectedLead.destination || "未填写" }}</dd></div><div><dt>出行日期</dt><dd>{{ formatDate(selectedLead.vehicleDemand?.travelDate || selectedLead.travelDate) }}</dd></div></dl><div v-if="selectedLead.vehicleDemand" class="lead-detail-section"><strong>用车行程</strong><p>{{ selectedLead.vehicleDemand.pickupLocation }} → {{ selectedLead.vehicleDemand.dropoffLocation }}</p><p>{{ selectedLead.vehicleDemand.peopleCount }} 人 · 行李 {{ selectedLead.vehicleDemand.luggageCount ?? "未填写" }} 件 · {{ selectedLead.vehicleDemand.vehicleOptionTitle || "未指定车型" }}</p></div><div class="lead-detail-section"><strong>备注</strong><p>{{ selectedLead.vehicleDemand?.specialRequirements || selectedLead.note || "暂无备注" }}</p></div></el-card>
</div>
</template>