一,目标
构建一个模块化、可扩展、支持 PWA 的前端工具小站,支持以下工具:
- JSON 格式化
- Base64 编解码
- 时间戳转换(Unix Timestamp ↔ 日期)
- 字段命名转换(驼峰 ↔ 下划线 ↔ 横线)
二,项目结构与核心框架(PWA + 模块化基础)
一、最终项目结构(模块化设计)
bash
tools-site/
│
├── index.html # 主页(工具列表)
├── manifest.json # PWA 配置
├── sw.js # Service Worker
│
├── css/
│ └── style.css # 全局样式
│
├── js/
│ ├── main.js # 主逻辑(注册 SW)
│ └── tools/ # 工具模块目录
│ ├── json-formatter.js # JSON 格式化
│ ├── base64-encoder.js # Base64 编解码
│ ├── timestamp-converter.js # 时间戳转换
│ └── case-converter.js # 命名转换(驼峰/下划线等)
│
└── assets/
├── favicon.svg
├── icon-72x72.png
├── icon-96x96.png
├── icon-128x128.png
├── icon-192x192.png
└── icon-512x512.png
所有工具独立成 JS 文件,便于维护和扩展。
二、核心文件(PWA + 页面结构)
1. index.html(主页面,工具入口)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name="description" content="轻量级在线工具集合:JSON格式化、Base64编解码、时间戳转换、字段命名转换等" />
<title>工具小站</title>
<!-- PWA 支持 -->
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#2c3e50">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="工具小站">
<link rel="apple-touch-icon" href="assets/icon-192x192.png">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="icon" type="image/png" href="assets/icon-192x192.png">
<!-- 样式 -->
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>工具小站</h1>
<p>轻量级在线工具集合 | 支持离线使用</p>
</header>
<main>
<div class="tool-grid">
<a href="#" class="tool-card" onclick="showTool('json'); return false;">
<h3>JSON 格式化</h3>
<p>美化或压缩 JSON 字符串</p>
</a>
<a href="#" class="tool-card" onclick="showTool('base64'); return false;">
<h3>Base64 编解码</h3>
<p>文本与 Base64 互转</p>
</a>
<a href="#" class="tool-card" onclick="showTool('timestamp'); return false;">
<h3>时间戳转换</h3>
<p>Unix 时间戳 ↔ 日期时间</p>
</a>
<a href="#" class="tool-card" onclick="showTool('case'); return false;">
<h3>命名转换</h3>
<p>驼峰、下划线、横线互转</p>
</a>
</div>
<!-- 工具容器 -->
<div id="tool-container" class="tool-container"></div>
</main>
<footer>
<p>© 2025 工具小站 | 支持离线使用</p>
</footer>
</div>
<!-- 工具模块 -->
<script src="js/tools/json-formatter.js"></script>
<script src="js/tools/base64-encoder.js"></script>
<script src="js/tools/timestamp-converter.js"></script>
<script src="js/tools/case-converter.js"></script>
<!-- 主逻辑 -->
<script src="js/main.js"></script>
</body>
</html>
2. css/style.css(全局样式)
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
font-size: 2.5rem;
color: #2c3e50;
}
header p {
color: #7f8c8d;
font-size: 1.1rem;
}
/* 工具卡片网格 */
.tool-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.tool-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-decoration: none;
color: #2c3e50;
transition: transform 0.2s;
}
.tool-card:hover {
transform: translateY(-4px);
}
.tool-card h3 {
margin-bottom: 8px;
font-size: 1.2rem;
}
.tool-card p {
color: #7f8c8d;
font-size: 0.9rem;
}
/* 工具容器 */
.tool-container {
display: none;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.tool-container.active {
display: block;
}
.tool-container h2 {
color: #2c3e50;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
textarea {
width: 100%;
height: 120px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-family: monospace;
font-size: 14px;
resize: vertical;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 16px;
margin: 10px 5px 0 0;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #2980b9;
}
pre {
background: #f1f1f1;
padding: 15px;
border-radius: 8px;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 10px;
font-family: monospace;
font-size: 14px;
}
footer {
text-align: center;
margin-top: 50px;
color: #95a5a6;
font-size: 0.9rem;
}
3. js/main.js(主逻辑)
javascript
// 显示指定工具
function showTool(toolName) {
// 隐藏所有工具
const containers = document.querySelectorAll('.tool-container');
containers.forEach(c => c.classList.remove('active'));
// 获取或创建工具容器
let toolContainer = document.getElementById(`tool-${toolName}`);
if (!toolContainer) {
toolContainer = document.createElement('div');
toolContainer.id = `tool-${toolName}`;
toolContainer.className = 'tool-container active';
document.getElementById('tool-container').appendChild(toolContainer);
// 调用对应工具的渲染函数
if (window[`render${capitalize(toolName)}Tool`]) {
window[`render${capitalize(toolName)}Tool`](toolContainer);
}
} else {
toolContainer.classList.add('active');
}
}
// 工具名首字母大写
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// 注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/tools-site/sw.js')
.then(registration => {
console.log('✅ Service Worker 注册成功:', registration.scope);
})
.catch(error => {
console.log('❌ Service Worker 注册失败:', error);
});
});
}
4. manifest.json(PWA 配置)
json
{
"name": "工具小站",
"short_name": "工具箱",
"description": "轻量级在线工具集合:JSON格式化、Base64编解码、时间戳转换、命名转换",
"start_url": "/tools-site/",
"scope": "/tools-site/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2c3e50",
"orientation": "portrait-primary",
"lang": "zh-CN",
"icons": [
{
"src": "assets/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "assets/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "assets/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "assets/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
5. sw.js(Service Worker - 离线缓存)
javascript
const CACHE_NAME = 'tools-site-v3';
const urlsToCache = [
'/tools-site/',
'/tools-site/index.html',
'/tools-site/manifest.json',
'/tools-site/css/style.css',
'/tools-site/js/main.js',
'/tools-site/js/tools/json-formatter.js',
'/tools-site/js/tools/base64-encoder.js',
'/tools-site/js/tools/timestamp-converter.js',
'/tools-site/js/tools/case-converter.js',
'/tools-site/assets/favicon.svg',
'/tools-site/assets/icon-192x192.png',
'/tools-site/assets/icon-512x512.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
self.addEventListener('fetch', (event) => {
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
}
});
太好了!现在我们进入 第二步:完整实现 4 个工具模块 ,每个工具都独立封装在 js/tools/ 目录下,支持动态加载、交互完整、样式统一。
三,工具模块实现(4 个完整工具)
工具 1:json-formatter.js(JSON 格式化)
javascript
// js/tools/json-formatter.js
// 渲染 JSON 格式化工具界面
function renderJsonTool(container) {
container.innerHTML = `
<h2>JSON 格式化</h2>
<p>输入 JSON 字符串,支持格式化或压缩</p>
<textarea id="jsonInput" placeholder="输入 JSON 字符串,例如:{\"name\":\"张三\",\"age\":25}"></textarea>
<div class="button-group">
<button onclick="formatJSON()">格式化</button>
<button onclick="minifyJSON()">压缩</button>
<button onclick="clearJSON()">清空</button>
</div>
<pre id="jsonOutput" class="output"></pre>
`;
}
// 格式化 JSON
function formatJSON() {
const input = document.getElementById('jsonInput').value.trim();
const output = document.getElementById('jsonOutput');
if (!input) {
output.textContent = '请输入 JSON 内容';
return;
}
try {
const obj = JSON.parse(input);
output.textContent = JSON.stringify(obj, null, 2);
} catch (e) {
output.textContent = '格式错误:' + e.message;
}
}
// 压缩 JSON
function minifyJSON() {
const input = document.getElementById('jsonInput').value.trim();
const output = document.getElementById('jsonOutput');
if (!input) {
output.textContent = '请输入 JSON 内容';
return;
}
try {
const obj = JSON.parse(input);
output.textContent = JSON.stringify(obj);
} catch (e) {
output.textContent = '格式错误:' + e.message;
}
}
// 清空
function clearJSON() {
document.getElementById('jsonInput').value = '';
document.getElementById('jsonOutput').textContent = '';
}
工具 2:base64-encoder.js(Base64 编解码)
javascript
// js/tools/base64-encoder.js
// 渲染 Base64 工具界面
function renderBase64Tool(container) {
container.innerHTML = `
<h2>Base64 编解码</h2>
<p>文本与 Base64 字符串互转</p>
<textarea id="base64Input" placeholder="输入要编码或解码的文本"></textarea>
<div class="button-group">
<button onclick="encodeBase64()">编码 → Base64</button>
<button onclick="decodeBase64()">解码 ← Base64</button>
<button onclick="clearBase64()">清空</button>
</div>
<pre id="base64Output" class="output"></pre>
`;
}
// 编码为 Base64
function encodeBase64() {
const input = document.getElementById('base64Input').value;
const output = document.getElementById('base64Output');
if (!input) {
output.textContent = '请输入文本';
return;
}
try {
// 支持中文
const encoded = btoa(unescape(encodeURIComponent(input)));
output.textContent = encoded;
} catch (e) {
output.textContent = '编码失败:' + e.message;
}
}
// 从 Base64 解码
function decodeBase64() {
const input = document.getElementById('base64Input').value;
const output = document.getElementById('base64Output');
if (!input) {
output.textContent = '请输入 Base64 字符串';
return;
}
try {
const decoded = decodeURIComponent(escape(atob(input)));
output.textContent = decoded;
} catch (e) {
output.textContent = '解码失败:可能不是有效的 Base64';
}
}
// 清空
function clearBase64() {
document.getElementById('base64Input').value = '';
document.getElementById('base64Output').textContent = '';
}
工具 3:timestamp-converter.js(时间戳转换)
javascript
// js/tools/timestamp-converter.js
// 渲染时间戳工具界面
function renderTimestampTool(container) {
container.innerHTML = `
<h2>时间戳转换</h2>
<p>Unix 时间戳 ↔ 日期时间(支持秒/毫秒)</p>
<div class="input-group">
<label>时间戳:</label>
<input type="text" id="timestampInput" placeholder="输入时间戳(13位毫秒或10位秒)">
<button onclick="convertTimestampToDate()">→ 转日期</button>
</div>
<div class="input-group">
<label>日期时间:</label>
<input type="datetime-local" id="dateInput">
<button onclick="convertDateToTimestamp()">→ 转时间戳</button>
</div>
<div class="result-group">
<h3>转换结果:</h3>
<pre id="timestampResult" class="output"></pre>
</div>
<div class="button-group">
<button onclick="setNowTimestamp()">设为当前时间</button>
<button onclick="clearTimestamp()">清空</button>
</div>
`;
}
// 时间戳 → 日期
function convertTimestampToDate() {
const input = document.getElementById('timestampInput').value.trim();
const result = document.getElementById('timestampResult');
if (!input) {
result.textContent = '请输入时间戳';
return;
}
const timestamp = Number(input);
if (isNaN(timestamp)) {
result.textContent = '请输入有效数字';
return;
}
// 判断是秒还是毫秒
const date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000);
if (isNaN(date.getTime())) {
result.textContent = '无效的时间戳';
return;
}
result.textContent = `日期:${formatDateTime(date)}\n时区:${date.toString().match(/\((.+)\)/)[1]}`;
}
// 日期 → 时间戳
function convertDateToTimestamp() {
const input = document.getElementById('dateInput').value;
const result = document.getElementById('timestampResult');
if (!input) {
result.textContent = '请选择日期时间';
return;
}
const date = new Date(input);
const timestampSec = Math.floor(date.getTime() / 1000);
const timestampMs = date.getTime();
result.textContent = `秒级时间戳:${timestampSec}\n毫秒级时间戳:${timestampMs}`;
}
// 设置当前时间
function setNowTimestamp() {
const now = new Date();
document.getElementById('dateInput').value = now.toISOString().slice(0, 16);
document.getElementById('timestampInput').value = now.getTime();
convertTimestampToDate();
}
// 清空
function clearTimestamp() {
document.getElementById('timestampInput').value = '';
document.getElementById('dateInput').value = '';
document.getElementById('timestampResult').textContent = '';
}
// 格式化日期显示
function formatDateTime(date) {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-');
}
工具 4:case-converter.js(命名转换)
javascript
// js/tools/case-converter.js
// 渲染命名转换工具界面
function renderCaseTool(container) {
container.innerHTML = `
<h2>命名转换</h2>
<p>支持驼峰、下划线、横线等格式互转</p>
<textarea id="caseInput" placeholder="输入字段名,例如:user_name 或 user-name 或 userName"></textarea>
<div class="button-group">
<button onclick="toCamelCase()">→ 驼峰命名 (userName)</button>
<button onclick="toSnakeCase()">→ 下划线 (user_name)</button>
<button onclick="toKebabCase()">→ 横线 (user-name)</button>
<button onclick="toPascalCase()">→ 大驼峰 (UserName)</button>
</div>
<div class="button-group">
<button onclick="clearCase()">清空</button>
</div>
<pre id="caseOutput" class="output"></pre>
`;
}
// 转驼峰命名 (userName)
function toCamelCase() {
const input = document.getElementById('caseInput').value;
const output = document.getElementById('caseOutput');
const result = convertCase(input, 'camel');
output.textContent = result;
}
// 转下划线 (user_name)
function toSnakeCase() {
const input = document.getElementById('caseInput').value;
const output = document.getElementById('caseOutput');
const result = convertCase(input, 'snake');
output.textContent = result;
}
// 转横线 (user-name)
function toKebabCase() {
const input = document.getElementById('caseInput').value;
const output = document.getElementById('caseOutput');
const result = convertCase(input, 'kebab');
output.textContent = result;
}
// 转大驼峰 (UserName)
function toPascalCase() {
const input = document.getElementById('caseInput').value;
const output = document.getElementById('caseOutput');
const result = convertCase(input, 'pascal');
output.textContent = result;
}
// 清空
function clearCase() {
document.getElementById('caseInput').value = '';
document.getElementById('caseOutput').textContent = '';
}
// 核心转换逻辑
function convertCase(str, type) {
if (!str) return '';
// 统一先转成单词数组
const words = str
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, ' ') // 非字母数字替换为空格
.split(/\s+/)
.filter(Boolean)
.map(word => word.toLowerCase());
if (words.length === 0) return str;
switch (type) {
case 'camel':
return words.map((word, i) =>
i === 0 ? word : capitalize(word)
).join('');
case 'snake':
return words.join('_');
case 'kebab':
return words.join('-');
case 'pascal':
return words.map(capitalize).join('');
default:
return str;
}
}
// 首字母大写
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
最终效果验证
| 功能 | 状态 |
|---|---|
| 点击"JSON 格式化"卡片 | 显示格式化界面,支持美化/压缩 |
| 点击"Base64 编解码" | 文本与 Base64 互转 |
| 点击"时间戳转换" | 秒/毫秒 ↔ 日期,支持本地时区 |
| 点击"命名转换" | 驼峰/下划线/横线互转 |
| 手机访问 | 出现"安装"按钮 |
| 安装后离线打开 | 所有工具仍可使用(Service Worker 缓存) |