TIJIAO
@@ -1,24 +1,6 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.local
|
||||
_tmp_*.txt
|
||||
.vite
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 杜康好客官网
|
||||
|
||||
React + Vite + TypeScript 多页品牌官网。文案对齐《杜康好客-官网文案定稿》,视觉素材取自品牌传承 / 清香宝典 / 诗酒文化三份 PDF。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 路由
|
||||
|
||||
- `/` 首页
|
||||
- `/brand` 品牌传承
|
||||
- `/poetry` 诗酒文化
|
||||
- `/qingxiang` 清香
|
||||
- `/haoke` 好客模式
|
||||
- `/system` 三端协同
|
||||
- `/tech` 科技驱动
|
||||
- `/subsidy` 酒厂实力
|
||||
- `/partner` 城市合伙人
|
||||
- `/tech-detail` 科技板块
|
||||
- `/contact` 联系我们
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。" />
|
||||
<title>杜康好客 · 酒祖杜康</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "dukang-haoke-official",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 6.4 MiB |
|
After Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 7.9 MiB |
|
After Width: | Height: | Size: 900 KiB |
|
After Width: | Height: | Size: 787 KiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 585 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 609 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 254 KiB |
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: '当酒厂资源与城市运营、门店核销连成闭环,酒祖的清香便从典籍走进餐桌,从纪念走进日常。',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -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: '读唐宋余韵,是在听一条悠长的回声:每个时代都借杜康说出自己的忧与喜。酒未冷,诗未央。',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -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 分钟)',
|
||||
'白酒鉴赏玩法 2~3 种',
|
||||
'DIY 自调酒:留下专属标签',
|
||||
'品鉴宴席与盲品',
|
||||
'领导总结与欢送',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
text: '溯源之旅',
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: '走进厂区与遗址,看见粮、曲、窖、酒的真实路径。清香的「一清到底」,最终要在现场被看见、被闻到、被记住。空间叙事与产线真实,共同完成信任交付。',
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: '文化馆不是展板堆砌,而是把酒祖、清香与好客连成可跟随的路径。每一次到访,都应留下可复述的故事与可回访的关系。',
|
||||
},
|
||||
],
|
||||
closing: '让每一次到访,都成为一次清雅之约。',
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}/`))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||