/ ===== 成果导出:CSV / 报告 / 存档 / 截图 ===== /
window.Exporter = (function () {
function download(filename, content, mime) {
const blob = content instanceof Blob ? content : new Blob([content], {type:mime || "text/plain;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 3000);}
const cell = v => /[",\n]/.test(String(v)) ? '"' + String(v).replace(/"/g, '""') + '"' : String(v);
const toCsv = rows => "\uFEFF" + rows.map(r => r.map(cell).join(",")).join("\r\n");
const stamp = () => new Date().toLocaleDateString("zh-CN").replace(///g, "-");
const projName = () => ($("#projName").value.trim() || "未命名项目");
const SRC_LABEL = {book:"导入清单", manual:"手工填写", ref:"内置经验估值(无来源)", "":"未填写"};
/ ---------- 苗木表 ---------- /
function plantCsv() {
const d = Calc.summary();
const rows = [["苗木配置表 —— " + projName(), "", "", "", "", "", "", "", "", "", ""],
["单价声明:本表单价由使用者填写或导入;标注「内置经验估值」的行无定额与造价信息价来源,不得用于报价、投标、结算。"],
[],
["序号","品种","拉丁名","类型","规格","冠幅(m)","常绿/落叶","乡土","计量单位",
"数量/面积","密度(株/m²)","损耗(%)","含损耗数量","单价(元)","单价来源","小计(元)"]];
S.plants.forEach((r, i) => {
PlantUI.derive(r);
rows.push([i + 1, r.name, r.latin, r.type, r.spec, r.cw, r.ev ? "常绿" : "落叶", r.na ? "乡土" : "引种",
r.unit, r.qty, r.unit === "m²" ? r.density : "", r.loss,
fmt(r._final, r._unitLabel === "m²" ? 1 : 0) + r._unitLabel,
r.price > 0 ? r.price : "", SRC_LABEL[r.priceSrc || ""], r.price > 0 ? Math.round(r._sub) : ""]);
});
rows.push([]);
rows.push(["合计", "", "", "", "", "", "", "", "", "", "", "", "", "", "", Math.round(d.st.cost)]);
if (d.st.missing) rows.push(["注意:有 " + d.st.missing + " 行未填单价,合计金额不完整。"]);
download(projName() + "_苗木表_" + stamp() + ".csv", toCsv(rows), "text/csv;charset=utf-8");
toast("苗木表已导出");}
/ ---------- 用地平衡表 ---------- /
function landCsv() {
const d = Calc.summary();
const rows = [["用地平衡表 —— " + projName()], [],
["用地类型", "面积(m²)", "占比(%)", "性质"]];
LAND_CATS.forEach(c => {
const a = +S.land[c.k] || 0;
rows.push([c.n, a.toFixed(2), d.landTotal > 0 ? (a / d.landTotal * 100).toFixed(2) : "", c.nature]);
});
rows.push(["合计", d.landTotal.toFixed(2), "100.00", ""]);
rows.push([]);
rows.push(["总用地面积(m²)", d.siteArea.toFixed(2)]);
rows.push(["绿地面积(m²)", d.greenArea.toFixed(2)]);
rows.push(["绿地率(%)", d.greenRate.toFixed(2)]);
rows.push(["绿化覆盖率(%)", d.coverRate.toFixed(2)]);
rows.push([]);
rows.push(["量算图元明细"]);
rows.push(["序号", "类型", "用地类型", "面积(m²)", "周长/长度(m)"]);
S.shapes.forEach((s, i) => {
rows.push([i + 1, s.kind === "poly" ? "面" : "线", s.kind === "poly" ? catByKey(s.cat).n : "长度测量",
s.area ? s.area.toFixed(2) : "", s.len ? s.len.toFixed(2) : ""]);
});
download(projName() + "_用地平衡表_" + stamp() + ".csv", toCsv(rows), "text/csv;charset=utf-8");
toast("用地平衡表已导出");}
/ ---------- 造价概算表 ---------- /
function costCsv() {
const d = Calc.summary();
const r = Calc.costRows(d);
const rows = [["景观工程造价估算表 —— " + projName()],
["声明:本表为方案阶段快速估算。综合单价与系数为经验估值占位,无定额来源;苗木单价由使用者录入。不得作为报价、投标或结算依据。"],
[],
["费用项", "工程量", "金额(元)", "占比(%)"]];
r.rows.forEach(x => rows.push([x[0], x[1], Math.round(x[2]), r.total > 0 ? (x[2] / r.total * 100).toFixed(2) : ""]));
rows.push(["工程总造价", "", Math.round(r.total), "100.00"]);
rows.push(["单位面积造价(元/m²)", fmt(d.siteArea, 0) + " m²", r.perSqm.toFixed(2), ""]);
rows.push([]);
rows.push(["单价参数", "取值"]);
[["种植施工费系数", S.cost.planting], ["一年养护费系数", S.cost.maint],
["绿地整理(元/m²)", S.cost.soil], ["铺装(元/m²)", S.cost.pave], ["园路(元/m²)", S.cost.road],
["水体(元/m²)", S.cost.water], ["水电灌溉(元/m²)", S.cost.mep], ["小品(元/m²)", S.cost.furn],
["不可预见费系数", S.cost.risk]].forEach(x => rows.push(x));
if (d.st.missing) rows.push(["注意", "有 " + d.st.missing + " 行苗木未填单价,苗木费被低估"]);
download(projName() + "_造价估算表_" + stamp() + ".csv", toCsv(rows), "text/csv;charset=utf-8");
toast("造价估算表已导出");}
/ ---------- 技术经济指标报告 ---------- /
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, c =>
({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));}
function report() {
const d = recalcAll();
const checks = Calc.buildChecks(d);
const cost = Calc.costRows(d);
const refUsed = S.plants.some(p => p.priceSrc === "ref");
const imported = Object.keys(S.priceBook || {}).length > 0;
let priceNote;
if (d.st.cost <= 0) priceNote = "本报告<b>未包含有效苗木单价</b>(尚未录入),造价部分不成立,仅工程量与指标可用。";
else if (d.st.missing > 0) priceNote = "有 <b>" + d.st.missing + " 行</b>苗木未填单价,苗木费及总造价被低估,<b>不可对外使用</b>。";
else if (refUsed) priceNote = "部分单价采用工具内置<b>经验估值(无定额或造价信息价来源)</b>,须替换为当地信息价后方可对外。";
else priceNote = "苗木单价来源:" + (imported ? "使用者导入的价格清单及手工填写" : "使用者手工填写") + "。请自行确认价格口径(含税 / 运输 / 种植)。";
const kpiRows = [
["总用地面积", fmt(d.siteArea, 2), "m²"], ["绿地面积", fmt(d.greenArea, 2), "m²"],
["绿地率", fmt(d.greenRate, 2), "%"], ["绿化覆盖率", fmt(d.coverRate, 2), "%"],
["硬质铺装面积", fmt(d.paveArea, 2), "m²"], ["水体面积", fmt(d.waterArea, 2), "m²"],
["建筑构筑物占地", fmt(d.buildArea, 2), "m²"],
["乔木数量", fmt0(d.st.tree), "株"], ["乔木密度", fmt(d.treeDen, 2), "株/100m²"],
["灌木数量", fmt0(d.st.shrubQty), "株"], ["地被 / 草坪面积", fmt(d.st.groundArea, 2), "m²"],
["三维绿量", fmt(d.st.gv, 0), "m³"], ["单位绿地绿量", fmt(d.gvPerSqm, 2), "m³/m²"],
["乡土植物比例", fmt(d.nativeRate, 1), "%"], ["常绿乔木占比", fmt(d.everRatio, 1), "%"],
["草坪占绿地比例", fmt(d.lawnRatio, 1), "%"], ["观赏月份覆盖", d.bloomMonths, "月"]
];
if (d.m.population > 0) kpiRows.push(["人均绿地面积", fmt(d.perCapita, 2), "m²/人"]);
const tr = (cells, tag) => "<tr>" + cells.map(c => "<" + (tag || "td") + ">" + c + "</" + (tag || "td") + ">").join("") + "</tr>";
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">${esc(projName())}
本报告全部阈值为常用参考值,须以项目所在地现行规范、规划条件及审批要求为准。