fix: refine smart management DeepSeek reports

V2.0
M先生 3 weeks ago
parent a264afc214
commit cd0aa26c98
  1. 770
      src/services/smartArtifactApi.js
  2. 157
      src/services/smartContextBuilder.js
  3. 50
      src/views/wisdomManagement/index.vue

@ -60,7 +60,7 @@ export async function generateSmartArtifact({
throw new Error("代码模型没有返回完整HTML")
}
if (isDataDumpArtifactHtml(html)) {
throw new Error("专题页面仍为原始数据表,已切换报告模板")
throw new Error("专题页面仍为原始数据表,已拒绝展示,请重新生成")
}
return {
@ -69,670 +69,6 @@ export async function generateSmartArtifact({
}
}
export function buildLocalSmartArtifact({
question,
answer = "",
charts = [],
datasets = [],
context = null,
reason = "",
} = {}) {
const data = JSON.parse(buildArtifactDataJson({ question, answer, charts, datasets, context }))
const report = buildLocalReport(data, reason)
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHtml(report.title)}</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: #18313b;
background: #f3f7fa;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
.page {
min-height: 100vh;
padding: 26px 30px 34px;
background:
linear-gradient(90deg, rgba(255,255,255,0.96), rgba(244,248,250,0.9)),
radial-gradient(circle at 84% 16%, rgba(32, 124, 138, 0.12), transparent 34%),
#f3f7fa;
}
.report-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
padding-bottom: 16px;
margin-bottom: 22px;
border-bottom: 2px solid #244956;
}
.kicker {
margin: 0 0 6px;
color: #307181;
font-size: 13px;
font-weight: 700;
}
h1 {
margin: 0;
color: #0f3340;
font-size: 30px;
line-height: 1.28;
font-weight: 800;
letter-spacing: 0;
}
.subtitle {
margin: 8px 0 0;
color: #54717b;
font-size: 14px;
}
.source {
flex: 0 0 auto;
padding: 8px 12px;
color: #5c747d;
background: #e8eef1;
border-radius: 999px;
font-size: 12px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
color: #153946;
font-size: 18px;
font-weight: 800;
}
.section-title::before {
content: "";
width: 9px;
height: 9px;
border-radius: 50%;
background: #1b7f8d;
box-shadow: 0 0 0 4px rgba(27, 127, 141, 0.12);
}
.grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.card {
min-width: 0;
padding: 18px;
border: 1px solid #e4ecef;
border-radius: 6px;
background: rgba(255,255,255,0.9);
box-shadow: 0 8px 22px rgba(25, 64, 77, 0.08);
}
.span-12 { grid-column: span 12; }
.span-7 { grid-column: span 7; }
.span-5 { grid-column: span 5; }
.span-6 { grid-column: span 6; }
.summary {
color: #294853;
font-size: 14px;
line-height: 1.9;
}
.summary p {
margin: 0 0 8px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 18px;
}
.metric-card {
min-width: 0;
padding: 18px 16px;
border: 1px solid #e6edf0;
border-radius: 6px;
background: #fff;
box-shadow: 0 8px 22px rgba(25, 64, 77, 0.08);
}
.metric-card span,
.metric-card small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #5f7780;
font-size: 13px;
}
.metric-card strong {
display: block;
margin: 8px 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #123542;
font-size: 26px;
line-height: 1.25;
}
.metric-card strong em {
margin-left: 3px;
color: #516c75;
font-size: 13px;
font-style: normal;
font-weight: 700;
}
.bar-list {
display: grid;
gap: 10px;
}
.bar-row {
display: grid;
grid-template-columns: minmax(92px, 150px) 1fr minmax(86px, auto);
align-items: center;
gap: 12px;
min-width: 0;
color: #385964;
font-size: 13px;
}
.bar-row span,
.bar-row em {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-style: normal;
}
.bar-track {
height: 18px;
overflow: hidden;
border-radius: 4px;
background: #e8eef1;
}
.bar-track i {
display: block;
height: 100%;
border-radius: 4px;
background: #1c7886;
}
.bar-note {
margin: 12px 0 0;
color: #647b84;
font-size: 12px;
text-align: right;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 13px;
}
th,
td {
padding: 11px 10px;
border-bottom: 1px solid #e5edf0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
th {
color: #315f6c;
font-weight: 700;
background: #edf4f6;
}
td { color: #375763; }
.callout {
border-left: 4px solid #e1a62c;
background: #fff6dc;
}
.recommend {
border-left: 4px solid #1b7f8d;
background: #f1f8f9;
}
.list {
margin: 0;
padding-left: 18px;
color: #355661;
font-size: 14px;
line-height: 1.8;
}
.list li + li { margin-top: 4px; }
.footnote {
margin-top: 16px;
color: #778b92;
font-size: 12px;
line-height: 1.7;
}
@media (max-width: 1100px) {
.report-header { display: block; }
.source { display: inline-block; margin-top: 12px; }
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.span-7,
.span-5,
.span-6 { grid-column: span 12; }
.page { padding: 18px; }
}
@media (max-width: 680px) {
.metric-grid { grid-template-columns: 1fr; }
h1 { font-size: 24px; }
}
</style>
</head>
<body>
<main class="page">
<header class="report-header">
<div>
<p class="kicker">智慧管理专题报告</p>
<h1>${escapeHtml(report.title)}</h1>
<p class="subtitle">${escapeHtml(report.subtitle)}</p>
</div>
<div class="source">${escapeHtml(report.sourceText)}</div>
</header>
${renderReportMetrics(report.metrics)}
<section class="grid">
<article class="card span-12">
<h2 class="section-title">总体态势</h2>
<div class="summary">${renderReportParagraphs(report.summary)}</div>
</article>
<article class="card span-7">
<h2 class="section-title">${escapeHtml(report.primaryChart.title)}</h2>
${renderReportBars(report.primaryChart)}
</article>
<article class="card span-5">
<h2 class="section-title">${escapeHtml(report.table.title)}</h2>
${renderReportTable(report.table)}
</article>
${report.secondaryChart ? `<article class="card span-12"><h2 class="section-title">${escapeHtml(report.secondaryChart.title)}</h2>${renderReportBars(report.secondaryChart)}</article>` : ""}
<article class="card callout span-6">
<h2 class="section-title">需关注事项</h2>
${renderReportList(report.attention)}
</article>
<article class="card recommend span-6">
<h2 class="section-title">工作建议</h2>
${renderReportList(report.recommendations)}
</article>
</section>
${report.footnotes.length ? `<div class="footnote">${report.footnotes.map((item) => `<div>${escapeHtml(item)}</div>`).join("")}</div>` : ""}
</main>
</body>
</html>`
}
function buildLocalReport(data, reason) {
const kind = getReportKind(data)
if (kind === "balance") return buildBalanceReport(data, reason)
if (kind === "yak") return buildYakReport(data, reason)
return buildGenericReport(data, reason)
}
function getReportKind(data) {
const question = String(data?.question || "")
const intentText = normalizeList(data?.context?.intents)
.map((item) => `${item?.key || ""}${item?.label || ""}`)
.join(" ")
const text = `${question} ${intentText}`
if (/草畜|平衡|承载|载畜|超载|放牧压力/.test(text)) return "balance"
if (/牦牛|存栏|畜群|牛群|出栏|交易|预测/.test(text)) return "yak"
return "generic"
}
function buildBalanceReport(data, reason) {
const scope = data.context?.scope || "红原县"
const county = firstRow(data, "balance.county") || firstRow(data, "balance.target") || findFirstRow(data, (row) => row?.type === "county") || {}
const townRows = rowsOf(data, "balance.townRows").length
? rowsOf(data, "balance.townRows")
: findRows(data, (row) => row?.type === "town" && row?.overloadRate !== undefined)
const chartRows = rowsOfFirstDataset(data, (dataset) => /草畜平衡|超载率/.test(dataset?.title || "") && normalizeList(dataset?.rows).some((row) => row?.value !== undefined))
const pressureRows = (townRows.length ? townRows : chartRows)
.map((row) => ({
name: row.name || row.areaName || row.key,
overloadRate: toLocalNumber(row.overloadRate ?? row.value),
level: row.level || row.extra || "",
livestockCapacity: toLocalNumber(row.livestockCapacity ?? row.yakNumber),
bearingCapacity: toLocalNumber(row.bearingCapacity),
}))
.filter((row) => row.name && Number.isFinite(row.overloadRate))
.sort((a, b) => b.overloadRate - a.overloadRate)
const topPressures = pressureRows.slice(0, 5)
const countyRate = toLocalNumber(county.overloadRate)
const livestock = toLocalNumber(county.livestockCapacity ?? county.yakNumber)
const bearing = toLocalNumber(county.bearingCapacity)
const area = toLocalNumber(county.areaRange)
const level = county.level || (Number.isFinite(countyRate) ? "基本实现草畜平衡" : "待核算")
const pressureNames = topPressures.map((row) => `${row.name}${formatRateSuffix(row.overloadRate)}`).join("、")
const advice = splitAdvice(county.advice)
return {
title: "草畜平衡情况",
subtitle: `${scope} · 草畜平衡专题`,
sourceText: "数据来源:平台草畜平衡核算数据",
metrics: [
metric("全县超载率", formatRate(countyRate), level),
metric("实际载畜量", formatHead(livestock), "当前核算口径"),
metric("理论承载力", formatHead(bearing), "按草地面积折算"),
metric("草地面积", formatWanMu(area), "纳入平衡核算"),
],
summary: [
`${scope}当前草畜平衡总体处于“${level}”状态,超载率为${formatRate(countyRate)},实际载畜量${formatHead(livestock)},理论承载力${formatHead(bearing)}`,
pressureNames
? `乡镇层面,放牧压力相对较高的区域集中在${pressureNames},这些区域应作为后续动态监测和调控的重点。`
: "乡镇层面的草畜平衡压力整体可控,后续仍需结合季节牧草产量和牲畜流动持续校核。",
],
primaryChart: {
title: "乡镇超载率对比",
note: "按超载率由高到低展示前几项",
rows: pressureRows.slice(0, 8).map((row) => ({
label: row.name,
value: row.overloadRate,
unit: "%",
})),
},
secondaryChart: buildCapacityGapChart(pressureRows),
table: {
title: "重点区域与平衡状态",
columns: ["区域", "超载率", "实际载畜量", "理论承载力", "状态"],
rows: pressureRows.slice(0, 6).map((row) => [
row.name,
formatRate(row.overloadRate),
formatHead(row.livestockCapacity),
formatHead(row.bearingCapacity),
row.level || "待跟踪",
]),
},
attention: [
topPressures.length ? `${topPressures[0].name}超载率最高,为${formatRate(topPressures[0].overloadRate)},建议优先复核载畜量和可利用草地面积。` : "暂无明显高压乡镇,需保持常态化监测。",
"草畜平衡结果受季节牧草产量、牲畜出入栏、补饲能力影响,应定期更新核算口径。",
"接近平衡上限的区域不宜继续扩大放牧规模,避免从基本平衡转为超载。",
],
recommendations: advice.length ? advice : [
"对高压力乡镇开展轮牧、休牧和补饲组合管理。",
"将超载率、载畜量和草地面积纳入月度跟踪,及时发现压力变化。",
"结合出栏计划和饲草供应能力,动态优化畜群规模。",
],
footnotes: buildReportFootnotes(data, reason),
}
}
function buildYakReport(data, reason) {
const scope = data.context?.scope || "红原县"
const townRows = rowsOf(data, "yak.townDistribution")
.map((row) => ({
name: row.name || row.areaName || row.key,
count: toLocalNumber(row.count ?? row.value ?? row.total),
}))
.filter((row) => row.name && Number.isFinite(row.count))
.sort((a, b) => b.count - a.count)
const total = townRows.reduce((sum, row) => sum + row.count, 0)
const topRows = townRows.slice(0, 5)
const topShare = total ? topRows.reduce((sum, row) => sum + row.count, 0) / total * 100 : NaN
const structureRows = rowsOf(data, "yak.structure")
.map((row) => ({
name: row.name || row.label || row.key,
ratio: toLocalNumber(row.ratio ?? row.value),
count: toLocalNumber(row.count),
}))
.filter((row) => row.name && (Number.isFinite(row.ratio) || Number.isFinite(row.count)))
const forecastRows = rowsOf(data, "yak.forecast")
const latestForecast = forecastRows[forecastRows.length - 1] || {}
const latestStock = toLocalNumber(latestForecast.stock)
const topTown = townRows[0]
return {
title: "牦牛分布情况",
subtitle: `${scope} · 牦牛存栏专题`,
sourceText: "数据来源:平台牦牛存栏与畜群结构数据",
metrics: [
metric("牦牛存栏", formatHead(total), "乡镇汇总"),
metric("最高乡镇", topTown ? topTown.name : "--", topTown ? formatHead(topTown.count) : ""),
metric("覆盖乡镇", `${townRows.length || "--"}`, "当前展示范围"),
metric("预测存栏", formatHead(latestStock), latestForecast.label || "趋势预测"),
],
summary: [
`${scope}当前牦牛存栏约${formatHead(total)},主要分布在${topRows.map((row) => row.name).join("、") || "重点牧区"}等区域。`,
Number.isFinite(topShare)
? `前五个乡镇合计占全县存栏的${formatRate(topShare)},存栏分布具有较明显的重点区域特征。`
: "当前已完成乡镇存栏数据整理,可用于后续联动草畜平衡、出栏计划和疫病防控管理。",
],
primaryChart: {
title: "乡镇牦牛存栏分布",
note: Number.isFinite(topShare) ? `前五项占比${formatRate(topShare)}` : "",
rows: townRows.slice(0, 9).map((row) => ({
label: row.name,
value: row.count,
unit: "头",
})),
},
secondaryChart: structureRows.length ? {
title: "畜群结构占比",
rows: structureRows.slice(0, 8).map((row) => ({
label: row.name,
value: Number.isFinite(row.ratio) ? row.ratio : row.count,
unit: Number.isFinite(row.ratio) ? "%" : "头",
})),
} : null,
table: {
title: "重点乡镇存栏情况",
columns: ["乡镇", "存栏量", "全县占比"],
rows: townRows.slice(0, 6).map((row) => [
row.name,
formatHead(row.count),
total ? formatRate(row.count / total * 100) : "--",
]),
},
attention: [
topTown ? `${topTown.name}存栏量最高,约${formatHead(topTown.count)},建议同步关注草地承载和疫病防控压力。` : "暂无可排序的乡镇存栏数据。",
"存栏密集区域需要与草畜平衡结果联动研判,避免单看数量忽略草场承载能力。",
"预测存栏继续上行时,应提前校核出栏节奏、补饲能力和畜群结构。",
],
recommendations: [
"对高存栏乡镇建立动态监测清单,联动草地面积、出栏和补饲指标。",
"围绕能繁母牛、犊牛和种公牛结构优化畜群质量,避免单纯追求数量增长。",
"将存栏分布结果用于牧户服务、疫病防控和草畜平衡调控的优先级排序。",
],
footnotes: buildReportFootnotes(data, reason),
}
}
function buildGenericReport(data, reason) {
const scope = data.context?.scope || "红原县"
const metrics = normalizeLocalMetrics(data.context?.insights)
const metricRows = metrics.length ? metrics : deriveLocalMetrics(data.datasets)
const firstChart = normalizeList(data.datasets)
.map((dataset) => ({
title: dataset?.title || "指标对比",
rows: normalizeList(dataset?.rows)
.map((row, index) => ({
label: pickRowLabel(row, index),
value: pickNumericValue(row),
unit: row?.unit || dataset?.unit || "",
}))
.filter((row) => row.label && Number.isFinite(row.value))
.slice(0, 8),
}))
.find((chart) => chart.rows.length)
return {
title: buildLocalTitle(data.question),
subtitle: `${scope} · 智慧管理专题`,
sourceText: "数据来源:平台业务汇总数据",
metrics: metricRows.slice(0, 4).map((item) => metric(item.label, item.value, item.note)),
summary: [
`${scope}已完成本次问题相关业务数据整理,并形成专题分析结果。`,
"报告优先呈现关键指标、主要分布和可执行建议,原始字段明细不作为页面主体展示。",
],
primaryChart: firstChart || { title: "核心指标对比", rows: [] },
secondaryChart: null,
table: buildGenericReportTable(data),
attention: ["需结合业务口径持续复核关键指标变化。", "若涉及乡镇、牧户或生态区域,应按重点对象继续下钻。"],
recommendations: ["将本报告作为专题研判入口,后续结合业务部门口径补充结论。", "对指标波动较大的对象建立跟踪清单。"],
footnotes: buildReportFootnotes(data, reason),
}
}
function buildCapacityGapChart(rows) {
const gapRows = rows
.map((row) => ({
label: row.name,
value: Number.isFinite(row.livestockCapacity) && Number.isFinite(row.bearingCapacity)
? Math.max(0, row.livestockCapacity - row.bearingCapacity)
: NaN,
unit: "头",
}))
.filter((row) => row.label && Number.isFinite(row.value) && row.value > 0)
.slice(0, 8)
if (!gapRows.length) return null
return {
title: "超承载量估算",
note: "仅展示实际载畜量高于理论承载力的区域",
rows: gapRows,
}
}
function buildGenericReportTable(data) {
const rows = normalizeList(data.datasets)
.flatMap((dataset) => normalizeList(dataset?.rows).map((row, index) => ({
target: pickRowLabel(row, index),
value: pickNumericValue(row),
unit: row?.unit || dataset?.unit || "",
note: dataset?.title || "",
})))
.filter((row) => row.target && Number.isFinite(row.value))
.slice(0, 6)
return {
title: "重点指标明细",
columns: ["对象", "数值", "说明"],
rows: rows.map((row) => [
row.target,
`${formatLocalNumber(row.value)}${row.unit || ""}`,
row.note,
]),
}
}
function renderReportMetrics(metrics) {
const rows = normalizeList(metrics).filter(Boolean).slice(0, 4)
if (!rows.length) return ""
return `<section class="metric-grid">${rows.map((item) => `
<article class="metric-card">
<span>${escapeHtml(item.label)}</span>
<strong>${escapeHtml(item.value)}</strong>
<small>${escapeHtml(item.note || "")}</small>
</article>`).join("")}</section>`
}
function renderReportParagraphs(paragraphs) {
return normalizeList(paragraphs)
.filter(Boolean)
.map((item) => `<p>${escapeHtml(item)}</p>`)
.join("")
}
function renderReportBars(chart) {
const rows = normalizeList(chart?.rows).filter((row) => row?.label && Number.isFinite(Number(row.value))).slice(0, 9)
if (!rows.length) return '<div class="summary"><p>当前问题暂无可绘制的核心指标。</p></div>'
const maxValue = Math.max(...rows.map((row) => Math.abs(Number(row.value))), 1)
return `<div class="bar-list">
${rows.map((row) => {
const value = Number(row.value)
const width = Math.max(3, Math.round(Math.abs(value) / maxValue * 100))
return `<div class="bar-row">
<span title="${escapeHtml(row.label)}">${escapeHtml(row.label)}</span>
<div class="bar-track"><i style="width:${width}%"></i></div>
<em>${escapeHtml(formatLocalNumber(value))}${escapeHtml(row.unit || "")}</em>
</div>`
}).join("")}
${chart?.note ? `<p class="bar-note">${escapeHtml(chart.note)}</p>` : ""}
</div>`
}
function renderReportTable(table) {
const rows = normalizeList(table?.rows).slice(0, 8)
const columns = normalizeList(table?.columns)
if (!rows.length || !columns.length) return '<div class="summary"><p>暂无可展示的重点明细。</p></div>'
return `<table>
<thead><tr>${columns.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead>
<tbody>
${rows.map((row) => `<tr>${normalizeList(row).map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`).join("")}
</tbody>
</table>`
}
function renderReportList(items) {
const rows = normalizeList(items).filter(Boolean).slice(0, 6)
if (!rows.length) return '<div class="summary"><p>暂无补充事项。</p></div>'
return `<ul class="list">${rows.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
}
function metric(label, value, note = "") {
return {
label: label || "指标",
value: value === undefined || value === null || value === "" ? "--" : String(value),
note: note || "",
}
}
function rowsOf(data, id) {
return normalizeList(normalizeList(data?.datasets).find((dataset) => dataset?.id === id)?.rows)
}
function firstRow(data, id) {
return rowsOf(data, id).find(Boolean)
}
function findRows(data, predicate) {
return normalizeList(data?.datasets)
.flatMap((dataset) => normalizeList(dataset?.rows))
.filter((row) => row && typeof row === "object" && predicate(row))
}
function findFirstRow(data, predicate) {
return findRows(data, predicate).find(Boolean)
}
function rowsOfFirstDataset(data, predicate) {
const dataset = normalizeList(data?.datasets).find(predicate)
return normalizeList(dataset?.rows)
}
function splitAdvice(value) {
return uniqueText(String(value || "").split(/[;;。.\n]+/)).slice(0, 5)
}
function buildReportFootnotes(data) {
const notes = uniqueText([...(data.context?.notes || [])])
return notes.slice(0, 3)
}
function formatWanMu(value) {
const number = Number(value)
if (!Number.isFinite(number)) return "--"
return `${formatLocalNumber(number / 10000)}万亩`
}
function formatHead(value) {
const number = Number(value)
if (!Number.isFinite(number)) return "--"
return `${formatLocalNumber(number)}`
}
function formatRate(value) {
const number = Number(value)
if (!Number.isFinite(number)) return "--"
return `${formatLocalNumber(number)}%`
}
function formatRateSuffix(value) {
const text = formatRate(value)
return text === "--" ? "" : `${text}`
}
function buildArtifactDataJson({ question, answer, charts, datasets, context }) {
const payload = {
question: String(question || "").trim(),
@ -761,110 +97,6 @@ function buildArtifactDataJson({ question, answer, charts, datasets, context })
return serialized
}
function buildLocalTitle(question) {
const text = String(question || "").replace(/[??。!!]+$/g, "").trim()
return text ? `${text}专题分析` : "智慧管理专题分析"
}
function normalizeList(value) {
return Array.isArray(value) ? value : []
}
function uniqueText(values) {
const seen = new Set()
return normalizeList(values)
.map((item) => String(item || "").trim())
.filter((item) => {
if (!item || seen.has(item)) return false
seen.add(item)
return true
})
}
function normalizeLocalMetrics(insights) {
return normalizeList(insights)
.map((item) => ({
label: item?.label || item?.name || "指标",
value: item?.value ?? "--",
note: item?.note || item?.title || "",
}))
.filter((item) => item.label && item.value !== undefined && item.value !== null)
.slice(0, 6)
}
function deriveLocalMetrics(datasets = []) {
return normalizeList(datasets)
.map((dataset) => {
const row = normalizeList(dataset?.rows).find(Boolean)
if (!row || typeof row !== "object") return null
const value = pickNumericValue(row)
if (!Number.isFinite(value)) return null
return {
label: pickRowLabel(row, 0),
value: `${formatLocalNumber(value)}${row?.unit || dataset?.unit || ""}`,
note: dataset?.title || "",
}
})
.filter(Boolean)
.slice(0, 6)
}
function pickRowLabel(row, index) {
if (!row || typeof row !== "object") return `指标${index + 1}`
const direct = row.name || row.label || row.areaName || row.town || row.title || row.key
if (direct) return String(direct)
const firstText = Object.values(row).find((value) => typeof value === "string" && value.trim())
return firstText || `指标${index + 1}`
}
function pickNumericValue(row) {
if (!row || typeof row !== "object") return NaN
const preferredKeys = [
"value",
"count",
"total",
"rate",
"ratio",
"yakNumber",
"livestockCapacity",
"overloadRate",
"onlineRate",
"areaRange",
"amount",
"number",
]
for (const key of preferredKeys) {
const number = toLocalNumber(row[key])
if (Number.isFinite(number)) return number
}
for (const value of Object.values(row)) {
const number = toLocalNumber(value)
if (Number.isFinite(number)) return number
}
return NaN
}
function toLocalNumber(value) {
if (value === undefined || value === null || value === "") return NaN
const number = Number(String(value).replace(/,/g, "").replace(/%/g, ""))
return Number.isFinite(number) ? number : NaN
}
function formatLocalNumber(value) {
const number = Number(value)
if (!Number.isFinite(number)) return value || "--"
return number.toLocaleString("zh-CN", { maximumFractionDigits: 2 })
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
}
function sanitizeContext(context) {
if (!context || typeof context !== "object") return null
const {

@ -1,35 +1,44 @@
import { getSmartChartsForQuestion } from "./smartChartApi"
import {
getYakForecastRows as fetchYakForecastRows,
getYakStructureRows as fetchYakStructureRows,
getYakTownRows as fetchYakTownRows,
getYakTradeRows as fetchYakTradeRows,
} from "./yakApi"
import {
businessMatrixRows,
centerOrbitNodes,
dataFlowRows,
homeHeroStats,
homeTopStats,
resourceCompositionRows,
supportRows,
townSynergyRows,
tradeRows,
yakTownRows as homeYakTownRows,
} from "@/views/home/data"
import {
forecastFactorRows,
forecastPeriods,
genderAgeRows,
structureRows,
townRows as yakTownRows,
tradeImpactRows as yakTradeRows,
} from "@/views/yakManagement/data"
import {
pastureAgeRows,
pastureCoreRows,
pastureGenderRows,
pastureGrasslandRows,
pastureListRows,
pastureMapRows,
pastureOnlineRows,
pastureSeasonRows,
pastureStructureRows,
pastureTopStats,
pastureTownRows,
} from "@/views/pastureManagement/data"
import {
BALANCE_CALCULATION_PARAMS,
balanceFallbackRows,
balanceTownRows,
countyBalanceRow,
} from "@/views/grassLivestockBalance/data"
import {
engineeringAnalysisPanels,
engineeringAnalysisProjectFeatures,
engineeringAnalysisTopStats,
parkCoreRows,
parkPanels,
@ -39,14 +48,20 @@ import {
restorationTopStats,
} from "@/views/ecologicalProtection/data"
import {
breedingLevelRows,
forageSecurityRows,
industryChainListRows,
industryFinancialRows,
industryOverviewRows,
industryPointRows,
industryStageRows,
industryTopStats,
industryValueRows,
processingPointRows,
processingAbilityRows,
serviceCapabilityRows,
stageCompositionRows,
supportPointRows,
} from "@/views/yakIndustryChain/data"
const PAGE_NAME = "智慧管理"
@ -99,7 +114,8 @@ export async function buildSmartQuestionContext(question, options = {}) {
const datasetResults = await Promise.all(selectedIntents.map((intent) => intent.build(normalizedQuestion, warnings)))
const datasets = uniqueDatasets(datasetResults.flat())
const charts = await chartsPromise
const mergedDatasets = datasets.length ? datasets : buildOverviewDatasets()
const isOverviewIntent = selectedIntents.length === 1 && selectedIntents[0]?.key === "overview"
const mergedDatasets = datasets.length ? datasets : (isOverviewIntent ? buildOverviewDatasets() : [])
const scope = inferScope(normalizedQuestion, mergedDatasets)
const context = {
question: normalizedQuestion,
@ -158,28 +174,112 @@ function buildOverviewDatasets() {
dataset("home.heroStats", "资源与草畜概览", homeHeroStats, "平台业务汇总数据"),
dataset("home.resourceComposition", "资源构成", resourceCompositionRows, "平台业务汇总数据"),
dataset("home.businessMatrix", "业务矩阵", businessMatrixRows, "平台业务汇总数据"),
dataset("home.townSynergy", "乡镇林草湿与牦牛协同", townSynergyRows, "平台业务汇总数据"),
dataset("home.yakTown", "首页牦牛乡镇排行", homeYakTownRows, "平台业务汇总数据"),
dataset("home.trade", "首页交易结构", tradeRows, "平台业务汇总数据"),
dataset("home.support", "首页支撑产业", supportRows, "平台业务汇总数据"),
dataset("home.dataFlow", "数据流汇总", dataFlowRows, "平台业务汇总数据"),
dataset("home.centerOrbit", "首页中心节点", centerOrbitNodes, "平台业务汇总数据"),
]
}
function buildYakDatasets() {
async function buildYakDatasets(question, warnings = []) {
const [townResult, structureResult, forecastResult, tradeResult] = await Promise.allSettled([
fetchYakTownRows(),
fetchYakStructureRows(),
fetchYakForecastRows(),
fetchYakTradeRows(),
])
appendDatasetWarning(townResult, warnings, "牦牛乡镇存栏数据暂未取得")
appendDatasetWarning(structureResult, warnings, "牦牛畜群结构数据暂未取得")
appendDatasetWarning(forecastResult, warnings, "牦牛预测数据暂未取得")
appendDatasetWarning(tradeResult, warnings, "牦牛交易数据暂未取得")
const townRows = normalizeActualYakTownRows(getSettledRows(townResult))
const structureRows = normalizeActualYakMetricRows(getSettledRows(structureResult))
const forecastRows = normalizeActualYakForecastRows(getSettledRows(forecastResult))
const tradeRows = normalizeActualYakMetricRows(getSettledRows(tradeResult))
return [
dataset("yak.townDistribution", "乡镇牦牛存栏分布", yakTownRows.length ? yakTownRows : homeYakTownRows, "平台业务汇总数据"),
dataset("yak.structure", "畜群结构", structureRows, "平台业务汇总数据"),
dataset("yak.genderAge", "年龄性别结构", genderAgeRows, "平台业务汇总数据"),
dataset("yak.forecast", "牦牛存栏预测", forecastPeriods, "平台业务汇总数据"),
dataset("yak.forecastFactors", "预测因素", forecastFactorRows, "平台业务汇总数据"),
dataset("yak.tradeImpact", "交易影响", yakTradeRows, "平台业务汇总数据"),
dataset("yak.town", "乡镇牦牛存栏分布", townRows, "牦牛管理接口数据"),
dataset("yak.structureRows", "畜群结构数量", structureRows, "牦牛管理接口数据"),
dataset("yak.forecastRows", "牦牛月度预测", forecastRows, "牦牛管理接口数据"),
dataset("yak.tradeImpact", "交易影响", tradeRows, "牦牛交易接口数据"),
]
}
function getSettledRows(result) {
return result.status === "fulfilled" && Array.isArray(result.value) ? result.value : []
}
function appendDatasetWarning(result, warnings, fallback) {
if (result.status === "rejected") {
warnings.push(getFriendlyContextError(result.reason, fallback))
}
}
function normalizeActualYakTownRows(rows = []) {
return rows
.map((row, index) => {
const name = String(row.name || row.areaName || row.area_name || row.townName || row.zoneName || "").trim()
const count = toDatasetNumber(row.count ?? row.value ?? row.number ?? row.yakCount ?? row.yak_count)
return {
key: row.key || row.id || row.areaCode || row.area_code || `${name || "yak-town"}-${index}`,
name,
count,
value: count,
unit: row.unit || "头",
}
})
.filter((row) => row.name && Number.isFinite(row.count))
.sort((a, b) => b.count - a.count)
}
function normalizeActualYakMetricRows(rows = []) {
return rows
.map((row, index) => {
const name = String(row.name || row.label || row.type || row.key || "").trim()
const count = toDatasetNumber(row.count ?? row.value ?? row.number ?? row.yakCount ?? row.headCount ?? row.amount)
return {
key: row.key || row.id || `${name || "yak-metric"}-${index}`,
name,
count,
value: count,
unit: row.unit || "头",
extra: row.extra || "",
}
})
.filter((row) => row.name && Number.isFinite(row.count))
.sort((a, b) => b.count - a.count)
}
function normalizeActualYakForecastRows(rows = []) {
return rows
.map((row, index) => {
const name = String(row.name || row.label || row.month || row.date || row.time || "").trim()
const stock = toDatasetNumber(row.stock ?? row.count ?? row.value ?? row.number ?? row.yakCount)
return {
key: row.key || row.id || `${name || "yak-forecast"}-${index}`,
name,
label: name,
stock,
count: stock,
value: stock,
unit: row.unit || "头",
extra: row.extra || "",
}
})
.filter((row) => row.name && Number.isFinite(row.stock))
}
function buildBalanceDatasets(question) {
const focusRow = findBalanceContextRow(question)
const focusName = focusRow?.name || "红原县"
return [
dataset("balance.target", `${focusName}草畜平衡`, [focusRow], "平台业务汇总数据"),
dataset("balance.county", "全县草畜平衡", [countyBalanceRow], "平台业务汇总数据"),
dataset("balance.townRows", "乡镇草畜平衡", balanceTownRows, "平台业务汇总数据"),
dataset("balance.town", "乡镇草畜平衡", balanceTownRows, "平台业务汇总数据"),
dataset("balance.fallbackRows", "草畜平衡兜底数据", balanceFallbackRows, "平台业务汇总数据"),
dataset("balance.params", "草畜平衡计算参数", [BALANCE_CALCULATION_PARAMS], "平台业务汇总数据"),
]
}
@ -187,11 +287,16 @@ function buildBalanceDatasets(question) {
function buildPastureDatasets() {
return [
dataset("pasture.core", "牧户核心指标", pastureCoreRows, "平台业务汇总数据"),
dataset("pasture.townDistribution", "牧户乡镇分布", pastureTownRows, "平台业务汇总数据"),
dataset("pasture.topStats", "牧户顶部指标", pastureTopStats, "平台业务汇总数据"),
dataset("pasture.town", "牧户乡镇分布", pastureTownRows, "平台业务汇总数据"),
dataset("pasture.online", "牧户在线率", pastureOnlineRows, "平台业务汇总数据"),
dataset("pasture.structure", "牧户年龄性别结构", pastureStructureRows, "平台业务汇总数据"),
dataset("pasture.age", "牧户年龄结构", pastureAgeRows, "平台业务汇总数据"),
dataset("pasture.gender", "牧户性别结构", pastureGenderRows, "平台业务汇总数据"),
dataset("pasture.examples", "牧户示例列表", pastureListRows, "平台业务汇总数据"),
dataset("pasture.list", "牧户示例列表", pastureListRows, "平台业务汇总数据"),
dataset("pasture.grassland", "牧户草场资源", pastureGrasslandRows, "平台业务汇总数据"),
dataset("pasture.season", "牧户季节草场", pastureSeasonRows, "平台业务汇总数据"),
dataset("pasture.map", "牧户地图统计", pastureMapRows, "平台业务汇总数据"),
]
}
@ -202,8 +307,10 @@ function buildEcologyDatasets() {
dataset("ecology.parkTownRows", "国家公园乡镇数据", parkTownRows, "平台业务汇总数据"),
dataset("ecology.restorationTopStats", "生态修复项目汇总", restorationTopStats, "平台业务汇总数据"),
dataset("ecology.engineeringTopStats", "生态工程分析指标", engineeringAnalysisTopStats, "平台业务汇总数据"),
dataset("ecology.engineeringProjectFeatures", "工程分析项目点", engineeringAnalysisProjectFeatures, "平台业务汇总数据"),
dataset("ecology.parkPanels", "生态保护图表数据", flattenPanelRows(parkPanels), "平台业务汇总数据"),
dataset("ecology.restorationPanels", "生态修复图表数据", flattenPanelRows(restorationPanels), "平台业务汇总数据"),
dataset("ecology.engineeringAnalysisPanels", "工程分析图表数据", flattenPanelRows(engineeringAnalysisPanels), "平台业务汇总数据"),
dataset("home.resourceComposition", "林草湿资源构成", resourceCompositionRows, "平台业务汇总数据"),
dataset("home.townSynergy", "乡镇林草湿与牦牛协同", townSynergyRows, "平台业务汇总数据"),
]
@ -215,10 +322,16 @@ function buildIndustryDatasets() {
dataset("industry.overview", "产业链总览", industryOverviewRows, "平台业务汇总数据"),
dataset("industry.stageComposition", "产业阶段构成", stageCompositionRows, "平台业务汇总数据"),
dataset("industry.stageRows", "产业阶段说明", industryStageRows, "平台业务汇总数据"),
dataset("industry.breedingLevel", "养殖水平", breedingLevelRows, "平台业务汇总数据"),
dataset("industry.forageSecurity", "饲草保障能力", forageSecurityRows, "平台业务汇总数据"),
dataset("industry.value", "产业价值指标", industryValueRows, "平台业务汇总数据"),
dataset("industry.processingAbility", "加工能力指标", processingAbilityRows, "平台业务汇总数据"),
dataset("industry.serviceCapability", "服务能力指标", serviceCapabilityRows, "平台业务汇总数据"),
dataset("industry.financial", "金融服务", industryFinancialRows, "平台业务汇总数据"),
dataset("industry.supportPoints", "饲草储备点位", supportPointRows, "平台业务汇总数据"),
dataset("industry.processingPoints", "加工屠宰点位", processingPointRows, "平台业务汇总数据"),
dataset("industry.points", "产业链点位", industryPointRows, "平台业务汇总数据"),
dataset("industry.list", "产业链主体列表", industryChainListRows, "平台业务汇总数据"),
]
}
@ -369,6 +482,12 @@ function getFriendlyContextError(error, fallback) {
return fallback
}
function toDatasetNumber(value) {
if (value === undefined || value === null || value === "") return NaN
const number = Number(String(value).replace(/,/g, "").replace(/%/g, ""))
return Number.isFinite(number) ? number : NaN
}
function formatNumber(value) {
const number = Number(String(value).replace(/,/g, ""))
if (!Number.isFinite(number)) return value || "--"

@ -217,7 +217,7 @@
<script setup>
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import { buildSmartContextAnswer, buildSmartQuestionContext } from "@/services/smartContextBuilder"
import { buildLocalSmartArtifact, generateSmartArtifact } from "@/services/smartArtifactApi"
import { generateSmartArtifact } from "@/services/smartArtifactApi"
import { secureArtifactHtml } from "./artifactRuntime"
const SmartChartRenderer = defineAsyncComponent({
@ -246,7 +246,7 @@ const artifactGenerating = computed(() => ["collecting", "generating"].includes(
const artifactStatusLabel = computed(() => {
if (artifact.value.status === "collecting") return "汇总数据"
if (artifact.value.status === "generating") return "生成页面"
if (artifact.value.status === "ready") return "分析完成"
if (artifact.value.status === "ready") return "DeepSeek生成"
if (artifact.value.status === "error") return "生成失败"
if (artifact.value.status === "waiting") return "等待选择"
return "待生成"
@ -255,7 +255,6 @@ const voiceSupported = ref(false)
const voiceListening = ref(false)
const voiceError = ref("")
const welcomeText = "请输入需要了解的业务事项,系统将依据平台汇总数据形成要点分析和专题页面。"
const ARTIFACT_SOFT_TIMEOUT = 28000
let streamTimer = null
let speechRecognition = null
let artifactRequestSerial = 0
@ -299,7 +298,6 @@ function createEmptyArtifact() {
html: "",
error: "",
model: "",
fallback: false,
renderKey: 0,
}
}
@ -340,10 +338,9 @@ async function launchArtifactGeneration(payload) {
html: "",
error: "",
model: "",
fallback: false,
}
try {
const result = await withArtifactSoftTimeout(generateSmartArtifact(normalizedPayload))
const result = await generateSmartArtifact(normalizedPayload)
if (requestId !== artifactRequestSerial) return { stale: true }
const securedHtml = secureArtifactHtml(result.html)
if (!securedHtml) throw new Error("生成页面未通过安全检查")
@ -353,62 +350,25 @@ async function launchArtifactGeneration(payload) {
html: securedHtml,
error: "",
model: result.model || "DeepSeek",
fallback: false,
renderKey: Date.now(),
}
return { ok: true, result }
} catch (error) {
if (requestId !== artifactRequestSerial) return { stale: true }
const errorMessage = getFriendlyErrorMessage(error, "专题分析页面生成暂时不可用,请稍后重试。")
const errorMessage = getFriendlyErrorMessage(error, "DeepSeek专题分析生成失败,请稍后重新生成。")
console.error("[智慧管理] DeepSeek HTML生成失败", error)
const fallbackHtml = buildLocalArtifactHtml(normalizedPayload, errorMessage)
if (fallbackHtml) {
artifact.value = {
...artifact.value,
status: "ready",
html: fallbackHtml,
error: errorMessage,
model: "本地兜底",
fallback: true,
renderKey: Date.now(),
}
return { ok: false, fallback: true, message: errorMessage, error }
}
artifact.value = {
...artifact.value,
status: "error",
html: "",
error: errorMessage,
model: "",
fallback: false,
renderKey: Date.now(),
}
return { ok: false, message: errorMessage, error }
}
}
function withArtifactSoftTimeout(promise) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
const error = new Error("专题分析页面生成服务响应时间较长,已切换本地分析页面")
error.code = "ARTIFACT_SOFT_TIMEOUT"
reject(error)
}, ARTIFACT_SOFT_TIMEOUT)
promise.then(resolve, reject).finally(() => window.clearTimeout(timer))
})
}
function buildLocalArtifactHtml(payload, reason) {
try {
const html = buildLocalSmartArtifact({ ...payload, reason })
return secureArtifactHtml(html)
} catch (error) {
console.error("[智慧管理] 本地专题页面生成失败", error)
return ""
}
}
function retryArtifactGeneration() {
if (!lastArtifactPayload.value || artifactGenerating.value) return
launchArtifactGeneration(lastArtifactPayload.value)
@ -421,7 +381,6 @@ function failArtifactPreparation(message) {
status: "error",
html: "",
error: message,
fallback: false,
renderKey: Date.now(),
}
}
@ -439,7 +398,6 @@ function handleArtifactMessage(event) {
status: "error",
html: "",
error: event.data.message || "生成界面运行异常",
fallback: false,
renderKey: Date.now(),
}
}

Loading…
Cancel
Save