chore: initial commit
This commit is contained in:
380
tools/china_map.html
Normal file
380
tools/china_map.html
Normal file
@@ -0,0 +1,380 @@
|
||||
<!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>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
|
||||
<!-- 引入中国地图数据 -->
|
||||
<script src="https://fastly.jsdelivr.net/npm/echarts@4.9.0/map/js/china.js"></script>
|
||||
<style>
|
||||
#map-container {
|
||||
height: calc(100vh - 120px);
|
||||
min-height: 500px;
|
||||
}
|
||||
.custom-card {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-900 font-sans">
|
||||
|
||||
<header class="bg-blue-600 text-white p-4 shadow-md">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<h1 class="text-xl font-bold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
全国采集数量分布地图
|
||||
</h1>
|
||||
<p class="text-sm opacity-80">支持批量导入与独立导出</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto p-4 lg:p-6 grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
|
||||
<!-- 左侧控制面板 -->
|
||||
<div class="lg:col-span-1 space-y-4">
|
||||
<div class="custom-card p-4 flex flex-col h-full">
|
||||
<h2 class="font-semibold mb-3 flex items-center gap-2 text-slate-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
数据管理
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 mb-2">每行格式:地区 数量</p>
|
||||
<textarea
|
||||
id="data-input"
|
||||
class="w-full flex-grow p-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none font-mono mb-4"
|
||||
placeholder="输入数据..."
|
||||
></textarea>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-slate-500 mb-1 block">导出文件名</label>
|
||||
<input
|
||||
id="export-filename"
|
||||
type="text"
|
||||
class="w-full p-2 border border-slate-200 rounded-md text-sm outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="如:1月采集分布图"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<button
|
||||
onclick="updateChart()"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 rounded-lg transition-all shadow-sm active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
实时刷新
|
||||
</button>
|
||||
<button
|
||||
onclick="exportHTML()"
|
||||
class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 rounded-lg transition-all shadow-sm active:scale-95 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
导出纯净地图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-card p-4">
|
||||
<h3 class="text-sm font-semibold mb-2 text-slate-700">数据统计</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">地区总数:</span>
|
||||
<span id="stat-count" class="font-bold">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">采集总量:</span>
|
||||
<span id="stat-total" class="font-bold text-blue-600">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧地图展示 -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="custom-card p-4 relative overflow-hidden">
|
||||
<div id="map-container" class="w-full"></div>
|
||||
<div id="no-data" class="absolute inset-0 flex items-center justify-center bg-white/80 hidden z-10">
|
||||
<p class="text-slate-400 italic">请在左侧输入有效数据以生成地图</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 初始数据
|
||||
const initialData = `大连市 158
|
||||
湖南省 1777
|
||||
新疆维吾尔 83
|
||||
青海省 37
|
||||
陕西省 1841
|
||||
河北省 2714
|
||||
青岛市 409
|
||||
广东省 6726
|
||||
福建省 1722
|
||||
浙江省 3031
|
||||
黑龙江省 500
|
||||
云南省 934
|
||||
深圳市 2332
|
||||
江苏省 4712
|
||||
甘肃省 786
|
||||
江西省 1174
|
||||
广西省 872
|
||||
吉林省 399
|
||||
内蒙古 701
|
||||
厦门市 431
|
||||
四川省 4560
|
||||
河南省 2779
|
||||
西藏 20
|
||||
山西省 926
|
||||
山东省 3229
|
||||
安徽省 2307
|
||||
海南省 384
|
||||
宁夏 46
|
||||
重庆市 838
|
||||
上海市 1621
|
||||
湖北省 1786
|
||||
贵州省 1129
|
||||
辽宁省 554
|
||||
宁波市 567
|
||||
天津市 452
|
||||
北京市 1666`;
|
||||
|
||||
const cityToProvince = {
|
||||
'大连市': '辽宁', '大连': '辽宁',
|
||||
'青岛市': '山东', '青岛': '山东',
|
||||
'深圳市': '广东', '深圳': '广东',
|
||||
'厦门市': '福建', '厦门': '福建',
|
||||
'宁波市': '浙江', '宁波': '浙江'
|
||||
};
|
||||
|
||||
let myChart = null;
|
||||
|
||||
function init() {
|
||||
document.getElementById('data-input').value = initialData;
|
||||
myChart = echarts.init(document.getElementById('map-container'));
|
||||
updateChart();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
myChart && myChart.resize();
|
||||
});
|
||||
}
|
||||
|
||||
function parseData(text) {
|
||||
const lines = text.trim().split('\n');
|
||||
const dataMap = {};
|
||||
let total = 0;
|
||||
|
||||
lines.forEach(line => {
|
||||
const parts = line.trim().split(/[\s\t,]+/);
|
||||
if (parts.length >= 2) {
|
||||
const rawName = parts[0];
|
||||
const value = parseFloat(parts[1]);
|
||||
if (isNaN(value)) return;
|
||||
|
||||
total += value;
|
||||
let cleanName = rawName.replace(/(省|市|自治区|维吾尔|壮族|回族)/g, '');
|
||||
if (cityToProvince[rawName]) {
|
||||
cleanName = cityToProvince[rawName];
|
||||
}
|
||||
|
||||
if (dataMap[cleanName]) {
|
||||
dataMap[cleanName] += value;
|
||||
} else {
|
||||
dataMap[cleanName] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
list: Object.keys(dataMap).map(name => ({ name, value: dataMap[name] })),
|
||||
total: total,
|
||||
count: Object.keys(dataMap).length
|
||||
};
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
const inputText = document.getElementById('data-input').value;
|
||||
const parsed = parseData(inputText);
|
||||
|
||||
if (parsed.list.length === 0) {
|
||||
document.getElementById('no-data').classList.remove('hidden');
|
||||
return;
|
||||
} else {
|
||||
document.getElementById('no-data').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('stat-count').innerText = parsed.count;
|
||||
document.getElementById('stat-total').innerText = parsed.total.toLocaleString();
|
||||
|
||||
const maxVal = Math.max(...parsed.list.map(i => i.value), 100);
|
||||
|
||||
const option = getChartOption(parsed, maxVal);
|
||||
myChart.setOption(option);
|
||||
}
|
||||
|
||||
function getChartOption(parsed, maxVal) {
|
||||
return {
|
||||
backgroundColor: '#ffffff',
|
||||
title: {
|
||||
text: '全国采集数量分布地图',
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: { color: '#1e293b', fontSize: 24, fontWeight: 'bold' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (isNaN(params.value)) return params.name + ': 无数据';
|
||||
return `${params.name}<br/>采集数量: <span style="font-weight:bold;color:#2563eb">${params.value.toLocaleString()}</span>`;
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
textStyle: { color: '#334155' }
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: maxVal,
|
||||
left: 40,
|
||||
bottom: 40,
|
||||
text: ['高', '低'],
|
||||
calculable: true,
|
||||
inRange: {
|
||||
color: ['#f0f9ff', '#bae6fd', '#38bdf8', '#0284c7', '#075985']
|
||||
}
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: true,
|
||||
zoom: 1.1,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 11,
|
||||
lineHeight: 15,
|
||||
color: '#334155',
|
||||
fontWeight: 'bold',
|
||||
textBorderColor: '#ffffff',
|
||||
textBorderWidth: 2,
|
||||
formatter: function(params) {
|
||||
if (params.name === '香港' || params.name === '澳门') return '';
|
||||
const item = parsed.list.find(i => i.name === params.name);
|
||||
return item && item.value > 0 ? `${params.name}\n${item.value}` : params.name;
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: 'rgba(255,255,255,0.6)',
|
||||
borderWidth: 1,
|
||||
areaColor: '#f8fafc'
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: { areaColor: '#fbbf24' },
|
||||
label: { color: '#000' }
|
||||
},
|
||||
regions: [
|
||||
{ name: '香港', label: { show: false } },
|
||||
{ name: '澳门', label: { show: false } }
|
||||
]
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data: parsed.list
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 导出功能:生成仅含地图的纯净 HTML
|
||||
function exportHTML() {
|
||||
const currentDataText = document.getElementById('data-input').value;
|
||||
const customFilename = document.getElementById('export-filename').value.trim();
|
||||
const parsed = parseData(currentDataText);
|
||||
const maxVal = Math.max(...parsed.list.map(i => i.value), 100);
|
||||
|
||||
// 构建纯净版模板
|
||||
const cleanTemplate = `<!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://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"><\/script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/echarts@4.9.0/map/js/china.js"><\/script>
|
||||
<style>
|
||||
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #fff; }
|
||||
#map { width: 100vw; height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
const parsedData = ${JSON.stringify(parsed)};
|
||||
const maxVal = ${maxVal};
|
||||
|
||||
const myChart = echarts.init(document.getElementById('map'));
|
||||
const option = {
|
||||
backgroundColor: '#ffffff',
|
||||
title: {
|
||||
text: '全国采集数量分布地图',
|
||||
left: 'center',
|
||||
top: 30,
|
||||
textStyle: { color: '#1e293b', fontSize: 26, fontWeight: 'bold' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (isNaN(params.value)) return params.name + ': 无数据';
|
||||
return params.name + '<br/>采集数量: ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
min: 0, max: maxVal, left: 50, bottom: 50, text: ['高', '低'], calculable: true,
|
||||
inRange: { color: ['#f0f9ff', '#bae6fd', '#38bdf8', '#0284c7', '#075985'] }
|
||||
},
|
||||
geo: {
|
||||
map: 'china', roam: true, zoom: 1.1,
|
||||
label: {
|
||||
show: true, fontSize: 12, lineHeight: 16, color: '#334155', fontWeight: 'bold',
|
||||
textBorderColor: '#ffffff', textBorderWidth: 2,
|
||||
formatter: function(params) {
|
||||
if (params.name === '香港' || params.name === '澳门') return '';
|
||||
const item = parsedData.list.find(i => i.name === params.name);
|
||||
return item && item.value > 0 ? params.name + '\\n' + item.value : params.name;
|
||||
}
|
||||
},
|
||||
itemStyle: { borderColor: 'rgba(255,255,255,0.6)', borderWidth: 1, areaColor: '#f8fafc' },
|
||||
emphasis: { itemStyle: { areaColor: '#fbbf24' } },
|
||||
regions: [{ name: '香港', label: { show: false } }, { name: '澳门', label: { show: false } }]
|
||||
},
|
||||
series: [{ type: 'map', geoIndex: 0, data: parsedData.list }]
|
||||
};
|
||||
myChart.setOption(option);
|
||||
window.addEventListener('resize', () => myChart.resize());
|
||||
<\/script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const blob = new Blob([cleanTemplate], { type: 'text/html' });
|
||||
const a = document.createElement('a');
|
||||
const date = new Date().toISOString().slice(0,10);
|
||||
|
||||
// 使用自定义名称或默认名称
|
||||
const finalFilename = customFilename ? `${customFilename}.html` : `全国采集地图_纯净版_${date}.html`;
|
||||
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = finalFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
window.onload = init;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
309
tools/data_utils.html
Normal file
309
tools/data_utils.html
Normal 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>
|
||||
Reference in New Issue
Block a user