feat: add Phase 1 website skeleton and design system

This commit is contained in:
flym 2026-09-08 17:20:15 +08:00
parent c26164a318
commit 30cec549c1
34 changed files with 2873 additions and 19 deletions

22
.gitignore vendored
View File

@ -1,10 +1,12 @@
.env
.env.*
!.env.example
!.env.*.example
*.pem
*.key
node_modules/
.next/
__pycache__/
.venv/
node_modules/
.next/
out/
.env
.env.*
!.env.example
!.env.*.example
*.pem
*.key
*.tsbuildinfo
__pycache__/
.venv/

View File

@ -1,6 +1,6 @@
# 考町科技官网暨 AI 工具平台
当前阶段:V1.1 规划已入库,尚未开始业务代码开发
当前阶段:Phase 1 设计系统与官网骨架已完成,真实业务服务尚未接入
- [产品与技术规划(含分阶段工作清单)](docs/V1_PLAN.md)
- [Word 规划书](docs/考町科技官网暨AI工具平台_V1.1.docx)
@ -9,6 +9,7 @@
- [TTS 接口契约](docs/API_CONTRACT.md)
- [技术决策](docs/DECISIONS.md)
- [后续执行计划](docs/EXECUTION_PLAN.md)
- [Phase 1 阶段报告](docs/phase-1-report.md)
项目根目录:`E:\RD\Kaotings\www_site`,以本次新仓库为准。

23
app/about/page.tsx Normal file
View File

@ -0,0 +1,23 @@
import { SectionHeading, StatusCard } from "@/components/ui";
export const metadata = { title: "关于我们" };
export default function AboutPage() {
return (
<div className="container-shell py-20 sm:py-28">
<SectionHeading eyebrow="About us" title="做连接技术,也做真实可用的产品。" description="考町科技的官网首先服务于蓝牙周边、音频和智能硬件的品牌表达AI Lab 则承载对语音与设备智能的持续探索。" />
<div className="mt-16 grid gap-5 md:grid-cols-3">
<StatusCard title="产品导向" description="从真实使用场景出发,产品资料和能力表达以可核实内容为准。" />
<StatusCard title="可靠连接" description="关注 Bluetooth、Audio、Embedded 等技术在设备中的稳定落地。" tone="info" />
<StatusCard title="持续探索" description="在 AI Lab 中从 TTS 开始验证语音能力,不把实验概念包装成已发布功能。" tone="success" />
</div>
<section className="mt-24 grid gap-10 border-t border-line pt-12 md:grid-cols-[0.65fr_1.35fr]">
<p className="eyebrow eyebrow-cyan">Principles</p>
<div className="space-y-10">
<div><h2 className="text-2xl font-semibold tracking-[-0.03em]"></h2><p className="body-copy mt-3"></p></div>
<div><h2 className="text-2xl font-semibold tracking-[-0.03em]"></h2><p className="body-copy mt-3"> CMS </p></div>
</div>
</section>
</div>
);
}

19
app/account/page.tsx Normal file
View File

@ -0,0 +1,19 @@
import Link from "next/link";
import { ArrowRight } from "@/components/icons";
import { PreviewNotice, StatusCard } from "@/components/ui";
export const metadata = { title: "账户中心" };
export default function AccountPage() {
return (
<div className="container-shell py-16 sm:py-24">
<div className="flex flex-col justify-between gap-5 border-b border-line pb-10 sm:flex-row sm:items-end"><div><p className="eyebrow eyebrow-blue">Account</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]"></h1></div><span className="text-sm text-subtle"></span></div>
<div className="mt-8"><PreviewNotice></PreviewNotice></div>
<div className="mt-8 grid gap-5 md:grid-cols-2 lg:grid-cols-4"><StatusCard title="账户状态" description="待认证" tone="info" /><StatusCard title="会员计划" description="Free / 待启用" /><StatusCard title="本周期额度" description="待服务返回" /><StatusCard title="验证状态" description="邮箱、手机均待接入" tone="warning" /></div>
<div className="mt-8 grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<section className="rounded-2xl border border-line bg-panel/60 p-6"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">Profile</p><h2 className="mt-3 text-xl font-semibold"></h2><dl className="mt-6 space-y-5 text-sm"><div className="flex justify-between gap-4 border-b border-line pb-4"><dt className="text-muted"></dt><dd className="text-subtle"></dd></div><div className="flex justify-between gap-4 border-b border-line pb-4"><dt className="text-muted"></dt><dd className="text-subtle"></dd></div><div className="flex justify-between gap-4"><dt className="text-muted"></dt><dd className="text-subtle">user / </dd></div></dl><Link className="button-ghost mt-7 w-full" href="/login"> <ArrowRight /></Link></section>
<section className="rounded-2xl border border-line bg-panel/60 p-6"><div className="flex items-center justify-between gap-4"><div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">History</p><h2 className="mt-3 text-xl font-semibold">TTS </h2></div><Link className="text-sm text-cyan hover:text-copy" href="/tts"></Link></div><div className="mt-8 rounded-xl border border-dashed border-line p-8 text-center"><p className="font-semibold"></p><p className="mt-2 text-sm leading-6 text-muted"> TTS </p></div></section>
</div>
</div>
);
}

9
app/admin/page.tsx Normal file
View File

@ -0,0 +1,9 @@
import Link from "next/link";
import { ArrowRight } from "@/components/icons";
import { PreviewNotice, StatusCard } from "@/components/ui";
export const metadata = { title: "管理后台" };
export default function AdminPage() {
return <div className="container-shell py-16 sm:py-24"><div className="flex flex-col justify-between gap-5 border-b border-line pb-10 sm:flex-row sm:items-end"><div><p className="eyebrow">Operations</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]"></h1></div><span className="text-sm text-subtle">role=admin / </span></div><div className="mt-8"><PreviewNotice></PreviewNotice></div><div className="mt-8 grid gap-5 md:grid-cols-3"><StatusCard title="用户" description="分页、禁用和恢复将在 Phase 4 接入。" /><StatusCard title="TTS 任务" description="任务、失败和使用量将在 Phase 4 接入。" tone="info" /><StatusCard title="审计" description="操作原因和前后值将在业务 API 接入后记录。" tone="warning" /></div><div className="mt-8 grid gap-4 sm:grid-cols-2"><Link className="group rounded-2xl border border-line bg-panel/50 p-6 hover:border-blue/60" href="/admin/users"><p className="text-xs uppercase tracking-[0.18em] text-subtle">01 / Users</p><h2 className="mt-3 text-xl font-semibold"></h2><p className="mt-2 text-sm leading-6 text-muted">Plan </p><span className="mt-5 inline-flex text-sm font-semibold text-cyan"> <ArrowRight /></span></Link><Link className="group rounded-2xl border border-line bg-panel/50 p-6 hover:border-cyan/60" href="/admin/tts"><p className="text-xs uppercase tracking-[0.18em] text-subtle">02 / TTS</p><h2 className="mt-3 text-xl font-semibold">TTS 使</h2><p className="mt-2 text-sm leading-6 text-muted"></p><span className="mt-5 inline-flex text-sm font-semibold text-cyan"> <ArrowRight /></span></Link></div></div>;
}

