小时工记账-从零构建多端工时记录工具

本文分享一个多端工时记录工具的完整技术实现,涵盖纯原生前端架构、日历组件自研、离线数据持久化、大规模JSON静态化、SEO工程化等实践。项目地址:https://xiaoshigongjizhang.com

一、项目背景与技术选型

在开发这个工具之前,我调研了市面上现有的工时记录类 App,发现大多数产品要么功能臃肿、广告满天飞,要么只支持单一平台、数据无法同步。对于真正的目标用户------小时工、兼职人员、临时工来说,他们需要的只是一个打开就能用、多端能同步、没有广告干扰的轻量工具。

基于此,我梳理了核心诉求:

  1. 多端覆盖:微信小程序、iOS、Android、Windows、Web 五端统一体验
  2. 离线优先:网络不稳定场景下(如工厂车间、外卖骑手路上)也能正常记录
  3. 零后端依赖:个人项目,不想维护服务器和数据库,降低长期运营成本
  4. SEO 友好:Web 版需要被搜索引擎收录,让更多有需求的用户自然发现

基于以上约束,技术选型非常明确:

技术方案 理由
Web 纯 HTML/CSS/JS,无框架 极致轻量、首屏快、SEO 可控
小程序 原生微信小程序 用户获取成本低
iOS/Android 原生/跨平台方案 体验接近原生
Windows UWP / PWA 微软商店分发
数据存储 localStorage + 单条 JSON 零后端、按需加载

为什么不选 Vue/React?

这个项目 Web 版的核心逻辑并不复杂(日历渲染 + 表单 + 数据列表),引入框架反而会增加包体积和首屏加载时间。对于 SEO 场景,SSR 方案又需要额外的构建和部署成本。最终采用了"原生 JS + 模块化思维"的写法,把代码按功能拆成独立的函数块,保持可维护性。

二、核心功能实现

2.1 日历组件------纯 JS 实现

日历是整个项目最核心的交互组件。没有借助任何日期库(如 moment.js、dayjs),全部用原生 Date API 实现,最终压缩后代码不到 3KB。

日期计算核心逻辑:

javascript 复制代码
function renderCalendar() {
    const year = currentDate.getFullYear();
    const month = currentDate.getMonth();
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const startDay = firstDay.getDay(); // 本月1号是星期几
    const daysInMonth = lastDay.getDate(); // 本月总天数

    // 渲染日历网格:前补空位 + 本月日期 + 后补空位
    let html = '';
    // 星期标题
    const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
    weekdays.forEach(d => {
        html += `<div class="weekday-header">${d}</div>`;
    });
    // 前置空位
    for (let i = 0; i < startDay; i++) {
        html += `<div class="calendar-day other-month"></div>`;
    }
    // 本月日期
    const records = getRecords();
    const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
    for (let day = 1; day <= daysInMonth; day++) {
        const dateKey = `${monthKey}-${String(day).padStart(2, '0')}`;
        const record = records[dateKey];
        const hasRecord = record && (record.hours || record.income);
        html += `
            <div class="calendar-day ${hasRecord ? 'has-record' : ''}" 
                 onclick="openModal('${dateKey}')">
                <div class="day-number">${day}</div>
                ${hasRecord ? `
                    <div class="day-hours">${record.hours}h</div>
                    <div class="day-income">${record.income}元</div>
                ` : ''}
            </div>
        `;
    }
    document.getElementById('calendarGrid').innerHTML = html;
}

这里有两个工程细节值得注意:

  1. 日期 Key 规范 :统一用 YYYY-MM-DD 字符串作为 localStorage 的键,避免时区问题。toISOString().split('T')[0] 是最简单的方案。
  2. 性能优化:日历渲染是 O(n) 操作,240 条数据以内完全无感。如果数据量更大,可以考虑虚拟滚动,但当前场景不需要。

2.2 数据层------localStorage 的工程化封装

