From 6348090dfefda9933fa3d7c46b163d24724f6f58 Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Thu, 7 May 2026 17:36:42 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/frontend/component-guidelines.md | 1 + .trellis/spec/frontend/type-safety.md | 3 + .../components/password-toggle/index.tsx | 25 +++ src/app/(auth)/index.scss | 9 + src/app/(auth)/layout.tsx | 137 ++++++++++++ src/app/(auth)/login/login-error.ts | 37 ++++ src/app/(auth)/login/page.tsx | 174 +++++++++++++++ src/app/(auth)/signup/page.tsx | 204 ++++++++++++++++++ src/app/(auth)/validate.ts | 14 ++ 9 files changed, 604 insertions(+) create mode 100644 src/app/(auth)/components/password-toggle/index.tsx create mode 100644 src/app/(auth)/index.scss create mode 100644 src/app/(auth)/layout.tsx create mode 100644 src/app/(auth)/login/login-error.ts create mode 100644 src/app/(auth)/login/page.tsx create mode 100644 src/app/(auth)/signup/page.tsx create mode 100644 src/app/(auth)/validate.ts diff --git a/.trellis/spec/frontend/component-guidelines.md b/.trellis/spec/frontend/component-guidelines.md index 15b22b4..1882135 100644 --- a/.trellis/spec/frontend/component-guidelines.md +++ b/.trellis/spec/frontend/component-guidelines.md @@ -71,6 +71,7 @@ If a component requires a large amount of static data (e.g., "dead" data for ren **Every method, property, interface, and complex logic block MUST be documented.** - **Language Requirement**: All comments inside the code (JSDoc and internal) **MUST be written in Chinese**. +- **Mandatory Positive Constraint**: Every new **interface**, **type**, **exported or module-level constant**, **function component**, and **business logic function** MUST have a JSDoc `/** */` comment in **Chinese**. The only exception is for local variables within a function that are immediately obvious and self-explanatory. - **Public API/Props/Interfaces**: Use JSDoc style `/** ... */` **mandatory** for every interface definition and **every single property** within that interface. - **Methods & Functions**: Every function (exported or internal) **must** have a `/** ... */` comment explaining its purpose, parameters, and return value. - **Internal Logic**: Use double-slash `//` for step-by-step explanations inside function bodies. diff --git a/.trellis/spec/frontend/type-safety.md b/.trellis/spec/frontend/type-safety.md index 6994bb5..684e2ee 100644 --- a/.trellis/spec/frontend/type-safety.md +++ b/.trellis/spec/frontend/type-safety.md @@ -124,6 +124,9 @@ export const SearchBar = ({ placeholder, onSearch }: SearchBarProps) => { Define the store state and actions with an interface. ```typescript +/** + * 权益状态 + */ interface AuthState { token: string | null; setToken: (token: string) => void; diff --git a/src/app/(auth)/components/password-toggle/index.tsx b/src/app/(auth)/components/password-toggle/index.tsx new file mode 100644 index 0000000..f74052f --- /dev/null +++ b/src/app/(auth)/components/password-toggle/index.tsx @@ -0,0 +1,25 @@ +import { Eye, EyeOff } from "lucide-react"; + +/** 密码显示/隐藏切换按钮的参数。 */ +interface PasswordToggleProps { + isShown: boolean; + onToggle: () => void; +} + +/** 密码显示/隐藏切换按钮,复用在登录和注册密码输入框中。 */ +export function PasswordToggle({ isShown, onToggle }: PasswordToggleProps) { + return ( + + ); +} diff --git a/src/app/(auth)/index.scss b/src/app/(auth)/index.scss new file mode 100644 index 0000000..82513b6 --- /dev/null +++ b/src/app/(auth)/index.scss @@ -0,0 +1,9 @@ +.storeai-auth { + /* 认证壳层的细网格背景,只在该路由组内生效。 */ + .auth-grid { + background-image: linear-gradient(to right, rgb(226 232 240 / 0.45) 1px, transparent 1px), + linear-gradient(to bottom, rgb(226 232 240 / 0.45) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%); + } +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..d930c28 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,137 @@ +import Link from "next/link"; +import { BellRing, Clock3, MessagesSquare, ShieldCheck } from "lucide-react"; + +import "./index.scss"; + +/** 认证壳层左侧卖点行的入参。 */ +interface FeatureRowProps { + icon: React.ComponentType<{ className?: string; "aria-hidden"?: true }>; + title: string; + body: string; +} + +/** + * 登录和注册共享的认证壳层。 + * 桌面端展示左侧品牌说明,移动端保留紧凑品牌头,表单内容由子路由提供。 + */ +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+ ); +} + +/** 认证壳层左侧的单条产品卖点。 */ +function FeatureRow({ icon: Icon, title, body }: FeatureRowProps) { + return ( +
  • + + + +
    +

    {title}

    +

    {body}

    +
    +
  • + ); +} diff --git a/src/app/(auth)/login/login-error.ts b/src/app/(auth)/login/login-error.ts new file mode 100644 index 0000000..5e4dd19 --- /dev/null +++ b/src/app/(auth)/login/login-error.ts @@ -0,0 +1,37 @@ +export type LoginErrorType = "1" | "2" | "3"; + +export interface LoginNotice { + tone: "warning" | "success"; + title: string; + body: string; +} + +const login_error_text: Record = { + "1": { + tone: "warning", + title: "Signed out from the extension.", + body: "Log back in to continue scanning.", + }, + "2": { + tone: "warning", + title: "Signed in on another device.", + body: + "StoreAI allows one active session per account, so this device was signed out. Log back in to continue here - the other device will then be signed out.", + }, + "3": { + tone: "success", + title: "An account already exists with this email.", + body: "Sign in below to continue.", + }, +}; + +/** + * 根据 query 中的 error_type 获取登录页提示配置。 + */ +export function getLoginNotice(errorType: string | null): LoginNotice | null { + if (!errorType) return null; + if (errorType in login_error_text) { + return login_error_text[errorType as LoginErrorType]; + } + return null; +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..f8df401 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,174 @@ +"use client"; + +import {Suspense, useState} from "react"; +import Link from "next/link"; +import {useSearchParams} from "next/navigation"; +import {AlertTriangle, Lock, Mail} from "lucide-react"; + +import {PasswordToggle} from "../components/password-toggle"; +import {getLoginNotice} from "./login-error"; +import {validateSignup} from "../validate"; + +/** 登录表单提交参数。 */ +interface LoginPayload { + email: string; + password: string; +} + +/** 登录页入口,为依赖查询参数的表单提供 Suspense 边界。 */ +export default function LoginPage() { + return ( + }> + + + ); +} + +/** 登录表单加载占位,保持认证壳层布局稳定。 */ +function LoginFormFallback() { + return
    ; +} + +/** 登录表单主体,保留 query 逻辑、校验和错误态。 */ +function LoginForm() { + const searchParams = useSearchParams(); + /*** 提示 */ + const loginNotice = getLoginNotice(searchParams.get("error_type")); + const prefillEmail = searchParams.get("email") ?? ""; + + const [email, setEmail] = useState(prefillEmail); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + /*** 错误*/ + const [error, setError] = useState(""); + /*** 提交中*/ + const [loading, setLoading] = useState(false); + + /** 处理登录表单提交,真实接口接入前只执行前端校验和占位提交。 */ + async function handleSubmit(event: any) { + event.preventDefault(); + setError(null); + //效验 + const validationError = validateSignup({email, password}); + if (validationError) { + setError(validationError); + return; + } + setLoading(true); + try { + + } catch (err) { + + } finally { + setLoading(false); + } + } + + return ( +
    +
    +

    Welcome back

    +

    + Sign in to your StoreAI dashboard. +

    +
    + + {/*提示*/} + {loginNotice && ( + + {loginNotice.body} + + )} + +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + + {error && ( +
    +

    {error}

    +
    + )} + +
    + +

    + New to StoreAI?{" "} + + Start your free trial + +

    +
    + ); +} + + +/** 登录页中用来解释 query 状态的提示条。 */ +function Notice({title, children, tone,}: { + title: string; + children: React.ReactNode; + tone: "warning" | "success"; +}) { + const className = + tone === "success" + ? "flex items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50 p-3 text-xs leading-relaxed text-emerald-900" + : "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900"; + + return ( +
    +
    + ); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..c467733 --- /dev/null +++ b/src/app/(auth)/signup/page.tsx @@ -0,0 +1,204 @@ +"use client"; + +import {useMemo, useState} from "react"; +import Link from "next/link"; +import {CheckCircle2, Lock, Mail, Sparkles} from "lucide-react"; + +import {PasswordToggle} from "../components/password-toggle"; +import {validateSignup} from "../validate"; + + +/** 密码强度的分级结果。 */ +interface PasswordStrength { + score: 0 | 1 | 2 | 3; + label: string; +} + +/** 密码强度条的颜色映射。 */ +const STRENGTH_BAR_COLOR: Record = { + 0: "bg-muted", + 1: "bg-rose-500", + 2: "bg-amber-500", + 3: "bg-emerald-500", +}; + +/** 密码强度标签的颜色映射。 */ +const STRENGTH_LABEL_COLOR: Record = { + 0: "text-muted-foreground", + 1: "text-rose-600", + 2: "text-amber-600", + 3: "text-emerald-600", +}; + + +/** 注册表单主体,保留 query 逻辑、密码强度和校验。 */ +export default function SignupForm() { + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const strength = useMemo(() => gradePassword(password), [password]); + + /** 处理注册表单提交,真实接口接入前只执行前端校验和占位提交。 */ + async function handleSubmit(event: any) { + event.preventDefault(); + setError(null); + + const validationError = validateSignup({email, password}); + if (validationError) { + setError(validationError); + return; + } + + setLoading(true); + try { + // await submitSignup({email, password}); + } catch (err) { + // setError(err instanceof Error ? err.message : "Sign-up failed."); + } finally { + setLoading(false); + } + } + + return ( +
    +
    +
    +
    +

    Create your StoreAI account

    +

    + Two scheduled reports a day from your store. Cancel anytime. +

    +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + + + {/*密码等级*/} + { + password &&
    +
    + {[1, 2, 3].map((slot) => ( +
    = slot ? STRENGTH_BAR_COLOR[strength.score] : "bg-muted" + }`} + /> + ))} +
    +
    + {strength.label} +
    +
    + } +
    + + {error && ( +
    + {error} +
    + )} + + + + +
      + 24-hour free trial - full feature access + No credit card needed to start + Cancel anytime from billing +
    + +

    + Already have an account?{" "} + + Sign in + +

    +
    + ); +} + + +/** 按长度和字符类型给密码强度分级。 */ +function gradePassword(password: string): PasswordStrength { + if (!password) return {score: 0, label: ""}; + if (password.length < 8) return {score: 1, label: "Too short"}; + + const hasNum = /\d/.test(password); + const hasLower = /[a-z]/.test(password); + const hasUpper = /[A-Z]/.test(password); + const hasSym = /[^A-Za-z0-9]/.test(password); + const variety = Number(hasNum) + Number(hasLower) + Number(hasUpper) + Number(hasSym); + + if (password.length >= 12 && variety >= 3) return {score: 3, label: "Strong"}; + if (password.length >= 10 && variety >= 2) return {score: 2, label: "Good"}; + return {score: 1, label: "Weak"}; +} + + +/** 注册页信任背书列表中的单行。 */ +function TrustRow({children}: { children: React.ReactNode }) { + return ( +
  • +
  • + ); +} diff --git a/src/app/(auth)/validate.ts b/src/app/(auth)/validate.ts new file mode 100644 index 0000000..2809b93 --- /dev/null +++ b/src/app/(auth)/validate.ts @@ -0,0 +1,14 @@ +/** 注册表单提交参数。 */ +interface SignupPayload { + email: string; + password: string; +} +/** 注册参数校验,保留原型里的邮箱和密码长度约束。 */ + +export function validateSignup({email, password}: SignupPayload): string | null { + if (!email.trim()) return "Email is required."; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Enter a valid email address."; + if (!password) return "Password is required."; + if (password.length < 8) return "Password must be at least 8 characters."; + return null; +} \ No newline at end of file