7
app/admin/tts/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import { PreviewNotice, StatusCard } from "@/components/ui";
export const metadata = { title: "TTS 任务管理" };
export default function AdminTtsPage() {
return <div className="container-shell py-16 sm:py-24"><p className="eyebrow eyebrow-cyan">Admin / TTS</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]">TTS 使</h1><p className="mt-4 max-w-xl text-muted"></p><div className="mt-8"><PreviewNotice></PreviewNotice></div><div className="mt-8 grid gap-5 md:grid-cols-3"><StatusCard title="任务数" description="待任务服务接入" /><StatusCard title="成功率" description="待统计服务接入" tone="success" /><StatusCard title="冻结异常" description="待额度服务接入" tone="warning" /></div><div className="mt-8 overflow-hidden rounded-2xl border border-line bg-panel/50"><div className="grid gap-4 border-b border-line p-5 sm:grid-cols-3"><select className="field-input" disabled aria-label="任务状态"><option></option></select><input className="field-input" disabled aria-label="按任务 ID 筛选" placeholder="任务 ID待接入" /><button className="button-ghost" disabled type="button"></button></div><div className="p-10 text-center text-sm text-muted"></div></div></div>;
}

7
app/admin/users/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import { PreviewNotice, StatusCard } from "@/components/ui";
export const metadata = { title: "用户管理" };
export default function AdminUsersPage() {
return <div className="container-shell py-16 sm:py-24"><p className="eyebrow">Admin / Users</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]"></h1><p className="mt-4 max-w-xl text-muted">Phase 4 </p><div className="mt-8"><PreviewNotice>VIP </PreviewNotice></div><div className="mt-8 grid gap-5 md:grid-cols-3"><StatusCard title="用户总数" description="待 API 接入" /><StatusCard title="VIP 用户" description="待会员服务接入" tone="info" /><StatusCard title="待处理" description="无可核实数据" tone="warning" /></div><div className="mt-8 overflow-hidden rounded-2xl border border-line bg-panel/50"><div className="flex flex-col gap-4 border-b border-line p-5 sm:flex-row"><input aria-label="按邮箱筛选" className="field-input sm:max-w-sm" disabled placeholder="按邮箱筛选(待接入)" /><button className="button-ghost sm:ml-auto" disabled type="button"></button></div><div className="p-10 text-center text-sm text-muted"> API </div></div></div>;
}

24
app/contact/page.tsx Normal file
View File

@ -0,0 +1,24 @@
import Link from "next/link";
import { ArrowRight } from "@/components/icons";
import { PreviewNotice, SectionHeading, StatusCard } from "@/components/ui";
export const metadata = { title: "联系我们" };
export default function ContactPage() {
return (
<div className="container-shell py-20 sm:py-28">
<SectionHeading eyebrow="Contact" title="让我们从真实需求开始。" description="正式联系方式尚未确认。本页先完成联系信息布局和内容边界,不展示虚构的电话、邮箱或地址。" />
<div className="mt-12"><PreviewNotice></PreviewNotice></div>
<div className="mt-8 grid gap-5 md:grid-cols-3">
<StatusCard title="业务合作" description="联系方式待确认" />
<StatusCard title="产品咨询" description="产品资料待确认" tone="info" />
<StatusCard title="技术交流" description="技术联系渠道待确认" tone="success" />
</div>
<div className="mt-12 rounded-2xl border border-line bg-panel/50 p-6 sm:p-8">
<p className="text-sm font-semibold text-copy"></p>
<p className="mt-2 text-sm leading-7 text-muted"> AI Lab TTS </p>
<div className="mt-6 flex flex-wrap gap-3"><Link className="button-secondary" href="/products"> <ArrowRight /></Link><Link className="button-ghost" href="/tts"> AI Lab</Link></div>
</div>
</div>
);
}

268
app/globals.css Normal file
View File