localStorage 虽然简单,但直接裸用会带来几个问题:异常数据导致 JSON 解析失败、并发写入冲突、存储空间限制(5MB)。

我的封装策略:

javascript 复制代码
const STORAGE_KEY = 'hourly_worker_records';

function getRecords() {
    try {
        return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
    } catch {
        // 数据损坏时自动回退,避免页面崩溃
        return {};
    }
}

function saveRecords(records) {
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(records));
    } catch (e) {
        if (e.name === 'QuotaExceededError') {
            showToast('存储空间不足,请清理历史记录');
        }
    }
}

数据结构设计:

json 复制代码
{
  "2024-05-01": { "hours": 8, "income": 200 },
  "2024-05-02": { "hours": 6.5, "income": 162.5 }
}

用日期字符串做 Key,而不是数组,是为了 O(1) 查询和更新。每个日期的数据量极小(两个数字),即使记录 10 年的数据,JSON 字符串也远不到 1MB,完全在 localStorage 的安全范围内。

实际使用中,很多用户会在月末回顾整月数据。为此在日历上方增加了月度统计面板(总工时、总收入、记录天数),用一次遍历即可算出,复杂度 O(n),240 条数据以内无感。

2.3 劳动法数据静态化------240 条 JSON 的工程实践

项目中有一个比较特殊的模块:劳动者权益保护法律手册。数据来源于一本 Word 文档,包含《劳动法》《劳动合同法》等 240 条法律条文。

数据拆分策略:

最初考虑过把所有数据塞进一个 laws.json,但 240 条条文 + 解析 + 案例,体积会很大,且用户通常只看其中几条。最终采用了"索引集中 + 单条拆分"的方案:

复制代码
data/
├── index.json          # 240 条索引(id/title/category/section/summary/tags)
└── law/
    ├── law_001.json    # 单条完整数据
    ├── law_002.json
    └── ...

索引结构设计:

json 复制代码
{
  "id": "law_001",
  "title": "第一条",
  "category": "劳动法",
  "section": "第一章  总则",
  "article": "第一条",
  "summary": "第一条  为了保护劳动者的合法权益...",
  "tags": ["劳动法"]
}

单条数据结构:

json 复制代码
{
  "id": "law_001",
  "title": "第一条",
  "category": "劳动法",
  "section": "第一章  总则",
  "article": "第一条",
  "content": "第一条  为了保护劳动者的合法权益...",
  "summary": "...",
  "tags": ["劳动法"],
  "analysis": "【条文解析】...",
  "case": "【典型案例】..."
}

这种设计的优势:

  • 列表页 只加载 index.json(约 200KB),前端内存过滤,无需后端
  • 详情页按需加载单条 JSON,平均 2-3KB,首屏极快
  • SEO 友好 :每个详情页都有独立 URL(detail.html?id=law_001),可被搜索引擎单独收录
  • 部署简单:纯静态文件,直接扔 nginx 即可

值得一提的是,这个法律手册模块上线后收到了不少用户正向反馈。很多小时工朋友表示,以前遇到拖欠工资、强制加班的情况不知道该怎么办,现在可以直接在工具里查到对应的法律条文和维权案例。这也让我意识到:技术工具除了解决效率问题,还可以承担一定的社会责任

批量处理脚本:

从 Word 到 JSON 的转换用 Python 脚本自动化处理,核心逻辑:

python 复制代码
import json, re, os

def parse_docx_to_entries(path):
    # 用 python-docx 解析文档,按段落拆分
    # 正则匹配"第X条"作为分割点
    ...

def split_to_single_json(entries):
    for i, entry in enumerate(entries, 1):
        filename = f"law_{i:03d}.json"
        with open(f"data/law/{filename}", 'w', encoding='utf-8') as f:
            json.dump(entry, f, ensure_ascii=False, indent=2)

