dingdanquanliucheng/frontend/web-sales/src/views/DashboardPage.vue
2026-05-27 17:05:55 +08:00

231 lines
9.3 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.

<template>
<section class="page">
<!-- <div class="page-hero">-->
<!-- <div>-->
<!-- <span class="eyebrow">工作台</span>-->
<!-- <h2>业务员工作台</h2>-->
<!-- <p class="desc">{{ greeting }}以下是您的今日概览</p>-->
<!-- </div>-->
<!-- <span class="mode-tag" :class="{ fallback: isMock }">-->
<!-- {{ isMock ? "演示数据" : "真实接口" }}-->
<!-- </span>-->
<!-- </div>-->
<p v-if="loading" class="loading">加载中...</p>
<div v-if="!loading" class="summary-row">
<article class="summary-card">
<strong>{{ dashStats.orderCount }}</strong>
<span>订单总数</span>
</article>
<article class="summary-card">
<strong>{{ dashStats.pendingCount }}</strong>
<span>待审批</span>
</article>
<article class="summary-card">
<strong>{{ dashStats.fulfillmentCount }}</strong>
<span>履约中</span>
</article>
<article class="summary-card">
<strong>{{ dashStats.draftCount }}</strong>
<span>草稿</span>
</article>
<article class="summary-card">
<strong>{{ reminderStats.total }}</strong>
<span>提醒总数</span>
</article>
<article class="summary-card highlight">
<strong>{{ reminderStats.pending }}</strong>
<span>待处理提醒</span>
</article>
</div>
<div v-if="!loading" class="grid-2col">
<section class="panel">
<div class="panel-header">
<h3>最新订单</h3>
<RouterLink to="/orders" class="link-btn">查看全部</RouterLink>
</div>
<div v-if="recentOrders.length === 0" class="empty-box">暂无订单数据</div>
<div v-else class="order-list">
<RouterLink
v-for="item in recentOrders"
:key="item.order_no"
:to="`/orders/${item.order_id}`"
class="order-row"
>
<span class="order-no">{{ item.order_no }}</span>
<span class="order-customer">{{ item.customer_name }}</span>
<span class="status" :class="statusClass(item.order_status)">{{ mapStatus(item.order_status) }}</span>
<span class="order-amount">{{ Number(item.sale_price_total || 0).toFixed(2) }}</span>
</RouterLink>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h3>待处理提醒</h3>
<RouterLink to="/reminders" class="link-btn">查看全部</RouterLink>
</div>
<div v-if="pendingReminders.length === 0" class="empty-box">暂无待处理提醒</div>
<div v-else class="reminder-list">
<article v-for="item in pendingReminders" :key="item.reminder_id" class="reminder-row">
<span class="reminder-title">{{ item.title }}</span>
<span class="reminder-type">{{ item.type || "-" }}</span>
<span class="reminder-time">{{ item.created_at || "-" }}</span>
</article>
</div>
</section>
</div>
<section v-if="!loading && flowTraces.length" class="panel">
<div class="panel-header">
<h3>最近物流动态</h3>
</div>
<div class="trace-list">
<article v-for="(trace, idx) in flowTraces" :key="idx" class="trace-row">
<strong>{{ trace.title }}</strong>
<span>{{ trace.detail }}</span>
<span class="trace-time">{{ trace.time }}</span>
</article>
</div>
</section>
</section>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { fetchFlowOverview, fetchRemindersDashboard, fetchSalesmanDashboard } from "../mockApi";
import { salesStore } from "../store";
const loading = ref(true);
const isMock = ref(false);
const dashOrders = ref([]);
const recentOrders = ref([]);
const pendingReminders = ref([]);
const flowTraces = ref([]);
const dashStats = ref({ orderCount: 0, pendingCount: 0, fulfillmentCount: 0, draftCount: 0 });
const reminderStats = ref({ total: 0, pending: 0, read: 0 });
const greeting = computed(() => {
const name = salesStore.user?.realName || "业务员";
const h = new Date().getHours();
if (h < 12) return `上午好,${name}`;
if (h < 18) return `下午好,${name}`;
return `晚上好,${name}`;
});
function mapStatus(s) {
return { draft: "草稿", pending_approve: "待审批", approved: "已审批", rejected: "已驳回", canceled: "已取消", pending_factory: "待工厂处理" }[s] || s || "-";
}
function statusClass(s) {
if (["approved", "settled", "finished"].includes(s)) return "success";
if (["rejected", "canceled"].includes(s)) return "danger";
if (["pending_approve", "cancel_pending", "pending_factory"].includes(s)) return "warning";
return "info";
}
onMounted(async () => {
try {
const [dashData, flowData, remData] = await Promise.all([
fetchSalesmanDashboard().catch(() => null),
fetchFlowOverview().catch(() => null),
fetchRemindersDashboard().catch(() => null),
]);
if (dashData) {
isMock.value = Boolean(dashData.is_mock);
dashOrders.value = dashData.orders || [];
recentOrders.value = dashOrders.value.slice(0, 8);
const s = dashData.summary || {};
dashStats.value.orderCount = s.order_count || 0;
}
if (flowData) {
const fs = flowData.stats || {};
dashStats.value.pendingCount = fs.pending || 0;
dashStats.value.fulfillmentCount = fs.fulfillment || 0;
dashStats.value.draftCount = fs.draft || 0;
flowTraces.value = flowData.traces || [];
}
if (remData) {
const rs = remData.summary || {};
reminderStats.value = { total: rs.total || 0, pending: rs.pending || 0, read: rs.read || 0 };
pendingReminders.value = (remData.list || []).filter((r) => r.status === "pending").slice(0, 6);
}
} catch {
isMock.value = true;
} finally {
loading.value = false;
}
});
</script>
<style scoped>
.page { display: grid; gap: 18px; }
.page-hero, .panel {
background: rgba(255,255,255,0.92);
border: 1px solid #e5e7eb;
border-radius: 20px;
box-shadow: 0 14px 40px rgba(15,23,42,0.06);
}
.page-hero { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; padding: 24px; }
.eyebrow { display: inline-flex; padding: 6px 12px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 12px; font-weight: 600; }
h2 { margin: 12px 0 8px; font-size: 30px; }
.desc, .loading, .panel-header span { color: #64748b; }
.mode-tag { align-self: flex-start; padding: 6px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; font-weight: 600; }
.mode-tag.fallback { background: #fef3c7; color: #92400e; }
.summary-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.summary-card { border: 1px solid #dbe3ef; border-radius: 16px; padding: 16px; background: linear-gradient(180deg, #ffffff, #f8fbff); display: grid; gap: 6px; }
.summary-card strong { font-size: 28px; color: #0f172a; }
.summary-card span { color: #64748b; }
.summary-card.highlight { border-color: #fbbf24; background: linear-gradient(180deg, #fffbeb, #fef3c7); }
.summary-card.highlight strong { color: #b45309; }
.grid-2col { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 18px; }
.panel { padding: 20px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 14px; }
.panel-header h3 { margin: 0; font-size: 16px; }
.link-btn { color: #1d4ed8; text-decoration: none; font-size: 13px; font-weight: 600; }
.empty-box { padding: 20px; border: 1px dashed #d1d5db; border-radius: 16px; color: #6b7280; text-align: center; }
.order-list { display: grid; gap: 8px; }
.order-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 12px; text-decoration: none; color: inherit; transition: background 0.15s; }
.order-row:hover { background: #f0f9ff; }
.order-no { font-weight: 600; color: #0f172a; min-width: 140px; }
.order-customer { color: #475569; flex: 1; }
.order-amount { font-weight: 600; color: #0f172a; }
.reminder-list { display: grid; gap: 8px; }
.reminder-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 12px; }
.reminder-title { font-weight: 600; color: #0f172a; flex: 1; }
.reminder-type { color: #64748b; font-size: 12px; }
.reminder-time { color: #9ca3af; font-size: 12px; }
.trace-list { display: grid; gap: 8px; }
.trace-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 12px; }
.trace-row strong { min-width: 140px; color: #0f172a; }
.trace-row span:nth-child(2) { flex: 1; color: #475569; }
.trace-time { color: #9ca3af; font-size: 12px; }
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.status.info { background: #dbeafe; color: #1d4ed8; }
.status.warning { background: #fef3c7; color: #b45309; }
.status.success { background: #dcfce7; color: #15803d; }
.status.danger { background: #fee2e2; color: #b91c1c; }
@media (max-width: 640px) {
.page-hero, .panel { padding: 14px; border-radius: 16px; }
h2 { font-size: 22px; }
.summary-card strong { font-size: 20px; }
.order-no, .order-row, .reminder-row, .trace-row { font-size: 12px; }
.grid-2col { grid-template-columns: 1fr; }
}
</style>