HTML版本“CSV to vCard(.vcf) 转换器”

[重要通告]如您遇疑难杂症,本站支持知识付费业务,扫右边二维码加博主微信,可节省您宝贵时间哦!

通讯录转成vCard 格式 (.vcf)方便一次性导入,网上的工具要么不趁手,要么要消费,本着自己动手丰衣足食的思想,然后就有了以下的代码;

正确食用方法:复制下面的代码,保存为:csv-to-vcard.html 访问这个.html页面即可食用!

csv-to-vcard.html代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSV to vCard 转换器-老梁博客</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
neutral: '#1F2937',
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
[url=home.php?mod=space&uid=1688376]@layer[/url] utilities {
.content-auto {
content-visibility: auto;
}
.transition-height {
transition: max-height 0.3s ease-in-out;
}
.shadow-soft {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.text-shadow {
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
}
</style>
</head>
<body class="bg-gray-50 font-inter text-neutral">
<div class="min-h-screen flex flex-col">
<!-- 导航栏 -->
<header class="bg-white shadow-sm sticky top-0 z-10">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-address-book text-primary text-2xl"></i>
<h1 class="text-xl font-bold text-neutral">CSV to vCard 转换器-BY 老梁博客</h1>
</div>
<div class="hidden md:flex items-center space-x-4">
<button id="help-btn" class="text-gray-600 hover:text-primary transition-colors flex items-center">
<i class="fa-solid fa-question-circle mr-1"></i>
<span>帮助</span>
</button>
<a href="#" class="text-gray-600 hover:text-primary transition-colors">关于</a>
</div>
</div>
</header>

<!-- 主内容区 -->
<main class="flex-grow container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto">
<!-- 上传区域 -->
<section class="bg-white rounded-xl shadow-soft p-6 mb-8 transition-all duration-300 hover:shadow-lg">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-neutral mb-2">转换您的联系人</h2>
<p class="text-gray-500">上传 CSV 文件,将其转换为 vCard 格式 (.vcf)</p>
</div>

<div id="drop-area" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer transition-all duration-300 hover:border-primary hover:bg-blue-50">
<i class="fa-solid fa-cloud-upload text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-500 mb-2">拖放 CSV 文件到此处,或</p>
<label class="inline-block bg-primary hover:bg-primary/90 text-white font-medium py-2 px-6 rounded-lg transition-all duration-300 cursor-pointer">
<span>选择文件</span>
<input type="file" id="file-input" accept=".csv" class="hidden">
</label>
<p id="file-name" class="mt-4 text-sm text-gray-600 hidden"></p>
</div>

<div class="mt-4 text-center">
<button id="generate-template-btn" class="text-primary hover:text-primary/80 flex items-center mx-auto">
<i class="fa-solid fa-file-csv mr-1"></i>
<span>生成 CSV 模板</span>
</button>
</div>
</section>

<!-- 转换选项 -->
<section id="options-section" class="bg-white rounded-xl shadow-soft p-6 mb-8 hidden transition-all duration-300 opacity-0">
<h3 class="text-xl font-semibold text-neutral mb-4">CSV 格式选项</h3>

<div class="grid md:grid-cols-2 gap-6">
<div>
<label class="block text-gray-700 mb-2">字段分隔符</label>
<div class="flex space-x-2">
<label class="inline-flex items-center">
<input type="radio" name="delimiter" value="," checked class="form-radio text-primary">
<span class="ml-2">逗号 (,)</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="delimiter" value=";" class="form-radio text-primary">
<span class="ml-2">分号 (;)</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="delimiter" value="tab" class="form-radio text-primary">
<span class="ml-2">制表符</span>
</label>
</div>
</div>

<div>
<label class="block text-gray-700 mb-2">包含标题行</label>
<label class="inline-flex items-center">
<input type="checkbox" id="has-header" checked class="form-checkbox text-primary">
<span class="ml-2">我的 CSV 文件包含标题行</span>
</label>
</div>

<div>
<label class="block text-gray-700 mb-2">CSV 编码</label>
<select id="encoding" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50">
<option value="utf-8">UTF-8</option>
<option value="gbk">GBK</option>
<option value="gb2312">GB2312</option>
<option value="iso-8859-1">ISO-8859-1</option>
</select>
</div>

<div>
<label class="block text-gray-700 mb-2">vCard 版本</label>
<select id="vcard-version" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50">
<option value="3.0">vCard 3.0 (大多数设备兼容)</option>
<option value="4.0">vCard 4.0 (最新标准)</option>
</select>
</div>
</div>

<div class="mt-6">
<button id="convert-btn" class="w-full bg-secondary hover:bg-secondary/90 text-white font-medium py-3 px-6 rounded-lg transition-all duration-300 flex items-center justify-center">
<i class="fa-solid fa-exchange-alt mr-2"></i>
开始转换
</button>
</div>
</section>

<!-- 映射设置 (高级选项) -->
<section id="mapping-section" class="bg-white rounded-xl shadow-soft p-6 mb-8 hidden transition-all duration-300 opacity-0">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-neutral">字段映射</h3>
<button id="advanced-toggle" class="text-primary hover:text-primary/80 text-sm flex items-center">
<span>高级选项</span>
<i class="fa-solid fa-chevron-down ml-1 transition-transform duration-300"></i>
</button>
</div>

<div id="advanced-options" class="max-h-0 overflow-hidden transition-height duration-300">
<p class="text-gray-600 mb-4">匹配 CSV 列与 vCard 字段。如果您的 CSV 文件格式标准,系统会自动尝试匹配。</p>

<div id="field-mapping" class="space-y-3"></div>

<div class="mt-6 bg-blue-50 border-l-4 border-primary p-4 rounded-r-lg">
<div class="flex">
<div class="flex-shrink-0">
<i class="fa-solid fa-info-circle text-primary"></i>
</div>
<div class="ml-3">
<h4 class="text-sm font-medium text-primary">处理逗号分隔的分组</h4>
<div class="mt-2 text-sm text-blue-700">
<p>如果您的 CSV 使用逗号作为分隔符,同时分组字段也包含逗号,请用双引号包裹分组内容。</p>
<p>例如:<code class="bg-blue-100 px-1 rounded">"家人,朋友"</code></p>
</div>
</div>
</div>
</div>
</div>
</section>

<!-- 预览区域 -->
<section id="preview-section" class="bg-white rounded-xl shadow-soft p-6 mb-8 hidden transition-all duration-300 opacity-0">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-neutral">转换预览</h3>
<div class="text-sm text-gray-500">
<span id="contact-count">0</span> 个联系人已转换
</div>
</div>

<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">姓名</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">电话</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">邮箱</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody id="preview-table" class="bg-white divide-y divide-gray-200"></tbody>
</table>
</div>

<div class="mt-6 flex justify-between">
<button id="back-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-6 rounded-lg transition-all duration-300">
返回
</button>
<button id="download-btn" class="bg-primary hover:bg-primary/90 text-white font-medium py-2 px-6 rounded-lg transition-all duration-300 flex items-center">
<i class="fa-solid fa-download mr-2"></i>
下载 vCard 文件
</button>
</div>
</section>

<!-- 帮助卡片 -->
<section class="bg-blue-50 border-l-4 border-primary p-4 rounded-r-lg mb-8">
<div class="flex">
<div class="flex-shrink-0">
<i class="fa-solid fa-info-circle text-primary"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-primary">CSV 文件格式要求</h3>
<div class="mt-2 text-sm text-blue-700 space-y-2">
<p>您的 CSV 文件应包含以下字段:姓名、电话、邮箱(可选)</p>
<p>示例格式:<code class="bg-blue-100 px-1 rounded">姓名,电话,邮箱</code></p>
<p>第一行建议包含标题(如:Name,Phone,Email)</p>
</div>
</div>
</div>
</section>
</div>
</main>

<!-- 页脚 -->
<footer class="bg-neutral text-white py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<p class="text-sm text-gray-400">&#169; 2025 CSV to vCard 转换器. BY 老梁博客.</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-brands fa-github"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-brands fa-twitter"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-brands fa-linkedin"></i>
</a>
</div>
</div>
</div>
</footer>
</div>

<!-- 帮助模态框 -->
<div id="help-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-xl font-semibold text-gray-900">vCard 3.0 与 4.0 对比</h3>
<button id="close-help-btn" class="text-gray-400 hover:text-gray-500">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="px-6 py-4">
<div class="space-y-6">
<div>
<h4 class="text-lg font-medium text-gray-900 mb-2">基本信息</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">字段</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">3.0 示例</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">4.0 示例</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">FN</td>
<td class="px-6 py-4 text-sm text-gray-500">显示名</td>
<td class="px-6 py-4 text-sm text-gray-500">FN:张三</td>
<td class="px-6 py-4 text-sm text-gray-500">FN:张三</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">N</td>
<td class="px-6 py-4 text-sm text-gray-500">结构化姓名 (姓;名;中间名;前缀;后缀)</td>
<td class="px-6 py-4 text-sm text-gray-500">N:张;三;;;N:Doe;John;;;Dr.</td>
<td class="px-6 py-4 text-sm text-gray-500">N:张;三;;;N:Doe;John;;;Dr.</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">NICKNAME</td>
<td class="px-6 py-4 text-sm text-gray-500">昵称</td>
<td class="px-6 py-4 text-sm text-gray-500">NICKNAME:小张</td>
<td class="px-6 py-4 text-sm text-gray-500">NICKNAME:小张</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">BDAY</td>
<td class="px-6 py-4 text-sm text-gray-500">生日</td>
<td class="px-6 py-4 text-sm text-gray-500">BDAY:1990-01-01</td>
<td class="px-6 py-4 text-sm text-gray-500">BDAY:1990-01-01</td>
</tr>
</tbody>
</table>
</div>
</div>

<div>
<h4 class="text-lg font-medium text-gray-900 mb-2">联系方式</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">字段</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">3.0 示例</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">4.0 示例</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">TEL</td>
<td class="px-6 py-4 text-sm text-gray-500">电话</td>
<td class="px-6 py-4 text-sm text-gray-500">TEL;TYPE=CELL:13800138000</td>
<td class="px-6 py-4 text-sm text-gray-500">TEL;TYPE=cell:tel:+86-138-0013-8000</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">EMAIL</td>
<td class="px-6 py-4 text-sm text-gray-500">邮箱</td>
<td class="px-6 py-4 text-sm text-gray-500">EMAIL:zhangsan@example.com</td>
<td class="px-6 py-4 text-sm text-gray-500">EMAIL:zhangsan@example.com</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">ADR</td>
<td class="px-6 py-4 text-sm text-gray-500">地址 (邮政信箱;扩展地址;街道;城市;区域;邮编;国家)</td>
<td class="px-6 py-4 text-sm text-gray-500">ADR:;;北京市朝阳区;北京市;朝阳区;100000;中国</td>
<td class="px-6 py-4 text-sm text-gray-500">ADR:;;北京市朝阳区;北京市;朝阳区;100000;中国</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">URL</td>
<td class="px-6 py-4 text-sm text-gray-500">网址</td>
<td class="px-6 py-4 text-sm text-gray-500">URL:[url=https://example.com]https://example.com[/url]</td>
<td class="px-6 py-4 text-sm text-gray-500">URL:[url=https://example.com]https://example.com[/url]</td>
</tr>
</tbody>
</table>
</div>
</div>

<div>
<h4 class="text-lg font-medium text-gray-900 mb-2">组织与职业</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">字段</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">3.0 示例</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">4.0 示例</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">ORG</td>
<td class="px-6 py-4 text-sm text-gray-500">组织/公司</td>
<td class="px-6 py-4 text-sm text-gray-500">ORG:科技有限公司</td>
<td class="px-6 py-4 text-sm text-gray-500">ORG:科技有限公司</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">TITLE</td>
<td class="px-6 py-4 text-sm text-gray-500">职位</td>
<td class="px-6 py-4 text-sm text-gray-500">TITLE:软件工程师</td>
<td class="px-6 py-4 text-sm text-gray-500">TITLE:软件工程师</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">ROLE</td>
<td class="px-6 py-4 text-sm text-gray-500">角色</td>
<td class="px-6 py-4 text-sm text-gray-500">不支持</td>
<td class="px-6 py-4 text-sm text-gray-500">ROLE:开发者</td>
</tr>
</tbody>
</table>
</div>
</div>

<div>
<h4 class="text-lg font-medium text-gray-900 mb-2">其他信息</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">字段</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">3.0 示例</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">4.0 示例</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">NOTE</td>
<td class="px-6 py-4 text-sm text-gray-500">备注</td>
<td class="px-6 py-4 text-sm text-gray-500">NOTE:这是一个备注</td>
<td class="px-6 py-4 text-sm text-gray-500">NOTE:这是一个备注</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">CATEGORIES</td>
<td class="px-6 py-4 text-sm text-gray-500">分类/标签</td>
<td class="px-6 py-4 text-sm text-gray-500">CATEGORIES:朋友,家人</td>
<td class="px-6 py-4 text-sm text-gray-500">CATEGORIES:朋友,家人</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">PHOTO</td>
<td class="px-6 py-4 text-sm text-gray-500">照片</td>
<td class="px-6 py-4 text-sm text-gray-500">PHOTO;TYPE=JPEG;VALUE=URL:[img]http://example.com/photo.jpg[/img]</td>
<td class="px-6 py-4 text-sm text-gray-500">PHOTO:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">GEO</td>
<td class="px-6 py-4 text-sm text-gray-500">地理位置</td>
<td class="px-6 py-4 text-sm text-gray-500">不支持</td>
<td class="px-6 py-4 text-sm text-gray-500">GEO:geo:39.9042,116.4074</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">SOCIALPROFILE</td>
<td class="px-6 py-4 text-sm text-gray-500">社交账号</td>
<td class="px-6 py-4 text-sm text-gray-500">不支持</td>
<td class="px-6 py-4 text-sm text-gray-500">SOCIALPROFILE;TYPE=wechat:zhangsan</td>
</tr>
</tbody>
</table>
</div>
</div>

<div>
<h4 class="text-lg font-medium text-gray-900 mb-2">版本差异总结</h4>
<ul class="list-disc pl-5 space-y-2 text-sm text-gray-700">
<li><strong>编码</strong>: 3.0 需要指定 CHARSET=utf-8,4.0 默认使用 UTF-8</li>
<li><strong>参数格式</strong>: 3.0 使用 TYPE=CELL,4.0 使用 type=cell(小写)</li>
<li><strong>电话格式</strong>: 4.0 支持 URI 格式(tel:+86-138-0013-8000)</li>
<li><strong>新增字段</strong>: 4.0 增加了 ROLE、GEO、SOCIALPROFILE 等字段</li>
<li><strong>数据类型</strong>: 4.0 支持更丰富的数据类型(如 URL、URI)</li>
<li><strong>兼容性</strong>: 3.0 被更广泛支持,4.0 需较新的设备或软件支持</li>
</ul>
</div>
</div>
</div>
</div>
</div>

<script>
// 全局变量
let csvData = [];
let headers = [];
let vcardData = [];
let delimiter = ',';
let hasHeader = true;
let encoding = 'utf-8';
let vcardVersion = '3.0';

// 模板CSV数据(包含所有vCard字段的示例)
const templateHeaders = [
'姓名 (FN)', 
'姓氏 (N)', 
'名字 (N)', 
'中间名 (N)', 
'前缀 (N)', 
'后缀 (N)',
'昵称 (NICKNAME)',
'电话 (TEL)', 
'手机 (TEL)',
'工作电话 (TEL)',
'家庭电话 (TEL)',
'传真 (TEL)',
'邮箱 (EMAIL)',
'公司 (ORG)',
'部门 (ORG)',
'职位 (TITLE)',
'角色 (ROLE)',
'地址 (ADR)',
'城市 (ADR)',
'省份 (ADR)',
'邮编 (ADR)',
'国家 (ADR)',
'网址 (URL)',
'生日 (BDAY)',
'纪念日 (ANNIVERSARY)',
'备注 (NOTE)',
'分类 (CATEGORIES)',
'照片URL (PHOTO)',
'社交账号 (SOCIALPROFILE)',
'地理位置 (GEO)'
];

const templateData = [
[
'张三', '张', '三', '', '先生', '', '小张',
'13800138000', '13900139000', '010-88888888', '010-99999999', '010-77777777',
'zhangsan@example.com',
'科技有限公司', '研发部', '软件工程师', '开发者',
'北京市朝阳区科技园', '北京市', '朝阳区', '100000', '中国',
'https://example.com',
'1990-01-01',
'2020-05-20',
'这是张三的备注信息',
'同事,朋友',
'https://picsum.photos/200/300',
'wechat:zhangsan',
'39.9042,116.4074'
],
[
'李四', '李', '四', '小明', '博士', '', '小李',
'13700137000', '13600136000', '021-88887777', '021-99996666', '021-77775555',
'lisi@example.com',
'设计公司', '创意部', '设计师', '艺术家',
'上海市静安区南京西路', '上海市', '静安区', '200000', '中国',
'https://lisi.design',
'1985-10-15',
'2018-12-25',
'这是李四的备注信息',
'"朋友,家人"',
'https://picsum.photos/200/301',
'weibo:lisi',
'31.2304,121.4737'
]
];

// DOM 元素
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('file-input');
const fileName = document.getElementById('file-name');
const optionsSection = document.getElementById('options-section');
const mappingSection = document.getElementById('mapping-section');
const previewSection = document.getElementById('preview-section');
const contactCount = document.getElementById('contact-count');
const previewTable = document.getElementById('preview-table');
const convertBtn = document.getElementById('convert-btn');
const backBtn = document.getElementById('back-btn');
const downloadBtn = document.getElementById('download-btn');
const advancedToggle = document.getElementById('advanced-toggle');
const advancedOptions = document.getElementById('advanced-options');
const fieldMapping = document.getElementById('field-mapping');
const helpBtn = document.getElementById('help-btn');
const closeHelpBtn = document.getElementById('close-help-btn');
const helpModal = document.getElementById('help-modal');
const generateTemplateBtn = document.getElementById('generate-template-btn');

// 初始化事件监听
function initEventListeners() {
// 文件拖放
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});

function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}

['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});

['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});

function highlight() {
dropArea.classList.add('border-primary', 'bg-blue-50');
}

function unhighlight() {
dropArea.classList.remove('border-primary', 'bg-blue-50');
}

dropArea.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', handleFileSelect, false);

// 选项变更
document.querySelectorAll('input[name="delimiter"]').forEach(radio => {
radio.addEventListener('change', function() {
delimiter = this.value === 'tab' ? '\t' : this.value;
});
});

document.getElementById('has-header').addEventListener('change', function() {
hasHeader = this.checked;
});

document.getElementById('encoding').addEventListener('change', function() {
encoding = this.value;
});

document.getElementById('vcard-version').addEventListener('change', function() {
vcardVersion = this.value;
});

// 高级选项切换
advancedToggle.addEventListener('click', function() {
const icon = this.querySelector('i');
if (advancedOptions.style.maxHeight) {
advancedOptions.style.maxHeight = null;
icon.classList.remove('rotate-180');
} else {
advancedOptions.style.maxHeight = advancedOptions.scrollHeight + 'px';
icon.classList.add('rotate-180');
}
});

// 按钮事件
convertBtn.addEventListener('click', convertToVcard);
backBtn.addEventListener('click', goBack);
downloadBtn.addEventListener('click', downloadVcard);
helpBtn.addEventListener('click', showHelp);
closeHelpBtn.addEventListener('click', hideHelp);
generateTemplateBtn.addEventListener('click', generateTemplate);
}

// 处理文件拖放
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;

if (files.length) {
handleFiles(files[0]);
}
}

// 处理文件选择
function handleFileSelect(e) {
const files = e.target.files;

if (files.length) {
handleFiles(files[0]);
}
}

// 处理文件
function handleFiles(file) {
if (!file.name.endsWith('.csv')) {
alert('请选择CSV文件!');
return;
}

fileName.textContent = `已选择: ${file.name}`;
fileName.classList.remove('hidden');

const reader = new FileReader();

reader.onload = function(e) {
try {
const content = e.target.result;
parseCsv(content);

// 显示选项区域
optionsSection.classList.remove('hidden');
setTimeout(() => {
optionsSection.style.opacity = '1';
}, 10);

// 显示映射区域
mappingSection.classList.remove('hidden');
setTimeout(() => {
mappingSection.style.opacity = '1';
}, 10);

// 生成字段映射UI
generateFieldMappingUI();

} catch (error) {
alert(`解析CSV文件时出错: ${error.message}`);
}
};

reader.onerror = function() {
alert('读取文件时出错!');
};

reader.readAsText(file, encoding);
}

// 解析CSV文件(改进引号处理)
function parseCsv(content) {
// 使用更健壮的CSV解析方法,处理引号内的逗号
const lines = content.split(/\r\n|\n|\r/).filter(line => line.trim() !== '');

if (lines.length === 0) {
throw new Error('CSV文件为空!');
}

// 获取分隔符
delimiter = document.querySelector('input[name="delimiter"]:checked').value;
delimiter = delimiter === 'tab' ? '\t' : delimiter;

// 健壮的CSV解析函数,处理引号内的分隔符
function parseCsvLine(line) {
const values = [];
let inQuotes = false;
let currentValue = '';

for (let i = 0; i < line.length; i++) {
const char = line[i];

if (char === '"') {
// 检查是否是双引号(即转义的引号)
if (i + 1 < line.length && line[i + 1] === '"') {
currentValue += '"';
i++; // 跳过下一个引号
} else {
inQuotes = !inQuotes;
}
} else if (char === delimiter && !inQuotes) {
// 如果是分隔符且不在引号内,则分割字段
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}

// 添加最后一个字段
values.push(currentValue.trim());

// 处理引号的去除
return values.map(value => {
if (value.startsWith('"') && value.endsWith('"')) {
return value.substring(1, value.length - 1).replace(/""/g, '"');
}
return value;
});
}

// 处理标题行
if (hasHeader && lines.length > 0) {
headers = parseCsvLine(lines[0]);
csvData = lines.slice(1).map(line => {
const values = parseCsvLine(line);
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] ? values[index].trim() : '';
});
return row;
});
} else {
// 如果没有标题行,使用默认标题
headers = Array.from({ length: parseCsvLine(lines[0]).length }, (_, i) => `字段${i+1}`);
csvData = lines.map(line => {
const values = parseCsvLine(line);
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] ? values[index].trim() : '';
});
return row;
});
}