后续还为每条补充了 analysis(条文解析)和 case(典型案例),用批量 Python 脚本写入,避免手动编辑 240 个文件。

三、搜索与目录------纯前端过滤的实现

法律手册页面需要支持搜索和分类目录。240 条数据量下,纯前端过滤完全够用。

搜索逻辑:

javascript 复制代码
function applyFilter() {
    const keywords = searchQuery.trim().toLowerCase().split(/\s+/).filter(k => k);
    filteredData = allData.filter(item => {
        if (currentCat && item.category !== currentCat) return false;
        if (currentSec && item.section !== currentSec) return false;
        if (keywords.length === 0) return true;
        const text = [
            item.title, item.summary, item.article,
            (item.tags || []).join(' ')
        ].join(' ').toLowerCase();
        return keywords.every(k => text.includes(k));
    });
}

支持多关键词 AND 搜索(空格分隔),支持分类联动过滤。实测在 240 条数据上,过滤延迟 < 5ms,无需 debounce 也能流畅,但还是加了 300ms debounce 防止输入过快时频繁渲染。

目录分组:

categorysection 两级分组,用原生 JS 构建侧边栏目录:

javascript 复制代码
catalogMap = {
  "劳动法": {
    "第一章 总则": 9,
    "第二章 促进就业": 3,
    ...
  },
  "劳动合同法": { ... }
};

点击章节后,右侧列表自动筛选并滚动定位。URL 同步 ?cat=劳动法&sec=第一章 总则,刷新后状态保留。

四、SEO 工程化------静态站的搜索优化

既然是面向公众的 Web 站点,SEO 必须认真做。以下是几个关键实践:

4.1 结构化数据

首页注入了 4 组 Schema:

html 复制代码
<!-- SoftwareApplication -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "小时工记账",
  "applicationCategory": "FinanceApplication",
  "offers": { "@type": "Offer", "price": "0", "priceCurrency": "CNY" },
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.8", "ratingCount": "10000" }
}
</script>

<!-- FAQPage -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [ ... ]
}
</script>

FAQPage 能让搜索引擎在结果页直接展示问答摘要,显著提升点击率。

4.2 动态 Meta 标签

详情页的内容是 JS 异步加载的,但搜索引擎爬虫不一定执行 JS。为此做了两层保障:

  1. 基础 Meta:HTML 中预置通用的 title/description/keywords
  2. 动态更新 :JS 加载数据后,通过 document.querySelector('meta[name="description"]').setAttribute('content', ...) 替换为精准内容

同时注入 BreadcrumbList Schema,强化页面层级关系:

javascript 复制代码
const breadcrumblist = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: [
        { '@type': 'ListItem', position: 1, name: '首页', item: 'https://xiaoshigongjizhang.com/' },
        { '@type': 'ListItem', position: 2, name: '劳动法', item: 'https://xiaoshigongjizhang.com/law.html?cat=劳动法' },
        { '@type': 'ListItem', position: 3, name: '第一条', item: 'https://xiaoshigongjizhang.com/detail.html?id=law_001' }
    ]
};

4.3 Sitemap 与 Robots

生成包含 243 个 URL 的 sitemap.xml(首页 + 法律手册 + 240 条详情页),配合 robots.txt 引导爬虫:

text 复制代码
User-agent: *
Allow: /
Sitemap: https://xiaoshigongjizhang.com/sitemap.xml

五、响应式与性能优化

5.1 响应式策略

采用 Mobile-First 的 CSS 写法,核心断点 768px:

css 复制代码
/* 桌面端:侧边栏固定 */
.law-layout { display: flex; gap: 20px; }
.sidebar { width: 260px; position: sticky; top: 20px; }

/* 移动端:侧边栏折叠 */
@media (max-width: 768px) {
    .law-layout { flex-direction: column; }
    .sidebar { display: none; width: 100%; position: static; }
    .sidebar.show { display: block; }
}

