Compare commits

...

6 Commits

Author SHA1 Message Date
ljy 404613a109 修改 2026-08-11 01:44:32 +08:00
ljy cfafec8a3c fix:二维码修改 2026-08-03 21:49:23 +08:00
ljy ee0c35cc34 fix:提交修改适配移动端 2026-08-03 21:14:34 +08:00
ljy 85bb826ca1 fix:修改样式 2026-07-29 23:17:36 +08:00
ljy ffc6b7414f fix:修改首页的动画 2026-07-29 23:02:06 +08:00
ljy 855be4d3ad fix:修改样式 2026-07-29 22:43:28 +08:00
136 changed files with 15370 additions and 1464 deletions
+1
View File
@@ -10,6 +10,7 @@ alwaysApply: false
- `hero``system``.poetic-section`(宣纸/国风:`--paper`、`--ink`、`--red`、`--gold`
- `tech``contact``.tech-section`(深色科技:`--tech`、`--tech-card`、`--tech-line`
- 普通模式 `activeIndex >= tech` 时顶栏加 `is-tech-theme`;展控(`?mode=stage`)不加
- 新增章节必须选对主题族;勿混用两套配色令牌
## 改章节
+3 -21
View File
@@ -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
+26 -27
View File
@@ -1,32 +1,31 @@
# React + TypeScript + Vite
# 杜康好客官网
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
React + Vite + TypeScript 多页品牌官网。文案对齐《杜康好客-官网文案定稿》,视觉素材取自品牌传承 / 清香宝典 / 诗酒文化三份 PDF。
Currently, two official plugins are available:
## 开发
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```bash
npm install
npm run dev
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
## 构建
```bash
npm run build
npm run preview
```
## 路由
- `/` 首页
- `/brand` 品牌传承
- `/poetry` 诗酒文化
- `/qingxiang` 清香
- `/haoke` 好客模式
- `/system` 三端协同
- `/tech` 科技驱动
- `/subsidy` 酒厂实力
- `/partner` 城市合伙人
- `/tech-detail` 科技板块
- `/contact` 联系我们
+11 -7
View File
@@ -2,13 +2,17 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="杜康好客品牌官网,你请客我买单。发布会展项单页展示,支持展控模式。"
/>
<title>杜康好客品牌官网|你请客,我买单</title>
<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="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。" />
<link rel="canonical" href="https://www.dukanghaoke.com/" />
<meta property="og:url" content="https://www.dukanghaoke.com/" />
<meta property="og:title" content="杜康好客 · 酒祖杜康" />
<meta property="og: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>
+1252 -718
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -1,25 +1,23 @@
{
"name": "dukangguanwang",
"name": "dukang-haoke-official",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7"
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
},
"devDependencies": {
"@types/node": "^24.13.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"oxlint": "^1.71.0",
"typescript": "~6.0.2",
"vite": "^8.1.1"
"@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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+494 -16
View File
@@ -65,6 +65,36 @@ a {
box-shadow: 0 1.125rem 4.375rem rgba(83, 42, 18, 0.14);
backdrop-filter: blur(1rem);
transform: translateX(-50%);
transition: background 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
}
.topbar.is-tech-theme {
border-color: rgba(201, 154, 69, 0.28);
background: rgba(8, 9, 15, 0.78);
box-shadow: 0 1.125rem 4.375rem rgba(0, 0, 0, 0.35);
}
.topbar.is-tech-theme .brand-lockup strong,
.topbar.is-tech-theme .brand-lockup small {
color: #fff4df;
transition: color 0.35s ease;
}
.topbar.is-tech-theme .nav-links {
color: rgba(241, 217, 154, 0.82);
transition: color 0.35s ease;
}
.topbar.is-tech-theme .nav-links a:hover,
.topbar.is-tech-theme .nav-cta:hover {
color: var(--gold-light);
}
.topbar.is-tech-theme .nav-cta {
border-color: rgba(201, 154, 69, 0.42);
color: var(--gold-light);
background: rgba(255, 244, 223, 0.06);
transition: color 0.35s ease, border-color 0.35s ease, background 0.35s ease, transform 0.2s ease;
}
.brand-lockup {
@@ -370,16 +400,26 @@ a {
display: grid;
min-height: 0;
width: 100%;
height: min(62vh, 34rem);
max-height: 62vh;
height: min(68vh, 38rem);
max-height: 68vh;
place-items: center;
border-radius: 1.5rem;
overflow: hidden;
overflow: visible;
background: transparent;
}
.hero-visual::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at center, rgba(8, 10, 14, 0.92) 0%, rgba(8, 10, 14, 0.55) 48%, transparent 72%);
radial-gradient(circle closest-side at center, rgba(8, 10, 14, 0.92) 0%, rgba(8, 10, 14, 0.55) 58%, transparent 100%);
pointer-events: none;
z-index: 0;
}
.hero-bottle {
position: relative;
z-index: 1;
width: auto;
height: min(56vh, 30rem);
max-width: min(36vw, 22rem);
@@ -930,7 +970,6 @@ blockquote cite {
.tech-grid article,
.ports-grid article,
.partner-list span,
.qr-card,
.stage-panel {
border: 1px solid var(--tech-line);
background: var(--tech-card);
@@ -1149,18 +1188,46 @@ blockquote cite {
font-weight: 800;
}
.contact-phone {
display: inline-block;
text-decoration: none;
-webkit-tap-highlight-color: rgba(241, 217, 154, 0.35);
touch-action: manipulation;
cursor: pointer;
}
.qr-card {
display: grid;
justify-items: center;
gap: 1rem;
padding: 1.75rem;
align-self: center;
gap: 1.35rem;
margin-block: 1.75rem;
padding: 3.75rem 1.75rem 3.5rem;
border: 1px solid rgba(201, 154, 69, 0.2);
border-radius: 1.25rem;
background: rgba(255, 255, 255, 0.045);
box-shadow: inset 0 1px 0 rgba(255, 244, 223, 0.06);
backdrop-filter: blur(1.125rem);
}
.qr-card img {
width: 15rem;
height: 15rem;
border-radius: 1.125rem;
width: 14.5rem;
height: 14.5rem;
padding: 0.65rem;
border-radius: 0.75rem;
background: #fff;
box-sizing: border-box;
object-fit: contain;
}
.qr-card span {
max-width: 13.5rem;
margin: 0;
color: rgba(255, 244, 223, 0.72);
font-size: 0.8125rem;
letter-spacing: 0.08em;
line-height: 1.6;
text-align: center;
}
.footer {
@@ -1529,6 +1596,11 @@ blockquote cite {
display: none;
}
.is-stage .hero-actions,
.is-stage .section-cta {
display: none;
}
.is-stage .footer {
display: none;
}
@@ -1817,13 +1889,43 @@ blockquote cite {
height: 4rem;
}
.is-stage .contact-section .qr-card {
margin-block: 1.75rem !important;
padding: 3.75rem 1.75rem 3.5rem !important;
gap: 1.35rem !important;
border: 1px solid rgba(201, 154, 69, 0.2);
border-radius: 1.25rem;
background: rgba(255, 255, 255, 0.045);
box-shadow: inset 0 1px 0 rgba(255, 244, 223, 0.06);
backdrop-filter: blur(1.125rem);
}
.is-stage .contact-section .qr-card img {
width: min(22rem, 32vh) !important;
height: min(22rem, 32vh) !important;
padding: 0.85rem !important;
border-radius: 0.75rem;
background: #fff;
box-sizing: border-box;
object-fit: contain;
}
.is-stage .contact-section .qr-card span {
max-width: 13.5rem;
color: rgba(255, 244, 223, 0.72);
font-size: 0.8125rem !important;
letter-spacing: 0.08em;
line-height: 1.6 !important;
text-align: center;
}
.is-stage .stage-panel-toggle {
right: 1rem;
bottom: 3.625rem;
}
@keyframes floatBottle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-0.75rem); }
0%, 100% { transform: translateY(0) rotate(-10deg); }
50% { transform: translateY(-2rem) rotate(0deg); }
}
@media (max-width: 1100px) {
@@ -1962,7 +2064,7 @@ blockquote cite {
.stage-dots {
display: none;
}
}\n
}
/* ===== Viewport fit: lock each screen to 100vw x 100vh ===== */
.page-section {
@@ -2113,13 +2215,20 @@ blockquote cite {
}
.qr-card {
padding: 0.75rem !important;
gap: 0.5rem !important;
margin-block: clamp(0.75rem, 2vh, 1.5rem) !important;
padding: 2.25rem 0.9rem 2.1rem !important;
gap: 0.85rem !important;
}
.qr-card img {
width: min(28vh, 11rem) !important;
height: min(28vh, 11rem) !important;
padding: 0.45rem !important;
}
.qr-card span {
font-size: clamp(0.7rem, 1.2vh, 0.8125rem) !important;
line-height: 1.5 !important;
}
.contact-person strong {
@@ -2695,3 +2804,372 @@ body.stage-mode {
transform: none !important;
}
}
/* ===== Mobile (≤760px): each section = one screen; PC/stage unchanged ===== */
@media (max-width: 760px) {
.site:not(.is-stage) .page-section {
height: 100vh !important;
height: 100svh !important;
max-height: 100vh !important;
max-height: 100svh !important;
min-height: 100vh !important;
min-height: 100svh !important;
overflow-x: hidden !important;
overflow-y: auto !important;
align-content: center !important;
align-content: safe center !important;
justify-items: stretch !important;
align-items: stretch !important;
gap: 1.1rem !important;
padding: 5rem 1.15rem 2rem !important;
scroll-snap-align: start;
scroll-snap-stop: always;
-webkit-overflow-scrolling: touch;
}
.site:not(.is-stage) .page-section > *:not(.ink-cloud):not(.tech-orbit) {
min-height: 0;
max-width: 100%;
min-width: 0;
}
/* Wrap-friendly typography on narrow screens */
.site:not(.is-stage) .brand-main {
letter-spacing: 0.12em !important;
overflow-wrap: anywhere;
word-break: break-word;
}
.site:not(.is-stage) .brand-sub,
.site:not(.is-stage) .eyebrow {
letter-spacing: 0.16em !important;
white-space: normal !important;
overflow-wrap: anywhere;
}
.site:not(.is-stage) .hero-copy h1,
.site:not(.is-stage) .section-heading h2,
.site:not(.is-stage) .poetry-main-card h2,
.site:not(.is-stage) .craft-card h3 {
letter-spacing: 0.02em !important;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.25 !important;
}
.site:not(.is-stage) .hero-text,
.site:not(.is-stage) .section-heading p,
.site:not(.is-stage) .poetry-main-card p,
.site:not(.is-stage) .lead,
.site:not(.is-stage) .closing-line,
.site:not(.is-stage) .closing-inline,
.site:not(.is-stage) .loop-line,
.site:not(.is-stage) .mode-line,
.site:not(.is-stage) .footnote,
.site:not(.is-stage) .dark-footnote,
.site:not(.is-stage) .mission-line,
.site:not(.is-stage) .product-tag,
.site:not(.is-stage) .contact-hint,
.site:not(.is-stage) .contact-role {
max-width: 100% !important;
white-space: normal !important;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.6 !important;
}
.site:not(.is-stage) .section-heading,
.site:not(.is-stage) .hero-copy {
width: 100%;
max-width: 100%;
min-width: 0;
}
.site:not(.is-stage) .paper-card span,
.site:not(.is-stage) .three-columns span,
.site:not(.is-stage) .position-grid h3,
.site:not(.is-stage) .partner-list span {
letter-spacing: 0.08em !important;
white-space: normal !important;
overflow-wrap: anywhere;
}
.site:not(.is-stage) .data-band strong {
letter-spacing: 0.02em !important;
overflow-wrap: anywhere;
word-break: break-word;
}
.site:not(.is-stage) .data-band span,
.site:not(.is-stage) .data-band small,
.site:not(.is-stage) .paper-card p,
.site:not(.is-stage) .three-columns p,
.site:not(.is-stage) .tech-grid p,
.site:not(.is-stage) .ports-grid p,
.site:not(.is-stage) .system-grid p,
.site:not(.is-stage) .subsidy-points p,
.site:not(.is-stage) .partner-detail-grid p,
.site:not(.is-stage) .position-grid p {
white-space: normal !important;
overflow-wrap: anywhere;
word-break: break-word;
}
.site:not(.is-stage) .primary-button,
.site:not(.is-stage) .secondary-button,
.site:not(.is-stage) .nav-cta {
white-space: normal !important;
text-align: center;
line-height: 1.35;
}
/* Qingxiang: single column, spacing, no overlap */
.site:not(.is-stage) .qingxiang-section .feature-layout {
grid-template-columns: 1fr !important;
max-height: none !important;
gap: 1.5rem !important;
}
.site:not(.is-stage) .qingxiang-section .data-band {
grid-template-columns: 1fr !important;
gap: 0 !important;
}
.site:not(.is-stage) .qingxiang-section .data-band div {
padding: 1rem 0 !important;
border: none !important;
border-bottom: 1px solid rgba(167, 25, 34, 0.18) !important;
}
.site:not(.is-stage) .qingxiang-section .data-band div:last-child {
border-bottom: none !important;
}
.site:not(.is-stage) .qingxiang-section .data-band strong {
font-size: clamp(1.75rem, 8vw, 2.5rem) !important;
line-height: 1.15;
}
.site:not(.is-stage) .qingxiang-section .data-band span,
.site:not(.is-stage) .qingxiang-section .data-band small {
font-size: 0.8125rem !important;
line-height: 1.55 !important;
margin-top: 0.35rem;
}
.site:not(.is-stage) .qingxiang-section .section-heading p {
display: block !important;
-webkit-line-clamp: unset;
overflow: visible !important;
}
.site:not(.is-stage) .qingxiang-section .position-grid {
grid-template-columns: 1fr !important;
gap: 0.75rem !important;
margin-top: 0.25rem;
}
.site:not(.is-stage) .qingxiang-section .position-grid article {
min-height: 0 !important;
padding: 0.75rem 0 0.75rem 0.85rem !important;
}
.site:not(.is-stage) .qingxiang-section .position-grid h3 {
font-size: 1rem !important;
}
.site:not(.is-stage) .qingxiang-section .position-grid p {
font-size: 0.875rem !important;
line-height: 1.55 !important;
}
.site:not(.is-stage) .qingxiang-section .craft-card {
padding: 0 !important;
overflow: visible !important;
}
.site:not(.is-stage) .qingxiang-section .craft-card h3 {
font-size: 1.5rem !important;
}
.site:not(.is-stage) .qingxiang-section .craft-card > p:not(.craft-extra):not(.mission-line):not(.product-tag) {
display: block !important;
font-size: 0.875rem !important;
line-height: 1.6 !important;
-webkit-line-clamp: unset;
overflow: visible !important;
}
.site:not(.is-stage) .qingxiang-section .tasting-detail {
grid-template-columns: 1fr !important;
gap: 0 !important;
margin-top: 0.75rem !important;
}
.site:not(.is-stage) .qingxiang-section .tasting-detail div {
display: flex !important;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0 !important;
border: none !important;
border-bottom: 1px solid rgba(122, 75, 38, 0.14) !important;
}
.site:not(.is-stage) .qingxiang-section .tasting-detail div:last-child {
border-bottom: none !important;
}
.site:not(.is-stage) .qingxiang-section .tasting-detail strong {
width: 2.25rem !important;
height: 2.25rem !important;
margin: 0 !important;
flex-shrink: 0;
font-size: 1rem !important;
}
.site:not(.is-stage) .qingxiang-section .tasting-detail p,
.site:not(.is-stage) .qingxiang-section .mission-line,
.site:not(.is-stage) .qingxiang-section .product-tag {
font-size: 0.8125rem !important;
line-height: 1.5 !important;
}
.site:not(.is-stage) .qingxiang-section .store-figure,
.site:not(.is-stage) .qingxiang-section .footnote,
.site:not(.is-stage) .qingxiang-section .craft-extra,
.site:not(.is-stage) .qingxiang-section .ornament {
display: none !important;
}
/* Hero */
.site:not(.is-stage) .hero-section {
grid-template-columns: 1fr !important;
}
.site:not(.is-stage) .hero-visual {
height: auto !important;
max-height: none !important;
min-height: 16rem;
margin-block: 0.5rem 1rem;
}
.site:not(.is-stage) .hero-bottle {
height: min(42vh, 18rem) !important;
max-width: min(70vw, 14rem) !important;
}
.site:not(.is-stage) .hero-actions {
flex-direction: column;
align-items: stretch;
}
/* Brand / poetry / other grids */
.site:not(.is-stage) .brand-layout {
grid-template-columns: 1fr !important;
max-height: none !important;
}
.site:not(.is-stage) .heritage-figure {
display: none;
}
.site:not(.is-stage) .heritage-grid,
.site:not(.is-stage) .timeline,
.site:not(.is-stage) .three-columns,
.site:not(.is-stage) .tech-grid,
.site:not(.is-stage) .ports-grid,
.site:not(.is-stage) .system-grid,
.site:not(.is-stage) .subsidy-points,
.site:not(.is-stage) .partner-detail-grid,
.site:not(.is-stage) .partner-list,
.site:not(.is-stage) .data-band {
grid-template-columns: 1fr !important;
}
.site:not(.is-stage) .feature-layout {
max-height: none !important;
grid-template-columns: 1fr !important;
}
.site:not(.is-stage) .paper-card,
.site:not(.is-stage) .three-columns article,
.site:not(.is-stage) .tech-grid article,
.site:not(.is-stage) .ports-grid article,
.site:not(.is-stage) .partner-detail-grid article,
.site:not(.is-stage) .system-grid article,
.site:not(.is-stage) .subsidy-points article {
overflow: visible !important;
min-height: 0 !important;
}
.site:not(.is-stage) .paper-card p,
.site:not(.is-stage) .three-columns p,
.site:not(.is-stage) .tech-grid p,
.site:not(.is-stage) .ports-grid p,
.site:not(.is-stage) .partner-detail-grid p,
.site:not(.is-stage) .subsidy-points p,
.site:not(.is-stage) .system-grid p {
display: block !important;
-webkit-line-clamp: unset;
overflow: visible !important;
font-size: 0.875rem !important;
line-height: 1.55 !important;
}
.site:not(.is-stage) .poetry-section {
grid-template-columns: 1fr !important;
}
.site:not(.is-stage) .poetry-tags {
display: none;
}
.site:not(.is-stage) .contact-section {
grid-template-columns: 1fr !important;
}
.site:not(.is-stage) .contact-person {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.site:not(.is-stage) .contact-phone {
padding: 0.35rem 0;
border-bottom: 1px solid rgba(241, 217, 154, 0.45);
color: var(--gold-light);
font-size: clamp(1.35rem, 6vw, 1.85rem) !important;
letter-spacing: 0.04em;
pointer-events: auto;
position: relative;
z-index: 2;
}
.site:not(.is-stage) .qr-card {
margin-block: 1rem !important;
padding: 1.75rem 1.25rem !important;
width: 100%;
max-width: 20rem;
justify-self: center;
}
.site:not(.is-stage) .qr-card img {
width: min(70vw, 14.5rem) !important;
height: min(70vw, 14.5rem) !important;
max-width: none !important;
max-height: none !important;
padding: 0.65rem !important;
border-radius: 0.75rem;
background: #fff;
box-sizing: border-box;
object-fit: contain !important;
}
.site:not(.is-stage) .footer.in-section {
margin-top: 1.5rem;
}
.site:not(.is-stage) .subsidy-number {
font-size: clamp(4rem, 22vw, 7rem) !important;
}
}
+36 -642
View File
@@ -1,646 +1,40 @@
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react'
import './App.css'
const sections = [
{ id: 'hero', label: 'LP-H 首屏', title: '你请客,我买单', summary: '杜康好客:你请客,我买单。买酒送等价好客权益,到合作门店抵餐。' },
{ id: 'brand', label: 'LP-01 品牌传承', title: '品牌传承', summary: '杜康被尊为秫酒酒宗、酒祖与酒神,是华夏酿造文明的精神图腾。' },
{ id: 'poetry', label: 'LP-02 诗酒文化', title: '诗酒文化', summary: '何以解忧,唯有杜康。曹操《短歌行》主诗签,苏轼、白居易、辛弃疾辅诗签。' },
{ id: 'qingxiang', label: 'LP-03 清香宝典', title: '清香', summary: '千年杜康,醉美清香。绵·净·雅,杏花村同源同脉,一清到底。' },
{ id: 'haoke', label: 'LP-04 好客模式', title: '好客模式', summary: '买酒送等价好客权益;到合作门店按面额抵餐。酒餐同赴,线上线下一体。' },
{ id: 'system', label: 'LP-05 三端协同', title: '三端协同', summary: '城市合伙人拓店 → 用户购酒得权益 → 门店核销结算 → 再反哺城市运营。' },
{ id: 'tech', label: 'LP-06 科技驱动', title: '科技驱动', summary: '四端一体、全链路闭环、宴请场景数字化、城市运营中台。' },
{ id: 'subsidy', label: 'LP-07 酒厂实力', title: '酒厂实力', summary: '十亿级支持,与餐饮共度寒冬。支持细则以合作政策为准。' },
{ id: 'partner', label: 'LP-08 城市合伙人', title: '城市合伙人', summary: '携酒祖入城,共建一座城的好客。了解合作方式,不承诺收益。' },
{ id: 'tech-detail', label: 'LP-09 科技板块', title: '科技板块', summary: '可运营、可结算、可规模化的好客系统:用户端、门店核销端、合伙人端、总部管控。' },
{ id: 'contact', label: 'LP-10 联系我们', title: '联系我们', summary: '扫码咨询项目负责人吕丹,电话 13073729990。无留资表单。' },
]
const navItems = [
{ href: '#brand', label: '品牌传承' },
{ href: '#poetry', label: '诗酒文化' },
{ href: '#qingxiang', label: '清香' },
{ href: '#haoke', label: '好客模式' },
{ href: '#system', label: '三端协同' },
{ href: '#tech', label: '科技驱动' },
{ href: '#subsidy', label: '酒厂实力' },
{ href: '#partner', label: '城市合伙人' },
{ href: '#tech-detail', label: '科技板块' },
{ href: '#contact', label: '联系我们' },
]
const heritageCards = [
{
title: '导语',
text: '杜康,被尊为秫酒「酒宗」「酒祖」,更被世人敬奉为「酒神」。从典籍到诗篇,从故里到祠祀,杜康不只是一杯酒的名字——它是华夏酿造文明的精神图腾。',
},
{
title: '典籍立祖',
text: '《世本》载「杜康作秫酒」;《说文解字》谓「少康,杜康也」。酒之所兴,肇自上皇——杜康造酒,写入中华文明的源头叙事。',
},
{
title: '故里承源',
text: '杜康村、杜康泉、空桑涧……酒祖故里的山水与作坊,延续着「以秫为骨、以曲为魂」的古老智慧,让技进乎道,香传千年。',
},
{
title: '酒以成礼',
text: '从封坛祭祖到「饮惟祀」的酒德观念,杜康文化把粮食之精,升华为礼乐之器——国之大事,在祀在戎,酒以成礼。',
},
]
const timeline = [
'先秦典籍立名',
'魏晋诗性转化',
'杜康祠祀千年',
'当代酒祖品牌复兴',
]
const qingxiangPositions = [
{ title: '企业定位', text: '承千年清韵,铸清香白酒标杆企业。' },
{ title: '品质定位', text: '守古法清酿,酿纯粹绵柔清雅好酒。' },
{ title: '品牌定位', text: '传酒祖清魂,塑高端清香文化名酒。' },
{ title: '营销定位', text: '拓清香赛道,引全民清雅饮酒新风尚。' },
]
const tastingSteps = [
{ title: '抿', text: '微启触香,品粮香本味' },
{ title: '咂', text: '舌卷含香,品绵柔入喉' },
{ title: '呵', text: '缓缓呵气,品回甘生津' },
]
const systemEnds = [
{
title: '城市合伙人',
role: '开城运营者:拓店、组仓配、带团队,让品牌在一座城真正落地。',
caps: '门店拓展与入驻协同 · 推广与代下单 · 佣金与经营数据可见',
},
{
title: '合作门店',
role: '好客权益核销端:把用户带回餐桌,用核销结算反哺门店经营。',
caps: '手机号/核销码核销 · 核销记录可查 · 结算路径清晰',
},
{
title: '用户',
role: '购酒与权益的享受者:小程序下单,酒到席、权益到店。',
caps: '清香选购 · 1:1 好客权益 · 同城配送/现场提货',
},
]
const techPoints = [
{
title: '四端一体',
text: '用户小程序、门店核销、城市合伙人、总部管控——权限分离、数据同源,开城可复制。',
},
{
title: '全链路闭环',
text: '下单履约、好客权益发放、门店核销、结算回款可视可追,减少「口头承诺、纸面对账」。',
},
{
title: '宴请场景数字化',
text: '面向商务宴请高频人群:酒到席、权益到店,把一次消费同时服务「喝」与「吃」。',
},
{
title: '城市运营中台',
text: '拓店、推广归因、经营数据与热力洞察,服务合伙人把一座城「盘清楚、推得动」。',
},
]
const techPorts = [
{
title: '用户端',
text: '清香选购与展示 · 下单履约(同城配送/现场提货) · 好客权益领取与使用 · 售后工单与发票入口',
},
{
title: '门店核销端',
text: '手机号/核销码核销 · 核销记录可查 · 结算与提现协同(细则以政策为准)',
},
{
title: '城市合伙人端',
text: '门店资料与入驻协同 · 推广/代下单 · 佣金与经营数据可见 · 子账号与权限管理',
},
{
title: '总部管控与数据',
text: '城市开通与审核 · 全链路订单/核销洞察 · 热力与经营分析 · 结算打款协同',
},
]
function App() {
const isStageMode = useMemo(() => new URLSearchParams(window.location.search).get('mode') === 'stage', [])
const [activeIndex, setActiveIndex] = useState(0)
const [autoPlay, setAutoPlay] = useState(false)
const [panelOpen, setPanelOpen] = useState(true)
const activeSection = sections[activeIndex] ?? sections[0]
useEffect(() => {
document.body.classList.toggle('stage-mode', isStageMode)
return () => document.body.classList.remove('stage-mode')
}, [isStageMode])
useEffect(() => {
if (!isStageMode) return
const hashIndex = sections.findIndex((section) => `#${section.id}` === window.location.hash)
if (hashIndex >= 0) setActiveIndex(hashIndex)
}, [isStageMode])
useEffect(() => {
if (isStageMode) return
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]
if (visible?.target.id) {
const nextIndex = sections.findIndex((section) => section.id === visible.target.id)
if (nextIndex >= 0) setActiveIndex(nextIndex)
}
},
{ threshold: [0.35, 0.6, 0.8] },
)
sections.forEach((section) => {
const node = document.getElementById(section.id)
if (node) observer.observe(node)
})
return () => observer.disconnect()
}, [isStageMode])
const goToSection = useCallback((index: number) => {
const safeIndex = Math.max(0, Math.min(index, sections.length - 1))
setActiveIndex(safeIndex)
if (isStageMode) {
window.history.replaceState(null, '', `?mode=stage#${sections[safeIndex].id}`)
return
}
document.getElementById(sections[safeIndex].id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, [isStageMode])
useEffect(() => {
if (!isStageMode) return
const root = document.getElementById('root')
root?.setAttribute('tabindex', '-1')
root?.focus({ preventScroll: true })
const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key
const code = event.code
const isNext = key === 'ArrowRight' || code === 'ArrowRight' || key === 'ArrowDown' || code === 'ArrowDown' || key === 'PageDown' || code === 'PageDown' || key === ' '
const isPrev = key === 'ArrowLeft' || code === 'ArrowLeft' || key === 'ArrowUp' || code === 'ArrowUp' || key === 'PageUp' || code === 'PageUp'
const isHome = key === 'Home' || code === 'Home'
const isEnd = key === 'End' || code === 'End'
if (!isNext && !isPrev && !isHome && !isEnd) return
event.preventDefault()
event.stopPropagation()
setActiveIndex((current) => {
let next = current
if (isNext) next = Math.min(current + 1, sections.length - 1)
if (isPrev) next = Math.max(current - 1, 0)
if (isHome) next = 0
if (isEnd) next = sections.length - 1
window.history.replaceState(null, '', `?mode=stage#${sections[next].id}`)
return next
})
}
window.addEventListener('keydown', handleKeyDown, true)
return () => {
window.removeEventListener('keydown', handleKeyDown, true)
root?.removeAttribute('tabindex')
}
}, [isStageMode])
useEffect(() => {
if (!isStageMode || !autoPlay) return
const timer = window.setInterval(() => {
setActiveIndex((current) => {
const next = current >= sections.length - 1 ? 0 : current + 1
window.history.replaceState(null, '', `?mode=stage#${sections[next].id}`)
return next
})
}, 8000)
return () => window.clearInterval(timer)
}, [autoPlay, isStageMode])
const progress = ((activeIndex + 1) / sections.length) * 100
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 (
<div className={isStageMode ? 'site is-stage' : 'site'}>
<header className="topbar">
<a className="brand-lockup" href="#hero" onClick={() => goToSection(0)}>
<img className="haoke-logo-img" src="/assets/haoke-logo.png" alt="杜康好客" />
<span>
<strong></strong>
<small></small>
</span>
</a>
<nav className="nav-links" aria-label="主导航">
{navItems.map((item) => (
<a href={item.href} key={item.href}>{item.label}</a>
))}
</nav>
<a className="nav-cta" href="#contact"></a>
</header>
<main
className={isStageMode ? 'stage-track' : undefined}
style={isStageMode ? { transform: `translate3d(-${activeIndex * 100}vw, 0, 0)` } : undefined}
>
<section className="page-section hero-section poetic-section reveal-section" id="hero">
<div className="ink-cloud ink-cloud-one" />
<div className="hero-copy">
<p className="brand-main"></p>
<p className="brand-sub"> · </p>
<h1></h1>
<p className="hero-text"></p>
<div className="hero-actions">
<a className="primary-button" href="#haoke"></a>
<a className="secondary-button" href="#partner"></a>
</div>
</div>
<div className="hero-visual" aria-label="杜康好客清香系列 T30 产品视觉">
<img className="hero-bottle" src="/assets/t30-hero-bottle.png" alt="杜康好客清香系列 T30" />
</div>
</section>
<section className={`page-section poetic-section brand-section${activeIndex === 1 ? ' is-entered' : ''}`} id="brand">
<div className="section-heading">
<img className="ornament haoke-mark" src="/assets/haoke-logo.png" alt="杜康好客" />
<span className="eyebrow"></span>
<h2></h2>
</div>
<div className="brand-layout">
<div className="heritage-grid">
{heritageCards.map((card, index) => (
<article className="paper-card" key={card.title} style={{ '--slide-i': index } as CSSProperties}>
<span>{card.title}</span>
<p>{card.text}</p>
</article>
))}
</div>
<figure className="heritage-figure">
<img src="/assets/heritage-scene.png" alt="酒祖杜康祠堂造像" />
<figcaption> · </figcaption>
</figure>
</div>
<div className="timeline" aria-label="品牌传承时间线">
{timeline.map((item, index) => (
<div className="timeline-item" key={item} style={{ '--slide-i': index } as CSSProperties}>{item}</div>
))}
</div>
<p className="closing-line"></p>
</section>
<section className={`page-section poetic-section poetry-section${activeIndex === 2 ? ' is-entered' : ''}`} id="poetry">
<div className="poetry-main-card reveal-card">
<img className="ornament haoke-mark" src="/assets/haoke-logo.png" alt="杜康好客" />
<span className="eyebrow"></span>
<h2></h2>
<p className="lead"></p>
<blockquote>
<span></span>
<strong></strong>
<cite> · </cite>
</blockquote>
<p></p>
<p className="closing-inline">穿</p>
</div>
<div className="poetry-tags">
<article style={{ '--slide-i': 0 } as CSSProperties}>
<b></b>
<span></span>
<small> · </small>
</article>
<article style={{ '--slide-i': 1 } as CSSProperties}>
<b></b>
<span>便</span>
<small> · </small>
</article>
<article style={{ '--slide-i': 2 } as CSSProperties}>
<b></b>
<span></span>
<small> · </small>
</article>
</div>
</section>
<section className="page-section poetic-section qingxiang-section" id="qingxiang">
<div className="section-heading">
<img className="ornament haoke-mark" src="/assets/haoke-logo.png" alt="杜康好客" />
<span className="eyebrow"></span>
<h2></h2>
<p>西</p>
</div>
<div className="data-band">
<div>
<strong>1000亿</strong>
<span>202413%350亿</span>
<small>2025</small>
</div>
<div>
<strong>45.6%</strong>
<span>202545.6%43.04%41.76%</span>
<small></small>
</div>
</div>
<div className="feature-layout">
<div className="position-grid">
{qingxiangPositions.map((item) => (
<article key={item.title}>
<h3>{item.title}</h3>
<p>{item.text}</p>
</article>
))}
</div>
<div className="craft-card">
<h3> · · </h3>
<p></p>
<p className="craft-extra">7321</p>
<div className="tasting-detail">
{tastingSteps.map((step) => (
<div key={step.title}>
<strong>{step.title}</strong>
<p>{step.text}</p>
</div>
))}
</div>
<p className="mission-line"></p>
<p className="product-tag"> · T30</p>
</div>
</div>
<figure className="store-figure">
<img src="/assets/store-preview.png" alt="杜康清香酒文馆效果示意" />
<figcaption> · </figcaption>
</figure>
<p className="footnote"></p>
</section>
<section className="page-section poetic-section haoke-section" id="haoke">
<div className="section-heading centered">
<span className="eyebrow"></span>
<h2></h2>
<p>广 + </p>
<p className="mode-line"></p>
</div>
<div className="three-columns">
<article>
<span>01</span>
<h3></h3>
<p></p>
</article>
<article>
<span>02</span>
<h3>线线</h3>
<p></p>
</article>
<article>
<span>03</span>
<h3></h3>
<p></p>
</article>
</div>
<p className="closing-line"></p>
<div className="section-cta">
<a className="primary-button" href="#system"></a>
</div>
</section>
<section className="page-section poetic-section system-section" id="system">
<div className="section-heading">
<span className="eyebrow"></span>
<h2> · · </h2>
<p></p>
</div>
<div className="system-grid">
{systemEnds.map((item) => (
<article key={item.title}>
<h3>{item.title}</h3>
<p>{item.role}</p>
<small>{item.caps}</small>
</article>
))}
</div>
<p className="loop-line"> </p>
<p className="closing-line">线</p>
<div className="section-cta">
<a className="primary-button" href="#tech"></a>
</div>
</section>
<section className="page-section tech-section tech-drive-section" id="tech">
<div className="tech-orbit" />
<div className="section-heading dark-heading">
<span className="eyebrow"></span>
<h2></h2>
<p>线</p>
</div>
<div className="tech-grid">
{techPoints.map((item) => (
<article key={item.title}>
<span>{item.title}</span>
<p>{item.text}</p>
</article>
))}
</div>
<p className="industry-claim">穿</p>
<div className="section-cta">
<a className="primary-button tech-button" href="#tech-detail"></a>
</div>
</section>
<section className="page-section tech-section subsidy-section" id="subsidy">
<div className="section-heading dark-heading centered">
<span className="eyebrow"></span>
<h2>亿</h2>
<p className="sub-title"> · </p>
<p></p>
</div>
<div className="subsidy-number reveal-number">亿</div>
<p className="number-desc">亿</p>
<div className="subsidy-points">
<article>
<h3></h3>
<p></p>
</article>
<article>
<h3></h3>
<p></p>
</article>
<article>
<h3></h3>
<p></p>
</article>
</div>
<p className="closing-line dark-closing"></p>
<p className="footnote dark-footnote"></p>
</section>
<section className="page-section tech-section partner-section" id="partner">
<div className="section-heading dark-heading">
<span className="eyebrow"></span>
<h2></h2>
<p> + </p>
</div>
<div className="partner-detail-grid">
<article><h3></h3><p> + + 亿</p></article>
<article><h3></h3><p></p></article>
<article><h3></h3><p></p></article>
<article><h3></h3><p>/ · · </p></article>
<article className="span-2"><h3></h3><p> · · · </p></article>
</div>
<div className="hero-actions">
<a className="primary-button tech-button" href="#contact"></a>
<a className="secondary-button tech-secondary" href="#tech-detail"></a>
</div>
</section>
<section className="page-section tech-section tech-detail-section" id="tech-detail">
<div className="section-heading dark-heading centered">
<span className="eyebrow"></span>
<h2></h2>
<p></p>
</div>
<div className="ports-grid">
{techPorts.map((port) => (
<article key={port.title}>
<h3>{port.title}</h3>
<p>{port.text}</p>
</article>
))}
</div>
<div className="tech-highlights">
<span>穿</span>
<span></span>
<span></span>
</div>
<p className="closing-line dark-closing"></p>
<p className="contact-hint"> · </p>
</section>
<section className="page-section tech-section contact-section" id="contact">
<div className="section-heading dark-heading">
<span className="eyebrow"></span>
<h2> · </h2>
<p></p>
<p className="contact-role"> · </p>
<div className="contact-person">
<strong></strong>
<a href="tel:13073729990">13073729990</a>
</div>
</div>
<div className="qr-card">
<img src="/assets/qr-contact.svg" alt="微信二维码占位图" />
<span></span>
</div>
<footer className="footer in-section">
<strong> · </strong>
<span></span>
<span>© 2026</span>
<span>ICP备2026004970号</span>
<p></p>
</footer>
</section>
</main>
{isStageMode && (
<>
<button
aria-label="上一节"
className="stage-click-zone stage-click-prev"
disabled={activeIndex <= 0}
onClick={() => goToSection(activeIndex - 1)}
type="button"
>
<span></span>
</button>
<button
aria-label="下一节"
className="stage-click-zone stage-click-next"
disabled={activeIndex >= sections.length - 1}
onClick={() => goToSection(activeIndex + 1)}
type="button"
>
<span></span>
</button>
{panelOpen ? (
<aside className="stage-panel is-open" aria-label="展控模式控制面板">
<div className="stage-status">
<span> · </span>
<div className="stage-status-right">
<b>{String(activeIndex + 1).padStart(2, '0')} / {String(sections.length).padStart(2, '0')}</b>
<button
type="button"
className="stage-collapse-btn"
onClick={() => setPanelOpen(false)}
aria-label="收起展控面板"
>
</button>
</div>
</div>
<p>{activeSection.summary}</p>
<div className="autoplay-row">
<button
type="button"
className={autoPlay ? 'is-on' : ''}
onClick={() => setAutoPlay((value) => !value)}
>
{autoPlay ? '自动播:开' : '自动播:关'}
</button>
<button type="button" onClick={() => goToSection(0)}></button>
</div>
<div className="highlight-jumps">
<button type="button" onClick={() => goToSection(2)}></button>
<button type="button" onClick={() => goToSection(7)}>亿</button>
<button type="button" onClick={() => goToSection(6)}></button>
</div>
<div className="stage-qr-grid">
<div>
<img src="/assets/qr-host.svg" alt="主持人控场二维码占位" />
<span> · </span>
</div>
<div>
<img src="/assets/qr-audience.svg" alt="观众伴读二维码占位" />
<span> · </span>
</div>
</div>
<div className="stage-keys"> · · · Home / End</div>
</aside>
) : (
<button
type="button"
className="stage-panel-toggle"
onClick={() => setPanelOpen(true)}
aria-label="展开展控面板"
>
</button>
)}
</>
)}
<div className={`progress-bar ${isStageMode ? 'with-label' : ''}`} aria-hidden={!isStageMode}>
<div style={{ width: `${progress}%` }} />
{isStageMode && (
<span className="progress-label">{activeSection.label}</span>
)}
</div>
{isStageMode && (
<div className="stage-dots" aria-label="展控章节导航">
{sections.map((section, index) => (
<button
aria-label={`跳转到${section.title}`}
className={index === activeIndex ? 'active' : ''}
key={section.id}
onClick={() => goToSection(index)}
type="button"
/>
))}
</div>
)}
</div>
<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>
)
}
export default App
+458
View File
@@ -0,0 +1,458 @@
.deep-page {
padding-bottom: 4rem;
}
.deep-hero {
position: relative;
min-height: 18rem;
display: flex;
align-items: flex-end;
padding: 5rem 0 2.5rem;
overflow: hidden;
margin-bottom: 2.5rem;
background: linear-gradient(160deg, var(--paper-deep), var(--paper) 55%);
}
.deep-hero.has-image {
min-height: 22rem;
}
.deep-hero-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transform: scale(1.04);
}
.deep-hero-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(247, 242, 234, 0.25), rgba(247, 242, 234, 0.94) 72%);
}
.theme-tech .deep-hero-mask {
background: linear-gradient(180deg, rgba(18, 16, 20, 0.35), rgba(18, 16, 20, 0.92) 72%);
}
.deep-hero-copy {
position: relative;
z-index: 1;
}
.deep-back {
display: inline-flex;
margin-top: 0.75rem;
color: var(--brand);
min-height: 2.75rem;
align-items: center;
}
.theme-tech .deep-back {
color: var(--gold);
}
.deep-body {
display: grid;
gap: 2rem;
}
.deep-prose {
max-width: 46rem;
margin-inline: auto;
width: 100%;
}
.deep-h {
margin: 0 0 0.85rem;
color: var(--brand);
font-size: 1.45rem;
letter-spacing: 0.08em;
}
.theme-tech .deep-h {
color: var(--gold);
}
.deep-p {
margin: 0 0 1rem;
color: var(--ink-soft);
font-size: 1.02rem;
line-height: 1.9;
}
.deep-quote {
margin: 0 0 1.25rem;
padding: 1.4rem 1.35rem;
border-left: 2px solid var(--brand);
background: rgba(255, 255, 255, 0.55);
}
.theme-tech .deep-quote {
border-left-color: var(--gold);
background: var(--tech-card);
}
.deep-quote p {
margin: 0;
font-size: 1.2rem;
line-height: 1.85;
}
.deep-quote cite {
display: block;
margin-top: 0.85rem;
font-style: normal;
color: var(--ink-soft);
font-family: 'Noto Sans SC', sans-serif;
font-size: 0.9rem;
}
.deep-list {
margin: 0 0 1rem;
padding-left: 1.1rem;
color: var(--ink-soft);
line-height: 1.9;
}
.deep-media img,
.deep-media-solo img {
width: 100%;
max-height: 28rem;
object-fit: contain;
background: rgba(0, 0, 0, 0.03);
}
.deep-media-solo {
max-width: 40rem;
margin-inline: auto;
}
/* person */
.deep-person-grid {
grid-template-columns: 1.2fr 0.8fr;
align-items: start;
}
/* timeline */
.deep-timeline {
list-style: none;
margin: 0;
padding: 0 0 0 1.25rem;
border-left: 2px solid rgba(166, 28, 32, 0.18);
display: grid;
gap: 1.25rem;
}
.deep-timeline-item {
position: relative;
padding-left: 0.75rem;
}
.deep-timeline-dot {
position: absolute;
left: -1.55rem;
top: 0.45rem;
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
background: var(--brand);
box-shadow: 0 0 0 0.25rem rgba(166, 28, 32, 0.12);
}
.deep-timeline-item p {
margin: 0;
color: var(--ink-soft);
line-height: 1.75;
}
/* shrine */
.deep-shrine-stack {
max-width: 42rem;
margin-inline: auto;
text-align: center;
}
.deep-shrine-stack .deep-list {
text-align: left;
display: inline-block;
}
/* sites */
.deep-sites-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
.deep-site-card {
padding: 1.35rem 1.2rem;
background: rgba(255, 255, 255, 0.55);
border-top: 2px solid var(--gold);
}
/* figures */
.deep-figure-rail {
display: grid;
gap: 1rem;
}
.deep-figure-card {
display: grid;
grid-template-columns: 3.5rem 1fr;
gap: 0.75rem 1.1rem;
padding: 1.25rem 0;
border-bottom: 1px solid rgba(166, 28, 32, 0.12);
}
.deep-figure-index {
color: var(--gold);
font-size: 1.5rem;
grid-row: span 2;
}
.deep-figure-card .deep-h {
grid-column: 2;
}
.deep-figure-card .deep-p {
grid-column: 2;
}
/* factory */
.deep-factory-panel {
padding: 2rem 1.75rem;
background: linear-gradient(135deg, rgba(166, 28, 32, 0.06), rgba(196, 163, 90, 0.08));
border: 1px solid rgba(166, 28, 32, 0.12);
}
.deep-factory-caps {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.85rem;
margin-top: 1.5rem;
}
.deep-factory-cap {
padding: 1rem 1.1rem;
background: rgba(255, 255, 255, 0.7);
min-height: 4.5rem;
display: flex;
align-items: center;
}
.deep-factory-cap p {
margin: 0;
color: var(--ink);
}
/* poem wall */
.deep-poem-wall {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
align-items: stretch;
}
.deep-poem-slip {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 1.5rem 1.25rem;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(166, 28, 32, 0.1);
box-sizing: border-box;
}
.deep-poem-slip p {
margin: 0;
font-size: 1.15rem;
line-height: 1.85;
}
.deep-poem-slip cite {
margin-top: auto;
padding-top: 0.85rem;
display: block;
font-style: normal;
color: var(--ink-soft);
font-family: 'Noto Sans SC', sans-serif;
font-size: 0.88rem;
}
/* ink scroll */
.deep-ink-flow {
max-width: 40rem;
margin-inline: auto;
border-left: 1px solid var(--gold-soft);
padding-left: 1.75rem;
}
.deep-ink-flow .deep-quote {
writing-mode: vertical-rl;
min-height: 14rem;
max-width: 100%;
border-left: none;
border-top: 2px solid var(--brand);
margin-inline: auto;
}
/* manifesto */
.deep-manifesto-lead {
max-width: 48rem;
margin-inline: auto;
text-align: center;
}
.deep-loop-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.85rem;
}
.deep-loop-node {
padding: 1.2rem 1rem;
background: rgba(255, 255, 255, 0.7);
border-bottom: 3px solid var(--brand);
min-height: 9rem;
}
.deep-loop-num {
display: block;
color: var(--gold);
margin-bottom: 0.55rem;
letter-spacing: 0.12em;
}
.deep-loop-node p {
margin: 0;
color: var(--ink-soft);
line-height: 1.65;
font-size: 0.95rem;
}
/* matrix */
.deep-matrix {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.15rem;
}
.deep-matrix-card {
padding: 1.4rem 1.25rem;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(166, 28, 32, 0.1);
}
/* triad */
.deep-triad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.deep-triad-col {
padding: 1.5rem 1.2rem;
text-align: center;
background: linear-gradient(180deg, rgba(166, 28, 32, 0.08), transparent);
border-top: 2px solid var(--brand);
}
/* steps */
.deep-steps {
display: grid;
gap: 0;
max-width: 40rem;
margin-inline: auto;
}
.deep-step {
display: grid;
grid-template-columns: 3.5rem 1fr;
gap: 1rem;
padding: 1.35rem 0;
border-bottom: 1px dashed rgba(166, 28, 32, 0.2);
align-items: start;
}
.deep-step-index {
color: var(--brand);
font-size: 1.6rem;
}
.deep-step p {
margin: 0;
font-size: 1.1rem;
color: var(--ink);
line-height: 1.7;
}
/* scenes */
.deep-scenes {
display: grid;
gap: 1.5rem;
}
.deep-scene {
padding: 1.5rem 1.35rem;
background: rgba(255, 255, 255, 0.5);
border-left: 3px solid var(--gold);
}
/* tour */
.deep-tour-flow {
max-width: 44rem;
margin-inline: auto;
}
.deep-tour-steps {
display: grid;
gap: 0.75rem;
}
.deep-tour-step {
display: grid;
grid-template-columns: 2.5rem 1fr;
gap: 0.85rem;
align-items: center;
padding: 0.95rem 1.1rem;
background: rgba(255, 255, 255, 0.65);
}
.deep-tour-step span {
width: 2.1rem;
height: 2.1rem;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--brand);
color: #fff;
font-size: 0.95rem;
}
.deep-tour-step p {
margin: 0;
color: var(--ink-soft);
}
@media (max-width: 48rem) {
.deep-person-grid,
.deep-sites-grid,
.deep-poem-wall,
.deep-loop-strip,
.deep-matrix,
.deep-triad,
.deep-factory-caps {
grid-template-columns: 1fr;
}
.deep-ink-flow .deep-quote {
writing-mode: horizontal-tb;
min-height: auto;
}
.deep-figure-card {
grid-template-columns: 2.5rem 1fr;
}
}
+544
View File
@@ -0,0 +1,544 @@
import { Link } from 'react-router-dom'
import type { ReactElement } from 'react'
import { Reveal } from './Reveal'
import type { DeepBlock, DeepLayoutKind, DeepPageContent } from '../content/deep/types'
import './DeepLayout.css'
function backPath(group: DeepPageContent['group']) {
if (group === 'qingxiang') return '/qingxiang'
if (group === 'poetry') return '/poetry'
return '/brand'
}
function Blocks({ blocks }: { blocks: DeepBlock[] }) {
return (
<>
{blocks.map((block, i) => (
<Reveal key={`${block.type}-${i}`} variant={i % 2 === 0 ? 'anim-slide-left' : 'anim-rise'} delayMs={(i % 5) * 60}>
{block.type === 'heading' ? <h2 className="deep-h serif">{block.text}</h2> : null}
{block.type === 'paragraph' ? <p className="deep-p">{block.text}</p> : null}
{block.type === 'quote' ? (
<blockquote className="deep-quote serif">
<p>{block.text}</p>
{block.source ? <cite>{block.source}</cite> : null}
</blockquote>
) : null}
{block.type === 'list' ? (
<ul className="deep-list">
{block.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
) : null}
</Reveal>
))}
</>
)
}
function Hero({ page, image }: { page: DeepPageContent; image?: string }) {
return (
<header className={`deep-hero ${image ? 'has-image' : 'no-image'}`}>
{image ? <div className="deep-hero-bg" style={{ backgroundImage: `url(${image})` }} /> : null}
<div className="deep-hero-mask" />
<div className="container deep-hero-copy">
<Reveal variant="anim-ink">
<p className="eyebrow">{page.eyebrow}</p>
<h1 className="page-title">{page.title}</h1>
<p className="lead">{page.lead}</p>
<Link className="deep-back" to={backPath(page.group)}>
</Link>
</Reveal>
</div>
</header>
)
}
function Closing({ text }: { text?: string }) {
if (!text) return null
return (
<Reveal variant="anim-rise">
<p className="closing">{text}</p>
</Reveal>
)
}
function SoloImage({ src }: { src?: string }) {
if (!src) return null
return (
<Reveal variant="anim-zoom">
<div className="media-frame deep-media deep-media-solo">
<img src={src} alt="" loading="lazy" />
</div>
</Reveal>
)
}
function listBlocks(blocks: DeepBlock[]) {
return blocks.filter((b): b is Extract<DeepBlock, { type: 'list' }> => b.type === 'list')
}
function quoteBlocks(blocks: DeepBlock[]) {
return blocks.filter((b): b is Extract<DeepBlock, { type: 'quote' }> => b.type === 'quote')
}
function headingPairs(blocks: DeepBlock[]) {
const pairs: { heading: string; body: DeepBlock[] }[] = []
let current: { heading: string; body: DeepBlock[] } | null = null
for (const b of blocks) {
if (b.type === 'heading') {
current = { heading: b.text, body: [] }
pairs.push(current)
} else if (current) {
current.body.push(b)
}
}
return pairs
}
function LayoutEssay({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-essay">
<Hero page={page} image={img} />
<div className="container deep-body deep-prose">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutPerson({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-person">
<Hero page={page} />
<div className="container deep-body deep-person-grid">
<div className="deep-person-copy">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
<SoloImage src={img} />
</div>
</article>
)
}
function LayoutTimeline({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-timeline">
<Hero page={page} />
<div className="container deep-body">
<SoloImage src={img} />
{lists.map((list, li) => (
<ol key={li} className="deep-timeline">
{list.items.map((item, i) => (
<Reveal key={item} variant="anim-slide-left" delayMs={i * 70} className="deep-timeline-item">
<span className="deep-timeline-dot" />
<p>{item}</p>
</Reveal>
))}
</ol>
))}
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutShrine({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-shrine">
<Hero page={page} image={img} />
<div className="container deep-body deep-shrine-stack">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutSites({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-sites">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-sites-grid">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 90} className="deep-site-card">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) =>
b.type === 'paragraph' ? (
<p key={j} className="deep-p">
{b.text}
</p>
) : null,
)}
</Reveal>
))}
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutFigures({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-figures">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-figure-rail">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-expand" delayMs={i * 100} className="deep-figure-card">
<span className="deep-figure-index serif">{String(i + 1).padStart(2, '0')}</span>
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) =>
b.type === 'paragraph' ? (
<p key={j} className="deep-p">
{b.text}
</p>
) : null,
)}
</Reveal>
))}
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutFactory({ page }: { page: DeepPageContent }) {
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-factory">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-factory-panel">
<Blocks blocks={rest} />
{lists.map((list, i) => (
<div key={i} className="deep-factory-caps">
{list.items.map((item) => (
<Reveal key={item} variant="anim-pop" className="deep-factory-cap">
<p>{item}</p>
</Reveal>
))}
</div>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutPoemWall({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const quotes = quoteBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'quote')
return (
<article className="deep-page deep-layout-poem-wall">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-poem-wall">
{quotes.map((q, i) => (
<Reveal key={`${q.source}-${i}`} variant="anim-unroll" delayMs={i * 120} className={`deep-poem-slip slip-${i % 3}`}>
<p className="serif">{q.text}</p>
{q.source ? <cite>{q.source}</cite> : null}
</Reveal>
))}
</div>
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutInkScroll({ page }: { page: DeepPageContent }) {
const img = page.images[0]
return (
<article className="deep-page deep-layout-ink-scroll">
<Hero page={page} image={img} />
<div className="container deep-body deep-ink-flow">
<Blocks blocks={page.blocks} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutManifesto({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-manifesto">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-manifesto-lead">
<Blocks blocks={rest} />
</div>
{lists.map((list, i) => (
<div key={i} className="deep-loop-strip">
{list.items.map((item, j) => (
<Reveal key={item} variant="anim-pop" delayMs={j * 80} className="deep-loop-node">
<span className="deep-loop-num serif">{String(j + 1).padStart(2, '0')}</span>
<p>{item}</p>
</Reveal>
))}
</div>
))}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutMatrix({ page }: { page: DeepPageContent }) {
const pairs = headingPairs(page.blocks)
const orphan = page.blocks.filter((b, i, arr) => {
if (b.type === 'heading') return false
const prevHeading = [...arr.slice(0, i)].reverse().find((x) => x.type === 'heading')
return !prevHeading
})
return (
<article className="deep-page deep-layout-matrix">
<Hero page={page} />
<div className="container deep-body">
{orphan.length ? (
<div className="deep-prose">
<Blocks blocks={orphan} />
</div>
) : null}
<div className="deep-matrix">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-expand" delayMs={i * 90} className="deep-matrix-card">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
return null
})}
</Reveal>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutTriad({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
return (
<article className="deep-page deep-layout-triad">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-triad">
{pairs.slice(0, 3).map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 100} className="deep-triad-col">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
return null
})}
</Reveal>
))}
</div>
{pairs.slice(3).length ? (
<div className="deep-prose">
{pairs.slice(3).map((pair) => (
<div key={pair.heading}>
<h2 className="deep-h serif">{pair.heading}</h2>
<Blocks blocks={pair.body} />
</div>
))}
</div>
) : null}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutSteps({ page }: { page: DeepPageContent }) {
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
const steps = lists[0]?.items ?? []
return (
<article className="deep-page deep-layout-steps">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-steps">
{steps.map((item, i) => (
<Reveal key={item} variant="anim-slide-left" delayMs={i * 100} className="deep-step">
<span className="deep-step-index serif">{String(i + 1).padStart(2, '0')}</span>
<p>{item}</p>
</Reveal>
))}
</div>
<div className="deep-prose">
<Blocks blocks={rest} />
{lists.slice(1).map((list, i) => (
<ul key={i} className="deep-list">
{list.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
))}
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutScenes({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const pairs = headingPairs(page.blocks)
const quotes = quoteBlocks(page.blocks)
return (
<article className="deep-page deep-layout-scenes">
<Hero page={page} />
<div className="container deep-body">
<div className="deep-scenes">
{pairs.map((pair, i) => (
<Reveal key={pair.heading} variant="anim-rise" delayMs={i * 90} className="deep-scene">
<h2 className="deep-h serif">{pair.heading}</h2>
{pair.body.map((b, j) => {
if (b.type === 'list') {
return (
<ul key={j} className="deep-list">
{b.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
if (b.type === 'paragraph') {
return (
<p key={j} className="deep-p">
{b.text}
</p>
)
}
return null
})}
</Reveal>
))}
</div>
{quotes.map((q, i) => (
<blockquote key={i} className="deep-quote serif">
<p>{q.text}</p>
{q.source ? <cite>{q.source}</cite> : null}
</blockquote>
))}
<SoloImage src={img} />
<Closing text={page.closing} />
</div>
</article>
)
}
function LayoutTour({ page }: { page: DeepPageContent }) {
const img = page.images[0]
const lists = listBlocks(page.blocks)
const rest = page.blocks.filter((b) => b.type !== 'list')
return (
<article className="deep-page deep-layout-tour">
<Hero page={page} image={img} />
<div className="container deep-body deep-tour-flow">
{lists.map((list, i) => (
<div key={i} className="deep-tour-steps">
{list.items.map((item, j) => (
<Reveal key={item} variant="anim-slide-right" delayMs={j * 70} className="deep-tour-step">
<span className="serif">{j + 1}</span>
<p>{item}</p>
</Reveal>
))}
</div>
))}
<div className="deep-prose">
<Blocks blocks={rest} />
</div>
<Closing text={page.closing} />
</div>
</article>
)
}
const layoutMap: Record<DeepLayoutKind, (p: { page: DeepPageContent }) => ReactElement> = {
essay: LayoutEssay,
person: LayoutPerson,
timeline: LayoutTimeline,
shrine: LayoutShrine,
sites: LayoutSites,
figures: LayoutFigures,
factory: LayoutFactory,
'poem-wall': LayoutPoemWall,
'ink-scroll': LayoutInkScroll,
manifesto: LayoutManifesto,
matrix: LayoutMatrix,
triad: LayoutTriad,
steps: LayoutSteps,
scenes: LayoutScenes,
tour: LayoutTour,
}
export function DeepLayout({ page }: { page: DeepPageContent }) {
const View = layoutMap[page.layout] ?? LayoutEssay
return <View page={page} />
}
+68
View File
@@ -0,0 +1,68 @@
.site-footer {
background: #1a1512;
color: rgba(247, 242, 234, 0.78);
padding: 2.75rem 0 2.25rem;
border-top: 1px solid rgba(196, 163, 90, 0.15);
}
.theme-tech .site-footer {
background: #0c0a0e;
border-top: 1px solid rgba(196, 163, 90, 0.28);
}
.footer-inner {
display: grid;
gap: 1.75rem;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.9rem;
}
.footer-logo {
width: 2.6rem;
height: 2.6rem;
object-fit: contain;
filter: brightness(1.1);
}
.footer-title {
margin: 0;
color: #f7f2ea;
font-size: 1.1rem;
}
.footer-slogan {
margin: 0.25rem 0 0;
color: var(--gold, #c4a35a);
letter-spacing: 0.12em;
}
.footer-meta {
font-size: 0.82rem;
line-height: 1.8;
color: rgba(247, 242, 234, 0.78);
}
.footer-meta p {
margin: 0.2rem 0;
}
.footer-meta a {
color: var(--gold, #c4a35a);
text-decoration: underline;
text-underline-offset: 3px;
}
.footer-meta a:hover {
color: #ffffff;
}
@media (min-width: 48rem) {
.footer-inner {
grid-template-columns: 1fr 1.4fr;
align-items: end;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Link } from 'react-router-dom'
import { footer, images } from '../content/site'
import './Footer.css'
export function Footer() {
return (
<footer className="site-footer">
<div className="container footer-inner">
<div className="footer-brand">
<img src={images.logo} alt="" className="footer-logo" />
<div>
<p className="serif footer-title">{footer.brandLine}</p>
<p className="footer-slogan">{footer.slogan}</p>
</div>
</div>
<div className="footer-meta">
<p>
{footer.domain} · {footer.copyright} · {footer.icp}
</p>
<p>{footer.compliance}</p>
<p>{footer.dataNote}</p>
<p>
<Link to="/contact"></Link>
</p>
</div>
</div>
</footer>
)
}
+113
View File
@@ -0,0 +1,113 @@
.site-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--paper);
color: var(--ink);
}
.site-shell > main {
flex: 1;
min-height: 0;
}
/* Global stage-mode HTML/Body scroll lock */
html.stage-mode,
body.stage-mode {
height: 100%;
overflow: hidden;
overscroll-behavior: none;
}
body.stage-mode #root {
height: 100%;
}
.page-section {
padding: 5rem 0;
}
.page-section.tight {
padding-top: 3.5rem;
}
.split {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 3rem;
align-items: center;
}
.media-frame {
overflow: hidden;
border-radius: 0.2rem;
box-shadow: var(--shadow-soft);
background: var(--paper-deep);
}
.media-frame img {
width: 100%;
height: 100%;
object-fit: cover;
aspect-ratio: 4 / 5;
}
.card-grid,
.module-grid,
.ends-grid,
.ports-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
.module-grid {
grid-template-columns: repeat(5, 1fr);
}
.soft-card {
padding: 1.4rem 1.35rem;
background: rgba(255, 255, 255, 0.55);
border: 1px solid rgba(166, 28, 32, 0.1);
border-radius: 0.2rem;
transition: transform 0.35s var(--ease), border-color 0.35s var(--ease), background 0.35s var(--ease);
}
.soft-card:hover {
transform: translateY(-0.2rem);
border-color: rgba(166, 28, 32, 0.28);
}
.theme-tech .soft-card {
background: var(--tech-card);
border-color: var(--tech-line);
}
.soft-card h3 {
margin: 0 0 0.65rem;
font-size: 1.15rem;
color: var(--brand);
}
.theme-tech .soft-card h3 {
color: var(--gold);
}
.soft-card p {
margin: 0;
color: var(--ink-soft);
font-size: 0.95rem;
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 1.75rem;
}
.footnote {
margin-top: 1.25rem;
font-size: 0.82rem;
color: var(--ink-soft);
}
+70
View File
@@ -0,0 +1,70 @@
import { useEffect } from 'react'
import { Outlet, useLocation } from 'react-router-dom'
import { useIsMobile } from '../hooks/useMobile'
import { StageProvider, useStageMode } from '../hooks/useStageMode'
import { MobileShell } from '../mobile/MobileShell'
import { MobileViews } from '../mobile/MobileViews'
import { StageShell } from '../stage/StageShell'
import { Footer } from './Footer'
import { Nav } from './Nav'
import './Layout.css'
const techRoutes = new Set(['/tech', '/subsidy', '/partner', '/tech-detail', '/contact'])
function LayoutInner() {
const location = useLocation()
const isStage = useStageMode()
const isMobile = useIsMobile(768)
const techLike = techRoutes.has(location.pathname)
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'auto' })
}, [location.pathname])
useEffect(() => {
document.documentElement.classList.toggle('stage-mode', isStage)
document.body.classList.toggle('stage-mode', isStage)
return () => {
document.documentElement.classList.remove('stage-mode')
document.body.classList.remove('stage-mode')
}
}, [isStage])
// 1. Stage Mode Branch
if (isStage) {
return <StageShell />
}
// 2. Mobile Dedicated Layout Branch (< 768px)
if (isMobile) {
// If accessing deep pages (e.g. /culture/status), render through Outlet; main modules render through MobileViews
const isDeepPage =
location.pathname.startsWith('/culture/') ||
location.pathname.startsWith('/poetry/') ||
location.pathname.startsWith('/qingxiang/')
return (
<MobileShell>
{isDeepPage ? <Outlet /> : <MobileViews />}
</MobileShell>
)
}
// 3. Desktop Normal Mode Branch
return (
<div className={`site-shell ${techLike ? 'theme-tech' : ''}`}>
<Nav />
<main className="page-enter" key={location.pathname}>
<Outlet />
</main>
<Footer />
</div>
)
}
export function Layout() {
return (
<StageProvider>
<LayoutInner />
</StageProvider>
)
}
+430
View File
@@ -0,0 +1,430 @@
.topnav {
position: sticky;
top: 0;
z-index: 1000;
height: var(--nav-h);
backdrop-filter: blur(12px);
background: rgba(247, 242, 234, 0.86);
border-bottom: 1px solid transparent;
transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.topnav.has-drawer-open {
z-index: 1001;
}
.theme-tech .topnav {
background: rgba(18, 16, 20, 0.9);
}
.topnav.is-scrolled {
border-bottom-color: rgba(166, 28, 32, 0.12);
background: rgba(247, 242, 234, 0.95);
}
.theme-tech .topnav.is-scrolled {
border-bottom-color: rgba(196, 163, 90, 0.22);
background: rgba(18, 16, 20, 0.96);
}
.theme-tech .nav-links a {
color: rgba(243, 236, 228, 0.72);
}
.theme-tech .nav-links a:hover,
.theme-tech .nav-links a.is-active {
color: var(--gold);
}
.theme-tech .nav-links a.is-active::after {
background: var(--gold);
}
.theme-tech .mobile-drawer {
background: rgba(18, 16, 20, 0.98);
}
.theme-tech .mobile-drawer a {
color: #f3ece4;
border-bottom-color: rgba(196, 163, 90, 0.15);
}
.topnav-inner {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.65rem;
min-height: 2.75rem;
}
.brand-logo {
width: 2.2rem;
height: 2.2rem;
object-fit: contain;
}
.brand-text {
font-size: 1.15rem;
color: var(--brand);
letter-spacing: 0.08em;
}
.nav-links {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.15rem 0.85rem;
max-width: 58rem;
}
.nav-links a {
position: relative;
font-size: 0.86rem;
color: var(--ink-soft);
padding: 0.35rem 0;
transition: color 0.25s var(--ease);
}
.nav-links a:hover,
.nav-links a.is-active {
color: var(--brand);
}
.nav-links a.is-active::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: var(--brand);
transform-origin: left;
animation: lineGrow 0.45s var(--ease) both;
}
.menu-toggle {
position: relative;
z-index: 1002;
width: 2.75rem;
height: 2.75rem;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.35rem;
}
.menu-toggle span {
width: 1.35rem;
height: 1.5px;
background: var(--brand);
transition: transform 0.3s var(--ease);
}
.mobile-drawer {
position: fixed;
inset: var(--nav-h) 0 0 0;
z-index: 1001;
background: rgba(247, 242, 234, 0.98);
backdrop-filter: blur(16px);
padding: 1.5rem 1.25rem 2rem;
transform: translateX(100%);
transition: transform 0.4s var(--ease);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-drawer.is-open {
transform: translateX(0);
}
.mobile-drawer a {
display: flex;
align-items: center;
min-height: 2.75rem;
border-bottom: 1px solid rgba(166, 28, 32, 0.1);
font-size: 1.05rem;
color: var(--ink);
}
.mobile-drawer a:hover {
color: var(--brand);
}
.nav-groups {
display: flex;
align-items: stretch;
gap: 0.35rem 1.1rem;
height: 100%;
}
.nav-group {
position: relative;
display: flex;
align-items: center;
height: 100%;
}
.nav-group-title {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 2.75rem;
font-size: 0.92rem;
color: var(--ink-soft);
letter-spacing: 0.06em;
transition: color 0.25s var(--ease);
}
.nav-chevron {
width: 0.4rem;
height: 0.4rem;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg) translateY(-0.1rem);
opacity: 0.55;
transition: transform 0.3s var(--ease), opacity 0.25s var(--ease);
}
.nav-group:hover .nav-group-title,
.nav-group.is-current .nav-group-title,
.nav-group.is-open .nav-group-title {
color: var(--brand);
}
.nav-group:hover .nav-chevron,
.nav-group.is-open .nav-chevron {
opacity: 1;
transform: rotate(225deg) translateY(-0.05rem);
}
.nav-dropdown {
position: absolute;
top: calc(100% - 0.1rem);
left: 50%;
transform: translateX(-50%) translateY(0.55rem) scale(0.98);
min-width: 13.5rem;
padding: 0.85rem 0.75rem 0.9rem;
background: var(--paper);
border: 1px solid rgba(166, 28, 32, 0.12);
border-radius: 0.2rem;
box-shadow: 0 1.1rem 2.4rem rgba(26, 21, 18, 0.12);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.28s var(--ease), transform 0.28s var(--ease), visibility 0.28s;
z-index: 50;
}
.nav-dropdown::before {
content: '';
position: absolute;
left: 0.85rem;
right: 0.85rem;
top: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.nav-dropdown.is-wide {
min-width: 22rem;
left: 50%;
}
.nav-group:hover .nav-dropdown,
.nav-group.is-open .nav-dropdown {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateX(-50%) translateY(0) scale(1);
}
.nav-drop-section + .nav-drop-section {
margin-top: 0.7rem;
padding-top: 0.7rem;
border-top: 1px solid rgba(166, 28, 32, 0.1);
}
.nav-drop-label {
margin: 0 0.35rem 0.4rem;
font-size: 0.72rem;
letter-spacing: 0.18em;
color: var(--gold);
text-transform: none;
}
.nav-drop-list.is-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.1rem 0.35rem;
}
.nav-drop-item {
--i: 0;
position: relative;
display: flex;
align-items: center;
min-height: 2.35rem;
padding: 0 0.85rem 0 1.05rem;
font-size: 0.88rem;
color: var(--ink-soft);
white-space: nowrap;
border-radius: 0.12rem;
opacity: 0;
transform: translateY(0.35rem);
transition: color 0.22s var(--ease), background 0.22s var(--ease), transform 0.22s var(--ease);
}
.nav-group:hover .nav-drop-item,
.nav-group.is-open .nav-drop-item {
opacity: 1;
transform: translateY(0);
transition-delay: calc(var(--i) * 35ms);
}
.nav-drop-item::before {
content: '';
position: absolute;
left: 0.25rem;
top: 50%;
width: 2px;
height: 0;
background: var(--brand);
transform: translateY(-50%);
transition: height 0.22s var(--ease);
}
.nav-drop-item:hover,
.nav-drop-item.is-active {
color: var(--brand);
background: var(--brand-soft);
transform: translateX(0.15rem);
}
.nav-drop-item:hover::before,
.nav-drop-item.is-active::before {
height: 1.05rem;
}
.nav-drop-item.deep {
font-size: 0.84rem;
}
.stage-links {
max-width: 70rem;
justify-content: flex-end;
}
.mobile-group-title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 2.9rem;
border-bottom: 1px solid rgba(166, 28, 32, 0.1);
color: var(--ink);
font-size: 1.05rem;
}
.mobile-chevron {
width: 0.55rem;
height: 0.55rem;
border-right: 1.5px solid var(--brand);
border-bottom: 1.5px solid var(--brand);
transform: rotate(45deg);
transition: transform 0.3s var(--ease);
}
.mobile-group.is-open .mobile-chevron {
transform: rotate(225deg);
}
.mobile-group-panel {
max-height: 0;
overflow: hidden;
opacity: 0;
padding: 0 0 0 0.75rem;
border-left: 2px solid transparent;
transition: max-height 0.35s var(--ease), opacity 0.28s var(--ease), padding 0.28s var(--ease), border-color 0.28s;
}
.mobile-group.is-open .mobile-group-panel {
max-height: 40rem;
opacity: 1;
padding: 0.35rem 0 0.85rem 0.85rem;
border-left-color: rgba(166, 28, 32, 0.22);
}
.mobile-drop-label {
margin: 0.55rem 0 0.2rem;
font-size: 0.72rem;
letter-spacing: 0.16em;
color: var(--gold);
}
.mobile-group-panel a {
font-size: 0.95rem;
color: var(--ink-soft);
border-bottom-color: rgba(166, 28, 32, 0.06);
}
.mobile-group-panel a.is-deep {
font-size: 0.9rem;
padding-left: 0.35rem;
}
.theme-tech .nav-group-title,
.theme-tech .nav-drop-item {
color: rgba(243, 236, 228, 0.72);
}
.theme-tech .nav-group:hover .nav-group-title,
.theme-tech .nav-group.is-current .nav-group-title,
.theme-tech .nav-drop-item:hover,
.theme-tech .nav-drop-item.is-active {
color: var(--gold);
}
.theme-tech .nav-dropdown {
background: rgba(28, 25, 31, 0.98);
border-color: var(--tech-line);
}
.theme-tech .nav-drop-item::before {
background: var(--gold);
}
.theme-tech .nav-drop-item:hover,
.theme-tech .nav-drop-item.is-active {
background: rgba(196, 163, 90, 0.1);
}
.theme-tech .nav-drop-section + .nav-drop-section {
border-top-color: rgba(196, 163, 90, 0.18);
}
@media (max-width: 64rem) and (min-width: 48.0625rem) {
.nav-links {
gap: 0.1rem 0.55rem;
}
.nav-links a,
.nav-group-title {
font-size: 0.78rem;
}
.nav-groups {
gap: 0.25rem 0.65rem;
}
.nav-dropdown.is-wide {
min-width: 18rem;
}
}
+195
View File
@@ -0,0 +1,195 @@
import { NavLink, Link, useLocation } from 'react-router-dom'
import { useEffect, useState, type CSSProperties } from 'react'
import { images } from '../content/site'
import { findGroupByPath, navGroups, stageNavItems, type NavChild, type NavGroup } from '../content/nav'
import { useStageMode } from '../hooks/useStageMode'
import './Nav.css'
function withStage(path: string, isStage: boolean) {
if (!isStage) return path
if (path === '/') return '/?mode=stage'
return `${path}?mode=stage`
}
function splitChildren(children: NavChild[]) {
return {
main: children.filter((c) => c.kind === 'main'),
deep: children.filter((c) => c.kind === 'deep'),
}
}
function isWideGroup(group: NavGroup) {
return group.id === 'culture' || group.id === 'qingxiang'
}
export function Nav() {
const isStage = useStageMode()
const location = useLocation()
const [drawerOpen, setDrawerOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [openGroup, setOpenGroup] = useState<string | null>(null)
const activeGroup = findGroupByPath(location.pathname)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
useEffect(() => {
document.body.style.overflow = drawerOpen ? 'hidden' : ''
return () => {
document.body.style.overflow = ''
}
}, [drawerOpen])
useEffect(() => {
setDrawerOpen(false)
setOpenGroup(null)
}, [location.pathname, location.search])
if (isStage) {
return (
<header className={`topnav ${scrolled ? 'is-scrolled' : ''}`}>
<div className="topnav-inner container">
<Link to={withStage('/', true)} className="brand">
<img src={images.logo} alt="杜康好客" className="brand-logo" />
<span className="brand-text serif"> · </span>
</Link>
<nav className="nav-links stage-links" aria-label="展控导航">
{stageNavItems.map((item) => (
<NavLink
key={item.path}
to={withStage(item.path, true)}
end={item.path === '/'}
className={({ isActive }) => (isActive ? 'is-active' : '')}
>
{item.label}
</NavLink>
))}
</nav>
</div>
</header>
)
}
return (
<header className={`topnav ${scrolled ? 'is-scrolled' : ''} ${drawerOpen ? 'has-drawer-open' : ''}`}>
<div className="topnav-inner container">
<Link to="/" className="brand" onClick={() => setDrawerOpen(false)}>
<img src={images.logo} alt="杜康好客" className="brand-logo" />
<span className="brand-text serif"></span>
</Link>
<nav className="nav-groups desktop-only" aria-label="主导航">
{navGroups.map((group) => {
const { main, deep } = splitChildren(group.children)
const wide = isWideGroup(group)
return (
<div
key={group.id}
className={`nav-group ${activeGroup?.id === group.id ? 'is-current' : ''} ${openGroup === group.id ? 'is-open' : ''}`}
onMouseEnter={() => setOpenGroup(group.id)}
onMouseLeave={() => setOpenGroup(null)}
>
<Link to={group.entryPath} className="nav-group-title">
<span>{group.label}</span>
<span className="nav-chevron" aria-hidden />
</Link>
<div className={`nav-dropdown ${wide ? 'is-wide' : ''}`} role="menu">
<div className="nav-drop-section">
<p className="nav-drop-label"></p>
<div className="nav-drop-list">
{main.map((child, i) => (
<NavLink
key={child.path}
to={child.path}
role="menuitem"
style={{ '--i': i } as CSSProperties}
className={({ isActive }) => `nav-drop-item main ${isActive ? 'is-active' : ''}`}
>
{child.label}
</NavLink>
))}
</div>
</div>
{deep.length > 0 ? (
<div className="nav-drop-section">
<p className="nav-drop-label"></p>
<div className={`nav-drop-list ${wide ? 'is-cols' : ''}`}>
{deep.map((child, i) => (
<NavLink
key={child.path}
to={child.path}
role="menuitem"
style={{ '--i': i } as CSSProperties}
className={({ isActive }) => `nav-drop-item deep ${isActive ? 'is-active' : ''}`}
>
{child.label}
</NavLink>
))}
</div>
</div>
) : null}
</div>
</div>
)
})}
</nav>
<button
className="menu-toggle mobile-only"
type="button"
aria-expanded={drawerOpen}
aria-label={drawerOpen ? '关闭菜单' : '打开菜单'}
onClick={() => setDrawerOpen((v) => !v)}
>
<span />
<span />
<span />
</button>
</div>
<div className={`mobile-drawer mobile-only ${drawerOpen ? 'is-open' : ''}`}>
<nav aria-label="移动导航">
{navGroups.map((group) => {
const expanded = openGroup === group.id
const { main, deep } = splitChildren(group.children)
return (
<div key={group.id} className={`mobile-group ${expanded ? 'is-open' : ''}`}>
<button
type="button"
className="mobile-group-title"
aria-expanded={expanded}
onClick={() => setOpenGroup(expanded ? null : group.id)}
>
<span>{group.label}</span>
<i className="mobile-chevron" aria-hidden />
</button>
<div className="mobile-group-panel">
<p className="mobile-drop-label"></p>
{main.map((child) => (
<NavLink key={child.path} to={child.path} onClick={() => setDrawerOpen(false)}>
{child.label}
</NavLink>
))}
{deep.length > 0 ? (
<>
<p className="mobile-drop-label"></p>
{deep.map((child) => (
<NavLink key={child.path} to={child.path} className="is-deep" onClick={() => setDrawerOpen(false)}>
{child.label}
</NavLink>
))}
</>
) : null}
</div>
</div>
)
})}
</nav>
</div>
</header>
)
}
+35
View File
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react'
import { Reveal } from './Reveal'
type PageHeroProps = {
eyebrow: string
title: string
lead: string
image?: string
imageAlt?: string
children?: ReactNode
}
export function PageHero({ eyebrow, title, lead, image, imageAlt = '', children }: PageHeroProps) {
return (
<section className="page-section tight">
<div className="container">
<div className={`split ${image ? '' : 'single'}`.trim()}>
<Reveal variant="anim-rise">
<p className="eyebrow">{eyebrow}</p>
<h1 className="page-title">{title}</h1>
<p className="lead">{lead}</p>
{children}
</Reveal>
{image ? (
<Reveal variant="anim-zoom" delayMs={120}>
<div className="media-frame">
<img src={image} alt={imageAlt} loading="lazy" />
</div>
</Reveal>
) : null}
</div>
</div>
</section>
)
}
+48
View File
@@ -0,0 +1,48 @@
import { useEffect, useRef, type ReactNode } from 'react'
type RevealProps = {
children: ReactNode
className?: string
/** Custom animation class applied when in view, e.g. anim-slide-left */
variant?: string
delayMs?: number
}
export function Reveal({ children, className = '', variant = 'anim-rise', delayMs = 0 }: RevealProps) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const node = ref.current
if (!node) return
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
node.classList.add('is-in')
return
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
node.classList.add('is-in')
observer.unobserve(node)
}
})
},
{ threshold: 0.16, rootMargin: '0px 0px -6% 0px' },
)
observer.observe(node)
return () => observer.disconnect()
}, [])
return (
<div
ref={ref}
className={`reveal-base ${variant} ${className}`.trim()}
style={delayMs ? { transitionDelay: `${delayMs}ms` } : undefined}
>
{children}
</div>
)
}
+34
View File
@@ -0,0 +1,34 @@
export const deepImages = {
brand: [
'/assets/brand/emb_p6_5.png',
'/assets/brand/emb_p31_1.png',
'/assets/brand/emb_p15_3.png',
'/assets/brand/emb_p33_1.png',
'/assets/brand/photo_p11_1.jpg',
'/assets/brand/photo_p8_4.jpg',
],
poetry: [
'/assets/poetry/emb_p33_4.png',
'/assets/poetry/emb_p49_4.png',
'/assets/poetry/emb_p59_4.png',
'/assets/poetry/emb_p67_4.png',
'/assets/poetry/emb_p25_4.png',
'/assets/poetry/emb_p66_4.png',
],
qingxiang: [
'/assets/qingxiang/emb_p7_5.png',
'/assets/qingxiang/emb_p6_8.png',
'/assets/qingxiang/emb_p7_4.png',
'/assets/qingxiang/emb_p12_2.png',
'/assets/qingxiang/emb_p7_6.png',
'/assets/qingxiang/emb_p6_2.png',
'/assets/t30-bottle.png',
],
}
export function resolveStageRedirect(pathname: string): string | null {
if (pathname.startsWith('/culture/')) return '/brand'
if (pathname.startsWith('/poetry/') && pathname !== '/poetry') return '/poetry'
if (pathname.startsWith('/qingxiang/') && pathname !== '/qingxiang') return '/qingxiang'
return null
}
+307
View File
@@ -0,0 +1,307 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const B = deepImages.brand
/** Deep-page images are unique across culture pages (no reuse). */
export const culturePages: DeepPageContent[] = [
{
path: '/culture/status',
group: 'culture',
navLabel: '文化地位',
eyebrow: '品牌传承 · 深读',
title: '杜康文化在中国酒文化中的地位',
lead: '杜康,是中国秫酒的「酒宗」「酒祖」。杜康造酒,不仅肇始了华夏酒业的辉煌诞生,铸就了酒行业顶礼膜拜的精神图腾,而且创造了行业特征与地域色彩交相辉映的杜康文化。',
layout: 'essay',
images: [B[0]],
blocks: [
{
type: 'paragraph',
text: '经过上下五千年的岁月积淀,这种文化形态所蕴含的思想观念、人文情怀、道德规范和意志品质,已成为中国酒文化的标志性符号。历史典籍的记载奠定了杜康的酒祖地位,使杜康文化成为中国酒文化的「精神原点」。',
},
{
type: 'heading',
text: '酒神的诞生',
},
{
type: 'paragraph',
text: '从谷物发酵到文明觉醒,杜康文化被赋予通神媒介,成为礼制变迁的物化见证。酒祖杜康被世人尊崇为「酒神」,是杜康文化区别于其他酒文化的主要特征。世人敬奉杜康的记载始见于隋唐。继隋人王绩的《祭杜康新庙文》之后,关于「酒神」的颂词便不断地见诸文人笔下。',
},
{
type: 'quote',
text: '他年血食汝,当配杜康神。',
source: '宋 · 苏轼《饮酒四首》',
},
{
type: 'paragraph',
text: '在民间,对杜康的崇拜以及和酒神对话的宗教意识更具生命力。酒祖故里的杜康祠祭祀酒祖的完整仪轨,酿酒作坊供奉杜康神像的传统已延续千年。杜康封坛仪式、祭祖大典等传统习俗一直保持着上古酒祭的遗风。',
},
{
type: 'heading',
text: '酒以成礼',
},
{
type: 'paragraph',
text: '杜康文化衍化出的「饮惟祀」的酒德观念,揭示了「国之大事,在祀在戎」的深层关联,塑造了中国人「酒以成礼」的文明传统。这位「始作秫酒」的传奇人物,早已超越了具体的历史人物形象,演变为民族创造力的精神图腾。',
},
{
type: 'paragraph',
text: '因而,理解杜康文化,不能只停留在「一个酒名」的层面。它是典籍立名、祠祀传承、诗酒互证与当代产业复兴共同编织的文化系统——既属于历史,也属于仍在杯盏间延续的日常。',
},
],
closing: '站在今日回望,杜康如一坛陈年佳酿——愈久愈醇,愈传愈清。',
},
{
path: '/culture/person',
group: 'culture',
navLabel: '杜康其人',
eyebrow: '品牌传承 · 深读',
title: '杜康其人',
lead: '先秦典籍《世本》载「杜康作秫酒」;东汉许慎《说文解字》谓:「古者少康初作箕帚、秫酒。少康,杜康也。」酒之所兴,肇自上皇。',
layout: 'person',
images: [B[1]],
blocks: [
{
type: 'paragraph',
text: '关于杜康的生平,历代文献记载虽有异说,但其「始作秫酒」的文化身份却高度一致。杜康既是人名,也逐渐成为地名与酒名——三重身份相互映照,构成中国酒文化最独特的符号结构。',
},
{
type: 'heading',
text: '典籍中的酒祖',
},
{
type: 'paragraph',
text: '《世本》将杜康写入文明源头叙事;《说文解字》以语言学佐证「少康即杜康」。后世史家、文人不断追述,使杜康从传说人物上升为可被祭祀、可被吟咏、可被继承的文化共主。',
},
{
type: 'list',
items: [
'人名:始酿秫酒的传奇人物',
'地名:杜康村、杜康泉、杜康仙庄等地理记忆',
'酒名:杜康成为清香美酒与诗酒意象的通行符号',
],
},
{
type: 'heading',
text: '从人物到符号',
},
{
type: 'paragraph',
text: '当「杜康」同时指向人、地、酒,它便不再只是传记对象,而是一套可被世代调用的文化语法。正因如此,走进杜康,从来不只是认识一个人,而是进入一整套关于粮食、礼乐、故土与诗心的文明系统。',
},
],
},
{
path: '/culture/brew',
group: 'culture',
navLabel: '杜康造酒',
eyebrow: '品牌传承 · 深读',
title: '杜康造酒',
lead: '西晋江统《酒诰》云:酒之所兴,肇自上皇;或云仪狄,一曰杜康。有饭不尽,委余空桑,郁积成味,久蓄气芳——本出于此,不由奇方。',
layout: 'timeline',
images: [B[2]],
blocks: [
{
type: 'list',
items: [
'余饭委空桑:偶然的发酵,成为文明对自然法则的觉悟',
'以秫为骨:原料之纯,决定酒体之正',
'以曲为魂:曲药生香,技艺传续',
'清蒸二次清:后世清香路径仍可辨认的工艺基因',
'从作坊到祠祀:造酒升华为礼乐与祭祀的文明工程',
],
},
{
type: 'heading',
text: '空桑造酒',
},
{
type: 'paragraph',
text: '「空桑造酒」的叙事,把偶然的余饭发酵,升华为文明对自然法则的觉悟。杜康并非炫技的奇方发明者,而是把「以秫为骨、以曲为魂」的智慧带入人间日常的那一位。',
},
{
type: 'quote',
text: '有饭不尽,委余空桑,郁积成味,久蓄气芳。',
source: '晋 · 江统《酒诰》',
},
{
type: 'paragraph',
text: '秫酒传统强调原料之纯与工艺之清。清蒸、清糟、清烧的路径,在后世清香型白酒中仍清晰可辨。杜康造酒遗址公园内,至今仍生长着被称为「酒树」的古树,枝繁叶茂,见证酒兴千年。',
},
],
closing: '技进乎道,香传千年。',
},
{
path: '/culture/temple',
group: 'culture',
navLabel: '杜康祠',
eyebrow: '品牌传承 · 深读',
title: '杜康祠',
lead: '杜康因始创秫酒被称为酒祖。唐宋以来,杜康祠、杜康庙遍布各地;古人敬酒祖、拜酒神,多在杜康祠内完成。',
layout: 'shrine',
images: [B[3]],
blocks: [
{
type: 'paragraph',
text: '杜康造酒遗址公园内的杜康祠,为当今规模大、保留完整的古建筑群之一。祠祀仪轨延续封坛祭祖的遗风,使「饮惟祀」的酒德观念可触可感。',
},
{
type: 'heading',
text: '祠祀与诗心',
},
{
type: 'paragraph',
text: '苏轼有「从今东坡室,不立杜康祀」之句,正说明杜康祠祀在文人精神世界中的位置——可立可罢,却始终是心灵秩序的参照。陆游「堂上祠杜康」、周淑禧《杜康庙》等作品,亦不断加固这一文化记忆。',
},
{
type: 'list',
items: ['封坛祭祖:延续上古酒祭遗风', '供奉神像:酿酒作坊千年传统', '诗酒互证:祠祀进入文人书写'],
},
{
type: 'paragraph',
text: '走进杜康祠,看见的不只是建筑与香火,更是一条把「酒」抬升为「礼」的文化路径:敬畏粮食、敬畏创造、敬畏可共享的人间秩序。',
},
],
},
{
path: '/culture/sites',
group: 'culture',
navLabel: '造酒遗址',
eyebrow: '品牌传承 · 深读',
title: '杜康造酒遗址',
lead: '杜康村、杜康泉、空桑涧……酒祖故里的山水与作坊,把「故土」写成可以走入的文化现场。',
layout: 'sites',
images: [B[4]],
blocks: [
{
type: 'heading',
text: '杜康村',
},
{
type: 'paragraph',
text: '村落因酒祖得名,也因酒香延续。山水环抱之中,作坊、祠庙与口述传统并存,构成活态的遗产空间。访客在巷陌与田野间行走,便是在阅读一部不用翻页的酒史。',
},
{
type: 'heading',
text: '杜康泉',
},
{
type: 'paragraph',
text: '泉脉滋养酿造,也滋养地方记忆。好水出好酒,在酒祖叙事里不是广告语,而是地理与工艺相互成就的常识。泉、曲、粮、火,共同写下清香的底色。',
},
{
type: 'heading',
text: '遗址中的文明证据',
},
{
type: 'paragraph',
text: '从空桑传说到全国重点文物保护单位的碑刻与陵寝记忆,故里把「可说的史」与「可见的物」叠合在一起,让访客在行走中完成对酒祖的确认。',
},
],
},
{
path: '/culture/figures',
group: 'culture',
navLabel: '名人与杜康',
eyebrow: '品牌传承 · 深读',
title: '名人与杜康',
lead: '三曹、苏轼、邵雍,乃至近现代政治人物与杜康的交集,使酒祖叙事不断获得新的时代回声。',
layout: 'figures',
images: [B[5]],
blocks: [
{
type: 'heading',
text: '三曹与杜康',
},
{
type: 'paragraph',
text: '曹操《短歌行》「何以解忧,唯有杜康」一出,杜康从酒祖之名跃升为诗心密码。曹植等作品亦把酒与志向、生命感喟相连,奠定魏晋诗性转化的基调。',
},
{
type: 'heading',
text: '苏轼与杜康',
},
{
type: 'paragraph',
text: '苏轼一生爱酒,也爱杜康。贬谪海南途中,仍以杜康为兄弟相别、自我安顿的意象——「从今东坡室,不立杜康祀」的旷达背后,是诗酒对命运的温柔对抗。',
},
{
type: 'heading',
text: '时代回响',
},
{
type: 'paragraph',
text: '从邵雍安乐窝的哲思,到当代「复兴杜康」的历史批示记忆,名人与杜康的对话从未停止。酒祖因此始终活在公共文化的现场,而不只沉睡在典籍里。',
},
],
},
{
path: '/culture/chronicle',
group: 'culture',
navLabel: '大事记',
eyebrow: '品牌传承 · 深读',
title: '杜康大事记',
lead: '从先秦典籍立名,到魏晋诗性转化,再到祠祀千年与当代品牌复兴——时间本身,就是最好的陈酿。',
layout: 'timeline',
images: [],
blocks: [
{
type: 'list',
items: [
'先秦:典籍记载「杜康作秫酒」,酒祖身份确立',
'魏晋:曹操《短歌行》完成诗性转化,杜康进入文人精神世界',
'隋唐:祭文与祠庙推动酒神崇拜,杜康可祀可敬',
'唐宋以降:杜康祠庙广布,文人吟咏与地方祭祀并行',
'近现代:故里保护、产业复兴与文化传播相互推动',
'当代:酒祖杜康清香品牌复兴,好客机制让千年清香可落地',
],
},
{
type: 'heading',
text: '香传的线索',
},
{
type: 'paragraph',
text: '大事记不是年表的堆砌,而是一条「香传」的线索:每一次被书写、被祭祀、被酿造,都是杜康文化的再一次醒酒。沿着时间往前走,也能看见清香如何从典籍走入杯盏,从纪念走入日常。',
},
],
closing: '先秦立名 → 魏晋诗化 → 祠祀千年 → 当代复兴',
},
{
path: '/culture/factory',
group: 'culture',
navLabel: '酒厂介绍',
eyebrow: '品牌传承 · 深读',
title: '酒厂介绍',
lead: '山西杜康酒业扎根杏花村千年酿酒圣地,与清香名酒同源同脉,一清到底。',
layout: 'factory',
images: [],
blocks: [
{
type: 'paragraph',
text: '酒厂承续酒祖故里与产区生态,以清香工艺与品质体系服务宴请与日常多场景。品牌不只卖酒,更把「酒祖叙事 + 清香表达 + 好客机制」做成可感知的体验。',
},
{
type: 'heading',
text: '产区与使命',
},
{
type: 'paragraph',
text: '杏花村一带的酿酒微气候与生态,为清香提供了稳定的地理底座。企业以「坚守纯粹品质,悦享清逸人生」为使命句,把品质、体验与城市运营放在同一条链路里。',
},
{
type: 'list',
items: [
'产区:杏花村一带酿酒生态与微气候',
'工艺:清香「7321」路径,讲究清蒸二次清',
'体验:文化馆、品鉴与溯源之旅可走入',
'使命:坚守纯粹品质,悦享清逸人生',
],
},
{
type: 'paragraph',
text: '当酒厂资源与城市运营、门店核销连成闭环,酒祖的清香便从典籍走进餐桌,从纪念走进日常。',
},
],
},
]
+126
View File
@@ -0,0 +1,126 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const P = deepImages.poetry
/** Unique images among poetry deep pages. */
export const poetryDeepPages: DeepPageContent[] = [
{
path: '/poetry/anthology',
group: 'poetry',
navLabel: '诗酒选萃',
eyebrow: '诗酒文化 · 深读',
title: '酒香醉天下,千年诗未央',
lead: '「诗酒」二字,是中国人心里最动人的共鸣。以杜康为意象创作的诗词曲赋,名家荟萃:曹操、曹植、杜甫、白居易、苏轼、辛弃疾……把一抹醇香写进东方美学。',
layout: 'poem-wall',
images: [P[0]],
blocks: [
{
type: 'paragraph',
text: '《杜康古诗词曲赋译注》精选汉代至清代关于杜康的韵文作品数十篇。走进这些篇章,才能体味先贤对生命、创造与宿命的感悟。诗与酒彼此成全:酒给诗以温度,诗给酒以永恒。',
},
{
type: 'heading',
text: '主诗签 · 曹操《短歌行》',
},
{
type: 'quote',
text: '对酒当歌,人生几何?譬如朝露,去日苦多。……何以解忧,唯有杜康。',
source: '东汉 · 曹操《短歌行》',
},
{
type: 'paragraph',
text: '一边饮酒一边高歌,一边感慨叹息:人生短促,像晨露转瞬即逝。什么可以消除心中的忧愁?唯有杜康美酒。魏武帝以此完成从酒祖之名到诗心密码的决定性转化。',
},
{
type: 'heading',
text: '辅诗签 · 苏轼',
},
{
type: 'quote',
text: '他年血食汝,当配杜康神。',
source: '宋 · 苏轼',
},
{
type: 'heading',
text: '辅诗签 · 白居易',
},
{
type: 'quote',
text: '不似杜康神用速,十分一盏便开眉。',
source: '唐 · 白居易',
},
{
type: 'paragraph',
text: '白居易写杜康酒能使人愁苦尽消;十分之一杯便足以开眉展颜——神速、开眉,是唐人对杜康最亲切的体认。',
},
{
type: 'heading',
text: '辅诗签 · 辛弃疾',
},
{
type: 'quote',
text: '杜康初筮,正得云雷。',
source: '宋 · 辛弃疾',
},
{
type: 'paragraph',
text: '从魏武的感喟,到唐人的开眉,再到宋人的神思——杜康完成了从「酒祖」到「诗心密码」的转化,成为文人精神世界的通行意象。',
},
],
closing: '斟半盏清醑,展卷轻吟:让酒香携着古人的魂魄,穿越千年与我们共醉。',
},
{
path: '/poetry/tang',
group: 'poetry',
navLabel: '唐宋余韵',
eyebrow: '诗酒文化 · 深读',
title: '唐宋余韵:开眉与神思',
lead: '唐人开眉,宋人神思。杜康在不同朝代被反复调用,成为情绪、命运与礼赞的公共修辞。',
layout: 'ink-scroll',
images: [P[1]],
blocks: [
{
type: 'heading',
text: '江统《酒诰》',
},
{
type: 'quote',
text: '酒之所兴,肇自上皇。或云仪狄,一曰杜康。有饭不尽,委余空桑,郁积成味,久蓄气芳。本出于此,不由奇方。',
source: '晋 · 江统',
},
{
type: 'paragraph',
text: '这篇文献把造酒还原为自然与日用的相遇,也为后世「空桑造酒」图像提供了经典出处。它提醒我们:杜康之美,首先来自对日常与自然的诚实。',
},
{
type: 'heading',
text: '王绩《祭杜康新庙文》',
},
{
type: 'paragraph',
text: '隋唐之际,祭文把杜康正式推上「可祀」的神位。酒祖不仅可饮可咏,更可敬可拜——诗酒文化由此与祠祀传统合流。庙与诗、香火与墨迹,共同把杜康写进公共记忆。',
},
{
type: 'heading',
text: '苏黄辛陆的回声',
},
{
type: 'paragraph',
text: '苏轼、张耒、晁说之、陆游等不断以杜康入诗。或配神,或皈依,或庙里做忠臣——杜康成为文人自我安顿时最顺手的一杯。',
},
{
type: 'list',
items: [
'唐:开眉、神用速,贴近日常情感',
'宋:配神、初筮云雷,指向命运与哲思',
'后世:祭庙、踏青、题咏,杜康成为可游可祀的文化现场',
],
},
{
type: 'paragraph',
text: '读唐宋余韵,是在听一条悠长的回声:每个时代都借杜康说出自己的忧与喜。酒未冷,诗未央。',
},
],
},
]
+273
View File
@@ -0,0 +1,273 @@
import type { DeepPageContent } from './types'
import { deepImages } from './assets'
const Q = deepImages.qingxiang
/**
* Unique images across qingxiang deep pages:
* project=Q5, style=Q3, experience=Q1, tour=Q0 hall
* position / tasting = text-only
* Q2/Q4/Q6 reserved for main landing (hall/storefront/bottle)
*/
export const qingxiangDeepPages: DeepPageContent[] = [
{
path: '/qingxiang/project',
group: 'qingxiang',
navLabel: '项目与赛道',
eyebrow: '清香宝典 · 深读',
title: '清香是未来白酒行业的黄金赛道',
lead: '浓香内卷、酱香回落的时代,清香正成为确定性更强的黄金赛道。酒祖杜康扎根山西杏花村——千年酒都,与清香名酒同源同脉,一清到底。',
layout: 'manifesto',
images: [Q[5]],
blocks: [
{
type: 'paragraph',
text: '山西杜康酒业有限公司以酒祖杜康清香系列产品为经营主线,位于中国白酒名镇山西汾阳。产区与名酒企业同脉相邻,共享酿酒微气候与生态。选择清香,是选择一条更干净、更可持续、更适配当代饮酒场景的路径。',
},
{
type: 'heading',
text: '杜康清香模式',
},
{
type: 'paragraph',
text: '以杜康文化为核心,以沉浸式体验为营销抓手,把社群变成增长引擎。一套可复制的体验闭环:一座杜康文化馆,一瓶杜康清香酒,一桌杜康好客宴,一群杜康粉丝圈。',
},
{
type: 'list',
items: [
'文化馆:可参观、可讲解、可品鉴',
'产品:清香表达清晰,适配宴请与日常',
'宴席:酒餐同赴,把体验落在餐桌上',
'社群:口碑裂变,让清香可传播',
],
},
{
type: 'paragraph',
text: '赛道判断要落到可运营的模式上。清香不只是风味标签,更是「文化可讲、产品可品、宴请可落、社群可传」的系统能力。',
},
],
},
{
path: '/qingxiang/position',
group: 'qingxiang',
navLabel: '定位与价值',
eyebrow: '清香宝典 · 深读',
title: '杜康清香的定位与使命',
lead: '承千年清韵,铸清香白酒标杆企业。守古法清酿,酿纯粹绵柔清雅好酒。',
layout: 'matrix',
images: [],
blocks: [
{
type: 'heading',
text: '四维定位',
},
{
type: 'list',
items: [
'企业定位:承千年清韵,铸清香白酒标杆企业',
'品质定位:守古法清酿,酿纯粹绵柔清雅好酒',
'品牌定位:传酒祖清魂,塑高端清香文化名酒',
'营销定位:拓清香赛道,引全民清雅饮酒新风尚',
],
},
{
type: 'heading',
text: '价值观',
},
{
type: 'list',
items: [
'对产品:真材实料,好酒说话',
'对客户:以客为尊,让好客也能赚钱',
'对员工:成长与成果共享',
'对股东:稳健经营,可持续回报',
],
},
{
type: 'heading',
text: '使命',
},
{
type: 'paragraph',
text: '使命句:坚守纯粹品质,悦享清逸人生。定位不是口号墙,而是产品、体验与合作机制共同兑现的承诺边界。',
},
],
},
{
path: '/qingxiang/style',
group: 'qingxiang',
navLabel: '风格与工艺',
eyebrow: '清香宝典 · 深读',
title: '绵 · 净 · 雅,与清香五大法宝',
lead: '粮香纯正无杂味,清香干净通透;入口绵柔顺滑,落口爽利干脆;杂醇含量低,不口干、不上头,醒酒快。',
layout: 'triad',
images: [Q[3]],
blocks: [
{
type: 'heading',
text: '绵',
},
{
type: 'paragraph',
text: '入口绵柔顺滑,余味悠长。宴请与日常都需要「喝得下去、坐得住」的体感,绵是清香最易亲近的第一印象。',
},
{
type: 'heading',
text: '净',
},
{
type: 'paragraph',
text: '清香通透,一清到底,无杂味杂香。「净」是工艺自信:清蒸二次清的路径,让干净成为可被验证的风格。',
},
{
type: 'heading',
text: '雅',
},
{
type: 'paragraph',
text: '低刺激、多场景,商务与雅集皆宜。雅不是疏离,而是让饮酒回到从容与体面。',
},
{
type: 'heading',
text: '工艺要点',
},
{
type: 'paragraph',
text: '大麦、豌豆、高粱三种原料;清香「7321」工艺,讲究「清蒸二次清」「一清到底」,味道更醇厚。',
},
{
type: 'heading',
text: '五大法宝',
},
{
type: 'list',
items: [
'产地:扎根杏花村千年酿酒圣地,同源同脉',
'基因:与龙头清香企业同脉同艺的品质参照',
'产能与粮源:稳定供给,品质可控',
'团队:专业品评与全链路品控',
'资本与体系:支撑产品规划与渠道建设',
],
},
],
closing: '千年杜康,醉美清香。',
},
{
path: '/qingxiang/tasting',
group: 'qingxiang',
navLabel: '品鉴玩法',
eyebrow: '清香宝典 · 深读',
title: '抿 · 咂 · 呵,与更多品鉴玩法',
lead: '一场好的品鉴,不只是喝,而是把感官、故事与互动编织在一起。',
layout: 'steps',
images: [],
blocks: [
{
type: 'list',
items: [
'抿——微启触香,品粮香本味',
'咂——舌卷含香,品绵柔入喉',
'呵——缓缓呵气,品回甘生津',
],
},
{
type: 'heading',
text: '观色 · 闻香 · 品味 · 余味',
},
{
type: 'paragraph',
text: '观色:清亮透明微黄。闻香:粮香纯正清爽。品味:绵柔爽净,一清到底。余味:干净悠长,不上头。把感官步骤说清楚,用户才敢相信「好喝」不是推销。',
},
{
type: 'heading',
text: '更多玩法',
},
{
type: 'paragraph',
text: '地动山摇、肌肤相亲、酒王大赛、看酒花观挂杯、背标法等,让用户亲手证明「好不好喝」,比千言万语更有说服力。一次沉浸体验,往往胜过十次广告。',
},
],
},
{
path: '/qingxiang/experience',
group: 'qingxiang',
navLabel: '沉浸体验',
eyebrow: '清香宝典 · 深读',
title: '三级沉浸式体验与品鉴会',
lead: '场景体验是用户快速认知的重要武器,也是制造口碑、传播引爆的发源地。',
layout: 'scenes',
images: [Q[1]],
blocks: [
{
type: 'heading',
text: '三级体验',
},
{
type: 'list',
items: [
'酒厂文化溯源之旅:参观酿造、包装、酒库与遗址',
'杜康清香文化馆:沉浸式产品与文化体验',
'品鉴会 123:一场品鉴宴、两支宣传片、三本手册',
],
},
{
type: 'heading',
text: '文化馆「五个一」',
},
{
type: 'list',
items: [
'一尊酒祖像',
'一幅画(空桑秽饭图)',
'一首诗歌(《短歌行》)',
'一座杜康祠',
'一份伟大的批示记忆:复兴杜康,为国争光',
],
},
{
type: 'heading',
text: '体验闭环',
},
{
type: 'paragraph',
text: '品鉴结束要闭环:发朋友圈、加好友、确认回访。体验不是热闹一场,而是把信任留下来。',
},
],
},
{
path: '/qingxiang/tour',
group: 'qingxiang',
navLabel: '文化馆与溯源',
eyebrow: '清香宝典 · 深读',
title: '文化馆体验与酒厂溯源',
lead: '把杜康故事做成可走入的空间:讲解、观影、玩法、DIY 调酒与品鉴宴,一气呵成。',
layout: 'tour',
images: [Q[0]],
blocks: [
{
type: 'list',
items: [
'杜康文化讲解(约 10 分钟)',
'观看杜康清香宣传片(约 10 分钟)',
'白酒鉴赏玩法 23 种',
'DIY 自调酒:留下专属标签',
'品鉴宴席与盲品',
'领导总结与欢送',
],
},
{
type: 'heading',
text: '溯源之旅',
},
{
type: 'paragraph',
text: '走进厂区与遗址,看见粮、曲、窖、酒的真实路径。清香的「一清到底」,最终要在现场被看见、被闻到、被记住。空间叙事与产线真实,共同完成信任交付。',
},
{
type: 'paragraph',
text: '文化馆不是展板堆砌,而是把酒祖、清香与好客连成可跟随的路径。每一次到访,都应留下可复述的故事与可回访的关系。',
},
],
closing: '让每一次到访,都成为一次清雅之约。',
},
]
+39
View File
@@ -0,0 +1,39 @@
export type DeepBlock =
| { type: 'paragraph'; text: string }
| { type: 'quote'; text: string; source?: string }
| { type: 'list'; items: string[] }
| { type: 'heading'; text: string }
/** Distinct page shells — not just a CSS rename of one template */
export type DeepLayoutKind =
| 'essay'
| 'person'
| 'timeline'
| 'shrine'
| 'sites'
| 'figures'
| 'factory'
| 'poem-wall'
| 'ink-scroll'
| 'manifesto'
| 'matrix'
| 'triad'
| 'steps'
| 'scenes'
| 'tour'
export type DeepPageContent = {
path: string
group: 'culture' | 'poetry' | 'qingxiang'
navLabel: string
eyebrow: string
title: string
lead: string
/** @deprecated kept for theme hints; prefer layout */
variant?: 'classic' | 'scroll' | 'stage' | 'river'
layout: DeepLayoutKind
/** Unique across all deep pages; empty = text-only */
images: string[]
blocks: DeepBlock[]
closing?: string
}
+94
View File
@@ -0,0 +1,94 @@
import { culturePages } from './deep/culture'
import { poetryDeepPages } from './deep/poetry'
import { qingxiangDeepPages } from './deep/qingxiang'
import type { DeepPageContent } from './deep/types'
export const allDeepPages: DeepPageContent[] = [...culturePages, ...poetryDeepPages, ...qingxiangDeepPages]
export const deepPageMap = Object.fromEntries(allDeepPages.map((p) => [p.path, p])) as Record<
string,
DeepPageContent
>
export type NavChild = { label: string; path: string; kind: 'main' | 'deep' }
export type NavGroup = {
id: string
label: string
/** first landing when clicking group title on desktop */
entryPath: string
children: NavChild[]
}
export const navGroups: NavGroup[] = [
{
id: 'culture',
label: '酒祖文化',
entryPath: '/brand',
children: [
{ label: '品牌传承', path: '/brand', kind: 'main' },
{ label: '诗酒文化', path: '/poetry', kind: 'main' },
...culturePages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
...poetryDeepPages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
],
},
{
id: 'qingxiang',
label: '清香臻品',
entryPath: '/qingxiang',
children: [
{ label: '清香', path: '/qingxiang', kind: 'main' },
...qingxiangDeepPages.map((p) => ({ label: p.navLabel, path: p.path, kind: 'deep' as const })),
],
},
{
id: 'haoke',
label: '好客体系',
entryPath: '/haoke',
children: [
{ label: '好客模式', path: '/haoke', kind: 'main' },
{ label: '三端协同', path: '/system', kind: 'main' },
],
},
{
id: 'tech',
label: '科技实力',
entryPath: '/tech',
children: [
{ label: '科技驱动', path: '/tech', kind: 'main' },
{ label: '酒厂实力', path: '/subsidy', kind: 'main' },
{ label: '科技板块', path: '/tech-detail', kind: 'main' },
],
},
{
id: 'contact',
label: '合作联系',
entryPath: '/partner',
children: [
{ label: '城市合伙人', path: '/partner', kind: 'main' },
{ label: '联系我们', path: '/contact', kind: 'main' },
],
},
]
/** flat main modules for stage mode */
export const stageNavItems = [
{ path: '/', label: '首页' },
{ path: '/brand', label: '品牌传承' },
{ path: '/poetry', label: '诗酒文化' },
{ path: '/qingxiang', label: '清香' },
{ path: '/haoke', label: '好客模式' },
{ path: '/system', label: '三端协同' },
{ path: '/tech', label: '科技驱动' },
{ path: '/subsidy', label: '酒厂实力' },
{ path: '/partner', label: '城市合伙人' },
{ path: '/tech-detail', label: '科技板块' },
{ path: '/contact', label: '联系我们' },
]
export function findGroupByPath(pathname: string) {
return navGroups.find((g) =>
g.children.some((c) => pathname === c.path || (c.path !== '/' && pathname.startsWith(`${c.path}/`))),
)
}
+328
View File
@@ -0,0 +1,328 @@
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: '你请客,我买单',
domain: 'www.dukanghaoke.com',
copyright: '© 杜康好客 2026',
icp: '晋ICP备2026004970号',
compliance:
'理性饮酒,未成年人禁止饮酒。本展示内容仅供品牌与合作了解,不构成任何收益承诺。',
dataNote: '行业数据引自公开研究报告,仅供参考。支持细则以合作政策为准。',
}
export const moduleEntries = [
{ to: '/brand', label: '品牌传承', desc: '酒祖精神原点' },
{ to: '/poetry', label: '诗酒文化', desc: '何以解忧,唯有杜康' },
{ to: '/qingxiang', label: '清香', desc: '绵 · 净 · 雅' },
{ to: '/haoke', label: '好客模式', desc: '你请客,我买单' },
{ to: '/system', label: '三端协同', desc: '一城一盘棋' },
{ to: '/tech', label: '科技驱动', desc: '可运营的数字轨道' },
{ to: '/subsidy', label: '酒厂实力', desc: '十亿级支持' },
{ to: '/partner', label: '城市合伙人', desc: '共建一座城的好客' },
{ to: '/tech-detail', label: '科技板块', desc: '四端系统详情' },
{ to: '/contact', label: '联系我们', desc: '扫码对接负责人' },
] as const
/** Pure visuals only (no PDF page screenshots with baked-in text) */
export const images = {
logo: '/assets/logo.png',
qr: '/assets/qr-contact.png',
t30: '/assets/t30-bottle.png',
brand: {
manor: '/assets/brand/emb_p6_5.png',
monument: '/assets/brand/emb_p31_1.png',
landscape: '/assets/brand/emb_p15_3.png',
relic: '/assets/brand/emb_p33_1.png',
},
poetry: {
inkA: '/assets/poetry/emb_p33_4.png',
inkB: '/assets/poetry/emb_p49_4.png',
inkC: '/assets/poetry/emb_p59_4.png',
inkD: '/assets/poetry/emb_p67_4.png',
},
qingxiang: {
hall: '/assets/qingxiang/emb_p7_4.png',
interior: '/assets/qingxiang/emb_p6_2.png',
storefront: '/assets/qingxiang/emb_p7_6.png',
scene: '/assets/qingxiang/emb_p12_2.png',
},
}
+29
View File
@@ -0,0 +1,29 @@
import { useEffect, useState } from 'react'
export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState<boolean>(() => {
if (typeof window === 'undefined') return false
return window.innerWidth < breakpoint
})
useEffect(() => {
if (typeof window === 'undefined') return
const mql = window.matchMedia(`(max-width: ${breakpoint - 0.02}px)`)
const onChange = (e: MediaQueryListEvent) => {
setIsMobile(e.matches)
}
setIsMobile(mql.matches)
if (mql.addEventListener) {
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
} else {
mql.addListener(onChange)
return () => mql.removeListener(onChange)
}
}, [breakpoint])
return isMobile
}
+14
View File
@@ -0,0 +1,14 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react'
import { useLocation } from 'react-router-dom'
const StageContext = createContext(false)
export function StageProvider({ children }: { children: ReactNode }) {
const location = useLocation()
const isStage = useMemo(() => new URLSearchParams(location.search).get('mode') === 'stage', [location.search])
return <StageContext.Provider value={isStage}>{children}</StageContext.Provider>
}
export function useStageMode() {
return useContext(StageContext)
}
+6
View File
@@ -58,6 +58,12 @@ html:has(body.stage-mode) {
height: 100%;
}
@media (max-width: 760px) {
html {
scroll-snap-type: y proximity;
}
}
button,
input,
textarea,
+4 -2
View File
@@ -1,7 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import App from './App'
import './styles/tokens.css'
import './styles/animations.css'
import './styles/responsive.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
+237
View File
@@ -0,0 +1,237 @@
/* ===== Mobile Shell Dedicated Styling ===== */
.mobile-shell {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
background: var(--paper, #f7f2ea);
color: var(--ink, #1a1512);
padding-bottom: env(safe-area-inset-bottom, 0px);
position: relative;
overflow-x: hidden;
}
/* Fixed Header with Status Bar Safe Area Top */
.m-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(3.6rem + env(safe-area-inset-top, 0px));
padding-top: env(safe-area-inset-top, 0px);
padding-left: 1rem;
padding-right: 1rem;
box-sizing: border-box;
z-index: 1000;
background: rgba(247, 242, 234, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(166, 28, 32, 0.12);
display: flex;
align-items: center;
justify-content: space-between;
transition: background 0.3s ease;
}
.theme-tech .m-header {
background: rgba(18, 16, 20, 0.95);
border-bottom-color: rgba(196, 163, 90, 0.2);
}
.m-brand {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.m-logo {
width: 2rem;
height: 2rem;
object-fit: contain;
}
.m-title {
font-family: 'Noto Serif SC', serif;
font-size: 1.1rem;
font-weight: 600;
color: var(--brand, #a61c20);
letter-spacing: 0.05em;
}
.theme-tech .m-title {
color: var(--gold, #c4a35a);
}
.m-header-actions {
display: flex;
align-items: center;
gap: 0.6rem;
}
.m-stage-badge {
padding: 0.25rem 0.6rem;
border-radius: 1rem;
font-size: 0.72rem;
color: #ffffff;
background: linear-gradient(135deg, #a61c20 0%, #7d1216 100%);
border: 1px solid rgba(196, 163, 90, 0.3);
text-decoration: none;
font-weight: 500;
box-shadow: 0 2px 8px rgba(166, 28, 32, 0.25);
}
.m-menu-btn {
width: 2.5rem;
height: 2.5rem;
border: none;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.32rem;
cursor: pointer;
z-index: 1002;
padding: 0;
}
.m-menu-btn span {
width: 1.3rem;
height: 2px;
background: var(--brand, #a61c20);
border-radius: 1px;
transition: all 0.3s ease;
}
.theme-tech .m-menu-btn span {
background: var(--gold, #c4a35a);
}
.m-menu-btn.is-open span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.m-menu-btn.is-open span:nth-child(2) {
opacity: 0;
}
.m-menu-btn.is-open span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* Drawer Overlay & Panel */
.m-drawer-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.m-drawer-overlay.is-open {
opacity: 1;
visibility: visible;
}
.m-drawer {
position: fixed;
top: calc(3.6rem + env(safe-area-inset-top, 0px));
right: 0;
bottom: 0;
width: min(20rem, 85vw);
background: #f7f2ea;
z-index: 1001;
padding: 1.25rem 1.25rem calc(2rem + env(safe-area-inset-bottom, 0px));
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
}
.theme-tech .m-drawer {
background: #141117;
color: #f3ece4;
}
.m-drawer.is-open {
transform: translateX(0);
}
.m-drawer-section {
margin-bottom: 1.25rem;
}
.m-drawer-sec-title {
font-size: 0.78rem;
letter-spacing: 0.15em;
color: var(--gold, #c4a35a);
margin: 0 0 0.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid rgba(196, 163, 90, 0.2);
}
.m-drawer-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.7rem 0.5rem;
font-size: 0.95rem;
color: var(--ink, #1a1512);
text-decoration: none;
border-bottom: 1px solid rgba(166, 28, 32, 0.06);
border-radius: 0.2rem;
transition: background 0.2s ease;
}
.theme-tech .m-drawer-item {
color: #f3ece4;
border-bottom-color: rgba(196, 163, 90, 0.08);
}
.m-drawer-item.is-active {
color: var(--brand, #a61c20);
font-weight: 600;
background: rgba(166, 28, 32, 0.08);
}
.theme-tech .m-drawer-item.is-active {
color: var(--gold, #c4a35a);
background: rgba(196, 163, 90, 0.12);
}
.m-drawer-sublist {
padding-left: 0.85rem;
display: grid;
gap: 0.2rem;
margin-top: 0.2rem;
}
.m-drawer-subitem {
display: block;
padding: 0.5rem 0.5rem;
font-size: 0.86rem;
color: var(--ink-soft, #5c534c);
text-decoration: none;
border-radius: 0.2rem;
}
.theme-tech .m-drawer-subitem {
color: rgba(243, 236, 228, 0.7);
}
.m-drawer-subitem.is-active {
color: var(--brand, #a61c20);
font-weight: 600;
}
/* Main Content Wrapper - Push down content to avoid Fixed Header overlap */
.m-main {
flex: 1;
width: 100%;
padding-top: calc(3.6rem + env(safe-area-inset-top, 0px));
box-sizing: border-box;
}
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState, type ReactNode } from 'react'
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
import { navGroups } from '../content/nav'
import { images } from '../content/site'
import { Footer } from '../components/Footer'
import './MobileShell.css'
const techRoutes = new Set(['/tech', '/subsidy', '/partner', '/tech-detail', '/contact'])
export function MobileShell({ children }: { children?: ReactNode }) {
const location = useLocation()
const [drawerOpen, setDrawerOpen] = useState(false)
const isTechPage = techRoutes.has(location.pathname)
// Close drawer on navigation
useEffect(() => {
setDrawerOpen(false)
window.scrollTo({ top: 0, behavior: 'auto' })
}, [location.pathname, location.search])
// Prevent background scroll when drawer is open
useEffect(() => {
document.body.style.overflow = drawerOpen ? 'hidden' : ''
return () => {
document.body.style.overflow = ''
}
}, [drawerOpen])
return (
<div className={`mobile-shell ${isTechPage ? 'theme-tech' : ''}`}>
{/* Mobile Top Header */}
<header className="m-header">
<Link to="/" className="m-brand">
<img src={images.logo} alt="杜康好客" className="m-logo" />
<span className="m-title"></span>
</Link>
<div className="m-header-actions">
<Link to="/?mode=stage" className="m-stage-badge">
</Link>
<button
type="button"
className={`m-menu-btn ${drawerOpen ? 'is-open' : ''}`}
onClick={() => setDrawerOpen((v) => !v)}
aria-label={drawerOpen ? '关闭菜单' : '打开菜单'}
>
<span />
<span />
<span />
</button>
</div>
</header>
{/* Drawer Mask & Navigation Panel */}
<div
className={`m-drawer-overlay ${drawerOpen ? 'is-open' : ''}`}
onClick={() => setDrawerOpen(false)}
/>
<div className={`m-drawer ${drawerOpen ? 'is-open' : ''}`}>
<div style={{ marginBottom: '1rem', fontWeight: 600, fontSize: '1.05rem', color: 'var(--brand)' }}>
</div>
<div className="m-drawer-section">
<Link
to="/"
className={`m-drawer-item ${location.pathname === '/' ? 'is-active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
<span></span>
<span></span>
</Link>
</div>
{navGroups.map((group) => (
<div key={group.id} className="m-drawer-section">
<div className="m-drawer-sec-title">{group.label}</div>
<div className="m-drawer-sublist">
{group.children.map((child) => (
<NavLink
key={child.path}
to={child.path}
className={({ isActive }) => `m-drawer-subitem ${isActive ? 'is-active' : ''}`}
onClick={() => setDrawerOpen(false)}
>
{child.label}
</NavLink>
))}
</div>
</div>
))}
</div>
{/* Main Content Area */}
<main className="m-main" key={location.pathname}>
{children || <Outlet />}
</main>
<Footer />
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More