// 移除空行
csvData = csvData.filter(row => Object.values(row).some(value => value.trim() !== ''));

console.log('解析后的CSV数据:', csvData);
}

// 生成字段映射UI
function generateFieldMappingUI() {
// 清空现有映射
fieldMapping.innerHTML = '';

// 定义vCard字段(完整列表)
const vcardFields = [
'不映射',
'FN (姓名)', 
'N (全名)',
'NICKNAME (昵称)',
'TEL (电话)', 
'EMAIL (邮箱)', 
'ORG (组织)', 
'TITLE (职位)',
'ROLE (角色)',
'ADR (地址)', 
'URL (网址)',
'BDAY (生日)',
'ANNIVERSARY (纪念日)',
'NOTE (备注)',
'CATEGORIES (分类)',
'PHOTO (照片)',
'SOCIALPROFILE (社交账号)',
'GEO (地理位置)'
];

// 为每个CSV列创建一个映射选择
headers.forEach(header => {
const mappingRow = document.createElement('div');
mappingRow.className = 'flex items-center';

const label = document.createElement('label');
label.className = 'w-1/3 text-gray-700';
label.textContent = header;

const select = document.createElement('select');
select.className = 'w-2/3 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50';

// 添加选项
vcardFields.forEach(field => {
const option = document.createElement('option');
const fieldKey = field.split(' ')[0];
option.value = fieldKey;

// 自动匹配常见字段
if ((header.includes('姓名') || header.includes('名称')) && fieldKey === 'FN') {
option.selected = true;
} else if (header.includes('姓氏') && fieldKey === 'N') {
option.selected = true;
} else if (header.includes('名字') && fieldKey === 'N') {
option.selected = true;
} else if (header.includes('昵称') && fieldKey === 'NICKNAME') {
option.selected = true;
} else if (header.includes('电话') && fieldKey === 'TEL') {
option.selected = true;
} else if (header.includes('邮箱') && fieldKey === 'EMAIL') {
option.selected = true;
} else if (header.includes('公司') && fieldKey === 'ORG') {
option.selected = true;
} else if (header.includes('职位') && fieldKey === 'TITLE') {
option.selected = true;
} else if (header.includes('角色') && fieldKey === 'ROLE') {
option.selected = true;
} else if (header.includes('地址') && fieldKey === 'ADR') {
option.selected = true;
} else if (header.includes('网址') && fieldKey === 'URL') {
option.selected = true;
} else if (header.includes('生日') && fieldKey === 'BDAY') {
option.selected = true;
} else if (header.includes('纪念日') && fieldKey === 'ANNIVERSARY') {
option.selected = true;
} else if (header.includes('备注') && fieldKey === 'NOTE') {
option.selected = true;
} else if (header.includes('分类') && fieldKey === 'CATEGORIES') {
option.selected = true;
} else if (header.includes('照片') && fieldKey === 'PHOTO') {
option.selected = true;
} else if (header.includes('社交') && fieldKey === 'SOCIALPROFILE') {
option.selected = true;
} else if (header.includes('地理') && fieldKey === 'GEO') {
option.selected = true;
}

option.textContent = field;
select.appendChild(option);
});

mappingRow.appendChild(label);
mappingRow.appendChild(select);
fieldMapping.appendChild(mappingRow);
});
}

