Files
data-index-report/tools/data_utils.html
2026-05-18 13:52:47 +08:00

309 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据驱动型漏斗分析看板</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style id="main-styles">
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f8fafc;
color: #1e293b;
}
.dashboard-container {
display: grid;
grid-template-columns: 320px 1fr;
height: 100vh;
overflow: hidden;
}
.sidebar {
background: #ffffff;
border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
z-index: 50;
}
.main-content {
overflow-y: auto;
padding: 2rem;
background: radial-gradient(circle at top right, #f1f5f9, #f8fafc);
}
.funnel-wrapper {
position: relative;
width: 100%;
min-height: 500px;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 20px 0;
}
.funnel-row {
display: flex;
align-items: center;
width: 100%;
margin: 8px 0;
}
.label-container {
width: 180px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.guide-line {
flex-grow: 1;
height: 1px;
background: linear-gradient(90deg, #e2e8f0 0%, #cbd5e1 100%);
margin: 0 15px;
opacity: 0.4;
}
.layer-box {
width: 420px;
display: flex;
justify-content: center;
}
.layer-3d {
position: relative;
height: 36px;
border-radius: 50% / 40%;
display: flex;
justify-content: center;
align-items: center;
font-weight: 700;
font-size: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border: 1px solid rgba(0,0,0,0.02);
}
.layer-3d::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; height: 12px;
background: rgba(255,255,255,0.2);
border-radius: 50% / 100%;
}
.hidden-in-export { display: block; }
.export-mode .hidden-in-export { display: none !important; }
.export-mode .dashboard-container { grid-template-columns: 1fr; height: auto; overflow: visible; }
.export-mode .main-content { padding: 3rem; width: 100%; max-width: 1000px; margin: 0 auto; }
@keyframes fadeInRight {
from { opacity: 0; transform: translateX(15px); }
to { opacity: 1; transform: translateX(0); }
}
</style>
</head>
<body>
<div class="dashboard-container" id="app-root">
<!-- 左侧控制面板 -->
<aside class="sidebar p-6 hidden-in-export">
<div class="mb-8">
<h2 class="text-xl font-bold text-slate-800">配置中心</h2>
<p class="text-xs text-slate-400 mt-1 uppercase tracking-widest">Config Panel</p>
</div>
<div class="flex-grow flex flex-col space-y-6">
<div>
<label class="block text-xs font-bold text-slate-500 mb-2 uppercase tracking-wide">批量导入数据</label>
<textarea id="data-input" class="w-full h-72 p-3 text-sm border border-slate-200 rounded-xl focus:ring-2 focus:ring-red-500 outline-none transition-all font-mono leading-relaxed" placeholder="名称 百分比 (支持复制粘贴)">录入企业信息 0.74%
查询税局维护状态 0.06%
四要素异常 0.06%
税局维护中 0.00%
签署授权 0.29%
登录税局(新版) 1.04%
登录帮助 0.03%
忘记密码 0.18%</textarea>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2 uppercase">导出文件名</label>
<input type="text" id="filename-input" class="w-full p-3 text-sm border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none" value="业务转化率分析报告">
</div>
<button id="btn-apply" class="w-full bg-slate-800 hover:bg-slate-900 text-white font-bold py-3 rounded-xl transition-all shadow-md flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
刷新预览
</button>
</div>
<div class="mt-8 pt-6 border-t border-slate-100">
<button id="btn-export" class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
生成 HTML 文件
</button>
</div>
</aside>
<!-- 右侧主展示区 -->
<main class="main-content">
<div class="max-w-4xl mx-auto">
<!-- 统计卡片 -->
<div class="grid grid-cols-2 gap-6 mb-10">
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between">
<div>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">最高占比环节</p>
<p id="stat-max-name" class="text-sm font-semibold text-slate-600 mt-1">--</p>
</div>
<p id="stat-max-val" class="text-3xl font-black text-red-600 tracking-tighter">0.00%</p>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between">
<div>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">最低占比环节</p>
<p id="stat-min-name" class="text-sm font-semibold text-slate-600 mt-1">--</p>
</div>
<p id="stat-min-val" class="text-3xl font-black text-slate-300 tracking-tighter">0.00%</p>
</div>
</div>
<!-- 图表区域 -->
<div class="bg-white rounded-3xl shadow-xl p-12 border border-slate-50 flex flex-col items-center">
<div id="funnel-container" class="funnel-wrapper">
<!-- 动态渲染内容 -->
</div>
</div>
</div>
</main>
</div>
<script id="main-logic">
function calculateStyles(val, maxVal) {
// 如果最大值为0比例设为0
const ratio = maxVal > 0 ? val / maxVal : 0;
// 宽度范围 15% 到 100%
const width = 15 + (ratio * 85);
// 颜色亮度映射:红(45%亮度) 到 白(100%亮度)
const lightness = 100 - (ratio * 55);
const bgColor = `hsl(0, 85%, ${lightness}%)`;
// 当背景亮度较低时使用白色文字
const textColor = lightness < 65 ? '#ffffff' : '#1e293b';
return { width: `${width}%`, bgColor, textColor };
}
function parseData(inputText) {
return inputText.trim().split('\n').map(line => {
// 兼容 制表符(\t)、逗号(,)、以及多个空格
const parts = line.split(/[\t,]|\s{2,}/);
const name = parts[0]?.trim() || '未知环节';
// 清理百分比符号并转为浮点数
const valStr = parts[1]?.trim().replace('%', '') || '0';
const percent = parseFloat(valStr);
return { name, percent };
}).filter(d => d.name);
}
function render(data) {
const container = document.getElementById('funnel-container');
if (!container) return;
container.innerHTML = '';
if (data.length === 0) return;
const percents = data.map(d => d.percent);
const maxVal = Math.max(...percents);
const minVal = Math.min(...percents);
document.getElementById('stat-max-val').innerText = maxVal.toFixed(2) + '%';
document.getElementById('stat-max-name').innerText = data.find(d => d.percent === maxVal)?.name || '--';
document.getElementById('stat-min-val').innerText = minVal.toFixed(2) + '%';
document.getElementById('stat-min-name').innerText = data.find(d => d.percent === minVal)?.name || '--';
data.forEach((item, index) => {
const styles = calculateStyles(item.percent, maxVal);
const row = document.createElement('div');
row.className = 'funnel-row';
row.style.animation = `fadeInRight 0.6s ease forwards ${index * 0.08}s`;
row.style.opacity = '0';
row.innerHTML = `
<div class="label-container">${item.name}</div>
<div class="guide-line"></div>
<div class="layer-box">
<div class="layer-3d" style="width: ${styles.width}; background: ${styles.bgColor}; color: ${styles.textColor}">
<span>${item.percent.toFixed(2)}%</span>
</div>
</div>
`;
container.appendChild(row);
});
}
// 初始化
window.onload = () => {
const initialInput = document.getElementById('data-input').value;
render(parseData(initialInput));
};
// 应用按钮
document.getElementById('btn-apply')?.addEventListener('click', () => {
const input = document.getElementById('data-input').value;
render(parseData(input));
});
// 核心导出逻辑
document.getElementById('btn-export')?.addEventListener('click', function() {
const filename = document.getElementById('filename-input').value || '业务环节分析报告';
const finalName = filename.endsWith('.html') ? filename : filename + '.html';
const currentDataStr = document.getElementById('data-input').value;
const exportHtml = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${filename}</title>
<script src="https://cdn.tailwindcss.com"><\/script>
<style>
${document.getElementById('main-styles').innerHTML}
body { background-color: #ffffff; }
.main-content { background: none; padding: 40px 20px; }
.dashboard-container { display: block; height: auto; }
.hidden-in-export { display: none !important; }
</style>
</head>
<body>
<div class="dashboard-container">
<main class="main-content">
<div class="max-w-4xl mx-auto">
<div class="grid grid-cols-2 gap-6 mb-10">
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between">
<div><p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">最高占比环节</p><p id="stat-max-name" class="text-sm font-semibold text-slate-600 mt-1">--</p></div>
<p id="stat-max-val" class="text-3xl font-black text-red-600 tracking-tighter">0.00%</p>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between">
<div><p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">最低占比环节</p><p id="stat-min-name" class="text-sm font-semibold text-slate-600 mt-1">--</p></div>
<p id="stat-min-val" class="text-3xl font-black text-slate-300 tracking-tighter">0.00%</p>
</div>
</div>
<div class="bg-white rounded-3xl shadow-xl p-12 border border-slate-50 flex flex-col items-center">
<div id="funnel-container" class="funnel-wrapper"></div>
</div>
</div>
</main>
</div>
<script>
const dataStr = \`${currentDataStr.replace(/`/g, '\\`')}\`;
${document.getElementById('main-logic').innerHTML.split('// 初始化')[0]}
window.onload = () => { render(parseData(dataStr)); };
<\/script>
</body>
</html>`;
const blob = new Blob([exportHtml], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = finalName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
</script>
</body>
</html>