Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01593dcbdc | |||
| afdd26cb59 | |||
| 4b4758a60f | |||
| f9360c15e3 |
@@ -44,7 +44,7 @@ export default function ApiKeysPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function AuditLogPage() {
|
||||
const pages = Math.max(1, Math.ceil(total / size));
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-800">
|
||||
<ScrollText className="h-5 w-5 text-emerald-600" /> 操作日志
|
||||
<span className="text-sm font-normal text-gray-400">共 {total} 条</span>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function BrandsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function CategoriesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function StatsPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-800">
|
||||
<BarChart3 className="h-5 w-5 text-emerald-600" /> 数据概览
|
||||
</h2>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function SubmissionsPage({ onPending }: { onPending?: (n: number)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
{STATUS_TABS.map((t) => (
|
||||
<button
|
||||
@@ -193,7 +193,7 @@ function SubmissionView({ id, onBack }: { id: string; onBack: () => void }) {
|
||||
const nutri = Object.entries(p.nutriments || {});
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<button onClick={onBack} className="text-sm text-gray-500 flex items-center gap-1 mb-4">
|
||||
<ArrowLeft className="h-4 w-4" /> 返回投稿队列
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Boxes, Search, PlusCircle, Code2 } from "lucide-react";
|
||||
import Home from "./components/Home";
|
||||
import ProductView from "./components/ProductView";
|
||||
import Contribute from "./components/Contribute";
|
||||
import ApiDocs from "./components/ApiDocs";
|
||||
import { api } from "./api";
|
||||
|
||||
type View =
|
||||
| { name: "home" }
|
||||
@@ -13,6 +14,14 @@ type View =
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<View>({ name: "home" });
|
||||
const [qualified, setQualified] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.stats()
|
||||
.then((s) => setQualified(s.qualified))
|
||||
.catch(() => setQualified(null));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col">
|
||||
@@ -73,6 +82,15 @@ export default function App() {
|
||||
|
||||
<footer className="border-t bg-white">
|
||||
<div className="max-w-5xl mx-auto px-4 py-4 text-xs text-gray-400 leading-relaxed">
|
||||
{qualified != null && (
|
||||
<div className="mb-2 text-gray-500">
|
||||
目前已收录
|
||||
<span className="mx-1 font-semibold text-emerald-600">
|
||||
{qualified.toLocaleString()}
|
||||
</span>
|
||||
条合格商品档案
|
||||
</div>
|
||||
)}
|
||||
天工商品档案公共仓 是公益性「商品事实库」,仅收录客观商品信息(条码、品牌、品类、营养、官方建议零售价快照等),不含任何购买/交易功能。
|
||||
公众投稿须经人工审核后方可收纳。
|
||||
<button onClick={() => setView({ name: "api" })} className="ml-1 text-emerald-600 hover:underline">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Search, PlusCircle, Code2 } from "lucide-react";
|
||||
import { api } from "../api";
|
||||
import type { ProductSummary } from "../types";
|
||||
@@ -13,31 +13,18 @@ export default function Home({
|
||||
onApi: () => void;
|
||||
}) {
|
||||
const [q, setQ] = useState("");
|
||||
const [brand, setBrand] = useState("");
|
||||
const [country, setCountry] = 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("");
|
||||
const [qualified, setQualified] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.stats()
|
||||
.then((s) => setQualified(s.qualified))
|
||||
.catch(() => setQualified(null));
|
||||
}, []);
|
||||
|
||||
async function run(e?: React.FormEvent) {
|
||||
e?.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await api.search(q.trim(), 1, 30, {
|
||||
brand: brand.trim() || undefined,
|
||||
country: country.trim() || undefined,
|
||||
});
|
||||
const res = await api.search(q.trim(), 1, 30);
|
||||
setItems(res.items);
|
||||
setTotal(res.total);
|
||||
setSearched(true);
|
||||
@@ -55,15 +42,6 @@ export default function Home({
|
||||
<p className="mt-2 text-gray-500">
|
||||
输入商品名称或条码,检索客观、可溯源的商品资料。人人可查,人人可贡献。
|
||||
</p>
|
||||
{qualified != null && (
|
||||
<p className="mt-3 text-sm text-gray-500">
|
||||
目前已收录
|
||||
<span className="mx-1 font-semibold text-emerald-600">
|
||||
{qualified.toLocaleString()}
|
||||
</span>
|
||||
条合格商品档案
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={run} className="mt-6 max-w-2xl mx-auto flex gap-2">
|
||||
<div className="flex-1 flex items-center gap-2 bg-white border rounded-lg px-3 shadow-sm focus-within:ring-2 focus-within:ring-emerald-400">
|
||||
<Search className="w-5 h-5 text-gray-400" />
|
||||
@@ -83,20 +61,6 @@ export default function Home({
|
||||
{loading ? "检索中…" : "检索"}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-3 max-w-2xl mx-auto flex flex-wrap items-center justify-center gap-2 text-sm">
|
||||
<input
|
||||
value={brand}
|
||||
onChange={(e) => setBrand(e.target.value)}
|
||||
placeholder="按品牌筛选(如 Ferrero)"
|
||||
className="flex-1 min-w-[14rem] bg-white border rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-300"
|
||||
/>
|
||||
<input
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
placeholder="按产地筛选(如 China)"
|
||||
className="flex-1 min-w-[14rem] bg-white border rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-300"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={onApi}
|
||||
className="mt-4 inline-flex items-center gap-1.5 text-sm text-emerald-700 hover:underline"
|
||||
|
||||
Reference in New Issue
Block a user