v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)

This commit is contained in:
BigCode
2026-11-01 00:15:29 +08:00
commit 5b83e71ad9
128 changed files with 20008 additions and 0 deletions
+282
View File
@@ -0,0 +1,282 @@
/**
* # 路由 /checkout 结账页
* 未登录:内联登录 / 注册表单(复用 ~/server/auth 的 loginFn/registerFn)。
* 已登录:账单信息表单 → createOrder(银行转账占位支付)→ 成功页展示订单号。
*/
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { useState } from "react";
import { getCart, createOrder, type BillingInfo } from "~/server/commerce";
import { useT } from "~/i18n";
import { getCurrentUser, loginFn, registerFn } from "~/server/auth";
import { getLayoutData, Layout } from "~/components/website/Layout";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
export const Route = createFileRoute("/checkout")({
loader: async () => {
const [layout, user, cart] = await Promise.all([getLayoutData(), getCurrentUser(), getCart()]);
return { layout, user, cart };
},
head: () => ({ meta: [{ title: "Checkout - DusunIoT" }] }),
component: CheckoutPage,
});
/** # 函数 组件 结账页面(外壳:公共布局) */
function CheckoutPage() {
const { layout } = Route.useLoaderData();
return (
<Layout layout={layout}>
<CheckoutInner />
</Layout>
);
}
/** # 函数 组件 结账表单区 */
function CheckoutInner() {
const t = useT();
const { user, cart } = Route.useLoaderData();
const router = useRouter();
const [authError, setAuthError] = useState("");
const [orderNumber, setOrderNumber] = useState("");
const [orderError, setOrderError] = useState("");
const [submitting, setSubmitting] = useState(false);
/** # 函数 登录提交(成功后 invalidate 以带会话重跑 loader) */
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setAuthError("");
const fd = new FormData(e.currentTarget);
const res = await loginFn({ data: { email: String(fd.get("email") ?? ""), password: String(fd.get("password") ?? "") } });
if (!res.ok) return setAuthError(res.error);
router.invalidate();
}
/** # 函数 注册提交 */
async function handleRegister(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setAuthError("");
const fd = new FormData(e.currentTarget);
const res = await registerFn({
data: {
email: String(fd.get("email") ?? ""),
password: String(fd.get("password") ?? ""),
displayName: String(fd.get("displayName") ?? "") || undefined,
},
});
if (!res.ok) return setAuthError(res.error);
router.invalidate();
}
/** # 函数 下单提交 */
async function handlePlaceOrder(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setOrderError("");
setSubmitting(true);
const fd = new FormData(e.currentTarget);
const billing: BillingInfo = {
firstName: String(fd.get("firstName") ?? ""),
lastName: String(fd.get("lastName") ?? ""),
email: String(fd.get("email") ?? ""),
phone: String(fd.get("phone") ?? ""),
address: String(fd.get("address") ?? ""),
city: String(fd.get("city") ?? ""),
country: String(fd.get("country") ?? ""),
postcode: String(fd.get("postcode") ?? ""),
};
const shippingSame = fd.get("shippingSame") === "on";
const shipping = shippingSame
? undefined
: {
firstName: String(fd.get("shipFirstName") ?? ""),
lastName: String(fd.get("shipLastName") ?? ""),
email: billing.email,
address: String(fd.get("shipAddress") ?? ""),
city: String(fd.get("shipCity") ?? ""),
country: String(fd.get("shipCountry") ?? ""),
postcode: String(fd.get("shipPostcode") ?? ""),
};
const res = await createOrder({
data: { billing, shippingSame, shipping, note: String(fd.get("note") ?? "") },
});
setSubmitting(false);
if (!res.ok) return setOrderError(res.error);
setOrderNumber(res.orderNumber);
router.invalidate();
}
// 下单成功页
if (orderNumber) {
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">Order received</h1>
<div className="max-w-[560px] rounded-md border border-[#b5e3c4] bg-[#e7f6ec] px-4 py-3 text-[#15803d]">
Thank you! Your order <strong>{orderNumber}</strong> has been placed.
</div>
<p className="mt-4">
Payment method: bank transfer (our sales team will contact you with payment instructions).
</p>
<p className="flex gap-2.5">
<Button asChild>
<Link to="/my-account/">{t("checkout_view_orders")}</Link>
</Button>
<Button asChild variant="outline">
<Link to="/shop/">{t("cart_continue")}</Link>
</Button>
</p>
</div>
);
}
// 未登录:内联登录 / 注册
if (!user) {
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">Checkout</h1>
<p>
Please log in or create an account to place your order.{" "}
{cart.count > 0 && <>{t("checkout_cart_kept", { count: cart.count })}</>}
</p>
{authError && (
<p className="max-w-[420px] rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">
{authError}
</p>
)}
<div className="mt-4 grid grid-cols-1 items-start gap-7 min-[860px]:grid-cols-[minmax(0,7fr)_minmax(0,4fr)]">
<div className="rounded-lg border border-line bg-soft p-5">
<h3 className="mt-0">Log in</h3>
<form className="grid grid-cols-1 gap-3.5 min-[640px]:grid-cols-2" onSubmit={handleLogin}>
<Label className="col-span-full">
Email
<Input name="email" type="email" required autoComplete="email" />
</Label>
<Label className="col-span-full">
Password
<Input name="password" type="password" required autoComplete="current-password" />
</Label>
<div className="col-span-full">
<Button type="submit">
Log in
</Button>
</div>
</form>
</div>
<div className="rounded-lg border border-line bg-soft p-5">
<h3 className="mt-0">Create account</h3>
<form className="grid grid-cols-1 gap-3.5 min-[640px]:grid-cols-2" onSubmit={handleRegister}>
<Label className="col-span-full">
Display name (optional)
<Input name="displayName" type="text" />
</Label>
<Label className="col-span-full">
Email
<Input name="email" type="email" required autoComplete="email" />
</Label>
<Label className="col-span-full">
Password (min 8 chars)
<Input name="password" type="password" required minLength={8} autoComplete="new-password" />
</Label>
<div className="col-span-full">
<Button type="submit" variant="outline">
Register
</Button>
</div>
</form>
</div>
</div>
</div>
);
}
// 购物车为空
if (cart.items.length === 0) {
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">Checkout</h1>
<div className="py-[60px] text-center text-muted-foreground">
<p>{t("cart_empty")}</p>
<Button asChild>
<Link to="/shop/">{t("checkout_browse")}</Link>
</Button>
</div>
</div>
);
}
// 已登录:账单表单 + 订单汇总
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">Checkout</h1>
{orderError ? <p className="rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">{orderError}</p> : null}
<div className="mt-4 grid grid-cols-1 items-start gap-7 min-[860px]:grid-cols-[minmax(0,7fr)_minmax(0,4fr)]">
<form className="grid grid-cols-1 gap-3.5 rounded-lg border border-line bg-soft p-5 min-[640px]:grid-cols-2" onSubmit={handlePlaceOrder}>
<h2 className="col-span-full mt-0">Billing details</h2>
<Label>
First name *
<Input name="firstName" required />
</Label>
<Label>
Last name *
<Input name="lastName" required />
</Label>
<Label>
Email *
<Input name="email" type="email" required defaultValue={user.email} />
</Label>
<Label>
Phone
<Input name="phone" type="tel" />
</Label>
<Label className="col-span-full">
Street address *
<Input name="address" required />
</Label>
<Label>
City *
<Input name="city" required />
</Label>
<Label>
Country *
<Input name="country" required />
</Label>
<Label>
Postcode
<Input name="postcode" />
</Label>
<Label className="col-span-full flex-row items-center gap-2">
<input name="shippingSame" type="checkbox" defaultChecked />
<span>{t("checkout_same_address")}</span>
</Label>
<Label className="col-span-full">
Order notes
<Textarea name="note" rows={3} placeholder={t("checkout_notes_ph")} />
</Label>
<div className="col-span-full">
<Button type="submit" disabled={submitting}>
{submitting ? "Placing order…" : "Place order"}
</Button>
<span className="text-[13px] text-muted-foreground">Payment: bank transfer (pay after confirmation)</span>
</div>
</form>
<div className="rounded-lg border border-line bg-soft p-5">
<h3 className="mt-0">Your order</h3>
<div className="overflow-hidden rounded-md border border-line bg-white">
{cart.items.map((line) => (
<div className="flex justify-between gap-3 border-b border-line px-3.5 py-2.5 text-sm" key={line.id}>
<span>
{line.title} × {line.qty}
</span>
<span>${(Number(line.price) * line.qty).toFixed(2)}</span>
</div>
))}
<div className="flex justify-between gap-3 px-3.5 py-2.5 text-sm font-bold">
<span>{t("checkout_subtotal")}</span>
<span>${cart.subtotal}</span>
</div>
</div>
</div>
</div>
</div>
);
}