309 lines
14 KiB
HTML
309 lines
14 KiB
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>
|
||
<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> |