机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 分析看板:对接主账 /inquiry/dashboard/metrics。
|
||||
* 指标 / 询价趋势 / 人员排名 / 成本趋势均来自主账聚合;禁止用 Mock 假线冒充生产。
|
||||
*/
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import { getDashboardMetrics } from "/@/views/inquiry/dashboard/dashboard.api";
|
||||
@@ -100,6 +100,28 @@ async function loadMetrics() {
|
||||
const res: any = await getDashboardMetrics(req);
|
||||
const data = res?.result || res || {};
|
||||
todayMetrics.value = (data.metrics || []) as MetricCard[];
|
||||
// #region agent log
|
||||
fetch("http://127.0.0.1:7800/ingest/a69bf0d0-32e3-486e-80eb-ae1824ba17a1", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "7b2ee5" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "7b2ee5",
|
||||
runId: "post-fix",
|
||||
hypothesisId: "THOUSAND_SEP",
|
||||
location: "dashboard/index.vue:loadMetrics",
|
||||
message: "指标千分位展示抽样",
|
||||
data: {
|
||||
samples: todayMetrics.value.slice(0, 9).map((m) => ({
|
||||
key: m.key,
|
||||
raw: m.value,
|
||||
display: formatMetricDisplay(m),
|
||||
isTime: !!m.isTime,
|
||||
})),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
inquiryTrend.value = ((data.trend || []) as any[]).map((p) => ({
|
||||
date: String(p.date || ""),
|
||||
value: Number(p.value || 0),
|
||||
@@ -202,6 +224,36 @@ function formatCost(v: number | null | undefined) {
|
||||
return v.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 看板统计数字千分位:百分比、时长原样;件数/金额加分组符。
|
||||
* 主账已格式化时再跑一遍也安全(先去逗号再 format)。
|
||||
*/
|
||||
function formatGroupedNumberText(numStr: string): string {
|
||||
const cleaned = String(numStr || "").replace(/,/g, "").trim();
|
||||
if (!/^\d+(\.\d+)?$/.test(cleaned)) return numStr;
|
||||
const n = Number(cleaned);
|
||||
if (Number.isNaN(n)) return numStr;
|
||||
if (cleaned.includes(".")) {
|
||||
const frac = cleaned.split(".")[1].length;
|
||||
return n.toLocaleString("zh-CN", { minimumFractionDigits: frac, maximumFractionDigits: frac });
|
||||
}
|
||||
return n.toLocaleString("zh-CN");
|
||||
}
|
||||
|
||||
function formatMetricDisplay(m: MetricCard): string {
|
||||
if (m.isTime) return m.value;
|
||||
const raw = String(m.value ?? "");
|
||||
if (!raw || raw === "-" || raw.includes("%")) return raw;
|
||||
const match = raw.match(/^([\d,]+(?:\.\d+)?)(.*)$/);
|
||||
if (!match) return raw;
|
||||
return formatGroupedNumberText(match[1]) + match[2];
|
||||
}
|
||||
|
||||
function formatCountDisplay(n: number): string {
|
||||
if (n == null || Number.isNaN(n)) return "-";
|
||||
return Number(n).toLocaleString("zh-CN");
|
||||
}
|
||||
|
||||
const inquirySvg = computed(() => {
|
||||
const points = inquiryTrend.value.length
|
||||
? inquiryTrend.value
|
||||
@@ -237,26 +289,6 @@ const costDates = computed(() =>
|
||||
costDateLabels.value.length ? costDateLabels.value : inquiryTrend.value.map((p) => p.date),
|
||||
);
|
||||
|
||||
const costSampleDays = computed(() => {
|
||||
const set = new Set<string>();
|
||||
for (const r of costRoutes.value) {
|
||||
r.values.forEach((v, i) => {
|
||||
if (v != null && !Number.isNaN(v) && costDates.value[i]) set.add(costDates.value[i]);
|
||||
});
|
||||
}
|
||||
return [...set];
|
||||
});
|
||||
|
||||
const costFootnote = computed(() => {
|
||||
if (!costRoutes.value.length) {
|
||||
return "近 7 日暂无带金额的工单,有报价后将显示线路成本趋势";
|
||||
}
|
||||
const days = costSampleDays.value;
|
||||
if (!days.length) {
|
||||
return "已选线路在近 7 日无金额样本";
|
||||
}
|
||||
return `成本来自主账工单(成本价优先,缺省销售价);有金额日期:${days.join("、")},其余日悬停为 —`;
|
||||
});
|
||||
const costXs = computed(() => {
|
||||
const n = Math.max(costDates.value.length, 1);
|
||||
return costDates.value.map((_, i) => (n === 1 ? 380 : 36 + (654 / (n - 1)) * i));
|
||||
@@ -290,7 +322,7 @@ function showInquiryTip(date: string, value: number, x: number, y: number, wrapE
|
||||
left: `${(x / 760) * box.width}px`,
|
||||
top: `${(y / 240) * box.height}px`,
|
||||
date,
|
||||
value: `${value} 单/日`,
|
||||
value: `${formatCountDisplay(value)} 单/日`,
|
||||
lines: [],
|
||||
};
|
||||
}
|
||||
@@ -331,7 +363,7 @@ function exportRanking() {
|
||||
row.name,
|
||||
row.dept,
|
||||
roleLabels[row.role] || row.role,
|
||||
`${row.volumeNum}单`,
|
||||
`${formatCountDisplay(row.volumeNum)}单`,
|
||||
]);
|
||||
const csv = [header, ...body]
|
||||
.map((cols) => cols.map((col) => `"${String(col).replace(/"/g, '""')}"`).join(","))
|
||||
@@ -351,6 +383,92 @@ function onDocClick(e: MouseEvent) {
|
||||
if (!t.closest(".ref-route-filter")) routeFilterOpen.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试:打开线路筛选时采集弹层/chips/祖先裁切与可滚尺寸。
|
||||
*/
|
||||
function probeRouteFilterMenu(runId: string) {
|
||||
// #region agent log
|
||||
const menu = document.querySelector(".ref-route-filter__menu") as HTMLElement | null;
|
||||
const chips = document.querySelector(".ref-route-filter__chips") as HTMLElement | null;
|
||||
const filter = document.querySelector(".ref-route-filter") as HTMLElement | null;
|
||||
if (!menu || !chips) {
|
||||
fetch("http://127.0.0.1:7800/ingest/a69bf0d0-32e3-486e-80eb-ae1824ba17a1", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "7b2ee5" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "7b2ee5",
|
||||
runId,
|
||||
hypothesisId: "H2",
|
||||
location: "dashboard/index.vue:probeRouteFilterMenu",
|
||||
message: "弹层或 chips 未找到",
|
||||
data: { hasMenu: !!menu, hasChips: !!chips },
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
const menuCs = getComputedStyle(menu);
|
||||
const chipsCs = getComputedStyle(chips);
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const chipsRect = chips.getBoundingClientRect();
|
||||
const clipAncestors: Array<{ tag: string; cls: string; overflow: string; overflowY: string; rectBottom: number }> = [];
|
||||
let node: HTMLElement | null = filter;
|
||||
while (node && node !== document.body) {
|
||||
const cs = getComputedStyle(node);
|
||||
if (cs.overflow !== "visible" || cs.overflowY !== "visible" || cs.overflowX !== "visible") {
|
||||
clipAncestors.push({
|
||||
tag: node.tagName.toLowerCase(),
|
||||
cls: (node.className || "").toString().slice(0, 80),
|
||||
overflow: cs.overflow,
|
||||
overflowY: cs.overflowY,
|
||||
rectBottom: Math.round(node.getBoundingClientRect().bottom),
|
||||
});
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
const spaceBelow = Math.round(window.innerHeight - menuRect.top);
|
||||
fetch("http://127.0.0.1:7800/ingest/a69bf0d0-32e3-486e-80eb-ae1824ba17a1", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "7b2ee5" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "7b2ee5",
|
||||
runId,
|
||||
hypothesisId: "H1-H5",
|
||||
location: "dashboard/index.vue:probeRouteFilterMenu",
|
||||
message: "线路筛选弹层尺寸与溢出",
|
||||
data: {
|
||||
routeCount: costRoutes.value.length,
|
||||
menu: {
|
||||
h: Math.round(menuRect.height),
|
||||
bottom: Math.round(menuRect.bottom),
|
||||
overflow: menuCs.overflow,
|
||||
overflowY: menuCs.overflowY,
|
||||
maxHeight: menuCs.maxHeight,
|
||||
},
|
||||
chips: {
|
||||
h: Math.round(chipsRect.height),
|
||||
clientH: chips.clientHeight,
|
||||
scrollH: chips.scrollHeight,
|
||||
canScroll: chips.scrollHeight > chips.clientHeight + 1,
|
||||
overflow: chipsCs.overflow,
|
||||
overflowY: chipsCs.overflowY,
|
||||
maxHeight: chipsCs.maxHeight,
|
||||
},
|
||||
viewport: { innerH: window.innerHeight, spaceBelow, menuClippedByViewport: menuRect.bottom > window.innerHeight + 1 },
|
||||
clipAncestors,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
watch(routeFilterOpen, async (open) => {
|
||||
if (!open) return;
|
||||
await nextTick();
|
||||
requestAnimationFrame(() => probeRouteFilterMenu("post-fix"));
|
||||
});
|
||||
|
||||
const inquiryWrap = ref<HTMLElement | null>(null);
|
||||
const costWrap = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -409,7 +527,7 @@ onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
<span class="ref-metric-tip">{{ m.tip }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ref-metric-card__value" :class="{ 'is-time': m.isTime }">{{ m.value }}</div>
|
||||
<div class="ref-metric-card__value" :class="{ 'is-time': m.isTime }">{{ formatMetricDisplay(m) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -447,7 +565,7 @@ onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
<text v-for="c in inquirySvg.coords" :key="'d-' + c.date" :x="c.x" y="218">{{ c.date }}</text>
|
||||
</g>
|
||||
<g fill="#0f172a" font-size="8" font-weight="800" text-anchor="middle">
|
||||
<text v-for="c in inquirySvg.coords" :key="'v-' + c.date" :x="c.x" :y="c.y - 11">{{ c.value }}</text>
|
||||
<text v-for="c in inquirySvg.coords" :key="'v-' + c.date" :x="c.x" :y="c.y - 11">{{ formatCountDisplay(c.value) }}</text>
|
||||
</g>
|
||||
<g
|
||||
v-for="c in inquirySvg.coords"
|
||||
@@ -575,9 +693,6 @@ 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">
|
||||
{{ costFootnote }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -625,7 +740,7 @@ onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
<span class="col-name">{{ row.name }}</span>
|
||||
<span class="col-dept">{{ row.dept }}</span>
|
||||
</div>
|
||||
<span class="col-volume">{{ row.volumeNum }} 单</span>
|
||||
<span class="col-volume">{{ formatCountDisplay(row.volumeNum) }} 单</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -682,13 +797,6 @@ onUnmounted(() => document.removeEventListener("click", onDocClick));
|
||||
z-index: 120;
|
||||
}
|
||||
|
||||
.dash-cost-footnote {
|
||||
margin-top: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ref-surface--metrics,
|
||||
.ref-surface--metrics .ref-metrics-grid,
|
||||
.ref-surface--metrics .ref-metric-card {
|
||||
|
||||
Reference in New Issue
Block a user