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

380
tools/china_map.html Normal file
View 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
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>