fix:提交代码
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
@@ -34,7 +34,7 @@
|
||||
"name": "杜康好客",
|
||||
"alternateName": "酒祖杜康",
|
||||
"url": "https://www.dukanghaoke.com/",
|
||||
"logo": "https://www.dukanghaoke.com/assets/logo.png",
|
||||
"logo": "https://www.dukanghaoke.com/assets/favicon.png",
|
||||
"description": "杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。"
|
||||
},
|
||||
{
|
||||
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 234 B After Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 449 KiB |
@@ -8,6 +8,9 @@ const root = path.resolve(__dirname, '..')
|
||||
const outThumb = path.join(root, 'public/assets/stage/thumb')
|
||||
const outFull = path.join(root, 'public/assets/stage/full')
|
||||
|
||||
/** Stage UI dark bg — fringe target */
|
||||
const DARK = { r: 14, g: 12, b: 16 }
|
||||
|
||||
/** @type {{ src: string; name: string; thumbW: number; fullW: number }[]} */
|
||||
const STAGE_SOURCES = [
|
||||
{ src: 'public/assets/logo.png', name: 'logo', thumbW: 48, fullW: 512 },
|
||||
@@ -22,29 +25,156 @@ async function ensureDirs() {
|
||||
await fs.mkdir(outFull, { recursive: true })
|
||||
}
|
||||
|
||||
function lum(r, g, b) {
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b
|
||||
}
|
||||
|
||||
function sat(r, g, b) {
|
||||
return Math.max(r, g, b) - Math.min(r, g, b)
|
||||
}
|
||||
|
||||
/**
|
||||
* White fringe → dark fringe for dark UI:
|
||||
* 1) transparent RGB → black
|
||||
* 2) semi-transparent: un-white-matte + premultiply toward black
|
||||
* 3) opaque edge near transparency: crush whitish fringe to dark
|
||||
*/
|
||||
async function defringeBottle(srcPathOrBuf) {
|
||||
const { data, info } = await sharp(srcPathOrBuf).ensureAlpha().raw().toBuffer({ resolveWithObject: true })
|
||||
const w = info.width
|
||||
const h = info.height
|
||||
const out = Buffer.from(data)
|
||||
const idx = (x, y) => (y * w + x) * 4
|
||||
|
||||
// Flood-fill only transparent pixels connected to the canvas edge.
|
||||
// This prevents transparent details inside labels/text from being treated as silhouette.
|
||||
const exterior = new Uint8Array(w * h)
|
||||
const queue = new Int32Array(w * h)
|
||||
let head = 0
|
||||
let tail = 0
|
||||
const enqueue = (x, y) => {
|
||||
const p = y * w + x
|
||||
if (exterior[p] || data[p * 4 + 3] > 16) return
|
||||
exterior[p] = 1
|
||||
queue[tail++] = p
|
||||
}
|
||||
for (let x = 0; x < w; x++) {
|
||||
enqueue(x, 0)
|
||||
enqueue(x, h - 1)
|
||||
}
|
||||
for (let y = 0; y < h; y++) {
|
||||
enqueue(0, y)
|
||||
enqueue(w - 1, y)
|
||||
}
|
||||
while (head < tail) {
|
||||
const p = queue[head++]
|
||||
const x = p % w
|
||||
const y = Math.floor(p / w)
|
||||
if (x > 0) enqueue(x - 1, y)
|
||||
if (x + 1 < w) enqueue(x + 1, y)
|
||||
if (y > 0) enqueue(x, y - 1)
|
||||
if (y + 1 < h) enqueue(x, y + 1)
|
||||
}
|
||||
|
||||
const R = 3
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const p = y * w + x
|
||||
const i = p * 4
|
||||
if (exterior[p]) {
|
||||
out[i] = out[i + 1] = out[i + 2] = 0
|
||||
continue
|
||||
}
|
||||
|
||||
let distance = R + 1
|
||||
for (let dy = -R; dy <= R; dy++) {
|
||||
for (let dx = -R; dx <= R; dx++) {
|
||||
const xx = x + dx
|
||||
const yy = y + dy
|
||||
if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue
|
||||
if (exterior[yy * w + xx]) {
|
||||
distance = Math.min(distance, Math.max(Math.abs(dx), Math.abs(dy)))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (distance > R) continue
|
||||
|
||||
const a = data[i + 3]
|
||||
let r = data[i]
|
||||
let g = data[i + 1]
|
||||
let b = data[i + 2]
|
||||
|
||||
if (a < 255) {
|
||||
const af = a / 255
|
||||
r = Math.min(255, Math.max(0, Math.round((r - 255 * (1 - af)) / af)))
|
||||
g = Math.min(255, Math.max(0, Math.round((g - 255 * (1 - af)) / af)))
|
||||
b = Math.min(255, Math.max(0, Math.round((b - 255 * (1 - af)) / af)))
|
||||
}
|
||||
|
||||
// Turn the complete exterior silhouette into a dark feather:
|
||||
// strongest on the outermost pixel, fading by the third pixel.
|
||||
// Because the mask is flood-filled from the canvas edge, internal labels stay untouched.
|
||||
const strength = distance === 1 ? 0.96 : distance === 2 ? 0.68 : 0.34
|
||||
out[i] = Math.round(r * (1 - strength) + DARK.r * strength)
|
||||
out[i + 1] = Math.round(g * (1 - strength) + DARK.g * strength)
|
||||
out[i + 2] = Math.round(b * (1 - strength) + DARK.b * strength)
|
||||
}
|
||||
}
|
||||
|
||||
return sharp(out, { raw: { width: w, height: h, channels: 4 } }).png().toBuffer()
|
||||
}
|
||||
|
||||
/** After WebP encode, force transparent RGB to black again. */
|
||||
async function neutralizeTransparentRgb(webpBuf) {
|
||||
const { data, info } = await sharp(webpBuf).ensureAlpha().raw().toBuffer({ resolveWithObject: true })
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] === 0) {
|
||||
data[i] = data[i + 1] = data[i + 2] = 0
|
||||
}
|
||||
}
|
||||
return sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } })
|
||||
.webp({ quality: 86, alphaQuality: 100 })
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
async function writeThumb(srcPath, name, width) {
|
||||
const dest = path.join(outThumb, `${name}.webp`)
|
||||
await sharp(srcPath)
|
||||
.resize(width, null, { withoutEnlargement: true })
|
||||
.blur(2)
|
||||
.webp({ quality: 50 })
|
||||
.toFile(dest)
|
||||
let pipe = sharp(srcPath).resize(width, null, { withoutEnlargement: true }).blur(2)
|
||||
|
||||
let buf = await pipe.webp({ quality: 50, alphaQuality: 100 }).toBuffer()
|
||||
if (name === 't30-bottle') buf = await neutralizeTransparentRgb(buf)
|
||||
await fs.writeFile(dest, buf)
|
||||
const stat = await fs.stat(dest)
|
||||
return { dest, kb: Math.round(stat.size / 1024 * 10) / 10 }
|
||||
return { dest, kb: Math.round((stat.size / 1024) * 10) / 10 }
|
||||
}
|
||||
|
||||
async function writeFull(srcPath, name, width) {
|
||||
const dest = path.join(outFull, `${name}.webp`)
|
||||
await sharp(srcPath)
|
||||
.resize(width, null, { withoutEnlargement: true })
|
||||
.webp({ quality: 82 })
|
||||
.toFile(dest)
|
||||
let pipe = sharp(srcPath).resize(width, null, {
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
|
||||
let buf = await pipe.webp({ quality: 86, alphaQuality: 100 }).toBuffer()
|
||||
if (name === 't30-bottle') buf = await neutralizeTransparentRgb(buf)
|
||||
await fs.writeFile(dest, buf)
|
||||
const stat = await fs.stat(dest)
|
||||
return { dest, kb: Math.round(stat.size / 1024 * 10) / 10 }
|
||||
return { dest, kb: Math.round((stat.size / 1024) * 10) / 10 }
|
||||
}
|
||||
|
||||
async function rewriteBottlePng(srcPath) {
|
||||
const fixed = await defringeBottle(srcPath)
|
||||
await fs.writeFile(srcPath, fixed)
|
||||
const stat = await fs.stat(srcPath)
|
||||
return Math.round((stat.size / 1024) * 10) / 10
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ensureDirs()
|
||||
|
||||
const bottleSrc = path.join(root, 'public/assets/t30-bottle.png')
|
||||
const pngKb = await rewriteBottlePng(bottleSrc)
|
||||
console.log(`t30-bottle.png rewritten (${pngKb} KB)`)
|
||||
|
||||
let thumbTotal = 0
|
||||
let fullTotal = 0
|
||||
|
||||
@@ -57,6 +187,7 @@ async function main() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Bottle PNG was already defringed once above; do not process it twice.
|
||||
const thumb = await writeThumb(srcPath, item.name, item.thumbW)
|
||||
const full = await writeFull(srcPath, item.name, item.fullW)
|
||||
thumbTotal += thumb.kb
|
||||
|
||||
@@ -9,6 +9,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const outDir = path.resolve(__dirname, '../dist')
|
||||
|
||||
const ROUTES = [
|
||||
'/h5',
|
||||
'/h5/brand',
|
||||
'/h5/poetry',
|
||||
'/h5/qingxiang',
|
||||
'/h5/haoke',
|
||||
'/h5/contact',
|
||||
'/brand',
|
||||
'/poetry',
|
||||
'/qingxiang',
|
||||
|
||||
@@ -12,29 +12,33 @@ import { PartnerPage } from './pages/Partner'
|
||||
import { TechDetailPage } from './pages/TechDetail'
|
||||
import { ContactPage } from './pages/Contact'
|
||||
import { DeepPage } from './pages/DeepPage'
|
||||
import { ThemeProvider } from './hooks/useTheme'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="brand" element={<BrandPage />} />
|
||||
<Route path="poetry" element={<PoetryPage />} />
|
||||
<Route path="qingxiang" element={<QingxiangPage />} />
|
||||
<Route path="haoke" element={<HaokePage />} />
|
||||
<Route path="system" element={<SystemPage />} />
|
||||
<Route path="tech" element={<TechPage />} />
|
||||
<Route path="subsidy" element={<SubsidyPage />} />
|
||||
<Route path="partner" element={<PartnerPage />} />
|
||||
<Route path="tech-detail" element={<TechDetailPage />} />
|
||||
<Route path="contact" element={<ContactPage />} />
|
||||
<Route path="culture/:slug" element={<DeepPage />} />
|
||||
<Route path="poetry/:slug" element={<DeepPage />} />
|
||||
<Route path="qingxiang/:slug" element={<DeepPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<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="h5/*" element={<HomePage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
border-top: 1px solid rgba(196, 163, 90, 0.15);
|
||||
}
|
||||
|
||||
.theme-tech .site-footer {
|
||||
[data-theme='technology'] .site-footer {
|
||||
background: #0c0a0e;
|
||||
border-top: 1px solid rgba(196, 163, 90, 0.28);
|
||||
}
|
||||
@@ -22,8 +22,9 @@
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
max-width: 11rem;
|
||||
object-fit: contain;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { footer, images } from '../content/site'
|
||||
import './Footer.css'
|
||||
|
||||
export function Footer() {
|
||||
const location = useLocation()
|
||||
const contactPath =
|
||||
location.pathname === '/h5' || location.pathname.startsWith('/h5/') ? '/h5/contact' : '/contact'
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="container footer-inner">
|
||||
<div className="footer-brand">
|
||||
<img src={images.logo} alt="" className="footer-logo" />
|
||||
<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>
|
||||
@@ -20,7 +23,7 @@ export function Footer() {
|
||||
<p>{footer.compliance}</p>
|
||||
<p>{footer.dataNote}</p>
|
||||
<p>
|
||||
<Link to="/contact">联系我们</Link>
|
||||
<Link to={contactPath}>联系我们</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useIsMobile } from '../hooks/useMobile'
|
||||
import { StageProvider, useRemoteMode, useStageMode } from '../hooks/useStageMode'
|
||||
import { MobileShell } from '../mobile/MobileShell'
|
||||
@@ -8,9 +9,11 @@ import { StageShell } from '../stage/StageShell'
|
||||
import './Layout.css'
|
||||
|
||||
function LayoutInner() {
|
||||
const location = useLocation()
|
||||
const isStage = useStageMode()
|
||||
const isRemote = useRemoteMode()
|
||||
const isMobile = useIsMobile(768)
|
||||
const isH5 = location.pathname === '/h5' || location.pathname.startsWith('/h5/')
|
||||
|
||||
useEffect(() => {
|
||||
const stageOn = isStage && !isMobile
|
||||
@@ -30,13 +33,13 @@ function LayoutInner() {
|
||||
return <RemoteShell />
|
||||
}
|
||||
|
||||
if (isStage && !isMobile) {
|
||||
if (isStage && !isMobile && !isH5) {
|
||||
return <StageShell />
|
||||
}
|
||||
|
||||
// PC 普通 + 手机:五页菜单切换,页内上下滚动(无下拉子菜单)
|
||||
// PC 普通 + 手机:五页菜单切换,顶栏支持下拉子菜单锚点跳转
|
||||
return (
|
||||
<MobileShell>
|
||||
<MobileShell forceMobile={isH5}>
|
||||
<MobileViews />
|
||||
</MobileShell>
|
||||
)
|
||||
|
||||
@@ -65,8 +65,9 @@
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
height: 2.6rem;
|
||||
width: auto;
|
||||
max-width: 7.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Nav() {
|
||||
<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>
|
||||
<span className="brand-text serif">展控</span>
|
||||
</Link>
|
||||
<nav className="nav-links stage-links" aria-label="展控导航">
|
||||
{stageNavItems.map((item) => (
|
||||
@@ -79,7 +79,6 @@ export function Nav() {
|
||||
<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="主导航">
|
||||
|
||||
@@ -8,10 +8,11 @@ export const deepImages = {
|
||||
assetUrl('/assets/brand/emb_p33_1.png'),
|
||||
assetUrl('/assets/brand/photo_p11_1.jpg'),
|
||||
assetUrl('/assets/brand/photo_p8_4.jpg'),
|
||||
assetUrl('/assets/brand/emb_p58_2.png'),
|
||||
],
|
||||
poetry: [
|
||||
assetUrl('/assets/poetry/emb_p33_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p49_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p35_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p59_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p67_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p25_4.png'),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const culturePages: DeepPageContent[] = [
|
||||
title: '杜康文化在中国酒文化中的地位',
|
||||
lead: '杜康,是中国秫酒的「酒宗」「酒祖」。杜康造酒,不仅肇始了华夏酒业的辉煌诞生,铸就了酒行业顶礼膜拜的精神图腾,而且创造了行业特征与地域色彩交相辉映的杜康文化。',
|
||||
layout: 'essay',
|
||||
images: [B[0]],
|
||||
images: [B[6]],
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
|
||||
@@ -272,10 +272,10 @@ export const contact = {
|
||||
eyebrow: '联系我们',
|
||||
title: '扫码咨询 · 对接项目负责人',
|
||||
lead: '现场如需进一步了解酒祖杜康清香、好客模式或城市合作,请扫码添加对接人,或致电联系。',
|
||||
role: '杜康好客 · 项目负责人',
|
||||
role: '杜康好客CEO',
|
||||
name: '吕丹',
|
||||
phone: '13073729990',
|
||||
wechatHint: '扫一扫上方二维码,添加项目负责人微信。',
|
||||
wechatHint: '扫一扫上方二维码,添加企业微信。',
|
||||
}
|
||||
|
||||
export const footer = {
|
||||
@@ -307,6 +307,7 @@ import { assetUrl } from '../lib/assetUrl'
|
||||
/** Pure visuals only (no PDF page screenshots with baked-in text). Paths resolve via VITE_OSS_BASE. */
|
||||
export const images = {
|
||||
logo: assetUrl('/assets/logo.png'),
|
||||
brandMarkH: assetUrl('/assets/brand-mark-h.png'),
|
||||
qr: assetUrl('/assets/qr-contact.png'),
|
||||
t30: assetUrl('/assets/t30-bottle.png'),
|
||||
brand: {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
|
||||
export type SiteTheme = 'nostalgic' | 'technology'
|
||||
|
||||
type ThemeContextValue = {
|
||||
theme: SiteTheme
|
||||
setTheme: (theme: SiteTheme) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'dukang-site-theme'
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
|
||||
function readTheme(): SiteTheme {
|
||||
if (typeof window === 'undefined') return 'nostalgic'
|
||||
return window.localStorage.getItem(STORAGE_KEY) === 'technology' ? 'technology' : 'nostalgic'
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<SiteTheme>(readTheme)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme
|
||||
document.documentElement.style.colorScheme = theme === 'technology' ? 'dark' : 'light'
|
||||
window.localStorage.setItem(STORAGE_KEY, theme)
|
||||
}, [theme])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme,
|
||||
toggleTheme: () => setTheme((current) => (current === 'nostalgic' ? 'technology' : 'nostalgic')),
|
||||
}),
|
||||
[theme],
|
||||
)
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) throw new Error('useTheme must be used within ThemeProvider')
|
||||
return context
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className = '' }: { className?: string }) {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const isTechnology = theme === 'technology'
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`theme-toggle ${className}`.trim()}
|
||||
onClick={toggleTheme}
|
||||
aria-label={`切换到${isTechnology ? '怀古风' : '科技风'}`}
|
||||
title={`当前:${isTechnology ? '科技风' : '怀古风'}`}
|
||||
>
|
||||
<span aria-hidden>{isTechnology ? '科' : '古'}</span>
|
||||
<span className="theme-toggle-label">{isTechnology ? '科技风' : '怀古风'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -24,9 +24,9 @@
|
||||
padding-bottom: 0.35rem;
|
||||
box-sizing: border-box;
|
||||
z-index: 1000;
|
||||
background: rgba(247, 242, 234, 0.96);
|
||||
background: var(--surface-header);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(166, 28, 32, 0.12);
|
||||
border-bottom: 1px solid var(--line);
|
||||
transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,19 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.m-theme-toggle {
|
||||
position: absolute;
|
||||
top: calc(0.65rem + env(safe-area-inset-top, 0px));
|
||||
right: 0.7rem;
|
||||
min-height: 1.8rem;
|
||||
padding: 0.18rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.m-theme-toggle .theme-toggle-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -48,8 +61,9 @@
|
||||
}
|
||||
|
||||
.m-logo {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
height: 2.2rem;
|
||||
width: auto;
|
||||
max-width: 6.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -85,12 +99,82 @@
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.mobile-shell.is-h5 .m-nav-link {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.m-nav-link.is-active {
|
||||
color: var(--brand, #a61c20);
|
||||
font-weight: 600;
|
||||
background: rgba(166, 28, 32, 0.08);
|
||||
}
|
||||
|
||||
.m-nav-trigger {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.m-nav-chevron {
|
||||
width: 0.35rem;
|
||||
height: 0.35rem;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(45deg) translateY(-0.05rem);
|
||||
opacity: 0.55;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.m-nav-group.is-open .m-nav-chevron,
|
||||
.m-nav-group.is-mobile-open .m-nav-chevron {
|
||||
transform: rotate(225deg) translateY(0.05rem);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.m-nav-group {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.m-nav-dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m-mobile-subnav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0 0.15rem;
|
||||
border-top: 1px solid rgba(166, 28, 32, 0.1);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.m-header.has-mobile-subnav {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.m-mobile-subnav-item {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid rgba(166, 28, 32, 0.15);
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
font: inherit;
|
||||
font-size: 0.72rem;
|
||||
color: var(--ink-soft, #5c534c);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-mobile-subnav-item:active {
|
||||
background: rgba(166, 28, 32, 0.08);
|
||||
color: var(--brand, #a61c20);
|
||||
}
|
||||
|
||||
.m-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -105,23 +189,17 @@
|
||||
.is-desktop-longpage .m-header {
|
||||
min-height: 4.5rem;
|
||||
padding: 0;
|
||||
background: rgba(247, 242, 234, 0.88);
|
||||
background: var(--surface-header);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-scrolled {
|
||||
background: rgba(247, 242, 234, 0.96);
|
||||
border-bottom-color: rgba(166, 28, 32, 0.14);
|
||||
background: var(--surface-header);
|
||||
border-bottom-color: var(--line);
|
||||
box-shadow: 0 0.65rem 1.75rem rgba(83, 42, 18, 0.06);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech {
|
||||
background: rgba(12, 11, 16, 0.9);
|
||||
border-bottom-color: rgba(201, 154, 69, 0.22);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech.is-scrolled {
|
||||
background: rgba(12, 11, 16, 0.96);
|
||||
[data-theme='technology'] .is-desktop-longpage .m-header.is-scrolled {
|
||||
box-shadow: 0 0.65rem 1.75rem rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
@@ -130,7 +208,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
width: min(100% - 3rem, 77.5rem);
|
||||
width: var(--max);
|
||||
margin: 0 auto;
|
||||
min-height: 4.5rem;
|
||||
padding: 0.65rem 0;
|
||||
@@ -141,8 +219,9 @@
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-logo {
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
height: 2.6rem;
|
||||
width: auto;
|
||||
max-width: 7.5rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-title {
|
||||
@@ -151,7 +230,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech .m-title {
|
||||
[data-theme='technology'] .is-desktop-longpage .m-title {
|
||||
color: #fff4df;
|
||||
}
|
||||
|
||||
@@ -163,6 +242,18 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-theme-toggle {
|
||||
position: static;
|
||||
flex-shrink: 0;
|
||||
min-height: 2.15rem;
|
||||
padding: 0.3rem 0.65rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-theme-toggle .theme-toggle-label {
|
||||
display: inline;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-link {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
@@ -172,7 +263,7 @@
|
||||
background: transparent !important;
|
||||
font-size: 0.92rem;
|
||||
letter-spacing: 0.14em;
|
||||
color: #6b5344;
|
||||
color: var(--ink-soft);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -195,20 +286,106 @@
|
||||
background: var(--brand, #a61c20);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech .m-nav-link {
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-link {
|
||||
color: rgba(255, 244, 223, 0.7);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech .m-nav-link:hover,
|
||||
.is-desktop-longpage .m-header.is-tech .m-nav-link.is-active {
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-link:hover,
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-link.is-active {
|
||||
color: var(--gold-light, #f1d99a);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-header.is-tech .m-nav-link.is-active::after {
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-link.is-active::after {
|
||||
background: var(--gold-light, #f1d99a);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-main {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-group {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-dropdown {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(0.35rem) scale(0.98);
|
||||
min-width: 11rem;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 0.65rem 0.5rem;
|
||||
background: var(--surface-header);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.85rem 2rem rgba(26, 21, 18, 0.12);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-dropdown.is-wide {
|
||||
min-width: 18rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-group.is-open .m-nav-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: translateX(-50%) translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-drop-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-drop-list.is-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.05rem 0.25rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-drop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 2.1rem;
|
||||
padding: 0 0.65rem;
|
||||
border: none;
|
||||
border-radius: 0.15rem;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
color: var(--ink-soft, #5c534c);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-nav-drop-item:hover {
|
||||
background: rgba(166, 28, 32, 0.06);
|
||||
color: var(--brand, #a61c20);
|
||||
}
|
||||
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-dropdown {
|
||||
background: rgba(18, 16, 20, 0.98);
|
||||
border-color: rgba(201, 154, 69, 0.22);
|
||||
box-shadow: 0 0.85rem 2rem rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-drop-item {
|
||||
color: rgba(255, 244, 223, 0.72);
|
||||
}
|
||||
|
||||
[data-theme='technology'] .is-desktop-longpage .m-nav-drop-item:hover {
|
||||
background: rgba(201, 154, 69, 0.12);
|
||||
color: var(--gold-light, #f1d99a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,16 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { Link, Outlet } from 'react-router-dom'
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom'
|
||||
import { images } from '../content/site'
|
||||
import { useIsMobile } from '../hooks/useMobile'
|
||||
import { ThemeToggle } from '../hooks/useTheme'
|
||||
import { getSubnavForPanel, ordinarySubnav } from './ordinaryNav'
|
||||
import './MobileShell.css'
|
||||
|
||||
export const longNav = [
|
||||
@@ -22,9 +26,14 @@ export const longNav = [
|
||||
|
||||
export type NavId = (typeof longNav)[number]['id']
|
||||
|
||||
type GoSectionOpts = {
|
||||
force?: boolean
|
||||
anchor?: string
|
||||
}
|
||||
|
||||
type OrdinaryNavContextValue = {
|
||||
activeId: NavId
|
||||
goSection: (id: NavId, opts?: { force?: boolean }) => void
|
||||
goSection: (id: NavId, opts?: GoSectionOpts) => void
|
||||
}
|
||||
|
||||
const OrdinaryNavContext = createContext<OrdinaryNavContextValue | null>(null)
|
||||
@@ -41,29 +50,69 @@ function isNavId(id: string): id is NavId {
|
||||
return longNav.some((n) => n.id === id)
|
||||
}
|
||||
|
||||
function scrollToAnchor(anchor: string) {
|
||||
const scrollEl = document.querySelector<HTMLElement>('.m-panel.is-active .m-panel-scroll')
|
||||
if (!scrollEl) return
|
||||
const target = scrollEl.querySelector<HTMLElement>(`#${CSS.escape(anchor)}`)
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
} else {
|
||||
scrollEl.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
function resetActivePanelScroll() {
|
||||
const scrollEl = document.querySelector<HTMLElement>('.m-panel.is-active .m-panel-scroll')
|
||||
if (scrollEl) scrollEl.scrollTop = 0
|
||||
}
|
||||
|
||||
export function MobileShell({ children }: { children?: ReactNode }) {
|
||||
const isMobile = useIsMobile(768)
|
||||
export function MobileShell({ children, forceMobile = false }: { children?: ReactNode; forceMobile?: boolean }) {
|
||||
const responsiveMobile = useIsMobile(768)
|
||||
const isMobile = forceMobile || responsiveMobile
|
||||
const location = useLocation()
|
||||
const routeBase = location.pathname === '/h5' || location.pathname.startsWith('/h5/') ? '/h5' : ''
|
||||
const [activeId, setActiveId] = useState<NavId>(() => {
|
||||
const hash = typeof window !== 'undefined' ? window.location.hash.replace('#', '') : ''
|
||||
return hash && isNavId(hash) ? hash : 'm-home'
|
||||
})
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [openGroup, setOpenGroup] = useState<NavId | null>(null)
|
||||
const [mobileSubnav, setMobileSubnav] = useState<NavId | null>(null)
|
||||
const pendingScrollRef = useRef<'top' | string | null>(null)
|
||||
const headerRef = useRef<HTMLElement>(null)
|
||||
const closeTimerRef = useRef<number | null>(null)
|
||||
|
||||
const goSection = useCallback((id: NavId, opts?: { force?: boolean }) => {
|
||||
const cancelClose = useCallback(() => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scheduleClose = useCallback(() => {
|
||||
cancelClose()
|
||||
closeTimerRef.current = window.setTimeout(() => setOpenGroup(null), 180)
|
||||
}, [cancelClose])
|
||||
|
||||
const goSection = useCallback((id: NavId, opts?: GoSectionOpts) => {
|
||||
setActiveId((prev) => {
|
||||
if (prev !== id || opts?.force) {
|
||||
queueMicrotask(() => {
|
||||
setScrolled(false)
|
||||
resetActivePanelScroll()
|
||||
})
|
||||
if (prev === id && opts?.anchor) {
|
||||
queueMicrotask(() => scrollToAnchor(opts.anchor!))
|
||||
return prev
|
||||
}
|
||||
|
||||
if (opts?.anchor) {
|
||||
pendingScrollRef.current = opts.anchor
|
||||
} else if (prev !== id || opts?.force) {
|
||||
pendingScrollRef.current = 'top'
|
||||
}
|
||||
|
||||
return id
|
||||
})
|
||||
|
||||
setOpenGroup(null)
|
||||
setMobileSubnav(null)
|
||||
|
||||
const nextHash = `#${id}`
|
||||
if (window.location.hash !== nextHash) {
|
||||
window.history.replaceState(
|
||||
@@ -74,6 +123,21 @@ export function MobileShell({ children }: { children?: ReactNode }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const pending = pendingScrollRef.current
|
||||
if (pending === null) return
|
||||
pendingScrollRef.current = null
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (pending === 'top') {
|
||||
resetActivePanelScroll()
|
||||
setScrolled(false)
|
||||
} else {
|
||||
scrollToAnchor(pending)
|
||||
}
|
||||
})
|
||||
}, [activeId])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollEl = document.querySelector<HTMLElement>('.m-panel.is-active .m-panel-scroll')
|
||||
if (!scrollEl) return
|
||||
@@ -84,38 +148,168 @@ export function MobileShell({ children }: { children?: ReactNode }) {
|
||||
return () => scrollEl.removeEventListener('scroll', onScroll)
|
||||
}, [activeId])
|
||||
|
||||
const techTheme = activeId === 'm-haoke'
|
||||
useEffect(() => {
|
||||
if (!mobileSubnav && !openGroup) return
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (!headerRef.current?.contains(e.target as Node)) {
|
||||
setMobileSubnav(null)
|
||||
setOpenGroup(null)
|
||||
}
|
||||
}
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setMobileSubnav(null)
|
||||
setOpenGroup(null)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pointerdown', onPointerDown)
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', onPointerDown)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [mobileSubnav, openGroup])
|
||||
|
||||
useEffect(() => () => cancelClose(), [cancelClose])
|
||||
const navValue = useMemo(() => ({ activeId, goSection }), [activeId, goSection])
|
||||
|
||||
const subnavMap = useMemo(
|
||||
() => Object.fromEntries(ordinarySubnav.map((m) => [m.id, m.children])) as Record<NavId, typeof ordinarySubnav[number]['children']>,
|
||||
[],
|
||||
)
|
||||
|
||||
const handleNavTitleClick = (id: NavId, hasChildren: boolean) => {
|
||||
if (isMobile && hasChildren) {
|
||||
setMobileSubnav((prev) => (prev === id ? null : id))
|
||||
if (activeId !== id) {
|
||||
goSection(id, { force: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!isMobile && hasChildren) {
|
||||
cancelClose()
|
||||
if (activeId !== id) {
|
||||
goSection(id, { force: true })
|
||||
setOpenGroup(id)
|
||||
} else {
|
||||
setOpenGroup((prev) => (prev === id ? null : id))
|
||||
}
|
||||
return
|
||||
}
|
||||
goSection(id, { force: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<OrdinaryNavContext.Provider value={navValue}>
|
||||
<div className={`mobile-shell ${!isMobile ? 'is-desktop-longpage' : ''}`}>
|
||||
<div className={`mobile-shell ${!isMobile ? 'is-desktop-longpage' : ''} ${forceMobile ? 'is-h5' : ''}`}>
|
||||
<header
|
||||
className={`m-header ${scrolled ? 'is-scrolled' : ''} ${techTheme && !isMobile ? 'is-tech' : ''}`}
|
||||
ref={headerRef}
|
||||
className={`m-header ${scrolled ? 'is-scrolled' : ''} ${mobileSubnav ? 'has-mobile-subnav' : ''}`}
|
||||
>
|
||||
<div className="m-header-inner">
|
||||
<Link
|
||||
to="/#m-home"
|
||||
to={`${routeBase}/#m-home`}
|
||||
className="m-brand"
|
||||
onClick={() => goSection('m-home', { force: true })}
|
||||
>
|
||||
<img src={images.logo} alt="杜康好客" className="m-logo" />
|
||||
<span className="m-title">杜康好客</span>
|
||||
</Link>
|
||||
|
||||
<nav className="m-header-nav" aria-label={isMobile ? '手机导航' : '主导航'}>
|
||||
{longNav.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={`/#${item.id}`}
|
||||
className={`m-nav-link ${activeId === item.id ? 'is-active' : ''}`}
|
||||
onClick={() => goSection(item.id, { force: true })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
{longNav.map((item) => {
|
||||
const children = subnavMap[item.id] ?? []
|
||||
const hasChildren = children.length > 0
|
||||
const isWide = item.id === 'm-culture' || item.id === 'm-qingxiang'
|
||||
|
||||
if (!hasChildren) {
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={`${routeBase}/#${item.id}`}
|
||||
className={`m-nav-link ${activeId === item.id ? 'is-active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
goSection(item.id, { force: true })
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`m-nav-group ${activeId === item.id ? 'is-current' : ''} ${openGroup === item.id ? 'is-open' : ''} ${mobileSubnav === item.id ? 'is-mobile-open' : ''}`}
|
||||
onMouseEnter={() => {
|
||||
if (!isMobile) {
|
||||
cancelClose()
|
||||
setOpenGroup(item.id)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => !isMobile && scheduleClose()}
|
||||
onFocusCapture={() => {
|
||||
if (!isMobile) {
|
||||
cancelClose()
|
||||
setOpenGroup(item.id)
|
||||
}
|
||||
}}
|
||||
onBlurCapture={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) scheduleClose()
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`m-nav-link m-nav-trigger ${activeId === item.id ? 'is-active' : ''}`}
|
||||
aria-expanded={openGroup === item.id || mobileSubnav === item.id}
|
||||
onClick={() => handleNavTitleClick(item.id, hasChildren)}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<span className="m-nav-chevron" aria-hidden />
|
||||
</button>
|
||||
|
||||
<div className={`m-nav-dropdown ${isWide ? 'is-wide' : ''}`} role="menu">
|
||||
<ul className={`m-nav-drop-list ${isWide ? 'is-cols' : ''}`}>
|
||||
{children.map((child, idx) => (
|
||||
<li key={`${item.id}-${child.anchor ?? child.label}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="m-nav-drop-item"
|
||||
role="menuitem"
|
||||
style={{ '--i': idx } as CSSProperties}
|
||||
onClick={() =>
|
||||
goSection(item.id, { anchor: child.anchor, force: true })
|
||||
}
|
||||
>
|
||||
{child.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<ThemeToggle className="m-theme-toggle" />
|
||||
</div>
|
||||
|
||||
{isMobile && mobileSubnav ? (
|
||||
<div className="m-mobile-subnav" role="menu">
|
||||
{getSubnavForPanel(mobileSubnav).map((child) => (
|
||||
<button
|
||||
key={`mobile-${child.anchor ?? child.label}`}
|
||||
type="button"
|
||||
className="m-mobile-subnav-item"
|
||||
role="menuitem"
|
||||
onClick={() => goSection(mobileSubnav, { anchor: child.anchor, force: true })}
|
||||
>
|
||||
{child.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<main className="m-main">{children || <Outlet />}</main>
|
||||
|
||||
@@ -38,6 +38,20 @@
|
||||
padding-bottom: calc(2.25rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
#m-home .m-panel-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#m-home .m-panel-scroll > .m-view-container {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
#m-home .m-panel-scroll > .site-footer {
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.m-panel-tech {
|
||||
background: var(--tech-bg, #121014);
|
||||
color: #f3ece4;
|
||||
@@ -281,6 +295,19 @@
|
||||
padding: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.m-hero-heading,
|
||||
.m-hero-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.m-hero-visual {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.m-hero-sub {
|
||||
font-size: 0.88rem;
|
||||
color: var(--gold, #c4a35a);
|
||||
@@ -296,6 +323,18 @@
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.m-hero-tagline {
|
||||
margin: 0;
|
||||
color: var(--gold);
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.m-hero-desc {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.m-hero-img {
|
||||
width: min(12rem, 50vw);
|
||||
height: auto;
|
||||
@@ -303,6 +342,15 @@
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.m-qingxiang-scene {
|
||||
width: min(100%, 42rem);
|
||||
max-height: 24rem;
|
||||
margin: 0 auto;
|
||||
border-radius: 0.5rem;
|
||||
object-fit: cover;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
/* Specific Cards & Rings */
|
||||
.m-rings-row {
|
||||
display: flex;
|
||||
@@ -331,6 +379,9 @@
|
||||
}
|
||||
|
||||
.m-poem-box {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, rgba(166, 28, 32, 0.06), rgba(196, 163, 90, 0.08));
|
||||
border: 1px solid rgba(196, 163, 90, 0.3);
|
||||
padding: 1.25rem 1rem;
|
||||
@@ -459,10 +510,15 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.m-view-container > :not(.m-card) {
|
||||
.m-view-container > * {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.m-view-container > .m-card {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.m-page-title {
|
||||
font-size: clamp(1.75rem, 2.5vw, 2.35rem);
|
||||
}
|
||||
@@ -496,3 +552,295 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Unified theme and explicit content layouts */
|
||||
.m-panel,
|
||||
.m-panel-tech,
|
||||
.m-section-tech {
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.m-panel-tech .m-join-label,
|
||||
.m-panel-tech .m-eyebrow,
|
||||
.m-panel-tech .m-page-title,
|
||||
.m-panel-tech .m-card h3,
|
||||
.m-panel-tech .m-big-num,
|
||||
.m-panel-tech .m-closing,
|
||||
.m-panel-tech .m-deep-h,
|
||||
.m-section-tech .m-eyebrow,
|
||||
.m-section-tech .m-page-title,
|
||||
.m-section-tech .m-card h3,
|
||||
.m-section-tech .m-big-num,
|
||||
.m-section-tech .m-closing {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.m-panel-tech .m-lead,
|
||||
.m-panel-tech .m-card p,
|
||||
.m-panel-tech .m-poem-source,
|
||||
.m-panel-tech .m-deep-p,
|
||||
.m-panel-tech .m-deep-list,
|
||||
.m-section-tech .m-lead,
|
||||
.m-section-tech .m-card p,
|
||||
.m-section-tech .m-poem-source {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.m-card,
|
||||
.m-panel-tech .m-card,
|
||||
.m-section-tech .m-card {
|
||||
color: var(--ink);
|
||||
background: var(--surface-card);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
[data-theme='technology'] .m-eyebrow,
|
||||
[data-theme='technology'] .m-page-title,
|
||||
[data-theme='technology'] .m-card h3,
|
||||
[data-theme='technology'] .m-big-num,
|
||||
[data-theme='technology'] .m-deep-h {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
[data-theme='technology'] .m-lead,
|
||||
[data-theme='technology'] .m-card p,
|
||||
[data-theme='technology'] .m-deep-p,
|
||||
[data-theme='technology'] .m-deep-list {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.m-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.m-card-grid > .m-card--banner {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.m-card--subsidy-hero {
|
||||
text-align: center;
|
||||
padding: 1.5rem 1.1rem;
|
||||
}
|
||||
|
||||
.m-subsidy-label {
|
||||
font-size: 0.88rem;
|
||||
color: var(--gold, #c4a35a);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m-subsidy-note {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ink-soft, #5c534c);
|
||||
}
|
||||
|
||||
.theme-tech .m-subsidy-note {
|
||||
color: rgba(243, 236, 228, 0.78);
|
||||
}
|
||||
|
||||
.m-tasting-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem 1.25rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.m-tasting-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.m-tasting-item strong {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.m-tasting-item span {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.m-card-highlight {
|
||||
background: var(--brand-soft) !important;
|
||||
border-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.m-card-role {
|
||||
color: var(--ink) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-card-loop {
|
||||
text-align: center;
|
||||
background: var(--brand-soft) !important;
|
||||
}
|
||||
|
||||
.m-hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.m-btn-stage {
|
||||
border: 1px solid var(--gold);
|
||||
color: var(--ink);
|
||||
background: linear-gradient(135deg, rgba(196, 163, 90, 0.2), var(--brand-soft));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.m-join-label {
|
||||
padding-inline: max(1.5rem, calc((100vw - var(--content-max)) / 2));
|
||||
}
|
||||
|
||||
.m-view-container {
|
||||
max-width: var(--content-max);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-lead {
|
||||
max-width: none;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.m-card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.m-card-grid.m-card-grid--subsidy {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.m-card-grid.m-card-grid--four {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.m-card-grid.m-card-grid--four > .m-card--tasting {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.is-desktop-longpage .m-tasting-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-deep-blocks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.m-qr-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(18rem, 1fr);
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
width: min(100%, 48rem);
|
||||
padding: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.m-card-grid:not(.m-card-grid--brand):not(.m-card-grid--poetry):not(.m-card-grid--four) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.m-card-grid.m-card-grid--subsidy {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.m-card-grid.m-card-grid--four {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-shell.is-h5 .m-panel-scroll {
|
||||
padding-top: calc(5.6rem + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
.mobile-shell.is-h5 .m-view-container {
|
||||
max-width: 42rem;
|
||||
padding: 1.25rem 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.mobile-shell.is-h5 .m-card-grid,
|
||||
.mobile-shell.is-h5 .m-card-grid--brand,
|
||||
.mobile-shell.is-h5 .m-card-grid--poetry,
|
||||
.mobile-shell.is-h5 .m-card-grid--four,
|
||||
.mobile-shell.is-h5 .m-card-grid--subsidy {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mobile-shell.is-h5 .m-qr-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.is-desktop-longpage #m-home .m-view-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.08fr) minmax(14rem, 0.92fr);
|
||||
grid-template-areas:
|
||||
'heading visual'
|
||||
'content visual';
|
||||
column-gap: clamp(2.5rem, 7vw, 7rem);
|
||||
row-gap: 1.5rem;
|
||||
width: 100%;
|
||||
padding: clamp(1rem, 3vh, 2.5rem) clamp(1rem, 3vw, 3rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-heading {
|
||||
grid-area: heading;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-content {
|
||||
grid-area: content;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-visual {
|
||||
grid-area: visual;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-title {
|
||||
font-size: clamp(2.8rem, 4.6vw, 4.5rem);
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-tagline {
|
||||
font-size: clamp(1.05rem, 1.35vw, 1.35rem);
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-img {
|
||||
width: clamp(15rem, 22vw, 23rem);
|
||||
max-width: none;
|
||||
max-height: 58vh;
|
||||
margin: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-desc {
|
||||
max-width: 42rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.is-desktop-longpage #m-home .m-hero-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { Footer } from '../components/Footer'
|
||||
import { MobileDeepSection, pathToSectionId } from './MobileDeepSection'
|
||||
import { useOrdinaryNav, type NavId } from './MobileShell'
|
||||
import { pathToAnchorId } from './ordinaryNav'
|
||||
import './MobileViews.css'
|
||||
|
||||
const cultureDeep = allDeepPages.filter((p) => p.group === 'culture')
|
||||
@@ -52,6 +53,28 @@ function isNavId(id: string): id is NavId {
|
||||
return id === 'm-home' || id === 'm-culture' || id === 'm-qingxiang' || id === 'm-haoke' || id === 'm-contact'
|
||||
}
|
||||
|
||||
function normalizePath(pathname: string) {
|
||||
if (pathname === '/h5') return '/'
|
||||
return pathname.startsWith('/h5/') ? pathname.slice(3) || '/' : pathname
|
||||
}
|
||||
|
||||
function anchorForPath(pathname: string) {
|
||||
if (allDeepPages.some((page) => page.path === pathname)) return pathToAnchorId(pathname)
|
||||
const anchors: Record<string, string> = {
|
||||
'/brand': 'm-brand',
|
||||
'/poetry': 'm-poetry',
|
||||
'/qingxiang': 'm-qingxiang-main',
|
||||
'/haoke': 'm-haoke-main',
|
||||
'/system': 'm-system',
|
||||
'/tech': 'm-tech',
|
||||
'/subsidy': 'm-subsidy',
|
||||
'/partner': 'm-partner',
|
||||
'/tech-detail': 'm-tech-detail',
|
||||
'/contact': 'm-contact-main',
|
||||
}
|
||||
return anchors[pathname]
|
||||
}
|
||||
|
||||
function panelClass(id: NavId, activeId: NavId, extra = '') {
|
||||
return `m-panel${extra ? ` ${extra}` : ''}${activeId === id ? ' is-active' : ''}`
|
||||
}
|
||||
@@ -62,8 +85,9 @@ export function MobileViews() {
|
||||
|
||||
useEffect(() => {
|
||||
const fromHash = location.hash.replace('#', '')
|
||||
const nextId = (isNavId(fromHash) ? fromHash : PATH_TO_SECTION[location.pathname]) || 'm-home'
|
||||
goSection(nextId)
|
||||
const pathname = normalizePath(location.pathname)
|
||||
const nextId = (isNavId(fromHash) ? fromHash : PATH_TO_SECTION[pathname]) || 'm-home'
|
||||
goSection(nextId, isNavId(fromHash) ? undefined : { anchor: anchorForPath(pathname) })
|
||||
}, [location.pathname, location.hash, goSection])
|
||||
|
||||
return (
|
||||
@@ -78,13 +102,15 @@ export function MobileViews() {
|
||||
<section id="m-culture" className={panelClass('m-culture', activeId)} hidden={activeId !== 'm-culture'}>
|
||||
<div className="m-panel-scroll">
|
||||
<p className="m-join-label">酒祖文化</p>
|
||||
<MobileBrandView />
|
||||
<div id="m-brand" className="m-panel-block m-section">
|
||||
<MobileBrandView />
|
||||
</div>
|
||||
{cultureDeep.map((page) => (
|
||||
<div key={page.path} id={pathToSectionId(page.path)} className="m-panel-block">
|
||||
<MobileDeepSection page={page} />
|
||||
</div>
|
||||
))}
|
||||
<div id="m-poetry" className="m-panel-block">
|
||||
<div id="m-poetry" className="m-panel-block m-section">
|
||||
<MobilePoetryView />
|
||||
</div>
|
||||
{poetryDeep.map((page) => (
|
||||
@@ -99,7 +125,9 @@ export function MobileViews() {
|
||||
<section id="m-qingxiang" className={panelClass('m-qingxiang', activeId)} hidden={activeId !== 'm-qingxiang'}>
|
||||
<div className="m-panel-scroll">
|
||||
<p className="m-join-label">清香臻品</p>
|
||||
<MobileQingxiangView />
|
||||
<div id="m-qingxiang-main" className="m-panel-block m-section">
|
||||
<MobileQingxiangView />
|
||||
</div>
|
||||
{qingxiangDeep.map((page) => (
|
||||
<div key={page.path} id={pathToSectionId(page.path)} className="m-panel-block">
|
||||
<MobileDeepSection page={page} />
|
||||
@@ -116,8 +144,10 @@ export function MobileViews() {
|
||||
>
|
||||
<div className="m-panel-scroll">
|
||||
<p className="m-join-label">好客科技</p>
|
||||
<MobileHaokeView />
|
||||
<div id="m-system" className="m-panel-block">
|
||||
<div id="m-haoke-main" className="m-panel-block m-section">
|
||||
<MobileHaokeView />
|
||||
</div>
|
||||
<div id="m-system" className="m-panel-block m-section">
|
||||
<MobileSystemView />
|
||||
</div>
|
||||
<div id="m-tech" className="m-panel-block m-section-tech">
|
||||
@@ -136,10 +166,12 @@ export function MobileViews() {
|
||||
<section id="m-contact" className={panelClass('m-contact', activeId)} hidden={activeId !== 'm-contact'}>
|
||||
<div className="m-panel-scroll">
|
||||
<p className="m-join-label">合作联系</p>
|
||||
<div id="m-partner" className="m-panel-block">
|
||||
<div id="m-partner" className="m-panel-block m-section">
|
||||
<MobilePartnerView />
|
||||
</div>
|
||||
<MobileContactView />
|
||||
<div id="m-contact-main" className="m-panel-block m-section">
|
||||
<MobileContactView />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</section>
|
||||
@@ -149,26 +181,35 @@ export function MobileViews() {
|
||||
|
||||
function MobileHomeView() {
|
||||
const { goSection } = useOrdinaryNav()
|
||||
const location = useLocation()
|
||||
const isH5 = location.pathname === '/h5' || location.pathname.startsWith('/h5/')
|
||||
|
||||
return (
|
||||
<div className="m-view-container">
|
||||
<div className="m-hero">
|
||||
<div className="m-hero-sub">{hero.subBrand}</div>
|
||||
<h1 className="m-hero-title">{hero.brand}</h1>
|
||||
<p style={{ fontSize: '1.05rem', color: 'var(--gold)', fontFamily: 'Noto Serif SC', margin: 0 }}>
|
||||
{hero.title}
|
||||
</p>
|
||||
<img src={images.t30} alt="酒祖杜康 T30" className="m-hero-img" />
|
||||
<p className="m-lead" style={{ textAlign: 'center' }}>
|
||||
{hero.subtitle}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
|
||||
<button type="button" className="m-btn m-btn-primary" onClick={() => goSection('m-culture')}>
|
||||
{hero.primaryCta.label}
|
||||
</button>
|
||||
<button type="button" className="m-btn m-btn-outline" onClick={() => goSection('m-contact')}>
|
||||
{hero.secondaryCta.label}
|
||||
</button>
|
||||
<div className="m-hero-heading">
|
||||
<div className="m-hero-sub">{hero.subBrand}</div>
|
||||
<h1 className="m-hero-title">{hero.brand}</h1>
|
||||
<p className="m-hero-tagline">{hero.title}</p>
|
||||
</div>
|
||||
<div className="m-hero-visual">
|
||||
<img src={images.t30} alt="酒祖杜康 T30" className="m-hero-img" />
|
||||
</div>
|
||||
<div className="m-hero-content">
|
||||
<p className="m-lead m-hero-desc">{hero.subtitle}</p>
|
||||
<div className="m-hero-actions">
|
||||
<button type="button" className="m-btn m-btn-primary" onClick={() => goSection('m-culture')}>
|
||||
{hero.primaryCta.label}
|
||||
</button>
|
||||
<button type="button" className="m-btn m-btn-outline" onClick={() => goSection('m-contact')}>
|
||||
{hero.secondaryCta.label}
|
||||
</button>
|
||||
{!isH5 ? (
|
||||
<a href="/?mode=stage" className="m-btn m-btn-stage">
|
||||
进入大屏
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +225,7 @@ function MobileBrandView() {
|
||||
<p className="m-lead">{brand.lead}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid m-card-grid--brand">
|
||||
{brand.cards.map((card, idx) => (
|
||||
<div key={card.title} className="m-card">
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--gold)', fontWeight: 600 }}>0{idx + 1}</div>
|
||||
@@ -247,7 +288,7 @@ function MobilePoetryView() {
|
||||
<div className="m-poem-source">—— {poetry.mainPoem.source}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid m-card-grid--poetry">
|
||||
{poetry.poems.map((p) => (
|
||||
<div key={p.source} className="m-card">
|
||||
<p style={{ fontFamily: 'Noto Serif SC', fontSize: '1rem', color: 'var(--ink)' }}>“{p.quote}”</p>
|
||||
@@ -271,7 +312,11 @@ function MobileQingxiangView() {
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<img src={images.t30} alt="酒祖杜康清香 T30" style={{ width: '9rem', margin: '0 auto' }} />
|
||||
<img
|
||||
src={images.qingxiang.storefront}
|
||||
alt="酒祖杜康清香宴饮场景"
|
||||
className="m-qingxiang-scene"
|
||||
/>
|
||||
<div className="m-rings-row">
|
||||
{qingxiang.styleWords.map((word) => (
|
||||
<div key={word} className="m-ring-item">
|
||||
@@ -281,24 +326,23 @@ function MobileQingxiangView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid m-card-grid--four">
|
||||
{qingxiang.positions.map((pos) => (
|
||||
<div key={pos.title} className="m-card">
|
||||
<h3>{pos.title}</h3>
|
||||
<p>{pos.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="m-card">
|
||||
<h3>品鉴仪式 · 抿咂呵</h3>
|
||||
<div style={{ display: 'grid', gap: '0.5rem', marginTop: '0.3rem' }}>
|
||||
{qingxiang.tasting.map((t) => (
|
||||
<div key={t.title} style={{ fontSize: '0.88rem' }}>
|
||||
<strong style={{ color: 'var(--brand)' }}>{t.title}:</strong>
|
||||
<span style={{ color: 'var(--ink-soft)' }}>{t.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="m-card m-card--tasting">
|
||||
<h3>品鉴仪式 · 抿咂呵</h3>
|
||||
<div className="m-tasting-row">
|
||||
{qingxiang.tasting.map((t) => (
|
||||
<div key={t.title} className="m-tasting-item">
|
||||
<strong>{t.title}:</strong>
|
||||
<span>{t.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -316,14 +360,13 @@ function MobileHaokeView() {
|
||||
<p className="m-lead">{haoke.lead}</p>
|
||||
</div>
|
||||
|
||||
<div className="m-card" style={{ background: 'rgba(166, 28, 32, 0.08)', borderColor: 'var(--brand)' }}>
|
||||
<h3 style={{ fontSize: '1.15rem' }}>核心商业机制</h3>
|
||||
<p style={{ fontWeight: 600, color: 'var(--brand)', fontSize: '1.02rem', lineHeight: 1.5 }}>
|
||||
{haoke.oneLiner}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid">
|
||||
<div className="m-card m-card-highlight m-card--banner">
|
||||
<h3 style={{ fontSize: '1.15rem' }}>核心商业机制</h3>
|
||||
<p style={{ fontWeight: 600, color: 'var(--brand)', fontSize: '1.02rem', lineHeight: 1.5 }}>
|
||||
{haoke.oneLiner}
|
||||
</p>
|
||||
</div>
|
||||
{haoke.points.map((pt) => (
|
||||
<div key={pt.title} className="m-card">
|
||||
<h3>{pt.title}</h3>
|
||||
@@ -346,11 +389,11 @@ function MobileSystemView() {
|
||||
<p className="m-lead">{system.lead}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid">
|
||||
{system.ends.map((end) => (
|
||||
<div key={end.title} className="m-card">
|
||||
<h3>{end.title}</h3>
|
||||
<p style={{ fontWeight: 500, color: 'var(--ink)' }}>{end.role}</p>
|
||||
<p className="m-card-role">{end.role}</p>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.82rem',
|
||||
@@ -363,11 +406,10 @@ function MobileSystemView() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="m-card" style={{ textAlign: 'center', background: 'rgba(166, 28, 32, 0.05)' }}>
|
||||
<div style={{ fontSize: '0.82rem', color: 'var(--brand)', fontWeight: 600 }}>闭环链路</div>
|
||||
<p style={{ fontWeight: 500, color: 'var(--brand)' }}>{system.loop}</p>
|
||||
<div className="m-card m-card-loop m-card--banner">
|
||||
<div style={{ fontSize: '0.82rem', color: 'var(--brand)', fontWeight: 600 }}>闭环链路</div>
|
||||
<p style={{ fontWeight: 500, color: 'var(--brand)' }}>{system.loop}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="m-closing">{system.closing}</div>
|
||||
@@ -384,7 +426,7 @@ function MobileTechView() {
|
||||
<p className="m-lead">{tech.lead}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid">
|
||||
{tech.points.map((pt) => (
|
||||
<div key={pt.title} className="m-card">
|
||||
<h3>{pt.title}</h3>
|
||||
@@ -410,13 +452,12 @@ function MobileSubsidyView() {
|
||||
<p className="m-lead">{subsidy.lead}</p>
|
||||
</div>
|
||||
|
||||
<div className="m-card" style={{ textAlign: 'center', padding: '1.5rem 1rem' }}>
|
||||
<div style={{ fontSize: '0.88rem', color: 'var(--gold)', fontWeight: 600 }}>专项补贴规模</div>
|
||||
<div className="m-big-num">{subsidy.number}</div>
|
||||
<p style={{ fontSize: '0.85rem' }}>{subsidy.numberNote}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid m-card-grid--subsidy">
|
||||
<div className="m-card m-card--banner m-card--subsidy-hero">
|
||||
<div className="m-subsidy-label">专项补贴规模</div>
|
||||
<div className="m-big-num">{subsidy.number}</div>
|
||||
<p className="m-subsidy-note">{subsidy.numberNote}</p>
|
||||
</div>
|
||||
{subsidy.points.map((pt) => (
|
||||
<div key={pt.title} className="m-card">
|
||||
<h3>{pt.title}</h3>
|
||||
@@ -431,6 +472,8 @@ function MobileSubsidyView() {
|
||||
}
|
||||
|
||||
function MobilePartnerView() {
|
||||
const { goSection } = useOrdinaryNav()
|
||||
|
||||
return (
|
||||
<div className="m-view-container">
|
||||
<div>
|
||||
@@ -439,7 +482,7 @@ function MobilePartnerView() {
|
||||
<p className="m-lead">{partner.lead}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid">
|
||||
{partner.points.map((pt) => (
|
||||
<div key={pt.title} className="m-card">
|
||||
<h3>{pt.title}</h3>
|
||||
@@ -449,12 +492,12 @@ function MobilePartnerView() {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', marginTop: '1rem' }}>
|
||||
<a href="#m-contact" className="m-btn m-btn-primary">
|
||||
<button type="button" className="m-btn m-btn-primary" onClick={() => goSection('m-contact', { anchor: 'm-contact-main' })}>
|
||||
{partner.primaryCta.label}
|
||||
</a>
|
||||
<a href="#m-tech-detail" className="m-btn m-btn-outline">
|
||||
</button>
|
||||
<button type="button" className="m-btn m-btn-outline" onClick={() => goSection('m-haoke', { anchor: 'm-tech-detail' })}>
|
||||
{partner.secondaryCta.label}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -469,7 +512,7 @@ function MobileTechDetailView() {
|
||||
<p className="m-lead">{techDetail.lead}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div className="m-card-grid">
|
||||
{techDetail.ports.map((port) => (
|
||||
<div key={port.title} className="m-card">
|
||||
<h3>{port.title}</h3>
|
||||
@@ -494,7 +537,7 @@ function MobileContactView() {
|
||||
|
||||
<div className="m-card m-qr-card">
|
||||
<div className="m-qr-img">
|
||||
<img src={images.qr} alt="微信二维码" />
|
||||
<img src={images.qr} alt="企业微信二维码" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.25rem' }}>{contact.name}</h3>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { allDeepPages } from '../content/nav'
|
||||
|
||||
export type OrdinaryPanelId = 'm-home' | 'm-culture' | 'm-qingxiang' | 'm-haoke' | 'm-contact'
|
||||
|
||||
export type OrdinarySubnavItem = {
|
||||
label: string
|
||||
anchor?: string
|
||||
}
|
||||
|
||||
export type OrdinaryNavMenu = {
|
||||
id: OrdinaryPanelId
|
||||
children: OrdinarySubnavItem[]
|
||||
}
|
||||
|
||||
const cultureDeep = allDeepPages.filter((p) => p.group === 'culture')
|
||||
const poetryDeep = allDeepPages.filter((p) => p.group === 'poetry')
|
||||
const qingxiangDeep = allDeepPages.filter((p) => p.group === 'qingxiang')
|
||||
|
||||
export function pathToAnchorId(path: string) {
|
||||
return `m${path.replace(/\//g, '-')}`
|
||||
}
|
||||
|
||||
export const ordinarySubnav: OrdinaryNavMenu[] = [
|
||||
{ id: 'm-home', children: [] },
|
||||
{
|
||||
id: 'm-culture',
|
||||
children: [
|
||||
{ label: '品牌传承', anchor: 'm-brand' },
|
||||
...cultureDeep.map((p) => ({ label: p.navLabel, anchor: pathToAnchorId(p.path) })),
|
||||
{ label: '诗酒文化', anchor: 'm-poetry' },
|
||||
...poetryDeep.map((p) => ({ label: p.navLabel, anchor: pathToAnchorId(p.path) })),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'm-qingxiang',
|
||||
children: [
|
||||
{ label: '清香', anchor: 'm-qingxiang-main' },
|
||||
...qingxiangDeep.map((p) => ({ label: p.navLabel, anchor: pathToAnchorId(p.path) })),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'm-haoke',
|
||||
children: [
|
||||
{ label: '好客模式', anchor: 'm-haoke-main' },
|
||||
{ label: '三端协同', anchor: 'm-system' },
|
||||
{ label: '科技驱动', anchor: 'm-tech' },
|
||||
{ label: '酒厂实力', anchor: 'm-subsidy' },
|
||||
{ label: '科技板块', anchor: 'm-tech-detail' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'm-contact',
|
||||
children: [
|
||||
{ label: '城市合伙人', anchor: 'm-partner' },
|
||||
{ label: '联系我们', anchor: 'm-contact-main' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function getSubnavForPanel(id: OrdinaryPanelId) {
|
||||
return ordinarySubnav.find((m) => m.id === id)?.children ?? []
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export function ContactPage() {
|
||||
</Reveal>
|
||||
|
||||
<Reveal variant="anim-meet-right" className="qr-block">
|
||||
<img src={images.qr} alt="项目负责人微信二维码" className="qr-image" />
|
||||
<img src={images.qr} alt="企业微信二维码" className="qr-image" />
|
||||
<p>{contact.wechatHint}</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* ===== Stage Shell Ambient Theme & HUD ===== */
|
||||
.stage-shell {
|
||||
--vw: 1vw;
|
||||
--vh: 1vh;
|
||||
--stage-content-max: 76vw;
|
||||
--brand: #a61c20;
|
||||
--gold: #c4a35a;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -15,6 +20,30 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-shell {
|
||||
--stage-brand: #a61c20;
|
||||
--stage-gold: #9f762f;
|
||||
--stage-gold-light: #c4a35a;
|
||||
--stage-glass-bg: rgba(255, 252, 246, 0.82);
|
||||
--stage-glass-border: rgba(166, 28, 32, 0.16);
|
||||
--stage-glow: rgba(166, 28, 32, 0.1);
|
||||
color: #251b16;
|
||||
background:
|
||||
radial-gradient(circle at 15% 20%, rgba(166, 28, 32, 0.08), transparent 38%),
|
||||
linear-gradient(180deg, #fbf7ef 0%, #eee4d5 100%);
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-shell::before {
|
||||
background-image:
|
||||
linear-gradient(rgba(123, 83, 45, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(123, 83, 45, 0.035) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-hud-top {
|
||||
background: linear-gradient(180deg, rgba(247, 242, 234, 0.98), rgba(247, 242, 234, 0.72));
|
||||
border-bottom-color: rgba(166, 28, 32, 0.13);
|
||||
}
|
||||
|
||||
/* Subtle grid overlay for tech/exhibition feel */
|
||||
.stage-shell::before {
|
||||
content: '';
|
||||
@@ -23,7 +52,7 @@
|
||||
background-image:
|
||||
linear-gradient(rgba(196, 163, 90, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(196, 163, 90, 0.03) 1px, transparent 1px);
|
||||
background-size: 4rem 4rem;
|
||||
background-size: 3.333vw 3.333vw;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -34,100 +63,100 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(4.5rem + env(safe-area-inset-top, 0px));
|
||||
height: calc(6.667vh + env(safe-area-inset-top, 0px));
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
padding-left: 2.083vw;
|
||||
padding-right: 2.083vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.75rem;
|
||||
overflow: hidden;
|
||||
gap: 0.625vw;
|
||||
z-index: 50;
|
||||
background: linear-gradient(180deg, rgba(14, 12, 16, 0.95) 0%, rgba(14, 12, 16, 0) 100%);
|
||||
border-bottom: 1px solid rgba(196, 163, 90, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(0.417vw);
|
||||
}
|
||||
|
||||
.stage-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
text-decoration: none;
|
||||
flex-shrink: 1;
|
||||
gap: 0.708vw;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stage-logo,
|
||||
.stage-logo-prog {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
height: min(3.2vw, 5.2vh);
|
||||
width: auto;
|
||||
max-width: min(12vw, 18vh);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stage-logo-prog .stage-prog-img__thumb,
|
||||
.stage-logo-prog .stage-prog-img__full {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
height: min(3.2vw, 5.2vh);
|
||||
width: auto;
|
||||
max-width: min(12vw, 18vh);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 8px rgba(196, 163, 90, 0.4));
|
||||
filter: drop-shadow(0 0 0.417vw rgba(196, 163, 90, 0.4));
|
||||
}
|
||||
|
||||
.stage-logo-prog .stage-prog-img__thumb {
|
||||
filter: blur(4px) drop-shadow(0 0 8px rgba(196, 163, 90, 0.4));
|
||||
filter: blur(0.208vw) drop-shadow(0 0 0.417vw rgba(196, 163, 90, 0.4));
|
||||
}
|
||||
|
||||
.stage-logo {
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 8px rgba(196, 163, 90, 0.4));
|
||||
filter: drop-shadow(0 0 0.417vw rgba(196, 163, 90, 0.4));
|
||||
}
|
||||
|
||||
.stage-title {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.042vw;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
letter-spacing: 0.102vw;
|
||||
color: var(--gold, #c4a35a);
|
||||
background: linear-gradient(135deg, #f3ece4 0%, #c4a35a 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
background: rgba(166, 28, 32, 0.25);
|
||||
border: 1px solid rgba(166, 28, 32, 0.5);
|
||||
color: #f3ece4;
|
||||
}
|
||||
|
||||
/* Slide Nav Pills */
|
||||
.stage-nav-pills {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.292vw;
|
||||
background: rgba(28, 25, 31, 0.7);
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 2rem;
|
||||
padding: 0.444vh 0.417vw;
|
||||
border-radius: 1.667vw;
|
||||
border: 1px solid rgba(196, 163, 90, 0.2);
|
||||
flex-shrink: 1;
|
||||
flex: 0 1 auto;
|
||||
width: max-content;
|
||||
max-width: min(var(--stage-content-max), 100%);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.stage-nav-pills::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stage-nav-pill {
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.519vh 0.708vw;
|
||||
border-radius: 1.25vw;
|
||||
font-size: 0.683vw;
|
||||
color: rgba(243, 236, 228, 0.65);
|
||||
text-decoration: none;
|
||||
transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stage-nav-pill:hover {
|
||||
@@ -139,7 +168,7 @@
|
||||
color: #121014;
|
||||
background: linear-gradient(135deg, #e8d08d 0%, #c4a35a 100%);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 0 12px rgba(196, 163, 90, 0.35);
|
||||
box-shadow: 0 0 0.625vw rgba(196, 163, 90, 0.35);
|
||||
}
|
||||
|
||||
/* AutoPlay Button, Exit & Index Indicator */
|
||||
@@ -148,23 +177,58 @@
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
gap: 0.85rem;
|
||||
gap: 0.708vw;
|
||||
}
|
||||
|
||||
.stage-autoplay-btn,
|
||||
.stage-remote-toggle,
|
||||
.stage-exit-btn {
|
||||
.stage-exit-btn,
|
||||
.stage-theme-toggle {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stage-theme-toggle {
|
||||
min-height: auto;
|
||||
padding: 0.519vh 0.625vw;
|
||||
font-size: 0.65vw;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.stage-theme-toggle > span:first-child {
|
||||
width: 1.1vw;
|
||||
height: 1.1vw;
|
||||
font-size: 0.55vw;
|
||||
}
|
||||
|
||||
.stage-theme-toggle .theme-toggle-label {
|
||||
font-size: 0.65vw;
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-nav-pills,
|
||||
[data-theme='nostalgic'] .stage-autoplay-btn,
|
||||
[data-theme='nostalgic'] .stage-remote-toggle {
|
||||
color: #5c534c;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border-color: rgba(166, 28, 32, 0.14);
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-nav-pill {
|
||||
color: #6b5344;
|
||||
}
|
||||
|
||||
[data-theme='nostalgic'] .stage-nav-pill:hover {
|
||||
color: #a61c20;
|
||||
background: rgba(166, 28, 32, 0.08);
|
||||
}
|
||||
|
||||
.stage-autoplay-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.78rem;
|
||||
gap: 0.292vw;
|
||||
padding: 0.519vh 0.625vw;
|
||||
border-radius: 1.25vw;
|
||||
font-size: 0.65vw;
|
||||
color: rgba(243, 236, 228, 0.85);
|
||||
background: rgba(28, 25, 31, 0.8);
|
||||
border: 1px solid rgba(196, 163, 90, 0.3);
|
||||
@@ -179,23 +243,23 @@
|
||||
}
|
||||
|
||||
.stage-play-icon {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.583vw;
|
||||
}
|
||||
|
||||
.stage-index-num {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 0.917vw;
|
||||
letter-spacing: 0.085vw;
|
||||
color: rgba(196, 163, 90, 0.85);
|
||||
}
|
||||
|
||||
.stage-exit-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.82rem;
|
||||
gap: 0.333vw;
|
||||
padding: 0.593vh 0.833vw;
|
||||
border-radius: 0.167vw;
|
||||
font-size: 0.683vw;
|
||||
color: #f3ece4;
|
||||
background: rgba(166, 28, 32, 0.3);
|
||||
border: 1px solid rgba(166, 28, 32, 0.6);
|
||||
@@ -207,7 +271,7 @@
|
||||
.stage-exit-btn:hover {
|
||||
background: rgba(166, 28, 32, 0.7);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 15px rgba(166, 28, 32, 0.4);
|
||||
box-shadow: 0 0 0.781vw rgba(166, 28, 32, 0.4);
|
||||
}
|
||||
|
||||
/* Main Viewport Container */
|
||||
@@ -216,36 +280,38 @@
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
padding: calc(4.5rem + env(safe-area-inset-top, 0px)) 2.5rem calc(2.5rem + env(safe-area-inset-bottom, 0px));
|
||||
padding: calc(6.667vh + env(safe-area-inset-top, 0px)) 2.083vw calc(3.704vh + env(safe-area-inset-bottom, 0px));
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Bottom Progress & Hints with Safe Area Bottom */
|
||||
.stage-hud-bottom {
|
||||
position: absolute;
|
||||
bottom: calc(0.85rem + env(safe-area-inset-bottom, 0px));
|
||||
bottom: calc(1.259vh + env(safe-area-inset-bottom, 0px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: 0.333vw;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stage-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.417vw;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.stage-dot {
|
||||
position: relative;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
width: min(0.417vw, 0.741vh);
|
||||
height: min(0.417vw, 0.741vh);
|
||||
border-radius: 50%;
|
||||
background: rgba(196, 163, 90, 0.25);
|
||||
border: none;
|
||||
@@ -256,8 +322,8 @@
|
||||
}
|
||||
|
||||
.stage-dot.is-active {
|
||||
width: 2rem;
|
||||
border-radius: 0.25rem;
|
||||
width: 1.667vw;
|
||||
border-radius: 0.208vw;
|
||||
background: rgba(196, 163, 90, 0.3);
|
||||
}
|
||||
|
||||
@@ -268,7 +334,7 @@
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, var(--brand, #a61c20), var(--gold, #c4a35a));
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 0.208vw;
|
||||
transform-origin: left;
|
||||
animation: stageProgressBar var(--stage-progress-dur, 6s) linear forwards;
|
||||
}
|
||||
@@ -283,40 +349,34 @@
|
||||
}
|
||||
|
||||
.stage-keyboard-hint {
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.6vw;
|
||||
color: rgba(243, 236, 228, 0.4);
|
||||
letter-spacing: 0.08em;
|
||||
letter-spacing: 0.068vw;
|
||||
}
|
||||
|
||||
/* Mobile Responsive Adjustments for Stage Shell */
|
||||
@media (max-width: 72rem) {
|
||||
.stage-nav-pills {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stage-hud-top {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
height: calc(3.6rem + env(safe-area-inset-top, 0px));
|
||||
padding-left: 0.625vw;
|
||||
padding-right: 0.625vw;
|
||||
height: calc(5.333vh + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
.stage-title,
|
||||
.stage-badge,
|
||||
.stage-index-num {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stage-hud-actions {
|
||||
gap: 0.4rem;
|
||||
gap: 0.333vw;
|
||||
}
|
||||
|
||||
.stage-viewport {
|
||||
padding-top: calc(3.6rem + env(safe-area-inset-top, 0px));
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-bottom: calc(3rem + env(safe-area-inset-bottom, 0px));
|
||||
padding-top: calc(5.333vh + env(safe-area-inset-top, 0px));
|
||||
padding-left: 0.833vw;
|
||||
padding-right: 0.833vw;
|
||||
padding-bottom: calc(4.444vh + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.stage-exit-btn span:first-child {
|
||||
@@ -332,14 +392,17 @@
|
||||
.stage-view-anim {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
animation: stageFadeIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@keyframes stageFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98) translateY(10px);
|
||||
filter: blur(4px);
|
||||
transform: scale(0.98) translateY(0.926vh);
|
||||
filter: blur(0.208vw);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { stageNavItems } from '../content/nav'
|
||||
import { images } from '../content/site'
|
||||
import { StageViews } from './StageViews'
|
||||
import { StageProgressiveImage } from './StageProgressiveImage'
|
||||
import { stageImages } from './stageAssets'
|
||||
import { useStageImagePreload } from './useStageImagePreload'
|
||||
import { StageRemotePanel } from './remote/StageRemotePanel'
|
||||
import { useStageHost } from './remote/useStageHost'
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
type RemoteToHost,
|
||||
} from './remote/protocol'
|
||||
import { assetUrl } from '../lib/assetUrl'
|
||||
import { ThemeToggle } from '../hooks/useTheme'
|
||||
import './StageShell.css'
|
||||
|
||||
const BGM_SRC = assetUrl('/assets/stage/bgm.wav')
|
||||
@@ -30,6 +30,7 @@ export function StageShell() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const touchStartX = useRef<number | null>(null)
|
||||
const navPillsRef = useRef<HTMLElement>(null)
|
||||
const bgmRef = useRef<HTMLAudioElement | null>(null)
|
||||
const audioWantedRef = useRef(true)
|
||||
const [isAutoPlay, setIsAutoPlay] = useState(true)
|
||||
@@ -224,6 +225,13 @@ export function StageShell() {
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [handleNext, handlePrev])
|
||||
|
||||
useEffect(() => {
|
||||
const nav = navPillsRef.current
|
||||
if (!nav) return
|
||||
const activePill = nav.querySelector<HTMLElement>('.stage-nav-pill.is-active')
|
||||
activePill?.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' })
|
||||
}, [activeIdx])
|
||||
|
||||
const normalUrl = location.pathname === '/' ? '/' : location.pathname
|
||||
|
||||
return (
|
||||
@@ -234,14 +242,12 @@ export function StageShell() {
|
||||
>
|
||||
{/* Top HUD */}
|
||||
<header className="stage-hud-top">
|
||||
<Link to="/?mode=stage" className="stage-brand">
|
||||
<StageProgressiveImage pair={stageImages.logo} alt="杜康好客" className="stage-logo-prog" priority />
|
||||
<span className="stage-title">杜康好客</span>
|
||||
<span className="stage-badge">全景展控</span>
|
||||
</Link>
|
||||
<div className="stage-brand">
|
||||
<img src={images.logo} alt="杜康好客" className="stage-logo" />
|
||||
</div>
|
||||
|
||||
{/* Module Navigation Pills */}
|
||||
<nav className="stage-nav-pills">
|
||||
<nav className="stage-nav-pills" ref={navPillsRef}>
|
||||
{stageNavItems.map((item, idx) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
@@ -255,6 +261,7 @@ export function StageShell() {
|
||||
|
||||
{/* Index, AutoPlay Toggle & Exit */}
|
||||
<div className="stage-hud-actions">
|
||||
<ThemeToggle className="stage-theme-toggle" />
|
||||
<button
|
||||
type="button"
|
||||
className={`stage-remote-toggle ${clientCount > 0 ? 'is-live' : ''}`}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
contact,
|
||||
haoke,
|
||||
hero,
|
||||
images,
|
||||
partner,
|
||||
poetry,
|
||||
qingxiang,
|
||||
@@ -61,9 +62,12 @@ function StageHomeView() {
|
||||
<h1 className="stage-home-brand stage-stagger" style={{ '--i': 2 } as React.CSSProperties}>
|
||||
{hero.brand}
|
||||
</h1>
|
||||
<div className="stage-home-sub stage-stagger" style={{ '--i': 3 } as React.CSSProperties}>
|
||||
杜康好客
|
||||
</div>
|
||||
<img
|
||||
src={images.brandMarkH}
|
||||
alt="杜康好客"
|
||||
className="stage-home-brand-mark stage-stagger"
|
||||
style={{ '--i': 3 } as React.CSSProperties}
|
||||
/>
|
||||
<h2 className="stage-home-title stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
|
||||
{hero.title}
|
||||
</h2>
|
||||
@@ -71,7 +75,7 @@ function StageHomeView() {
|
||||
{hero.subtitle}
|
||||
</p>
|
||||
<div className="stage-stagger" style={{ '--i': 6 } as React.CSSProperties}>
|
||||
<Link to="/brand?mode=stage" className="stage-exit-btn" style={{ padding: '0.6rem 1.5rem', fontSize: '1rem' }}>
|
||||
<Link to="/brand?mode=stage" className="stage-exit-btn stage-cta-btn">
|
||||
进入品牌展厅 ›
|
||||
</Link>
|
||||
</div>
|
||||
@@ -252,24 +256,9 @@ function StageHaokeView() {
|
||||
|
||||
<div className="stage-haoke-grid">
|
||||
<div className="stage-card stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
|
||||
<h3 style={{ fontFamily: 'Noto Serif SC', fontSize: '1.4rem', color: '#ffffff', margin: '0 0 0.8rem' }}>
|
||||
核心模式机制
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
padding: '1.2rem',
|
||||
background: 'rgba(166, 28, 32, 0.2)',
|
||||
border: '1px solid var(--stage-brand)',
|
||||
borderRadius: '0.4rem',
|
||||
fontSize: '1.15rem',
|
||||
color: 'var(--stage-gold-light)',
|
||||
fontFamily: 'Noto Serif SC',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{haoke.oneLiner}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.95rem', color: 'rgba(243,236,228,0.8)', lineHeight: 1.6 }}>{haoke.closing}</p>
|
||||
<h3 className="stage-haoke-core-title">核心模式机制</h3>
|
||||
<div className="stage-haoke-oneliner">{haoke.oneLiner}</div>
|
||||
<p className="stage-haoke-closing-text">{haoke.closing}</p>
|
||||
</div>
|
||||
|
||||
<div className="stage-haoke-mechanism">
|
||||
@@ -374,9 +363,9 @@ function StageSubsidyView() {
|
||||
|
||||
<div className="stage-subsidy-layout">
|
||||
<div className="stage-card stage-subsidy-hero stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
|
||||
<div style={{ fontSize: '1.1rem', color: 'var(--stage-gold)' }}>专项资源规模</div>
|
||||
<div className="stage-subsidy-label">专项资源规模</div>
|
||||
<div className="stage-big-num">{subsidy.number}</div>
|
||||
<p style={{ fontSize: '0.9rem', color: 'rgba(243,236,228,0.75)' }}>{subsidy.numberNote}</p>
|
||||
<p className="stage-subsidy-note">{subsidy.numberNote}</p>
|
||||
</div>
|
||||
|
||||
<div className="stage-subsidy-cards">
|
||||
@@ -438,8 +427,8 @@ function StagePartnerView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stage-stagger" style={{ '--i': 7, marginTop: '1.5rem' } as React.CSSProperties}>
|
||||
<Link to="/contact?mode=stage" className="stage-exit-btn" style={{ padding: '0.6rem 1.5rem', fontSize: '1rem' }}>
|
||||
<div className="stage-stagger stage-partner-cta" style={{ '--i': 7 } as React.CSSProperties}>
|
||||
<Link to="/contact?mode=stage" className="stage-exit-btn stage-cta-btn">
|
||||
立即联系合作 ›
|
||||
</Link>
|
||||
</div>
|
||||
@@ -497,7 +486,7 @@ function StageContactView() {
|
||||
|
||||
<div className="stage-contact-box stage-stagger" style={{ '--i': 4 } as React.CSSProperties}>
|
||||
<div className="stage-qr-frame">
|
||||
<StageProgressiveImage pair={stageImages.qr} alt="微信二维码" className="stage-qr-img" priority />
|
||||
<StageProgressiveImage pair={stageImages.qr} alt="企业微信二维码" className="stage-qr-img" priority />
|
||||
</div>
|
||||
|
||||
<div className="stage-contact-info">
|
||||
|
||||
@@ -23,8 +23,9 @@ body.remote-mode {
|
||||
}
|
||||
|
||||
.remote-logo {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
height: 2.6rem;
|
||||
width: auto;
|
||||
max-width: 7.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: calc(5.2rem + env(safe-area-inset-top, 0px)) 2.5rem calc(3rem + env(safe-area-inset-bottom, 0px));
|
||||
padding: calc(7.704vh + env(safe-area-inset-top, 0px)) 2.083vw calc(4.444vh + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.stage-remote-panel {
|
||||
width: min(22rem, 92vw);
|
||||
padding: 1.35rem 1.4rem 1.5rem;
|
||||
width: min(18.333vw, 92vw);
|
||||
padding: 2vh 1.167vw 2.222vh;
|
||||
background: linear-gradient(180deg, #1c191f 0%, #121014 100%);
|
||||
border: 1px solid rgba(196, 163, 90, 0.35);
|
||||
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.45);
|
||||
box-shadow: 0 1.25vw 2.5vw rgba(0, 0, 0, 0.45);
|
||||
color: #f3ece4;
|
||||
}
|
||||
|
||||
@@ -22,26 +22,26 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.1rem;
|
||||
gap: 0.833vw;
|
||||
margin-bottom: 1.63vh;
|
||||
}
|
||||
|
||||
.stage-remote-kicker {
|
||||
margin: 0 0 0.2rem;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
margin: 0 0 0.296vh;
|
||||
font-size: 0.6vw;
|
||||
letter-spacing: 0.153vw;
|
||||
color: var(--gold, #c4a35a);
|
||||
}
|
||||
|
||||
.stage-remote-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 1.042vw;
|
||||
letter-spacing: 0.068vw;
|
||||
}
|
||||
|
||||
.stage-remote-close {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: min(1.667vw, 2.963vh);
|
||||
height: min(1.667vw, 2.963vh);
|
||||
color: rgba(243, 236, 228, 0.8);
|
||||
border: 1px solid rgba(196, 163, 90, 0.3);
|
||||
background: transparent;
|
||||
@@ -51,8 +51,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 12.5rem;
|
||||
padding: 0.75rem;
|
||||
min-height: 18.519vh;
|
||||
padding: min(0.625vw, 1.111vh);
|
||||
background: #f7f2ea;
|
||||
}
|
||||
|
||||
@@ -62,18 +62,18 @@
|
||||
}
|
||||
|
||||
.stage-remote-code {
|
||||
margin: 1rem 0 0.35rem;
|
||||
margin: 1.481vh 0 0.519vh;
|
||||
text-align: center;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 0.958vw;
|
||||
letter-spacing: 0.136vw;
|
||||
color: var(--gold, #c4a35a);
|
||||
}
|
||||
|
||||
.stage-remote-status {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.708vw;
|
||||
color: rgba(243, 236, 228, 0.65);
|
||||
}
|
||||
|
||||
@@ -86,27 +86,27 @@
|
||||
}
|
||||
|
||||
.stage-remote-hint {
|
||||
margin: 0.85rem 0 0;
|
||||
margin: 1.259vh 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.65vw;
|
||||
line-height: 1.6;
|
||||
color: rgba(243, 236, 228, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
@media (max-width: 768px) {
|
||||
.stage-remote-overlay {
|
||||
justify-content: center;
|
||||
padding: calc(4.2rem + env(safe-area-inset-top, 0px)) 1rem calc(1.5rem + env(safe-area-inset-bottom, 0px));
|
||||
padding: calc(6.222vh + env(safe-area-inset-top, 0px)) 0.833vw calc(2.222vh + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
.stage-remote-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.78rem;
|
||||
gap: 0.292vw;
|
||||
padding: 0.519vh 0.625vw;
|
||||
border-radius: 1.25vw;
|
||||
font-size: 0.65vw;
|
||||
color: rgba(243, 236, 228, 0.85);
|
||||
background: rgba(28, 25, 31, 0.8);
|
||||
border: 1px solid rgba(196, 163, 90, 0.3);
|
||||
|
||||
@@ -8,7 +8,8 @@ function pair(thumb: string, full: string): StageImagePair {
|
||||
|
||||
export const stageImages = {
|
||||
logo: pair('/assets/stage/thumb/logo.webp', '/assets/stage/full/logo.webp'),
|
||||
t30: pair('/assets/stage/thumb/t30-bottle.webp', '/assets/stage/full/t30-bottle.webp'),
|
||||
// Use the shared OSS PNG; stage webp on OSS may lag behind.
|
||||
t30: pair('/assets/t30-bottle.png', '/assets/t30-bottle.png'),
|
||||
brand: {
|
||||
manor: pair('/assets/stage/thumb/emb_p6_5.webp', '/assets/stage/full/emb_p6_5.webp'),
|
||||
relic: pair('/assets/stage/thumb/emb_p33_1.webp', '/assets/stage/full/emb_p33_1.webp'),
|
||||
|
||||
@@ -12,12 +12,39 @@
|
||||
--tech-bg: #121014;
|
||||
--tech-card: #1c191f;
|
||||
--tech-line: rgba(196, 163, 90, 0.28);
|
||||
--max: min(72rem, 92vw);
|
||||
--content-max: 77.5rem;
|
||||
--max: min(var(--content-max), calc(100vw - 3rem));
|
||||
--surface: var(--paper);
|
||||
--surface-card: rgba(255, 255, 255, 0.85);
|
||||
--surface-header: rgba(247, 242, 234, 0.94);
|
||||
--line: rgba(166, 28, 32, 0.12);
|
||||
--accent: var(--brand);
|
||||
--nav-h: 4.5rem;
|
||||
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--shadow-soft: 0 1.25rem 3rem rgba(26, 21, 18, 0.08);
|
||||
}
|
||||
|
||||
html[data-theme='technology'] {
|
||||
--brand: #d4aa56;
|
||||
--brand-deep: #b88a35;
|
||||
--brand-soft: rgba(212, 170, 86, 0.12);
|
||||
--gold: #e5c477;
|
||||
--gold-soft: rgba(229, 196, 119, 0.28);
|
||||
--ink: #f2f6fa;
|
||||
--ink-soft: #aeb9c6;
|
||||
--paper: #0b1017;
|
||||
--paper-deep: #111923;
|
||||
--surface: #0b1017;
|
||||
--surface-card: rgba(20, 30, 42, 0.88);
|
||||
--surface-header: rgba(9, 14, 21, 0.94);
|
||||
--line: rgba(116, 170, 210, 0.2);
|
||||
--accent: #62bce8;
|
||||
--tech-bg: #0b1017;
|
||||
--tech-card: #141e2a;
|
||||
--tech-line: rgba(98, 188, 232, 0.25);
|
||||
--shadow-soft: 0 1.25rem 3rem rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
/* rem base: 375px -> 14px, 1920px -> 18px */
|
||||
html {
|
||||
font-size: clamp(0.875rem, 0.75rem + 0.35vw, 1.125rem);
|
||||
@@ -37,6 +64,7 @@ body {
|
||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.7;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: color 0.25s ease, background 0.25s ease;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -151,7 +179,7 @@ h3,
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-tech {
|
||||
[data-theme='technology'] {
|
||||
--ink: #f3ece4;
|
||||
--ink-soft: rgba(243, 236, 228, 0.72);
|
||||
--paper: var(--tech-bg);
|
||||
@@ -159,28 +187,63 @@ h3,
|
||||
background: var(--tech-bg);
|
||||
}
|
||||
|
||||
.theme-tech .eyebrow,
|
||||
.theme-tech .closing {
|
||||
[data-theme='technology'] .eyebrow,
|
||||
[data-theme='technology'] .closing {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.theme-tech .eyebrow::before {
|
||||
[data-theme='technology'] .eyebrow::before {
|
||||
background: var(--gold);
|
||||
}
|
||||
|
||||
.theme-tech .closing {
|
||||
[data-theme='technology'] .closing {
|
||||
border-top-color: var(--tech-line);
|
||||
}
|
||||
|
||||
.theme-tech .btn-ghost {
|
||||
[data-theme='technology'] .btn-ghost {
|
||||
color: var(--gold);
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
.theme-tech .btn-ghost:hover {
|
||||
[data-theme='technology'] .btn-ghost:hover {
|
||||
background: rgba(196, 163, 90, 0.12);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
min-height: 2.15rem;
|
||||
padding: 0.3rem 0.65rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--ink-soft);
|
||||
background: var(--surface-card);
|
||||
box-shadow: var(--shadow-soft);
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.theme-toggle > span:first-child {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border-radius: 50%;
|
||||
color: var(--paper);
|
||||
background: var(--accent);
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.deep-read-rail {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
|
||||