// 转换为vCard格式
function convertToVcard() {
vcardData = [];

// 获取映射关系
const mappings = {};
const selects = fieldMapping.querySelectorAll('select');
headers.forEach((header, index) => {
mappings[header] = selects[index].value;
});

// 转换每一行数据为vCard
csvData.forEach(row => {
const vcard = {
version: vcardVersion,
FN: '',
N: '',
NICKNAME: '',
TEL: [],
EMAIL: [],
ORG: '',
TITLE: '',
ROLE: '',
ADR: '',
URL: '',
BDAY: '',
ANNIVERSARY: '',
NOTE: '',
CATEGORIES: '',
PHOTO: '',
SOCIALPROFILE: '',
GEO: ''
};

// 处理姓名字段
let lastName = '';
let firstName = '';
let middleName = '';
let prefix = '';
let suffix = '';

// 填充vCard数据
Object.keys(row).forEach(header => {
const value = row[header];
if (!value) return;

const vcardField = mappings[header];
if (!vcardField || vcardField === '不映射') return;

if (vcardField === 'N') {
// 特殊处理:如果同时有"姓氏"和"名字"字段
if (header.includes('姓氏')) {
lastName = value;
} else if (header.includes('名字')) {
firstName = value;
} else if (header.includes('中间名')) {
middleName = value;
} else if (header.includes('前缀')) {
prefix = value;
} else if (header.includes('后缀')) {
suffix = value;
} else {
// 如果只有一个N字段,尝试自动分割
const nameParts = value.split(' ');
lastName = nameParts.length > 1 ? nameParts[1] : '';
firstName = nameParts.length > 0 ? nameParts[0] : '';
}

vcard.N = `${lastName};${firstName};${middleName};${prefix};${suffix}`;

// 如果FN为空,使用N生成FN
if (!vcard.FN) {
vcard.FN = `${firstName}${lastName}`;
}
} else if (vcardField === 'FN') {
vcard.FN = value;
} else if (vcardField === 'NICKNAME') {
vcard.NICKNAME = value;
} else if (vcardField === 'TEL') {
// 根据列名确定电话类型
let type = 'CELL';
if (header.includes('手机')) type = 'CELL';
else if (header.includes('工作')) type = 'WORK';
else if (header.includes('家庭')) type = 'HOME';
else if (header.includes('传真')) type = 'FAX';

vcard.TEL.push({ type, value });
} else if (vcardField === 'EMAIL') {
vcard.EMAIL.push(value);
} else if (vcardField === 'ORG') {
// 检查是否有部门信息
if (header.includes('部门') && vcard.ORG) {
vcard.ORG += `;${value}`;
} else {
vcard.ORG = value;
}
} else if (vcardField === 'TITLE') {
vcard.TITLE = value;
} else if (vcardField === 'ROLE') {
vcard.ROLE = value;
} else if (vcardField === 'ADR') {
// 特殊处理:如果同时有"地址"、"城市"等字段
if (header.includes('地址')) {
vcard.ADR = `;;${value}`;
} else if (header.includes('城市')) {
if (vcard.ADR) {
const parts = vcard.ADR.split(';');
parts[3] = value;
vcard.ADR = parts.join(';');
} else {
vcard.ADR = `;;;${value}`;
}
} else if (header.includes('省份')) {
if (vcard.ADR) {
const parts = vcard.ADR.split(';');
parts[4] = value;
vcard.ADR = parts.join(';');
} else {
vcard.ADR = `;;;;${value}`;
}
} else if (header.includes('邮编')) {
if (vcard.ADR) {
const parts = vcard.ADR.split(';');
parts[5] = value;
vcard.ADR = parts.join(';');
} else {
vcard.ADR = `;;;;;${value}`;
}
} else if (header.includes('国家')) {
if (vcard.ADR) {
const parts = vcard.ADR.split(';');
parts[6] = value;
vcard.ADR = parts.join(';');
} else {
vcard.ADR = `;;;;;;${value}`;
}
} else {
vcard.ADR = `;;${value}`;
}
} else if (vcardField === 'URL') {
vcard.URL = value;
} else if (vcardField === 'BDAY') {
vcard.BDAY = value;
} else if (vcardField === 'ANNIVERSARY') {
vcard.ANNIVERSARY = value;
} else if (vcardField === 'NOTE') {
vcard.NOTE = value;
} else if (vcardField === 'CATEGORIES') {
// 处理可能包含引号的分类
let categories = value;
if (categories.startsWith('"') && categories.endsWith('"')) {
categories = categories.substring(1, categories.length - 1);
}
vcard.CATEGORIES = categories;
} else if (vcardField === 'PHOTO') {
vcard.PHOTO = value;
} else if (vcardField === 'SOCIALPROFILE') {
vcard.SOCIALPROFILE = value;
} else if (vcardField === 'GEO') {
vcard.GEO = value;
}
});

// 如果FN仍然为空,但N有值,则从N生成FN
if (!vcard.FN && vcard.N) {
const nameParts = vcard.N.split(';');
vcard.FN = `${nameParts[1]}${nameParts[0]}`;
}

vcardData.push(vcard);
});

// 显示预览
showPreview();
}

