This commit is contained in:
ljy
2026-08-11 01:34:54 +08:00
parent 738f00d442
commit eba21aedf9
133 changed files with 15595 additions and 21 deletions
+40
View File
@@ -0,0 +1,40 @@
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from './components/Layout'
import { HomePage } from './pages/Home'
import { BrandPage } from './pages/Brand'
import { PoetryPage } from './pages/Poetry'
import { QingxiangPage } from './pages/Qingxiang'
import { HaokePage } from './pages/Haoke'
import { SystemPage } from './pages/System'
import { TechPage } from './pages/Tech'
import { SubsidyPage } from './pages/Subsidy'
import { PartnerPage } from './pages/Partner'
import { TechDetailPage } from './pages/TechDetail'
import { ContactPage } from './pages/Contact'
import { DeepPage } from './pages/DeepPage'
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="brand" element={<BrandPage />} />
<Route path="poetry" element={<PoetryPage />} />
<Route path="qingxiang" element={<QingxiangPage />} />
<Route path="haoke" element={<HaokePage />} />
<Route path="system" element={<SystemPage />} />
<Route path="tech" element={<TechPage />} />
<Route path="subsidy" element={<SubsidyPage />} />
<Route path="partner" element={<PartnerPage />} />
<Route path="tech-detail" element={<TechDetailPage />} />
<Route path="contact" element={<ContactPage />} />
<Route path="culture/:slug" element={<DeepPage />} />
<Route path="poetry/:slug" element={<DeepPage />} />
<Route path="qingxiang/:slug" element={<DeepPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</BrowserRouter>
)
}
+458
View File
@@ -0,0 +1,458 @@
.deep-page {
padding-bottom: 4rem;
}
.deep-hero {
position: relative;
min-height: 18rem;
display: flex;
align-items: flex-end;
padding: 5rem 0 2.5rem;
overflow: hidden;
margin-bottom: 2.5rem;
background: linear-gradient(160deg, var(--paper-deep), var(--paper) 55%);
}
.deep-hero.has-image {
min-height: 22rem;
}
.deep-hero-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transform: scale(1.04);
}
.deep-hero-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(247, 242, 234, 0.25), rgba(247, 242, 234, 0.94) 72%);
}
.theme-tech .deep-hero-mask {
background: linear-gradient(180deg, rgba(18, 16, 20, 0.35), rgba(18, 16, 20, 0.92) 72%);
}
.deep-hero-copy {
position: relative;
z-index: 1;
}
.deep-back {
display: inline-flex;
margin-top: 0.75rem;
color: var(--brand);
min-height: 2.75rem;
align-items: center;
}
.theme-tech .deep-back {
color: var(--gold);
}
.deep-body {
display: grid;
gap: 2rem;
}
.deep-prose {
max-width: 46rem;
margin-inline: auto;
width: 100%;
}
.deep-h {
margin: 0 0 0.85rem;
color: var(--brand);
font-size: 1.45rem;
letter-spacing: 0.08em;
}
.theme-tech .deep-h {
color: var(--gold);
}
.deep-p {
margin: 0 0 1rem;
color: var(--ink-soft);
font-size: 1.02rem;
line-height: 1.9;
}
.deep-quote {
margin: 0 0 1.25rem;
padding: 1.4rem 1.35rem;
border-left: 2px solid var(--brand);
background: rgba(255, 255, 255, 0.55);
}
.theme-tech .deep-quote {
border-left-color: var(--gold);
background: var(--tech-card);
}
.deep-quote p {
margin: 0;
font-size: 1.2rem;
line-height: 1.85;
}
.deep-quote cite {
display: block;
margin-top: 0.85rem;
font-style: normal;
color: var(--ink-soft);
font-family: 'Noto Sans SC', sans-serif;
font-size: 0.9rem;
}
.deep-list {
margin: 0 0 1rem;
padding-left: 1.1rem;
color: var(--ink-soft);
line-height: 1.9;
}
.deep-media img,
.deep-media-solo img {
width: 100%;
max-height: 28rem;
object-fit: contain;
background: rgba(0, 0, 0, 0.03);
}
.deep-media-solo {
max-width: 40rem;
margin-inline: auto;
}
/* person */
.deep-person-grid {
grid-template-columns: 1.2fr 0.8fr;
align-items: start;
}
/* timeline */
.deep-timeline {
list-style: none;
margin: 0;
padding: 0 0 0 1.25rem;
border-left: 2px solid rgba(166, 28, 32, 0.18);
display: grid;
gap: 1.25rem;
}
.deep-timeline-item {
position: relative;
padding-left: 0.75rem;
}
.deep-timeline-dot {
position: absolute;
left: -1.55rem;
top: 0.45rem;
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
background: var(--brand);
box-shadow: 0 0 0 0.25rem rgba(166, 28, 32, 0.12);
}
.deep-timeline-item p {
margin: 0;
color: var(--ink-soft);
line-height: 1.75;
}
/* shrine */
.deep-shrine-stack {
max-width: 42rem;
margin-inline: auto;
text-align: center;
}
.deep-shrine-stack .deep-list {
text-align: left;
display: inline-block;
}
/* sites */
.deep-sites-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
.deep-site-card {
padding: 1.35rem 1.2rem;
background: rgba(255, 255, 255, 0.55);
border-top: 2px solid var(--gold);
}
/* figures */
.deep-figure-rail {
display: grid;
gap: 1rem;
}
.deep-figure-card {
display: grid;
grid-template-columns: 3.5rem 1fr;
gap: 0.75rem 1.1rem;
padding: 1.25rem 0;
border-bottom: 1px solid rgba(166, 28, 32, 0.12);
}
.deep-figure-index {
color: var(--gold);
font-size: 1.5rem;
grid-row: span 2;
}
.deep-figure-card .deep-h {
grid-column: 2;
}
.deep-figure-card .deep-p {
grid-column: 2;
}
/* factory */
.deep-factory-panel {
padding: 2rem 1.75rem;
background: linear-gradient(135deg, rgba(166, 28, 32, 0.06), rgba(196, 163, 90, 0.08));
border: 1px solid rgba(166, 28, 32, 0.12);
}
.deep-factory-caps {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.85rem;
margin-top: 1.5rem;
}
.deep-factory-cap {
padding: 1rem 1.1rem;
background: rgba(255, 255, 255, 0.7);
min-height: 4.5rem;
display: flex;
align-items: center;
}
.deep-factory-cap p {
margin: 0;
color: var(--ink);
}
/* poem wall */
.deep-poem-wall {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
align-items: stretch;
}
.deep-poem-slip {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 1.5rem 1.25rem;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(166, 28, 32, 0.1);
box-sizing: border-box;
}
.deep-poem-slip p {
margin: 0;
font-size: 1.15rem;
line-height: 1.85;
}
.deep-poem-slip cite {
margin-top: auto;
padding-top: 0.85rem;
display: block;
font-style: normal;
color: var(--ink-soft);
font-family: 'Noto Sans SC', sans-serif;
font-size: 0.88rem;
}
/* ink scroll */
.deep-ink-flow {
max-width: 40rem;
margin-inline: auto;
border-left: 1px solid var(--gold-soft);
padding-left: 1.75rem;
}
.deep-ink-flow .deep-quote {
writing-mode: vertical-rl;
min-height: 14rem;
max-width: 100%;
border-left: none;
border-top: 2px solid var(--brand);
margin-inline: auto;
}
/* manifesto */
.deep-manifesto-lead {
max-width: 48rem;
margin-inline: auto;
text-align: center;
}
.deep-loop-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.85rem;
}
.deep-loop-node {
padding: 1.2rem 1rem;
background: rgba(255, 255, 255, 0.7);
border-bottom: 3px solid var(--brand);
min-height: 9rem;
}
.deep-loop-num {
display: block;
color: var(--gold);
margin-bottom: 0.55rem;
letter-spacing: 0.12em;
}
.deep-loop-node p {
margin: 0;
color: var(--ink-soft);
line-height: 1.65;
font-size: 0.95rem;
}
/* matrix */
.deep-matrix {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.15rem;
}
.deep-matrix-card {
padding: 1.4rem 1.25rem;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(166, 28, 32, 0.1);
}
/* triad */
.deep-triad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.deep-triad-col {
padding: 1.5rem 1.2rem;
text-align: center;
background: linear-gradient(180deg, rgba(166, 28, 32, 0.08), transparent);
border-top: 2px solid var(--brand);
}
/* steps */
.deep-steps {
display: grid;
gap: 0;
max-width: 40rem;
margin-inline: auto;
}
.deep-step {
display: grid;
grid-template-columns: 3.5rem 1fr;
gap: 1rem;
padding: 1.35rem 0;
border-bottom: 1px dashed rgba(166, 28, 32, 0.2);
align-items: start;
}
.deep-step-index {
color: var(--brand);
font-size: 1.6rem;
}
.deep-step p {
margin: 0;
font-size: 1.1rem;
color: var(--ink);
line-height: 1.7;
}
/* scenes */
.deep-scenes {
display: grid;
gap: 1.5rem;
}
.deep-scene {
padding: 1.5rem 1.35rem;
background: rgba(255, 255, 255, 0.5);
border-left: 3px solid var(--gold);
}
/* tour */
.deep-tour-flow {
max-width: 44rem;
margin-inline: auto;
}
.deep-tour-steps {
display: grid;
gap: 0.75rem;
}
.deep-tour-step {
display: grid;
grid-template-columns: 2.5rem 1fr;
gap: 0.85rem;
align-items: center;
padding: 0.95rem 1.1rem;
background: rgba(255, 255, 255, 0.65);
}
.deep-tour-step span {
width: 2.1rem;
height: 2.1rem;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--brand);
color: #fff;
font-size: 0.95rem;
}
.deep-tour-step p {
margin: 0;
color: var(--ink-soft);
}
@media (max-width: 48rem) {
.deep-person-grid,
.deep-sites-grid,
.deep-poem-wall,
.deep-loop-strip,
.deep-matrix,
.deep-triad,
.deep-factory-caps {
grid-template-columns: 1fr;
}
.deep-ink-flow .deep-quote {
writing-mode: horizontal-tb;
min-height: auto;
}
.deep-figure-card {
grid-template-columns: 2.5rem 1fr;
}
}
+544
View File
@@ -0,0 +1,544 @@
import { Link } from 'react-router-dom'
import type { ReactElement } from 'react'
import { Reveal } from './Reveal'
import type { DeepBlock, DeepLayoutKind, DeepPageContent } from '../content/deep/types'
import './DeepLayout.css'
function backPath(group: DeepPageContent['group']) {
if (group === 'qingxiang') return '/qingxiang'
if (group === 'poetry') return '/poetry'
return '/brand'
}
function Blocks({ blocks }: { blocks: DeepBlock[] }) {
return (
<>
{blocks.map((block, i) => (
<Reveal key={`${block.type}-${i}`} variant={i % 2 === 0 ? 'anim-slide-left' : 'anim-rise'} delayMs={(i % 5) * 60}>
{block.type === 'heading' ? <h2 className="deep-h serif">{block.text}</h2> : null}
{block.type === 'paragraph' ? <p className="deep-p">{block.text}</p> : null}
{block.type === 'quote' ? (
<blockquote className="deep-quote serif">
<p>{block.text}</p>
{block.source ? <cite>{block.source}</cite> : null}
</blockquote>
) : null}
{block.type === 'list' ? (
<ul className="deep-list">
{block.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : null}
</Reveal>
))}
</>
)
}
function Hero({ page, image }: { page: DeepPageContent; image?: string }) {
return (
<header className={`deep-hero ${image ? 'has-image' : 'no-image'}`}>
{image ? <div className="deep-hero-bg" style={{ backgroundImage: `url(${image})` }} /> : null}
<div className="deep-hero-mask" />
<div className="container deep-hero-copy">
<Reveal variant="anim-ink">
<p className="eyebrow">{page.eyebrow}</p>
<h1 className="page-title">{page.title}</h1>
<p className="lead">{page.lead}</p>
<Link className="deep-back" to={backPath(page.group)}>
</Link>
</Reveal>
</div>
</header>
)
}
function Closing({ text }: { text?: string }) {
if (!text) return null
return (
<Reveal variant="anim-rise">
<p className="closing">{text}</p>
</Reveal>
)
}
function SoloImage({ src }: { src?: string }) {
if (!src) return null
return (
<Reveal variant="anim-zoom">
<div className="media-frame deep-media deep-media-solo">
<img src={src} alt="" loading="lazy" />
</div>
</Reveal>
)
}
function listBlocks(blocks: DeepBlock[]) {
return blocks.filter((b): b is Extract<DeepBlock, { type: 'list' }> => b.type === 'list')
}
function quoteBlocks(blocks: DeepBlock[]) {
return blocks.filter((b): b is Extract<DeepBlock, { type: 'quote' }> => b.type === 'quote')
}
function headingPairs(blocks: DeepBlock[]) {
const pairs: { heading: string; body: DeepBlock[] }[] = []
let current: { heading: string; body: DeepBlock[] } | null = null
for (const b of blocks) {
if (b.type === 'heading') {
current = { heading: b.text, body: [] }
pairs.push(current)
} else if (current) {
current.body.push(b)
}
}
return pairs
}
function LayoutEssay({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-essay">
<Hero page={page} image={img} />
<div className="container deep-body deep-prose">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutPerson({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-person">
<Hero page={page} />
<div className="container deep-body deep-person-grid">
<div className="deep-person-copy">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
<SoloImage src={img} />
</div>
</article>
)
}
function LayoutTimeline({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-timeline">
<Hero page={page} />
<div className="container deep-body">
<SoloImage src={img} />
{lists.map((list, li) => (
<ol key={li} className="deep-timeline">
{list.items.map((item, i) => (
<Reveal key={item} variant="anim-slide-left" delayMs={i * 70} className="deep-timeline-item">
<span className="deep-timeline-dot" />
<p>{item}</p>
</Reveal>
))}
</ol>
))}
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutShrine({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-shrine">
<Hero page={page} image={img} />
<div className="container deep-body deep-shrine-stack">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutSites({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-sites">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-sites-grid">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 90} className="deep-site-card">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) =>
b.type === 'paragraph' ? (
<p key={j} className="deep-p">
{b.text}
</p>
) : null,
)}
</Reveal>
))}
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutFigures({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-figures">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-figure-rail">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-expand" delayMs={i * 100} className="deep-figure-card">
<span className="deep-figure-index serif">{String(i + 1).padStart(2, '0')}</span>
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) =>
b.type === 'paragraph' ? (
<p key={j} className="deep-p">
{b.text}
</p>
) : null,
)}
</Reveal>
))}
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutFactory({ page }: { page: DeepPageContent }) {
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-factory">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-factory-panel">
<Blocks blocks={rest} />
{lists.map((list, i) => (
<div key={i} className="deep-factory-caps">
{list.items.map((item) => (
<Reveal key={item} variant="anim-pop" className="deep-factory-cap">
<p>{item}</p>
</Reveal>
))}
</div>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutPoemWall({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const quotes = quoteBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'quote')
return (
<article className="deep-page deep-layout-poem-wall">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-poem-wall">
{quotes.map((q, i) => (
<Reveal key={`${q.source}-${i}`} variant="anim-unroll" delayMs={i * 120} className={`deep-poem-slip slip-${i % 3}`}>
<p className="serif">{q.text}</p>
{q.source ? <cite>{q.source}</cite> : null}
</Reveal>
))}
</div>
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutInkScroll({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-ink-scroll">
<Hero page={page} image={img} />
<div className="container deep-body deep-ink-flow">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutManifesto({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-manifesto">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-manifesto-lead">
<Blocks blocks={rest} />
</div>
{lists.map((list, i) => (
<div key={i} className="deep-loop-strip">
{list.items.map((item, j) => (
<Reveal key={item} variant="anim-pop" delayMs={j * 80} className="deep-loop-node">
<span className="deep-loop-num serif">{String(j + 1).padStart(2, '0')}</span>
<p>{item}</p>
</Reveal>
))}
</div>
))}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutMatrix({ page }: { page: DeepPageContent }) {
const pairs = headingPairs(page.blocks)
const orphan = page.blocks.filter((b, i, arr) => {
if (b.type === 'heading') return false
const prevHeading = [...arr.slice(0, i)].reverse().find((x) => x.type === 'heading')
return !prevHeading
})
return (
<article className="deep-page deep-layout-matrix">
<Hero page={page} />
<div className="container deep-body">
{orphan.length ? (
<div className="deep-prose">
<Blocks blocks={orphan} />
</div>
) : null}
<div className="deep-matrix">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-expand" delayMs={i * 90} className="deep-matrix-card">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
return null
})}
</Reveal>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutTriad({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-triad">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-triad">
{pairs.slice(0, 3).map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 100} className="deep-triad-col">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
return null
})}
</Reveal>
))}
</div>
{pairs.slice(3).length ? (
<div className="deep-prose">
{pairs.slice(3).map((pair) => (
<div key={pair.heading}>
<h2 className="deep-h serif">{pair.heading}</h2>
<Blocks blocks={pair.body} />
</div>
))}
</div>
) : null}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutSteps({ page }: { page: DeepPageContent }) {
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
const steps = lists[0]?.items ?? []
return (
<article className="deep-page deep-layout-steps">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-steps">
{steps.map((item, i) => (
<Reveal key={item} variant="anim-slide-left" delayMs={i * 100} className="deep-step">
<span className="deep-step-index serif">{String(i + 1).padStart(2, '0')}</span>
<p>{item}</p>
</Reveal>
))}
</div>
<div className="deep-prose">
<Blocks blocks={rest} />
{lists.slice(1).map((list, i) => (
<ul key={i} className="deep-list">
{list.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutScenes({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
const quotes = quoteBlocks(page.blocks)
return (
<article className="deep-page deep-layout-scenes">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-scenes">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 90} className="deep-scene">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
return null
})}
</Reveal>
))}
</div>
{quotes.map((q, i) => (
<blockquote key={i} className="deep-quote serif">
<p>{q.text}</p>
{q.source ? <cite>{q.source}</cite> : null}
</blockquote>
))}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutTour({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-tour">
<Hero page={page} image={img} />
<div className="container deep-body deep-tour-flow">
{lists.map((list, i) => (
<div key={i} className="deep-tour-steps">
{list.items.map((item, j) => (
<Reveal key={item} variant="anim-slide-right" delayMs={j * 70} className="deep-tour-step">
<span className="serif">{j + 1}</span>
<p>{item}</p>
</Reveal>
))}
</div>
))}
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
const layoutMap: Record<DeepLayoutKind, (p: { page: DeepPageContent }) => ReactElement> = {
essay: LayoutEssay,
person: LayoutPerson,
timeline: LayoutTimeline,
shrine: LayoutShrine,
sites: LayoutSites,
figures: LayoutFigures,
factory: LayoutFactory,
'poem-wall': LayoutPoemWall,
'ink-scroll': LayoutInkScroll,
manifesto: LayoutManifesto,
matrix: LayoutMatrix,
triad: LayoutTriad,
steps: LayoutSteps,
scenes: LayoutScenes,
tour: LayoutTour,
}
export function DeepLayout({ page }: { page: DeepPageContent }) {
const View = layoutMap[page.layout] ?? LayoutEssay
return <View page={page} />
}
+68
View File
@@ -0,0 +1,68 @@
.site-footer {
background: #1a1512;
color: rgba(247, 242, 234, 0.78);
padding: 2.75rem 0 2.25rem;
border-top: 1px solid rgba(196, 163, 90, 0.15);
}
.theme-tech .site-footer {
background: #0c0a0e;
border-top: 1px solid rgba(196, 163, 90, 0.28);
}
.footer-inner {
display: grid;
gap: 1.75rem;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.9rem;
}
.footer-logo {
width: 2.6rem;
height: 2.6rem;
object-fit: contain;
filter: brightness(1.1);
}
.footer-title {
margin: 0;
color: #f7f2ea;
font-size: 1.1rem;
}
.footer-slogan {
margin: 0.25rem 0 0;
color: var(--gold, #c4a35a);
letter-spacing: 0.12em;
}
.footer-meta {
font-size: 0.82rem;
line-height: 1.8;
color: rgba(247, 242, 234, 0.78);
}
.footer-meta p {
margin: 0.2rem 0;
}
.footer-meta a {
color: var(--gold, #c4a35a);
text-decoration: underline;
text-underline-offset: 3px;
}
.footer-meta a:hover {
color: #ffffff;
}
@media (min-width: 48rem) {
.footer-inner {
grid-template-columns: 1fr 1.4fr;
align-items: end;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Link } from 'react-router-dom'
import { footer, images } from '../content/site'
import './Footer.css'
export function Footer() {
return (
<footer className="site-footer">
<div className="container footer-inner">
<div className="footer-brand">
<img src={images.logo} alt="" className="footer-logo" />
<div>
<p className="serif footer-title">{footer.brandLine}</p>
<p className="footer-slogan">{footer.slogan}</p>
</div>
</div>
<div className="footer-meta">
<p>
{footer.copyright} · {footer.icp}
</p>
<p>{footer.compliance}</p>
<p>{footer.dataNote}</p>
<p>
<Link to="/contact"></Link>
</p>
</div>
</div>
</footer>
)
}
+113
View File
@@ -0,0 +1,113 @@
.site-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--paper);
color: var(--ink);
}
.site-shell > main {
flex: 1;
min-height: 0;
}
/* Global stage-mode HTML/Body scroll lock */
html.stage-mode,
body.stage-mode {
height: 100%;
overflow: hidden;
overscroll-behavior: none;
}
body.stage-mode #root {
height: 100%;
}
.page-section {
padding: 5rem 0;
}
.page-section.tight {
padding-top: 3.5rem;
}
.split {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 3rem;
align-items: center;
}
.media-frame {
overflow: hidden;
border-radius: 0.2rem;
box-shadow: var(--shadow-soft);
background: var(--paper-deep);
}
.media-frame img {
width: 100%;
height: 100%;
object-fit: cover;
aspect-ratio: 4 / 5;
}
.card-grid,
.module-grid,
.ends-grid,
.ports-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
.module-grid {
grid-template-columns: repeat(5, 1fr);
}
.soft-card {
padding: 1.4rem 1.35rem;
background: rgba(255, 255, 255, 0.55);
border: 1px solid rgba(166, 28, 32, 0.1);
border-radius: 0.2rem;
transition: transform 0.35s var(--ease), border-color 0.35s var(--ease), background 0.35s var(--ease);
}
.soft-card:hover {
transform: translateY(-0.2rem);
border-color: rgba(166, 28, 32, 0.28);
}
.theme-tech .soft-card {
background: var(--tech-card);
border-color: var(--tech-line);
}
.soft-card h3 {
margin: 0 0 0.65rem;
font-size: 1.15rem;
color: var(--brand);
}
.theme-tech .soft-card h3 {
color: var(--gold);
}
.soft-card p {
margin: 0;
color: var(--ink-soft);
font-size: 0.95rem;
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 1.75rem;
}
.footnote {
margin-top: 1.25rem;
font-size: 0.82rem;
color: var(--ink-soft);
}
+70
View File
@@ -0,0 +1,70 @@
import { useEffect } from 'react'
import { Outlet, useLocation } from 'react-router-dom'
import { useIsMobile } from '../hooks/useMobile'
import { StageProvider, useStageMode } from '../hooks/useStageMode'
import { MobileShell } from '../mobile/MobileShell'
import { MobileViews } from '../mobile/MobileViews'
import { StageShell } from '../stage/StageShell'
import { Footer } from './Footer'
import { Nav } from './Nav'
import './Layout.css'
const techRoutes = new Set(['/tech', '/subsidy', '/partner', '/tech-detail', '/contact'])
function LayoutInner() {
const location = useLocation()
const isStage = useStageMode()
const isMobile = useIsMobile(768)
const techLike = techRoutes.has(location.pathname)
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'auto' })
}, [location.pathname])
useEffect(() => {
document.documentElement.classList.toggle('stage-mode', isStage)
document.body.classList.toggle('stage-mode', isStage)
return () => {
document.documentElement.classList.remove('stage-mode')
document.body.classList.remove('stage-mode')
}
}, [isStage])
// 1. Stage Mode Branch
if (isStage) {
return <StageShell />
}
// 2. Mobile Dedicated Layout Branch (< 768px)
if (isMobile) {
// If accessing deep pages (e.g. /culture/status), render through Outlet; main modules render through MobileViews
const isDeepPage =
location.pathname.startsWith('/culture/') ||
location.pathname.startsWith('/poetry/') ||
location.pathname.startsWith('/qingxiang/')
return (
<MobileShell>
{isDeepPage ? <Outlet /> : <MobileViews />}
</MobileShell>
)
}
// 3. Desktop Normal Mode Branch
return (
<div className={`site-shell ${techLike ? 'theme-tech' : ''}`}>
<Nav />
<main className="page-enter" key={location.pathname}>
<Outlet />
</main>
<Footer />
</div>
)
}
export function Layout() {
return (
<StageProvider>
<LayoutInner />
</StageProvider>
)
}
+430
View File
@@ -0,0 +1,430 @@
.topnav {
position: sticky;
top: 0;
z-index: 1000;
height: var(--nav-h);
backdrop-filter: blur(12px);
background: rgba(247, 242, 234, 0.86);
border-bottom: 1px solid transparent;
transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.topnav.has-drawer-open {
z-index: 1001;
}
.theme-tech .topnav {
background: rgba(18, 16, 20, 0.9);
}
.topnav.is-scrolled {
border-bottom-color: rgba(166, 28, 32, 0.12);
background: rgba(247, 242, 234, 0.95);
}
.theme-tech .topnav.is-scrolled {
border-bottom-color: rgba(196, 163, 90, 0.22);
background: rgba(18, 16, 20, 0.96);
}
.theme-tech .nav-links a {
color: rgba(243, 236, 228, 0.72);
}
.theme-tech .nav-links a:hover,
.theme-tech .nav-links a.is-active {
color: var(--gold);
}
.theme-tech .nav-links a.is-active::after {
background: var(--gold);
}
.theme-tech .mobile-drawer {
background: rgba(18, 16, 20, 0.98);
}
.theme-tech .mobile-drawer a {
color: #f3ece4;
border-bottom-color: rgba(196, 163, 90, 0.15);
}
.topnav-inner {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.65rem;
min-height: 2.75rem;
}
.brand-logo {
width: 2.2rem;
height: 2.2rem;
object-fit: contain;
}
.brand-text {
font-size: 1.15rem;
color: var(--brand);
letter-spacing: 0.08em;
}
.nav-links {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.15rem 0.85rem;
max-width: 58rem;
}
.nav-links a {
position: relative;
font-size: 0.86rem;
color: var(--ink-soft);
padding: 0.35rem 0;
transition: color 0.25s var(--ease);
}
.nav-links a:hover,
.nav-links a.is-active {
color: var(--brand);
}
.nav-links a.is-active::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: var(--brand);
transform-origin: left;
animation: lineGrow 0.45s var(--ease) both;
}
.menu-toggle {
position: relative;
z-index: 1002;
width: 2.75rem;
height: 2.75rem;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.35rem;
}
.menu-toggle span {
width: 1.35rem;
height: 1.5px;
background: var(--brand);
transition: transform 0.3s var(--ease);
}
.mobile-drawer {
position: fixed;
inset: var(--nav-h) 0 0 0;
z-index: 1001;
background: rgba(247, 242, 234, 0.98);
backdrop-filter: blur(16px);
padding: 1.5rem 1.25rem 2rem;
transform: translateX(100%);
transition: transform 0.4s var(--ease);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-drawer.is-open {
transform: translateX(0);
}
.mobile-drawer a {
display: flex;
align-items: center;
min-height: 2.75rem;
border-bottom: 1px solid rgba(166, 28, 32, 0.1);
font-size: 1.05rem;
color: var(--ink);
}
.mobile-drawer a:hover {
color: var(--brand);
}
.nav-groups {
display: flex;
align-items: stretch;
gap: 0.35rem 1.1rem;
height: 100%;
}
.nav-group {
position: relative;
display: flex;
align-items: center;
height: 100%;
}
.nav-group-title {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 2.75rem;
font-size: 0.92rem;
color: var(--ink-soft);
letter-spacing: 0.06em;
transition: color 0.25s var(--ease);
}
.nav-chevron {
width: 0.4rem;
height: 0.4rem;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg) translateY(-0.1rem);
opacity: 0.55;
transition: transform 0.3s var(--ease), opacity 0.25s var(--ease);
}
.nav-group:hover .nav-group-title,
.nav-group.is-current .nav-group-title,
.nav-group.is-open .nav-group-title {
color: var(--brand);
}
.nav-group:hover .nav-chevron,
.nav-group.is-open .nav-chevron {
opacity: 1;
transform: rotate(225deg) translateY(-0.05rem);
}
.nav-dropdown {
position: absolute;
top: calc(100% - 0.1rem);
left: 50%;
transform: translateX(-50%) translateY(0.55rem) scale(0.98);
min-width: 13.5rem;
padding: 0.85rem 0.75rem 0.9rem;
background: var(--paper);
border: 1px solid rgba(166, 28, 32, 0.12);
border-radius: 0.2rem;
box-shadow: 0 1.1rem 2.4rem rgba(26, 21, 18, 0.12);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.28s var(--ease), transform 0.28s var(--ease), visibility 0.28s;
z-index: 50;
}
.nav-dropdown::before {
content: '';
position: absolute;
left: 0.85rem;
right: 0.85rem;
top: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.nav-dropdown.is-wide {
min-width: 22rem;
left: 50%;
}
.nav-group:hover .nav-dropdown,
.nav-group.is-open .nav-dropdown {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateX(-50%) translateY(0) scale(1);
}
.nav-drop-section + .nav-drop-section {
margin-top: 0.7rem;
padding-top: 0.7rem;
border-top: 1px solid rgba(166, 28, 32, 0.1);
}
.nav-drop-label {
margin: 0 0.35rem 0.4rem;
font-size: 0.72rem;
letter-spacing: 0.18em;
color: var(--gold);
text-transform: none;
}
.nav-drop-list.is-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.1rem 0.35rem;
}
.nav-drop-item {
--i: 0;
position: relative;
display: flex;
align-items: center;
min-height: 2.35rem;
padding: 0 0.85rem 0 1.05rem;
font-size: 0.88rem;
color: var(--ink-soft);
white-space: nowrap;
border-radius: 0.12rem;
opacity: 0;
transform: translateY(0.35rem);
transition: color 0.22s var(--ease), background 0.22s var(--ease), transform 0.22s var(--ease);
}
.nav-group:hover .nav-drop-item,
.nav-group.is-open .nav-drop-item {
opacity: 1;
transform: translateY(0);
transition-delay: calc(var(--i) * 35ms);
}
.nav-drop-item::before {
content: '';
position: absolute;
left: 0.25rem;
top: 50%;
width: 2px;
height: 0;
background: var(--brand);
transform: translateY(-50%);
transition: height 0.22s var(--ease);
}
.nav-drop-item:hover,
.nav-drop-item.is-active {
color: var(--brand);
background: var(--brand-soft);
transform: translateX(0.15rem);
}
.nav-drop-item:hover::before,
.nav-drop-item.is-active::before {
height: 1.05rem;
}
.nav-drop-item.deep {
font-size: 0.84rem;
}
.stage-links {
max-width: 70rem;
justify-content: flex-end;
}
.mobile-group-title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 2.9rem;
border-bottom: 1px solid rgba(166, 28, 32, 0.1);
color: var(--ink);
font-size: 1.05rem;
}
.mobile-chevron {
width: 0.55rem;
height: 0.55rem;
border-right: 1.5px solid var(--brand);
border-bottom: 1.5px solid var(--brand);
transform: rotate(45deg);
transition: transform 0.3s var(--ease);
}
.mobile-group.is-open .mobile-chevron {
transform: rotate(225deg);
}
.mobile-group-panel {
max-height: 0;
overflow: hidden;
opacity: 0;
padding: 0 0 0 0.75rem;
border-left: 2px solid transparent;
transition: max-height 0.35s var(--ease), opacity 0.28s var(--ease), padding 0.28s var(--ease), border-color 0.28s;
}
.mobile-group.is-open .mobile-group-panel {
max-height: 40rem;
opacity: 1;
padding: 0.35rem 0 0.85rem 0.85rem;
border-left-color: rgba(166, 28, 32, 0.22);
}
.mobile-drop-label {
margin: 0.55rem 0 0.2rem;
font-size: 0.72rem;
letter-spacing: 0.16em;
color: var(--gold);
}
.mobile-group-panel a {
font-size: 0.95rem;
color: var(--ink-soft);
border-bottom-color: rgba(166, 28, 32, 0.06);
}
.mobile-group-panel a.is-deep {
font-size: 0.9rem;
padding-left: 0.35rem;
}
.theme-tech .nav-group-title,
.theme-tech .nav-drop-item {
color: rgba(243, 236, 228, 0.72);
}
.theme-tech .nav-group:hover .nav-group-title,
.theme-tech .nav-group.is-current .nav-group-title,
.theme-tech .nav-drop-item:hover,
.theme-tech .nav-drop-item.is-active {
color: var(--gold);
}
.theme-tech .nav-dropdown {
background: rgba(28, 25, 31, 0.98);
border-color: var(--tech-line);
}
.theme-tech .nav-drop-item::before {
background: var(--gold);
}
.theme-tech .nav-drop-item:hover,
.theme-tech .nav-drop-item.is-active {
background: rgba(196, 163, 90, 0.1);
}
.theme-tech .nav-drop-section + .nav-drop-section {
border-top-color: rgba(196, 163, 90, 0.18);
}
@media (max-width: 64rem) and (min-width: 48.0625rem) {
.nav-links {
gap: 0.1rem 0.55rem;
}
.nav-links a,
.nav-group-title {
font-size: 0.78rem;
}
.nav-groups {
gap: 0.25rem 0.65rem;
}
.nav-dropdown.is-wide {
min-width: 18rem;
}
}
+195
View File
@@ -0,0 +1,195 @@
import { NavLink, Link, useLocation } from 'react-router-dom'
import { useEffect, useState, type CSSProperties } from 'react'
import { images } from '../content/site'
import { findGroupByPath, navGroups, stageNavItems, type NavChild, type NavGroup } from '../content/nav'
import { useStageMode } from '../hooks/useStageMode'
import './Nav.css'
function withStage(path: string, isStage: boolean) {
if (!isStage) return path
if (path === '/') return '/?mode=stage'
return `${path}?mode=stage`
}
function splitChildren(children: NavChild[]) {
return {
main: children.filter((c) => c.kind === 'main'),
deep: children.filter((c) => c.kind === 'deep'),
}
}
function isWideGroup(group: NavGroup) {
return group.id === 'culture' || group.id === 'qingxiang'
}
export function Nav() {
const isStage = useStageMode()
const location = useLocation()
const [drawerOpen, setDrawerOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [openGroup, setOpenGroup] = useState<string | null>(null)
const activeGroup = findGroupByPath(location.pathname)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
useEffect(() => {
document.body.style.overflow = drawerOpen ? 'hidden' : ''
return () => {
document.body.style.overflow = ''
}
}, [drawerOpen])
useEffect(() => {
setDrawerOpen(false)
setOpenGroup(null)
}, [location.pathname, location.search])
if (isStage) {
return (
<header className={`topnav ${scrolled ? 'is-scrolled' : ''}`}>
<div className="topnav-inner container">
<Link to={withStage('/', true)} className="brand">
<img src={images.logo} alt="杜康好客" className="brand-logo" />
<span className="brand-text serif"> · </span>
</Link>
<nav className="nav-links stage-links" aria-label="展控导航">
{stageNavItems.map((item) => (
<NavLink
key={item.path}
to={withStage(item.path, true)}
end={item.path === '/'}
className={({ isActive }) => (isActive ? 'is-active' : '')}
>
{item.label}
</NavLink>
))}
</nav>
</div>
</header>
)
}
return (
<header className={`topnav ${scrolled ? 'is-scrolled' : ''} ${drawerOpen ? 'has-drawer-open' : ''}`}>
<div className="topnav-inner container">
<Link to="/" className="brand" onClick={() => setDrawerOpen(false)}>
<img src={images.logo} alt="杜康好客" className="brand-logo" />
<span className="brand-text serif"></span>
</Link>
<nav className="nav-groups desktop-only" aria-label="主导航">
{navGroups.map((group) => {
const { main, deep } = splitChildren(group.children)
const wide = isWideGroup(group)
return (
<div
key={group.id}
className={`nav-group ${activeGroup?.id === group.id ? 'is-current' : ''} ${openGroup === group.id ? 'is-open' : ''}`}
onMouseEnter={() => setOpenGroup(group.id)}
onMouseLeave={() => setOpenGroup(null)}
>
<Link to={group.entryPath} className="nav-group-title">
<span>{group.label}</span>
<span className="nav-chevron" aria-hidden />
</Link>
<div className={`nav-dropdown ${wide ? 'is-wide' : ''}`} role="menu">
<div className="nav-drop-section">
<p className="nav-drop-label"></p>
<div className="nav-drop-list">
{main.map((child, i) => (
<NavLink
key={child.path}
to={child.path}
role="menuitem"
style={{ '--i': i } as CSSProperties}
className={({ isActive }) => `nav-drop-item main ${isActive ? 'is-active' : ''}`}
>
{child.label}
</NavLink>
))}
</div>
</div>
{deep.length > 0 ? (
<div className="nav-drop-section">
<p className="nav-drop-label"></p>
<div className={`nav-drop-list ${wide ? 'is-cols' : ''}`}>
{deep.map((child, i) => (
<NavLink
key={child.path}
to={child.path}
role="menuitem"
style={{ '--i': i } as CSSProperties}
className={({ isActive }) => `nav-drop-item deep ${isActive ? 'is-active' : ''}`}
>
{child.label}
</NavLink>
))}
</div>
</div>
) : null}
</div>
</div>
)
})}
</nav>
<button
className="menu-toggle mobile-only"
type="button"
aria-expanded={drawerOpen}
aria-label={drawerOpen ? '关闭菜单' : '打开菜单'}
onClick={() => setDrawerOpen((v) => !v)}
>
<span />
<span />
<span />
</button>
</div>
<div className={`mobile-drawer mobile-only ${drawerOpen ? 'is-open' : ''}`}>
<nav aria-label="移动导航">
{navGroups.map((group) => {
const expanded = openGroup === group.id
const { main, deep } = splitChildren(group.children)
return (
<div key={group.id} className={`mobile-group ${expanded ? 'is-open' : ''}`}>
<button
type="button"
className="mobile-group-title"
aria-expanded={expanded}
onClick={() => setOpenGroup(expanded ? null : group.id)}
>
<span>{group.label}</span>
<i className="mobile-chevron" aria-hidden />
</button>
<div className="mobile-group-panel">
<p className="mobile-drop-label"></p>
{main.map((child) => (
<NavLink key={child.path} to={child.path} onClick={() => setDrawerOpen(false)}>
{child.label}
</NavLink>
))}
{deep.length > 0 ? (
<>
<p className="mobile-drop-label"></p>
{deep.map((child) => (
<NavLink key={child.path} to={child.path} className="is-deep" onClick={() => setDrawerOpen(false)}>
{child.label}
</NavLink>
))}
</>
) : null}
</div>
</div>
)
})}
</nav>
</div>
</header>
)
}
+35
View File
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react'
import { Reveal } from './Reveal'
type PageHeroProps = {
eyebrow: string
title: string
lead: string
image?: string
imageAlt?: string
children?: ReactNode
}
export function PageHero({ eyebrow, title, lead, image, imageAlt = '', children }: PageHeroProps) {
return (
<section className="page-section tight">
<div className="container">
<div className={`split ${image ? '' : 'single'}`.trim()}>
<Reveal variant="anim-rise">
<p className="eyebrow">{eyebrow}</p>
<h1 className="page-title">{title}</h1>
<p className="lead">{lead}</p>
{children}
</Reveal>
{image ? (
<Reveal variant="anim-zoom" delayMs={120}>
<div className="media-frame">
<img src={image} alt={imageAlt} loading="lazy" />
</div>
</Reveal>
) : null}
</div>
</div>
</section>
)
}
+48
View File
@@ -0,0 +1,48 @@
import { useEffect, useRef, type ReactNode } from 'react'
type RevealProps = {
children: ReactNode
className?: string
/** Custom animation class applied when in view, e.g. anim-slide-left */
variant?: string
delayMs?: number
}
export function Reveal({ children, className = '', variant = 'anim-rise', delayMs = 0 }: RevealProps) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const node = ref.current
if (!node) return
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
node.classList.add('is-in')
return
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
node.classList.add('is-in')
observer.unobserve(node)
}
})
},
{ threshold: 0.16, rootMargin: '0px 0px -6% 0px' },
)
observer.observe(node)
return () => observer.disconnect()
}, [])
return (
<div
ref={ref}
className={`reveal-base ${variant} ${className}`.trim()}
style={delayMs ? { transitionDelay: `${delayMs}ms` } : undefined}
>
{children}
</div>
)
}
+34
View File
@@ -0,0 +1,34 @@
export const deepImages = {
brand: [
'/assets/brand/emb_p6_5.png',
'/assets/brand/emb_p31_1.png',
'/assets/brand/emb_p15_3.png',
'/assets/brand/emb_p33_1.png',
'/assets/brand/photo_p11_1.jpg',
'/assets/brand/photo_p8_4.jpg',
],
poetry: [
'/assets/poetry/emb_p33_4.png',
'/assets/poetry/emb_p49_4.png',
'/assets/poetry/emb_p59_4.png',
'/assets/poetry/emb_p67_4.png',
'/assets/poetry/emb_p25_4.png',
'/assets/poetry/emb_p66_4.png',
],
qingxiang: [
'/assets/qingxiang/emb_p7_5.png',
'/assets/qingxiang/emb_p6_8.png',
'/assets/qingxiang/emb_p7_4.png',
'/assets/qingxiang/emb_p12_2.png',
'/assets/qingxiang/emb_p7_6.png',
'/assets/qingxiang/emb_p6_2.png',
'/assets/t30-bottle.png',
],
}
export function resolveStageRedirect(pathname: string): string | null {
if (pathname.startsWith('/culture/')) return '/brand'
if (pathname.startsWith('/poetry/') && pathname !== '/poetry') return '/poetry'
if (pathname.startsWith('/qingxiang/') && pathname !== '/qingxiang') return '/qingxiang'
return null
}
+307
View File
@@ -0,0 +1,307 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const B = deepImages.brand
/** Deep-page images are unique across culture pages (no reuse). */
export const culturePages: DeepPageContent[] = [
{
path: '/culture/status',
group: 'culture',
navLabel: '文化地位',
eyebrow: '品牌传承 · 深读',
title: '杜康文化在中国酒文化中的地位',
lead: '杜康,是中国秫酒的「酒宗」「酒祖」。杜康造酒,不仅肇始了华夏酒业的辉煌诞生,铸就了酒行业顶礼膜拜的精神图腾,而且创造了行业特征与地域色彩交相辉映的杜康文化。',
layout: 'essay',
images: [B[0]],
blocks: [
{
type: 'paragraph',
text: '经过上下五千年的岁月积淀,这种文化形态所蕴含的思想观念、人文情怀、道德规范和意志品质,已成为中国酒文化的标志性符号。历史典籍的记载奠定了杜康的酒祖地位,使杜康文化成为中国酒文化的「精神原点」。',
},
{
type: 'heading',
text: '酒神的诞生',
},
{
type: 'paragraph',
text: '从谷物发酵到文明觉醒,杜康文化被赋予通神媒介,成为礼制变迁的物化见证。酒祖杜康被世人尊崇为「酒神」,是杜康文化区别于其他酒文化的主要特征。世人敬奉杜康的记载始见于隋唐。继隋人王绩的《祭杜康新庙文》之后,关于「酒神」的颂词便不断地见诸文人笔下。',
},
{
type: 'quote',
text: '他年血食汝,当配杜康神。',
source: '宋 · 苏轼《饮酒四首》',
},
{
type: 'paragraph',
text: '在民间,对杜康的崇拜以及和酒神对话的宗教意识更具生命力。酒祖故里的杜康祠祭祀酒祖的完整仪轨,酿酒作坊供奉杜康神像的传统已延续千年。杜康封坛仪式、祭祖大典等传统习俗一直保持着上古酒祭的遗风。',
},
{
type: 'heading',
text: '酒以成礼',
},
{
type: 'paragraph',
text: '杜康文化衍化出的「饮惟祀」的酒德观念,揭示了「国之大事,在祀在戎」的深层关联,塑造了中国人「酒以成礼」的文明传统。这位「始作秫酒」的传奇人物,早已超越了具体的历史人物形象,演变为民族创造力的精神图腾。',
},
{
type: 'paragraph',
text: '因而,理解杜康文化,不能只停留在「一个酒名」的层面。它是典籍立名、祠祀传承、诗酒互证与当代产业复兴共同编织的文化系统——既属于历史,也属于仍在杯盏间延续的日常。',
},
],
closing: '站在今日回望,杜康如一坛陈年佳酿——愈久愈醇,愈传愈清。',
},
{
path: '/culture/person',
group: 'culture',
navLabel: '杜康其人',
eyebrow: '品牌传承 · 深读',
title: '杜康其人',
lead: '先秦典籍《世本》载「杜康作秫酒」;东汉许慎《说文解字》谓:「古者少康初作箕帚、秫酒。少康,杜康也。」酒之所兴,肇自上皇。',
layout: 'person',
images: [B[1]],
blocks: [
{
type: 'paragraph',
text: '关于杜康的生平,历代文献记载虽有异说,但其「始作秫酒」的文化身份却高度一致。杜康既是人名,也逐渐成为地名与酒名——三重身份相互映照,构成中国酒文化最独特的符号结构。',
},
{
type: 'heading',
text: '典籍中的酒祖',
},
{
type: 'paragraph',
text: '《世本》将杜康写入文明源头叙事;《说文解字》以语言学佐证「少康即杜康」。后世史家、文人不断追述,使杜康从传说人物上升为可被祭祀、可被吟咏、可被继承的文化共主。',
},
{
type: 'list',
items: [
'人名:始酿秫酒的传奇人物',
'地名:杜康村、杜康泉、杜康仙庄等地理记忆',
'酒名:杜康成为清香美酒与诗酒意象的通行符号',
],
},
{
type: 'heading',
text: '从人物到符号',
},
{
type: 'paragraph',
text: '当「杜康」同时指向人、地、酒,它便不再只是传记对象,而是一套可被世代调用的文化语法。正因如此,走进杜康,从来不只是认识一个人,而是进入一整套关于粮食、礼乐、故土与诗心的文明系统。',
},
],
},
{
path: '/culture/brew',
group: 'culture',
navLabel: '杜康造酒',
eyebrow: '品牌传承 · 深读',
title: '杜康造酒',
lead: '西晋江统《酒诰》云:酒之所兴,肇自上皇;或云仪狄,一曰杜康。有饭不尽,委余空桑,郁积成味,久蓄气芳——本出于此,不由奇方。',
layout: 'timeline',
images: [B[2]],
blocks: [
{
type: 'list',
items: [
'余饭委空桑:偶然的发酵,成为文明对自然法则的觉悟',
'以秫为骨:原料之纯,决定酒体之正',
'以曲为魂:曲药生香,技艺传续',
'清蒸二次清:后世清香路径仍可辨认的工艺基因',
'从作坊到祠祀:造酒升华为礼乐与祭祀的文明工程',
],
},
{
type: 'heading',
text: '空桑造酒',
},
{
type: 'paragraph',
text: '「空桑造酒」的叙事,把偶然的余饭发酵,升华为文明对自然法则的觉悟。杜康并非炫技的奇方发明者,而是把「以秫为骨、以曲为魂」的智慧带入人间日常的那一位。',
},
{
type: 'quote',
text: '有饭不尽,委余空桑,郁积成味,久蓄气芳。',
source: '晋 · 江统《酒诰》',
},
{
type: 'paragraph',
text: '秫酒传统强调原料之纯与工艺之清。清蒸、清糟、清烧的路径,在后世清香型白酒中仍清晰可辨。杜康造酒遗址公园内,至今仍生长着被称为「酒树」的古树,枝繁叶茂,见证酒兴千年。',
},
],
closing: '技进乎道,香传千年。',
},
{
path: '/culture/temple',
group: 'culture',
navLabel: '杜康祠',
eyebrow: '品牌传承 · 深读',
title: '杜康祠',
lead: '杜康因始创秫酒被称为酒祖。唐宋以来,杜康祠、杜康庙遍布各地;古人敬酒祖、拜酒神,多在杜康祠内完成。',
layout: 'shrine',
images: [B[3]],
blocks: [
{
type: 'paragraph',
text: '杜康造酒遗址公园内的杜康祠,为当今规模大、保留完整的古建筑群之一。祠祀仪轨延续封坛祭祖的遗风,使「饮惟祀」的酒德观念可触可感。',
},
{
type: 'heading',
text: '祠祀与诗心',
},
{
type: 'paragraph',
text: '苏轼有「从今东坡室,不立杜康祀」之句,正说明杜康祠祀在文人精神世界中的位置——可立可罢,却始终是心灵秩序的参照。陆游「堂上祠杜康」、周淑禧《杜康庙》等作品,亦不断加固这一文化记忆。',
},
{
type: 'list',
items: ['封坛祭祖:延续上古酒祭遗风', '供奉神像:酿酒作坊千年传统', '诗酒互证:祠祀进入文人书写'],
},
{
type: 'paragraph',
text: '走进杜康祠,看见的不只是建筑与香火,更是一条把「酒」抬升为「礼」的文化路径:敬畏粮食、敬畏创造、敬畏可共享的人间秩序。',
},
],
},
{
path: '/culture/sites',
group: 'culture',
navLabel: '造酒遗址',
eyebrow: '品牌传承 · 深读',
title: '杜康造酒遗址',
lead: '杜康村、杜康泉、空桑涧……酒祖故里的山水与作坊,把「故土」写成可以走入的文化现场。',
layout: 'sites',
images: [B[4]],
blocks: [
{
type: 'heading',
text: '杜康村',
},
{
type: 'paragraph',
text: '村落因酒祖得名,也因酒香延续。山水环抱之中,作坊、祠庙与口述传统并存,构成活态的遗产空间。访客在巷陌与田野间行走,便是在阅读一部不用翻页的酒史。',
},
{
type: 'heading',
text: '杜康泉',
},
{
type: 'paragraph',
text: '泉脉滋养酿造,也滋养地方记忆。好水出好酒,在酒祖叙事里不是广告语,而是地理与工艺相互成就的常识。泉、曲、粮、火,共同写下清香的底色。',
},
{
type: 'heading',
text: '遗址中的文明证据',
},
{
type: 'paragraph',
text: '从空桑传说到全国重点文物保护单位的碑刻与陵寝记忆,故里把「可说的史」与「可见的物」叠合在一起,让访客在行走中完成对酒祖的确认。',
},
],
},
{
path: '/culture/figures',
group: 'culture',
navLabel: '名人与杜康',
eyebrow: '品牌传承 · 深读',
title: '名人与杜康',
lead: '三曹、苏轼、邵雍,乃至近现代政治人物与杜康的交集,使酒祖叙事不断获得新的时代回声。',
layout: 'figures',
images: [B[5]],
blocks: [
{
type: 'heading',
text: '三曹与杜康',
},
{
type: 'paragraph',
text: '曹操《短歌行》「何以解忧,唯有杜康」一出,杜康从酒祖之名跃升为诗心密码。曹植等作品亦把酒与志向、生命感喟相连,奠定魏晋诗性转化的基调。',
},
{
type: 'heading',
text: '苏轼与杜康',
},
{
type: 'paragraph',
text: '苏轼一生爱酒,也爱杜康。贬谪海南途中,仍以杜康为兄弟相别、自我安顿的意象——「从今东坡室,不立杜康祀」的旷达背后,是诗酒对命运的温柔对抗。',
},
{
type: 'heading',
text: '时代回响',
},
{
type: 'paragraph',
text: '从邵雍安乐窝的哲思,到当代「复兴杜康」的历史批示记忆,名人与杜康的对话从未停止。酒祖因此始终活在公共文化的现场,而不只沉睡在典籍里。',
},
],
},
{
path: '/culture/chronicle',
group: 'culture',
navLabel: '大事记',
eyebrow: '品牌传承 · 深读',
title: '杜康大事记',
lead: '从先秦典籍立名,到魏晋诗性转化,再到祠祀千年与当代品牌复兴——时间本身,就是最好的陈酿。',
layout: 'timeline',
images: [],
blocks: [
{
type: 'list',
items: [
'先秦:典籍记载「杜康作秫酒」,酒祖身份确立',
'魏晋:曹操《短歌行》完成诗性转化,杜康进入文人精神世界',
'隋唐:祭文与祠庙推动酒神崇拜,杜康可祀可敬',
'唐宋以降:杜康祠庙广布,文人吟咏与地方祭祀并行',
'近现代:故里保护、产业复兴与文化传播相互推动',
'当代:酒祖杜康清香品牌复兴,好客机制让千年清香可落地',
],
},
{
type: 'heading',
text: '香传的线索',
},
{
type: 'paragraph',
text: '大事记不是年表的堆砌,而是一条「香传」的线索:每一次被书写、被祭祀、被酿造,都是杜康文化的再一次醒酒。沿着时间往前走,也能看见清香如何从典籍走入杯盏,从纪念走入日常。',
},
],
closing: '先秦立名 → 魏晋诗化 → 祠祀千年 → 当代复兴',
},
{
path: '/culture/factory',
group: 'culture',
navLabel: '酒厂介绍',
eyebrow: '品牌传承 · 深读',
title: '酒厂介绍',
lead: '山西杜康酒业扎根杏花村千年酿酒圣地,与清香名酒同源同脉,一清到底。',
layout: 'factory',
images: [],
blocks: [
{
type: 'paragraph',
text: '酒厂承续酒祖故里与产区生态,以清香工艺与品质体系服务宴请与日常多场景。品牌不只卖酒,更把「酒祖叙事 + 清香表达 + 好客机制」做成可感知的体验。',
},
{
type: 'heading',
text: '产区与使命',
},
{
type: 'paragraph',
text: '杏花村一带的酿酒微气候与生态,为清香提供了稳定的地理底座。企业以「坚守纯粹品质,悦享清逸人生」为使命句,把品质、体验与城市运营放在同一条链路里。',
},
{
type: 'list',
items: [
'产区:杏花村一带酿酒生态与微气候',
'工艺:清香「7321」路径,讲究清蒸二次清',
'体验:文化馆、品鉴与溯源之旅可走入',
'使命:坚守纯粹品质,悦享清逸人生',
],
},
{
type: 'paragraph',
text: '当酒厂资源与城市运营、门店核销连成闭环,酒祖的清香便从典籍走进餐桌,从纪念走进日常。',
},
],
},
]
+126
View File
@@ -0,0 +1,126 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const P = deepImages.poetry
/** Unique images among poetry deep pages. */
export const poetryDeepPages: DeepPageContent[] = [
{
path: '/poetry/anthology',
group: 'poetry',
navLabel: '诗酒选萃',
eyebrow: '诗酒文化 · 深读',
title: '酒香醉天下,千年诗未央',
lead: '「诗酒」二字,是中国人心里最动人的共鸣。以杜康为意象创作的诗词曲赋,名家荟萃:曹操、曹植、杜甫、白居易、苏轼、辛弃疾……把一抹醇香写进东方美学。',
layout: 'poem-wall',
images: [P[0]],
blocks: [
{
type: 'paragraph',
text: '《杜康古诗词曲赋译注》精选汉代至清代关于杜康的韵文作品数十篇。走进这些篇章,才能体味先贤对生命、创造与宿命的感悟。诗与酒彼此成全:酒给诗以温度,诗给酒以永恒。',
},
{
type: 'heading',
text: '主诗签 · 曹操《短歌行》',
},
{
type: 'quote',
text: '对酒当歌,人生几何?譬如朝露,去日苦多。……何以解忧,唯有杜康。',
source: '东汉 · 曹操《短歌行》',
},
{
type: 'paragraph',
text: '一边饮酒一边高歌,一边感慨叹息:人生短促,像晨露转瞬即逝。什么可以消除心中的忧愁?唯有杜康美酒。魏武帝以此完成从酒祖之名到诗心密码的决定性转化。',
},
{
type: 'heading',
text: '辅诗签 · 苏轼',
},
{
type: 'quote',
text: '他年血食汝,当配杜康神。',
source: '宋 · 苏轼',
},
{
type: 'heading',
text: '辅诗签 · 白居易',
},
{
type: 'quote',
text: '不似杜康神用速,十分一盏便开眉。',
source: '唐 · 白居易',
},
{
type: 'paragraph',
text: '白居易写杜康酒能使人愁苦尽消;十分之一杯便足以开眉展颜——神速、开眉,是唐人对杜康最亲切的体认。',
},
{
type: 'heading',
text: '辅诗签 · 辛弃疾',
},
{
type: 'quote',
text: '杜康初筮,正得云雷。',
source: '宋 · 辛弃疾',
},
{
type: 'paragraph',
text: '从魏武的感喟,到唐人的开眉,再到宋人的神思——杜康完成了从「酒祖」到「诗心密码」的转化,成为文人精神世界的通行意象。',
},
],
closing: '斟半盏清醑,展卷轻吟:让酒香携着古人的魂魄,穿越千年与我们共醉。',
},
{
path: '/poetry/tang',
group: 'poetry',
navLabel: '唐宋余韵',
eyebrow: '诗酒文化 · 深读',
title: '唐宋余韵:开眉与神思',
lead: '唐人开眉,宋人神思。杜康在不同朝代被反复调用,成为情绪、命运与礼赞的公共修辞。',
layout: 'ink-scroll',
images: [P[1]],
blocks: [
{
type: 'heading',
text: '江统《酒诰》',
},
{
type: 'quote',
text: '酒之所兴,肇自上皇。或云仪狄,一曰杜康。有饭不尽,委余空桑,郁积成味,久蓄气芳。本出于此,不由奇方。',
source: '晋 · 江统',
},
{
type: 'paragraph',
text: '这篇文献把造酒还原为自然与日用的相遇,也为后世「空桑造酒」图像提供了经典出处。它提醒我们:杜康之美,首先来自对日常与自然的诚实。',
},
{
type: 'heading',
text: '王绩《祭杜康新庙文》',
},
{
type: 'paragraph',
text: '隋唐之际,祭文把杜康正式推上「可祀」的神位。酒祖不仅可饮可咏,更可敬可拜——诗酒文化由此与祠祀传统合流。庙与诗、香火与墨迹,共同把杜康写进公共记忆。',
},
{
type: 'heading',
text: '苏黄辛陆的回声',
},
{
type: 'paragraph',
text: '苏轼、张耒、晁说之、陆游等不断以杜康入诗。或配神,或皈依,或庙里做忠臣——杜康成为文人自我安顿时最顺手的一杯。',
},
{
type: 'list',
items: [
'唐:开眉、神用速,贴近日常情感',
'宋:配神、初筮云雷,指向命运与哲思',
'后世:祭庙、踏青、题咏,杜康成为可游可祀的文化现场',
],
},
{
type: 'paragraph',
text: '读唐宋余韵,是在听一条悠长的回声:每个时代都借杜康说出自己的忧与喜。酒未冷,诗未央。',
},
],
},
]
+273
View File
@@ -0,0 +1,273 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const Q = deepImages.qingxiang
/**
* Unique images across qingxiang deep pages:
* project=Q5, style=Q3, experience=Q1, tour=Q0 hall
* position / tasting = text-only
* Q2/Q4/Q6 reserved for main landing (hall/storefront/bottle)
*/
export const qingxiangDeepPages: DeepPageContent[] = [
{
path: '/qingxiang/project',
group: 'qingxiang',
navLabel: '项目与赛道',
eyebrow: '清香宝典 · 深读',
title: '清香是未来白酒行业的黄金赛道',
lead: '浓香内卷、酱香回落的时代,清香正成为确定性更强的黄金赛道。酒祖杜康扎根山西杏花村——千年酒都,与清香名酒同源同脉,一清到底。',
layout: 'manifesto',
images: [Q[5]],
blocks: [
{
type: 'paragraph',
text: '山西杜康酒业有限公司以酒祖杜康清香系列产品为经营主线,位于中国白酒名镇山西汾阳。产区与名酒企业同脉相邻,共享酿酒微气候与生态。选择清香,是选择一条更干净、更可持续、更适配当代饮酒场景的路径。',
},
{
type: 'heading',
text: '杜康清香模式',
},
{
type: 'paragraph',
text: '以杜康文化为核心,以沉浸式体验为营销抓手,把社群变成增长引擎。一套可复制的体验闭环:一座杜康文化馆,一瓶杜康清香酒,一桌杜康好客宴,一群杜康粉丝圈。',
},
{
type: 'list',
items: [
'文化馆:可参观、可讲解、可品鉴',
'产品:清香表达清晰,适配宴请与日常',
'宴席:酒餐同赴,把体验落在餐桌上',
'社群:口碑裂变,让清香可传播',
],
},
{
type: 'paragraph',
text: '赛道判断要落到可运营的模式上。清香不只是风味标签,更是「文化可讲、产品可品、宴请可落、社群可传」的系统能力。',
},
],
},
{
path: '/qingxiang/position',
group: 'qingxiang',
navLabel: '定位与价值',
eyebrow: '清香宝典 · 深读',
title: '杜康清香的定位与使命',
lead: '承千年清韵,铸清香白酒标杆企业。守古法清酿,酿纯粹绵柔清雅好酒。',
layout: 'matrix',
images: [],
blocks: [
{
type: 'heading',
text: '四维定位',
},
{
type: 'list',
items: [
'企业定位:承千年清韵,铸清香白酒标杆企业',
'品质定位:守古法清酿,酿纯粹绵柔清雅好酒',
'品牌定位:传酒祖清魂,塑高端清香文化名酒',
'营销定位:拓清香赛道,引全民清雅饮酒新风尚',
],
},
{
type: 'heading',
text: '价值观',
},
{
type: 'list',
items: [
'对产品:真材实料,好酒说话',
'对客户:以客为尊,让好客也能赚钱',
'对员工:成长与成果共享',
'对股东:稳健经营,可持续回报',
],
},
{
type: 'heading',
text: '使命',
},
{
type: 'paragraph',
text: '使命句:坚守纯粹品质,悦享清逸人生。定位不是口号墙,而是产品、体验与合作机制共同兑现的承诺边界。',
},
],
},
{
path: '/qingxiang/style',
group: 'qingxiang',
navLabel: '风格与工艺',
eyebrow: '清香宝典 · 深读',
title: '绵 · 净 · 雅,与清香五大法宝',
lead: '粮香纯正无杂味,清香干净通透;入口绵柔顺滑,落口爽利干脆;杂醇含量低,不口干、不上头,醒酒快。',
layout: 'triad',
images: [Q[3]],
blocks: [
{
type: 'heading',
text: '绵',
},
{
type: 'paragraph',
text: '入口绵柔顺滑,余味悠长。宴请与日常都需要「喝得下去、坐得住」的体感,绵是清香最易亲近的第一印象。',
},
{
type: 'heading',
text: '净',
},
{
type: 'paragraph',
text: '清香通透,一清到底,无杂味杂香。「净」是工艺自信:清蒸二次清的路径,让干净成为可被验证的风格。',
},
{
type: 'heading',
text: '雅',
},
{
type: 'paragraph',
text: '低刺激、多场景,商务与雅集皆宜。雅不是疏离,而是让饮酒回到从容与体面。',
},
{
type: 'heading',
text: '工艺要点',
},
{
type: 'paragraph',
text: '大麦、豌豆、高粱三种原料;清香「7321」工艺,讲究「清蒸二次清」「一清到底」,味道更醇厚。',
},
{
type: 'heading',
text: '五大法宝',
},
{
type: 'list',
items: [
'产地:扎根杏花村千年酿酒圣地,同源同脉',
'基因:与龙头清香企业同脉同艺的品质参照',
'产能与粮源:稳定供给,品质可控',
'团队:专业品评与全链路品控',
'资本与体系:支撑产品规划与渠道建设',
],
},
],
closing: '千年杜康,醉美清香。',
},
{
path: '/qingxiang/tasting',
group: 'qingxiang',
navLabel: '品鉴玩法',
eyebrow: '清香宝典 · 深读',
title: '抿 · 咂 · 呵,与更多品鉴玩法',
lead: '一场好的品鉴,不只是喝,而是把感官、故事与互动编织在一起。',
layout: 'steps',
images: [],
blocks: [
{
type: 'list',
items: [
'抿——微启触香,品粮香本味',
'咂——舌卷含香,品绵柔入喉',
'呵——缓缓呵气,品回甘生津',
],
},
{
type: 'heading',
text: '观色 · 闻香 · 品味 · 余味',
},
{
type: 'paragraph',
text: '观色:清亮透明微黄。闻香:粮香纯正清爽。品味:绵柔爽净,一清到底。余味:干净悠长,不上头。把感官步骤说清楚,用户才敢相信「好喝」不是推销。',
},
{
type: 'heading',
text: '更多玩法',
},
{
type: 'paragraph',
text: '地动山摇、肌肤相亲、酒王大赛、看酒花观挂杯、背标法等,让用户亲手证明「好不好喝」,比千言万语更有说服力。一次沉浸体验,往往胜过十次广告。',
},
],
},
{
path: '/qingxiang/experience',
group: 'qingxiang',
navLabel: '沉浸体验',
eyebrow: '清香宝典 · 深读',
title: '三级沉浸式体验与品鉴会',
lead: '场景体验是用户快速认知的重要武器,也是制造口碑、传播引爆的发源地。',
layout: 'scenes',
images: [Q[1]],
blocks: [
{
type: 'heading',
text: '三级体验',
},
{
type: 'list',
items: [
'酒厂文化溯源之旅:参观酿造、包装、酒库与遗址',
'杜康清香文化馆:沉浸式产品与文化体验',
'品鉴会 123:一场品鉴宴、两支宣传片、三本手册',
],
},
{
type: 'heading',
text: '文化馆「五个一」',
},
{
type: 'list',
items: [
'一尊酒祖像',
'一幅画(空桑秽饭图)',
'一首诗歌(《短歌行》)',
'一座杜康祠',
'一份伟大的批示记忆:复兴杜康,为国争光',
],
},
{
type: 'heading',
text: '体验闭环',
},
{
type: 'paragraph',
text: '品鉴结束要闭环:发朋友圈、加好友、确认回访。体验不是热闹一场,而是把信任留下来。',
},
],
},
{
path: '/qingxiang/tour',
group: 'qingxiang',
navLabel: '文化馆与溯源',
eyebrow: '清香宝典 · 深读',
title: '文化馆体验与酒厂溯源',
lead: '把杜康故事做成可走入的空间:讲解、观影、玩法、DIY 调酒与品鉴宴,一气呵成。',
layout: 'tour',
images: [Q[0]],
blocks: [
{
type: 'list',
items: [
'杜康文化讲解(约 10 分钟)',
'观看杜康清香宣传片(约 10 分钟)',
'白酒鉴赏玩法 23 种',
'DIY 自调酒:留下专属标签',
'品鉴宴席与盲品',
'领导总结与欢送',
],
},
{
type: 'heading',
text: '溯源之旅',
},
{
type: 'paragraph',
text: '走进厂区与遗址,看见粮、曲、窖、酒的真实路径。清香的「一清到底」,最终要在现场被看见、被闻到、被记住。空间叙事与产线真实,共同完成信任交付。',
},
{
type: 'paragraph',
text: '文化馆不是展板堆砌,而是把酒祖、清香与好客连成可跟随的路径。每一次到访,都应留下可复述的故事与可回访的关系。',
},
],
closing: '让每一次到访,都成为一次清雅之约。',
},
]
+39
View File
@@ -0,0 +1,39 @@
export type DeepBlock =
| { type: 'paragraph'; text: string }
| { type: 'quote'; text: string; source?: string }
| { type: 'list'; items: string[] }
| { type: 'heading'; text: string }
/** Distinct page shells — not just a CSS rename of one template */
export type DeepLayoutKind =
| 'essay'
| 'person'
| 'timeline'
| 'shrine'
| 'sites'
| 'figures'
| 'factory'
| 'poem-wall'
| 'ink-scroll'
| 'manifesto'
| 'matrix'
| 'triad'
| 'steps'
| 'scenes'
| 'tour'
export type DeepPageContent = {
path: string
group: 'culture' | 'poetry' | 'qingxiang'
navLabel: string
eyebrow: string
title: string
lead: string
/** @deprecated kept for theme hints; prefer layout */
variant?: 'classic' | 'scroll' | 'stage' | 'river'
layout: DeepLayoutKind
/** Unique across all deep pages; empty = text-only */
images: string[]
blocks: DeepBlock[]
closing?: string
}
+94
View File
@@ -0,0 +1,94 @@
import { culturePages } from './deep/culture'
import { poetryDeepPages } from './deep/poetry'
import { qingxiangDeepPages } from './deep/qingxiang'
import type { DeepPageContent } from './deep/types'
export const allDeepPages: DeepPageContent[] = [...culturePages, ...poetryDeepPages, ...qingxiangDeepPages]
export const deepPageMap = Object.fromEntries(allDeepPages.map((p) => [p.path, p])) as Record<
string,
DeepPageContent
>
export type NavChild = { label: string; path: string; kind: 'main' | 'deep' }
export type NavGroup = {
id: string
label: string
/** first landing when clicking group title on desktop */
entryPath: string
children: NavChild[]
}
export const navGroups: NavGroup[] = [
{
id: 'culture',
label: '酒祖文化',
entryPath: '/brand',
children: [
{ label: '品牌传承', path: '/brand', kind: 'main' },
{ label: '诗酒文化', path: '/poetry', kind: 'main' },
...culturePages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
...poetryDeepPages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
],
},
{
id: 'qingxiang',
label: '清香臻品',
entryPath: '/qingxiang',
children: [
{ label: '清香', path: '/qingxiang', kind: 'main' },
...qingxiangDeepPages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
],
},
{
id: 'haoke',
label: '好客体系',
entryPath: '/haoke',
children: [
{ label: '好客模式', path: '/haoke', kind: 'main' },
{ label: '三端协同', path: '/system', kind: 'main' },
],
},
{
id: 'tech',
label: '科技实力',
entryPath: '/tech',
children: [
{ label: '科技驱动', path: '/tech', kind: 'main' },
{ label: '酒厂实力', path: '/subsidy', kind: 'main' },
{ label: '科技板块', path: '/tech-detail', kind: 'main' },
],
},
{
id: 'contact',
label: '合作联系',
entryPath: '/partner',
children: [
{ label: '城市合伙人', path: '/partner', kind: 'main' },
{ label: '联系我们', path: '/contact', kind: 'main' },
],
},
]
/** flat main modules for stage mode */
export const stageNavItems = [
{ path: '/', label: '首页' },
{ path: '/brand', label: '品牌传承' },
{ path: '/poetry', label: '诗酒文化' },
{ path: '/qingxiang', label: '清香' },
{ path: '/haoke', label: '好客模式' },
{ path: '/system', label: '三端协同' },
{ path: '/tech', label: '科技驱动' },
{ path: '/subsidy', label: '酒厂实力' },
{ path: '/partner', label: '城市合伙人' },
{ path: '/tech-detail', label: '科技板块' },
{ path: '/contact', label: '联系我们' },
]
export function findGroupByPath(pathname: string) {
return navGroups.find((g) =>
g.children.some((c) => pathname === c.path || (c.path !== '/' && pathname.startsWith(`${c.path}/`))),
)
}
+327
View File
@@ -0,0 +1,327 @@
export const navItems = [
{ path: '/brand', label: '品牌传承' },
{ path: '/poetry', label: '诗酒文化' },
{ path: '/qingxiang', label: '清香' },
{ path: '/haoke', label: '好客模式' },
{ path: '/system', label: '三端协同' },
{ path: '/tech', label: '科技驱动' },
{ path: '/subsidy', label: '酒厂实力' },
{ path: '/partner', label: '城市合伙人' },
{ path: '/tech-detail', label: '科技板块' },
{ path: '/contact', label: '联系我们' },
] as const
export const hero = {
brand: '酒祖杜康',
subBrand: '杜康好客',
title: '千年酒祖,一清到底',
subtitle:
'你请客,我买单。买酒送等价好客权益,到合作门店抵餐——让酒与席,同赴一场清雅之约。',
primaryCta: { label: '走进酒祖杜康', to: '/brand' },
secondaryCta: { label: '了解城市合伙人', to: '/partner' },
}
export const brand = {
eyebrow: '品牌传承',
title: '酒祖杜康:中国酒文化的精神原点',
lead:
'杜康,被尊为秫酒「酒宗」「酒祖」,更被世人敬奉为「酒神」。从典籍到诗篇,从故里到祠祀,杜康不只是一杯酒的名字——它是华夏酿造文明的精神图腾。',
cards: [
{
title: '典籍立祖',
text: '《世本》载「杜康作秫酒」;《说文解字》谓「少康,杜康也」。酒之所兴,肇自上皇——杜康造酒,写入中华文明的源头叙事。',
},
{
title: '故里承源',
text: '杜康村、杜康泉、空桑涧……酒祖故里的山水与作坊,延续着「以秫为骨、以曲为魂」的古老智慧,让技进乎道,香传千年。',
},
{
title: '酒以成礼',
text: '从封坛祭祖到「饮惟祀」的酒德观念,杜康文化把粮食之精,升华为礼乐之器——国之大事,在祀在戎,酒以成礼。',
},
],
timeline: ['先秦典籍立名', '魏晋诗性转化', '杜康祠祀千年', '当代酒祖品牌复兴'],
closing: '站在今日回望,杜康如一坛陈年佳酿——愈久愈醇,愈传愈清。',
deepReads: [
{ label: '文化地位', path: '/culture/status', blurb: '酒宗酒祖与酒神崇拜,何以成为中国酒文化精神原点。' },
{ label: '杜康其人', path: '/culture/person', blurb: '人名·地名·酒名:三重身份如何写成文明符号。' },
{ label: '杜康造酒', path: '/culture/brew', blurb: '空桑造酒与以秫为骨、以曲为魂的工艺叙事。' },
{ label: '杜康祠', path: '/culture/temple', blurb: '祠祀仪轨与「饮惟祀」的酒德传统。' },
{ label: '造酒遗址', path: '/culture/sites', blurb: '杜康村、杜康泉与可走入的故里现场。' },
{ label: '名人与杜康', path: '/culture/figures', blurb: '从三曹到苏轼,诗心与酒祖的对话。' },
{ label: '大事记', path: '/culture/chronicle', blurb: '先秦立名到当代复兴的香传线索。' },
{ label: '酒厂介绍', path: '/culture/factory', blurb: '杏花村产区、清香工艺与可感知体验。' },
],
}
export const poetry = {
eyebrow: '诗酒文化',
title: '何以解忧,唯有杜康',
lead:
'「诗酒」二字,是中国人心里最动人的共鸣。千载光阴里,曹操、杜甫、白居易、苏轼、辛弃疾……无数名家以杜康为意象,把一抹醇香写进东方美学。',
mainPoem: {
quote: '对酒当歌,人生几何?……何以解忧,唯有杜康。',
source: '东汉 · 曹操《短歌行》',
},
poems: [
{ quote: '他年血食汝,当配杜康神。', source: '宋 · 苏轼' },
{ quote: '不似杜康神用速,十分一盏便开眉。', source: '唐 · 白居易' },
{ quote: '杜康初筮,正得云雷。', source: '宋 · 辛弃疾' },
],
reading:
'从魏武的感喟,到唐人的开眉,再到宋人的神思——杜康完成了从「酒祖」到「诗心密码」的转化,成为文人精神世界的通行意象。',
closing: '斟半盏清醑,展卷轻吟:让酒香携着古人的魂魄,穿越千年与我们共醉。',
deepReads: [
{ label: '诗酒选萃', path: '/poetry/anthology', blurb: '主诗签与辅诗签:曹操、苏轼、白居易、辛弃疾。' },
{ label: '唐宋余韵', path: '/poetry/tang', blurb: '《酒诰》、祭文与开眉神思的时代回声。' },
],
}
export const qingxiang = {
eyebrow: '清香宝典',
title: '千年杜康,醉美清香',
lead:
'浓香内卷、酱香回落的时代,清香正成为确定性更强的黄金赛道。酒祖杜康扎根山西杏花村——千年酒都,与清香名酒同源同脉,一清到底。',
positions: [
{ title: '企业定位', text: '承千年清韵,铸清香白酒标杆企业。' },
{ title: '品质定位', text: '守古法清酿,酿纯粹绵柔清雅好酒。' },
{ title: '品牌定位', text: '传酒祖清魂,塑高端清香文化名酒。' },
{ title: '营销定位', text: '拓清香赛道,引全民清雅饮酒新风尚。' },
],
styleWords: ['绵', '净', '雅'],
styleText:
'粮香纯正无杂味,清香干净通透;入口绵柔顺滑,落口爽利干脆;杂醇含量低,不口干、不上头,醒酒快——适配小酌与宴席多场景。',
craft: '大麦、豌豆、高粱三种原料;清香「7321」工艺,讲究「清蒸二次清」「一清到底」,味道更醇厚。',
origin: '扎根杏花村千年酿酒圣地,同源同脉,品质有据。',
tasting: [
{ title: '抿', text: '微启触香,品粮香本味' },
{ title: '咂', text: '舌卷含香,品绵柔入喉' },
{ title: '呵', text: '缓缓呵气,品回甘生津' },
],
mission: '坚守纯粹品质,悦享清逸人生。',
product: '酒祖杜康清香系列 T30',
deepReads: [
{ label: '项目与赛道', path: '/qingxiang/project', blurb: '黄金赛道判断与可复制的清香体验闭环。' },
{ label: '定位与价值', path: '/qingxiang/position', blurb: '四维定位、价值观与使命句。' },
{ label: '风格与工艺', path: '/qingxiang/style', blurb: '绵净雅与清香五大法宝。' },
{ label: '品鉴玩法', path: '/qingxiang/tasting', blurb: '抿咂呵与互动证明好喝。' },
{ label: '沉浸体验', path: '/qingxiang/experience', blurb: '三级体验与文化馆五个一。' },
{ label: '文化馆与溯源', path: '/qingxiang/tour', blurb: '到访流程与厂区溯源路径。' },
],
}
export const haoke = {
eyebrow: '杜康好客',
title: '你请客,我买单',
lead:
'杜康好客,是酒祖杜康面向宴请场景的供应链与权益平台:总部供酒,城市合伙人推广,门店核销好客权益,用户一次购买,同时覆盖「酒 + 餐」。',
oneLiner: '买酒,送等价好客权益;到合作门店,按面额抵餐。',
points: [
{
title: '酒餐同赴',
text: '商务宴请不必再「酒一套、餐一套」分开算。买多少酒,送多少面额的好客权益,让席间更从容。',
},
{
title: '线上线下一体',
text: '小程序购酒、同城送达或现场提货;好客权益到合作门店核销——品牌、履约与核销,同一条链路。',
},
{
title: '与餐饮共度时艰',
text: '把酒厂资源与门店客流重新连上:用户得实惠,门店得核销结算,合伙人得城市运营空间——让清香美酒,成为餐桌上的暖流。',
},
],
closing: '千年酒祖的清香,遇见一套可落地的好客机制。',
}
export const system = {
eyebrow: '三端协同',
title: '城市合伙人 · 门店 · 用户,一城一盘棋',
lead:
'杜康好客用一套系统,把「推得动、核得清、买得爽」连成闭环——每一端各司其职,全城一起把清香与好客做起来。',
ends: [
{
title: '城市合伙人',
role: '开城运营者:拓店、组仓配、带团队,让品牌在一座城真正落地。',
caps: '门店拓展与入驻协同 · 推广与代下单 · 佣金与经营数据可见',
},
{
title: '合作门店',
role: '好客权益核销端:把用户带回餐桌,用核销结算反哺门店经营。',
caps: '手机号/核销码核销 · 核销记录可查 · 结算路径清晰',
},
{
title: '用户',
role: '购酒与权益的享受者:小程序下单,酒到席、权益到店。',
caps: '清香选购 · 1:1 好客权益 · 同城配送/现场提货',
},
],
loop: '合伙人拓店 → 用户购酒得权益 → 门店核销结算 → 再反哺城市运营',
closing: '三端同屏,好客才成立;系统在线,信任才可复制。',
}
export const tech = {
eyebrow: '科技驱动',
title: '让白酒行业,跑在可运营的数字轨道上',
lead:
'传统白酒拼品牌、拼渠道;杜康好客把「购酒—权益—核销—结算」做成在线闭环,让酒厂资源、城市运营与餐饮门店,第一次用同一套系统对话。',
points: [
{
title: '四端一体',
text: '用户小程序、门店核销、城市合伙人、总部管控——权限分离、数据同源,开城可复制。',
},
{
title: '全链路闭环',
text: '下单履约、好客权益发放、门店核销、结算回款可视可追,减少「口头承诺、纸面对账」。',
},
{
title: '宴请场景数字化',
text: '面向商务宴请高频人群:酒到席、权益到店,把一次消费同时服务「喝」与「吃」。',
},
{
title: '城市运营中台',
text: '拓店、推广归因、经营数据与热力洞察,服务合伙人把一座城「盘清楚、推得动」。',
},
],
claim:
'科技不是白酒的装饰,而是穿越周期的基础设施——帮酒厂更精准触达,帮餐饮更快回血,帮合伙人更可控地开城。',
cta: { label: '查看科技板块详情', to: '/tech-detail' },
}
export const subsidy = {
eyebrow: '酒厂实力',
title: '十亿级支持,与餐饮共度寒冬',
subtitle: '酒祖杜康清香 · 真资源入场,真补贴到位',
lead:
'当餐饮遭遇客流与成本的双重压力,杜康好客选择站到餐桌这一边:以酒厂之力,把补贴与流量送进合作门店,把清香美酒变成可核销、可回血的经营助力。',
number: '十亿',
numberNote:
'酒厂专项支持规模:十亿级资源投入,用于权益让利、门店激励与城市拓展——不是口号,是可落地的好客供给。',
points: [
{
title: '品牌硬实力',
text: '酒祖杜康清香背书,杏花村产区同源同脉;宴请场景有故事、有品质、有辨识度。',
},
{
title: '补贴真机制',
text: '用户购酒即获等价好客权益,到店抵餐;门店核销有结算路径,让「请吃饭」变成双方都愿意持续的生意。',
},
{
title: '共度周期',
text: '寒冬里最缺的不是又一张海报,而是把客流、酒水与结算打通的系统——杜康好客,为此而来。',
},
],
closing: '酒厂出资源,科技出闭环,门店与用户同受益。',
footnote: '支持细则以合作政策为准。',
}
export const partner = {
eyebrow: '城市合伙人',
title: '携酒祖入城,共建一座城的好客',
lead:
'若你认同酒祖杜康的清香价值,也看好「酒 + 餐」的数字化机会——欢迎了解城市合伙人:用系统化能力,把品牌、门店与用户在一座城运营起来。',
points: [
{ title: '杜康实力', text: '酒祖品牌 + 清香产品矩阵 + 十亿级支持背书' },
{ title: '科技底座', text: '三端协同、全链路可追踪,开城不靠口头账' },
{ title: '定制风格', text: '城市策略、拓店节奏与运营支持,按城匹配' },
{
title: '适合谁',
text: '有本地餐饮/渠道资源 · 认同长期运营 · 愿意用系统做管理的城市操盘手',
},
{
title: '我们提供',
text: '品牌与货盘支持 · 四端系统权限 · 拓店与培训协同 · 结算与数据可见',
},
],
primaryCta: { label: '了解合作方式', to: '/contact' },
secondaryCta: { label: '查看科技板块', to: '/tech-detail' },
}
export const techDetail = {
eyebrow: '科技板块',
title: '可运营、可结算、可规模化的好客系统',
lead:
'杜康好客科技板块,围绕「开城—履约—核销—结算—洞察」构建四端能力,让白酒宴请场景从经验驱动,升级为数据与流程驱动。',
ports: [
{
title: '用户端',
text: '清香选购与展示 · 下单履约(同城配送/现场提货) · 好客权益领取与使用 · 售后工单与发票入口',
},
{
title: '门店核销端',
text: '手机号/核销码核销 · 核销记录可查 · 结算与提现协同(细则以政策为准)',
},
{
title: '城市合伙人端',
text: '门店资料与入驻协同 · 推广/代下单 · 佣金与经营数据可见 · 子账号与权限管理',
},
{
title: '总部管控与数据',
text: '城市开通与审核 · 全链路订单/核销洞察 · 热力与经营分析 · 结算打款协同',
},
],
highlights: [
'一套账本贯穿购酒到核销',
'权限分离保障多方协作',
'开城可复制降低口头协同成本',
],
closing: '科技板块的目标很明确:让每一瓶清香、每一笔好客权益、每一次门店核销,都跑在可信的轨道上。',
contactHint: '了解更多请联系杜康好客 · 项目负责人',
}
export const contact = {
eyebrow: '联系我们',
title: '扫码咨询 · 对接项目负责人',
lead: '现场如需进一步了解酒祖杜康清香、好客模式或城市合作,请扫码添加对接人,或致电联系。',
role: '杜康好客 · 项目负责人',
name: '吕丹',
phone: '13073729990',
wechatHint: '扫一扫上方二维码,添加项目负责人微信。',
}
export const footer = {
brandLine: '酒祖杜康 · 杜康好客',
slogan: '你请客,我买单',
copyright: '© 杜康好客 2026',
icp: '晋ICP备2026004970号',
compliance:
'理性饮酒,未成年人禁止饮酒。本展示内容仅供品牌与合作了解,不构成任何收益承诺。',
dataNote: '行业数据引自公开研究报告,仅供参考。支持细则以合作政策为准。',
}
export const moduleEntries = [
{ to: '/brand', label: '品牌传承', desc: '酒祖精神原点' },
{ to: '/poetry', label: '诗酒文化', desc: '何以解忧,唯有杜康' },
{ to: '/qingxiang', label: '清香', desc: '绵 · 净 · 雅' },
{ to: '/haoke', label: '好客模式', desc: '你请客,我买单' },
{ to: '/system', label: '三端协同', desc: '一城一盘棋' },
{ to: '/tech', label: '科技驱动', desc: '可运营的数字轨道' },
{ to: '/subsidy', label: '酒厂实力', desc: '十亿级支持' },
{ to: '/partner', label: '城市合伙人', desc: '共建一座城的好客' },
{ to: '/tech-detail', label: '科技板块', desc: '四端系统详情' },
{ to: '/contact', label: '联系我们', desc: '扫码对接负责人' },
] as const
/** Pure visuals only (no PDF page screenshots with baked-in text) */
export const images = {
logo: '/assets/logo.png',
qr: '/assets/qr-contact.png',
t30: '/assets/t30-bottle.png',
brand: {
manor: '/assets/brand/emb_p6_5.png',
monument: '/assets/brand/emb_p31_1.png',
landscape: '/assets/brand/emb_p15_3.png',
relic: '/assets/brand/emb_p33_1.png',
},
poetry: {
inkA: '/assets/poetry/emb_p33_4.png',
inkB: '/assets/poetry/emb_p49_4.png',
inkC: '/assets/poetry/emb_p59_4.png',
inkD: '/assets/poetry/emb_p67_4.png',
},
qingxiang: {
hall: '/assets/qingxiang/emb_p7_4.png',
interior: '/assets/qingxiang/emb_p6_2.png',
storefront: '/assets/qingxiang/emb_p7_6.png',
scene: '/assets/qingxiang/emb_p12_2.png',
},
}
+29
View File
@@ -0,0 +1,29 @@
import { useEffect, useState } from 'react'
export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState<boolean>(() => {
if (typeof window === 'undefined') return false
return window.innerWidth < breakpoint
})
useEffect(() => {
if (typeof window === 'undefined') return
const mql = window.matchMedia(`(max-width: ${breakpoint - 0.02}px)`)
const onChange = (e: MediaQueryListEvent) => {
setIsMobile(e.matches)
}
setIsMobile(mql.matches)
if (mql.addEventListener) {
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
} else {
mql.addListener(onChange)
return () => mql.removeListener(onChange)
}
}, [breakpoint])
return isMobile
}
+14
View File
@@ -0,0 +1,14 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react'
import { useLocation } from 'react-router-dom'
const StageContext = createContext(false)
export function StageProvider({ children }: { children: ReactNode }) {
const location = useLocation()
const isStage = useMemo(() => new URLSearchParams(location.search).get('mode') === 'stage', [location.search])
return <StageContext.Provider value={isStage}>{children}</StageContext.Provider>
}
export function useStageMode() {
return useContext(StageContext)
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles/tokens.css'
import './styles/animations.css'
import './styles/responsive.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+237
View File
@@ -0,0 +1,237 @@
/* ===== Mobile Shell Dedicated Styling ===== */
.mobile-shell {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
background: var(--paper, #f7f2ea);
color: var(--ink, #1a1512);
padding-bottom: env(safe-area-inset-bottom, 0px);
position: relative;
overflow-x: hidden;
}
/* Fixed Header with Status Bar Safe Area Top */
.m-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(3.6rem + env(safe-area-inset-top, 0px));
padding-top: env(safe-area-inset-top, 0px);
padding-left: 1rem;
padding-right: 1rem;
box-sizing: border-box;
z-index: 1000;
background: rgba(247, 242, 234, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(166, 28, 32, 0.12);
display: flex;
align-items: center;
justify-content: space-between;
transition: background 0.3s ease;
}
.theme-tech .m-header {
background: rgba(18, 16, 20, 0.95);
border-bottom-color: rgba(196, 163, 90, 0.2);
}
.m-brand {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.m-logo {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.m-title {
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
font-weight: 600;
color: var(--brand, #a61c20);
letter-spacing: 0.05em;
}
.theme-tech .m-title {
color: var(--gold, #c4a35a);
}
.m-header-actions {
display: flex;
align-items: center;
gap: 0.6rem;
}
.m-stage-badge {
padding: 0.25rem 0.6rem;
border-radius: 1rem;
font-size: 0.72rem;
color: #ffffff;
background: linear-gradient(135deg, #a61c20 0%, #7d1216 100%);
border: 1px solid rgba(196, 163, 90, 0.3);
text-decoration: none;
font-weight: 500;
box-shadow: 0 2px 8px rgba(166, 28, 32, 0.25);
}
.m-menu-btn {
width: 2.5rem;
height: 2.5rem;
border: none;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.32rem;
cursor: pointer;
z-index: 1002;
padding: 0;
}
.m-menu-btn span {
width: 1.3rem;
height: 2px;
background: var(--brand, #a61c20);
border-radius: 1px;
transition: all 0.3s ease;
}
.theme-tech .m-menu-btn span {
background: var(--gold, #c4a35a);
}
.m-menu-btn.is-open span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.m-menu-btn.is-open span:nth-child(2) {
opacity: 0;
}
.m-menu-btn.is-open span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* Drawer Overlay & Panel */
.m-drawer-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.m-drawer-overlay.is-open {
opacity: 1;
visibility: visible;
}
.m-drawer {
position: fixed;
top: calc(3.6rem + env(safe-area-inset-top, 0px));
right: 0;
bottom: 0;
width: min(20rem, 85vw);
background: #f7f2ea;
z-index: 1001;
padding: 1.25rem 1.25rem calc(2rem + env(safe-area-inset-bottom, 0px));
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
}
.theme-tech .m-drawer {
background: #141117;
color: #f3ece4;
}
.m-drawer.is-open {
transform: translateX(0);
}
.m-drawer-section {
margin-bottom: 1.25rem;
}
.m-drawer-sec-title {
font-size: 0.78rem;
letter-spacing: 0.15em;
color: var(--gold, #c4a35a);
margin: 0 0 0.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid rgba(196, 163, 90, 0.2);
}
.m-drawer-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.7rem 0.5rem;
font-size: 0.95rem;
color: var(--ink, #1a1512);
text-decoration: none;
border-bottom: 1px solid rgba(166, 28, 32, 0.06);
border-radius: 0.2rem;
transition: background 0.2s ease;
}
.theme-tech .m-drawer-item {
color: #f3ece4;
border-bottom-color: rgba(196, 163, 90, 0.08);
}
.m-drawer-item.is-active {
color: var(--brand, #a61c20);
font-weight: 600;
background: rgba(166, 28, 32, 0.08);
}
.theme-tech .m-drawer-item.is-active {
color: var(--gold, #c4a35a);
background: rgba(196, 163, 90, 0.12);
}
.m-drawer-sublist {
padding-left: 0.85rem;
display: grid;
gap: 0.2rem;
margin-top: 0.2rem;
}
.m-drawer-subitem {
display: block;
padding: 0.5rem 0.5rem;
font-size: 0.86rem;
color: var(--ink-soft, #5c534c);
text-decoration: none;
border-radius: 0.2rem;
}
.theme-tech .m-drawer-subitem {
color: rgba(243, 236, 228, 0.7);
}
.m-drawer-subitem.is-active {
color: var(--brand, #a61c20);
font-weight: 600;
}
/* Main Content Wrapper - Push down content to avoid Fixed Header overlap */
.m-main {
flex: 1;
width: 100%;
padding-top: calc(3.6rem + env(safe-area-inset-top, 0px));
box-sizing: border-box;
}
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState, type ReactNode } from 'react'
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
import { navGroups } from '../content/nav'
import { images } from '../content/site'
import { Footer } from '../components/Footer'
import './MobileShell.css'
const techRoutes = new Set(['/tech', '/subsidy', '/partner', '/tech-detail', '/contact'])
export function MobileShell({ children }: { children?: ReactNode }) {
const location = useLocation()
const [drawerOpen, setDrawerOpen] = useState(false)
const isTechPage = techRoutes.has(location.pathname)
// Close drawer on navigation
useEffect(() => {
setDrawerOpen(false)
window.scrollTo({ top: 0, behavior: 'auto' })
}, [location.pathname, location.search])
// Prevent background scroll when drawer is open
useEffect(() => {
document.body.style.overflow = drawerOpen ? 'hidden' : ''
return () => {
document.body.style.overflow = ''
}
}, [drawerOpen])
return (
<div className={`mobile-shell ${isTechPage ? 'theme-tech' : ''}`}>
{/* Mobile Top Header */}
<header className="m-header">
<Link to="/" className="m-brand">
<img src={images.logo} alt="杜康好客" className="m-logo" />
<span className="m-title"></span>
</Link>
<div className="m-header-actions">
<Link to="/?mode=stage" className="m-stage-badge">
</Link>
<button
type="button"
className={`m-menu-btn ${drawerOpen ? 'is-open' : ''}`}
onClick={() => setDrawerOpen((v) => !v)}
aria-label={drawerOpen ? '关闭菜单' : '打开菜单'}
>
<span />
<span />
<span />
</button>
</div>
</header>
{/* Drawer Mask & Navigation Panel */}
<div
className={`m-drawer-overlay ${drawerOpen ? 'is-open' : ''}`}
onClick={() => setDrawerOpen(false)}
/>
<div className={`m-drawer ${drawerOpen ? 'is-open' : ''}`}>
<div style={{ marginBottom: '1rem', fontWeight: 600, fontSize: '1.05rem', color: 'var(--brand)' }}>
</div>
<div className="m-drawer-section">
<Link
to="/"
className={`m-drawer-item ${location.pathname === '/' ? 'is-active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
<span></span>
<span></span>
</Link>
</div>
{navGroups.map((group) => (
<div key={group.id} className="m-drawer-section">
<div className="m-drawer-sec-title">{group.label}</div>
<div className="m-drawer-sublist">
{group.children.map((child) => (
<NavLink
key={child.path}
to={child.path}
className={({ isActive }) => `m-drawer-subitem ${isActive ? 'is-active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
{child.label}
</NavLink>
))}
</div>
</div>
))}
</div>
{/* Main Content Area */}
<main className="m-main" key={location.pathname}>
{children || <Outlet />}
</main>
<Footer />
</div>
)
}
+292
View File
@@ -0,0 +1,292 @@
/* ===== Mobile Views Dedicated CSS ===== */
.m-view-container {
width: 100%;
padding: 1.25rem 1rem 2.5rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Header & Typography */
.m-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8rem;
color: var(--brand, #a61c20);
letter-spacing: 0.15em;
}
.m-eyebrow::before {
content: '';
width: 1rem;
height: 1px;
background: currentColor;
}
.theme-tech .m-eyebrow {
color: var(--gold, #c4a35a);
}
.m-page-title {
font-family: 'Noto Serif SC', serif;
font-size: clamp(1.4rem, 5.5vw, 1.85rem);
font-weight: 600;
margin: 0.2rem 0 0.6rem;
color: var(--ink, #1a1512);
line-height: 1.35;
}
.theme-tech .m-page-title {
color: #ffffff;
}
.m-lead {
font-size: 0.95rem;
line-height: 1.65;
color: var(--ink-soft, #5c534c);
margin: 0 0 1rem;
}
.theme-tech .m-lead {
color: rgba(243, 236, 228, 0.78);
}
.m-closing {
margin-top: 1rem;
padding-top: 0.85rem;
border-top: 1px solid rgba(196, 163, 90, 0.3);
font-family: 'Noto Serif SC', serif;
font-size: 0.95rem;
color: var(--gold, #c4a35a);
text-align: center;
}
/* Card Component */
.m-card {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(166, 28, 32, 0.12);
border-radius: 0.5rem;
padding: 1.15rem 1.1rem;
box-shadow: 0 4px 15px rgba(26, 21, 18, 0.05);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.theme-tech .m-card {
background: rgba(28, 25, 31, 0.85);
border-color: rgba(196, 163, 90, 0.22);
color: #f3ece4;
}
.m-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
margin: 0;
color: var(--brand, #a61c20);
}
.theme-tech .m-card h3 {
color: var(--gold, #c4a35a);
}
.m-card p {
font-size: 0.9rem;
line-height: 1.6;
color: var(--ink-soft, #5c534c);
margin: 0;
}
.theme-tech .m-card p {
color: rgba(243, 236, 228, 0.75);
}
/* Button & CTAs */
.m-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
padding: 0.6rem 1.25rem;
border-radius: 0.3rem;
font-size: 0.92rem;
font-weight: 500;
text-decoration: none;
transition: all 0.25s ease;
}
.m-btn-primary {
background: var(--brand, #a61c20);
color: #ffffff;
box-shadow: 0 4px 12px rgba(166, 28, 32, 0.3);
}
.m-btn-outline {
border: 1px solid var(--brand, #a61c20);
color: var(--brand, #a61c20);
background: transparent;
}
.theme-tech .m-btn-outline {
border-color: var(--gold, #c4a35a);
color: var(--gold, #c4a35a);
}
/* Mobile Hero Component */
.m-hero {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem 0 1rem;
}
.m-hero-sub {
font-size: 0.88rem;
color: var(--gold, #c4a35a);
letter-spacing: 0.15em;
font-weight: 600;
}
.m-hero-title {
font-family: 'Noto Serif SC', serif;
font-size: clamp(1.8rem, 7vw, 2.5rem);
color: var(--brand, #a61c20);
margin: 0;
line-height: 1.25;
}
.m-hero-img {
width: min(12rem, 50vw);
height: auto;
margin: 0.5rem auto;
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.15));
}
/* Specific Cards & Rings */
.m-rings-row {
display: flex;
justify-content: center;
gap: 1rem;
margin: 0.85rem 0;
}
.m-ring-item {
width: 3.2rem;
height: 3.2rem;
border-radius: 50%;
border: 1px solid var(--gold, #c4a35a);
background: rgba(166, 28, 32, 0.08);
display: grid;
place-items: center;
font-family: 'Noto Serif SC', serif;
font-size: 1.15rem;
font-weight: 600;
color: var(--brand, #a61c20);
}
.theme-tech .m-ring-item {
background: rgba(196, 163, 90, 0.12);
color: var(--gold, #c4a35a);
}
.m-poem-box {
background: linear-gradient(135deg, rgba(166, 28, 32, 0.06), rgba(196, 163, 90, 0.08));
border: 1px solid rgba(196, 163, 90, 0.3);
padding: 1.25rem 1rem;
border-radius: 0.4rem;
text-align: center;
}
.m-poem-quote {
font-family: 'Noto Serif SC', serif;
font-size: 1.15rem;
line-height: 1.7;
color: var(--brand, #a61c20);
margin: 0 0 0.5rem;
}
.theme-tech .m-poem-quote {
color: var(--gold, #c4a35a);
}
.m-poem-source {
font-size: 0.82rem;
color: var(--ink-soft, #5c534c);
}
.theme-tech .m-poem-source {
color: rgba(243, 236, 228, 0.65);
}
.m-big-num {
font-family: 'Noto Serif SC', serif;
font-size: 3.8rem;
font-weight: 700;
color: var(--brand, #a61c20);
line-height: 1;
text-shadow: 0 2px 10px rgba(166, 28, 32, 0.2);
text-align: center;
margin: 0.3rem 0;
}
.theme-tech .m-big-num {
color: var(--gold, #c4a35a);
}
.m-qr-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 1.5rem 1rem;
gap: 1rem;
}
.m-qr-img {
width: 10rem;
height: 10rem;
padding: 0.5rem;
background: #ffffff;
border-radius: 0.4rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.m-qr-img img {
width: 100%;
height: 100%;
object-fit: contain;
}
.m-deep-links {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid rgba(166, 28, 32, 0.12);
display: grid;
gap: 0.65rem;
}
.m-deep-card {
padding: 0.85rem 0.95rem;
background: rgba(255, 255, 255, 0.6);
border-left: 3px solid var(--gold, #c4a35a);
border-radius: 0.2rem;
text-decoration: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.m-deep-card strong {
font-size: 0.95rem;
color: var(--brand, #a61c20);
}
.m-deep-card span {
font-size: 0.82rem;
color: var(--ink-soft, #5c534c);
}
+468
View File
@@ -0,0 +1,468 @@
import { Link, Outlet, useLocation } from 'react-router-dom'
import {
brand,
contact,
haoke,
hero,
images,
partner,
poetry,
qingxiang,
subsidy,
system,
tech,
techDetail,
} from '../content/site'
import './MobileViews.css'
export function MobileViews() {
const location = useLocation()
const path = location.pathname
switch (path) {
case '/':
return <MobileHomeView />
case '/brand':
return <MobileBrandView />
case '/poetry':
return <MobilePoetryView />
case '/qingxiang':
return <MobileQingxiangView />
case '/haoke':
return <MobileHaokeView />
case '/system':
return <MobileSystemView />
case '/tech':
return <MobileTechView />
case '/subsidy':
return <MobileSubsidyView />
case '/partner':
return <MobilePartnerView />
case '/tech-detail':
return <MobileTechDetailView />
case '/contact':
return <MobileContactView />
default:
return <Outlet />
}
}
/* 1. Mobile Home View */
function MobileHomeView() {
return (
<div className="m-view-container">
<div className="m-hero">
<div className="m-hero-sub">{hero.subBrand} · </div>
<h1 className="m-hero-title">{hero.brand}</h1>
<p style={{ fontSize: '1.05rem', color: 'var(--gold)', fontFamily: 'Noto Serif SC', margin: 0 }}>
{hero.title}
</p>
<img src={images.t30} alt="酒祖杜康 T30" className="m-hero-img" />
<p className="m-lead" style={{ textAlign: 'center' }}>
{hero.subtitle}
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
<Link to="/brand" className="m-btn m-btn-primary">
{hero.primaryCta.label}
</Link>
<Link to="/partner" className="m-btn m-btn-outline">
{hero.secondaryCta.label}
</Link>
</div>
</div>
</div>
)
}
/* 2. Mobile Brand View */
function MobileBrandView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{brand.eyebrow}</div>
<h2 className="m-page-title">{brand.title}</h2>
<p className="m-lead">{brand.lead}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{brand.cards.map((card, idx) => (
<div key={card.title} className="m-card">
<div style={{ fontSize: '0.8rem', color: 'var(--gold)', fontWeight: 600 }}>0{idx + 1}</div>
<h3>{card.title}</h3>
<p>{card.text}</p>
</div>
))}
</div>
<div className="m-card">
<h3></h3>
<img
src={images.brand.manor}
alt="杜康庄园"
style={{ width: '100%', borderRadius: '0.3rem', marginTop: '0.3rem' }}
/>
<div style={{ display: 'grid', gap: '0.4rem', marginTop: '0.5rem' }}>
{brand.timeline.map((item) => (
<div
key={item}
style={{
fontSize: '0.88rem',
color: 'var(--ink-soft)',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
}}
>
<span
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: 'var(--brand)',
flexShrink: 0,
}}
/>
<span>{item}</span>
</div>
))}
</div>
</div>
<div className="m-closing">{brand.closing}</div>
<div className="m-deep-links">
<div style={{ fontSize: '0.9rem', color: 'var(--brand)', fontWeight: 600 }}></div>
{brand.deepReads.map((dp) => (
<Link key={dp.path} to={dp.path} className="m-deep-card">
<strong>{dp.label} </strong>
<span>{dp.blurb}</span>
</Link>
))}
</div>
</div>
)
}
/* 3. Mobile Poetry View */
function MobilePoetryView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{poetry.eyebrow}</div>
<h2 className="m-page-title">{poetry.title}</h2>
<p className="m-lead">{poetry.lead}</p>
</div>
<div className="m-poem-box">
<p className="m-poem-quote">{poetry.mainPoem.quote}</p>
<div className="m-poem-source"> {poetry.mainPoem.source}</div>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{poetry.poems.map((p) => (
<div key={p.source} className="m-card">
<p style={{ fontFamily: 'Noto Serif SC', fontSize: '1rem', color: 'var(--ink)' }}>{p.quote}</p>
<div style={{ fontSize: '0.82rem', color: 'var(--gold)', textAlign: 'right' }}> {p.source}</div>
</div>
))}
</div>
<div className="m-closing">{poetry.closing}</div>
<div className="m-deep-links">
<div style={{ fontSize: '0.9rem', color: 'var(--brand)', fontWeight: 600 }}></div>
{poetry.deepReads.map((dp) => (
<Link key={dp.path} to={dp.path} className="m-deep-card">
<strong>{dp.label} </strong>
<span>{dp.blurb}</span>
</Link>
))}
</div>
</div>
)
}
/* 4. Mobile Qingxiang View */
function MobileQingxiangView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{qingxiang.eyebrow}</div>
<h2 className="m-page-title">{qingxiang.title}</h2>
<p className="m-lead">{qingxiang.lead}</p>
</div>
<div style={{ textAlign: 'center' }}>
<img src={images.t30} alt="酒祖杜康清香 T30" style={{ width: '9rem', margin: '0 auto' }} />
<div className="m-rings-row">
{qingxiang.styleWords.map((word) => (
<div key={word} className="m-ring-item">
{word}
</div>
))}
</div>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{qingxiang.positions.map((pos) => (
<div key={pos.title} className="m-card">
<h3>{pos.title}</h3>
<p>{pos.text}</p>
</div>
))}
</div>
<div className="m-card">
<h3> · </h3>
<div style={{ display: 'grid', gap: '0.5rem', marginTop: '0.3rem' }}>
{qingxiang.tasting.map((t) => (
<div key={t.title} style={{ fontSize: '0.88rem' }}>
<strong style={{ color: 'var(--brand)' }}>{t.title}</strong>
<span style={{ color: 'var(--ink-soft)' }}>{t.text}</span>
</div>
))}
</div>
</div>
<div className="m-closing">{qingxiang.mission}</div>
<div className="m-deep-links">
<div style={{ fontSize: '0.9rem', color: 'var(--brand)', fontWeight: 600 }}></div>
{qingxiang.deepReads.map((dp) => (
<Link key={dp.path} to={dp.path} className="m-deep-card">
<strong>{dp.label} </strong>
<span>{dp.blurb}</span>
</Link>
))}
</div>
</div>
)
}
/* 5. Mobile Haoke View */
function MobileHaokeView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{haoke.eyebrow}</div>
<h2 className="m-page-title">{haoke.title}</h2>
<p className="m-lead">{haoke.lead}</p>
</div>
<div className="m-card" style={{ background: 'rgba(166, 28, 32, 0.08)', borderColor: 'var(--brand)' }}>
<h3 style={{ fontSize: '1.15rem' }}></h3>
<p style={{ fontWeight: 600, color: 'var(--brand)', fontSize: '1.02rem', lineHeight: 1.5 }}>
{haoke.oneLiner}
</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{haoke.points.map((pt) => (
<div key={pt.title} className="m-card">
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
<div className="m-closing">{haoke.closing}</div>
</div>
)
}
/* 6. Mobile System View */
function MobileSystemView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{system.eyebrow}</div>
<h2 className="m-page-title">{system.title}</h2>
<p className="m-lead">{system.lead}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{system.ends.map((end) => (
<div key={end.title} className="m-card">
<h3>{end.title}</h3>
<p style={{ fontWeight: 500, color: 'var(--ink)' }}>{end.role}</p>
<div
style={{
fontSize: '0.82rem',
color: 'var(--gold)',
paddingTop: '0.4rem',
borderTop: '1px dashed rgba(196, 163, 90, 0.3)',
}}
>
{end.caps}
</div>
</div>
))}
</div>
<div className="m-card" style={{ textAlign: 'center', background: 'rgba(166, 28, 32, 0.05)' }}>
<div style={{ fontSize: '0.82rem', color: 'var(--brand)', fontWeight: 600 }}></div>
<p style={{ fontWeight: 500, color: 'var(--brand)' }}>{system.loop}</p>
</div>
<div className="m-closing">{system.closing}</div>
</div>
)
}
/* 7. Mobile Tech View */
function MobileTechView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{tech.eyebrow}</div>
<h2 className="m-page-title">{tech.title}</h2>
<p className="m-lead">{tech.lead}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{tech.points.map((pt) => (
<div key={pt.title} className="m-card">
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
<div className="m-closing">{tech.claim}</div>
<div style={{ textAlign: 'center', marginTop: '0.5rem' }}>
<Link to={tech.cta.to} className="m-btn m-btn-primary">
{tech.cta.label}
</Link>
</div>
</div>
)
}
/* 8. Mobile Subsidy View */
function MobileSubsidyView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{subsidy.eyebrow}</div>
<h2 className="m-page-title">{subsidy.title}</h2>
<div style={{ fontSize: '0.92rem', color: 'var(--gold)', fontWeight: 600, marginBottom: '0.5rem' }}>
{subsidy.subtitle}
</div>
<p className="m-lead">{subsidy.lead}</p>
</div>
<div className="m-card" style={{ textAlign: 'center', padding: '1.5rem 1rem' }}>
<div style={{ fontSize: '0.88rem', color: 'var(--gold)', fontWeight: 600 }}></div>
<div className="m-big-num">{subsidy.number}</div>
<p style={{ fontSize: '0.85rem' }}>{subsidy.numberNote}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{subsidy.points.map((pt) => (
<div key={pt.title} className="m-card">
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
<div className="m-closing">{subsidy.closing}</div>
</div>
)
}
/* 9. Mobile Partner View */
function MobilePartnerView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{partner.eyebrow}</div>
<h2 className="m-page-title">{partner.title}</h2>
<p className="m-lead">{partner.lead}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{partner.points.map((pt) => (
<div key={pt.title} className="m-card">
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', marginTop: '1rem' }}>
<Link to={partner.primaryCta.to} className="m-btn m-btn-primary">
{partner.primaryCta.label}
</Link>
<Link to={partner.secondaryCta.to} className="m-btn m-btn-outline">
{partner.secondaryCta.label}
</Link>
</div>
</div>
)
}
/* 10. Mobile Tech Detail View */
function MobileTechDetailView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{techDetail.eyebrow}</div>
<h2 className="m-page-title">{techDetail.title}</h2>
<p className="m-lead">{techDetail.lead}</p>
</div>
<div style={{ display: 'grid', gap: '0.85rem' }}>
{techDetail.ports.map((port) => (
<div key={port.title} className="m-card">
<h3>{port.title}</h3>
<p>{port.text}</p>
</div>
))}
</div>
<div className="m-closing">{techDetail.closing}</div>
</div>
)
}
/* 11. Mobile Contact View */
function MobileContactView() {
return (
<div className="m-view-container">
<div>
<div className="m-eyebrow">{contact.eyebrow}</div>
<h2 className="m-page-title">{contact.title}</h2>
<p className="m-lead">{contact.lead}</p>
</div>
<div className="m-card m-qr-card">
<div className="m-qr-img">
<img src={images.qr} alt="微信二维码" />
</div>
<div>
<h3 style={{ fontSize: '1.25rem' }}>{contact.name}</h3>
<div style={{ fontSize: '0.88rem', color: 'var(--gold)', margin: '0.2rem 0 0.5rem' }}>
{contact.role}
</div>
<a
href={`tel:${contact.phone}`}
style={{
display: 'inline-block',
fontSize: '1.3rem',
fontWeight: 700,
color: 'var(--brand)',
textDecoration: 'none',
}}
>
📞 {contact.phone}
</a>
<p style={{ fontSize: '0.8rem', color: 'var(--ink-soft)', marginTop: '0.4rem' }}>
{contact.wechatHint}
</p>
</div>
</div>
</div>
)
}
+102
View File
@@ -0,0 +1,102 @@
.brand-layout {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 2.5rem;
margin-top: 1rem;
}
.brand-timeline {
position: relative;
display: grid;
gap: 1.75rem;
padding-left: 1.5rem;
}
.brand-line {
position: absolute;
left: 0.35rem;
top: 0.4rem;
bottom: 0.4rem;
width: 1px;
background: var(--brand);
transform-origin: top;
animation: drawLineY 1.2s var(--ease) both;
}
.brand-item {
position: relative;
}
.brand-item::before {
content: '';
position: absolute;
left: -1.32rem;
top: 0.45rem;
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: var(--brand);
}
.brand-num {
display: block;
color: var(--gold);
letter-spacing: 0.14em;
font-size: 0.85rem;
margin-bottom: 0.35rem;
}
.brand-vtitle {
writing-mode: vertical-rl;
float: left;
margin: 0 0.85rem 0 0;
color: var(--brand);
font-size: 1.35rem;
letter-spacing: 0.2em;
height: 7rem;
}
.brand-aside {
display: grid;
gap: 1rem;
align-content: start;
}
.brand-photo img {
aspect-ratio: 4 / 3;
object-fit: cover;
width: 100%;
}
.brand-photo.secondary img {
aspect-ratio: 16 / 10;
}
.brand-era {
list-style: none;
margin: 2.5rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.5rem;
border-top: 1px solid var(--gold-soft);
padding-top: 1.5rem;
}
.brand-era li {
color: var(--brand);
font-size: 1.05rem;
}
@media (max-width: 48rem) {
.brand-layout {
grid-template-columns: 1fr;
}
.brand-vtitle {
writing-mode: horizontal-tb;
float: none;
height: auto;
margin: 0 0 0.5rem;
}
}
+68
View File
@@ -0,0 +1,68 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { brand, images } from '../content/site'
import './Brand.css'
export function BrandPage() {
return (
<section className="brand-page page-section">
<div className="container">
<Reveal variant="anim-slide-left">
<p className="eyebrow">{brand.eyebrow}</p>
<h1 className="page-title">{brand.title}</h1>
<p className="lead">{brand.lead}</p>
</Reveal>
<div className="brand-layout">
<div className="brand-timeline">
<div className="brand-line" aria-hidden />
{brand.cards.map((card, i) => (
<Reveal key={card.title} variant="anim-slide-left" delayMs={i * 120} className="brand-item">
<span className="brand-num serif">{String(i + 1).padStart(2, '0')}</span>
<h2 className="brand-vtitle serif">{card.title}</h2>
<p>{card.text}</p>
</Reveal>
))}
</div>
<Reveal variant="anim-zoom" className="brand-aside">
<div className="media-frame brand-photo">
<img src={images.brand.landscape} alt="酒祖故里景致" loading="lazy" />
</div>
<div className="media-frame brand-photo secondary">
<img src={images.brand.relic} alt="酒祖相关文物保护单位" loading="lazy" />
</div>
</Reveal>
</div>
<Reveal variant="anim-slide-right">
<ol className="brand-era">
{brand.timeline.map((item) => (
<li key={item} className="serif">
{item}
</li>
))}
</ol>
<p className="closing">{brand.closing}</p>
</Reveal>
<div className="deep-read-rail">
<Reveal variant="anim-rise">
<h2 className="serif deep-read-title"></h2>
<p className="deep-read-lead"></p>
</Reveal>
<div className="deep-read-grid">
{brand.deepReads.map((item, i) => (
<Reveal key={item.path} variant="anim-rise" delayMs={i * 50}>
<Link to={item.path} className="deep-read-card">
<strong className="serif">{item.label}</strong>
<span>{item.blurb}</span>
</Link>
</Reveal>
))}
</div>
</div>
</div>
</section>
)
}
+77
View File
@@ -0,0 +1,77 @@
.contact-ceremony {
text-align: center;
}
.contact-lead {
margin-inline: auto;
text-align: center;
}
.contact-meet {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
justify-items: center;
margin-top: 2.5rem;
max-width: 42rem;
margin-inline: auto;
}
.contact-info {
text-align: center;
display: grid;
gap: 0.35rem;
justify-items: center;
}
.role {
margin: 0;
color: var(--gold);
letter-spacing: 0.14em;
font-size: 0.95rem;
}
.name {
margin: 0;
font-size: clamp(2rem, 1.6rem + 1.2vw, 2.75rem);
letter-spacing: 0.12em;
}
.phone {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
margin-top: 0.5rem;
font-size: 1.35rem;
color: var(--gold);
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(196, 163, 90, 0.35);
}
.qr-block {
text-align: center;
}
.qr-image {
width: min(14rem, 70vw);
margin: 0 auto 0.85rem;
padding: 0.65rem;
background: #fff;
border-radius: 0.2rem;
box-shadow: var(--shadow-soft);
}
.qr-block p {
margin: 0;
color: var(--ink-soft);
font-size: 0.9rem;
}
@media (max-width: 48rem) {
.contact-meet {
grid-template-columns: 1fr;
gap: 2rem;
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Reveal } from '../components/Reveal'
import { contact, images } from '../content/site'
import './Contact.css'
export function ContactPage() {
return (
<section className="contact-page page-section">
<div className="container contact-ceremony">
<Reveal variant="anim-rise">
<p className="eyebrow">{contact.eyebrow}</p>
<h1 className="page-title">{contact.title}</h1>
<p className="lead contact-lead">{contact.lead}</p>
</Reveal>
<div className="contact-meet">
<Reveal variant="anim-meet-left" className="contact-info">
<p className="role">{contact.role}</p>
<h2 className="serif name">{contact.name}</h2>
<a className="phone" href={`tel:${contact.phone}`}>
{contact.phone}
</a>
</Reveal>
<Reveal variant="anim-meet-right" className="qr-block">
<img src={images.qr} alt="项目负责人微信二维码" className="qr-image" />
<p>{contact.wechatHint}</p>
</Reveal>
</div>
</div>
</section>
)
}
+22
View File
@@ -0,0 +1,22 @@
import { useParams, Navigate, useLocation } from 'react-router-dom'
import { DeepLayout } from '../components/DeepLayout'
import { deepPageMap } from '../content/nav'
import { useStageMode } from '../hooks/useStageMode'
import { resolveStageRedirect } from '../content/deep/assets'
export function DeepPage() {
const { slug } = useParams()
const location = useLocation()
const isStage = useStageMode()
const section = location.pathname.split('/').filter(Boolean)[0]
const path = `/${section}/${slug}`
if (isStage) {
const main = resolveStageRedirect(path) ?? '/'
return <Navigate to={main === '/' ? '/?mode=stage' : `${main}?mode=stage`} replace />
}
const page = deepPageMap[path]
if (!page) return <Navigate to="/" replace />
return <DeepLayout page={page} />
}
+58
View File
@@ -0,0 +1,58 @@
.haoke-page {
position: relative;
}
.haoke-banner {
position: absolute;
inset: 0 0 auto 0;
height: 14rem;
background-size: cover;
background-position: center;
opacity: 0.18;
mask-image: linear-gradient(180deg, #000, transparent);
}
.haoke-slogan {
margin: 1.5rem 0 2.5rem;
font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.4rem);
color: var(--brand);
line-height: 1.45;
max-width: 40rem;
}
.haoke-steps {
display: grid;
gap: 0;
border-left: 1px solid rgba(166, 28, 32, 0.25);
margin-left: 0.55rem;
padding-left: 1.5rem;
}
.haoke-step {
position: relative;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.85rem;
padding: 1.15rem 0 1.5rem;
}
.haoke-dot {
position: absolute;
left: -1.85rem;
top: 1.45rem;
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
background: var(--brand);
box-shadow: 0 0 0 0.35rem rgba(166, 28, 32, 0.12);
}
.haoke-step h3 {
margin: 0 0 0.4rem;
color: var(--brand);
}
.haoke-step p {
margin: 0;
color: var(--ink-soft);
}
+44
View File
@@ -0,0 +1,44 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { haoke, images } from '../content/site'
import './Haoke.css'
export function HaokePage() {
return (
<section className="haoke-page page-section">
<div className="haoke-banner" style={{ backgroundImage: `url(${images.qingxiang.scene})` }} />
<div className="container">
<Reveal variant="anim-rise">
<p className="eyebrow">{haoke.eyebrow}</p>
<h1 className="page-title">{haoke.title}</h1>
<p className="lead">{haoke.lead}</p>
</Reveal>
<Reveal variant="anim-sweep" className="haoke-slogan serif">
{haoke.oneLiner}
</Reveal>
<div className="haoke-steps">
{haoke.points.map((point, i) => (
<Reveal key={point.title} variant="anim-slide-left" delayMs={i * 160} className="haoke-step">
<span className="haoke-dot" />
<div>
<h3 className="serif">{point.title}</h3>
<p>{point.text}</p>
</div>
</Reveal>
))}
</div>
<Reveal variant="anim-rise">
<p className="closing">{haoke.closing}</p>
<div className="cta-row">
<Link className="btn btn-primary" to="/system">
</Link>
</div>
</Reveal>
</div>
</section>
)
}
+118
View File
@@ -0,0 +1,118 @@
.hero {
position: relative;
min-height: calc(100vh - var(--nav-h));
display: flex;
align-items: center;
overflow: hidden;
padding: 3rem 0 4rem;
}
.hero-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transform: scale(1.05);
animation: softZoom 1.4s var(--ease) both;
}
.hero-mask {
position: absolute;
inset: 0;
background: linear-gradient(
105deg,
rgba(247, 242, 234, 0.94) 0%,
rgba(247, 242, 234, 0.82) 42%,
rgba(247, 242, 234, 0.35) 70%,
rgba(18, 16, 20, 0.25) 100%
);
}
.hero-grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 2.5rem;
align-items: center;
}
.hero-brand {
margin: 0;
font-size: clamp(2.4rem, 2rem + 2vw, 4rem);
color: var(--brand);
letter-spacing: 0.12em;
}
.hero-subbrand {
margin: 0.35rem 0 1.25rem;
color: var(--ink-soft);
letter-spacing: 0.35em;
font-size: 0.95rem;
}
.hero-title {
margin: 0 0 1rem;
font-size: clamp(1.8rem, 1.4rem + 1.4vw, 2.8rem);
}
.hero-subtitle {
margin: 0;
max-width: 34rem;
color: var(--ink-soft);
font-size: 1.05rem;
}
.hero-visual {
justify-self: center;
width: min(22rem, 100%);
filter: drop-shadow(0 1.5rem 2.5rem rgba(26, 21, 18, 0.25));
}
.hero-visual img {
width: 100%;
height: auto;
object-fit: contain;
}
.modules {
background: linear-gradient(180deg, transparent, rgba(166, 28, 32, 0.04));
}
.module-rail {
display: flex;
gap: 1rem;
overflow-x: auto;
padding-bottom: 0.75rem;
scroll-snap-type: x mandatory;
}
.module-rail-item {
flex: 0 0 auto;
width: min(14rem, 72vw);
min-height: 9rem;
padding: 1.2rem 1.1rem;
border-left: 2px solid var(--brand);
background: rgba(255, 255, 255, 0.55);
scroll-snap-align: start;
display: flex;
flex-direction: column;
gap: 0.4rem;
transition: transform 0.35s var(--ease), background 0.35s var(--ease);
}
.module-rail-item:hover {
transform: translateY(-0.25rem);
background: #fff;
}
.module-index {
font-size: 0.78rem;
letter-spacing: 0.14em;
color: var(--brand);
}
.module-desc {
color: var(--ink-soft);
font-size: 0.88rem;
}
+61
View File
@@ -0,0 +1,61 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { hero, images, moduleEntries } from '../content/site'
import './Home.css'
export function HomePage() {
return (
<>
<section className="hero">
<div className="hero-bg" style={{ backgroundImage: `url(${images.qingxiang.interior})` }} />
<div className="hero-mask" />
<div className="container hero-grid">
<div className="hero-copy">
<p className="hero-brand serif reveal-base anim-rise is-in">{hero.brand}</p>
<p className="hero-subbrand reveal-base anim-rise is-in" style={{ transitionDelay: '120ms' }}>
{hero.subBrand}
</p>
<h1 className="hero-title serif reveal-base anim-rise is-in" style={{ transitionDelay: '220ms' }}>
{hero.title}
</h1>
<p className="hero-subtitle reveal-base anim-rise is-in" style={{ transitionDelay: '320ms' }}>
{hero.subtitle}
</p>
<div className="cta-row reveal-base anim-rise is-in" style={{ transitionDelay: '420ms' }}>
<Link className="btn btn-primary" to={hero.primaryCta.to}>
{hero.primaryCta.label}
</Link>
<Link className="btn btn-ghost" to={hero.secondaryCta.to}>
{hero.secondaryCta.label}
</Link>
</div>
</div>
<div className="hero-visual hero-float reveal-base anim-zoom is-in">
<img src={images.t30} alt="酒祖杜康清香系列 T30" />
</div>
</div>
</section>
<section className="page-section modules">
<div className="container">
<Reveal variant="anim-slide-left">
<p className="eyebrow"></p>
<h2 className="page-title"></h2>
<p className="lead">稿</p>
</Reveal>
<div className="module-rail">
{moduleEntries.map((item, index) => (
<Reveal key={item.to} variant="anim-slide-right" delayMs={index * 70}>
<Link to={item.to} className="module-rail-item" style={{ fontSize: `${1.05 + (index % 3) * 0.12}rem` }}>
<span className="module-index">{String(index + 1).padStart(2, '0')}</span>
<strong className="serif">{item.label}</strong>
<span className="module-desc">{item.desc}</span>
</Link>
</Reveal>
))}
</div>
</div>
</section>
</>
)
}
+54
View File
@@ -0,0 +1,54 @@
.partner-layout {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 2.5rem;
align-items: start;
}
.partner-list {
list-style: none;
margin: 1.5rem 0 0;
padding: 0;
display: grid;
gap: 0;
}
.partner-row li {
display: grid;
grid-template-columns: 3rem 1fr;
gap: 0.85rem;
padding: 1.1rem 0;
border-bottom: 1px solid var(--tech-line);
}
.partner-idx {
color: var(--gold);
font-size: 1.15rem;
}
.partner-row h3 {
margin: 0 0 0.35rem;
color: var(--gold);
font-size: 1.1rem;
}
.partner-row p {
margin: 0;
color: var(--ink-soft);
}
.partner-aside .media-frame img {
width: 100%;
aspect-ratio: 4 / 5;
object-fit: cover;
}
.partner-cta {
margin-top: 1.25rem;
}
@media (max-width: 48rem) {
.partner-layout {
grid-template-columns: 1fr;
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { images, partner } from '../content/site'
import './Partner.css'
export function PartnerPage() {
return (
<section className="partner-page page-section">
<div className="container partner-layout">
<div>
<Reveal variant="anim-slide-left">
<p className="eyebrow">{partner.eyebrow}</p>
<h1 className="page-title">{partner.title}</h1>
<p className="lead">{partner.lead}</p>
</Reveal>
<ol className="partner-list">
{partner.points.map((point, i) => (
<Reveal key={point.title} variant="anim-slide-left" delayMs={i * 100} className="partner-row">
<li>
<span className="partner-idx serif">{String(i + 1).padStart(2, '0')}</span>
<div>
<h3 className="serif">{point.title}</h3>
<p>{point.text}</p>
</div>
</li>
</Reveal>
))}
</ol>
</div>
<Reveal variant="anim-slide-right" className="partner-aside">
<div className="media-frame">
<img src={images.brand.landscape} alt="" loading="lazy" />
</div>
<div className="cta-row partner-cta">
<Link className="btn btn-primary" to={partner.primaryCta.to}>
{partner.primaryCta.label}
</Link>
<Link className="btn btn-ghost" to={partner.secondaryCta.to}>
{partner.secondaryCta.label}
</Link>
</div>
</Reveal>
</div>
</section>
)
}
+87
View File
@@ -0,0 +1,87 @@
.poetry-page {
position: relative;
overflow: hidden;
}
.poetry-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
opacity: 0.12;
pointer-events: none;
}
.poetry-shell {
position: relative;
z-index: 1;
}
.poem-stage {
margin: 1.5rem 0 2rem;
}
.poem-main {
margin: 0 auto;
max-width: 36rem;
padding: 2rem 1.75rem;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(166, 28, 32, 0.15);
writing-mode: vertical-rl;
text-orientation: mixed;
min-height: 18rem;
}
.poem-main p {
margin: 0;
font-size: 1.35rem;
letter-spacing: 0.14em;
line-height: 2;
}
.poem-main cite,
.poem-slip cite {
display: block;
margin-top: 1rem;
font-style: normal;
color: var(--ink-soft);
font-size: 0.9rem;
font-family: 'Noto Sans SC', sans-serif;
}
.poem-scatter {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
align-items: stretch;
margin-bottom: 2rem;
}
.poem-slip {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 1.25rem 1.1rem;
background: rgba(247, 242, 234, 0.95);
border-top: 2px solid var(--brand);
box-shadow: var(--shadow-soft);
box-sizing: border-box;
}
.poem-slip cite {
margin-top: auto;
padding-top: 0.85rem;
}
@media (max-width: 48rem) {
.poem-main {
writing-mode: horizontal-tb;
text-orientation: mixed;
min-height: auto;
}
.poem-scatter {
grid-template-columns: 1fr;
}
}
+57
View File
@@ -0,0 +1,57 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { images, poetry } from '../content/site'
import './Poetry.css'
export function PoetryPage() {
return (
<section className="poetry-page page-section">
<div className="poetry-bg" style={{ backgroundImage: `url(${images.poetry.inkC})` }} />
<div className="container poetry-shell">
<Reveal variant="anim-ink">
<p className="eyebrow">{poetry.eyebrow}</p>
<h1 className="page-title">{poetry.title}</h1>
<p className="lead">{poetry.lead}</p>
</Reveal>
<Reveal variant="anim-unroll" className="poem-stage">
<blockquote className="poem-main serif">
<p>{poetry.mainPoem.quote}</p>
<cite>{poetry.mainPoem.source}</cite>
</blockquote>
</Reveal>
<div className="poem-scatter">
{poetry.poems.map((poem, i) => (
<Reveal key={poem.source} variant="anim-ink" delayMs={150 + i * 160} className={`poem-slip slip-${i}`}>
<p className="serif">{poem.quote}</p>
<cite>{poem.source}</cite>
</Reveal>
))}
</div>
<Reveal variant="anim-rise">
<p className="lead">{poetry.reading}</p>
<p className="closing">{poetry.closing}</p>
</Reveal>
<div className="deep-read-rail">
<Reveal variant="anim-rise">
<h2 className="serif deep-read-title"></h2>
<p className="deep-read-lead"></p>
</Reveal>
<div className="deep-read-grid poetry-deep-grid">
{poetry.deepReads.map((item, i) => (
<Reveal key={item.path} variant="anim-rise" delayMs={i * 80}>
<Link to={item.path} className="deep-read-card">
<strong className="serif">{item.label}</strong>
<span>{item.blurb}</span>
</Link>
</Reveal>
))}
</div>
</div>
</div>
</section>
)
}
+138
View File
@@ -0,0 +1,138 @@
.qx-stage {
position: relative;
display: grid;
place-items: center;
min-height: 22rem;
margin: 1.5rem 0 2rem;
}
.qx-bottle {
width: min(14rem, 46vw);
z-index: 2;
}
.qx-bottle img {
width: 100%;
filter: drop-shadow(0 1rem 2rem rgba(26, 21, 18, 0.28));
}
.qx-rings {
position: absolute;
inset: 0;
pointer-events: none;
}
.qx-ring {
position: absolute;
width: 4.2rem;
height: 4.2rem;
border: 1px solid var(--brand);
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.75);
color: var(--brand);
font-size: 1.5rem;
}
.ring-0 {
left: 8%;
top: 18%;
}
.ring-1 {
right: 10%;
top: 12%;
}
.ring-2 {
right: 18%;
bottom: 16%;
}
.qx-style-text {
text-align: center;
margin-inline: auto;
}
.qx-cross {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
max-width: 48rem;
margin: 0 auto 2.5rem;
}
.qx-arm {
padding: 1.2rem 1.1rem;
border: 1px solid rgba(166, 28, 32, 0.14);
background: rgba(255, 255, 255, 0.5);
}
.qx-arm h3 {
margin: 0 0 0.5rem;
color: var(--brand);
font-size: 1.1rem;
}
.qx-arm p {
margin: 0;
color: var(--ink-soft);
font-size: 0.95rem;
}
.arm-0 {
border-right: none;
border-bottom: none;
}
.arm-1 {
border-bottom: none;
}
.arm-2 {
border-right: none;
}
.qx-split {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 2rem;
align-items: center;
}
.qx-h {
color: var(--brand);
margin: 1.2rem 0 0.45rem;
}
.qx-taste {
display: grid;
gap: 0.65rem;
margin: 1.25rem 0;
}
.qx-taste strong {
color: var(--brand);
margin-right: 0.65rem;
}
.qx-split .media-frame img {
width: 100%;
aspect-ratio: 16 / 10;
object-fit: contain;
background: rgba(0, 0, 0, 0.03);
}
@media (max-width: 48rem) {
.qx-cross,
.qx-split {
grid-template-columns: 1fr;
}
.qx-arm {
border: 1px solid rgba(166, 28, 32, 0.14) !important;
}
.qx-ring {
width: 3.2rem;
height: 3.2rem;
font-size: 1.15rem;
}
}
+85
View File
@@ -0,0 +1,85 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { images, qingxiang } from '../content/site'
import './Qingxiang.css'
export function QingxiangPage() {
return (
<section className="qx-page page-section">
<div className="container">
<Reveal variant="anim-rise">
<p className="eyebrow">{qingxiang.eyebrow}</p>
<h1 className="page-title">{qingxiang.title}</h1>
<p className="lead">{qingxiang.lead}</p>
</Reveal>
<div className="qx-stage">
<Reveal variant="anim-zoom" className="qx-bottle hero-float">
<img src={images.t30} alt={qingxiang.product} />
</Reveal>
<div className="qx-rings">
{qingxiang.styleWords.map((word, i) => (
<Reveal key={word} variant="anim-pop" delayMs={i * 140} className={`qx-ring ring-${i}`}>
<span className="serif">{word}</span>
</Reveal>
))}
</div>
</div>
<Reveal variant="anim-rise">
<p className="lead qx-style-text">{qingxiang.styleText}</p>
</Reveal>
<div className="qx-cross">
{qingxiang.positions.map((item, i) => (
<Reveal key={item.title} variant="anim-expand" delayMs={i * 90} className={`qx-arm arm-${i}`}>
<h3 className="serif">{item.title}</h3>
<p>{item.text}</p>
</Reveal>
))}
</div>
<div className="qx-split">
<Reveal variant="anim-slide-left">
<h3 className="serif qx-h"></h3>
<p>{qingxiang.craft}</p>
<h3 className="serif qx-h"></h3>
<p>{qingxiang.origin}</p>
<div className="qx-taste">
{qingxiang.tasting.map((step) => (
<div key={step.title}>
<strong className="serif">{step.title}</strong>
<span>{step.text}</span>
</div>
))}
</div>
<p className="closing">{qingxiang.mission}</p>
<p className="footnote">{qingxiang.product}</p>
</Reveal>
<Reveal variant="anim-slide-right">
<div className="media-frame">
<img src={images.qingxiang.hall} alt="杜康清香酒文化场景" loading="lazy" />
</div>
</Reveal>
</div>
<div className="deep-read-rail">
<Reveal variant="anim-rise">
<h2 className="serif deep-read-title"> · </h2>
<p className="deep-read-lead"></p>
</Reveal>
<div className="deep-read-grid">
{qingxiang.deepReads.map((item, i) => (
<Reveal key={item.path} variant="anim-rise" delayMs={i * 50}>
<Link to={item.path} className="deep-read-card">
<strong className="serif">{item.label}</strong>
<span>{item.blurb}</span>
</Link>
</Reveal>
))}
</div>
</div>
</div>
</section>
)
}
+59
View File
@@ -0,0 +1,59 @@
.subsidy-center {
text-align: center;
}
.subsidy-sub {
color: var(--gold);
letter-spacing: 0.12em;
margin: 0 0 1rem;
}
.big-number {
font-size: clamp(4.5rem, 3rem + 6vw, 8rem);
color: var(--gold);
line-height: 1;
margin: 1rem 0 1rem;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.9s var(--ease), transform 0.9s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.big-number.is-in {
opacity: 1;
transform: scale(1);
}
.number-note {
max-width: 40rem;
margin-inline: auto;
text-align: center;
}
.subsidy-ring {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
text-align: left;
margin: 2rem 0;
}
.subsidy-point {
padding: 0 0.25rem 0 1rem;
border-left: 2px solid var(--gold);
}
.subsidy-point h3 {
margin: 0 0 0.5rem;
color: var(--gold);
}
.subsidy-point p {
margin: 0;
color: var(--ink-soft);
}
@media (max-width: 48rem) {
.subsidy-ring {
grid-template-columns: 1fr;
}
}
+61
View File
@@ -0,0 +1,61 @@
import { useEffect, useRef, useState } from 'react'
import { Reveal } from '../components/Reveal'
import { subsidy } from '../content/site'
import './Subsidy.css'
export function SubsidyPage() {
const [showNumber, setShowNumber] = useState(false)
const numberRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const node = numberRef.current
if (!node) return
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setShowNumber(true)
observer.disconnect()
}
},
{ threshold: 0.35 },
)
observer.observe(node)
return () => observer.disconnect()
}, [])
return (
<section className="subsidy-page page-section">
<div className="container subsidy-center">
<Reveal variant="anim-rise">
<p className="eyebrow">{subsidy.eyebrow}</p>
<h1 className="page-title">{subsidy.title}</h1>
<p className="subsidy-sub">{subsidy.subtitle}</p>
<p className="lead" style={{ marginInline: 'auto', textAlign: 'center' }}>
{subsidy.lead}
</p>
</Reveal>
<div ref={numberRef} className={`big-number serif ${showNumber ? 'is-in' : ''}`}>
{subsidy.number}
</div>
<Reveal variant="anim-rise">
<p className="lead number-note">{subsidy.numberNote}</p>
</Reveal>
<div className="subsidy-ring">
{subsidy.points.map((point, i) => (
<Reveal key={point.title} variant="anim-diag" delayMs={i * 120} className="subsidy-point">
<h3 className="serif">{point.title}</h3>
<p>{point.text}</p>
</Reveal>
))}
</div>
<Reveal variant="anim-rise">
<p className="closing">{subsidy.closing}</p>
<p className="footnote">{subsidy.footnote}</p>
</Reveal>
</div>
</section>
)
}
+47
View File
@@ -0,0 +1,47 @@
.system-tri {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
margin: 2.5rem 0;
padding-top: 1rem;
}
.system-node {
padding: 1.35rem 1.2rem;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(166, 28, 32, 0.16);
min-height: 12rem;
}
.system-node h3 {
margin: 0 0 0.65rem;
color: var(--brand);
}
.system-node p {
margin: 0;
color: var(--ink-soft);
font-size: 0.95rem;
}
.system-node .caps {
margin-top: 0.9rem;
padding-top: 0.9rem;
border-top: 1px dashed rgba(166, 28, 32, 0.2);
font-size: 0.85rem;
}
.system-loop {
margin: 0;
padding: 1.2rem 1.4rem;
border: 1px dashed rgba(166, 28, 32, 0.28);
color: var(--brand);
font-size: 1.1rem;
}
@media (max-width: 48rem) {
.system-tri {
grid-template-columns: 1fr;
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { system } from '../content/site'
import './System.css'
export function SystemPage() {
return (
<section className="system-page page-section">
<div className="container">
<Reveal variant="anim-rise">
<p className="eyebrow">{system.eyebrow}</p>
<h1 className="page-title">{system.title}</h1>
<p className="lead">{system.lead}</p>
</Reveal>
<div className="system-tri">
{system.ends.map((item, i) => (
<Reveal key={item.title} variant="anim-expand" delayMs={i * 180} className={`system-node node-${i}`}>
<h3 className="serif">{item.title}</h3>
<p>{item.role}</p>
<p className="caps">{item.caps}</p>
</Reveal>
))}
</div>
<Reveal variant="anim-slide-right">
<p className="system-loop serif">{system.loop}</p>
<p className="closing">{system.closing}</p>
<div className="cta-row">
<Link className="btn btn-primary" to="/tech">
</Link>
</div>
</Reveal>
</div>
</section>
)
}
+101
View File
@@ -0,0 +1,101 @@
.tech-page {
position: relative;
overflow: hidden;
}
.tech-grid-bg {
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(196, 163, 90, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(196, 163, 90, 0.08) 1px, transparent 1px);
background-size: 2.5rem 2.5rem;
mask-image: radial-gradient(circle at center, #000 20%, transparent 75%);
pointer-events: none;
}
.tech-orbit {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-rows: 1fr;
gap: 1.25rem;
margin: 3rem 0 2rem;
align-items: stretch;
}
.tech-orbit > .reveal-base {
height: 100%;
min-height: 11rem;
display: flex;
}
.tech-core {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 6rem;
height: 6rem;
border-radius: 50%;
border: 1px solid var(--gold);
display: grid;
place-items: center;
text-align: center;
color: var(--gold);
letter-spacing: 0.1em;
background: rgba(28, 25, 31, 0.9);
z-index: 2;
font-size: 0.95rem;
pointer-events: none;
}
.tech-sat {
width: 100%;
height: 100%;
min-height: 11rem;
padding: 1.3rem 1.2rem;
background: var(--tech-card);
border: 1px solid var(--tech-line);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.tech-sat h3 {
margin: 0 0 0.55rem;
color: var(--gold);
flex-shrink: 0;
}
.tech-sat p {
margin: 0;
color: var(--ink-soft);
font-size: 0.95rem;
flex: 1;
}
.sat-0,
.sat-1,
.sat-2,
.sat-3 {
justify-self: stretch;
max-width: none;
margin-top: 0;
}
@media (max-width: 48rem) {
.tech-orbit {
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
.tech-core {
position: static;
transform: none;
margin: 0 auto 1rem;
}
.tech-sat {
min-height: 9rem;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { tech } from '../content/site'
import './Tech.css'
export function TechPage() {
return (
<section className="tech-page page-section">
<div className="tech-grid-bg" aria-hidden />
<div className="container">
<Reveal variant="anim-rise">
<p className="eyebrow">{tech.eyebrow}</p>
<h1 className="page-title">{tech.title}</h1>
<p className="lead">{tech.lead}</p>
</Reveal>
<div className="tech-orbit">
<div className="tech-core serif" aria-hidden>
</div>
{tech.points.map((point, i) => (
<Reveal key={point.title} variant="anim-expand" delayMs={i * 140} className={`tech-sat sat-${i}`}>
<h3 className="serif">{point.title}</h3>
<p>{point.text}</p>
</Reveal>
))}
</div>
<Reveal variant="anim-slide-left">
<p className="closing">{tech.claim}</p>
<div className="cta-row">
<Link className="btn btn-primary" to={tech.cta.to}>
{tech.cta.label}
</Link>
</div>
</Reveal>
</div>
</section>
)
}
+54
View File
@@ -0,0 +1,54 @@
.spec-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--tech-line);
border: 1px solid var(--tech-line);
margin: 2rem 0;
}
.spec-card {
background: var(--tech-card);
padding: 1.5rem 1.35rem;
position: relative;
}
.spec-no {
position: absolute;
top: 1rem;
right: 1.1rem;
color: rgba(196, 163, 90, 0.45);
font-size: 1.5rem;
}
.spec-card h3 {
margin: 0 0 0.65rem;
color: var(--gold);
}
.spec-card p {
margin: 0;
color: var(--ink-soft);
max-width: 28rem;
}
.spec-highlights {
list-style: none;
margin: 0 0 1.5rem;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.5rem;
}
.spec-highlights li {
color: var(--gold);
border-bottom: 1px solid rgba(196, 163, 90, 0.35);
padding-bottom: 0.2rem;
}
@media (max-width: 48rem) {
.spec-grid {
grid-template-columns: 1fr;
}
}
+50
View File
@@ -0,0 +1,50 @@
import { Link } from 'react-router-dom'
import { Reveal } from '../components/Reveal'
import { techDetail } from '../content/site'
import './TechDetail.css'
export function TechDetailPage() {
return (
<section className="tech-detail-page page-section">
<div className="container">
<Reveal variant="anim-rise">
<p className="eyebrow">{techDetail.eyebrow}</p>
<h1 className="page-title">{techDetail.title}</h1>
<p className="lead">{techDetail.lead}</p>
</Reveal>
<div className="spec-grid">
{techDetail.ports.map((port, i) => (
<Reveal
key={port.title}
variant={i % 2 === 0 ? 'anim-diag' : 'anim-slide-right'}
delayMs={i * 110}
className="spec-card"
>
<span className="spec-no serif">{String(i + 1).padStart(2, '0')}</span>
<h3 className="serif">{port.title}</h3>
<p>{port.text}</p>
</Reveal>
))}
</div>
<Reveal variant="anim-rise">
<ul className="spec-highlights">
{techDetail.highlights.map((item) => (
<li key={item} className="serif">
{item}
</li>
))}
</ul>
<p className="closing">{techDetail.closing}</p>
<p className="lead">{techDetail.contactHint}</p>
<div className="cta-row">
<Link className="btn btn-primary" to="/contact">
</Link>
</div>
</Reveal>
</div>
</section>
)
}
+298
View File
@@ -0,0 +1,298 @@
/* ===== Stage Shell Ambient Theme & HUD ===== */
.stage-shell {
position: relative;
width: 100vw;
height: 100vh;
height: 100dvh;
overflow: hidden;
background: #0e0c10;
background-image:
radial-gradient(circle at 15% 20%, rgba(166, 28, 32, 0.15) 0%, transparent 40%),
radial-gradient(circle at 85% 80%, rgba(196, 163, 90, 0.12) 0%, transparent 45%),
linear-gradient(180deg, #141117 0%, #0c0a0e 100%);
color: #f3ece4;
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
user-select: none;
}
/* Subtle grid overlay for tech/exhibition feel */
.stage-shell::before {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(196, 163, 90, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(196, 163, 90, 0.03) 1px, transparent 1px);
background-size: 4rem 4rem;
pointer-events: none;
z-index: 0;
}
/* HUD Top Bar with Safe Area Top */
.stage-hud-top {
position: absolute;
top: 0;
left: 0;
right: 0;
height: calc(4.5rem + env(safe-area-inset-top, 0px));
padding-top: env(safe-area-inset-top, 0px);
padding-left: 2.5rem;
padding-right: 2.5rem;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 50;
background: linear-gradient(180deg, rgba(14, 12, 16, 0.95) 0%, rgba(14, 12, 16, 0) 100%);
border-bottom: 1px solid rgba(196, 163, 90, 0.15);
backdrop-filter: blur(8px);
}
.stage-brand {
display: flex;
align-items: center;
gap: 0.85rem;
text-decoration: none;
}
.stage-logo {
width: 2.4rem;
height: 2.4rem;
object-fit: contain;
filter: drop-shadow(0 0 8px rgba(196, 163, 90, 0.4));
}
.stage-title {
font-family: 'Noto Serif SC', serif;
font-size: 1.25rem;
font-weight: 600;
letter-spacing: 0.12em;
color: var(--gold, #c4a35a);
background: linear-gradient(135deg, #f3ece4 0%, #c4a35a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stage-badge {
padding: 0.2rem 0.6rem;
border-radius: 0.2rem;
font-size: 0.75rem;
letter-spacing: 0.1em;
background: rgba(166, 28, 32, 0.25);
border: 1px solid rgba(166, 28, 32, 0.5);
color: #f3ece4;
}
/* Slide Nav Pills */
.stage-nav-pills {
display: flex;
align-items: center;
gap: 0.35rem;
background: rgba(28, 25, 31, 0.7);
padding: 0.3rem 0.5rem;
border-radius: 2rem;
border: 1px solid rgba(196, 163, 90, 0.2);
}
.stage-nav-pill {
padding: 0.35rem 0.85rem;
border-radius: 1.5rem;
font-size: 0.82rem;
color: rgba(243, 236, 228, 0.65);
text-decoration: none;
transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
white-space: nowrap;
}
.stage-nav-pill:hover {
color: #f3ece4;
background: rgba(196, 163, 90, 0.15);
}
.stage-nav-pill.is-active {
color: #121014;
background: linear-gradient(135deg, #e8d08d 0%, #c4a35a 100%);
font-weight: 600;
box-shadow: 0 0 12px rgba(196, 163, 90, 0.35);
}
/* AutoPlay Button, Exit & Index Indicator */
.stage-hud-actions {
display: flex;
align-items: center;
gap: 0.85rem;
}
.stage-autoplay-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
border-radius: 1.5rem;
font-size: 0.78rem;
color: rgba(243, 236, 228, 0.85);
background: rgba(28, 25, 31, 0.8);
border: 1px solid rgba(196, 163, 90, 0.3);
cursor: pointer;
transition: all 0.25s ease;
}
.stage-autoplay-btn.is-playing {
color: var(--gold, #c4a35a);
border-color: rgba(196, 163, 90, 0.6);
background: rgba(196, 163, 90, 0.15);
}
.stage-play-icon {
font-size: 0.7rem;
}
.stage-index-num {
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
letter-spacing: 0.1em;
color: rgba(196, 163, 90, 0.85);
}
.stage-exit-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 1rem;
border-radius: 0.2rem;
font-size: 0.82rem;
color: #f3ece4;
background: rgba(166, 28, 32, 0.3);
border: 1px solid rgba(166, 28, 32, 0.6);
cursor: pointer;
transition: all 0.25s ease;
text-decoration: none;
}
.stage-exit-btn:hover {
background: rgba(166, 28, 32, 0.7);
color: #ffffff;
box-shadow: 0 0 15px rgba(166, 28, 32, 0.4);
}
/* Main Viewport Container */
.stage-viewport {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: calc(4.5rem + env(safe-area-inset-top, 0px)) 2.5rem calc(2.5rem + env(safe-area-inset-bottom, 0px));
}
/* Bottom Progress & Hints with Safe Area Bottom */
.stage-hud-bottom {
position: absolute;
bottom: calc(0.85rem + env(safe-area-inset-bottom, 0px));
left: 0;
right: 0;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
pointer-events: none;
}
.stage-dots {
display: flex;
align-items: center;
gap: 0.5rem;
pointer-events: auto;
}
.stage-dot {
position: relative;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: rgba(196, 163, 90, 0.25);
border: none;
padding: 0;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
}
.stage-dot.is-active {
width: 2rem;
border-radius: 0.25rem;
background: rgba(196, 163, 90, 0.3);
}
.stage-dot-progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
background: linear-gradient(90deg, var(--brand, #a61c20), var(--gold, #c4a35a));
border-radius: 0.25rem;
transform-origin: left;
animation: stageProgressBar 6s linear forwards;
}
@keyframes stageProgressBar {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.stage-keyboard-hint {
font-size: 0.72rem;
color: rgba(243, 236, 228, 0.4);
letter-spacing: 0.08em;
}
/* Mobile Responsive Adjustments for Stage Shell */
@media (max-width: 48rem) {
.stage-hud-top {
padding-left: 1rem;
padding-right: 1rem;
height: calc(3.6rem + env(safe-area-inset-top, 0px));
}
.stage-nav-pills {
display: none;
}
.stage-viewport {
padding-top: calc(3.6rem + env(safe-area-inset-top, 0px));
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: calc(3rem + env(safe-area-inset-bottom, 0px));
}
.stage-exit-btn span:first-child {
display: none;
}
}
/* Animations */
.stage-view-anim {
width: 100%;
height: 100%;
animation: stageFadeIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes stageFadeIn {
from {
opacity: 0;
transform: scale(0.98) translateY(10px);
filter: blur(4px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
filter: blur(0);
}
}
+159
View File
@@ -0,0 +1,159 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { stageNavItems } from '../content/nav'
import { images } from '../content/site'
import { StageViews } from './StageViews'
import './StageShell.css'
const AUTOPLAY_INTERVAL_MS = 6000 // 6s per slide
export function StageShell() {
const location = useLocation()
const navigate = useNavigate()
const touchStartX = useRef<number | null>(null)
const [isAutoPlay, setIsAutoPlay] = useState(true)
const currentIndex = stageNavItems.findIndex((item) => item.path === location.pathname)
const activeIdx = currentIndex >= 0 ? currentIndex : 0
const goToSlide = useCallback(
(idx: number) => {
const target = stageNavItems[idx]
if (!target) return
const url = target.path === '/' ? '/?mode=stage' : `${target.path}?mode=stage`
navigate(url)
},
[navigate],
)
const handlePrev = useCallback(() => {
const prevIdx = (activeIdx - 1 + stageNavItems.length) % stageNavItems.length
goToSlide(prevIdx)
}, [activeIdx, goToSlide])
const handleNext = useCallback(() => {
const nextIdx = (activeIdx + 1) % stageNavItems.length
goToSlide(nextIdx)
}, [activeIdx, goToSlide])
// Auto-play interval timer
useEffect(() => {
if (!isAutoPlay) return
const timer = setInterval(() => {
handleNext()
}, AUTOPLAY_INTERVAL_MS)
return () => clearInterval(timer)
}, [isAutoPlay, handleNext])
// Touch Swipe navigation for mobile
const handleTouchStart = (e: React.TouchEvent) => {
touchStartX.current = e.touches[0].clientX
}
const handleTouchEnd = (e: React.TouchEvent) => {
if (touchStartX.current === null) return
const diffX = e.changedTouches[0].clientX - touchStartX.current
if (diffX > 50) {
handlePrev()
} else if (diffX < -50) {
handleNext()
}
touchStartX.current = null
}
// Keyboard navigation
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') {
e.preventDefault()
handleNext()
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault()
handlePrev()
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [handleNext, handlePrev])
const normalUrl = location.pathname === '/' ? '/' : location.pathname
return (
<div
className="stage-shell"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Top HUD */}
<header className="stage-hud-top">
<Link to="/?mode=stage" className="stage-brand">
<img src={images.logo} alt="杜康好客" className="stage-logo" />
<span className="stage-title"></span>
<span className="stage-badge"></span>
</Link>
{/* Module Navigation Pills */}
<nav className="stage-nav-pills">
{stageNavItems.map((item, idx) => (
<Link
key={item.path}
to={item.path === '/' ? '/?mode=stage' : `${item.path}?mode=stage`}
className={`stage-nav-pill ${idx === activeIdx ? 'is-active' : ''}`}
>
{item.label}
</Link>
))}
</nav>
{/* Index, AutoPlay Toggle & Exit */}
<div className="stage-hud-actions">
<button
type="button"
className={`stage-autoplay-btn ${isAutoPlay ? 'is-playing' : ''}`}
onClick={() => setIsAutoPlay((v) => !v)}
title={isAutoPlay ? '点击暂停自动轮播' : '点击开启自动轮播'}
>
<span className="stage-play-icon">{isAutoPlay ? '⏸' : '▶'}</span>
<span>{isAutoPlay ? '自动演示' : '已暂停'}</span>
</button>
<span className="stage-index-num">
{String(activeIdx + 1).padStart(2, '0')} / {String(stageNavItems.length).padStart(2, '0')}
</span>
<Link to={normalUrl} className="stage-exit-btn">
<span>退</span>
<span></span>
</Link>
</div>
</header>
{/* Main Exhibition Viewport */}
<main className="stage-viewport">
<div className="stage-view-anim" key={location.pathname}>
<StageViews activePath={stageNavItems[activeIdx].path} />
</div>
</main>
{/* Bottom Progress, Dots & Hints */}
<footer className="stage-hud-bottom">
<div className="stage-dots">
{stageNavItems.map((item, idx) => (
<button
key={item.path}
type="button"
className={`stage-dot ${idx === activeIdx ? 'is-active' : ''}`}
onClick={() => goToSlide(idx)}
aria-label={`跳转到 ${item.label}`}
>
{idx === activeIdx && isAutoPlay ? (
<span className="stage-dot-progress" key={location.pathname} />
) : null}
</button>
))}
</div>
<div className="stage-keyboard-hint"> 6 · </div>
</footer>
</div>
)
}
+672
View File
@@ -0,0 +1,672 @@
/* ===== Stage Views Presentation CSS ===== */
:root {
--stage-brand: #a61c20;
--stage-gold: #c4a35a;
--stage-gold-light: #e8d08d;
--stage-glass-bg: rgba(28, 25, 31, 0.78);
--stage-glass-border: rgba(196, 163, 90, 0.22);
--stage-glow: rgba(196, 163, 90, 0.15);
}
.stage-view-container {
width: 100%;
max-width: 82rem;
height: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
/* Glass Card Component */
.stage-card {
background: var(--stage-glass-bg);
border: 1px solid var(--stage-glass-border);
backdrop-filter: blur(14px);
border-radius: 0.5rem;
padding: 1.5rem 1.75rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
transition: all 0.35s cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
overflow: hidden;
}
.stage-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--stage-gold), transparent);
opacity: 0.6;
}
.stage-card:hover {
border-color: rgba(196, 163, 90, 0.5);
box-shadow: 0 12px 35px rgba(166, 28, 32, 0.25);
transform: translateY(-2px);
}
/* Common Header Typography */
.stage-eyebrow {
font-size: clamp(0.8rem, 1vw, 0.95rem);
color: var(--stage-gold);
letter-spacing: 0.2em;
margin: 0 0 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stage-eyebrow::before {
content: '';
width: 1.5rem;
height: 2px;
background: var(--stage-brand);
}
.stage-page-title {
font-family: 'Noto Serif SC', serif;
font-size: clamp(1.8rem, 2.5vw, 2.75rem);
font-weight: 600;
margin: 0 0 0.85rem;
color: #ffffff;
letter-spacing: 0.05em;
background: linear-gradient(135deg, #ffffff 0%, #f3ece4 60%, var(--stage-gold-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stage-lead {
font-size: clamp(0.95rem, 1.2vw, 1.15rem);
line-height: 1.6;
color: rgba(243, 236, 228, 0.8);
max-width: 52rem;
margin: 0 auto 1.5rem;
text-align: center;
}
.stage-closing {
font-family: 'Noto Serif SC', serif;
font-size: clamp(1rem, 1.3vw, 1.25rem);
color: var(--stage-gold);
letter-spacing: 0.1em;
margin-top: 1.25rem;
text-align: center;
padding-top: 0.85rem;
border-top: 1px solid rgba(196, 163, 90, 0.2);
width: 100%;
}
/* Stagger Animation Base */
.stage-stagger {
opacity: 0;
animation: stageSlideUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
animation-delay: calc(var(--i, 0) * 80ms);
}
@keyframes stageSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ==================== Slide 1: Home ==================== */
.stage-home-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 3rem;
align-items: center;
width: 100%;
}
.stage-home-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.stage-home-brand {
font-family: 'Noto Serif SC', serif;
font-size: clamp(2.5rem, 4vw, 4.2rem);
color: var(--stage-brand);
margin: 0;
letter-spacing: 0.15em;
text-shadow: 0 0 20px rgba(166, 28, 32, 0.4);
}
.stage-home-sub {
font-size: clamp(1.2rem, 1.8vw, 1.6rem);
color: var(--stage-gold);
letter-spacing: 0.12em;
margin: 0.2rem 0 1rem;
}
.stage-home-title {
font-family: 'Noto Serif SC', serif;
font-size: clamp(2rem, 3.2vw, 3.2rem);
color: #ffffff;
margin: 0 0 1rem;
letter-spacing: 0.08em;
line-height: 1.25;
}
.stage-home-desc {
font-size: clamp(1rem, 1.25vw, 1.25rem);
color: rgba(243, 236, 228, 0.82);
line-height: 1.75;
margin-bottom: 2rem;
max-width: 40rem;
}
.stage-home-visual {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.stage-home-visual img {
width: min(20rem, 36vh);
height: auto;
filter: drop-shadow(0 15px 30px rgba(0, 0, 0, 0.6));
animation: stageFloat 4s ease-in-out infinite;
}
.stage-pedestal {
position: absolute;
bottom: -1rem;
width: 18rem;
height: 2.5rem;
background: radial-gradient(ellipse at center, rgba(196, 163, 90, 0.4) 0%, rgba(166, 28, 32, 0.2) 50%, transparent 75%);
border-radius: 50%;
filter: blur(8px);
}
@keyframes stageFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
/* ==================== Slide 2: Brand ==================== */
.stage-brand-grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 2.5rem;
width: 100%;
align-items: center;
}
.stage-brand-cards {
display: grid;
gap: 1.1rem;
}
.stage-brand-card {
display: grid;
grid-template-columns: 3rem 1fr;
gap: 1rem;
align-items: start;
}
.stage-card-num {
font-family: 'Noto Serif SC', serif;
font-size: 1.5rem;
color: var(--stage-gold);
}
.stage-brand-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.25rem;
color: #ffffff;
margin: 0 0 0.4rem;
}
.stage-brand-card p {
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.75);
margin: 0;
line-height: 1.6;
}
.stage-brand-media {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.stage-brand-media img {
width: 100%;
max-height: 14rem;
object-fit: cover;
border-radius: 0.35rem;
border: 1px solid var(--stage-glass-border);
}
.stage-timeline-row {
display: flex;
justify-content: space-around;
width: 100%;
margin-top: 1.5rem;
padding: 1rem;
background: var(--stage-glass-bg);
border: 1px solid var(--stage-glass-border);
border-radius: 0.35rem;
}
.stage-timeline-item {
display: flex;
align-items: center;
gap: 0.65rem;
font-family: 'Noto Serif SC', serif;
font-size: 1.05rem;
color: var(--stage-gold-light);
}
.stage-timeline-dot {
width: 0.5rem;
height: 0.5rem;
background: var(--stage-brand);
border-radius: 50%;
box-shadow: 0 0 8px var(--stage-brand);
}
/* ==================== Slide 3: Poetry ==================== */
.stage-poetry-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2.5rem;
align-items: center;
width: 100%;
}
.stage-poem-main {
background: linear-gradient(145deg, rgba(38, 30, 42, 0.9), rgba(28, 25, 31, 0.85));
border: 1px solid rgba(196, 163, 90, 0.35);
padding: 2.2rem;
border-radius: 0.5rem;
text-align: center;
}
.stage-poem-quote {
font-family: 'Noto Serif SC', serif;
font-size: clamp(1.4rem, 2vw, 2rem);
line-height: 1.8;
color: var(--stage-gold-light);
margin: 0 0 1rem;
}
.stage-poem-cite {
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.6);
font-style: normal;
}
.stage-poem-slips {
display: grid;
gap: 1.1rem;
}
.stage-poem-slip p {
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
color: #ffffff;
margin: 0 0 0.4rem;
}
.stage-poem-slip cite {
font-size: 0.88rem;
color: var(--stage-gold);
font-style: normal;
}
/* ==================== Slide 4: Qingxiang ==================== */
.stage-qx-grid {
display: grid;
grid-template-columns: 0.85fr 0.8fr 0.85fr;
gap: 1.5rem;
align-items: center;
width: 100%;
}
.stage-qx-center {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.stage-qx-center img {
width: min(16rem, 30vh);
height: auto;
filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5));
animation: stageFloat 4s ease-in-out infinite;
}
.stage-qx-rings {
display: flex;
gap: 0.85rem;
margin-top: 1rem;
}
.stage-qx-ring {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
border: 1px solid var(--stage-gold);
background: rgba(166, 28, 32, 0.2);
display: grid;
place-items: center;
font-family: 'Noto Serif SC', serif;
font-size: 1.25rem;
color: var(--stage-gold-light);
box-shadow: 0 0 15px rgba(196, 163, 90, 0.2);
}
.stage-qx-list {
display: grid;
gap: 1rem;
}
.stage-qx-item h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.15rem;
color: var(--stage-gold);
margin: 0 0 0.3rem;
}
.stage-qx-item p {
font-size: 0.92rem;
color: rgba(243, 236, 228, 0.75);
margin: 0;
line-height: 1.55;
}
/* ==================== Slide 5: Haoke ==================== */
.stage-haoke-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2.5rem;
align-items: center;
width: 100%;
}
.stage-haoke-slogan {
font-family: 'Noto Serif SC', serif;
font-size: clamp(2rem, 3vw, 3rem);
color: var(--stage-brand);
margin: 0 0 1rem;
text-shadow: 0 0 15px rgba(166, 28, 32, 0.3);
}
.stage-haoke-mechanism {
display: grid;
gap: 1.2rem;
}
.stage-haoke-mech-card {
border-left: 3px solid var(--stage-brand);
}
.stage-haoke-mech-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.2rem;
color: var(--stage-gold-light);
margin: 0 0 0.4rem;
}
.stage-haoke-mech-card p {
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.78);
margin: 0;
line-height: 1.6;
}
/* ==================== Slide 6: System ==================== */
.stage-system-tri {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
width: 100%;
margin-bottom: 1.5rem;
}
.stage-system-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.3rem;
color: var(--stage-gold);
margin: 0 0 0.6rem;
}
.stage-system-card .role {
font-size: 0.98rem;
color: rgba(243, 236, 228, 0.9);
margin-bottom: 0.85rem;
line-height: 1.6;
}
.stage-system-card .caps {
font-size: 0.85rem;
color: rgba(196, 163, 90, 0.85);
padding-top: 0.75rem;
border-top: 1px dashed rgba(196, 163, 90, 0.25);
margin: 0;
}
.stage-loop-bar {
width: 100%;
padding: 1.2rem;
background: rgba(166, 28, 32, 0.15);
border: 1px dashed var(--stage-brand);
border-radius: 0.4rem;
text-align: center;
font-family: 'Noto Serif SC', serif;
font-size: 1.15rem;
color: var(--stage-gold-light);
}
/* ==================== Slide 7: Tech ==================== */
.stage-tech-orbit {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
width: 100%;
}
.stage-sat-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.25rem;
color: var(--stage-gold);
margin: 0 0 0.5rem;
}
.stage-sat-card p {
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.78);
margin: 0;
line-height: 1.6;
}
/* ==================== Slide 8: Subsidy ==================== */
.stage-subsidy-layout {
display: grid;
grid-template-columns: 0.85fr 1.15fr;
gap: 2.5rem;
align-items: center;
width: 100%;
}
.stage-subsidy-hero {
text-align: center;
background: radial-gradient(circle at center, rgba(166, 28, 32, 0.2) 0%, rgba(28, 25, 31, 0.8) 75%);
}
.stage-big-num {
font-family: 'Noto Serif SC', serif;
font-size: clamp(4.5rem, 8vw, 7.5rem);
font-weight: 700;
color: var(--stage-brand);
text-shadow: 0 0 30px rgba(166, 28, 32, 0.5);
line-height: 1;
margin: 0.5rem 0;
}
.stage-subsidy-cards {
display: grid;
gap: 1.2rem;
}
.stage-subsidy-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.2rem;
color: var(--stage-gold);
margin: 0 0 0.4rem;
}
.stage-subsidy-card p {
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.78);
margin: 0;
line-height: 1.6;
}
/* ==================== Slide 9: Partner ==================== */
.stage-partner-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
width: 100%;
}
.stage-partner-section h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.25rem;
color: var(--stage-brand);
margin: 0 0 0.85rem;
border-bottom: 1px solid rgba(166, 28, 32, 0.3);
padding-bottom: 0.4rem;
}
.stage-partner-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.85rem;
}
.stage-partner-list li {
display: flex;
align-items: flex-start;
gap: 0.65rem;
font-size: 0.95rem;
color: rgba(243, 236, 228, 0.85);
line-height: 1.55;
}
.stage-partner-list li::before {
content: '◆';
color: var(--stage-gold);
font-size: 0.8rem;
margin-top: 0.2rem;
}
/* ==================== Slide 10: Tech Detail ==================== */
.stage-ports-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
width: 100%;
}
.stage-port-card h3 {
font-family: 'Noto Serif SC', serif;
font-size: 1.2rem;
color: var(--stage-gold);
margin: 0 0 0.5rem;
}
.stage-port-card p {
font-size: 0.92rem;
color: rgba(243, 236, 228, 0.78);
margin: 0;
line-height: 1.6;
}
/* ==================== Slide 11: Contact ==================== */
.stage-contact-box {
display: flex;
align-items: center;
justify-content: center;
gap: 3.5rem;
padding: 2.5rem 3.5rem;
background: linear-gradient(135deg, rgba(38, 30, 42, 0.9), rgba(28, 25, 31, 0.85));
border: 1px solid var(--stage-glass-border);
border-radius: 0.6rem;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5);
}
.stage-qr-frame {
width: 11rem;
height: 11rem;
padding: 0.65rem;
background: #ffffff;
border-radius: 0.4rem;
box-shadow: 0 0 25px rgba(196, 163, 90, 0.3);
}
.stage-qr-frame img {
width: 100%;
height: 100%;
object-fit: contain;
}
.stage-contact-info h2 {
font-family: 'Noto Serif SC', serif;
font-size: 2rem;
color: var(--stage-brand);
margin: 0 0 0.3rem;
}
.stage-contact-info .role {
font-size: 1.1rem;
color: var(--stage-gold);
margin-bottom: 1.25rem;
}
.stage-contact-info .phone {
font-size: 1.8rem;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.08em;
margin-bottom: 0.85rem;
}
.stage-contact-info .hint {
font-size: 0.9rem;
color: rgba(243, 236, 228, 0.6);
margin: 0;
}
/* Responsive Scaling for Ultra-wide / Smaller Display Height */
@media (max-height: 800px) {
.stage-view-container {
transform: scale(0.92);
}
}
@media (max-height: 700px) {
.stage-view-container {
transform: scale(0.82);
}
}
+511
View File
@@ -0,0 +1,511 @@
import { Link } from 'react-router-dom'
import {
brand,
contact,
haoke,
hero,
images,
partner,
poetry,
qingxiang,
subsidy,
system,
tech,
techDetail,
} from '../content/site'
import './StageViews.css'
interface StageViewsProps {
activePath: string
}
export function StageViews({ activePath }: StageViewsProps) {
switch (activePath) {
case '/':
return <StageHomeView />
case '/brand':
return <StageBrandView />
case '/poetry':
return <StagePoetryView />
case '/qingxiang':
return <StageQingxiangView />
case '/haoke':
return <StageHaokeView />
case '/system':
return <StageSystemView />
case '/tech':
return <StageTechView />
case '/subsidy':
return <StageSubsidyView />
case '/partner':
return <StagePartnerView />
case '/tech-detail':
return <StageTechDetailView />
case '/contact':
return <StageContactView />
default:
return <StageHomeView />
}
}
/* 1. Home View */
function StageHomeView() {
return (
<div className="stage-view-container">
<div className="stage-home-grid">
<div className="stage-home-copy">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{hero.subBrand} ·
</div>
<h1 className="stage-home-brand stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{hero.brand}
</h1>
<div className="stage-home-sub stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
</div>
<h2 className="stage-home-title stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
{hero.title}
</h2>
<p className="stage-home-desc stage-stagger" style={{ '--i': 5 } as React.CSSProperties}>
{hero.subtitle}
</p>
<div className="stage-stagger" style={{ '--i': 6 } as React.CSSProperties}>
<Link to="/brand?mode=stage" className="stage-exit-btn" style={{ padding: '0.6rem 1.5rem', fontSize: '1rem' }}>
</Link>
</div>
</div>
<div className="stage-home-visual stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<img src={images.t30} alt="酒祖杜康 T30" />
<div className="stage-pedestal" />
</div>
</div>
</div>
)
}
/* 2. Brand View */
function StageBrandView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{brand.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{brand.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{brand.lead}
</p>
<div className="stage-brand-grid">
<div className="stage-brand-cards">
{brand.cards.map((card, idx) => (
<div
key={card.title}
className="stage-card stage-brand-card stage-stagger"
style={{ '--i': 4 + idx } as React.CSSProperties}
>
<div className="stage-card-num">0{idx + 1}</div>
<div>
<h3>{card.title}</h3>
<p>{card.text}</p>
</div>
</div>
))}
</div>
<div className="stage-brand-media stage-stagger" style={{ '--i': 6 } as React.CSSProperties}>
<img src={images.brand.manor} alt="杜康庄园" />
<img src={images.brand.relic} alt="造酒遗址" />
</div>
</div>
<div className="stage-timeline-row stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{brand.timeline.map((item) => (
<div key={item} className="stage-timeline-item">
<span className="stage-timeline-dot" />
<span>{item}</span>
</div>
))}
</div>
</div>
)
}
/* 3. Poetry View */
function StagePoetryView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{poetry.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{poetry.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{poetry.lead}
</p>
<div className="stage-poetry-layout">
<div className="stage-poem-main stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<p className="stage-poem-quote">{poetry.mainPoem.quote}</p>
<cite className="stage-poem-cite"> {poetry.mainPoem.source}</cite>
</div>
<div className="stage-poem-slips">
{poetry.poems.map((poem, idx) => (
<div
key={poem.source}
className="stage-card stage-poem-slip stage-stagger"
style={{ '--i': 5 + idx } as React.CSSProperties}
>
<p>{poem.quote}</p>
<cite> {poem.source}</cite>
</div>
))}
</div>
</div>
<div className="stage-closing stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{poetry.closing}
</div>
</div>
)
}
/* 4. Qingxiang View */
function StageQingxiangView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{qingxiang.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{qingxiang.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{qingxiang.lead}
</p>
<div className="stage-qx-grid">
<div className="stage-qx-list">
{qingxiang.positions.slice(0, 2).map((pos, idx) => (
<div
key={pos.title}
className="stage-card stage-qx-item stage-stagger"
style={{ '--i': 4 + idx } as React.CSSProperties}
>
<h3>{pos.title}</h3>
<p>{pos.text}</p>
</div>
))}
</div>
<div className="stage-qx-center stage-stagger" style={{ '--i': 5 } as React.CSSProperties}>
<img src={images.t30} alt="酒祖杜康清香 T30" />
<div className="stage-qx-rings">
{qingxiang.styleWords.map((word) => (
<div key={word} className="stage-qx-ring">
{word}
</div>
))}
</div>
</div>
<div className="stage-qx-list">
{qingxiang.positions.slice(2, 4).map((pos, idx) => (
<div
key={pos.title}
className="stage-card stage-qx-item stage-stagger"
style={{ '--i': 6 + idx } as React.CSSProperties}
>
<h3>{pos.title}</h3>
<p>{pos.text}</p>
</div>
))}
</div>
</div>
<div className="stage-closing stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{qingxiang.mission}
</div>
</div>
)
}
/* 5. Haoke View */
function StageHaokeView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{haoke.eyebrow}
</div>
<h2 className="stage-haoke-slogan stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{haoke.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{haoke.lead}
</p>
<div className="stage-haoke-grid">
<div className="stage-card stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<h3 style={{ fontFamily: 'Noto Serif SC', fontSize: '1.4rem', color: '#ffffff', margin: '0 0 0.8rem' }}>
</h3>
<div
style={{
padding: '1.2rem',
background: 'rgba(166, 28, 32, 0.2)',
border: '1px solid var(--stage-brand)',
borderRadius: '0.4rem',
fontSize: '1.15rem',
color: 'var(--stage-gold-light)',
fontFamily: 'Noto Serif SC',
marginBottom: '1rem',
}}
>
{haoke.oneLiner}
</div>
<p style={{ fontSize: '0.95rem', color: 'rgba(243,236,228,0.8)', lineHeight: 1.6 }}>{haoke.closing}</p>
</div>
<div className="stage-haoke-mechanism">
{haoke.points.map((pt, idx) => (
<div
key={pt.title}
className="stage-card stage-haoke-mech-card stage-stagger"
style={{ '--i': 5 + idx } as React.CSSProperties}
>
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
</div>
</div>
)
}
/* 6. System View */
function StageSystemView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{system.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{system.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{system.lead}
</p>
<div className="stage-system-tri">
{system.ends.map((end, idx) => (
<div
key={end.title}
className="stage-card stage-system-card stage-stagger"
style={{ '--i': 4 + idx } as React.CSSProperties}
>
<h3>{end.title}</h3>
<div className="role">{end.role}</div>
<p className="caps">{end.caps}</p>
</div>
))}
</div>
<div className="stage-loop-bar stage-stagger" style={{ '--i': 7 } as React.CSSProperties}>
{system.loop}
</div>
</div>
)
}
/* 7. Tech View */
function StageTechView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{tech.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{tech.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{tech.lead}
</p>
<div className="stage-tech-orbit">
{tech.points.map((pt, idx) => (
<div
key={pt.title}
className="stage-card stage-sat-card stage-stagger"
style={{ '--i': 4 + idx } as React.CSSProperties}
>
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
<div className="stage-closing stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{tech.claim}
</div>
</div>
)
}
/* 8. Subsidy View */
function StageSubsidyView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{subsidy.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{subsidy.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{subsidy.lead}
</p>
<div className="stage-subsidy-layout">
<div className="stage-card stage-subsidy-hero stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<div style={{ fontSize: '1.1rem', color: 'var(--stage-gold)' }}></div>
<div className="stage-big-num">{subsidy.number}</div>
<p style={{ fontSize: '0.9rem', color: 'rgba(243,236,228,0.75)' }}>{subsidy.numberNote}</p>
</div>
<div className="stage-subsidy-cards">
{subsidy.points.map((pt, idx) => (
<div
key={pt.title}
className="stage-card stage-subsidy-card stage-stagger"
style={{ '--i': 5 + idx } as React.CSSProperties}
>
<h3>{pt.title}</h3>
<p>{pt.text}</p>
</div>
))}
</div>
</div>
<div className="stage-closing stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{subsidy.closing}
</div>
</div>
)
}
/* 9. Partner View */
function StagePartnerView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{partner.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{partner.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{partner.lead}
</p>
<div className="stage-partner-grid">
<div className="stage-card stage-partner-section stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<h3></h3>
<ul className="stage-partner-list">
{partner.points.slice(0, 3).map((pt) => (
<li key={pt.title}>
<strong>{pt.title}</strong> {pt.text}
</li>
))}
</ul>
</div>
<div className="stage-card stage-partner-section stage-stagger" style={{ '--i': 5 } as React.CSSProperties}>
<h3></h3>
<ul className="stage-partner-list">
{partner.points.slice(3, 5).map((pt) => (
<li key={pt.title}>
<strong>{pt.title}</strong> {pt.text}
</li>
))}
</ul>
</div>
</div>
<div className="stage-stagger" style={{ '--i': 7, marginTop: '1.5rem' } as React.CSSProperties}>
<Link to="/contact?mode=stage" className="stage-exit-btn" style={{ padding: '0.6rem 1.5rem', fontSize: '1rem' }}>
</Link>
</div>
</div>
)
}
/* 10. Tech Detail View */
function StageTechDetailView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{techDetail.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{techDetail.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{techDetail.lead}
</p>
<div className="stage-ports-grid">
{techDetail.ports.map((port, idx) => (
<div
key={port.title}
className="stage-card stage-port-card stage-stagger"
style={{ '--i': 4 + idx } as React.CSSProperties}
>
<h3>{port.title}</h3>
<p>{port.text}</p>
</div>
))}
</div>
<div className="stage-closing stage-stagger" style={{ '--i': 8 } as React.CSSProperties}>
{techDetail.closing}
</div>
</div>
)
}
/* 11. Contact View */
function StageContactView() {
return (
<div className="stage-view-container">
<div className="stage-eyebrow stage-stagger" style={{ '--i': 1 } as React.CSSProperties}>
{contact.eyebrow}
</div>
<h2 className="stage-page-title stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
{contact.title}
</h2>
<p className="stage-lead stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
{contact.lead}
</p>
<div className="stage-contact-box stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
<div className="stage-qr-frame">
<img src={images.qr} alt="微信二维码" />
</div>
<div className="stage-contact-info">
<h2>{contact.name}</h2>
<div className="role">{contact.role}</div>
<div className="phone">{contact.phone}</div>
<p className="hint">{contact.wechatHint}</p>
</div>
</div>
</div>
)
}
+291
View File
@@ -0,0 +1,291 @@
@keyframes fadeRise {
from {
opacity: 0;
transform: translateY(1.25rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes softZoom {
from {
opacity: 0;
transform: scale(1.08);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideLeft {
from {
opacity: 0;
transform: translateX(-2rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideRight {
from {
opacity: 0;
transform: translateX(2rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes unroll {
from {
opacity: 0;
clip-path: inset(0 0 100% 0);
}
to {
opacity: 1;
clip-path: inset(0 0 0 0);
}
}
@keyframes inkBloom {
from {
opacity: 0;
filter: blur(8px);
transform: scale(0.96);
}
to {
opacity: 1;
filter: blur(0);
transform: scale(1);
}
}
@keyframes popScale {
from {
opacity: 0;
transform: scale(0.5);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes letterSweep {
from {
opacity: 0;
letter-spacing: 0.55em;
}
to {
opacity: 1;
letter-spacing: 0.08em;
}
}
@keyframes floatY {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-0.6rem);
}
}
@keyframes drawLineY {
from {
transform: scaleY(0);
}
to {
transform: scaleY(1);
}
}
@keyframes expandOut {
from {
opacity: 0;
transform: scale(0.7);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes diagIn {
from {
opacity: 0;
transform: translate(1.5rem, 1.5rem);
}
to {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes meetInLeft {
from {
opacity: 0;
transform: translateX(-3rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes meetInRight {
from {
opacity: 0;
transform: translateX(3rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.reveal-base {
opacity: 0;
}
.reveal-base.is-in {
opacity: 1;
}
.anim-rise {
transform: translateY(1.25rem);
transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}
.anim-rise.is-in {
transform: translateY(0);
}
.anim-slide-left {
transform: translateX(-2rem);
transition: opacity 0.85s var(--ease), transform 0.85s var(--ease);
}
.anim-slide-left.is-in {
transform: translateX(0);
}
.anim-slide-right {
transform: translateX(2rem);
transition: opacity 0.85s var(--ease), transform 0.85s var(--ease);
}
.anim-slide-right.is-in {
transform: translateX(0);
}
.anim-zoom {
transform: scale(1.08);
transition: opacity 1s var(--ease), transform 1s var(--ease);
}
.anim-zoom.is-in {
transform: scale(1);
}
.anim-unroll {
clip-path: inset(0 0 100% 0);
transition: opacity 1s var(--ease), clip-path 1.1s var(--ease);
}
.anim-unroll.is-in {
clip-path: inset(0 0 0 0);
}
.anim-ink {
filter: blur(8px);
transform: scale(0.96);
transition: opacity 1s var(--ease), filter 1s var(--ease), transform 1s var(--ease);
}
.anim-ink.is-in {
filter: blur(0);
transform: scale(1);
}
.anim-pop {
transform: scale(0.5);
transition: opacity 0.6s var(--ease), transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.anim-pop.is-in {
transform: scale(1);
}
.anim-sweep {
letter-spacing: 0.55em;
transition: opacity 1s var(--ease), letter-spacing 1.1s var(--ease);
}
.anim-sweep.is-in {
letter-spacing: 0.08em;
}
.anim-expand {
transform: scale(0.7);
transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}
.anim-expand.is-in {
transform: translate(0) scale(1);
}
.anim-diag {
transform: translate(1.5rem, 1.5rem);
transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
}
.anim-diag.is-in {
transform: translate(0, 0);
}
.anim-meet-left {
transform: translateX(-3rem);
transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.anim-meet-left.is-in {
transform: translateX(0);
}
.anim-meet-right {
transform: translateX(3rem);
transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.anim-meet-right.is-in {
transform: translateX(0);
}
.hero-float {
animation: floatY 4.5s ease-in-out infinite;
}
.media-frame img,
.hero-visual img {
transition: transform 0.8s var(--ease);
}
.media-frame:hover img {
transform: scale(1.03);
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.reveal-base,
.hero-float,
.media-frame img,
.btn,
.page-enter {
animation: none !important;
transition: none !important;
opacity: 1 !important;
transform: none !important;
filter: none !important;
clip-path: none !important;
letter-spacing: normal !important;
}
}
+97
View File
@@ -0,0 +1,97 @@
@media (max-width: 48rem) {
:root {
--nav-h: 3.75rem;
}
.desktop-only {
display: none !important;
}
.page-section {
padding: 3.5rem 0 !important;
}
.split {
grid-template-columns: 1fr !important;
gap: 1.75rem !important;
}
.hero {
min-height: auto !important;
padding: 5.5rem 0 3rem !important;
}
.hero-grid {
grid-template-columns: 1fr !important;
gap: 2rem !important;
}
.hero-title {
font-size: 2rem !important;
}
.module-grid,
.card-grid,
.ends-grid,
.ports-grid,
.qx-cross,
.qx-split,
.spec-grid,
.subsidy-ring,
.partner-layout,
.brand-layout,
.contact-meet,
.system-tri,
.tech-orbit {
grid-template-columns: 1fr !important;
}
.module-rail {
margin-inline: -0.25rem;
}
.poem-main {
writing-mode: horizontal-tb !important;
text-orientation: mixed !important;
min-height: auto !important;
padding: 1.5rem !important;
}
.timeline {
flex-direction: column !important;
align-items: flex-start !important;
}
.contact-grid {
grid-template-columns: 1fr !important;
}
.nav-links {
display: none;
}
.photo-placeholder {
max-width: 100% !important;
}
.loop-line {
flex-direction: column;
align-items: flex-start;
}
}
@media (min-width: 48.0625rem) {
.mobile-only {
display: none !important;
}
}
@media (min-width: 90rem) {
.page-title {
font-size: 3rem;
}
.hero-title {
font-size: 3.5rem !important;
}
}
+242
View File
@@ -0,0 +1,242 @@
:root {
--brand: #a61c20;
--brand-deep: #7d1216;
--brand-soft: rgba(166, 28, 32, 0.08);
--gold: #c4a35a;
--gold-soft: rgba(196, 163, 90, 0.35);
--ink: #1a1512;
--ink-soft: #5c534c;
--paper: #f7f2ea;
--paper-deep: #ebe3d6;
--white: #ffffff;
--tech-bg: #121014;
--tech-card: #1c191f;
--tech-line: rgba(196, 163, 90, 0.28);
--max: min(72rem, 92vw);
--nav-h: 4.5rem;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--shadow-soft: 0 1.25rem 3rem rgba(26, 21, 18, 0.08);
}
/* rem base: 375px -> 14px, 1920px -> 18px */
html {
font-size: clamp(0.875rem, 0.75rem + 0.35vw, 1.125rem);
scroll-behavior: smooth;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
background: var(--paper);
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
img {
max-width: 100%;
display: block;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
cursor: pointer;
border: none;
background: none;
}
h1,
h2,
h3,
.serif {
font-family: 'Noto Serif SC', 'Songti SC', serif;
font-weight: 600;
line-height: 1.35;
letter-spacing: 0.02em;
}
.container {
width: var(--max);
margin-inline: auto;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
padding: 0.7rem 1.6rem;
border-radius: 0.15rem;
transition: background 0.35s var(--ease), color 0.35s var(--ease), transform 0.35s var(--ease),
box-shadow 0.35s var(--ease);
}
.btn-primary {
background: var(--brand);
color: var(--white);
}
.btn-primary:hover {
background: var(--brand-deep);
transform: translateY(-0.1rem);
box-shadow: var(--shadow-soft);
}
.btn-ghost {
color: var(--brand);
border: 1px solid var(--brand);
background: transparent;
}
.btn-ghost:hover {
background: var(--brand-soft);
}
.btn-light {
background: var(--white);
color: var(--brand);
}
.btn-light:hover {
transform: translateY(-0.1rem);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.55rem;
color: var(--brand);
font-size: 0.875rem;
letter-spacing: 0.18em;
text-transform: none;
margin-bottom: 0.85rem;
}
.eyebrow::before {
content: '';
width: 1.5rem;
height: 1px;
background: var(--brand);
}
.page-title {
font-size: clamp(1.75rem, 1.4rem + 1.2vw, 2.75rem);
margin: 0 0 1rem;
}
.lead {
font-size: 1.05rem;
color: var(--ink-soft);
max-width: 42rem;
margin: 0 0 2rem;
}
.closing {
margin-top: 2.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--gold-soft);
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
color: var(--ink);
}
.theme-tech {
--ink: #f3ece4;
--ink-soft: rgba(243, 236, 228, 0.72);
--paper: var(--tech-bg);
color: var(--ink);
background: var(--tech-bg);
}
.theme-tech .eyebrow,
.theme-tech .closing {
color: var(--gold);
}
.theme-tech .eyebrow::before {
background: var(--gold);
}
.theme-tech .closing {
border-top-color: var(--tech-line);
}
.theme-tech .btn-ghost {
color: var(--gold);
border-color: var(--gold);
}
.theme-tech .btn-ghost:hover {
background: rgba(196, 163, 90, 0.12);
}
.deep-read-rail {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid rgba(166, 28, 32, 0.12);
}
.deep-read-title {
margin: 0 0 0.45rem;
color: var(--brand);
font-size: 1.45rem;
letter-spacing: 0.08em;
}
.deep-read-lead {
margin: 0 0 1.35rem;
color: var(--ink-soft);
}
.deep-read-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.85rem 1rem;
}
.poetry-deep-grid {
grid-template-columns: 1fr 1fr;
}
.deep-read-card {
display: grid;
gap: 0.35rem;
padding: 1.15rem 1.2rem;
background: rgba(255, 255, 255, 0.55);
border-left: 3px solid var(--gold);
transition: transform 0.25s var(--ease), background 0.25s var(--ease);
}
.deep-read-card:hover {
transform: translateY(-0.15rem);
background: rgba(255, 255, 255, 0.85);
}
.deep-read-card strong {
color: var(--brand);
font-size: 1.05rem;
}
.deep-read-card span {
color: var(--ink-soft);
font-size: 0.92rem;
line-height: 1.55;
}
@media (max-width: 48rem) {
.deep-read-grid,
.poetry-deep-grid {
grid-template-columns: 1fr;
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />