feat: 增强弱网与断线场景下的移动端交互反馈
This commit is contained in:
@@ -8,19 +8,19 @@ function formatTime(ts: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryListProps {
|
interface HistoryListProps {
|
||||||
sendJSON: (obj: Record<string, unknown>) => void;
|
sendPaste: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryList({ sendJSON }: HistoryListProps) {
|
export function HistoryList({ sendPaste }: HistoryListProps) {
|
||||||
const history = useAppStore((s) => s.history);
|
const history = useAppStore((s) => s.history);
|
||||||
const clearHistory = useAppStore((s) => s.clearHistory);
|
const clearHistory = useAppStore((s) => s.clearHistory);
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
sendJSON({ type: "paste", text });
|
sendPaste(text);
|
||||||
toast.info("发送粘贴…");
|
toast.info("发送粘贴…");
|
||||||
},
|
},
|
||||||
[sendJSON],
|
[sendPaste],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ interface MicButtonProps {
|
|||||||
|
|
||||||
export function MicButton({ onStart, onStop }: MicButtonProps) {
|
export function MicButton({ onStart, onStop }: MicButtonProps) {
|
||||||
const connected = useAppStore((s) => s.connectionStatus === "connected");
|
const connected = useAppStore((s) => s.connectionStatus === "connected");
|
||||||
|
const micReady = useAppStore((s) => s.micReady);
|
||||||
const recording = useAppStore((s) => s.recording);
|
const recording = useAppStore((s) => s.recording);
|
||||||
const pendingStart = useAppStore((s) => s.pendingStart);
|
const pendingStart = useAppStore((s) => s.pendingStart);
|
||||||
const isActive = recording || pendingStart;
|
const stopping = useAppStore((s) => s.stopping);
|
||||||
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const isActive = recording || pendingStart || stopping;
|
||||||
|
const disabled = !connected || !micReady || stopping;
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent<HTMLButtonElement>) => {
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
@@ -46,6 +50,8 @@ export function MicButton({ onStart, onStop }: MicButtonProps) {
|
|||||||
"cursor-not-allowed border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary opacity-30 shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
"cursor-not-allowed border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary opacity-30 shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
||||||
const activeClasses =
|
const activeClasses =
|
||||||
"animate-mic-breathe scale-[1.06] border-accent-hover bg-accent text-white shadow-[0_0_32px_rgba(99,102,241,0.35),0_0_80px_rgba(99,102,241,0.2)]";
|
"animate-mic-breathe scale-[1.06] border-accent-hover bg-accent text-white shadow-[0_0_32px_rgba(99,102,241,0.35),0_0_80px_rgba(99,102,241,0.2)]";
|
||||||
|
const weakClasses =
|
||||||
|
"border-amber-400 bg-linear-to-br from-amber-400/15 to-surface text-amber-200 shadow-[0_0_22px_rgba(251,191,36,0.28)]";
|
||||||
const idleClasses =
|
const idleClasses =
|
||||||
"border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
"border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
||||||
|
|
||||||
@@ -54,10 +60,12 @@ export function MicButton({ onStart, onStop }: MicButtonProps) {
|
|||||||
<div className="relative flex touch-none items-center justify-center">
|
<div className="relative flex touch-none items-center justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!connected}
|
disabled={disabled}
|
||||||
className={`relative z-1 flex size-24 cursor-pointer touch-none select-none items-center justify-center rounded-full border-2 transition-all duration-[250ms] ease-[cubic-bezier(0.4,0,0.2,1)] ${
|
className={`relative z-1 flex size-24 cursor-pointer touch-none select-none items-center justify-center rounded-full border-2 transition-all duration-[250ms] ease-[cubic-bezier(0.4,0,0.2,1)] ${
|
||||||
!connected
|
disabled
|
||||||
? disabledClasses
|
? disabledClasses
|
||||||
|
: weakNetwork
|
||||||
|
? weakClasses
|
||||||
: isActive
|
: isActive
|
||||||
? activeClasses
|
? activeClasses
|
||||||
: idleClasses
|
: idleClasses
|
||||||
@@ -98,7 +106,17 @@ export function MicButton({ onStart, onStop }: MicButtonProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-fg-dim text-sm">{"按住说话"}</p>
|
<p className="font-medium text-fg-dim text-sm">
|
||||||
|
{!micReady
|
||||||
|
? "请先准备麦克风"
|
||||||
|
: !connected
|
||||||
|
? "连接中断,等待重连"
|
||||||
|
: stopping
|
||||||
|
? "收尾中…"
|
||||||
|
: weakNetwork
|
||||||
|
? "网络波动,已启用缓冲"
|
||||||
|
: "按住说话"}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { useAppStore } from "../stores/app-store";
|
|||||||
export function PreviewBox() {
|
export function PreviewBox() {
|
||||||
const text = useAppStore((s) => s.previewText);
|
const text = useAppStore((s) => s.previewText);
|
||||||
const active = useAppStore((s) => s.previewActive);
|
const active = useAppStore((s) => s.previewActive);
|
||||||
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const recording = useAppStore((s) => s.recording);
|
||||||
const hasText = text.length > 0;
|
const hasText = text.length > 0;
|
||||||
|
const placeholder =
|
||||||
|
weakNetwork && recording ? "网络波动中,音频缓冲后发送…" : "按住说话…";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="shrink-0 pb-3">
|
<section className="shrink-0 pb-3">
|
||||||
@@ -17,7 +21,7 @@ export function PreviewBox() {
|
|||||||
<p
|
<p
|
||||||
className={`break-words text-base leading-relaxed ${hasText ? "" : "text-fg-dim"}`}
|
className={`break-words text-base leading-relaxed ${hasText ? "" : "text-fg-dim"}`}
|
||||||
>
|
>
|
||||||
{hasText ? text : "按住说话…"}
|
{hasText ? text : placeholder}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ const statusConfig = {
|
|||||||
|
|
||||||
export function StatusBadge() {
|
export function StatusBadge() {
|
||||||
const status = useAppStore((s) => s.connectionStatus);
|
const status = useAppStore((s) => s.connectionStatus);
|
||||||
const { text, dotClass, borderClass } = statusConfig[status];
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const { dotClass, borderClass } = statusConfig[status];
|
||||||
|
const text =
|
||||||
|
status === "connected" && weakNetwork
|
||||||
|
? "网络波动"
|
||||||
|
: statusConfig[status].text;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user