v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* # 路由 /my-account 账户页(Tailwind + shadcn/ui 实现)
|
||||
* 未登录:登录 / 注册两个 Tab 表单;已登录:资料卡 + 订单列表 + 退出登录。
|
||||
*/
|
||||
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { getCurrentUser, loginFn, registerFn, logoutFn } from "~/server/auth";
|
||||
import { useT } from "~/i18n";
|
||||
import { getMyOrders } from "~/server/commerce";
|
||||
import { getLayoutData, Layout } from "~/components/website/Layout";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
|
||||
export const Route = createFileRoute("/my-account")({
|
||||
loader: async () => {
|
||||
const user = await getCurrentUser();
|
||||
const [layout, orders] = await Promise.all([getLayoutData(), user ? getMyOrders() : Promise.resolve([])]);
|
||||
return { layout, user, orders };
|
||||
},
|
||||
head: () => ({ meta: [{ title: "My Account - DusunIoT" }] }),
|
||||
component: MyAccountPage,
|
||||
});
|
||||
|
||||
/** # 函数 组件 账户页面(外壳:公共布局) */
|
||||
function MyAccountPage() {
|
||||
const { layout } = Route.useLoaderData();
|
||||
return (
|
||||
<Layout layout={layout}>
|
||||
<AccountInner />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
/** # 函数 组件 账户内容区 */
|
||||
function AccountInner() {
|
||||
const t = useT();
|
||||
const { user, orders } = Route.useLoaderData();
|
||||
const router = useRouter();
|
||||
|
||||
/** # 函数 退出登录 */
|
||||
async function handleLogout() {
|
||||
await logoutFn();
|
||||
router.invalidate();
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="site-container">
|
||||
<h1 className="mt-5 mb-3.5 text-[26px]">My Account</h1>
|
||||
<div className="max-w-[520px] rounded-lg border border-line bg-soft p-5">
|
||||
<Tabs defaultValue="login">
|
||||
<TabsList>
|
||||
<TabsTrigger value="login">{t("acct_login")}</TabsTrigger>
|
||||
<TabsTrigger value="register">{t("acct_register")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<LoginForm onDone={() => router.invalidate()} />
|
||||
</TabsContent>
|
||||
<TabsContent value="register">
|
||||
<RegisterForm onDone={() => router.invalidate()} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="site-container">
|
||||
<h1 className="mt-5 mb-3.5 text-[26px]">My Account</h1>
|
||||
<div className="mb-5 rounded-lg border border-line bg-soft p-5">
|
||||
<h3 className="mt-0">Profile</h3>
|
||||
<p className="m-0">
|
||||
<strong>{user.displayName || user.email}</strong> ({user.email}) — role: {user.role}
|
||||
<button className="ml-3.5 cursor-pointer border-0 bg-none text-[13px] text-muted-foreground underline hover:text-[#b91c1c]" onClick={() => void handleLogout()} type="button">
|
||||
Log out
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<h2>{t("acct_recent_orders")}</h2>
|
||||
{orders.length === 0 ? (
|
||||
<div className="py-[60px] text-center text-muted-foreground">
|
||||
<p>{t("acct_no_orders")}</p>
|
||||
<Button asChild>
|
||||
<Link to="/shop/">{t("checkout_browse")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full border-collapse bg-white text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
{(["acct_th_order", "acct_th_date", "acct_th_items", "acct_th_total", "acct_th_status", "acct_th_payment"] as const).map((k) => (
|
||||
<th key={k} className="border-b border-line bg-soft px-3 py-2.5 text-left font-semibold">
|
||||
{t(k)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((o) => (
|
||||
<tr key={o.id}>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">
|
||||
<strong>{o.orderNumber}</strong>
|
||||
</td>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">{o.createdAt ? new Date(o.createdAt).toLocaleDateString("en-US") : "-"}</td>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">{Number(o.itemCount)}</td>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">${Number(o.total).toFixed(2)}</td>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">
|
||||
<Badge variant={o.status === "completed" ? "success" : "default"}>{o.status}</Badge>
|
||||
</td>
|
||||
<td className="border-b border-line px-3 py-2.5 text-left">
|
||||
<Badge variant={o.paymentStatus === "paid" ? "success" : "warning"}>{o.paymentStatus}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** # 组件 登录表单 */
|
||||
function LoginForm({ onDone }: { onDone: () => void }) {
|
||||
const t = useT();
|
||||
const [authError, setAuthError] = useState("");
|
||||
|
||||
/** # 函数 登录提交 */
|
||||
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);
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="grid grid-cols-1 gap-3.5" onSubmit={handleLogin}>
|
||||
{authError ? <p className="m-0 rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">{authError}</p> : null}
|
||||
<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">{t("acct_login")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
/** # 组件 注册表单 */
|
||||
function RegisterForm({ onDone }: { onDone: () => void }) {
|
||||
const [authError, setAuthError] = useState("");
|
||||
|
||||
/** # 函数 注册提交 */
|
||||
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);
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="grid grid-cols-1 gap-3.5" onSubmit={handleRegister}>
|
||||
{authError ? <p className="m-0 rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">{authError}</p> : null}
|
||||
<Label>
|
||||
Display name (optional)
|
||||
<Input name="displayName" type="text" />
|
||||
</Label>
|
||||
<Label>
|
||||
Email
|
||||
<Input name="email" type="email" required autoComplete="email" />
|
||||
</Label>
|
||||
<Label>
|
||||
Password (min 8 chars)
|
||||
<Input name="password" type="password" required minLength={8} autoComplete="new-password" />
|
||||
</Label>
|
||||
<div>
|
||||
<Button type="submit" variant="outline">
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user