- models.rs:新增 spot_check_count 字段(默认 5) - db.rs:INSERT/SELECT 添加 spotcheckcount 列 - migrations/007:ALTER TABLE weather_data ADD spotcheckcount - afterbody.js:使用 data.spotCheckCount 动态渲染抽测数据
400 lines
15 KiB
JavaScript
400 lines
15 KiB
JavaScript
const WeatherReport = (function () {
|
||
const BASE_WIND_COUNT = 10;
|
||
|
||
const AREA_TYPE_MAP = {
|
||
urban: "城市",
|
||
rural: "农村",
|
||
};
|
||
|
||
const SUITABILITY_MAP = {
|
||
a: "不利于污染物的扩散和稀释,适宜于进行无组织排放监测",
|
||
b: "较不利于污染物的扩散和稀释,较适宜于进行无组织排放监测",
|
||
c: "有利于污染物的扩散和稀释,较不适宜于进行无组织排放监测",
|
||
d: "很有利于污染物的扩散和稀释,不适宜于进行无组织排放监测",
|
||
wind0: "静风",
|
||
overall0: "取消监测或改期",
|
||
};
|
||
|
||
function formatDate(dateStr) {
|
||
const d = new Date(dateStr);
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
function getSuitabilityText(value, type) {
|
||
const normalized = String(value).toLowerCase();
|
||
if (type === "wind" && normalized === "0") return SUITABILITY_MAP.wind0;
|
||
if (type === "overall" && normalized === "0") return SUITABILITY_MAP.overall0;
|
||
return SUITABILITY_MAP[normalized] || value;
|
||
}
|
||
|
||
function formatTime(hours, min) {
|
||
return `${hours}:${String(min).padStart(2, "0")}`;
|
||
}
|
||
|
||
function joinWithComma(arr, start, end) {
|
||
return arr.slice(start, end).map((v) => v ?? "").join(",");
|
||
}
|
||
|
||
function buildReportHTML(data) {
|
||
const staticPrefix = window.staticPrefix || '/static';
|
||
const timeStr = `${formatDate(data.date)} ${formatTime(data.hours, data.min)}`;
|
||
const version = data.version || "unknown";
|
||
|
||
let html = `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>总体适宜度测量报表</title>
|
||
<link rel="stylesheet" href="${staticPrefix}/css/report.css">
|
||
</head>
|
||
<body>
|
||
<div class="report-container">
|
||
<h1>总体适宜度测量报表</h1>
|
||
|
||
<div class="two-column">
|
||
<div class="field full-width">
|
||
<div class="field-label">项目名称:</div>
|
||
<div class="field-value">${data.title || "-"}</div>
|
||
</div>
|
||
<div class="field full-width">
|
||
<div class="field-label">测试时间:</div>
|
||
<div class="field-value">${timeStr}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">检测类型:</div>
|
||
<div class="field-value">${data.inspectionType || "-"}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">委托单号:</div>
|
||
<div class="field-value">${data.assignmentNumber || "-"}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">经度:</div>
|
||
<div class="field-value">E ${data.longitude || "-"}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">纬度:</div>
|
||
<div class="field-value">N ${data.latitude || "-"}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="two-column">
|
||
<div class="field">
|
||
<div class="field-label">日期序数:</div>
|
||
<div class="field-value">${data.daySinceJanFirst}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">太阳倾角:</div>
|
||
<div class="field-value">${Number(data.solarDeclination).toFixed(2)}°</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">太阳高度角:</div>
|
||
<div class="field-value">${Number(data.sunAltitude).toFixed(2)}°</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">总云量:</div>
|
||
<div class="field-value">${data.overallCloudiness}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">低云量:</div>
|
||
<div class="field-value">${data.lowCloudiness}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">辐射等级:</div>
|
||
<div class="field-value">${data.solarRadiationLevel}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">平均风速:</div>
|
||
<div class="field-value">${data.averageWindSpeed} m/s</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">风速适宜度:</div>
|
||
<div class="field-value">${data.windSpeedSuitability}</div>
|
||
</div>`;
|
||
|
||
if (data.hasMeasuredWindSpeed === true) {
|
||
html += `
|
||
<div class="field">
|
||
<div class="field-label">实测风速:</div>
|
||
<div class="field-value">${data.measuredWindSpeed} m/s</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">转换风速:</div>
|
||
<div class="field-value">${data.convertedWindSpeed} m/s</div>
|
||
</div>`;
|
||
} else {
|
||
html += `
|
||
<div class="field">
|
||
<div class="field-label">区域类型:</div>
|
||
<div class="field-value">${AREA_TYPE_MAP[data.areaType] || data.areaType || "-"}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">测点高度:</div>
|
||
<div class="field-value">${data.pointHeight} m</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">点位风速:</div>
|
||
<div class="field-value">${data.pointWindSpeed} m/s</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">推算风速:</div>
|
||
<div class="field-value">${data.calculatedWindSpeed} m/s</div>
|
||
</div>`;
|
||
}
|
||
|
||
html += `
|
||
<div class="field">
|
||
<div class="field-label">平均风向:</div>
|
||
<div class="field-value">${data.averageWindDirection}°</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">风向标准差:</div>
|
||
<div class="field-value">${data.windDirectionStandardDeviation}°</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">风向适宜度:</div>
|
||
<div class="field-value">${data.windDirectionSuitability}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">大气稳定度:</div>
|
||
<div class="field-value">${data.atmosphericStability}</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">稳定度适宜度:</div>
|
||
<div class="field-value">${data.suitabilityDegree}</div>
|
||
</div>
|
||
<div class="field full-width">
|
||
<div class="field-label">总体适宜度:</div>
|
||
<div class="field-value">${getSuitabilityText(data.overallSuitability, "overall")}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="two-column">
|
||
<div class="field full-width">
|
||
<div class="field-label">十次风速值:</div>
|
||
<div class="field-value">
|
||
<div class="ten-values">${joinWithComma(data.windSpeed, 0, BASE_WIND_COUNT)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="field full-width">
|
||
<div class="field-label">十次风向值:</div>
|
||
<div class="field-value">
|
||
<div class="ten-values">${joinWithComma(data.windDirection, 0, BASE_WIND_COUNT)}</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
const spotCount = data.spotCheckCount || 5;
|
||
if (data.hasSpotCheckWindSpeed && spotCount > 0) {
|
||
html += `
|
||
<div class="two-column">
|
||
<div class="field full-width">
|
||
<div class="field-label">抽测风速值:</div>
|
||
<div class="field-value">
|
||
<div class="ten-values">${joinWithComma(data.windSpeed, BASE_WIND_COUNT, BASE_WIND_COUNT + spotCount)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="field full-width">
|
||
<div class="field-label">抽测风向值:</div>
|
||
<div class="field-value">
|
||
<div class="ten-values">${joinWithComma(data.windDirection, BASE_WIND_COUNT, BASE_WIND_COUNT + spotCount)}</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
html += `
|
||
<div class="divider"></div>
|
||
<div class="footer">----报表结束 version:${version}----</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
return html;
|
||
}
|
||
|
||
function downloadPDF(htmlContent, filename) {
|
||
const options = {
|
||
margin: 10,
|
||
filename: filename,
|
||
image: { type: "jpeg", quality: 0.99 },
|
||
html2canvas: { scale: 2 },
|
||
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
|
||
};
|
||
html2pdf().set(options).from(htmlContent).save();
|
||
}
|
||
|
||
// 下载状态管理(供重试使用)
|
||
let downloadState = {
|
||
ready: false,
|
||
html: null,
|
||
filename: null,
|
||
};
|
||
|
||
// 下载提示浮层
|
||
function showDownloadPrompt(type, message) {
|
||
let overlay = document.getElementById("download-overlay");
|
||
if (!overlay) {
|
||
overlay = document.createElement("div");
|
||
overlay.id = "download-overlay";
|
||
overlay.innerHTML = `
|
||
<style>
|
||
#download-overlay {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0;
|
||
z-index: 9999;
|
||
padding: 16px;
|
||
text-align: center;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
transition: opacity 0.3s;
|
||
pointer-events: none;
|
||
}
|
||
#download-overlay .msg {
|
||
display: inline-block;
|
||
background: rgba(0,0,0,0.75);
|
||
color: #fff;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
}
|
||
#download-overlay .retry-btn {
|
||
display: inline-block;
|
||
margin-left: 12px;
|
||
background: #1677ff;
|
||
color: #fff;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
pointer-events: auto;
|
||
}
|
||
</style>
|
||
<span class="msg" id="dl-msg"></span>
|
||
<span class="retry-btn" id="dl-retry" style="display:none" onclick="WeatherReport.retryDownload()">重新下载</span>
|
||
`;
|
||
document.body.appendChild(overlay);
|
||
}
|
||
const msgEl = document.getElementById("dl-msg");
|
||
const retryEl = document.getElementById("dl-retry");
|
||
if (msgEl) msgEl.textContent = message;
|
||
if (retryEl) {
|
||
if (type === "loading" || type === "success") {
|
||
retryEl.style.display = "none";
|
||
} else {
|
||
retryEl.style.display = "inline-block";
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideDownloadPrompt() {
|
||
const overlay = document.getElementById("download-overlay");
|
||
if (overlay) {
|
||
overlay.style.opacity = "0";
|
||
setTimeout(() => { if (overlay) overlay.remove(); }, 300);
|
||
}
|
||
}
|
||
|
||
function retryDownload() {
|
||
if (!downloadState.ready || !downloadState.html || !downloadState.filename) {
|
||
showDownloadPrompt("error", "数据未准备好,请刷新页面重试");
|
||
return;
|
||
}
|
||
downloadPDF(downloadState.html, downloadState.filename);
|
||
showDownloadPrompt("success", "正在重新下载,请稍候...");
|
||
}
|
||
|
||
function init() {
|
||
if (typeof window.weatherData === "undefined") {
|
||
console.error("错误:window.weatherData 未定义");
|
||
showDownloadPrompt("error", "数据加载失败,请刷新页面重试");
|
||
return;
|
||
}
|
||
|
||
const data = window.weatherData;
|
||
console.log("【调试】从后端获取的 weatherData:", data);
|
||
|
||
const versionEl = document.querySelector(".version");
|
||
if (versionEl) {
|
||
versionEl.textContent = `版本号: ${data.version || "default"}`;
|
||
}
|
||
|
||
const html = buildReportHTML(data);
|
||
const filename = `${data.id}-${data.version || "unknown"}.pdf`;
|
||
|
||
// 保存下载状态,供重试使用
|
||
downloadState.ready = true;
|
||
downloadState.html = html;
|
||
downloadState.filename = filename;
|
||
|
||
// 显示下载中提示
|
||
showDownloadPrompt("loading", "正在生成 PDF,请稍候...");
|
||
|
||
// 延迟执行下载,让用户看到提示
|
||
setTimeout(() => {
|
||
downloadPDF(html, filename);
|
||
// 显示成功提示,3秒后消失
|
||
showDownloadPrompt("success", "PDF 开始下载...");
|
||
setTimeout(() => hideDownloadPrompt(), 3000);
|
||
// 额外等3秒,如果提示已消失(下载成功)则不再显示重试
|
||
// 如果提示还在(下载失败),则替换为重试按钮
|
||
setTimeout(() => {
|
||
const overlay = document.getElementById("download-overlay");
|
||
if (overlay) {
|
||
showDownloadPrompt("retry", "下载未开始,请点击重试");
|
||
}
|
||
}, 3300);
|
||
}, 300);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
if (typeof renderMathInElement === "function") {
|
||
renderMathInElement(document.body, {
|
||
delimiters: [
|
||
{ left: "$$", right: "$$", display: true },
|
||
{ left: "$", right: "$", display: false },
|
||
],
|
||
throwOnError: false,
|
||
});
|
||
}
|
||
init();
|
||
});
|
||
|
||
function toggleFormula(element) {
|
||
const content = element.nextElementSibling;
|
||
const isHidden = content.style.display === "none";
|
||
content.style.display = isHidden ? "block" : "none";
|
||
element.textContent = element.textContent.includes("▶")
|
||
? element.textContent.replace("▶", "▼")
|
||
: element.textContent.replace("▼", "▶");
|
||
}
|
||
|
||
function showNotification(message, type = "success") {
|
||
const notification = document.getElementById("notification");
|
||
if (!notification) return;
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type}`;
|
||
notification.style.display = "block";
|
||
setTimeout(() => {
|
||
notification.style.display = "none";
|
||
}, 3000);
|
||
}
|
||
|
||
return {
|
||
buildReportHTML,
|
||
downloadPDF,
|
||
init,
|
||
toggleFormula,
|
||
showNotification,
|
||
retryDownload,
|
||
};
|
||
})();
|