落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,35 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 分析看板:图标 / 帮助气泡 / 图表悬停提示对齐原型。
|
||||
* 顶栏工具条用 Teleport defer 挂到布局 #page-topbar-actions。
|
||||
* 分析看板:对接主账 /inquiry/dashboard/metrics。
|
||||
* 指标 / 趋势 / 排名来自聚合接口;成本趋势本期主账未提供,展示空态不造假数。
|
||||
*/
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import {
|
||||
todayMetrics,
|
||||
inquiryTrend,
|
||||
rankingPeople,
|
||||
rankingDepts,
|
||||
costRoutes,
|
||||
type RankingPerson,
|
||||
} from "/@/views/inquiry/mock/dashboard";
|
||||
import { getDashboardMetrics } from "/@/views/inquiry/dashboard/dashboard.api";
|
||||
import type { MetricCard, InquiryPoint, RankingPerson, CostRoute } from "/@/views/inquiry/mock/dashboard";
|
||||
|
||||
const dateDisplay = ref("2026-07-01——2026-07-31");
|
||||
const dateFrom = ref("2026-07-01");
|
||||
const dateTo = ref("2026-07-31");
|
||||
const dateFromMonth = ref("2026-07");
|
||||
const dateToMonth = ref("2026-07");
|
||||
const dateFromYear = ref("2026");
|
||||
const dateToYear = ref("2026");
|
||||
const todayMetrics = ref<MetricCard[]>([]);
|
||||
const inquiryTrend = ref<InquiryPoint[]>([]);
|
||||
const rankingPeople = ref<RankingPerson[]>([]);
|
||||
const rankingDepts = ref<string[]>([]);
|
||||
/** 成本趋势主账暂无接口,保持空数组避免 Mock 假线。 */
|
||||
const costRoutes = ref<CostRoute[]>([]);
|
||||
|
||||
const now = new Date();
|
||||
const isoDay = now.toISOString().slice(0, 10);
|
||||
const dateDisplay = ref(`${isoDay}——${isoDay}`);
|
||||
const dateFrom = ref(isoDay);
|
||||
const dateTo = ref(isoDay);
|
||||
const dateFromMonth = ref(isoDay.slice(0, 7));
|
||||
const dateToMonth = ref(isoDay.slice(0, 7));
|
||||
const dateFromYear = ref(isoDay.slice(0, 4));
|
||||
const dateToYear = ref(isoDay.slice(0, 4));
|
||||
const pickerOpen = ref(false);
|
||||
const pickerMode = ref<"day" | "month" | "year">("day");
|
||||
const updatedTime = ref("00:00");
|
||||
const routeFilterOpen = ref(false);
|
||||
const selectedRoutes = ref<string[]>(costRoutes.map((r) => r.id));
|
||||
const selectedRoutes = ref<string[]>([]);
|
||||
const rankingRole = ref<"all" | "sales" | "route" | "product">("all");
|
||||
const rankingDept = ref("");
|
||||
const chartReady = ref(false);
|
||||
const loading = ref(false);
|
||||
const chartTip = ref({
|
||||
visible: false,
|
||||
left: "0px",
|
||||
@@ -51,12 +55,56 @@ const activePoint = ref<string | null>(null);
|
||||
const INQUIRY_PLOT_TOP = 36;
|
||||
const INQUIRY_PLOT_BOTTOM = 188;
|
||||
|
||||
function resolveRange(): string {
|
||||
if (pickerMode.value === "day") {
|
||||
const from = new Date(dateFrom.value);
|
||||
const to = new Date(dateTo.value);
|
||||
const days = Math.max(0, Math.round((to.getTime() - from.getTime()) / 86400000));
|
||||
if (days <= 0) return "today";
|
||||
if (days <= 6) return "7d";
|
||||
return "30d";
|
||||
}
|
||||
if (pickerMode.value === "month") return "30d";
|
||||
return "30d";
|
||||
}
|
||||
|
||||
async function loadMetrics() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await getDashboardMetrics(resolveRange());
|
||||
const data = res?.result || res || {};
|
||||
todayMetrics.value = (data.metrics || []) as MetricCard[];
|
||||
inquiryTrend.value = ((data.trend || []) as any[]).map((p) => ({
|
||||
date: String(p.date || ""),
|
||||
value: Number(p.value || 0),
|
||||
}));
|
||||
rankingPeople.value = ((data.rankings || []) as any[]).map((p) => ({
|
||||
name: String(p.name || ""),
|
||||
role: (p.role || "sales") as RankingPerson["role"],
|
||||
dept: String(p.dept || "-"),
|
||||
volumeNum: Number(p.volumeNum || 0),
|
||||
}));
|
||||
rankingDepts.value = Array.from(
|
||||
new Set(rankingPeople.value.map((p) => p.dept).filter((d) => d && d !== "-")),
|
||||
);
|
||||
const t = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
updatedTime.value = `${pad(t.getHours())}:${pad(t.getMinutes())}`;
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "加载看板失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inquirySvg = computed(() => {
|
||||
const points = inquiryTrend;
|
||||
const points = inquiryTrend.value.length
|
||||
? inquiryTrend.value
|
||||
: [{ date: "—", value: 0 }];
|
||||
const xs = points.map((_, i) => (points.length === 1 ? 380 : 36 + (654 / (points.length - 1)) * i));
|
||||
const values = points.map((p) => p.value);
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values, 1);
|
||||
const min = Math.min(...values, 0);
|
||||
const range = Math.max(max - min, 1);
|
||||
const plotH = INQUIRY_PLOT_BOTTOM - INQUIRY_PLOT_TOP;
|
||||
const yFor = (v: number) => INQUIRY_PLOT_BOTTOM - ((v - min) / range) * plotH;
|
||||
@@ -71,30 +119,31 @@ const inquirySvg = computed(() => {
|
||||
return { coords, line, area };
|
||||
});
|
||||
|
||||
const filteredRoutes = computed(() => costRoutes.filter((r) => selectedRoutes.value.includes(r.id)));
|
||||
const filteredRoutes = computed(() => costRoutes.value.filter((r) => selectedRoutes.value.includes(r.id)));
|
||||
|
||||
const rankingRows = computed(() => {
|
||||
let rows: RankingPerson[] = [...rankingPeople];
|
||||
let rows: RankingPerson[] = [...rankingPeople.value];
|
||||
if (rankingRole.value !== "all") rows = rows.filter((p) => p.role === rankingRole.value);
|
||||
if (rankingDept.value) rows = rows.filter((p) => p.dept === rankingDept.value);
|
||||
return rows.sort((a, b) => b.volumeNum - a.volumeNum || a.name.localeCompare(b.name, "zh-CN"));
|
||||
});
|
||||
|
||||
const costDates = ["06-12", "06-13", "06-14", "06-15", "06-16", "06-17", "06-18"];
|
||||
const costXs = [36, 145, 254, 363, 472, 581, 690];
|
||||
const costDates = computed(() => inquiryTrend.value.map((p) => p.date));
|
||||
const costXs = computed(() => {
|
||||
const n = Math.max(costDates.value.length, 1);
|
||||
return costDates.value.map((_, i) => (n === 1 ? 380 : 36 + (654 / (n - 1)) * i));
|
||||
});
|
||||
|
||||
function applyDateRange() {
|
||||
if (pickerMode.value === "day") dateDisplay.value = `${dateFrom.value}——${dateTo.value}`;
|
||||
else if (pickerMode.value === "month") dateDisplay.value = `${dateFromMonth.value}——${dateToMonth.value}`;
|
||||
else dateDisplay.value = `${dateFromYear.value}——${dateToYear.value}`;
|
||||
pickerOpen.value = false;
|
||||
loadMetrics();
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
updatedTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
||||
message.success("数据已更新");
|
||||
loadMetrics().then(() => message.success("数据已更新"));
|
||||
}
|
||||
|
||||
function toggleRoute(id: string) {
|
||||
@@ -105,7 +154,7 @@ function toggleRoute(id: string) {
|
||||
}
|
||||
|
||||
function selectAllRoutes() {
|
||||
selectedRoutes.value = costRoutes.map((r) => r.id);
|
||||
selectedRoutes.value = costRoutes.value.map((r) => r.id);
|
||||
}
|
||||
|
||||
function clearRoutes() {
|
||||
@@ -191,6 +240,7 @@ const costWrap = ref<HTMLElement | null>(null);
|
||||
onMounted(() => {
|
||||
chartReady.value = true;
|
||||
document.addEventListener("click", onDocClick);
|
||||
loadMetrics();
|
||||
});
|
||||
onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
</script>
|
||||
@@ -407,7 +457,9 @@ onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
<span v-for="(line, i) in costTip.lines" :key="i" class="value-line">{{ line }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dash-cost-footnote">成本数据来源于 TMS</div>
|
||||
<div class="dash-cost-footnote">
|
||||
{{ costRoutes.length ? "成本数据来源于 TMS" : "成本趋势主账暂未开放,待接口就绪后展示" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user