Files
WonderQ-Project/WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue
T
duanshuwen cb1c695453 fix(admin-ui): 修复线索状态更新后详情弹窗未关闭,优化管家顾问页面文案与格式
- 为LeadsPage的submitStatus函数的finally块添加关闭详情弹窗的逻辑,确保更新状态后自动关闭弹窗
- 修正ConciergePage页面的介绍文案,将原表述"停用后不会进入 Public API"调整为"停用后不会进入小程序显示"
- 格式化两个页面的Vue模板代码,拆分长行并统一缩进,提升代码可读性
2026-08-27 10:06:52 +08:00

202 lines
9.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 detailOpen = ref(false);
const selectedId = ref("");
const pendingStatus = ref<LeadStatus | "">("");
const leadType = ref<LeadType | "">("");
const status = ref<LeadStatus | "">("");
const keyword = ref("");
const page = ref(1);
const pageSize = ref(20);
const total = ref(0);
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(nextPage = page.value) {
loading.value = true;
error.value = "";
page.value = nextPage;
try {
const result = await getLeads({ leadType: leadType.value, status: status.value, keyword: keyword.value, offset: (page.value - 1) * pageSize.value, take: pageSize.value });
leads.value = result.items;
total.value = result.total;
selectedId.value = result.items.some((item) => item.id === selectedId.value) ? selectedId.value : "";
} catch (loadError) {
error.value = loadError instanceof Error ? loadError.message : "线索加载失败";
leads.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function resetFilters() {
leadType.value = "";
status.value = "";
keyword.value = "";
void load(1);
}
function openDetail(lead: Lead) {
selectedId.value = lead.id;
pendingStatus.value = lead.status;
detailOpen.value = true;
}
async function submitStatus(lead: Lead) {
const nextStatus = pendingStatus.value;
if (updatingId.value || !nextStatus || lead.status === nextStatus) return;
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;
pendingStatus.value = saved.status;
ElMessage.success(`线索已更新为${leadStatusLabel(nextStatus)}`);
} catch (updateError) {
ElMessage.error(updateError instanceof Error ? updateError.message : "线索状态更新失败");
} finally {
updatingId.value = "";
detailOpen.value = false;
}
}
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" ? "用车需求" : "一般需求");
}
function statusTagType(value: LeadStatus) {
if (value === "won") return "success";
if (value === "invalid") return "info";
return "warning";
}
onMounted(load);
</script>
<template>
<div class="page-stack">
<div class="page-intro">
<div>
<p class="eyebrow">INBOX / LEADS</p>
<h2>需求线索</h2>
<p>按类型、状态和关键词筛选需求,并记录跟进状态。</p>
</div>
</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(1)" />
<el-button type="primary" :loading="loading" @click="load(1)">查询</el-button>
<el-button :disabled="loading" @click="resetFilters">重置</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(page)">重试</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="暂无符合条件的需求线索" />
<template v-else>
<el-table :data="leads" stripe row-key="id">
<el-table-column label="需求" min-width="250"><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="110"><template #default="{ row }"><el-tag :type="statusTagType(row.status)"
effect="plain">{{ leadStatusLabel(row.status) }}</el-tag></template></el-table-column>
<el-table-column label="提交时间" min-width="175"><template #default="{ row }">{{ formatDate(row.createdAt)
}}</template></el-table-column>
<el-table-column label="操作" width="110" fixed="right"><template #default="{ row }"><el-button link
type="primary" @click="openDetail(row)">查看详情</el-button></template></el-table-column>
</el-table>
<div class="lead-pagination"><span>共 {{ total }} 条</span><el-pagination v-model:current-page="page"
v-model:page-size="pageSize" background layout="sizes, prev, pager, next, jumper"
:page-sizes="[20, 50, 100]" :total="total" @current-change="load" @size-change="load(1)" /></div>
</template>
</el-card>
<el-dialog v-model="detailOpen" title="线索详情" width="min(720px, 94vw)" class="lead-detail-dialog" destroy-on-close>
<template v-if="selectedLead">
<div class="lead-detail-card">
<dl class="lead-detail-grid">
<div class="lead-detail-item">
<dt>联系人</dt>
<dd>{{ selectedLead.contactName || "未填写" }}</dd>
</div>
<div class="lead-detail-item">
<dt>联系方式</dt>
<dd>{{ selectedLead.phone || "未填写" }}</dd>
</div>
<div class="lead-detail-item">
<dt>需求类型</dt>
<dd>{{ selectedLead.leadType === "vehicle" ? "用车需求" : "一般需求" }}</dd>
</div>
<div class="lead-detail-item">
<dt>提交时间</dt>
<dd>{{ formatDate(selectedLead.createdAt) }}</dd>
</div>
<div class="lead-detail-item">
<dt>目的地</dt>
<dd>{{ selectedLead.destination || "未填写" }}</dd>
</div>
<div class="lead-detail-item">
<dt>出行日期</dt>
<dd>{{ formatDate(selectedLead.vehicleDemand?.travelDate || selectedLead.travelDate) }}</dd>
</div>
</dl>
</div>
<section v-if="selectedLead.vehicleDemand" class="lead-detail-section lead-detail-trip">
<div class="lead-detail-section-title"><strong>用车行程</strong></div>
<p>{{ selectedLead.vehicleDemand.pickupLocation }} → {{ selectedLead.vehicleDemand.dropoffLocation }}</p>
<p>{{ selectedLead.vehicleDemand.peopleCount }} 人 · 行李 {{ selectedLead.vehicleDemand.luggageCount ?? "未填写" }}
件 · {{ selectedLead.vehicleDemand.vehicleOptionTitle || "未指定车型" }}</p>
</section>
<section class="lead-detail-section lead-detail-note">
<div class="lead-detail-section-title"><strong>备注</strong></div>
<p>{{ selectedLead.vehicleDemand?.specialRequirements || selectedLead.note || "暂无备注" }}</p>
</section>
</template>
<template #footer>
<div v-if="selectedLead" class="lead-detail-footer">
<div class="lead-detail-footer-status">
<span>处理状态</span>
<el-tag :type="statusTagType(pendingStatus || selectedLead.status)" effect="plain">{{
leadStatusLabel(pendingStatus || selectedLead.status) }}</el-tag>
</div>
<div class="lead-detail-footer-actions">
<el-select v-permission="'admin:leads:update'" v-model="pendingStatus"
:loading="updatingId === selectedLead.id" aria-label="选择处理状态">
<el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label"
:value="option.value" />
</el-select>
<el-button v-permission="'admin:leads:update'" type="primary"
:disabled="!pendingStatus || pendingStatus === selectedLead.status"
:loading="updatingId === selectedLead.id" @click="submitStatus(selectedLead)">提交状态</el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>