chore(product): replace template branding with 弹弹 Web copy and product SEO

This commit is contained in:
noelorin 2026-09-26 03:13:48 +08:00
parent 668f90bcc0
commit f48f0c8d51
12 changed files with 171 additions and 131 deletions

View File

@ -1,6 +1,6 @@
# App Template — 设计方向 # 弹弹 Web — 设计方向
脚手架视觉基线:**深空工业中控终端**。不绑定具体产品叙事;下游可在此 palette / type 上换肤,但 **`@app/design-tokens` 色板 token 路径约定不变**(改值可以,改结构需同步全局)。 产品视觉基线:**shadcn/ui Dashboard**(媒体五页侧栏/顶栏导航、卡片网格、中性灰阶);模板深空工业 token 仍可用于非媒体页,但 **`@app/design-tokens` 色板 token 路径约定不变**(改值可以,改结构需同步全局)。
## Style anchor ## Style anchor
深色工业 HUD / 控制台:近黑底、信号黄强调、发丝线框、等宽铭牌。 深色工业 HUD / 控制台:近黑底、信号黄强调、发丝线框、等宽铭牌。
@ -36,8 +36,8 @@
## Interaction ## Interaction
- 主题:系统 / 亮 / 暗(`app-theme`) - 主题:系统 / 亮 / 暗(`app-theme`)
- 语言:cookie `app-lang` + Accept-Language - 语言:cookie `app-lang` + Accept-Language
- SSR 输出结构与内容;动效只在浏览器 effect 内绑定 - **薄 SSR**:服务端输出布局 / head / 导航;页面主体(`components/pages/*Body`)`client:load` 在浏览器渲染
- 尊重 `prefers-reduced-motion` - 动效只在浏览器 effect 内绑定;尊重 `prefers-reduced-motion`
## 范围 ## 范围
`tokens.css` · `globals.css` · `AppLayout.astro` · `ThemeSwitch` · `UserMenu` · `home` / `dashboard` 视图 · `ui/*` `tokens.css` · `globals.css` · `AppLayout.astro` · `ThemeSwitch` · `UserMenu` · `home` / `dashboard` 视图 · `ui/*`

View File

@ -1,6 +1,8 @@
# App Template — Astro + tRPC + Drizzle Monorepo # 弹弹 Web — 本地片源 · 在线弹幕
可复用全栈脚手架:**不绑定产品业务**,只保留鉴权 + `user` 垂直示例,演示完整分层接线。 多用户 Web 媒体中心:WebDAV/OpenList 挂载、媒体库扫描与 Bangumi 刮削、ArtPlayer 播放与弹幕(开放网络 + 本地 XML)。
基于 monorepo 分层(types → models → db → dao → trpc → web),本仓库按**产品仓**维护。
``` ```
. .

View File

@ -0,0 +1,63 @@
/**
* 首页主体:薄 SSR — Astro 壳负责 head/导航,本组件在浏览器渲染。
*/
import { t, type Locale } from "@app/i18n";
export interface HomeBodyProps {
locale: Locale;
}
export default function HomeBody({ locale }: HomeBodyProps) {
const tr = (key: Parameters<typeof t>[1]) => t(locale, key);
const libraryHref = locale === "zh-CN" ? "/zh-CN/library" : "/en/library";
const loginHref = locale === "zh-CN" ? "/zh-CN/auth/login" : "/en/auth/login";
return (
<section className="space-y-10 py-10">
<div className="u-reveal">
<p className="u-label u-label-edge">{tr("meta.tagline")}</p>
<h1 className="mt-3 text-[40px] font-black tracking-tight leading-[1.05]">
{tr("home.heroLine1")}
<br />
{tr("home.heroLine2")}
</h1>
<span className="u-edge-bar mt-5" />
<p
className="mt-5 max-w-xl text-[14px] leading-relaxed"
style={{ color: "var(--ink-2);" }}
>
{tr("home.subtitle")}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a href={libraryHref} className="u-btn u-btn--edge">
{tr("home.ctaDashboard")}
</a>
<a href={loginHref} className="u-btn u-btn--ghost">
{tr("home.ctaLogin")}
</a>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
<section className="u-card p-6 u-reveal">
<p className="u-label">{tr("home.stackTitle")}</p>
<p
className="mt-3 text-[14px] leading-relaxed"
style={{ color: "var(--ink-2);" }}
>
{tr("home.stackBody")}
</p>
</section>
<section className="u-card p-6 u-reveal">
<p className="u-label">{tr("home.layerTitle")}</p>
<p
className="mt-3 text-[14px] leading-relaxed"
style={{ color: "var(--ink-2);" }}
>
{tr("home.layerBody")}
</p>
</section>
</div>
</section>
);
}