@ -0,0 +1,268 @@
@import "tailwindcss";
@theme {
--color-ink: #0d0f12;
--color-panel: #14181d;
--color-panel-raised: #1a1d21;
--color-line: #2a2f36;
--color-copy: #f5f7fa;
--color-muted: #9ca3af;
--color-subtle: #4b5563;
--color-orange: #ff6a00;
--color-blue: #1677ff;
--color-cyan: #00d1e6;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--font-sans: var(--font-inter), "PingFang SC", "Microsoft YaHei", sans-serif;
}
:root {
color-scheme: dark;
background: #0d0f12;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-width: 320px;
margin: 0;
background: #0d0f12;
color: #f5f7fa;
font-family: var(--font-inter), "PingFang SC", "Microsoft YaHei", sans-serif;
}
a,
button,
input,
textarea,
select {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 2px solid #00d1e6;
outline-offset: 4px;
}
::selection {
background: #1677ff;
color: #f5f7fa;
}
.skip-link {
position: fixed;
z-index: 100;
top: 1rem;
left: 1rem;
transform: translateY(-200%);
border-radius: 999px;
background: #f5f7fa;
color: #0d0f12;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 700;
transition: transform 180ms ease;
}
.skip-link:focus {
transform: translateY(0);
}
.container-shell {
width: min(1180px, calc(100% - 2.5rem));
margin-inline: auto;
}
.section-rule {
border-top: 1px solid rgba(75, 85, 99, 0.42);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: #9ca3af;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.eyebrow::before {
width: 2.25rem;
height: 2px;
background: #ff6a00;
content: "";
}
.eyebrow-cyan::before {
background: #00d1e6;
}
.eyebrow-blue::before {
background: #1677ff;
}
.display-heading {
max-width: 10ch;
margin-top: 1.25rem;
font-size: clamp(2.8rem, 7vw, 6.5rem);
font-weight: 650;
letter-spacing: -0.07em;
line-height: 0.98;
}
.body-copy {
color: #9ca3af;
line-height: 1.8;
}
.surface-grid {
background-image: linear-gradient(rgba(42, 47, 54, 0.22) 1px, transparent 1px),
linear-gradient(90deg, rgba(42, 47, 54, 0.22) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: linear-gradient(to bottom, black 0%, transparent 80%);
}
.hero-glow {
background: radial-gradient(circle at 74% 38%, rgba(22, 119, 255, 0.14), transparent 26rem),
radial-gradient(circle at 68% 65%, rgba(0, 209, 230, 0.08), transparent 22rem);
}
.button-primary,
.button-secondary,
.button-ghost {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
gap: 0.7rem;
border-radius: 0.65rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 700;
transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease;
}
.button-primary {
background: #ff6a00;
color: #0d0f12;
}
.button-primary:hover {
background: #ff812b;
transform: translateY(-2px);
}
.button-secondary {
border: 1px solid #1677ff;
background: rgba(22, 119, 255, 0.08);
color: #dbeafe;
}
.button-secondary:hover {
background: rgba(22, 119, 255, 0.18);
transform: translateY(-2px);
}
.button-ghost {
border: 1px solid #2a2f36;
color: #d1d5db;
}
.button-ghost:hover {
border-color: #00d1e6;
color: #f5f7fa;
}
.button-disabled,
button:disabled {
cursor: not-allowed;
opacity: 0.52;
}
.product-visual {
position: relative;
min-height: 15rem;
overflow: hidden;
border: 1px solid rgba(75, 85, 99, 0.5);
border-radius: 1rem;
background: linear-gradient(135deg, #1a1d21, #101317 68%);
}
.product-visual::after {
position: absolute;
right: -15%;
bottom: -52%;
width: 80%;
height: 95%;
border: 1px solid rgba(0, 209, 230, 0.36);
border-radius: 50%;
box-shadow: 0 0 80px rgba(0, 209, 230, 0.13);
content: "";
transform: rotate(-18deg);
}
.field-label {
display: block;
margin-bottom: 0.5rem;
color: #d1d5db;
font-size: 0.8125rem;
font-weight: 600;
}
.field-input {
width: 100%;
border: 1px solid #2a2f36;
border-radius: 0.65rem;
background: #14181d;
color: #f5f7fa;
padding: 0.8rem 0.9rem;
}
.field-input:disabled {
cursor: not-allowed;
color: #6b7280;
}
@media (max-width: 640px) {
.container-shell {
width: min(100% - 2rem, 1180px);
}
.display-heading {
max-width: 8ch;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}

40
app/layout.tsx Normal file
View File

@ -0,0 +1,40 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "swap",
});
export const metadata: Metadata = {
title: {
default: "考町科技 | 连接技术与日常",
template: "%s | 考町科技",
},
description: "考町科技专注于蓝牙周边、音频与智能设备,也在 AI Lab 探索可落地的语音能力。",
};
export const viewport: Viewport = {
themeColor: "#0D0F12",
width: "device-width",
initialScale: 1,
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN">
<body className={`${inter.variable} antialiased`}>
<a className="skip-link" href="#main-content">
</a>
<SiteHeader />
<main id="main-content">{children}</main>
<SiteFooter />
</body>
</html>
);
}

8
app/login/page.tsx Normal file
View File

@ -0,0 +1,8 @@
import Link from "next/link";
import { AuthForm } from "@/components/auth-form";
export const metadata = { title: "登录" };
export default function LoginPage() {
return <div className="container-shell flex min-h-[680px] flex-col items-center justify-center py-16"><div className="mb-8 text-center"><p className="eyebrow">Account</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]"></h1><p className="mt-3 text-sm text-muted"> AI Lab 使</p></div><AuthForm mode="login" /><Link className="mt-6 text-sm text-muted hover:text-copy" href="/"></Link></div>;
}

5
app/not-found.tsx Normal file
View File

@ -0,0 +1,5 @@
import Link from "next/link";
export default function NotFound() {
return <div className="container-shell flex min-h-[560px] flex-col items-center justify-center py-20 text-center"><p className="eyebrow">404 / Not found</p><h1 className="mt-6 text-5xl font-semibold tracking-[-0.06em]"></h1><p className="mt-5 max-w-md text-sm leading-7 text-muted"></p><Link className="button-primary mt-8" href="/"></Link></div>;
}

102
app/page.tsx Normal file
View File