// 显示预览
function showPreview() {
previewTable.innerHTML = '';
contactCount.textContent = vcardData.length;

vcardData.forEach((vcard, index) => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors';

// 姓名
const nameCell = document.createElement('td');
nameCell.className = 'px-6 py-4 whitespace-nowrap';
nameCell.textContent = vcard.FN || '未指定姓名';

// 电话
const phoneCell = document.createElement('td');
phoneCell.className = 'px-6 py-4 whitespace-nowrap';
if (vcard.TEL.length > 0) {
phoneCell.textContent = vcard.TEL[0].value;
} else {
phoneCell.textContent = '无电话';
}

// 邮箱
const emailCell = document.createElement('td');
emailCell.className = 'px-6 py-4 whitespace-nowrap';
if (vcard.EMAIL.length > 0) {
emailCell.textContent = vcard.EMAIL[0];
} else {
emailCell.textContent = '无邮箱';
}

// 操作
const actionCell = document.createElement('td');
actionCell.className = 'px-6 py-4 whitespace-nowrap text-sm font-medium';

const viewBtn = document.createElement('button');
viewBtn.className = 'text-primary hover:text-primary/80 mr-3';
viewBtn.textContent = '查看详情';
viewBtn.addEventListener('click', () => {
showVcardDetails(vcard);
});

actionCell.appendChild(viewBtn);

row.appendChild(nameCell);
row.appendChild(phoneCell);
row.appendChild(emailCell);
row.appendChild(actionCell);

previewTable.appendChild(row);
});

