落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 14:09:45 +08:00
co-authored by Cursor
parent 8bdf4990a2
commit a70bce3c8f
138 changed files with 7239 additions and 1085 deletions
@@ -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>