@ -0,0 +1,102 @@
import Link from "next/link";
import { ArrowRight, BluetoothIcon, ChipIcon, SparkIcon, WaveIcon } from "@/components/icons";
import { ArrowLink, DevPlaceholder, ProductVisual, SectionHeading } from "@/components/ui";
import { products } from "@/lib/content";
const capabilities = [
{ icon: BluetoothIcon, title: "Bluetooth", copy: "稳定连接" },
{ icon: WaveIcon, title: "Audio", copy: "更好的声音体验" },
{ icon: ChipIcon, title: "Embedded", copy: "可靠的硬件与固件" },
{ icon: SparkIcon, title: "AI", copy: "让设备更智能" },
];
export default function HomePage() {
return (
<>
<section className="relative isolate overflow-hidden hero-glow">
<div className="surface-grid pointer-events-none absolute inset-0 -z-10 opacity-60" />
<div className="container-shell grid min-h-[680px] items-center gap-12 py-20 lg:grid-cols-[0.8fr_1.2fr] lg:py-28">
<div>
<p className="eyebrow">Connecting technology with everyday life</p>
<h1 className="display-heading"></h1>
<p className="body-copy mt-8 max-w-md text-lg"></p>
<div className="mt-9 flex flex-wrap items-center gap-4">
<Link className="button-primary" href="/products"> <ArrowRight /></Link>
<Link className="text-sm font-semibold text-muted hover:text-copy" href="#technology"> <span className="ml-2"></span></Link>
</div>
</div>
<div className="relative lg:pl-8">
<div className="absolute -right-16 top-1/2 h-64 w-64 -translate-y-1/2 rounded-full bg-blue/10 blur-3xl" />
<ProductVisual label="AUDIO / HARDWARE CONCEPT" />
<div className="absolute -bottom-5 left-5 rounded-xl border border-line bg-panel/90 px-4 py-3 backdrop-blur sm:left-12">
<p className="text-[0.65rem] uppercase tracking-[0.2em] text-subtle">Product first</p>
<p className="mt-1 text-sm text-copy"></p>
</div>
</div>
</div>
</section>
<section className="section-rule py-24 sm:py-32">
<div className="container-shell">
<div className="flex flex-col justify-between gap-8 sm:flex-row sm:items-end">
<SectionHeading eyebrow="Products" title="为真实场景,打造可靠的蓝牙周边。" description="产品中心将由结构化内容驱动。当前资料尚未确认,以下内容仅用于开发阶段验证模板。" />
<ArrowLink href="/products"></ArrowLink>
</div>
<div className="mt-14 grid gap-5 lg:grid-cols-2">
{products.map((product) => (
<Link className="group grid gap-5 rounded-2xl border border-line bg-panel/60 p-4 transition-colors hover:border-blue/60 sm:grid-cols-[0.9fr_1.1fr] sm:p-5" href={`/products/${product.slug}`} key={product.slug}>
<ProductVisual accent={product.accent} label={product.category.toUpperCase()} />
<div className="flex flex-col justify-center p-2 sm:p-4">
<DevPlaceholder>{product.shortLabel}</DevPlaceholder>
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.18em] text-subtle">{product.category}</p>
<h3 className="mt-3 text-2xl font-semibold tracking-[-0.03em] text-copy">{product.name}</h3>
<p className="mt-3 text-sm leading-7 text-muted">{product.summary}</p>
<span className="mt-6 inline-flex items-center gap-2 text-sm font-semibold text-cyan"> <ArrowRight /></span>
</div>
</Link>
))}
</div>
</div>
</section>
<section className="section-rule py-24 sm:py-32" id="technology">
<div className="container-shell">
<SectionHeading eyebrow="Technology" title="从无线连接到嵌入式开发,关注产品背后的核心技术。" description="以硬件经验为基础,持续探索可靠连接、音频体验、嵌入式系统与 AI 在设备中的应用。" tone="cyan" />
<div className="mt-14 grid border-y border-line sm:grid-cols-2 lg:grid-cols-4">
{capabilities.map(({ icon: Icon, title, copy }, index) => (
<div className="flex items-start gap-4 border-line py-7 sm:px-6 lg:border-r lg:first:pl-0 lg:last:border-r-0" key={title}>
<span className={index === 0 ? "text-blue" : index === 3 ? "text-cyan" : "text-copy"}><Icon /></span>
<div><h3 className="font-semibold text-copy">{title}</h3><p className="mt-2 text-sm text-muted">{copy}</p></div>
</div>
))}
</div>
</div>
</section>
<section className="section-rule overflow-hidden py-24 sm:py-32" id="ai-lab">
<div className="container-shell grid items-center gap-12 lg:grid-cols-[0.8fr_1.2fr]">
<div>
<p className="eyebrow eyebrow-cyan">AI Lab</p>
<h2 className="mt-5 max-w-md text-4xl font-semibold tracking-[-0.05em] text-copy sm:text-6xl"></h2>
<p className="body-copy mt-6 max-w-md"></p>
<Link className="button-secondary mt-8" href="/tts"> TTS <ArrowRight /></Link>
</div>
<div className="relative flex min-h-64 items-center justify-center overflow-hidden rounded-2xl border border-line bg-panel/50 p-8">
<div className="absolute inset-x-12 h-px bg-cyan/30" />
<div className="flex items-center gap-1 opacity-90" aria-hidden="true">
{[22, 42, 70, 105, 66, 132, 90, 48, 112, 78, 36, 58, 96, 50, 26].map((height, index) => <span className={`w-1 rounded-full ${index % 3 === 0 ? "bg-orange" : "bg-cyan"}`} style={{ height }} key={index} />)}
</div>
<span className="absolute bottom-5 right-5 text-[0.65rem] font-semibold uppercase tracking-[0.22em] text-subtle">Voice / exploration</span>
</div>
</div>
</section>
<section className="section-rule py-20 sm:py-24">
<div className="container-shell flex flex-col justify-between gap-8 rounded-2xl border border-line bg-panel/50 p-7 sm:flex-row sm:items-center sm:p-10">
<div><p className="eyebrow eyebrow-blue">About Kaotings</p><h2 className="mt-4 text-3xl font-semibold tracking-[-0.04em]"></h2></div>
<ArrowLink href="/about"></ArrowLink>
</div>
</section>
</>
);
}

View File

@ -0,0 +1,45 @@
import { notFound } from "next/navigation";
import { ArrowRight } from "@/components/icons";
import { DevPlaceholder, FeatureList, PreviewNotice, ProductVisual } from "@/components/ui";
import { products } from "@/lib/content";
export function generateStaticParams() {
return products.map((product) => ({ slug: product.slug }));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const product = products.find((item) => item.slug === slug);
return { title: product?.name ?? "产品详情" };
}
export default async function ProductDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const product = products.find((item) => item.slug === slug);
if (!product) notFound();
return (
<div className="container-shell py-16 sm:py-24">
<PreviewNotice></PreviewNotice>
<div className="mt-10 grid gap-12 lg:grid-cols-[1fr_0.85fr] lg:items-center">
<div>
<DevPlaceholder>{product.shortLabel}</DevPlaceholder>
<p className="mt-6 text-xs font-semibold uppercase tracking-[0.2em] text-subtle">{product.category}</p>
<h1 className="mt-4 max-w-xl text-5xl font-semibold tracking-[-0.06em] sm:text-7xl">{product.name}</h1>
<p className="body-copy mt-7 max-w-lg text-lg">{product.summary}</p>
<div className="mt-8 flex flex-wrap gap-3"><a className="button-primary" href="#details"> <ArrowRight /></a><a className="button-ghost" href="/contact"></a></div>
</div>
<ProductVisual accent={product.accent} label={product.category.toUpperCase()} />
</div>
<section className="mt-24 grid gap-10 border-t border-line pt-12 lg:grid-cols-[0.8fr_1.2fr]" id="details">
<div><p className="eyebrow eyebrow-blue">Product template</p><h2 className="mt-5 text-3xl font-semibold tracking-[-0.04em]"></h2></div>
<div className="grid gap-8 sm:grid-cols-2">
<div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle"></p><p className="mt-3 text-sm leading-7 text-muted"></p></div>
<div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle"></p><FeatureList items={["真实卖点待确认", "规格字段预留", "应用场景待补充"]} /></div>
<div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle"></p><p className="mt-3 text-sm leading-7 text-muted"></p></div>
<div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle"> FAQ</p><p className="mt-3 text-sm leading-7 text-muted"> CMS </p></div>
</div>
</section>
</div>
);
}

32
app/products/page.tsx Normal file
View File

@ -0,0 +1,32 @@
import { SectionHeading, DevPlaceholder, ProductVisual, ArrowLink } from "@/components/ui";
import { products } from "@/lib/content";
export const metadata = { title: "产品中心" };
export default function ProductsPage() {
return (
<div className="container-shell py-20 sm:py-28">
<SectionHeading eyebrow="Products" title="产品中心" description="围绕蓝牙周边、音频与智能硬件的产品结构。真实产品资料入库前,页面使用明确标注的开发占位数据。" />
<div className="mt-12 flex flex-wrap gap-2 border-y border-line py-4">
<span className="rounded-full bg-copy px-4 py-2 text-sm font-semibold text-ink"></span>
<span className="rounded-full border border-line px-4 py-2 text-sm text-muted">线</span>
<span className="rounded-full border border-line px-4 py-2 text-sm text-muted"></span>
<span className="ml-auto self-center text-xs text-subtle"> CMS </span>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-2">
{products.map((product) => (
<article className="overflow-hidden rounded-2xl border border-line bg-panel/50" key={product.slug}>
<ProductVisual accent={product.accent} label={product.category.toUpperCase()} />
<div className="p-6 sm:p-8">
<DevPlaceholder>{product.shortLabel}</DevPlaceholder>
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.18em] text-subtle">{product.category}</p>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.03em]">{product.name}</h2>
<p className="mt-3 max-w-lg text-sm leading-7 text-muted">{product.summary}</p>
<div className="mt-6"><ArrowLink href={`/products/${product.slug}`}></ArrowLink></div>
</div>
</article>
))}
</div>
</div>
);
}