// 显示预览区域
previewSection.classList.remove('hidden');
setTimeout(() => {
previewSection.style.opacity = '1';
}, 10);
}

// 显示vCard详情
function showVcardDetails(vcard) {
// 创建模态框
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';

const modalContent = document.createElement('div');
modalContent.className = 'bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto';

// 标题栏
const header = document.createElement('div');
header.className = 'px-6 py-4 border-b border-gray-200 flex justify-between items-center';

const title = document.createElement('h3');
title.className = 'text-xl font-semibold text-gray-900';
title.textContent = vcard.FN || '未指定姓名';

const closeBtn = document.createElement('button');
closeBtn.className = 'text-gray-400 hover:text-gray-500';
closeBtn.innerHTML = '<i class="fa-solid fa-times"></i>';
closeBtn.addEventListener('click', () => {
document.body.removeChild(modal);
});

header.appendChild(title);
header.appendChild(closeBtn);

// 内容区域
const content = document.createElement('div');
content.className = 'px-6 py-4';

// 生成vCard文本
let vcardText = `BEGIN:VCARD\nVERSION:${vcard.version}\n`;

if (vcard.FN) vcardText += `FN:${vcard.FN}\n`;
if (vcard.N) vcardText += `N:${vcard.N}\n`;
if (vcard.NICKNAME) vcardText += `NICKNAME:${vcard.NICKNAME}\n`;

vcard.TEL.forEach(tel => {
if (vcard.version === '3.0') {
vcardText += `TEL;TYPE=${tel.type}:${tel.value}\n`;
} else {
vcardText += `TEL;type=${tel.type.toLowerCase()}:tel:${tel.value}\n`;
}
});

vcard.EMAIL.forEach(email => {
vcardText += `EMAIL:${email}\n`;
});

if (vcard.ORG) vcardText += `ORG:${vcard.ORG}\n`;
if (vcard.TITLE) vcardText += `TITLE:${vcard.TITLE}\n`;
if (vcard.ROLE) vcardText += `ROLE:${vcard.ROLE}\n`;
if (vcard.ADR) vcardText += `ADR:${vcard.ADR}\n`;
if (vcard.URL) vcardText += `URL:${vcard.URL}\n`;
if (vcard.BDAY) vcardText += `BDAY:${vcard.BDAY}\n`;
if (vcard.ANNIVERSARY) vcardText += `ANNIVERSARY:${vcard.ANNIVERSARY}\n`;
if (vcard.NOTE) vcardText += `NOTE:${vcard.NOTE}\n`;
if (vcard.CATEGORIES) vcardText += `CATEGORIES:${vcard.CATEGORIES}\n`;
if (vcard.PHOTO) {
if (vcard.version === '3.0') {
vcardText += `PHOTO;TYPE=JPEG;VALUE=URL:${vcard.PHOTO}\n`;
} else {
vcardText += `PHOTO:${vcard.PHOTO}\n`;
}
}
if (vcard.SOCIALPROFILE) vcardText += `SOCIALPROFILE:${vcard.SOCIALPROFILE}\n`;
if (vcard.GEO) vcardText += `GEO:${vcard.GEO}\n`;

vcardText += 'END:VCARD';

const pre = document.createElement('pre');
pre.className = 'bg-gray-50 border border-gray-200 rounded-md p-4 overflow-x-auto text-sm';
pre.textContent = vcardText;

content.appendChild(pre);

modalContent.appendChild(header);
modalContent.appendChild(content);
modal.appendChild(modalContent);

document.body.appendChild(modal);
}

