介绍实现 omnilude-tools 时使用的部分 PRD 文档
前言
在上一篇介绍 omnilude-tools 的文章里提到过,那个项目里有不少工具都直接建立在具体的 PRD 文档之上。这次公开的文档,就是其中之一: Unix Timestamp Converter PRD。
这份文档最初是为了实现真实的 timestamp-converter 工具而用韩文写成的,现在对应的实现痕迹仍然保留在 src/app/[locale]/(tools)/developer/timestamp-converter/ 目录下。也就是说,它并不是一份松散的点子备忘,而是一份把页面结构、状态管理、SEO 与测试设计一起绑住的工作文档。
这篇文章里我尤其想强调三点。
- 这份文档,是前面介绍过的
omnilude-tools里某个真实工具背后的 PRD 之一。 - 它也是
prd-generator技能在真实开发里很好的一次使用案例。 - 这份 PRD 的原文是韩文,因此这篇文章在各个语言版本里都尽量提供整份文档的完整翻译,而不是只给一段摘要。
目前也可以直接通过 tools.omnilude.com/developer/timestamp-converter 访问这个工具。
如果可以的话,我也想一并提供生成这份 PRD 时使用的提示词,但由于磁盘空间限制,我已经删除了当时的会话数据,因此暂时无法提供,还请理解。
正文
简单说,这份文档把范围压缩到了实现者可以立刻动手的程度。
- 做什么: 一个在 Unix 时间戳与人类可读日期之间双向转换的工具
- 做到哪里: MVP、高级功能、时区、当前时间实时显示、代码片段、计算器
- 怎么做: 文件结构、Zustand 状态管理、转换工具函数、SEO、测试、无障碍、性能要求
- 如何验证: 单元测试、E2E 测试、实现清单
当文档细到这个程度时,真正的实现会简单很多。实际上,timestamp-converter 页面就是建立在这种结构化指引之上,如今也仍然作为 omnilude-tools 的 developer 分组工具存在。
这份文档由韩文翻译而来
这篇文章的基准原文是一份韩文 PRD。因此,各语言版本都优先保留整份文档,而不是把它压缩成一个简短说明。即使它本质上是 PRD 文档,我也更看重保留真实实现背景和技术语境,而不是单纯缩短篇幅。
PRD 原文
下面是整理后公开的 Unix Timestamp Converter PRD 全文。
PRD: Unix Timestamp Converter
基本信息
| 项目 | 值 |
|---|---|
| 工具 ID | timestamp-converter |
| 分组 | developer |
| 路径 | /developer/timestamp-converter |
| 优先级 | P0 (必需) |
| 预估工时 | 1-2 天 |
| 状态 | Draft |
1. 概述
1.1 目的
一个提供 Unix 时间戳与人类可读日期格式之间双向转换的工具。
1.2 目标用户
- 后端/前端开发者
- 数据分析师
- 系统管理员
1.3 竞品分析
| 网站 | 优点 | 缺点 |
|---|---|---|
| epochconverter.com | 格式丰富、带代码片段 | UI 较旧 |
| unixtimestamp.com | 界面简单 | 功能有限 |
| timestamp-converter.com | 设计整洁 | 时区支持有限 |
2. 功能需求
2.1 核心功能 (MVP)
F1: 时间戳 → 日期转换
输入: Unix 时间戳 (自动识别秒/毫秒/微秒)
输出: 多种日期格式
自动识别逻辑:
function detectTimestampUnit(value: number): 'seconds' | 'milliseconds' | 'microseconds' {
if (value < 1e12) return 'seconds'; // 小于 10 位
if (value < 1e15) return 'milliseconds'; // 13 位
return 'microseconds'; // 16 位
}
输出格式:
- ISO 8601:
2026-01-29T13:45:30.000Z - RFC 2822:
Wed, 29 Jan 2026 13:45:30 +0000 - 本地格式:
2026年1月29日 22:45:30 - 相对时间:
5 分钟前,3 天后
F2: 日期 → 时间戳转换
输入: 日期字符串或日期选择器
输出: Unix 时间戳 (秒、毫秒)
支持的输入格式:
- ISO 8601 字符串
- 日期/时间选择器 (DatePicker)
- 自然语言 (可选):
now,yesterday,next week
F3: 时区支持
默认值: 浏览器本地时区
选项: UTC、主要城市时区选择
主要时区列表:
const TIMEZONES = [
{ value: 'UTC', label: 'UTC' },
{ value: 'Asia/Seoul', label: '首尔 (KST, UTC+9)' },
{ value: 'Asia/Tokyo', label: '东京 (JST, UTC+9)' },
{ value: 'America/New_York', label: '纽约 (EST/EDT, UTC-5/-4)' },
{ value: 'America/Los_Angeles', label: '洛杉矶 (PST/PDT, UTC-8/-7)' },
{ value: 'Europe/London', label: '伦敦 (GMT/BST, UTC+0/+1)' },
// ... 继续补充
];
F4: 当前时间实时显示
在页面顶部实时显示当前 Unix 时间戳,并每 1 秒更新一次
2.2 高级功能
F5: 生成代码片段
为选中的时间戳提供多种编程语言的代码片段。
const CODE_SNIPPETS = {
javascript: (ts: number) => `new Date(${ts * 1000})`,
python: (ts: number) => `datetime.fromtimestamp(${ts})`,
java: (ts: number) => `Instant.ofEpochSecond(${ts}L)`,
go: (ts: number) => `time.Unix(${ts}, 0)`,
php: (ts: number) => `date('Y-m-d H:i:s', ${ts})`,
ruby: (ts: number) => `Time.at(${ts})`,
csharp: (ts: number) => `DateTimeOffset.FromUnixTimeSeconds(${ts})`,
};
F6: 时间戳计算器
基准时间 + 天/小时/分钟/秒 = 结果时间戳
例: 当前时间 + 7 天 = 下周的时间戳
3. UI/UX 设计
3.1 布局
┌─────────────────────────────────────────────────────────────┐
│ 当前 Unix 时间戳: 1738150800 (实时更新) [复制] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ 输入 │ │ 结果 │ │
│ │ │ │ │ │
│ │ [输入时间戳] │ │ ISO 8601: ... [复制] │ │
│ │ 或 │ │ RFC 2822: ... [复制] │ │
│ │ [选择日期时间] │ │ 本地: ... [复制] │ │
│ │ │ │ 相对: ... [复制] │ │
│ │ 时区: [选择 ▼] │ │ │ │
│ │ │ │ 时间戳 (秒): ... │ │
│ │ [转换] [清空] │ │ 时间戳 (ms): ... │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ 代码片段 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [JS] [Python] [Java] [Go] [PHP] [Ruby] [C#] │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ new Date(1738150800000) [复制] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 响应式设计
Desktop (lg+):
- 双栏布局 (输入 | 结果)
Tablet/Mobile (< lg):
- 单栏布局 (输入在上,结果在下)
3.3 交互
- 实时转换: 输入时自动转换 (300ms debounce)
- 复制反馈: 点击复制按钮时显示 toast 提示
- 键盘快捷键: Enter 执行转换,Ctrl+C 复制结果
4. 技术规格
4.1 文件结构
src/app/[locale]/(tools)/developer/timestamp-converter/
├── page.tsx # 页面入口 + SEO
├── _store/
│ └── timestamp-store.ts # Zustand 状态管理
└── _components/
├── timestamp-page.tsx # 主客户端组件
├── current-time-display.tsx # 当前时间显示
├── timestamp-input.tsx # 输入区域
├── conversion-result.tsx # 结果区域
└── code-snippets.tsx # 代码片段区域
4.2 状态管理 (Zustand)
// _store/timestamp-store.ts
import { create } from 'zustand';
type InputMode = 'timestamp' | 'datetime';
interface TimestampState {
// 输入
inputMode: InputMode;
timestampInput: string;
dateInput: Date | null;
timezone: string;
// 结果
result: ConversionResult | null;
// 代码片段
selectedLanguage: string;
// 动作
setInputMode: (mode: InputMode) => void;
setTimestampInput: (value: string) => void;
setDateInput: (date: Date | null) => void;
setTimezone: (tz: string) => void;
setSelectedLanguage: (lang: string) => void;
convert: () => void;
clear: () => void;
}
interface ConversionResult {
timestampSeconds: number;
timestampMillis: number;
iso8601: string;
rfc2822: string;
localFormat: string;
relative: string;
}
export const useTimestampStore = create<TimestampState>((set, get) => ({
inputMode: 'timestamp',
timestampInput: '',
dateInput: null,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
result: null,
selectedLanguage: 'javascript',
setInputMode: (mode) => set({ inputMode: mode }),
setTimestampInput: (value) => set({ timestampInput: value }),
setDateInput: (date) => set({ dateInput: date }),
setTimezone: (tz) => set({ timezone: tz }),
setSelectedLanguage: (lang) => set({ selectedLanguage: lang }),
convert: () => {
const { inputMode, timestampInput, dateInput, timezone } = get();
// 转换逻辑
},
clear: () => set({
timestampInput: '',
dateInput: null,
result: null,
}),
}));
4.3 核心转换逻辑
// lib/timestamp-utils.ts
import { format, formatDistanceToNow } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { ko, en, ja } from 'date-fns/locale';
export function convertTimestamp(
timestamp: number,
timezone: string,
locale: string
): ConversionResult {
// 自动识别单位
const unit = detectTimestampUnit(timestamp);
const timestampMs = unit === 'seconds'
? timestamp * 1000
: unit === 'microseconds'
? Math.floor(timestamp / 1000)
: timestamp;
const date = new Date(timestampMs);
const localeObj = { ko, en, ja }[locale] || ko;
return {
timestampSeconds: Math.floor(timestampMs / 1000),
timestampMillis: timestampMs,
iso8601: date.toISOString(),
rfc2822: format(date, 'EEE, dd MMM yyyy HH:mm:ss xx', { locale: localeObj }),
localFormat: formatInTimeZone(date, timezone, 'PPpp', { locale: localeObj }),
relative: formatDistanceToNow(date, { addSuffix: true, locale: localeObj }),
};
}
export function dateToTimestamp(date: Date): { seconds: number; millis: number } {
const millis = date.getTime();
return {
seconds: Math.floor(millis / 1000),
millis,
};
}
4.4 依赖项
{
"dependencies": {
"date-fns": "^3.x",
"date-fns-tz": "^3.x"
}
}
项目中已经安装。
5. 多语言支持
5.1 翻译键
// messages/zh/tools/developer/timestamp-converter.json
{
"tools": {
"developer": {
"timestampConverter": "时间戳转换器",
"timestampConverterDesc": "在 Unix 时间戳与日期之间相互转换",
"timestamp": {
"currentTime": "当前 Unix 时间戳",
"inputTimestamp": "输入时间戳",
"inputDatetime": "输入日期/时间",
"timezone": "时区",
"convert": "转换",
"clear": "清空",
"copy": "复制",
"copied": "已复制",
"results": "转换结果",
"iso8601": "ISO 8601",
"rfc2822": "RFC 2822",
"localFormat": "本地格式",
"relative": "相对时间",
"timestampSeconds": "时间戳 (秒)",
"timestampMillis": "时间戳 (毫秒)",
"codeSnippets": "代码片段",
"invalidTimestamp": "无效的时间戳",
"autoDetected": "自动识别: {unit}"
}
}
}
}
6. SEO 元数据
// page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale });
return generateSeoMetadata({
locale,
title: t('tools.developer.timestampConverter'),
description: t('tools.developer.timestampConverterDesc'),
path: '/developer/timestamp-converter',
keywords: [
'unix timestamp',
'epoch converter',
'时间戳转换',
'日期转换',
'timestamp to date',
'date to timestamp',
],
});
}
7. 测试用例
7.1 单元测试
describe('detectTimestampUnit', () => {
it('should detect seconds', () => {
expect(detectTimestampUnit(1738150800)).toBe('seconds');
});
it('should detect milliseconds', () => {
expect(detectTimestampUnit(1738150800000)).toBe('milliseconds');
});
it('should detect microseconds', () => {
expect(detectTimestampUnit(1738150800000000)).toBe('microseconds');
});
});
describe('convertTimestamp', () => {
it('should convert timestamp to ISO 8601', () => {
const result = convertTimestamp(1738150800, 'UTC', 'en');
expect(result.iso8601).toBe('2025-01-29T09:00:00.000Z');
});
});
7.2 E2E 测试
test('时间戳转换流程', async ({ page }) => {
await page.goto('/zh/developer/timestamp-converter');
// 输入时间戳
await page.fill('[data-testid="timestamp-input"]', '1738150800');
// 检查结果
await expect(page.locator('[data-testid="iso8601-result"]'))
.toContainText('2025-01-29');
// 点击复制按钮
await page.click('[data-testid="copy-iso8601"]');
await expect(page.locator('.toast')).toContainText('已复制');
});
8. 无障碍要求
- 所有输入字段都要有合适的 label
- 复制按钮需要 aria-label
- 支持键盘导航
- 支持屏幕阅读器
9. 性能要求
- 输入后显示转换结果: < 100ms
- 当前时间更新: 每秒准确刷新
- 包体积增加: < 5KB (gzip)
10. 实现清单
- 创建文件结构
- 实现 Zustand store
- 实现转换工具函数
- 实现主页面组件
- 实现当前时间显示组件
- 实现输入区域组件
- 实现结果区域组件
- 实现代码片段区域组件
- 添加 7 种语言的翻译键
- 在
tools.ts中注册工具 - 完成响应式样式
- 做无障碍检查
- 编写测试