全新UI语言

This commit is contained in:
2026-07-11 23:37:48 +08:00
parent 4dde861c99
commit 386a28709f
19 changed files with 1032 additions and 1931 deletions
+15 -30
View File
@@ -87,8 +87,8 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
}
return (
<div className="rounded-[32px] border border-white/75 bg-white/84 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur sm:p-7">
<div className="flex flex-wrap gap-3">
<div>
<div className="segment">
{[
{ id: "login", label: "已有账号,直接登录" },
{ id: "register", label: "第一次使用,创建账号" },
@@ -103,11 +103,7 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
setMode(item.id as AuthMode);
setNotice(null);
}}
className={`inline-flex h-11 items-center justify-center rounded-full px-4 text-sm font-semibold transition ${
active
? "bg-[var(--ink)] text-white shadow-[0_12px_30px_rgba(36,27,20,0.18)]"
: "border border-[var(--line)] bg-white text-[var(--ink)] hover:bg-[var(--paper-soft)]"
}`}
className={active ? "active" : ""}
>
{item.label}
</button>
@@ -115,28 +111,21 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
})}
</div>
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
<form style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 12 }} onSubmit={handleSubmit}>
<div>
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-username">
用户名
</label>
<label className="sr-only" htmlFor="caregiver-username">用户名</label>
<input
id="caregiver-username"
value={username}
onChange={(event) => setUsername(event.target.value)}
autoComplete="username"
placeholder="例如:family_guardian"
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
placeholder="用户名"
className="field"
/>
<p className="mt-2 text-xs leading-6 text-[var(--muted)]">
建议使用 3 到 24 位字母、数字、下划线或横线,方便全家统一登录。
</p>
</div>
<div>
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-password">
密码
</label>
<label className="sr-only" htmlFor="caregiver-password">密码</label>
<input
id="caregiver-password"
type="password"
@@ -144,18 +133,13 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
onChange={(event) => setPassword(event.target.value)}
autoComplete={mode === "login" ? "current-password" : "new-password"}
placeholder="请输入密码"
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
className="field"
/>
<p className="mt-2 text-xs leading-6 text-[var(--muted)]">
密码至少 8 位。同一账号可以在多台家属设备登录,消息提醒会同步到所有已登录设备。
</p>
</div>
{mode === "register" ? (
<div>
<label className="text-sm font-semibold text-[var(--ink)]" htmlFor="caregiver-password-confirm">
再输入一次密码
</label>
<label className="sr-only" htmlFor="caregiver-password-confirm">再输入一次密码</label>
<input
id="caregiver-password-confirm"
type="password"
@@ -163,7 +147,7 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
onChange={(event) => setConfirmPassword(event.target.value)}
autoComplete="new-password"
placeholder="再次输入密码"
className="mt-2 h-13 w-full rounded-[22px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
className="field"
/>
</div>
) : null}
@@ -171,7 +155,8 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
<button
type="submit"
disabled={submitting}
className="inline-flex h-12 w-full items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)] disabled:cursor-not-allowed disabled:opacity-60"
className="btn primary block"
style={{ minHeight: 44 }}
>
{submitting
? mode === "login"
@@ -184,10 +169,10 @@ export function AccountAuthForm({ redirectTo }: AccountAuthFormProps) {
</form>
{notice ? (
<p className="mt-4 rounded-[20px] bg-[var(--paper-soft)] px-4 py-3 text-sm leading-7 text-[var(--muted)]">
<p style={{ margin: "10px 0 0", borderRadius: 8, background: "var(--surface)", padding: "10px 12px", color: "var(--muted)", fontSize: 12, lineHeight: 1.6 }}>
{notice}
</p>
) : null}
</div>
);
}
}
+21 -46
View File
@@ -31,45 +31,28 @@ export function BindDeviceForm() {
}
return (
<div className="rounded-[28px] border border-white/70 bg-white/80 p-5 shadow-[0_24px_60px_rgba(125,80,46,0.12)] backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
添加设备
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
扫码或粘贴设备码连接长辈
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
让长辈打开手机上的「家人连接」页面,扫描二维码最快;也可以将设备码粘贴到下方。
</p>
</div>
<Link
href="/scan"
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
>
扫码连接
</Link>
</div>
<form className="mt-5 flex flex-col gap-3 lg:flex-row" onSubmit={handleSubmit}>
<div className="card card-pad">
<p style={{ margin: 0, fontSize: 13, fontWeight: 600 }}>用设备码添加</p>
<form className="button-row" style={{ marginTop: 9 }} onSubmit={handleSubmit}>
<input
value={rawCode}
onChange={(event) => setRawCode(event.target.value)}
placeholder="粘贴设备码或二维码链接"
className="h-13 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
placeholder="粘贴长辈屏幕下方的设备码"
className="field"
style={{ height: 36, flex: 1, minWidth: 0 }}
/>
<button
type="submit"
disabled={submitting}
className="inline-flex h-13 min-w-36 items-center justify-center rounded-full bg-[var(--olive)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)] disabled:cursor-not-allowed disabled:opacity-60"
className="btn dark"
style={{ minHeight: 36, flex: "none" }}
>
{submitting ? "正在连接" : "立即绑定"}
{submitting ? "连接中" : "连接"}
</button>
</form>
{notice ? (
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
<p style={{ margin: "8px 0 0", whiteSpace: "pre-line", color: "var(--success)", fontSize: 12 }}>
{notice}
</p>
) : null}
@@ -142,20 +125,14 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
}
return (
<form className="rounded-[28px] border border-white/70 bg-white/88 p-5" onSubmit={handleSubmit}>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
留言
</p>
<h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
给长辈写一句问候
</h4>
</div>
<form className="card card-pad" onSubmit={handleSubmit}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<h4 style={{ margin: 0, fontSize: 13, fontWeight: 600 }}>给长辈留言</h4>
<select
value={importance}
onChange={(event) => setImportance(event.target.value)}
className="h-11 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none"
className="field"
style={{ width: "auto", height: 32, padding: "0 8px", fontSize: 12 }}
>
<option value="LOW">温和提醒</option>
<option value="NORMAL">日常问候</option>
@@ -169,24 +146,22 @@ export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
onChange={(event) => setContent(event.target.value)}
rows={4}
placeholder="例如:妈,晚上天气凉,记得添件衣服。晚饭后我给您打电话。"
className="mt-4 w-full rounded-[24px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-4 text-sm leading-7 text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
className="field"
style={{ marginTop: 8 }}
/>
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-7 text-[var(--muted)]">
长辈和智能助理聊天时,就能看到这条留言。
</p>
<div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", marginTop: 9 }}>
<button
type="submit"
disabled={submitting}
className="inline-flex h-12 min-w-32 items-center justify-center rounded-full bg-[var(--ink)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--ink-soft)] disabled:cursor-not-allowed disabled:opacity-60"
className="btn primary small"
>
{submitting ? "正在送达" : "写好并送出"}
</button>
</div>
{notice ? (
<div className="mt-3 space-y-2 text-sm leading-7 text-[var(--muted)]">
<div style={{ marginTop: 9, color: "var(--success)", fontSize: 12, lineHeight: 1.6 }}>
<p className="whitespace-pre-line">{notice}</p>
{createdMessagePublicId ? (
<Link
@@ -356,4 +331,4 @@ export function ManageDeviceForm({
) : null}
</section>
);
}
}
+52 -78
View File
@@ -1,39 +1,29 @@
import Link from "next/link";
import {
Home,
MessageCircle,
Smartphone,
Activity,
ScanLine,
Settings,
} from "lucide-react";
import { ChevronLeft, Home, MessageCircle, Smartphone, UserRound } from "lucide-react";
import type { ReactNode } from "react";
const navigationItems = [
{ href: "/", label: "首页", icon: Home },
{ href: "/", label: "概览", icon: Home },
{ href: "/messages", label: "留言", icon: MessageCircle },
{ href: "/devices", label: "设备", icon: Smartphone },
{ href: "/activity", label: "动态", icon: Activity },
{ href: "/scan", label: "扫码", icon: ScanLine },
{ href: "/settings", label: "设置", icon: Settings },
{ href: "/settings", label: "我的", icon: UserRound },
];
function isActivePath(currentPath: string, href: string) {
if (href === "/") {
return currentPath === "/";
}
if (href === "/") return currentPath === "/";
return currentPath === href || currentPath.startsWith(`${href}/`);
}
type PanelShellProps = {
currentPath: string;
title: string;
description: string;
description?: string;
caregiverLabel: string;
children: ReactNode;
actions?: ReactNode;
eyebrow?: string;
backHref?: string;
showTabs?: boolean;
unreadCount?: number;
};
export function PanelShell({
@@ -43,71 +33,55 @@ export function PanelShell({
caregiverLabel,
children,
actions,
eyebrow = "家人连线",
backHref,
showTabs = !backHref,
unreadCount = 0,
}: PanelShellProps) {
const initial = caregiverLabel.trim().slice(-1) || "安";
return (
<main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<div className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.8),transparent_70%)]" />
<div className="mx-auto max-w-7xl space-y-6">
<header className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-7 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-8">
<div className="absolute -right-12 top-[-50px] h-44 w-44 rounded-full bg-[rgba(53,84,141,0.14)] blur-2xl" />
<div className="absolute left-10 top-10 h-24 w-24 rounded-full border border-[rgba(118,132,94,0.18)] z-0 pointer-events-none" />
<div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
<div className="max-w-4xl">
<div className="flex flex-wrap gap-2">
{navigationItems.map((item) => {
const active = isActivePath(currentPath, item.href);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`inline-flex h-11 items-center justify-center gap-2 rounded-full px-4 text-sm font-semibold transition ${
active
? "bg-[var(--ink)] text-white shadow-[0_12px_30px_rgba(36,27,20,0.18)]"
: "border border-[var(--line)] bg-white/82 text-[var(--ink)] hover:bg-[var(--paper-soft)]"
}`}
>
<Icon size={16} />
<span>{item.label}</span>
</Link>
);
})}
</div>
<p className="mt-6 text-sm font-semibold tracking-[0.24em] text-[var(--copper)] uppercase">
{eyebrow}
</p>
<h1 className="mt-4 max-w-3xl font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)] sm:text-5xl">
{title}
</h1>
<p className="mt-5 max-w-2xl text-base leading-8 text-[var(--muted)] sm:text-lg">
{description}
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-white/76 px-4 py-2">
账号:{caregiverLabel}
</span>
<form action="/api/account/logout" method="post">
<button
type="submit"
className="inline-flex h-10 items-center justify-center rounded-full border border-[var(--line)] bg-white px-4 font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
>
退出登录
</button>
</form>
</div>
<main className="app-frame">
<div className={`app-content app-stack${backHref ? " detail" : ""}`}>
{backHref ? (
<header className="detail-header">
<Link href={backHref} className="back-button" aria-label="返回">
<ChevronLeft size={20} strokeWidth={2} />
</Link>
<h1>{title}</h1>
{actions}
</header>
) : (
<header className="page-heading">
<div>
<h1>{title}</h1>
{description ? <p>{description}</p> : null}
</div>
{actions ? <div className="w-full max-w-md xl:pt-2">{actions}</div> : null}
</div>
</header>
{actions ?? (
<Link href="/settings" className="avatar" aria-label="我的">
{initial}
</Link>
)}
</header>
)}
{children}
</div>
{showTabs ? (
<nav className="bottom-nav" aria-label="主导航">
{navigationItems.map((item) => {
const active = isActivePath(currentPath, item.href);
const Icon = item.icon;
return (
<Link key={item.href} href={item.href} className={`nav-item${active ? " active" : ""}`}>
<Icon size={22} strokeWidth={1.8} />
<span>{item.label}</span>
{item.href === "/messages" && unreadCount > 0 ? <span className="nav-badge">{unreadCount}</span> : null}
</Link>
);
})}
</nav>
) : null}
</main>
);
}
}