Files
voicepaste/web/src/components/HistoryList.tsx
imbytecat 70344bcd98 refactor: 迁移前端到 React 19 + Zustand + Tailwind CSS v4
- 将 vanilla TS 单文件 (app.ts 395行) 拆分为 React 组件化架构
- 引入 Zustand 管理全局状态 (连接/录音/预览/历史/toast)
- 自定义 hooks 封装 WebSocket 连接和音频录制管线
- CSS 全面 Tailwind 化,style.css 从 234 行精简到 114 行 (仅保留 tokens + keyframes)
- 新增依赖: react, react-dom, zustand, @vitejs/plugin-react
- Go 后端 embed 路径 web/dist 不变,无需改动
2026-03-02 06:36:02 +08:00

68 lines
2.2 KiB
TypeScript

import { useCallback } from "react";
import { useAppStore } from "../stores/app-store";
function formatTime(ts: number): string {
const d = new Date(ts);
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}
interface HistoryListProps {
sendJSON: (obj: Record<string, unknown>) => void;
}
export function HistoryList({ sendJSON }: HistoryListProps) {
const history = useAppStore((s) => s.history);
const clearHistory = useAppStore((s) => s.clearHistory);
const showToast = useAppStore((s) => s.showToast);
const handleItemClick = useCallback(
(text: string) => {
sendJSON({ type: "paste", text });
showToast("\u53d1\u9001\u7c98\u8d34\u2026");
},
[sendJSON, showToast],
);
return (
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="flex shrink-0 items-center justify-between pb-2.5">
<h2 className="font-semibold text-[13px] text-fg-dim uppercase tracking-[0.06em]">
{"\u5386\u53f2\u8bb0\u5f55"}
</h2>
<button
type="button"
onClick={clearHistory}
className="cursor-pointer rounded-lg border-none bg-transparent px-2.5 py-1 font-medium text-fg-dim text-xs transition-all duration-150 active:bg-danger/[0.08] active:text-danger"
>
{"\u6e05\u7a7a"}
</button>
</div>
{history.length === 0 ? (
<p className="py-10 text-center text-fg-dim text-sm">
{"\u6682\u65e0\u8bb0\u5f55"}
</p>
) : (
<div className="scrollbar-thin flex-1 overflow-y-auto">
{history.map((item, i) => (
<button
type="button"
key={`${item.ts}-${i}`}
onClick={() => handleItemClick(item.text)}
className="mb-2 flex w-full animate-slide-up cursor-pointer items-start gap-3 rounded-card border border-edge bg-surface px-4 py-3.5 text-left text-sm leading-relaxed transition-all duration-150 active:scale-[0.985] active:border-edge-active active:bg-surface-active"
style={{
animationDelay: `${Math.min(i, 10) * 40}ms`,
}}
>
<span className="flex-1 break-words">{item.text}</span>
<span className="shrink-0 whitespace-nowrap pt-0.5 text-[11px] text-fg-dim tabular-nums">
{formatTime(item.ts)}
</span>
</button>
))}
</div>
)}
</section>
);
}