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