- 将 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 不变,无需改动
68 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|