fa
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-26 21:29:54 +08:00
commit 2c4549ad79
85 changed files with 14664 additions and 0 deletions

44
src/lib/stats.ts Normal file
View File

@@ -0,0 +1,44 @@
import fs from 'fs';
import path from 'path';
const DATA_DIR = process.env.DATA_DIR || process.cwd();
const STATS_FILE = path.join(DATA_DIR, 'stats.json');
export interface Stats {
pv: number;
uv: number;
visitors: string[]; // Store hashed IPs or session IDs
}
export const getStats = (): Stats => {
try {
if (!fs.existsSync(STATS_FILE)) {
const initialStats: Stats = { pv: 0, uv: 0, visitors: [] };
fs.writeFileSync(STATS_FILE, JSON.stringify(initialStats, null, 2));
return initialStats;
}
const data = fs.readFileSync(STATS_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
console.error('[Stats] Error reading stats:', error);
return { pv: 0, uv: 0, visitors: [] };
}
};
export const recordHit = (visitorId: string): Stats => {
try {
const stats = getStats();
stats.pv += 1;
if (!stats.visitors.includes(visitorId)) {
stats.visitors.push(visitorId);
stats.uv = stats.visitors.length;
}
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
return stats;
} catch (error) {
console.error('[Stats] Error recording hit:', error);
return getStats();
}
};

332
src/lib/storage.ts Normal file
View File

@@ -0,0 +1,332 @@
import fs from 'fs';
import path from 'path';
import { Todo, Group, Stats, DEFAULT_GROUP_ID } from './types';
// 支持 Docker 数据目录 and 本地开发
const DATA_DIR = process.env.DATA_DIR || process.cwd();
const DATA_FILE = path.join(DATA_DIR, 'todos.json');
const STATS_FILE = path.join(DATA_DIR, 'stats.json');
const GROUPS_FILE = path.join(DATA_DIR, 'groups.json');
const RATE_LIMITS_FILE = path.join(DATA_DIR, 'rate-limits.json');
// 限流数据结构
export interface RateLimitData {
lastAddTime: number; // 上次添加任务的时间戳
dailyCount: number; // 当天已添加的任务数
lastResetDate: string; // 上次重置日期 (YYYY-MM-DD)
history: number[]; // 添加任务的历史时间戳
}
export interface RateLimits {
[clientId: string]: RateLimitData;
}
// 限流配置
export const RATE_LIMIT_CONFIG = {
INTERVAL_MS: 10 * 60 * 1000, // 10分钟
DAILY_MAX: 6, // 每天6个任务
};
export { DEFAULT_GROUP_ID };
export type { Todo, Group, Stats };
export const getTodos = (): Todo[] => {
try {
// 确保目录存在
const dir = path.dirname(DATA_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 如果文件不存在,创建空数组
if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, '[]', 'utf-8');
console.log(`[Storage] Created new todos file: ${DATA_FILE}`);
return [];
}
const data = fs.readFileSync(DATA_FILE, 'utf-8');
let todos: Todo[] = JSON.parse(data);
// 迁移逻辑:确保所有任务都有 groupId如果没有则归入默认分组
let needsSave = false;
todos = todos.map(todo => {
if (!todo.groupId) {
todo.groupId = DEFAULT_GROUP_ID;
needsSave = true;
}
return todo;
});
if (needsSave) {
saveTodos(todos);
}
console.log(`[Storage] Loaded ${todos.length} todos from ${DATA_FILE}`);
return todos;
} catch (error) {
console.error('[Storage] Error reading todos:', error);
return [];
}
};
export const saveTodos = (todos: Todo[]) => {
try {
// 确保目录存在
const dir = path.dirname(DATA_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2), 'utf-8');
console.log(`[Storage] Saved ${todos.length} todos to ${DATA_FILE}`);
} catch (error) {
console.error('[Storage] Error saving todos:', error);
throw error; // 抛出错误以便 API 能捕获
}
};
export const getGroups = (): Group[] => {
try {
const dir = path.dirname(GROUPS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (!fs.existsSync(GROUPS_FILE)) {
const defaultGroups: Group[] = [{ id: DEFAULT_GROUP_ID, name: 'Default', createdAt: Date.now() }];
fs.writeFileSync(GROUPS_FILE, JSON.stringify(defaultGroups, null, 2), 'utf-8');
return defaultGroups;
}
const data = fs.readFileSync(GROUPS_FILE, 'utf-8');
return JSON.parse(data);
} catch (error) {
console.error('[Storage] Error reading groups:', error);
return [{ id: DEFAULT_GROUP_ID, name: 'Default', createdAt: Date.now() }];
}
};
export const saveGroups = (groups: Group[]) => {
try {
const dir = path.dirname(GROUPS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(GROUPS_FILE, JSON.stringify(groups, null, 2), 'utf-8');
} catch (error) {
console.error('[Storage] Error saving groups:', error);
throw error;
}
};
export const getStats = (): Stats => {
try {
// 确保数据目录存在
const dir = path.dirname(STATS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`[Storage] Created data directory: ${dir}`);
}
if (!fs.existsSync(STATS_FILE)) {
const initialStats = { pv: 0, uv: 0 };
fs.writeFileSync(STATS_FILE, JSON.stringify(initialStats, null, 2), 'utf-8');
console.log(`[Storage] Created new stats file: ${STATS_FILE}`);
return initialStats;
}
const data = fs.readFileSync(STATS_FILE, 'utf-8');
const stats = JSON.parse(data);
console.log(`[Storage] Loaded stats from ${STATS_FILE}: PV=${stats.pv}, UV=${stats.uv}`);
return stats;
} catch (error) {
console.error('[Storage] Error reading stats:', error);
return { pv: 0, uv: 0 };
}
};
export const updateStats = (isNewVisitor: boolean): Stats => {
try {
// 确保数据目录存在
const dir = path.dirname(STATS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const stats = getStats();
stats.pv += 1;
if (isNewVisitor) {
stats.uv += 1;
}
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2), 'utf-8');
console.log(`[Storage] Updated stats: PV=${stats.pv}, UV=${stats.uv}`);
return stats;
} catch (error) {
console.error('[Storage] Error updating stats:', error);
return { pv: 0, uv: 0 };
}
};
// ============ 限流管理函数 ============
/**
* 获取所有限流数据
*/
export const getRateLimits = (): RateLimits => {
try {
const dir = path.dirname(RATE_LIMITS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (!fs.existsSync(RATE_LIMITS_FILE)) {
const initialData: RateLimits = {};
fs.writeFileSync(RATE_LIMITS_FILE, JSON.stringify(initialData, null, 2), 'utf-8');
console.log(`[Storage] Created new rate limits file: ${RATE_LIMITS_FILE}`);
return initialData;
}
const data = fs.readFileSync(RATE_LIMITS_FILE, 'utf-8');
const rateLimits = JSON.parse(data);
console.log(`[Storage] Loaded rate limits for ${Object.keys(rateLimits).length} clients`);
return rateLimits;
} catch (error) {
console.error('[Storage] Error reading rate limits:', error);
return {};
}
};
/**
* 保存限流数据
*/
export const saveRateLimits = (rateLimits: RateLimits): void => {
try {
const dir = path.dirname(RATE_LIMITS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(RATE_LIMITS_FILE, JSON.stringify(rateLimits, null, 2), 'utf-8');
console.log(`[Storage] Saved rate limits for ${Object.keys(rateLimits).length} clients`);
} catch (error) {
console.error('[Storage] Error saving rate limits:', error);
throw error;
}
};
/**
* 获取当前日期字符串 (YYYY-MM-DD)
*/
const getCurrentDate = (): string => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
/**
* 清理过期的限流历史记录保留最近24小时
*/
const cleanupOldHistory = (history: number[], currentTime: number): number[] => {
const oneDayAgo = currentTime - (24 * 60 * 60 * 1000);
return history.filter(timestamp => timestamp > oneDayAgo);
};
/**
* 检查客户端是否可以添加任务
* @param clientId 客户端标识IP地址或唯一ID
* @returns { canAdd: boolean, reason?: string, retryAfter?: number }
*/
export const checkRateLimit = (clientId: string): {
canAdd: boolean;
reason?: string;
retryAfter?: number;
dailyRemaining?: number;
} => {
const rateLimits = getRateLimits();
const currentTime = Date.now();
const currentDate = getCurrentDate();
// 获取或初始化客户端数据
let clientData = rateLimits[clientId];
if (!clientData) {
clientData = {
lastAddTime: 0,
dailyCount: 0,
lastResetDate: currentDate,
history: [],
};
rateLimits[clientId] = clientData;
}
// 检查日期是否变更,如果是则重置计数
if (clientData.lastResetDate !== currentDate) {
clientData.dailyCount = 0;
clientData.lastResetDate = currentDate;
clientData.history = [];
}
// 清理过期的历史记录
clientData.history = cleanupOldHistory(clientData.history, currentTime);
// 检查1时间间隔限制10分钟
const timeSinceLastAdd = currentTime - clientData.lastAddTime;
if (timeSinceLastAdd < RATE_LIMIT_CONFIG.INTERVAL_MS) {
const retryAfter = Math.ceil((RATE_LIMIT_CONFIG.INTERVAL_MS - timeSinceLastAdd) / 1000); // 秒
return {
canAdd: false,
reason: 'RATE_LIMIT_INTERVAL',
retryAfter,
dailyRemaining: RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount,
};
}
// 检查2每日限制6个任务
if (clientData.dailyCount >= RATE_LIMIT_CONFIG.DAILY_MAX) {
return {
canAdd: false,
reason: 'RATE_LIMIT_DAILY',
dailyRemaining: 0,
};
}
// 可以添加
return {
canAdd: true,
dailyRemaining: RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount - 1,
};
};
/**
* 记录客户端添加任务
* @param clientId 客户端标识
*/
export const recordTaskAdd = (clientId: string): void => {
const rateLimits = getRateLimits();
const currentTime = Date.now();
const currentDate = getCurrentDate();
let clientData = rateLimits[clientId];
if (!clientData) {
clientData = {
lastAddTime: 0,
dailyCount: 0,
lastResetDate: currentDate,
history: [],
};
}
clientData.lastAddTime = currentTime;
clientData.dailyCount += 1;
clientData.history.push(currentTime);
clientData.history = cleanupOldHistory(clientData.history, currentTime);
rateLimits[clientId] = clientData;
saveRateLimits(rateLimits);
console.log(`[Storage] Recorded task add for client ${clientId}: ` +
`dailyCount=${clientData.dailyCount}, ` +
`dailyRemaining=${RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount}`);
};

16
src/lib/timezones.ts Normal file
View File

@@ -0,0 +1,16 @@
export const TIMEZONES = [
{ value: 'UTC', label: 'UTC (UTC+0)' },
{ value: 'America/New_York', label: 'New York (UTC-5)' },
{ value: 'America/Los_Angeles', label: 'Los Angeles (UTC-8)' },
{ value: 'America/Chicago', label: 'Chicago (UTC-6)' },
{ value: 'Europe/London', label: 'London (UTC+0)' },
{ value: 'Europe/Paris', label: 'Paris (UTC+1)' },
{ value: 'Europe/Berlin', label: 'Berlin (UTC+1)' },
{ value: 'Asia/Dubai', label: 'Dubai (UTC+4)' },
{ value: 'Asia/Shanghai', label: 'Shanghai (UTC+8)' },
{ value: 'Asia/Tokyo', label: 'Tokyo (UTC+9)' },
{ value: 'Asia/Hong_Kong', label: 'Hong Kong (UTC+8)' },
{ value: 'Asia/Singapore', label: 'Singapore (UTC+8)' },
{ value: 'Australia/Sydney', label: 'Sydney (UTC+11)' },
{ value: 'Pacific/Auckland', label: 'Auckland (UTC+13)' },
];

187
src/lib/translations.ts Normal file
View File

@@ -0,0 +1,187 @@
export const translations = {
zh: {
// Common
save: '保存',
cancel: '取消',
close: '关闭',
// Home
addTask: '添加新任务',
addTaskPlaceholder: '添加新任务...',
all: '全部',
allTasks: '全部任务',
active: '进行中',
completed: '已完成',
noTasks: '暂无任务,开始记录你的灵感吧',
created: '创建',
completedAt: '完成',
subtitle1: '极简任务管理',
subtitle2: '高效生活方式',
// Settings
settings: '设置',
language: '语言',
languageDesc: '选择界面语言',
logoCustomization: 'Logo 自定义',
logoCustomizationDesc: '自定义主页 Logo 文字',
logoTextLabel: 'Logo 文字',
logoTextPlaceholder: '输入自定义文字(如 STARK',
timezone: '时区',
timezoneDesc: '选择显示时区',
themeMode: '主题模式',
themeModeDesc: '选择外观主题',
themeLight: '浅色模式',
themeDark: '深色模式',
themeSystem: '跟随系统',
// Analytics
insights: '数据分析',
analyticsLoading: '正在加载分析数据...',
aggregatingRecords: '正在汇总数据...',
past7Days: '过去 7 天',
pastMonth: '过去一个月',
allTime: '所有时间',
totalCreated: '总创建数',
successRate: '完成率',
dailyActivity: '每日动态',
creationVsCompletion: '创建与完成对比',
recentTaskTimeline: '近期任务时间轴',
completionDuration: '任务完成耗时分析',
took: '耗时约',
hours: '小时',
liveDataEngine: '实时数据引擎',
trend: '趋势',
createdLabel: '已创建',
completedLabel: '已完成',
// Notifications & Status
settingsSaved: '设置已保存',
syncingTasks: '正在同步任务...',
noActiveTasks: '暂无进行中的任务',
noCompletedTasks: '暂无已完成的任务',
pv: '访问量 (PV)',
uv: '访客数 (UV)',
// Auth
authRequired: '需要验证权限',
authDescription: '请输入密码以修改任务',
enterPassword: '请输入密码',
passwordRequired: '请输入密码',
invalidPassword: '密码错误',
verify: '验证',
verifying: '验证中...',
authHint: '如忘记密码,请联系管理员',
authSuccess: '验证成功',
logout: '退出登录',
authenticated: '已验证',
notAuthenticated: '未验证',
// Groups & Priority
groups: '分组',
addGroup: '添加分组',
deleteGroup: '删除分组',
defaultGroup: '默认分组',
selectGroup: '选择分组',
priority: '优先级',
p0: '高优先级 (P0)',
p1: '中优先级 (P1)',
p2: '低优先级 (P2)',
manageGroups: '管理分组',
newGroupName: '新分组名称',
noPriority: '无优先级',
},
en: {
// Common
save: 'Save',
cancel: 'Cancel',
close: 'Close',
// Home
addTask: 'Add Task',
addTaskPlaceholder: 'Add a new task...',
all: 'All',
allTasks: 'All Tasks',
active: 'Active',
completed: 'Completed',
noTasks: 'No tasks yet. Start tracking your ideas!',
created: 'Created',
completedAt: 'Completed',
subtitle1: 'Minimal Task Management',
subtitle2: 'Efficient Lifestyle',
// Settings
settings: 'Settings',
language: 'Language',
languageDesc: 'Choose interface language',
logoCustomization: 'Logo Customization',
logoCustomizationDesc: 'Customize homepage logo text',
logoTextLabel: 'Logo Text',
logoTextPlaceholder: 'Enter custom text (e.g., STARK)',
timezone: 'Timezone',
timezoneDesc: 'Select display timezone',
themeMode: 'Theme Mode',
themeModeDesc: 'Choose appearance theme',
themeLight: 'Light Mode',
themeDark: 'Dark Mode',
themeSystem: 'Follow System',
// Analytics
insights: 'Insights',
analyticsLoading: 'Loading analytics data...',
aggregatingRecords: 'Aggregating records...',
past7Days: 'Past 7 Days',
pastMonth: 'Past Month',
allTime: 'All Time',
totalCreated: 'Total Created',
successRate: 'Success Rate',
dailyActivity: 'Daily Activity',
creationVsCompletion: 'Creation vs Completion',
recentTaskTimeline: 'Recent Task Timeline',
completionDuration: 'Completion Time Analysis',
took: 'Took ~',
hours: 'h',
liveDataEngine: 'Live Data Engine',
trend: 'Trend',
createdLabel: 'Created',
completedLabel: 'Completed',
// Notifications & Status
settingsSaved: 'Settings Saved',
syncingTasks: 'Syncing tasks...',
noActiveTasks: 'No active tasks',
noCompletedTasks: 'No completed tasks',
pv: 'Page Views (PV)',
uv: 'Unique Visitors (UV)',
// Auth
authRequired: 'Authentication Required',
authDescription: 'Enter password to modify tasks',
enterPassword: 'Enter password',
passwordRequired: 'Password is required',
invalidPassword: 'Invalid password',
verify: 'Verify',
verifying: 'Verifying...',
authHint: 'Contact administrator if you forgot the password',
authSuccess: 'Verified successfully',
logout: 'Logout',
authenticated: 'Authenticated',
notAuthenticated: 'Not authenticated',
// Groups & Priority
groups: 'Groups',
addGroup: 'Add Group',
deleteGroup: 'Delete Group',
defaultGroup: 'Default Group',
selectGroup: 'Select Group',
priority: 'Priority',
p0: 'High (P0)',
p1: 'Medium (P1)',
p2: 'Low (P2)',
manageGroups: 'Manage Groups',
newGroupName: 'New Group Name',
noPriority: 'No Priority',
},
};
export type Language = keyof typeof translations;
export type TranslationKey = keyof typeof translations.zh;

26
src/lib/types.ts Normal file
View File

@@ -0,0 +1,26 @@
export type Priority = 'P0' | 'P1' | 'P2';
export interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: number;
completedAt?: number;
deleted?: boolean;
deletedAt?: number;
groupId?: string;
priority?: Priority;
}
export interface Group {
id: string;
name: string;
createdAt: number;
}
export const DEFAULT_GROUP_ID = 'default';
export interface Stats {
pv: number;
uv: number;
}