init:初始化项目

This commit is contained in:
2026-04-07 15:21:27 +08:00
commit 1b494b6029
366 changed files with 11461 additions and 0 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+116
View File
@@ -0,0 +1,116 @@
import Link from "next/link";
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function ConsolePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dictionary = getDictionary(locale as Locale);
return (
<div className="page-stack">
<section className="section-panel">
<div className="dashboard-top">
<div>
<div className="eyebrow">{dictionary.console.tag}</div>
<h1>{dictionary.console.title}</h1>
<p>{dictionary.console.subtitle}</p>
</div>
<div className="dashboard-actions">
<Link className="button button-light" href={`/${locale}/pricing`}>
{dictionary.console.billingCta}
</Link>
<Link className="button button-dark" href={`/${locale}/login`}>
{dictionary.console.loginCta}
</Link>
</div>
</div>
<div className="stats-grid">
{dictionary.console.metrics.map((metric) => (
<article className="stat-card" key={metric.label}>
<strong>{metric.value}</strong>
<span>{metric.label}</span>
</article>
))}
</div>
</section>
<section className="dashboard-grid">
<article className="dashboard-card">
<div className="section-heading compact">
<div className="eyebrow">{dictionary.console.skillsTag}</div>
<h2>{dictionary.console.skillsTitle}</h2>
</div>
<div className="table-list">
{dictionary.console.officialSkills.map((skill) => (
<div className="table-row" key={skill.name}>
<div>
<strong>{skill.name}</strong>
<span>{skill.routeKey}</span>
</div>
<p>{skill.status}</p>
</div>
))}
</div>
</article>
<article className="dashboard-card">
<div className="section-heading compact">
<div className="eyebrow">{dictionary.console.keysTag}</div>
<h2>{dictionary.console.keysTitle}</h2>
</div>
<div className="table-list">
{dictionary.console.apiKeys.map((key) => (
<div className="table-row" key={key.name}>
<div>
<strong>{key.name}</strong>
<span>{key.masked}</span>
</div>
<p>{key.scope}</p>
</div>
))}
</div>
</article>
</section>
<section className="dashboard-grid">
<article className="dashboard-card">
<div className="section-heading compact">
<div className="eyebrow">{dictionary.console.usageTag}</div>
<h2>{dictionary.console.usageTitle}</h2>
</div>
<div className="table-list">
{dictionary.console.usageRows.map((row) => (
<div className="table-row" key={row.name}>
<div>
<strong>{row.name}</strong>
<span>{row.count}</span>
</div>
<p>{row.cost}</p>
</div>
))}
</div>
</article>
<article className="dashboard-card billing-card">
<div className="section-heading compact">
<div className="eyebrow">{dictionary.console.planTag}</div>
<h2>{dictionary.console.planTitle}</h2>
</div>
<p className="billing-copy">{dictionary.console.planDescription}</p>
<ul className="bullet-list">
{dictionary.console.planBenefits.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<Link className="button button-dark" href={`/${locale}/pricing`}>
{dictionary.console.planCta}
</Link>
</article>
</section>
</div>
);
}
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function DocsPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dictionary = getDictionary(locale as Locale);
return (
<div className="page-stack">
<section className="section-panel hero-tight">
<div className="section-heading">
<div className="eyebrow">{dictionary.docs.tag}</div>
<h1>{dictionary.docs.title}</h1>
<p>{dictionary.docs.intro}</p>
</div>
</section>
<section className="docs-layout">
<article className="docs-card">
<h2>{dictionary.docs.quickStartTitle}</h2>
<ol className="number-list">
{dictionary.docs.quickSteps.map((step) => (
<li key={step.title}>
<strong>{step.title}</strong>
<p>{step.description}</p>
<pre>
<code>{step.code}</code>
</pre>
</li>
))}
</ol>
</article>
<article className="docs-card">
<h2>{dictionary.docs.envTitle}</h2>
<div className="docs-table">
{dictionary.docs.envVars.map((item) => (
<div className="docs-row" key={item.name}>
<div>
<strong>{item.name}</strong>
<span>{item.required}</span>
</div>
<p>{item.description}</p>
</div>
))}
</div>
</article>
</section>
<section className="section-panel">
<div className="section-heading">
<div className="eyebrow">{dictionary.docs.skillsTag}</div>
<h2>{dictionary.docs.skillsTitle}</h2>
</div>
<div className="card-grid">
{dictionary.docs.skills.map((skill) => (
<article className="feature-card" key={skill.name}>
<h3>{skill.name}</h3>
<pre>
<code>{skill.command}</code>
</pre>
<p>{skill.description}</p>
</article>
))}
</div>
</section>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { notFound } from "next/navigation";
import { SiteChrome } from "@/components/site-chrome";
import { getDictionary, isLocale, type Locale } from "@/lib/site-content";
export default async function LocaleLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!isLocale(locale)) {
notFound();
}
const dictionary = getDictionary(locale as Locale);
return (
<SiteChrome dictionary={dictionary} locale={locale}>
{children}
</SiteChrome>
);
}
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
import Link from "next/link";
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function LoginPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dictionary = getDictionary(locale as Locale);
return (
<div className="page-stack">
<section className="login-shell">
<div className="login-panel">
<div className="eyebrow">{dictionary.login.tag}</div>
<h1>{dictionary.login.title}</h1>
<p>{dictionary.login.subtitle}</p>
<div className="login-actions">
<button className="button button-dark" type="button">
{dictionary.login.primaryProvider}
</button>
<button className="button button-light" type="button">
{dictionary.login.secondaryProvider}
</button>
</div>
<p className="legal-copy">
{dictionary.login.termsPrefix}{" "}
<Link href={`/${locale}/pricing`}>{dictionary.login.termsLink}</Link>{" "}
{dictionary.login.and}{" "}
<Link href={`/${locale}/docs`}>{dictionary.login.privacyLink}</Link>
</p>
</div>
<div className="login-benefits">
{dictionary.login.benefits.map((benefit) => (
<article className="benefit-card" key={benefit.title}>
<span className="card-kicker">{benefit.kicker}</span>
<h2>{benefit.title}</h2>
<p>{benefit.description}</p>
</article>
))}
</div>
</section>
</div>
);
}
+98
View File
@@ -0,0 +1,98 @@
import Link from "next/link";
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dictionary = getDictionary(locale as Locale);
return (
<div className="page-stack">
<section className="hero-panel">
<div className="hero-copy">
<div className="eyebrow">{dictionary.home.tag}</div>
<h1>{dictionary.home.title}</h1>
<p className="hero-description">{dictionary.home.subtitle}</p>
<div className="hero-actions">
<Link className="button button-dark" href={`/${locale}/login`}>
{dictionary.home.primaryCta}
</Link>
<Link className="button button-light" href={`/${locale}/docs`}>
{dictionary.home.secondaryCta}
</Link>
</div>
<div className="hero-proof">
<span>{dictionary.home.freeTrial}</span>
<span>{dictionary.home.socialProof}</span>
</div>
</div>
<div className="hero-stage">
<div className="stage-card stage-card-primary">
<div className="stage-label">{dictionary.home.stageLabel}</div>
<div className="stage-stat">{dictionary.home.stageValue}</div>
<p>{dictionary.home.stageDescription}</p>
</div>
<div className="stage-card">
<div className="stage-mini-grid">
{dictionary.home.heroPhrases.map((phrase) => (
<span key={phrase}>{phrase}</span>
))}
</div>
</div>
</div>
</section>
<section className="section-panel">
<div className="section-heading">
<div className="eyebrow">{dictionary.home.scenesTag}</div>
<h2>{dictionary.home.scenesTitle}</h2>
<p>{dictionary.home.scenesSubtitle}</p>
</div>
<div className="card-grid card-grid-three">
{dictionary.home.scenes.map((scene) => (
<article className="feature-card" key={scene.name}>
<span className="card-kicker">{scene.category}</span>
<h3>{scene.name}</h3>
<p>{scene.description}</p>
</article>
))}
</div>
</section>
<section className="section-panel section-panel-accent">
<div className="section-heading">
<div className="eyebrow">{dictionary.home.flowTag}</div>
<h2>{dictionary.home.flowTitle}</h2>
<p>{dictionary.home.flowSubtitle}</p>
</div>
<div className="steps-grid">
{dictionary.home.flowSteps.map((step, index) => (
<article className="step-card" key={step.title}>
<div className="step-number">0{index + 1}</div>
<h3>{step.title}</h3>
<p>{step.description}</p>
</article>
))}
</div>
</section>
<section className="section-panel">
<div className="section-heading">
<div className="eyebrow">{dictionary.home.trustTag}</div>
<h2>{dictionary.home.trustTitle}</h2>
</div>
<div className="stats-grid">
{dictionary.home.trustStats.map((stat) => (
<article className="stat-card" key={stat.label}>
<strong>{stat.value}</strong>
<span>{stat.label}</span>
</article>
))}
</div>
</section>
</div>
);
}
Binary file not shown.
+67
View File
@@ -0,0 +1,67 @@
import Link from "next/link";
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function PricingPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const dictionary = getDictionary(locale as Locale);
return (
<div className="page-stack">
<section className="section-panel hero-tight">
<div className="section-heading">
<div className="eyebrow">{dictionary.pricing.tag}</div>
<h1>{dictionary.pricing.title}</h1>
<p>{dictionary.pricing.subtitle}</p>
</div>
</section>
<section className="pricing-grid">
{dictionary.pricing.plans.map((plan) => (
<article
className={`pricing-card ${plan.highlight ? "pricing-card-highlight" : ""}`}
key={plan.name}
>
<div className="pricing-top">
<div>
<span className="card-kicker">{plan.badge}</span>
<h2>{plan.name}</h2>
</div>
<div className="price-line">
<strong>{plan.price}</strong>
<span>{plan.cadence}</span>
</div>
</div>
<p className="pricing-summary">{plan.summary}</p>
<ul className="bullet-list">
{plan.features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
<Link className="button button-dark" href={`/${locale}/login`}>
{plan.cta}
</Link>
</article>
))}
</section>
<section className="section-panel">
<div className="section-heading">
<div className="eyebrow">{dictionary.pricing.faqTag}</div>
<h2>{dictionary.pricing.faqTitle}</h2>
</div>
<div className="faq-grid">
{dictionary.pricing.faqs.map((faq) => (
<article className="faq-card" key={faq.question}>
<h3>{faq.question}</h3>
<p>{faq.answer}</p>
</article>
))}
</div>
</section>
</div>
);
}
+645
View File
@@ -0,0 +1,645 @@
:root {
--bg: #f6f1e8;
--panel: rgba(255, 255, 255, 0.78);
--panel-strong: #fffaf3;
--ink: #1e1b16;
--muted: #665f57;
--line: rgba(30, 27, 22, 0.1);
--accent: #ea7c2b;
--accent-soft: #ffd9bf;
--shadow: 0 24px 64px rgba(36, 24, 12, 0.12);
--radius-xl: 32px;
--radius-lg: 24px;
--radius-md: 18px;
--container: 1200px;
--font-display: "Avenir Next", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--font-body: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--ink);
font-family: var(--font-body);
background:
radial-gradient(circle at top left, rgba(234, 124, 43, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(163, 214, 197, 0.18), transparent 24%),
linear-gradient(180deg, #fbf8f2 0%, #f4ede2 100%);
min-height: 100vh;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea,
select {
font: inherit;
}
pre,
code {
font-family: "SFMono-Regular", "JetBrains Mono", Consolas, monospace;
}
.site-shell {
min-height: 100vh;
}
.site-header,
.site-footer,
.main-content {
width: min(calc(100% - 32px), var(--container));
margin: 0 auto;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-top: 16px;
padding: 16px 20px;
border: 1px solid var(--line);
border-radius: 999px;
backdrop-filter: blur(14px);
background: rgba(255, 249, 242, 0.78);
box-shadow: 0 10px 30px rgba(55, 34, 20, 0.06);
}
.brand-lockup {
display: flex;
align-items: center;
gap: 14px;
}
.brand-lockup strong,
.section-heading h1,
.section-heading h2,
.hero-copy h1,
.pricing-card h2,
.feature-card h3,
.faq-card h3,
.dashboard-card h2,
.login-panel h1,
.benefit-card h2 {
font-family: var(--font-display);
}
.brand-lockup span {
display: block;
color: var(--muted);
font-size: 13px;
}
.brand-mark {
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: 16px;
background: linear-gradient(135deg, #1d1b18 0%, #614126 100%);
color: white;
font-weight: 700;
letter-spacing: 0.08em;
}
.main-nav,
.header-actions,
.locale-switch,
.hero-actions,
.hero-proof,
.dashboard-actions,
.pricing-top,
.docs-row,
.table-row {
display: flex;
align-items: center;
}
.main-nav {
gap: 18px;
color: var(--muted);
font-size: 15px;
}
.main-nav a:hover,
.footer-links a:hover,
.locale-switch a:hover {
color: var(--ink);
}
.header-actions {
gap: 14px;
}
.locale-switch {
gap: 10px;
padding: 6px;
border-radius: 999px;
background: rgba(30, 27, 22, 0.05);
}
.locale-switch a {
padding: 6px 10px;
border-radius: 999px;
color: var(--muted);
font-size: 13px;
}
.locale-switch .locale-active {
background: white;
color: var(--ink);
box-shadow: 0 6px 16px rgba(30, 27, 22, 0.08);
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 18px;
border-radius: 999px;
border: 1px solid transparent;
font-weight: 600;
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.button:hover {
transform: translateY(-1px);
}
.button-dark {
background: #1f1c18;
color: white;
box-shadow: 0 12px 24px rgba(31, 28, 24, 0.18);
}
.button-light {
background: rgba(255, 255, 255, 0.86);
color: var(--ink);
border-color: var(--line);
}
.button-small {
min-height: 40px;
padding: 0 14px;
}
.main-content {
padding: 28px 0 48px;
}
.page-stack {
display: grid;
gap: 24px;
}
.hero-panel,
.section-panel,
.pricing-card,
.docs-card,
.dashboard-card,
.login-panel,
.benefit-card,
.faq-card,
.feature-card,
.step-card,
.stat-card {
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: var(--panel);
box-shadow: var(--shadow);
}
.hero-panel,
.section-panel,
.login-shell,
.pricing-grid,
.docs-layout,
.dashboard-grid,
.card-grid,
.steps-grid,
.stats-grid,
.faq-grid {
display: grid;
gap: 20px;
}
.hero-panel {
grid-template-columns: 1.2fr 0.8fr;
padding: 42px;
min-height: 520px;
overflow: hidden;
}
.hero-copy,
.hero-stage,
.pricing-card,
.docs-card,
.dashboard-card,
.login-panel,
.benefit-card,
.faq-card,
.feature-card,
.step-card,
.stat-card {
position: relative;
}
.hero-copy h1,
.section-heading h1 {
margin: 0;
font-size: clamp(2.8rem, 5vw, 5rem);
line-height: 0.96;
max-width: 12ch;
}
.hero-description,
.section-heading p,
.pricing-summary,
.billing-copy,
.legal-copy,
.feature-card p,
.faq-card p,
.step-card p,
.docs-row p,
.table-row p,
.login-panel p,
.benefit-card p,
.stage-card p {
color: var(--muted);
line-height: 1.65;
}
.hero-actions {
gap: 14px;
margin-top: 28px;
flex-wrap: wrap;
}
.hero-proof {
gap: 18px;
margin-top: 18px;
flex-wrap: wrap;
color: var(--muted);
font-size: 14px;
}
.hero-stage {
align-content: end;
}
.stage-card {
padding: 24px;
border-radius: 28px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.8);
}
.stage-card + .stage-card {
margin-top: 16px;
}
.stage-card-primary {
background:
linear-gradient(155deg, rgba(255, 231, 212, 0.95) 0%, rgba(255, 255, 255, 0.88) 100%);
}
.stage-label,
.eyebrow,
.card-kicker {
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
font-weight: 700;
}
.stage-stat {
margin-top: 12px;
font-size: clamp(2rem, 4vw, 3.4rem);
line-height: 1;
font-weight: 700;
}
.stage-mini-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.stage-mini-grid span {
padding: 14px 16px;
border-radius: 16px;
background: rgba(30, 27, 22, 0.05);
font-size: 14px;
}
.section-panel,
.docs-card,
.dashboard-card,
.login-panel,
.benefit-card,
.pricing-card,
.faq-card,
.feature-card,
.step-card,
.stat-card {
padding: 28px;
}
.hero-tight .section-heading h1 {
max-width: none;
font-size: clamp(2.4rem, 4vw, 4rem);
}
.section-panel-accent {
background:
linear-gradient(145deg, rgba(255, 235, 221, 0.8) 0%, rgba(255, 249, 242, 0.86) 100%);
}
.section-heading {
display: grid;
gap: 8px;
}
.section-heading.compact h2 {
margin: 0;
font-size: 1.6rem;
}
.card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card-grid-three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.feature-card h3,
.faq-card h3,
.step-card h3 {
margin: 14px 0 8px;
font-size: 1.3rem;
}
.steps-grid,
.stats-grid,
.faq-grid,
.pricing-grid,
.docs-layout,
.dashboard-grid,
.login-shell {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 16px;
background: rgba(234, 124, 43, 0.15);
color: var(--accent);
font-weight: 700;
}
.stat-card {
gap: 10px;
}
.stat-card strong {
display: block;
font-size: 2rem;
}
.stat-card span {
color: var(--muted);
}
.pricing-card {
display: grid;
gap: 16px;
align-content: start;
}
.pricing-card-highlight {
background:
linear-gradient(155deg, rgba(255, 225, 197, 0.88) 0%, rgba(255, 250, 243, 0.95) 100%);
}
.pricing-top {
justify-content: space-between;
align-items: flex-start;
gap: 18px;
}
.price-line {
text-align: right;
}
.price-line strong {
display: block;
font-size: 2.4rem;
}
.price-line span {
color: var(--muted);
}
.bullet-list,
.number-list {
margin: 0;
padding-left: 20px;
color: var(--muted);
line-height: 1.8;
}
.number-list {
display: grid;
gap: 18px;
}
.number-list li strong {
display: block;
margin-bottom: 6px;
color: var(--ink);
}
.docs-card pre,
.feature-card pre {
margin: 14px 0;
padding: 16px;
overflow: auto;
border-radius: 18px;
background: #171411;
color: #f7f3ec;
}
.docs-table,
.table-list {
display: grid;
gap: 14px;
}
.docs-row,
.table-row {
justify-content: space-between;
gap: 20px;
padding: 14px 0;
border-bottom: 1px solid var(--line);
align-items: flex-start;
}
.docs-row:last-child,
.table-row:last-child {
border-bottom: 0;
}
.docs-row strong,
.table-row strong {
display: block;
}
.docs-row span,
.table-row span {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: 13px;
}
.login-shell {
align-items: stretch;
}
.login-panel {
background:
linear-gradient(165deg, rgba(255, 249, 240, 0.96) 0%, rgba(255, 255, 255, 0.9) 100%);
}
.login-actions {
display: grid;
gap: 12px;
margin-top: 28px;
}
.benefit-card {
display: grid;
align-content: start;
}
.dashboard-top {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: flex-end;
margin-bottom: 24px;
}
.dashboard-actions {
gap: 12px;
flex-wrap: wrap;
}
.dashboard-grid {
align-items: start;
}
.billing-card {
background:
linear-gradient(160deg, rgba(255, 230, 210, 0.85) 0%, rgba(255, 255, 255, 0.9) 100%);
}
.site-footer {
display: flex;
justify-content: space-between;
gap: 24px;
padding: 18px 8px 48px;
color: var(--muted);
}
.footer-links {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
@media (max-width: 1100px) {
.hero-panel,
.card-grid-three,
.pricing-grid,
.docs-layout,
.dashboard-grid,
.faq-grid,
.steps-grid,
.stats-grid,
.login-shell {
grid-template-columns: 1fr;
}
.hero-panel {
min-height: auto;
}
}
@media (max-width: 840px) {
.site-header,
.site-footer,
.main-content {
width: min(calc(100% - 20px), var(--container));
}
.site-header,
.dashboard-top,
.site-footer {
flex-direction: column;
align-items: stretch;
}
.main-nav {
flex-wrap: wrap;
}
.header-actions {
justify-content: space-between;
}
.hero-panel,
.section-panel,
.pricing-card,
.docs-card,
.dashboard-card,
.login-panel,
.benefit-card,
.faq-card,
.feature-card,
.step-card,
.stat-card {
padding: 22px;
}
.hero-copy h1,
.section-heading h1 {
font-size: clamp(2.2rem, 10vw, 3.2rem);
}
.card-grid {
grid-template-columns: 1fr;
}
}
+20
View File
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "PopiArt Console",
description:
"PopiArt official skills, documentation, console, pricing, and login experience.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function RootPage() {
redirect("/zh");
}