feat: add market opportunity visualization template

master
陈沅 4 days ago
parent e1fc632dd4
commit 1c37b6f055

@ -137,8 +137,8 @@ async function saveAndCompleteTraining() { if (saving.value || !stepFivePassed.v
function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); } function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }
function downloadInstructions() { downloadFile("市场机会识别与选品 - 实训操作说明\n\n1. 汇总三类产品的原始数据。\n2. 对正向指标计算 (x-min)/(max-min),对逆向指标计算 (max-x)/(max-min)。\n3. 将归一化得分乘以第 2 步的对应总权重后求和。\n4. 如案例资料提供三段式时间权重,分别计算第 120 天、第 2140 天和第 4160 天的加权得分。\n5. 将产品品类综合得分填写回第 3 步表格,保留两位小数。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); } function downloadInstructions() { downloadFile("市场机会识别与选品 - 实训操作说明\n\n1. 汇总三类产品的原始数据。\n2. 对正向指标计算 (x-min)/(max-min),对逆向指标计算 (max-x)/(max-min)。\n3. 将归一化得分乘以第 2 步的对应总权重后求和。\n4. 如案例资料提供三段式时间权重,分别计算第 120 天、第 2140 天和第 4160 天的加权得分。\n5. 将产品品类综合得分填写回第 3 步表格,保留两位小数。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }
function downloadTemplate() { const firstProductRow = 5; const lastProductRow = firstProductRow + products.length - 1; const rawStartColumn = 1; const normalizedStartColumn = rawStartColumn + indicators.length; const totalColumn = normalizedStartColumn + indicators.length; const negativeMetricIds = new Set(["online-products", "online-merchants", "return-rate"]); const rows = [["市场机会识别与选品 · Min-Max 加权评分自动化模板"], ["填写原始指标值后,归一得分、对应总权重和综合得分会自动计算。"], [], ["产品品类", ...indicators.map((item) => `${item.name} 原始值`), ...indicators.map((item) => `${item.name} 归一得分`), "综合得分"], ...products.map((product) => [product.name, ...indicators.map(() => ""), ...indicators.map(() => ""), ""]), [], ["权重设置(来自第 2 步)"], ["一级指标权重(%)", ...indicators.map((item) => primaryWeights.value[item.group] ?? ""), ...indicators.map(() => ""), ""], ["维度内权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ...indicators.map(() => ""), ""], ["对应总权重(%)", ...indicators.map(() => ""), ...indicators.map(() => ""), ""]]; const worksheet = XLSX.utils.aoa_to_sheet(rows); indicators.forEach((item, index) => { const rawColumn = XLSX.utils.encode_col(rawStartColumn + index); const normalizedColumn = XLSX.utils.encode_col(normalizedStartColumn + index); const primaryWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 9 }); const dimensionWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 10 }); const correspondingWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 11 }); worksheet[correspondingWeightCell] = { t: "n", f: `${primaryWeightCell}*${dimensionWeightCell}/100` }; products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const rawCell = `${rawColumn}${row}`; const range = `${rawColumn}$${firstProductRow}:${rawColumn}$${lastProductRow}`; const normalizedCell = `${normalizedColumn}${row}`; worksheet[normalizedCell] = { t: "n", f: negativeMetricIds.has(item.id) ? `IFERROR((MAX(${range})-${rawCell})/(MAX(${range})-MIN(${range})),0)` : `IFERROR((${rawCell}-MIN(${range}))/(MAX(${range})-MIN(${range})),0)` }; }); }); products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const scoreCell = `${XLSX.utils.encode_col(totalColumn)}${row}`; const normalizedStart = XLSX.utils.encode_col(normalizedStartColumn); const normalizedEnd = XLSX.utils.encode_col(normalizedStartColumn + indicators.length - 1); const correspondingStart = XLSX.utils.encode_col(rawStartColumn); const correspondingEnd = XLSX.utils.encode_col(rawStartColumn + indicators.length - 1); worksheet[scoreCell] = { t: "n", f: `SUMPRODUCT(${normalizedStart}${row}:${normalizedEnd}${row},$${correspondingStart}$12:$${correspondingEnd}$12)/100` }; }); worksheet["!cols"] = [{ wch: 18 }, ...indicators.map(() => ({ wch: 18 })), ...indicators.map(() => ({ wch: 18 })), { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "综合得分计算"); XLSX.writeFile(workbook, "市场机会识别与选品-自动化模板.xlsx"); } function downloadTemplate() { const firstProductRow = 5; const lastProductRow = firstProductRow + products.length - 1; const rawStartColumn = 1; const normalizedStartColumn = rawStartColumn + indicators.length; const totalColumn = normalizedStartColumn + indicators.length; const negativeMetricIds = new Set(["online-products", "online-merchants", "return-rate"]); const rows = [["市场机会识别与选品 · Min-Max 加权评分自动化模板"], ["填写原始指标值后,归一得分、对应总权重和综合得分会自动计算。"], [], ["产品品类", ...indicators.map((item) => `${item.name} 原始值`), ...indicators.map((item) => `${item.name} 归一得分`), "综合得分"], ...products.map((product) => [product.name, ...indicators.map(() => ""), ...indicators.map(() => ""), ""]), [], ["权重设置(来自第 2 步)"], ["一级指标权重(%)", ...indicators.map((item) => primaryWeights.value[item.group] ?? ""), ...indicators.map(() => ""), ""], ["维度内权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ...indicators.map(() => ""), ""], ["对应总权重(%)", ...indicators.map(() => ""), ...indicators.map(() => ""), ""]]; const worksheet = XLSX.utils.aoa_to_sheet(rows); indicators.forEach((item, index) => { const rawColumn = XLSX.utils.encode_col(rawStartColumn + index); const normalizedColumn = XLSX.utils.encode_col(normalizedStartColumn + index); const primaryWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 9 }); const dimensionWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 10 }); const correspondingWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 11 }); worksheet[correspondingWeightCell] = { t: "n", f: `${primaryWeightCell}*${dimensionWeightCell}/100` }; products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const rawCell = `${rawColumn}${row}`; const range = `${rawColumn}$${firstProductRow}:${rawColumn}$${lastProductRow}`; const normalizedCell = `${normalizedColumn}${row}`; worksheet[normalizedCell] = { t: "n", f: negativeMetricIds.has(item.id) ? `IFERROR((MAX(${range})-${rawCell})/(MAX(${range})-MIN(${range})),0)` : `IFERROR((${rawCell}-MIN(${range}))/(MAX(${range})-MIN(${range})),0)` }; }); }); products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const scoreCell = `${XLSX.utils.encode_col(totalColumn)}${row}`; const normalizedStart = XLSX.utils.encode_col(normalizedStartColumn); const normalizedEnd = XLSX.utils.encode_col(normalizedStartColumn + indicators.length - 1); const correspondingStart = XLSX.utils.encode_col(rawStartColumn); const correspondingEnd = XLSX.utils.encode_col(rawStartColumn + indicators.length - 1); worksheet[scoreCell] = { t: "n", f: `SUMPRODUCT(${normalizedStart}${row}:${normalizedEnd}${row},$${correspondingStart}$12:$${correspondingEnd}$12)/100` }; }); worksheet["!cols"] = [{ wch: 18 }, ...indicators.map(() => ({ wch: 18 })), ...indicators.map(() => ({ wch: 18 })), { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "综合得分计算"); XLSX.writeFile(workbook, "市场机会识别与选品-自动化模板.xlsx"); }
function downloadVisualizationInstructions() { downloadFile("市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜汇总产品综合得分。\n2. 分别计算各品类的平均分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传。", "市场机会识别与选品-数据可视化操作说明.txt", "text/plain;charset=utf-8"); } function downloadVisualizationInstructions() { downloadFile("市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜分别填写每日综合得分。\n2. 使用自动化模板计算各品类的日平均得分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传,并点击缩略图预览确认。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }
function downloadVisualizationTemplate() { const header = ["产品品类", "平均得分", "最高分", "最低分"]; const rows = products.map((product) => [product.name, "", "", ""]); downloadFile([header, ...rows].map((row) => row.join(",")).join("\n"), "市场机会识别与选品-数据可视化模板.csv", "text/csv;charset=utf-8"); } function downloadVisualizationTemplate() { const firstDataRow = 5; const dailyRows = products.flatMap((product) => Array.from({ length: 60 }, (_, dayIndex) => [`${dayIndex + 1}`, product.name, ""])); const lastDataRow = firstDataRow + dailyRows.length - 1; const dailyWorksheet = XLSX.utils.aoa_to_sheet([["市场机会识别与选品 · 数据可视化自动化模板"], ["请填入各产品每日综合得分,汇总结果会自动计算日平均得分、最高分和最低分。"], [], ["日期", "产品品类", "日综合得分"], ...dailyRows]); dailyWorksheet["!cols"] = [{ wch: 14 }, { wch: 20 }, { wch: 16 }]; const summaryWorksheet = XLSX.utils.aoa_to_sheet([["产品品类", "日平均得分", "最高分", "最低分"], ...products.map((product) => [product.name, "", "", ""])]); products.forEach((product, index) => { const row = index + 2; const scoreRange = `'日综合得分录入'!$C$${firstDataRow}:$C$${lastDataRow}`; const productRange = `'日综合得分录入'!$B$${firstDataRow}:$B$${lastDataRow}`; summaryWorksheet[`B${row}`] = { t: "n", f: `AVERAGEIF(${productRange},A${row},${scoreRange})` }; summaryWorksheet[`C${row}`] = { t: "n", f: `MAXIFS(${scoreRange},${productRange},A${row},${scoreRange},"<>")` }; summaryWorksheet[`D${row}`] = { t: "n", f: `MINIFS(${scoreRange},${productRange},A${row},${scoreRange},"<>")` }; }); summaryWorksheet["!cols"] = [{ wch: 20 }, { wch: 16 }, { wch: 14 }, { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, dailyWorksheet, "日综合得分录入"); XLSX.utils.book_append_sheet(workbook, summaryWorksheet, "汇总结果"); XLSX.writeFile(workbook, "市场机会识别与选品-数据可视化模板.xlsx"); }
function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), step5: buildStepFiveOutcome(), step6: buildStepSixOutcome(), currentStep: currentStep.value }, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${taskTitle.value}-实训结果.json`; link.click(); URL.revokeObjectURL(url); } function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), step5: buildStepFiveOutcome(), step6: buildStepSixOutcome(), currentStep: currentStep.value }, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${taskTitle.value}-实训结果.json`; link.click(); URL.revokeObjectURL(url); }
</script> </script>

@ -0,0 +1,23 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const source = fs.readFileSync(path.join(__dirname, "../src/views/product/market-opportunity-selection.vue"), "utf8");
test("第四步提供可预览的数据可视化结果图上传", () => {
assert.match(source, /accept="image\/png,image\/jpeg,image\/webp"/);
assert.match(source, /<el-image :src="visualization\.url" :preview-src-list="\[visualization\.url\]"/);
assert.match(source, /点击缩略图可放大查看/);
});
test("第四步下载含日综合得分汇总公式的 Excel 自动化模板", () => {
assert.match(source, /function downloadVisualizationTemplate\(\)/);
assert.match(source, /"日综合得分录入"/);
assert.match(source, /AVERAGEIF\(/);
assert.match(source, /MAXIFS\(/);
assert.match(source, /MINIFS\(/);
assert.match(source, /MAXIFS\([\s\S]*?,"<>"/);
assert.match(source, /MINIFS\([\s\S]*?,"<>"/);
assert.match(source, /XLSX\.writeFile\(workbook, "市场机会识别与选品-数据可视化模板\.xlsx"\)/);
});
Loading…
Cancel
Save