// 返回
function goBack() {
previewSection.style.opacity = '0';
setTimeout(() => {
previewSection.classList.add('hidden');
}, 300);
}

// 下载vCard文件
function downloadVcard() {
if (vcardData.length === 0) {
alert('没有可下载的vCard数据!');
return;
}

// 生成vCard文件内容
let vcardContent = '';

vcardData.forEach(vcard => {
vcardContent += `BEGIN:VCARD\nVERSION:${vcard.version}\n`;

if (vcard.FN) vcardContent += `FN:${vcard.FN}\n`;
if (vcard.N) vcardContent += `N:${vcard.N}\n`;
if (vcard.NICKNAME) vcardContent += `NICKNAME:${vcard.NICKNAME}\n`;

vcard.TEL.forEach(tel => {
if (vcard.version === '3.0') {
vcardContent += `TEL;TYPE=${tel.type}:${tel.value}\n`;
} else {
vcardContent += `TEL;type=${tel.type.toLowerCase()}:tel:${tel.value}\n`;
}
});

vcard.EMAIL.forEach(email => {
vcardContent += `EMAIL:${email}\n`;
});

if (vcard.ORG) vcardContent += `ORG:${vcard.ORG}\n`;
if (vcard.TITLE) vcardContent += `TITLE:${vcard.TITLE}\n`;
if (vcard.ROLE) vcardContent += `ROLE:${vcard.ROLE}\n`;
if (vcard.ADR) vcardContent += `ADR:${vcard.ADR}\n`;
if (vcard.URL) vcardContent += `URL:${vcard.URL}\n`;
if (vcard.BDAY) vcardContent += `BDAY:${vcard.BDAY}\n`;
if (vcard.ANNIVERSARY) vcardContent += `ANNIVERSARY:${vcard.ANNIVERSARY}\n`;
if (vcard.NOTE) vcardContent += `NOTE:${vcard.NOTE}\n`;
if (vcard.CATEGORIES) vcardContent += `CATEGORIES:${vcard.CATEGORIES}\n`;
if (vcard.PHOTO) {
if (vcard.version === '3.0') {
vcardContent += `PHOTO;TYPE=JPEG;VALUE=URL:${vcard.PHOTO}\n`;
} else {
// 4.0版本需要base64编码,但这里是URL,所以保持不变
vcardContent += `PHOTO:${vcard.PHOTO}\n`;
}
}
if (vcard.SOCIALPROFILE) vcardContent += `SOCIALPROFILE:${vcard.SOCIALPROFILE}\n`;
if (vcard.GEO) vcardContent += `GEO:${vcard.GEO}\n`;

vcardContent += 'END:VCARD\n\n';
});

// 创建Blob对象
const blob = new Blob([vcardContent], { type: 'text/vcard;charset=utf-8' });

// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;

// 设置文件名
const fileName = `contacts_${new Date().toISOString().slice(0,10)}.vcf`;
a.download = fileName;

// 触发下载
document.body.appendChild(a);
a.click();

// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);

// 显示下载成功提示
showNotification('下载成功!', 'success');
}