8
app/register/page.tsx Normal file
View File

@ -0,0 +1,8 @@
import Link from "next/link";
import { AuthForm } from "@/components/auth-form";
export const metadata = { title: "注册" };
export default function RegisterPage() {
return <div className="container-shell flex min-h-[680px] flex-col items-center justify-center py-16"><div className="mb-8 text-center"><p className="eyebrow eyebrow-cyan">Create account</p><h1 className="mt-5 text-4xl font-semibold tracking-[-0.05em]"></h1><p className="mt-3 text-sm text-muted"> Phase 2 </p></div><AuthForm mode="register" /><Link className="mt-6 text-sm text-muted hover:text-copy" href="/"></Link></div>;
}

32
app/tts/page.tsx Normal file
View File

@ -0,0 +1,32 @@
import Link from "next/link";
import { ArrowRight, WaveIcon } from "@/components/icons";
import { DevPlaceholder, PreviewNotice, SectionHeading, StatusCard } from "@/components/ui";
export const metadata = { title: "TTS 工作台" };
export default function TtsPage() {
return (
<div className="container-shell py-16 sm:py-24">
<div className="flex flex-col justify-between gap-8 lg:flex-row lg:items-end">
<SectionHeading eyebrow="AI Lab / Text to Speech" title="把文字变成声音。" description="独立的 TTS 工作台布局预览。生成、历史、回放和下载将在认证、上游适配与额度服务完成后启用。" tone="cyan" />
<DevPlaceholder>Phase 1 </DevPlaceholder>
</div>
<div className="mt-10"><PreviewNotice> TTS </PreviewNotice></div>
<div className="mt-8 grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
<section className="rounded-2xl border border-line bg-panel/60 p-5 sm:p-7" aria-labelledby="tts-input-title">
<div className="flex items-center justify-between gap-4"><div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">01 / Input</p><h2 className="mt-2 text-xl font-semibold" id="tts-input-title"></h2></div><span className="text-xs text-subtle">0 / </span></div>
<textarea className="field-input mt-6 min-h-64 resize-y leading-7" disabled placeholder="登录后输入需要转换的文本。" />
<div className="mt-5 flex flex-col gap-4 border-t border-line pt-5 sm:flex-row sm:items-center sm:justify-between"><p className="text-sm text-muted"></p><Link className="button-primary" href="/login"> <ArrowRight /></Link></div>
</section>
<aside className="space-y-6">
<section className="rounded-2xl border border-line bg-panel/60 p-5 sm:p-7" aria-labelledby="tts-settings-title"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">02 / Parameters</p><h2 className="mt-2 text-xl font-semibold" id="tts-settings-title"></h2><div className="mt-6 space-y-5"><div><label className="field-label" htmlFor="voice"></label><select className="field-input" disabled id="voice"><option></option></select></div><div><label className="field-label" htmlFor="format"></label><select className="field-input" disabled id="format"><option></option></select></div></div></section>
<StatusCard title="额度" description="登录后显示当前周期额度、已用与冻结。" tone="info" />
</aside>
</div>
<section className="mt-8 rounded-2xl border border-line bg-panel/40 p-5 sm:p-7" aria-labelledby="tts-history-title">
<div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-center"><div><p className="text-xs font-semibold uppercase tracking-[0.18em] text-subtle">03 / History</p><h2 className="mt-2 text-xl font-semibold" id="tts-history-title"></h2></div><span className="text-sm text-muted"></span></div>
<div className="mt-6 flex flex-col items-center justify-center rounded-xl border border-dashed border-line py-14 text-center"><span className="text-cyan"><WaveIcon className="h-9 w-9" /></span><p className="mt-4 font-semibold"></p><p className="mt-2 max-w-sm text-sm leading-6 text-muted"> TTS </p></div>
</section>
</div>
);
}

18
components/auth-form.tsx Normal file
View File

@ -0,0 +1,18 @@
import Link from "next/link";
import { PreviewNotice } from "@/components/ui";
export function AuthForm({ mode }: { mode: "login" | "register" }) {
const isLogin = mode === "login";
return (
<div className="w-full max-w-md rounded-2xl border border-line bg-panel/70 p-6 sm:p-8">
<PreviewNotice></PreviewNotice>
<form className="mt-8 space-y-5">
<div><label className="field-label" htmlFor="email"></label><input className="field-input" disabled id="email" placeholder="name@example.com" type="email" /></div>
<div><label className="field-label" htmlFor="password"></label><input className="field-input" disabled id="password" placeholder="至少 8 位字符" type="password" /></div>
{!isLogin ? <div><label className="field-label" htmlFor="password-confirm"></label><input className="field-input" disabled id="password-confirm" placeholder="再次输入密码" type="password" /></div> : null}
<button className="button-primary w-full" disabled type="submit">{isLogin ? "登录(待接入)" : "注册(待接入)"}</button>
</form>
<p className="mt-6 text-center text-sm text-muted">{isLogin ? "还没有账户?" : "已有账户?"}{" "}<Link className="text-cyan hover:text-copy" href={isLogin ? "/register" : "/login"}>{isLogin ? "去注册" : "去登录"}</Link></p>
</div>
);
}

14
components/brand-mark.tsx Normal file
View File

@ -0,0 +1,14 @@
import Image from "next/image";
export function BrandMark({ compact = false }: { compact?: boolean }) {
return (
<Image
src="/brand/logo_bt.png"
alt="考町科技 KAOTINGS"
width={compact ? 142 : 176}
height={compact ? 51 : 63}
className={compact ? "h-[34px] w-auto" : "h-10 w-auto"}
priority
/>
);
}

37
components/icons.tsx Normal file
View File

