重构:将 "lobster" 重命名为 "claw" 并添加国际化支持 (i18n)

This commit is contained in:
richarjiang
2026-03-13 12:07:28 +08:00
parent fa4c458eda
commit 9e30771180
38 changed files with 1003 additions and 344 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import {
AreaChart,
Area,
@@ -17,6 +18,7 @@ interface HourlyData {
}
export function ActivityTimeline() {
const t = useTranslations("activityTimeline");
const [data, setData] = useState<HourlyData[]>([]);
useEffect(() => {
@@ -40,7 +42,7 @@ export function ActivityTimeline() {
return (
<Card className="border-white/5">
<CardHeader className="pb-2">
<CardTitle>24h Activity</CardTitle>
<CardTitle>{t("title")}</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[160px] w-full">

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useTranslations, useLocale } from "next-intl";
import { motion, AnimatePresence } from "framer-motion";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -9,7 +10,7 @@ import { useSSE } from "@/hooks/use-sse";
interface FeedItem {
id: string;
type: "task" | "online" | "offline";
lobsterName: string;
clawName: string;
city?: string;
country?: string;
summary?: string;
@@ -17,7 +18,9 @@ interface FeedItem {
timestamp: number;
}
export function LobsterFeed() {
export function ClawFeed() {
const t = useTranslations("clawFeed");
const locale = useLocale();
const [items, setItems] = useState<FeedItem[]>([]);
const handleEvent = useCallback((event: { type: string; data: Record<string, unknown> }) => {
@@ -25,7 +28,7 @@ export function LobsterFeed() {
const newItem: FeedItem = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
type: event.type as FeedItem["type"],
lobsterName: (event.data.lobsterName as string) ?? "Unknown",
clawName: (event.data.clawName as string) ?? "Unknown",
city: event.data.city as string | undefined,
country: event.data.country as string | undefined,
summary: event.data.summary as string | undefined,
@@ -46,17 +49,17 @@ export function LobsterFeed() {
useEffect(() => {
const fetchRecent = async () => {
try {
const res = await fetch("/api/v1/lobsters?limit=10");
const res = await fetch("/api/v1/claws?limit=10");
if (res.ok) {
const data = await res.json();
const feedItems: FeedItem[] = (data.lobsters ?? [])
const feedItems: FeedItem[] = (data.claws ?? [])
.filter((l: Record<string, unknown>) => l.lastTask)
.map((l: Record<string, unknown>) => {
const task = l.lastTask as Record<string, unknown>;
return {
id: `init-${l.id}`,
type: "task" as const,
lobsterName: l.name as string,
clawName: l.name as string,
city: l.city as string,
country: l.country as string,
summary: task.summary as string,
@@ -95,13 +98,13 @@ export function LobsterFeed() {
return (
<Card className="border-white/5">
<CardHeader className="pb-2">
<CardTitle>Live Feed</CardTitle>
<CardTitle>{t("title")}</CardTitle>
</CardHeader>
<CardContent className="max-h-[400px] overflow-y-auto p-4 pt-0">
<AnimatePresence initial={false}>
{items.length === 0 ? (
<p className="py-8 text-center text-xs text-[var(--text-muted)]">
Waiting for lobster activity...
{t("waiting")}
</p>
) : (
items.map((item) => (
@@ -118,7 +121,7 @@ export function LobsterFeed() {
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-medium text-[var(--accent-cyan)]">
{item.lobsterName}
{item.clawName}
</span>
{item.city && (
<span className="text-xs text-[var(--text-muted)]">
@@ -136,7 +139,7 @@ export function LobsterFeed() {
<Badge variant="secondary">{formatDuration(item.durationMs)}</Badge>
)}
<span className="text-[10px] text-[var(--text-muted)]">
{new Date(item.timestamp).toLocaleTimeString()}
{new Date(item.timestamp).toLocaleTimeString(locale)}
</span>
</div>
</div>

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { motion } from "framer-motion";
@@ -18,7 +19,17 @@ const regionColors: Record<string, string> = {
Oceania: "var(--accent-green)",
};
const regionNameToKey: Record<string, string> = {
Asia: "asia",
Europe: "europe",
Americas: "americas",
Africa: "africa",
Oceania: "oceania",
};
export function RegionRanking() {
const t = useTranslations("regionRanking");
const tContinents = useTranslations("continents");
const [regions, setRegions] = useState<RegionData[]>([]);
useEffect(() => {
@@ -52,11 +63,11 @@ export function RegionRanking() {
return (
<Card className="border-white/5">
<CardHeader className="pb-2">
<CardTitle>Region Ranking</CardTitle>
<CardTitle>{t("title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3 p-4 pt-0">
{regions.length === 0 ? (
<p className="py-4 text-center text-xs text-[var(--text-muted)]">No data yet</p>
<p className="py-4 text-center text-xs text-[var(--text-muted)]">{t("noData")}</p>
) : (
regions.map((region, i) => (
<div key={region.name} className="space-y-1">
@@ -66,7 +77,9 @@ export function RegionRanking() {
#{i + 1}
</span>
<span className="text-sm font-medium" style={{ color: region.color }}>
{region.name}
{regionNameToKey[region.name]
? tContinents(regionNameToKey[region.name] as "asia" | "europe" | "americas" | "africa" | "oceania")
: region.name}
</span>
</div>
<span className="font-mono text-xs text-[var(--text-secondary)]">

View File

@@ -1,12 +1,13 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Card, CardContent } from "@/components/ui/card";
import { Users, Zap, Clock, Activity } from "lucide-react";
interface Stats {
totalLobsters: number;
activeLobsters: number;
totalClaws: number;
activeClaws: number;
tasksToday: number;
tasksTotal: number;
avgTaskDuration: number;
@@ -42,8 +43,9 @@ function AnimatedNumber({ value, suffix = "" }: { value: number; suffix?: string
requestAnimationFrame(animate);
}, [value]);
const locale = useLocale();
const formatted = Number.isInteger(value)
? Math.round(display).toLocaleString()
? Math.round(display).toLocaleString(locale)
: display.toFixed(1);
return (
@@ -56,29 +58,29 @@ function AnimatedNumber({ value, suffix = "" }: { value: number; suffix?: string
const statCards = [
{
key: "activeLobsters" as const,
label: "Online Now",
key: "activeClaws" as const,
labelKey: "onlineNow" as const,
icon: Activity,
color: "var(--accent-green)",
glow: "0 0 20px rgba(16, 185, 129, 0.3)",
},
{
key: "totalLobsters" as const,
label: "Total Lobsters",
key: "totalClaws" as const,
labelKey: "totalClaws" as const,
icon: Users,
color: "var(--accent-cyan)",
glow: "var(--glow-cyan)",
},
{
key: "tasksToday" as const,
label: "Tasks Today",
labelKey: "tasksToday" as const,
icon: Zap,
color: "var(--accent-purple)",
glow: "var(--glow-purple)",
},
{
key: "avgTaskDuration" as const,
label: "Avg Duration",
labelKey: "avgDuration" as const,
icon: Clock,
color: "var(--accent-orange)",
glow: "0 0 20px rgba(245, 158, 11, 0.3)",
@@ -88,9 +90,10 @@ const statCards = [
];
export function StatsPanel() {
const t = useTranslations("stats");
const [stats, setStats] = useState<Stats>({
totalLobsters: 0,
activeLobsters: 0,
totalClaws: 0,
activeClaws: 0,
tasksToday: 0,
tasksTotal: 0,
avgTaskDuration: 0,
@@ -116,7 +119,7 @@ export function StatsPanel() {
return (
<div className="flex flex-col gap-3">
{statCards.map(({ key, label, icon: Icon, color, glow, suffix, transform }) => {
{statCards.map(({ key, labelKey, icon: Icon, color, glow, suffix, transform }) => {
const raw = stats[key];
const value = transform ? transform(raw) : raw;
return (
@@ -133,7 +136,7 @@ export function StatsPanel() {
<Icon className="h-5 w-5" style={{ color }} />
</div>
<div className="min-w-0">
<p className="text-xs text-[var(--text-muted)]">{label}</p>
<p className="text-xs text-[var(--text-muted)]">{t(labelKey)}</p>
<p
className="font-mono text-xl font-bold"
style={{ color, textShadow: glow }}