View File

@ -40,7 +40,6 @@ const {
const brandName = brand ?? t(locale, "meta.brand"); const brandName = brand ?? t(locale, "meta.brand");
const nav = navProp ?? []; const nav = navProp ?? [];
const isProductBrand = Boolean(brand) && brand !== t(locale, "meta.brand");
const siteSeo = getSiteSeo(locale); const siteSeo = getSiteSeo(locale);
const pageSeoInput: PageSeoInput = { const pageSeoInput: PageSeoInput = {
@ -54,12 +53,9 @@ const jsonLdHtml = toJsonLdScript(buildDefaultJsonLd(siteSeo, pageSeoInput));
const pageTitle = headTags.find((tTag) => tTag.key === "og:title")?.value ?? title; const pageTitle = headTags.find((tTag) => tTag.key === "og:title")?.value ?? title;
const homeHref = locale === "zh-CN" ? "/zh-CN" : "/en"; const homeHref = locale === "zh-CN" ? "/zh-CN" : "/en";
const brandHref = isProductBrand ? (locale === "zh-CN" ? "/zh-CN/library" : "/en/library") : homeHref; const libraryHref = locale === "zh-CN" ? "/zh-CN/library" : "/en/library";
const brandInitial = isProductBrand const brandHref = hideSideRail ? libraryHref : homeHref;
? brandName.charAt(0) const brandInitial = brandName.charAt(0) || "弹";
: locale === "zh-CN"
? "模"
: "A";
--- ---
<!doctype html> <!doctype html>
@ -222,10 +218,8 @@ const brandInitial = isProductBrand
<footer class="site-footer"> <footer class="site-footer">
<div class="u-dotline" aria-hidden="true"></div> <div class="u-dotline" aria-hidden="true"></div>
<div class="site-footer__inner"> <div class="site-footer__inner">
<p class="u-mono">{isProductBrand ? brandName : t(locale, "meta.brand")}</p> <p class="u-mono">{brandName}</p>
<p class="u-label"> <p class="u-label">{t(locale, "home.footer")}</p>
{isProductBrand ? t(locale, "meta.productTagline") : t(locale, "home.footer")}
</p>
</div> </div>
</footer> </footer>
)} )}

View File

