fix(blindbox): 周末契约页输入框无法输入 — 非受控输入 + 全局 input 样式

- 创建/加入房间输入改为 ref + defaultValue,提交时从 DOM 取值,避免受控 state 导致无法打字
- globals: input/textarea 增加 user-select: text !important、touch-action: manipulation
- Input 组件: cursor-text、touch-manipulation、min-w-0、autoComplete=off
This commit is contained in:
2026-02-27 17:27:51 +08:00
parent 61ef54b2bd
commit 9aee4f0e9b
3 changed files with 63 additions and 33 deletions
+52 -32
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
@@ -38,9 +38,10 @@ export default function BlindboxLobbyPage() {
const [rooms, setRooms] = useState<RoomSummary[]>([]); const [rooms, setRooms] = useState<RoomSummary[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [createName, setCreateName] = useState(""); const createNameRef = useRef<HTMLInputElement>(null);
const joinCodeRef = useRef<HTMLInputElement>(null);
const [joinCodeLength, setJoinCodeLength] = useState(0);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [joinCode, setJoinCode] = useState("");
const [joining, setJoining] = useState(false); const [joining, setJoining] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loadError, setLoadError] = useState(false); const [loadError, setLoadError] = useState(false);
@@ -64,20 +65,23 @@ export default function BlindboxLobbyPage() {
return () => window.removeEventListener("nowhatever_auth", handler); return () => window.removeEventListener("nowhatever_auth", handler);
}, []); }, []);
const fetchRooms = useCallback(async () => { const fetchRooms = useCallback(async (silent = false) => {
const p = getCachedProfile(); const p = getCachedProfile();
if (!p) return; if (!p) return;
setLoading(true); if (!silent) {
setLoadError(false); setLoading(true);
setLoadError(false);
}
try { try {
const res = await fetch(`/api/blindbox/rooms?userId=${p.id}`); const res = await fetch(`/api/blindbox/rooms?userId=${p.id}`);
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
const data = await res.json(); const data = await res.json();
setRooms(Array.isArray(data.rooms) ? data.rooms : []); setRooms(Array.isArray(data.rooms) ? data.rooms : []);
setLoadError(false);
} catch { } catch {
setLoadError(true); if (!silent) setLoadError(true);
} finally { } finally {
setLoading(false); if (!silent) setLoading(false);
} }
}, []); }, []);
@@ -86,6 +90,17 @@ export default function BlindboxLobbyPage() {
else setLoading(false); else setLoading(false);
}, [loggedIn, fetchRooms]); }, [loggedIn, fetchRooms]);
useEffect(() => {
if (!loggedIn) return;
const onFocus = () => fetchRooms(true);
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, [loggedIn, fetchRooms]);
useEffect(() => {
if (rooms.length > 0) setJoinCodeLength(0);
}, [rooms.length]);
const handleAuth = (p: UserProfile) => { const handleAuth = (p: UserProfile) => {
setProfile(p); setProfile(p);
setLoggedIn(true); setLoggedIn(true);
@@ -94,13 +109,14 @@ export default function BlindboxLobbyPage() {
const handleCreate = async () => { const handleCreate = async () => {
if (creating || !profile) return; if (creating || !profile) return;
const name = createNameRef.current?.value?.trim() ?? "";
setCreating(true); setCreating(true);
setError(""); setError("");
try { try {
const res = await fetch("/api/blindbox/room", { const res = await fetch("/api/blindbox/room", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: profile.id, name: createName.trim() || undefined }), body: JSON.stringify({ userId: profile.id, name: name || undefined }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.error); if (!res.ok) throw new Error(data.error);
@@ -113,14 +129,15 @@ export default function BlindboxLobbyPage() {
}; };
const handleJoin = async () => { const handleJoin = async () => {
if (joining || !profile || !joinCode.trim()) return; const code = joinCodeRef.current?.value?.toUpperCase().trim().slice(0, 6) ?? "";
if (joining || !profile || code.length < 6) return;
setJoining(true); setJoining(true);
setError(""); setError("");
try { try {
const res = await fetch("/api/blindbox/room/join", { const res = await fetch("/api/blindbox/room/join", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: profile.id, code: joinCode.trim() }), body: JSON.stringify({ userId: profile.id, code }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.error); if (!res.ok) throw new Error(data.error);
@@ -282,17 +299,15 @@ export default function BlindboxLobbyPage() {
TA TA
</p> </p>
{/* Inline create form */} {/* Inline create form — z-10 + isolation so inputs are above any overlay and receive touch */}
<div className="mt-7 w-full max-w-xs"> <div className="relative z-10 isolate mt-7 w-full max-w-xs">
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
ref={createNameRef}
type="text" type="text"
placeholder="我们的周末" placeholder="我们的周末"
value={createName} defaultValue=""
onChange={(e) => { onInput={() => setError("")}
setCreateName(e.target.value.slice(0, 30));
setError("");
}}
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }} onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }}
maxLength={30} maxLength={30}
size="xl" size="xl"
@@ -319,11 +334,14 @@ export default function BlindboxLobbyPage() {
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
<Input <Input
ref={joinCodeRef}
type="text" type="text"
placeholder="6 位房间号" placeholder="6 位房间号"
value={joinCode} defaultValue=""
onChange={(e) => { onInput={(e) => {
setJoinCode(e.target.value.toUpperCase().slice(0, 6)); const v = (e.target as HTMLInputElement).value.toUpperCase().slice(0, 6);
(e.target as HTMLInputElement).value = v;
setJoinCodeLength(v.length);
setError(""); setError("");
}} }}
onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }} onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }}
@@ -336,7 +354,7 @@ export default function BlindboxLobbyPage() {
onClick={handleJoin} onClick={handleJoin}
variant="secondary" variant="secondary"
size="lg" size="lg"
disabled={joinCode.trim().length < 6} disabled={joinCodeLength < 6}
loading={joining} loading={joining}
icon={<LogIn size={16} />} icon={<LogIn size={16} />}
> >
@@ -364,16 +382,14 @@ export default function BlindboxLobbyPage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
> >
{/* Create row */} <div className="relative z-10 isolate flex flex-col">
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
ref={createNameRef}
type="text" type="text"
placeholder="新房间名称" placeholder="新房间名称"
value={createName} defaultValue=""
onChange={(e) => { onInput={() => setError("")}
setCreateName(e.target.value.slice(0, 30));
setError("");
}}
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }} onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }}
maxLength={30} maxLength={30}
size="lg" size="lg"
@@ -393,11 +409,14 @@ export default function BlindboxLobbyPage() {
{/* Join row */} {/* Join row */}
<div className="mt-2 flex gap-2"> <div className="mt-2 flex gap-2">
<Input <Input
ref={joinCodeRef}
type="text" type="text"
placeholder="输入 6 位房间号加入" placeholder="输入 6 位房间号加入"
value={joinCode} defaultValue=""
onChange={(e) => { onInput={(e) => {
setJoinCode(e.target.value.toUpperCase().slice(0, 6)); const v = (e.target as HTMLInputElement).value.toUpperCase().slice(0, 6);
(e.target as HTMLInputElement).value = v;
setJoinCodeLength(v.length);
setError(""); setError("");
}} }}
onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }} onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }}
@@ -409,7 +428,7 @@ export default function BlindboxLobbyPage() {
<Button <Button
onClick={handleJoin} onClick={handleJoin}
variant="secondary" variant="secondary"
disabled={joinCode.trim().length < 6} disabled={joinCodeLength < 6}
loading={joining} loading={joining}
icon={<LogIn size={14} />} icon={<LogIn size={14} />}
> >
@@ -426,6 +445,7 @@ export default function BlindboxLobbyPage() {
{error} {error}
</motion.p> </motion.p>
)} )}
</div>
{/* Room list */} {/* Room list */}
<div className="mt-5 flex flex-col gap-3"> <div className="mt-5 flex flex-col gap-3">
+9
View File
@@ -69,6 +69,15 @@ body {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
/* Allow inputs and textareas to receive focus and accept input (body has user-select: none) */
input,
textarea,
[contenteditable="true"] {
-webkit-user-select: text !important;
user-select: text !important;
touch-action: manipulation; /* prevent scroll container from capturing touch on iOS */
}
.scrollbar-none { .scrollbar-none {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
+2 -1
View File
@@ -21,7 +21,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
({ size = "md", variant = "default", className = "", ...rest }, ref) => ( ({ size = "md", variant = "default", className = "", ...rest }, ref) => (
<input <input
ref={ref} ref={ref}
className={`w-full border-none text-sm outline-none ring-1 ring-border placeholder:text-dim focus:ring-2 ${sizeStyles[size]} ${variantStyles[variant]} ${className}`} className={`w-full min-w-0 border-none text-sm outline-none ring-1 ring-border placeholder:text-dim focus:ring-2 cursor-text touch-manipulation ${sizeStyles[size]} ${variantStyles[variant]} ${className}`}
autoComplete="off"
{...rest} {...rest}
/> />
), ),