202 lines
7.3 KiB
TypeScript
202 lines
7.3 KiB
TypeScript
/**
|
|
* # 路由 /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>
|
|
);
|
|
}
|