// 显示通知
function showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');

// 设置样式
if (type === 'success') {
notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-y-10 opacity-0';
} else if (type === 'error') {
notification.className = 'fixed bottom-4 right-4 bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-y-10 opacity-0';
} else {
notification.className = 'fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300 translate-y-10 opacity-0';
}

// 设置内容
notification.textContent = message;

// 添加到文档
document.body.appendChild(notification);

// 显示通知
setTimeout(() => {
notification.classList.remove('translate-y-10', 'opacity-0');
}, 10);

// 3秒后隐藏通知
setTimeout(() => {
notification.classList.add('translate-y-10', 'opacity-0');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}

// 生成CSV模板
function generateTemplate() {
// 创建CSV内容
let csvContent = templateHeaders.join(delimiter) + '\n';

templateData.forEach(row => {
// 处理包含逗号的值,用双引号包裹
const escapedRow = row.map(value => {
if (typeof value === 'string' && value.includes(delimiter)) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
});

csvContent += escapedRow.join(delimiter) + '\n';
});

// 创建Blob对象
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });

// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'contacts_template.csv';

// 触发下载
document.body.appendChild(a);
a.click();

// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);

// 显示下载成功提示
showNotification('模板下载成功!', 'success');
}

// 显示帮助
function showHelp() {
helpModal.classList.remove('hidden');
}

