chore: initial commit

This commit is contained in:
hailin
2026-05-18 13:52:47 +08:00
commit 0753129afe
148 changed files with 14202 additions and 0 deletions

309
tools/data_utils.html Normal file
View File

@@ -0,0 +1,309 @@
<!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>