@ -0,0 +1,37 @@
type IconProps = { className?: string };
export function ArrowUpRight({ className = "h-4 w-4" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 20 20"><path d="M5 15 15 5M7 5h8v8" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" /></svg>;
}
export function ArrowRight({ className = "h-4 w-4" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 20 20"><path d="M3 10h13m-5-5 5 5-5 5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" /></svg>;
}
export function MenuIcon({ className = "h-5 w-5" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 20 20"><path d="M3 5h14M3 10h14M3 15h14" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /></svg>;
}
export function CloseIcon({ className = "h-5 w-5" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 20 20"><path d="m5 5 10 10M15 5 5 15" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /></svg>;
}
export function BluetoothIcon({ className = "h-8 w-8" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 24 24"><path d="m12 3 5 5-5 4 5 4-5 5V3ZM7 8l10 8M7 16l10-8" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" /></svg>;
}
export function WaveIcon({ className = "h-8 w-8" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 24 24"><path d="M3 12h2m2-4v8m4-11v14m4-10v6m4-9v12m2-7v2" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /></svg>;
}
export function ChipIcon({ className = "h-8 w-8" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 24 24"><rect height="10" rx="1.5" width="10" x="7" y="7" stroke="currentColor" strokeWidth="1.5" /><path d="M9 3v4m6-4v4m-6 10v4m6-4v4M3 9h4m-4 6h4m10-6h4m-4 6h4" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" /></svg>;
}
export function SparkIcon({ className = "h-8 w-8" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 24 24"><path d="m12 3 1.8 6.2L20 11l-6.2 1.8L12 19l-1.8-6.2L4 11l6.2-1.8L12 3Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.5" /></svg>;
}
export function CheckIcon({ className = "h-4 w-4" }: IconProps) {
return <svg aria-hidden="true" className={className} fill="none" viewBox="0 0 20 20"><path d="m4 10 4 4 8-8" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" /></svg>;
}

View File

@ -0,0 +1,32 @@
import Link from "next/link";
import { BrandMark } from "@/components/brand-mark";
export function SiteFooter() {
return (
<footer className="border-t border-line bg-ink">
<div className="container-shell grid gap-10 py-12 md:grid-cols-[1.3fr_1fr_1fr] md:py-16">
<div>
<BrandMark />
<p className="mt-5 max-w-xs text-sm leading-7 text-muted"></p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-subtle"></p>
<div className="mt-4 flex flex-col gap-3 text-sm text-muted">
<Link className="hover:text-copy" href="/products"></Link>
<Link className="hover:text-copy" href="/#technology"></Link>
<Link className="hover:text-copy" href="/tts">AI Lab / TTS</Link>
</div>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-subtle"></p>
<p className="mt-4 text-sm leading-7 text-muted"></p>
<Link className="mt-4 inline-flex text-sm text-cyan hover:text-copy" href="/contact"> <span className="ml-2"></span></Link>
</div>
</div>
<div className="container-shell flex flex-col gap-3 border-t border-line py-5 text-xs text-subtle sm:flex-row sm:items-center sm:justify-between">
<span>© 2026 Shenzhen Kaotings Technology Co., Ltd.</span>
<span>Phase 1 development preview</span>
</div>
</footer>
);
}

View File

@ -0,0 +1,53 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { CloseIcon, MenuIcon } from "@/components/icons";
import { navigation } from "@/lib/content";
import { BrandMark } from "@/components/brand-mark";
export function SiteHeader() {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-white/5 bg-ink/90 backdrop-blur-xl">
<div className="container-shell flex h-[70px] items-center justify-between gap-6">
<Link aria-label="返回考町科技首页" href="/" onClick={() => setOpen(false)}>
<BrandMark compact />
</Link>
<nav aria-label="主导航" className="hidden items-center gap-8 md:flex">
{navigation.map((item) => (
<Link className="text-sm text-muted transition-colors hover:text-copy" href={item.href} key={item.href}>
{item.label}
</Link>
))}
</nav>
<div className="hidden items-center gap-3 md:flex">
<Link className="text-sm text-muted transition-colors hover:text-copy" href="/login">
</Link>
<Link className="button-primary min-h-10 px-4 py-2" href="/contact">
</Link>
</div>
<button aria-expanded={open} aria-label={open ? "关闭导航" : "打开导航"} className="button-ghost min-h-10 p-2 md:hidden" onClick={() => setOpen((current) => !current)} type="button">
{open ? <CloseIcon /> : <MenuIcon />}
</button>
</div>
{open ? (
<div className="border-t border-line bg-panel md:hidden">
<nav aria-label="移动端主导航" className="container-shell flex flex-col gap-1 py-4">
{navigation.map((item) => (
<Link className="rounded-lg px-3 py-3 text-sm text-muted hover:bg-panel-raised hover:text-copy" href={item.href} key={item.href} onClick={() => setOpen(false)}>
{item.label}
</Link>
))}
<Link className="mt-2 rounded-lg border-t border-line px-3 py-4 text-sm text-copy" href="/login" onClick={() => setOpen(false)}>
/
</Link>
</nav>
</div>
) : null}
</header>
);
}

44
components/ui.tsx Normal file
View File

@ -0,0 +1,44 @@
import Link from "next/link";
import { ArrowRight, ArrowUpRight, CheckIcon } from "@/components/icons";
export function DevPlaceholder({ children = "开发占位" }: { children?: React.ReactNode }) {
return <span className="inline-flex items-center rounded-full border border-warning/30 bg-warning/10 px-2.5 py-1 text-[0.68rem] font-semibold tracking-wide text-warning">{children}</span>;
}
export function SectionHeading({ eyebrow, title, description, tone = "orange" }: { eyebrow: string; title: string; description?: string; tone?: "orange" | "blue" | "cyan" }) {
return (
<div className="max-w-2xl">
<p className={`eyebrow ${tone === "blue" ? "eyebrow-blue" : tone === "cyan" ? "eyebrow-cyan" : ""}`}>{eyebrow}</p>
<h2 className="mt-5 text-3xl font-semibold tracking-[-0.04em] text-copy sm:text-5xl">{title}</h2>
{description ? <p className="body-copy mt-5 max-w-xl text-base">{description}</p> : null}
</div>
);
}
export function ArrowLink({ href, children, external = false }: { href: string; children: React.ReactNode; external?: boolean }) {
return <Link className="inline-flex items-center gap-2 text-sm font-semibold text-cyan transition-colors hover:text-copy" href={href}>{children}{external ? <ArrowUpRight /> : <ArrowRight />}</Link>;
}
export function ProductVisual({ accent = "orange", label = "PRODUCT CONCEPT" }: { accent?: "orange" | "blue"; label?: string }) {
return (
<div aria-label="开发占位产品视觉,不代表真实产品" className="product-visual">
<div className={`absolute left-8 top-8 h-24 w-44 rounded-[2rem] border ${accent === "orange" ? "border-orange/70 shadow-[0_0_60px_rgba(255,106,0,0.14)]" : "border-blue/70 shadow-[0_0_60px_rgba(22,119,255,0.18)]"} bg-ink/80 sm:h-32 sm:w-60`} />
<div className={`absolute left-20 top-20 h-16 w-24 rounded-full border ${accent === "orange" ? "border-orange/60" : "border-blue/60"} bg-panel/80 sm:left-32 sm:top-28 sm:h-20 sm:w-36`} />
<span className="absolute bottom-5 left-6 text-[0.62rem] font-semibold tracking-[0.24em] text-subtle">{label}</span>
<DevPlaceholder />
</div>
);
}
export function PreviewNotice({ children = "当前为 Phase 1 界面预览,真实业务将在后续阶段接入。" }: { children?: React.ReactNode }) {
return <div className="rounded-xl border border-warning/25 bg-warning/8 px-4 py-3 text-sm leading-6 text-warning" role="status"><span className="font-semibold"></span>{children}</div>;
}
export function StatusCard({ title, description, tone = "neutral" }: { title: string; description: string; tone?: "neutral" | "info" | "warning" | "success" }) {
const styles = { neutral: "border-line bg-panel", info: "border-blue/25 bg-blue/8", warning: "border-warning/25 bg-warning/8", success: "border-success/25 bg-success/8" };
return <div className={`rounded-xl border p-4 ${styles[tone]}`}><p className="font-semibold text-copy">{title}</p><p className="mt-2 text-sm leading-6 text-muted">{description}</p></div>;
}
export function FeatureList({ items }: { items: string[] }) {
return <ul className="space-y-3">{items.map((item) => <li className="flex gap-3 text-sm leading-6 text-muted" key={item}><span className="mt-1 text-cyan"><CheckIcon /></span><span>{item}</span></li>)}</ul>;
}

View File

@ -15,7 +15,7 @@
### D-02 代码组织
- Web 使用 `apps/web`承载官网、TTS 工作台、账户和业务管理页面。
- Phase 1 Web 暂使用仓库根目录 App Router承载官网、TTS 工作台、账户和业务管理页面;在 API/CMS/Worker 进入同仓库前不为单一 Web 应用增加额外 monorepo 层。后续若引入多应用,再迁入 `apps/web`,保持页面和组件边界不变
- 业务 API 使用 `services/api`,统一负责身份、权限、权益、额度、任务和上游代理。
- Worker 使用 `services/worker`,以持久化任务表为恢复依据。
- CMS 采用同仓库独立落点 `apps/cms`,内容授权和 schema 与业务身份隔离。

View File

@ -36,14 +36,16 @@
### Phase 1视觉与官网骨架
- `P1-01` 建立品牌色、灰阶、字体、间距、按钮、输入、焦点和响应式变量。
- `P1-02` 建立 `apps/web`、导航、页脚、移动导航、通用容器和登录/账户入口。
- `P1-03` 实现以蓝牙、音频和智能硬件为主体的首页AI Lab/TTS 只做轻量入口。
- `P1-04` 实现产品列表、详情模板、关于和联系页面,使用结构化本地适配器
- `P1-05` 实现 TTS、认证、账户和管理页面状态结构不得假成功。
- `P1-06` 完成 SEO、404、键盘操作、移动端和无横向溢出检查
- `P1-01` 已完成:建立品牌色、灰阶、字体、间距、按钮、输入、焦点和响应式变量,见 `app/globals.css``components/ui.tsx`
- `P1-02` 已完成:建立当前 Web 骨架、导航、页脚、移动导航、通用容器和登录/账户入口。当前落点为仓库根目录 App Router后续可迁入 `apps/web`,不改变组件边界。
- `P1-03` 已完成:实现以蓝牙、音频和智能硬件为主体的首页AI Lab/TTS 仅作轻量入口。
- `P1-04` 已完成:实现产品列表、详情模板、关于和联系页面,使用 `lib/content.ts` 集中管理的开发占位数据
- `P1-05` 已完成(布局):实现 TTS、认证、账户和管理页面状态结构所有真实提交和操作均未启用,不显示假成功。
- `P1-06` 已完成(静态/HTTP完成 SEO metadata、404、键盘焦点、减少动效、响应式源码检查、构建和路由冒烟。真实浏览器截图待测试环境具备浏览器或授权自动化工具后补充
交付证据:可运行 Web 骨架、页面检查记录、截图和 `docs/phase-1-report.md`。动态认证、CMS 发布和真实 TTS 仍不在本阶段宣称完成。
交付证据:可运行 Web 骨架、页面检查记录和 `docs/phase-1-report.md`。动态认证、CMS 发布和真实 TTS 仍不在本阶段宣称完成。最小 LAN HTTP 部署方案已写入 Phase 1 报告。
Phase 1 当前可在本地继续开发,也可在测试机仅部署官网骨架。测试机部署前需准备 Node.js、用户目录、TCP/3000 局域网访问和必要安装权限数据库、TTS、CMS、正式域名和 HTTPS 不作为本阶段部署前置条件。
### Phase 2认证与权益基础

115
docs/phase-1-report.md Normal file
View File

@ -0,0 +1,115 @@
# Phase 1 设计系统与官网骨架报告
日期2026-09-08
范围P1-01 至 P1-06
状态官网骨架完成真实认证、TTS、CMS 和管理业务未实现。
## 1. 交付摘要
- 建立 Next.js App Router + TypeScript + Tailwind CSS 4 本地应用。
- 建立 KAOTINGS 深色设计系统:`#0D0F12` 背景、橙 `#FF6A00`、蓝 `#1677FF`、青 `#00D1E6`、近白文字及灰阶变量。
- 使用正式 Logo 的网页公开副本 `public/brand/logo_bt.png`AI/PDF 原档仍不直接公开。
- 完成响应式导航、移动导航、页脚、跳过导航链接、键盘焦点样式和减少动效处理。
- 首页以蓝牙、音频、嵌入式和硬件产品为主体AI Lab/TTS 保持轻量入口。
- 完成产品列表、产品详情模板、关于、联系、TTS、登录、注册、账户和管理页面布局。
- 所有缺少真实资料的产品内容集中在 `lib/content.ts`,并使用“开发占位”标识;没有虚构规格、联系方式、资质或操作成功。
## 2. P1 工作包
| 编号 | 状态 | 实现位置 | 验证 |
| --- | --- | --- | --- |
| P1-01 | 已完成 | `app/globals.css`、`components/ui.tsx` | 设计令牌、按钮、输入、焦点、间距和响应式规则已集中定义 |
| P1-02 | 已完成 | `components/site-header.tsx`、`components/site-footer.tsx`、`app/layout.tsx` | 桌面导航、移动菜单、账户入口、页脚和跳过导航已实现 |
| P1-03 | 已完成 | `app/page.tsx` | Hero、硬件产品、技术能力、AI Lab、联系入口已实现 |
| P1-04 | 已完成 | `app/products/`、`app/about/`、`app/contact/`、`lib/content.ts` | 列表和详情模板由集中式占位数据驱动 |
| P1-05 | 已完成(布局) | `app/tts/`、`app/login/`、`app/register/`、`app/account/`、`app/admin/` | 加载/空/待接入/禁用/错误语义已展示;真实业务未接入 |
| P1-06 | 功能实现与构建检查完成;真实视觉验收待完成 | `app/layout.tsx`、`app/not-found.tsx`、全局样式 | typecheck、build、路由冒烟和响应式源码检查通过真实浏览器桌面/移动验收未完成 |
## 3. 页面清单
| 路由 | 当前能力 |
| --- | --- |
| `/` | 品牌首页产品为主AI Lab 为入口 |
| `/products` | 开发占位产品列表和分类视觉 |
| `/products/product-placeholder-a` | 产品详情模板 |
| `/products/product-placeholder-b` | 产品详情模板 |
| `/about` | 公司定位和原则,未虚构公司履历 |
| `/contact` | 联系信息边界和待确认状态 |
| `/tts` | TTS 工作台布局,控件禁用,不调用服务 |
| `/login`、`/register` | 认证表单布局,提交禁用 |
| `/account` | 账户、权益、额度和历史空状态 |
| `/admin`、`/admin/users`、`/admin/tts` | 管理后台布局和禁用状态 |
| `404` | 友好 Not Found 页面 |
## 4. 占位数据规则
所有开发占位内容集中在 `lib/content.ts`。当前只使用:
- `产品资料待补充 A/B` 作为模板标识,不作为正式产品名称。
- `无线音频`、`蓝牙配件` 作为待确认分类示意,不包含规格和性能承诺。
- CSS 抽象产品视觉只用于检查版式,不冒充真实产品图片。
- 联系页、页脚和关于页明确说明正式联系方式和公司资料待确认。
后续替换时只需先替换内容适配器Phase 4 接入 CMS 时不应重写页面模板。
## 5. 本地验证
已执行:
- `npm install`:安装 46 个依赖包,审计结果为 0 vulnerabilities。
- `npm run typecheck`:通过。
- `npm run build`:通过,全部静态页面生成成功。
- 本地 `next start` 动态端口启动:成功,无启动错误。
- HTTP 冒烟首页、产品列表、两个产品详情、关于、联系、TTS、登录、注册、账户、管理首页、用户管理、TTS 管理和 Logo 静态资源均返回 `200`
- 响应式源码检查:容器、断点、移动导航、移动端排列、键盘焦点和 `prefers-reduced-motion` 已覆盖。此项是源码检查,不等同于实际设备视觉验收。
当前执行环境对固定本地端口 `3000`、`3100`、`3210` 返回 Windows `EACCES`;动态端口可启动,因此没有修改系统端口策略。当前环境没有可用的 Chrome、Edge、Firefox 或既有 Playwright尚未完成真实桌面/移动浏览器视觉验收,也未生成截图。该项明确交由测试站浏览器或用户电脑/手机验收,不将源码检查写成设备验收通过。
## 6. 最小测试部署方案
### 6.1 本阶段部署范围
只部署官网骨架不部署数据库、Redis、TTS、Worker、CMS 或认证服务。页面中的真实业务入口保持禁用或预览说明。
### 6.2 服务器所需软件
- Ubuntu 24.04 已有,建议安装 Node.js `20.9+`,当前项目本地使用 Node.js `24.11.1` 验证。
- npm 随 Node.js 提供。
- 不需要 PostgreSQL、Redis、对象存储、TTS 或 CMS。
- 可先不安装 Nginx/Caddy直接由 `next start` 提供局域网 HTTP 验证。
### 6.3 部署目录和命令建议
在权限未落实前不执行。为避免第一阶段申请 root 部署目录,可优先评估用户目录:
```text
/home/flym/kaotings-www
```
建议流程:
```bash
npm ci
npm run build
npm run start -- -H 0.0.0.0 -p 3000
```
访问地址:`http://192.168.199.22:3000/`
### 6.4 权限和端口
- 文件复制到 `/home/flym/kaotings-www` 可由 `flym` 用户完成,前提是目录由该用户拥有。
- 安装 Node.js、配置系统级进程、开放防火墙端口或绑定受控服务可能需要 sudo/管理员权限。
- 当前 `flym` 属于 sudo 组,但非交互 sudo 仍需要额外认证;本次未安装或修改服务器服务。
- 最小访问端口SSH `22` 用于运维,应用 TCP `3000` 仅允许局域网测试来源。
- 本阶段不开放 80/443不配置正式域名或 HTTPS认证和生产发布前仍必须完成可信 HTTPS。
### 6.5 测试部署退出条件
- 局域网浏览器能打开首页、产品模板、关于、联系和所有预览入口。
- 仅显示开发占位内容,不显示虚构联系方式、产品规格或成功状态。
- 服务器重启或进程重启后的启动方式已记录;正式部署前再替换为受控进程管理和反向代理。
## 7. 部署前核实状态
本方案已复核:本轮只部署 Next.js 官网骨架,所需新增软件为 Node.js 20.9+ 与 npm不安装 PostgreSQL、Redis、TTS、Worker、CMS 或对象存储。服务器变更前需要确认 Node.js 安装权限、`/home/flym/kaotings-www` 写权限、TCP/3000 局域网访问权限,以及后续进程保持方式。当前尚未执行服务器安装或部署。

37
lib/content.ts Normal file
View File

@ -0,0 +1,37 @@
export type Product = {
slug: string;
name: string;
category: string;
shortLabel: string;
summary: string;
state: "placeholder";
accent: "orange" | "blue";
};
export const products: Product[] = [
{
slug: "product-placeholder-a",
name: "产品资料待补充 A",
category: "无线音频",
shortLabel: "开发占位产品",
summary: "真实产品图片、型号与规格待确认。此卡片仅用于验证产品中心与详情模板。",
state: "placeholder",
accent: "orange",
},
{
slug: "product-placeholder-b",
name: "产品资料待补充 B",
category: "蓝牙配件",
shortLabel: "开发占位产品",
summary: "真实产品内容尚未入库。发布前将由结构化 CMS 内容替换。",
state: "placeholder",
accent: "blue",
},
];
export const navigation = [
{ label: "产品", href: "/products" },
{ label: "技术", href: "/#technology" },
{ label: "AI Lab", href: "/tts" },
{ label: "关于我们", href: "/about" },
];

7
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;

1787
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "kaotings-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "4.3.3",
"@types/node": "26.5.0",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.7",
"typescript": "5.9.3"
}
}

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

BIN
public/brand/logo_bt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

41
tsconfig.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}