@ -17,8 +17,8 @@ export function getSiteSeo(locale: Locale): SiteSeoConfig {
const name = t(locale, "meta.brand"); const name = t(locale, "meta.brand");
const isZh = locale === "zh-CN"; const isZh = locale === "zh-CN";
const description = isZh const description = isZh
? "Astro + tRPC + Drizzle monorepo 应用脚手架,开箱即用的分层基线。" ? "挂载 WebDAV/OpenList 片源的 Web 媒体库:扫描、Bangumi 刮削、ArtPlayer 播放与在线弹幕。"
: "Astro + tRPC + Drizzle monorepo application scaffold with a ready vertical baseline."; : "Web media library for WebDAV/OpenList: scan, Bangumi scrape, ArtPlayer playback and online danmaku.";
return { return {
siteUrl, siteUrl,
@ -29,15 +29,15 @@ export function getSiteSeo(locale: Locale): SiteSeoConfig {
organization: { name, url: siteUrl }, organization: { name, url: siteUrl },
llms: { llms: {
summary: isZh summary: isZh
? "这是可复用的应用脚手架模板:Astro SSR + React islands + tRPC + Drizzle/Turso,附带鉴权与用户垂直示例。" ? "弹弹 Web:多用户 Web 媒体中心。WebDAV 挂载、媒体库扫描、Bangumi 刮削、ArtPlayer 播放与弹弹play 开放弹幕网络中继,本地 XML 兜底。"
: "Reusable application scaffold: Astro SSR + React islands + tRPC + Drizzle/Turso, with auth and a user vertical example.", : "DanDan Web: multi-user web media hub. WebDAV mounts, library scan, Bangumi scraping, ArtPlayer playback, dandanplay open danmaku relay with local XML fallback.",
details: [ details: [
isZh isZh
? "分层:types → models → db → dao → trpc → web。业务实体按 user 切片扩展,禁止跨层。" ? "分层:types → models → db → dao → trpc → web。媒体域按垂直切片扩展。"
: "Layers: types → models → db → dao → trpc → web. Extend domains via the user slice; never skip layers.", : "Layers: types → models → db → dao → trpc → web. Extend the media domain as a vertical slice.",
isZh isZh
? "不绑定具体产品业务;works 等领域逻辑应在业务仓库中添加。" ? "挂载凭据仅存服务端加密;浏览器不直连网盘密码。"
: "No product domain baked in; add domains such as works in your product repo.", : "Mount credentials are encrypted server-side; the browser never sees remote passwords.",
], ],
pages: PUBLIC_PATHS.map((basePath) => ({ pages: PUBLIC_PATHS.map((basePath) => ({
path: pathForLocale(locale, basePath), path: pathForLocale(locale, basePath),
@ -45,9 +45,9 @@ export function getSiteSeo(locale: Locale): SiteSeoConfig {
description, description,
})), })),
discouraged: isZh discouraged: isZh
? ["/dashboard、/settings 等需登录页面", "/auth/* 等认证页", "/api/* 接口响应"] ? ["/library、/watch 等需登录页面", "/auth/* 等认证页", "/api/* 接口响应"]
: [ : [
"/dashboard, /settings and other app pages", "/library, /watch and other app pages",
"/auth/* and auth pages", "/auth/* and auth pages",
"/api/* responses", "/api/* responses",
], ],

View File

@ -15,16 +15,16 @@ function buildFor(locale: Locale): string {
} }
return buildLlmsFullTxt(siteSeo, { return buildLlmsFullTxt(siteSeo, {
title: `${t(locale, "meta.brand")} · scaffold index`, title: `${t(locale, "meta.brand")} · product guide`,
sections: [ sections: [
{ {
heading: t(locale, "home.stackTitle"), heading: t(locale, "home.stackTitle"),
path: "/zh-CN", path: "/zh-CN",
summary: t(locale, "home.stackBody"), summary: t(locale, "home.stackBody"),
points: [ points: [
"Astro 7 SSR + React 19 islands", "WebDAV / OpenList 挂载代理(凭据服务端加密)",
"tRPC v11 + Drizzle ORM + Turso/libSQL", "扫描入库 + Bangumi 刮削 + ArtPlayer 播放",
"Tailwind CSS 4 + Biome + Yarn 4 workspaces + Turbo", "弹弹play 开放弹幕网络中继,本地 XML 兜底",
], ],
}, },
{ {
@ -32,19 +32,19 @@ function buildFor(locale: Locale): string {
path: "/zh-CN", path: "/zh-CN",
summary: t(locale, "home.layerBody"), summary: t(locale, "home.layerBody"),
points: [ points: [
"types → models → db → dao → trpc → web", "多用户:每人独立媒体库与播放进度",
"user / auth 为垂直示例,可替换为业务实体", "媒体页 shadcn Dashboard 导航:媒体库 / 浏览 / 挂载 / 弹幕设置",
], ],
}, },
], ],
faqs: [ faqs: [
{ {
question: "这个模板包含业务逻辑吗?", question: "如何接入 OpenList?",
answer: "不包含产品业务。仅保留鉴权与 user 垂直示例,用于演示分层接线;具体业务应在下游仓库添加。", answer: "在挂载管理填写 OpenList 暴露的 WebDAV 地址(如 https://host/dav)与账号,测试连接后保存。",
}, },
{ {
question: "如何新增业务实体?", question: "弹幕从哪里来?",
answer: "按 user 切片扩展:@app/types 加 schema → @app/models 建表 → @app/dao 加查询 → @app/trpc 加 service/router → @app/web 挂页面。", answer: "优先走弹弹play 开放弹幕网络(需配置 AppId/AppSecret);失败或未配置时可在播放页导入本地 XML。",
}, },
], ],
}); });

View File

@ -1,7 +1,5 @@
--- ---
import { alternatePath, type Locale, t } from "@app/i18n"; import { alternatePath, type Locale, localePath, t } from "@app/i18n";
import { Toaster } from "@app/ui";
import UserList from "~/components/UserList";
import AppLayout from "~/layouts/AppLayout.astro"; import AppLayout from "~/layouts/AppLayout.astro";
export interface Props { export interface Props {
@ -10,6 +8,9 @@ export interface Props {
const { locale } = Astro.props; const { locale } = Astro.props;
const other: Locale = locale === "zh-CN" ? "en" : "zh-CN"; const other: Locale = locale === "zh-CN" ? "en" : "zh-CN";
// 产品:/dashboard 即媒体库(保留路由以免旧链接与 e2e 404)
return Astro.redirect(localePath(locale, "/library"));
--- ---
<AppLayout <AppLayout
@ -24,18 +25,5 @@ const other: Locale = locale === "zh-CN" ? "en" : "zh-CN";
robots: ["noindex", "nofollow"], robots: ["noindex", "nofollow"],
}} }}
> >
<div class="space-y-10 py-8"> <p class="py-10 text-sm text-muted-foreground">{t(locale, "dashboard.desc")}</p>
<section class="u-reveal">
<p class="u-label u-label-edge">{t(locale, "dashboard.label")}</p>
<h2 class="mt-2 text-[32px] font-black tracking-tight">{t(locale, "dashboard.heading")}</h2>
<span class="u-edge-bar mt-4"></span>
<p class="mt-4 max-w-2xl text-[14px] leading-relaxed" style="color: var(--ink-2);">
{t(locale, "dashboard.desc")}
</p>
<div class="u-card mt-8 p-6">
<UserList client:visible />
</div>
</section>
</div>
<Toaster client:load />
</AppLayout> </AppLayout>

View File

@ -1,5 +1,6 @@
--- ---
import { alternatePath, type Locale, t } from "@app/i18n"; import { alternatePath, type Locale, t } from "@app/i18n";
import HomeBody from "~/components/pages/HomeBody";
import AppLayout from "~/layouts/AppLayout.astro"; import AppLayout from "~/layouts/AppLayout.astro";
export interface Props { export interface Props {
@ -7,49 +8,16 @@ export interface Props {
} }
const { locale } = Astro.props; const { locale } = Astro.props;
const tr = (key: Parameters<typeof t>[1]) => t(locale, key);
const otherLocale: Locale = locale === "zh-CN" ? "en" : "zh-CN"; const otherLocale: Locale = locale === "zh-CN" ? "en" : "zh-CN";
const switchHref = alternatePath(Astro.url.pathname, otherLocale); const switchHref = alternatePath(Astro.url.pathname, otherLocale);
const dashHref = locale === "zh-CN" ? "/zh-CN/dashboard" : "/en/dashboard";
const loginHref = locale === "zh-CN" ? "/zh-CN/auth/login" : "/en/auth/login";
--- ---
<AppLayout <AppLayout
title={tr("home.title")} title={t(locale, "home.title")}
locale={locale} locale={locale}
nav={[]} nav={[]}
switchHref={switchHref} switchHref={switchHref}
switchLabel={t(otherLocale, `locale.${otherLocale}` as never)} switchLabel={t(otherLocale, `locale.${otherLocale}` as never)}
> >
<section class="space-y-10 py-10"> <HomeBody client:load locale={locale} />
<div class="u-reveal">
<p class="u-label u-label-edge">{tr("meta.tagline")}</p>
<h1 class="mt-3 text-[40px] font-black tracking-tight leading-[1.05]">
{tr("home.heroLine1")}<br />{tr("home.heroLine2")}
</h1>
<span class="u-edge-bar mt-5"></span>
<p class="mt-5 max-w-xl text-[14px] leading-relaxed" style="color: var(--ink-2);">
{tr("home.subtitle")}
</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href={dashHref} class="u-btn u-btn--edge">{tr("home.ctaDashboard")}</a>
<a href={loginHref} class="u-btn u-btn--ghost">{tr("home.ctaLogin")}</a>
</div>
</div>
<div class="grid gap-6 md:grid-cols-2">
<section class="u-card p-6 u-reveal">
<p class="u-label">{tr("home.stackTitle")}</p>
<p class="mt-3 text-[14px] leading-relaxed" style="color: var(--ink-2);">
{tr("home.stackBody")}
</p>
</section>
<section class="u-card p-6 u-reveal">
<p class="u-label">{tr("home.layerTitle")}</p>
<p class="mt-3 text-[14px] leading-relaxed" style="color: var(--ink-2);">
{tr("home.layerBody")}
</p>
</section>
</div>
</section>
</AppLayout> </AppLayout>

26
e2e/home.spec.ts Normal file
View File

@ -0,0 +1,26 @@
import { expect, test } from "@playwright/test";
test("中文首页渲染 hero 与 CTA", async ({ page }) => {
await page.goto("/zh-CN");
const h1 = page.getByRole("heading", { level: 1 });
await expect(h1).toContainText("本地片源");
await expect(h1).toContainText("在线弹幕");
await expect(page.getByRole("link", { name: "登录 / 注册" })).toBeVisible();
await expect(page.getByRole("link", { name: "打开媒体库" })).toBeVisible();
});
test("英文首页渲染英文文案", async ({ page }) => {
await page.goto("/en");
const h1 = page.getByRole("heading", { level: 1 });
await expect(h1).toBeVisible();
await expect(h1).not.toContainText("应用");
await expect(page.getByRole("link", { name: "登录 / 注册" })).toHaveCount(0);
});
test("首页无客户端控制台错误", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (err) => errors.push(err.message));
await page.goto("/zh-CN");
await page.waitForLoadState("networkidle");
expect(errors).toEqual([]);
});

View File

@ -17,7 +17,7 @@
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "biome check ." "lint": "../../node_modules/.bin/biome check ."
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.15.29", "@types/node": "^22.15.29",

View File

@ -2,13 +2,13 @@ import type { MessageKey } from "./zh-CN.js";
/** 与 zh-CN 同构;类型强制 key 对齐。 */ /** 与 zh-CN 同构;类型强制 key 对齐。 */
export const en: Record<MessageKey, string> = { export const en: Record<MessageKey, string> = {
"meta.brand": "App Template", "meta.brand": "DanDan Web",
"meta.product": "DanDan Web", "meta.product": "DanDan Web",
"meta.productTagline": "Local files · online danmaku", "meta.productTagline": "Local files · online danmaku",
"meta.tagline": "Astro · tRPC · Drizzle scaffold", "meta.tagline": "Local video · online danmaku",
"nav.home": "Home", "nav.home": "Home",
"nav.dashboard": "Dashboard", "nav.dashboard": "Console",
"nav.primary": "Primary nav", "nav.primary": "Primary nav",
"nav.brandHome": "Home", "nav.brandHome": "Home",
"nav.library": "Library", "nav.library": "Library",
@ -17,26 +17,26 @@ export const en: Record<MessageKey, string> = {
"nav.mounts": "Mounts", "nav.mounts": "Mounts",
"nav.danmaku": "Danmaku settings", "nav.danmaku": "Danmaku settings",
"home.title": "App Template", "home.title": "DanDan Web",
"home.subtitle": "home.subtitle":
"Astro + tRPC + Drizzle monorepo scaffold. Sign in to open the dashboard user-list example.", "Mount WebDAV / OpenList libraries, scan into your media library, play with ArtPlayer and online danmaku.",
"home.heroLine1": "App", "home.heroLine1": "Local files",
"home.heroLine2": "Template", "home.heroLine2": "Online danmaku",
"home.ctaDashboard": "Open dashboard", "home.ctaDashboard": "Open library",
"home.ctaLogin": "Sign in / Register", "home.ctaLogin": "Sign in / Register",
"home.stackTitle": "Stack", "home.stackTitle": "Features",
"home.stackBody": "home.stackBody":
"Astro 7 SSR · React 19 islands · tRPC v11 · Drizzle + Turso · Tailwind 4 · Biome · Yarn 4 + Turbo.", "WebDAV mounts · Bangumi scraping · ArtPlayer + danmaku · multi-user progress · media dashboard.",
"home.layerTitle": "Layering", "home.layerTitle": "Get started",
"home.layerBody": "home.layerBody":
"types → models → db → dao → trpc → web. Copy the user vertical slice for new entities; never skip layers.", "Register → add a WebDAV mount → scan in Browse → play from the Library with danmaku.",
"home.footer": "APP TEMPLATE · SSR", "home.footer": "DANDAN WEB · SSR",
"dashboard.title": "Dashboard", "dashboard.title": "Console",
"dashboard.heading": "Users", "dashboard.heading": "Media hub",
"dashboard.label": "Users", "dashboard.label": "Console",
"dashboard.desc": "dashboard.desc":
"Full vertical example: tRPC → service → dao → db (scaffold baseline — replace with your domain).", "Redirecting to the library… If nothing happens, open Library in the sidebar.",
"library.title": "Library", "library.title": "Library",
"library.scan": "Scan mounts", "library.scan": "Scan mounts",
@ -117,7 +117,7 @@ export const en: Record<MessageKey, string> = {
"auth.login.title": "Sign in", "auth.login.title": "Sign in",
"auth.login.heading": "Sign in", "auth.login.heading": "Sign in",
"auth.login.subtitle": "Continue with email and password", "auth.login.subtitle": "Continue with username and password",
"auth.login.submit": "Sign in", "auth.login.submit": "Sign in",
"auth.login.noAccount": "No account yet?", "auth.login.noAccount": "No account yet?",
"auth.login.toRegister": "Register", "auth.login.toRegister": "Register",
@ -144,8 +144,9 @@ export const en: Record<MessageKey, string> = {
"auth.settings.changeHeading": "Change password", "auth.settings.changeHeading": "Change password",
"auth.settings.changeSubmit": "Update password", "auth.settings.changeSubmit": "Update password",
"auth.settings.hint": "Other sessions will be signed out after a password change.", "auth.settings.hint": "Other sessions will be signed out after a password change.",
"auth.field.username": "Username",
"auth.field.name": "Name", "auth.field.name": "Name",
"auth.field.email": "Email", "auth.field.email": "Email (optional, for password reset)",
"auth.field.password": "Password", "auth.field.password": "Password",
"auth.field.currentPassword": "Current password", "auth.field.currentPassword": "Current password",
"auth.field.newPassword": "New password", "auth.field.newPassword": "New password",

View File

@ -1,16 +1,15 @@
/** /**
* zh-CN 文案:key 权威源。en.ts 必须与本文件 key 同构。 * zh-CN 文案:key 权威源。en.ts 必须与本文件 key 同构。
* 展示名与 description 的插值用 {name}。 * 展示名与 description 的插值用 {name}。
* 只保留脚手架基线文案;业务文案按同样模式追加。
*/ */
export const zhCN = { export const zhCN = {
"meta.brand": "应用模板", "meta.brand": "弹弹 Web",
"meta.product": "弹弹 Web", "meta.product": "弹弹 Web",
"meta.productTagline": "本地片源 · 在线弹幕", "meta.productTagline": "本地片源 · 在线弹幕",
"meta.tagline": "Astro · tRPC · Drizzle 脚手架", "meta.tagline": "本地视频 · 在线弹幕",
"nav.home": "首页", "nav.home": "首页",
"nav.dashboard": "仪表盘", "nav.dashboard": "控制台",
"nav.primary": "主导航", "nav.primary": "主导航",
"nav.brandHome": "首页", "nav.brandHome": "首页",
"nav.library": "媒体库", "nav.library": "媒体库",
@ -19,25 +18,23 @@ export const zhCN = {
"nav.mounts": "挂载管理", "nav.mounts": "挂载管理",
"nav.danmaku": "弹幕设置", "nav.danmaku": "弹幕设置",
"home.title": "应用模板", "home.title": "弹弹 Web",
"home.subtitle": "Astro + tRPC + Drizzle monorepo 脚手架。登录后可进入仪表盘查看用户列表示例。", "home.subtitle": "挂载 WebDAV / OpenList 片源,扫描入库,用 ArtPlayer 播放并加载在线弹幕。",
"home.heroLine1": "应用", "home.heroLine1": "本地片源",
"home.heroLine2": "模板", "home.heroLine2": "在线弹幕",
"home.ctaDashboard": "打开仪表盘", "home.ctaDashboard": "打开媒体库",
"home.ctaLogin": "登录 / 注册", "home.ctaLogin": "登录 / 注册",
"home.stackTitle": "技术栈", "home.stackTitle": "能力",
"home.stackBody": "home.stackBody":
"Astro 7 SSR · React 19 islands · tRPC v11 · Drizzle + Turso · Tailwind 4 · Biome · Yarn 4 + Turbo。", "WebDAV 挂载 · Bangumi 刮削 · ArtPlayer + 弹幕 · 多用户进度 · shadcn 风媒体库。",
"home.layerTitle": "分层约定", "home.layerTitle": "快速开始",
"home.layerBody": "home.layerBody": "注册登录 → 挂载管理添加 WebDAV → 文件浏览扫描 → 媒体库播放并看弹幕。",
"types → models → db → dao → trpc → web。业务实体按 user 垂直切片复制扩展,禁止跨层引用。", "home.footer": "DANDAN WEB · SSR",
"home.footer": "APP TEMPLATE · SSR",
"dashboard.title": "仪表盘", "dashboard.title": "控制台",
"dashboard.heading": "用户列表", "dashboard.heading": "媒体中心",
"dashboard.label": "Users", "dashboard.label": "Console",
"dashboard.desc": "dashboard.desc": "正在前往媒体库……若未跳转,请手动打开侧栏「媒体库」。",
"tRPC → service → dao → db 的完整垂直示例(脚手架基线,可替换为你的业务实体)。",
"library.title": "媒体库", "library.title": "媒体库",
"library.scan": "扫描挂载", "library.scan": "扫描挂载",
@ -117,7 +114,7 @@ export const zhCN = {
"auth.login.title": "登录", "auth.login.title": "登录",
"auth.login.heading": "登录", "auth.login.heading": "登录",
"auth.login.subtitle": "使用邮箱与密码继续", "auth.login.subtitle": "使用用户名与密码继续",
"auth.login.submit": "登录", "auth.login.submit": "登录",
"auth.login.noAccount": "还没有账号?", "auth.login.noAccount": "还没有账号?",
"auth.login.toRegister": "注册", "auth.login.toRegister": "注册",
@ -144,8 +141,9 @@ export const zhCN = {
"auth.settings.changeHeading": "修改密码", "auth.settings.changeHeading": "修改密码",
"auth.settings.changeSubmit": "更新密码", "auth.settings.changeSubmit": "更新密码",
"auth.settings.hint": "更新密码后其它设备的会话会失效。", "auth.settings.hint": "更新密码后其它设备的会话会失效。",
"auth.field.username": "用户名",
"auth.field.name": "名称", "auth.field.name": "名称",
"auth.field.email": "邮箱", "auth.field.email": "邮箱(可选,用于找回密码)",
"auth.field.password": "密码", "auth.field.password": "密码",
"auth.field.currentPassword": "当前密码", "auth.field.currentPassword": "当前密码",
"auth.field.newPassword": "新密码", "auth.field.newPassword": "新密码",