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 不变,无需改动
This commit is contained in:
67
web/src/components/HistoryList.tsx
Normal file
67
web/src/components/HistoryList.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user