Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36f9c95fa7 | |||
| 9ee83e3535 | |||
| 53ade9eeea | |||
| 313bc84cb0 | |||
| 8c8df2b8a2 | |||
| f04891d0b9 | |||
| 3a5575f51b |
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/baicai2026-baicai/goods/api/internal/gtin"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,8 +77,20 @@ func (s *Store) CreateBackflowSubmissions(ctx context.Context, items []Submissio
|
|||||||
if g == "" {
|
if g == "" {
|
||||||
in.GTIN = nil
|
in.GTIN = nil
|
||||||
} else {
|
} else {
|
||||||
in.GTIN = &g
|
// Validate/normalize so a malformed barcode is reported as an
|
||||||
res.GTIN = &g
|
// invalid item instead of aborting the batch (the gtin column
|
||||||
|
// is varchar(14) and only GS1 codes are archived).
|
||||||
|
norm, err := gtin.Normalize(g)
|
||||||
|
if err != nil {
|
||||||
|
res.GTIN = &g
|
||||||
|
res.Status = "invalid"
|
||||||
|
res.Reason = err.Error()
|
||||||
|
sum.Invalid++
|
||||||
|
sum.Results = append(sum.Results, res)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in.GTIN = &norm
|
||||||
|
res.GTIN = &norm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Search, PlusCircle, Code2, ScanBarcode, ShieldCheck, ArrowRight } from "lucide-react";
|
import { Search, PlusCircle, ArrowRight } from "lucide-react";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import type { ProductSummary } from "../types";
|
import type { ProductSummary } from "../types";
|
||||||
|
|
||||||
const EXAMPLES = ["可乐", "Nutella", "牛奶", "5449000000996"];
|
|
||||||
|
|
||||||
const FEATURES = [
|
|
||||||
{
|
|
||||||
icon: Search,
|
|
||||||
title: "客观可查",
|
|
||||||
desc: "按名称或条码检索商品的成分、营养、规格等官方事实。",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ShieldCheck,
|
|
||||||
title: "可溯源",
|
|
||||||
desc: "每条资料标注数据来源与质量分,公开透明、人人可核。",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Code2,
|
|
||||||
title: "开放 API",
|
|
||||||
desc: "免鉴权只读 REST 接口,开发者可直接接入商品档案。",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Home({
|
export default function Home({
|
||||||
onOpen,
|
onOpen,
|
||||||
onContribute,
|
onContribute,
|
||||||
@@ -57,47 +37,47 @@ export default function Home({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-up">
|
<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-6 py-14 text-center shadow-card">
|
<section
|
||||||
<div
|
className={`flex flex-col items-center px-4 text-center ${
|
||||||
aria-hidden
|
searched
|
||||||
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-brand-200/30 blur-3xl"
|
? "pt-2 pb-6"
|
||||||
/>
|
: "min-h-[calc(100vh-12rem)] justify-center"
|
||||||
<div className="relative">
|
}`}
|
||||||
<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" /> 公益 · 开放 · 可溯源
|
<h1
|
||||||
</span>
|
className={`font-medium tracking-tight text-gray-800 ${
|
||||||
<h1 className="mt-5 text-4xl sm:text-5xl font-extrabold tracking-tight text-gray-900">
|
searched ? "text-3xl" : "text-6xl sm:text-7xl"
|
||||||
天<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-gray-500">
|
天<span className="text-brand-600">工</span>
|
||||||
输入商品名称或条码,检索客观、可溯源的商品资料。人人可查,人人可贡献。
|
</h1>
|
||||||
</p>
|
|
||||||
|
|
||||||
<form onSubmit={(e) => run(undefined, e)} className="mx-auto mt-7 flex max-w-2xl gap-2">
|
<form
|
||||||
<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">
|
onSubmit={(e) => run(undefined, e)}
|
||||||
<Search className="h-5 w-5 shrink-0 text-gray-400" />
|
className="mx-auto mt-8 w-full max-w-xl"
|
||||||
<input
|
>
|
||||||
autoFocus
|
<div className="flex items-center gap-3 rounded-full border border-gray-200 bg-white px-5 py-3 shadow-sm transition hover:shadow-md focus-within:shadow-md">
|
||||||
value={q}
|
<Search className="h-5 w-5 shrink-0 text-gray-400" />
|
||||||
onChange={(e) => setQ(e.target.value)}
|
<input
|
||||||
placeholder="例如:可乐、Nutella、5449000000996"
|
autoFocus
|
||||||
className="flex-1 bg-transparent py-3.5 outline-none"
|
value={q}
|
||||||
/>
|
onChange={(e) => setQ(e.target.value)}
|
||||||
</div>
|
placeholder="搜索商品名称或条码"
|
||||||
<button type="submit" disabled={loading} className="btn-primary px-7">
|
className="flex-1 bg-transparent outline-none"
|
||||||
{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>
|
||||||
</div>
|
{!searched && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-md border border-gray-200 bg-gray-50 px-6 py-2.5 text-sm text-gray-700 transition hover:border-gray-300 hover:shadow-sm"
|
||||||
|
>
|
||||||
|
{loading ? "检索中…" : "检索"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -106,7 +86,7 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searched ? (
|
{searched && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="mb-3 text-sm text-gray-500">
|
<div className="mb-3 text-sm text-gray-500">
|
||||||
共 <span className="font-semibold text-gray-700">{total}</span> 条结果
|
共 <span className="font-semibold text-gray-700">{total}</span> 条结果
|
||||||
@@ -154,18 +134,6 @@ export default function Home({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="mt-8 grid gap-4 sm:grid-cols-3">
|
|
||||||
{FEATURES.map(({ icon: Icon, title, desc }) => (
|
|
||||||
<div key={title} className="card p-5 transition hover:shadow-card-hover">
|
|
||||||
<span className="grid h-10 w-10 place-items-center rounded-xl bg-brand-50 text-brand-600">
|
|
||||||
<Icon className="h-5 w-5" />
|
|
||||||
</span>
|
|
||||||
<h3 className="mt-3 font-semibold text-gray-800">{title}</h3>
|
|
||||||
<p className="mt-1 text-sm leading-relaxed text-gray-500">{desc}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user