- 为LeadsPage的submitStatus函数的finally块添加关闭详情弹窗的逻辑,确保更新状态后自动关闭弹窗 - 修正ConciergePage页面的介绍文案,将原表述"停用后不会进入 Public API"调整为"停用后不会进入小程序显示" - 格式化两个页面的Vue模板代码,拆分长行并统一缩进,提升代码可读性
202 lines
9.4 KiB
Vue
202 lines
9.4 KiB
Vue
<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>
|