Files
goods/public-frontend/src/components/Home.tsx
T
lixu 313bc84cb0
CI / Go (api) (pull_request) Successful in 52s
CI / Python (ingestion) (pull_request) Successful in 25s
CI / Migrations (postgres) (pull_request) Successful in 29s
feat(public): 首页搜索居中、移动端自适应,移除下方三卡片
2026-06-25 07:25:52 +00:00

152 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Search, PlusCircle, ScanBarcode, ShieldCheck, ArrowRight } from "lucide-react";
import { api } from "../api";
import type { ProductSummary } from "../types";
const EXAMPLES = ["可乐", "Nutella", "牛奶", "5449000000996"];
export default function Home({
onOpen,
onContribute,
}: {
onOpen: (id: string) => void;
onContribute: () => void;
}) {
const [q, setQ] = useState("");
const [items, setItems] = useState<ProductSummary[]>([]);
const [total, setTotal] = useState(0);
const [searched, setSearched] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function run(term?: string, e?: React.FormEvent) {
e?.preventDefault();
const keyword = (term ?? q).trim();
if (term !== undefined) setQ(term);
setLoading(true);
setError("");
try {
const res = await api.search(keyword, 1, 30);
setItems(res.items);
setTotal(res.total);
setSearched(true);
} catch (err) {
setError(err instanceof Error ? err.message : "搜索失败");
} finally {
setLoading(false);
}
}
return (
<div className="animate-fade-up">
<section
className={`relative overflow-hidden rounded-3xl border border-gray-100 bg-gradient-to-b from-white to-brand-50/40 px-4 text-center shadow-card sm:px-6 ${
searched
? "py-8 sm:py-10"
: "flex min-h-[calc(100vh-12rem)] flex-col items-center justify-center py-12 sm:min-h-[calc(100vh-13rem)] sm:py-16"
}`}
>
<div
aria-hidden
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-brand-200/30 blur-3xl"
/>
<div className="relative w-full">
<span className="inline-flex items-center gap-1.5 rounded-full border border-brand-200 bg-white/70 px-3 py-1 text-xs font-medium text-brand-700">
<ShieldCheck className="w-3.5 h-3.5" /> · ·
</span>
<h1 className="mt-5 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
<span className="bg-gradient-to-r from-brand-600 to-emerald-500 bg-clip-text text-transparent"></span>
</h1>
<p className="mx-auto mt-3 max-w-xl text-sm text-gray-500 sm:text-base">
</p>
<form
onSubmit={(e) => run(undefined, e)}
className="mx-auto mt-7 flex w-full max-w-2xl flex-col gap-2 sm:flex-row"
>
<div className="flex flex-1 items-center gap-2 rounded-xl border border-gray-200 bg-white px-3.5 shadow-sm transition focus-within:border-brand-400 focus-within:ring-4 focus-within:ring-brand-500/10">
<Search className="h-5 w-5 shrink-0 text-gray-400" />
<input
autoFocus
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="例如:可乐、Nutella、5449000000996"
className="flex-1 bg-transparent py-3.5 outline-none"
/>
</div>
<button type="submit" disabled={loading} className="btn-primary px-7 py-3.5 sm:py-0">
{loading ? "检索中…" : "检索"}
</button>
</form>
<div className="mt-4 flex flex-wrap items-center justify-center gap-2">
<span className="text-xs text-gray-400"></span>
{EXAMPLES.map((ex) => (
<button key={ex} type="button" className="chip" onClick={() => run(ex)}>
<ScanBarcode className="w-3 h-3" /> {ex}
</button>
))}
</div>
</div>
</section>
{error && (
<div className="mx-auto mt-6 max-w-2xl rounded-xl bg-red-50 px-4 py-2.5 text-sm text-red-700">
{error}
</div>
)}
{searched && (
<div className="mt-8">
<div className="mb-3 text-sm text-gray-500">
<span className="font-semibold text-gray-700">{total}</span>
{q ? `(关键词:${q}` : ""}
</div>
{items.length === 0 ? (
<div className="card p-10 text-center text-gray-500">
<p></p>
<button
onClick={onContribute}
className="mt-3 inline-flex items-center gap-1.5 font-medium text-brand-700 hover:underline"
>
<PlusCircle className="w-4 h-4" />
</button>
</div>
) : (
<ul className="card divide-y divide-gray-100 overflow-hidden">
{items.map((p) => (
<li key={p.id}>
<button
onClick={() => onOpen(p.id)}
className="group flex w-full items-center justify-between gap-4 px-4 py-3.5 text-left transition hover:bg-brand-50/50"
>
<div className="min-w-0">
<div className="truncate font-medium text-gray-800 group-hover:text-brand-700">
{p.name}
</div>
<div className="mt-0.5 text-xs text-gray-500">
{p.brand || "未知品牌"}
{p.gtin ? ` · ${p.gtin}` : ""}
</div>
</div>
<div className="flex shrink-0 items-center gap-3">
<span className="text-right text-xs text-gray-400">
<span className="block">{p.category_path || ""}</span>
{p.country_of_origin ? (
<span className="block">{p.country_of_origin}</span>
) : null}
</span>
<ArrowRight className="h-4 w-4 text-gray-300 transition group-hover:translate-x-0.5 group-hover:text-brand-500" />
</div>
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
);
}