// 隐藏帮助
function hideHelp() {
helpModal.classList.add('hidden');
}

// 初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
});
</script>
</body>
</html>

问题未解决?付费解决问题加Q或微信 2589053300 (即Q号又微信号)右上方扫一扫可加博主微信

所写所说,是心之所感,思之所悟,行之所得;文当无敷衍,落笔求简洁。 以所舍,求所获;有所依,方所成!

支付宝赞助
微信赞助

免责声明,若由于商用引起版权纠纷,一切责任均由使用者承担。

您必须遵守我们的协议,如您下载该资源,行为将被视为对《免责声明》全部内容的认可->联系老梁投诉资源
LaoLiang.Net部分资源来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。 敬请谅解! 侵权删帖/违法举报/投稿等事物联系邮箱:service@laoliang.net
意在交流学习,欢迎赞赏评论,如有谬误,请联系指正;转载请注明出处: » HTML版本“CSV to vCard(.vcf) 转换器”

发表回复

本站承接,网站推广(SEM,SEO);软件安装与调试;服务器或网络推荐及配置;APP开发与维护;网站开发修改及维护; 各财务软件安装调试及注册服务(金蝶,用友,管家婆,速达,星宇等);同时也有客户管理系统,人力资源,超市POS,医药管理等;

立即查看 了解详情