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:
+50
-30
@@ -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;
|
||||||
|
if (!silent) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError(false);
|
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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user