日历组件在小屏幕上做了自适应:网格间距缩小、日期单元格最小高度降低、字体缩小,保证在 375px 宽度的手机上也能正常显示。

5.2 性能指标

指标 数值 说明
首屏 HTML ~15KB 纯静态,无构建产物
index.json ~200KB 法律索引,一次性加载
单条 law JSON ~2-3KB 按需加载
首屏请求数 3 HTML + CSS(inline) + index.json
Lighthouse 性能 95+ 无框架、无大体积依赖

六、踩坑记录

6.1 Windows 终端编码陷阱

在 Windows PowerShell 下用 Python 处理中文 JSON 时,终端默认 GBK 编码会导致输出乱码。解决方案:所有文件读写显式指定 encoding='utf-8',避免依赖终端编码。

python 复制代码
with open(path, 'r', encoding='utf-8') as f:
    data = json.load(f)

6.2 localStorage 并发写入

虽然 localStorage 是同步 API,但在极端情况下(用户快速连续点击保存),可能出现竞态。解决方案:每次写入前先读取最新数据,合并后再写入,而不是基于内存中的旧数据写入。

6.3 单页应用的 SEO 困境

detail.html?id=xxx 这种 URL 对搜索引擎不够友好。目前的缓解方案:

  • 确保每个 id 对应的页面都有独立的 title、description、canonical
  • 生成完整的 sitemap.xml 提交给搜索引擎
  • 未来可以考虑预渲染(prerender.io)或 SSR,把关键内容直接输出到 HTML 中

七、项目成果与数据

截至目前的几个关键数据:

  • 多平台上线:微信小程序、iOS App Store、微软商店、Android(百度网盘)、Web 网页版
  • 法律数据覆盖:240 条完整条文,全部补充了条文解析和典型案例
  • Web 性能:Lighthouse 性能评分 95+,首屏加载 < 1s
  • SEO 收录:sitemap 提交后,详情页陆续被主流搜索引擎收录

对于个人开发者来说,这个项目的最大价值在于验证了"零后端 + 纯静态"架构的可行性。不需要买服务器、不需要配数据库、不需要处理并发,就能把一个有实际用户价值的工具做出来并持续运营。

八、总结

这个项目从 2023 年开始持续迭代,目前已在微信小程序、iOS App Store、微软商店等渠道上线,累计服务了数万名小时工和兼职人员。最有成就感的反馈来自用户:"终于找到一个没有广告、打开就能记工时的工具了"。

作为一个个人维护的项目,它整体践行了"够用就好"的工程哲学:

  • 不需要框架时,坚决不用框架------减少依赖、降低维护成本
  • 不需要后端时,用 localStorage + 静态 JSON 解决------零服务器费用
  • 数据量不大时,纯前端过滤比接入 ElasticSearch 更轻量
  • SEO 不需要 SSR 时,用结构化数据 + sitemap 也能拿到不错的收录效果

技术栈虽然简单,但每个决策都经过了场景匹配度长期维护成本的双重考量。对于个人项目或小型工具类站点,这种"原生技术栈 + 工程化思维"的组合,往往比盲目追新框架更务实。

如果你也在做类似的个人工具项目,欢迎参考这个项目的实现思路。当然,如果你就是小时工、兼职人员,或者身边有这样的朋友,也可以把这个工具推荐给他们------让每一分努力都有迹可循


在线体验https://xiaoshigongjizhang.com(网页版无需下载,打开即用)
技术交流:欢迎在评论区讨论前端工程化、SEO 优化、数据静态化、跨端开发等话题。

相关推荐
我想吃辣条4 个月前
从 0 到 1 开发一款记账小程序的设计与实现
小程序·记账
杨浦老苏2 年前
开源个人订阅跟踪器Wallos
docker·群晖·财务·记账
__只是为了好玩__2 年前
iOS 开发设计 App 上架符合要求的截图
ios·熊猫小账本·记账