feat: 美食多选 + 自定义输入标签

This commit is contained in:
2026-02-26 21:40:14 +08:00
parent ab55f0981e
commit c0662de3dd
2 changed files with 64 additions and 20 deletions
+59 -16
View File
@@ -39,7 +39,8 @@ export default function PanicPage() {
const [fetchingSuggestions, setFetchingSuggestions] = useState(false); const [fetchingSuggestions, setFetchingSuggestions] = useState(false);
const [radius, setRadius] = useState(3000); const [radius, setRadius] = useState(3000);
const [priceRange, setPriceRange] = useState("any"); const [priceRange, setPriceRange] = useState("any");
const [cuisine, setCuisine] = useState(""); const [cuisines, setCuisines] = useState<string[]>([]);
const [cuisineInput, setCuisineInput] = useState("");
const suggestRef = useRef<HTMLDivElement>(null); const suggestRef = useRef<HTMLDivElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null); const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
@@ -50,14 +51,15 @@ export default function PanicPage() {
useEffect(() => { useEffect(() => {
const prefs = getCachedPreferences(); const prefs = getCachedPreferences();
if (prefs.cuisine) setCuisine(prefs.cuisine); if (prefs.cuisines) setCuisines(prefs.cuisines);
else if (prefs.cuisine) setCuisines([prefs.cuisine]);
if (prefs.priceRange) setPriceRange(prefs.priceRange); if (prefs.priceRange) setPriceRange(prefs.priceRange);
if (prefs.radius) setRadius(prefs.radius); if (prefs.radius) setRadius(prefs.radius);
}, []); }, []);
const handleSceneChange = useCallback((s: SceneType) => { const handleSceneChange = useCallback((s: SceneType) => {
setScene(s); setScene(s);
setCuisine(""); setCuisines([]);
setPriceRange("any"); setPriceRange("any");
}, []); }, []);
@@ -142,7 +144,7 @@ export default function PanicPage() {
const res = await fetch("/api/room/create", { const res = await fetch("/api/room/create", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...coords, radius, priceRange, cuisine, userId: getUserId(), scene }), body: JSON.stringify({ ...coords, radius, priceRange, cuisine: cuisines.join("|"), userId: getUserId(), scene }),
}); });
const data = await res.json(); const data = await res.json();
@@ -395,16 +397,27 @@ export default function PanicPage() {
<div className="relative flex flex-1 items-center"> <div className="relative flex flex-1 items-center">
<input <input
type="text" type="text"
placeholder={sceneConfig.tagPlaceholder} placeholder={cuisines.length === 0 ? sceneConfig.tagPlaceholder : "继续添加..."}
value={cuisine} value={cuisineInput}
onChange={(e) => setCuisine(e.target.value)} onChange={(e) => setCuisineInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const val = cuisineInput.trim();
if (val && !cuisines.includes(val)) {
setCuisines((prev) => [...prev, val]);
}
setCuisineInput("");
}
}}
disabled={loading} disabled={loading}
className="h-7 w-full rounded-full border-none bg-elevated pl-3 pr-7 text-xs text-foreground outline-none ring-1 ring-subtle transition-colors placeholder:text-dim focus:ring-2 focus:ring-orange-500/50 disabled:opacity-50" className="h-7 w-full rounded-full border-none bg-elevated pl-3 pr-7 text-xs text-foreground outline-none ring-1 ring-subtle transition-colors placeholder:text-dim focus:ring-2 focus:ring-orange-500/50 disabled:opacity-50"
/> />
{cuisine && !loading && ( {cuisineInput && !loading && (
<button <button
onClick={() => setCuisine("")} type="button"
aria-label="清除口味" onClick={() => setCuisineInput("")}
aria-label="清除输入"
className="absolute right-2 flex h-4 w-4 items-center justify-center rounded-full text-muted hover:text-secondary" className="absolute right-2 flex h-4 w-4 items-center justify-center rounded-full text-muted hover:text-secondary"
> >
<X size={12} /> <X size={12} />
@@ -413,25 +426,55 @@ export default function PanicPage() {
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-start gap-3">
<span className="w-8 shrink-0 text-xs font-medium text-muted"></span> <span className="mt-0.5 w-8 shrink-0 text-xs font-medium text-muted"></span>
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-1 flex-wrap items-center gap-1.5">
<Flame size={11} className="shrink-0 text-orange-400" /> <Flame size={11} className="shrink-0 text-orange-400" />
{sceneConfig.hotTags.map((tag) => ( {sceneConfig.hotTags.map((tag) => {
const selected = cuisines.includes(tag);
return (
<button <button
key={tag} key={tag}
type="button" type="button"
onClick={() => setCuisine(tag)} onClick={() =>
setCuisines((prev) =>
selected ? prev.filter((t) => t !== tag) : [...prev, tag],
)
}
disabled={loading} disabled={loading}
className={`h-6 rounded-full px-2.5 text-xs font-medium transition-colors disabled:opacity-50 ${ className={`h-6 rounded-full px-2.5 text-xs font-medium transition-colors disabled:opacity-50 ${
cuisine === tag selected
? "bg-orange-500 text-white shadow-sm shadow-orange-500/25" ? "bg-orange-500 text-white shadow-sm shadow-orange-500/25"
: "bg-elevated text-muted hover:bg-subtle" : "bg-elevated text-muted hover:bg-subtle"
}`} }`}
> >
{tag} {tag}
</button> </button>
);
})}
{cuisines.filter((t) => !sceneConfig.hotTags.includes(t)).map((tag) => (
<button
key={tag}
type="button"
onClick={() => setCuisines((prev) => prev.filter((t) => t !== tag))}
disabled={loading}
className="flex h-6 items-center gap-1 rounded-full bg-orange-500 pl-2.5 pr-1.5 text-xs font-medium text-white shadow-sm shadow-orange-500/25 disabled:opacity-50"
>
{tag}
<X size={10} />
</button>
))} ))}
{cuisines.length > 0 && !loading && (
<button
type="button"
onClick={() => setCuisines([])}
aria-label="清除口味"
className="ml-1 flex h-5 items-center gap-0.5 rounded-full px-1.5 text-xs text-muted hover:text-secondary"
>
<X size={10} />
</button>
)}
</div> </div>
</div> </div>
+1
View File
@@ -49,6 +49,7 @@ export interface UserProfile {
export interface UserPreferences { export interface UserPreferences {
cuisine?: string; cuisine?: string;
cuisines?: string[];
priceRange?: string; priceRange?: string;
radius?: number; radius?: number;
} }