fix:提交更新
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Dev: load images/audio from OSS (same prefix as production).
|
||||
# Layout: {VITE_OSS_BASE}/assets/... → oss://dukang-prod/dukang_office/assets/...
|
||||
VITE_OSS_BASE=https://dukang-prod.oss-cn-hangzhou.aliyuncs.com/dukang_office
|
||||
@@ -0,0 +1,19 @@
|
||||
# Public CDN/OSS base for static images & audio (no trailing slash).
|
||||
# Object layout: {VITE_OSS_BASE}/assets/... → oss://dukang-prod/dukang_office/assets/...
|
||||
#
|
||||
# Used by:
|
||||
# .env.development → npm run dev
|
||||
# .env.production → npm run build
|
||||
#
|
||||
# Examples:
|
||||
# VITE_OSS_BASE=https://dukang-prod.oss-cn-hangzhou.aliyuncs.com/dukang_office
|
||||
# VITE_OSS_BASE=https://cdn.dukanghaoke.com/dukang_office
|
||||
# Leave empty only if you intentionally want local /assets during that mode.
|
||||
VITE_OSS_BASE=
|
||||
|
||||
# --- For npm run oss:sync only (do not commit secrets) ---
|
||||
# OSS_REGION=oss-cn-hangzhou
|
||||
# OSS_BUCKET=dukang-prod
|
||||
# OSS_PREFIX=dukang_office
|
||||
# OSS_ACCESS_KEY_ID=
|
||||
# OSS_ACCESS_KEY_SECRET=
|
||||
@@ -0,0 +1,3 @@
|
||||
# Production OSS public prefix (no trailing slash).
|
||||
# Layout: {VITE_OSS_BASE}/assets/... → oss://dukang-prod/dukang_office/assets/...
|
||||
VITE_OSS_BASE=https://dukang-prod.oss-cn-hangzhou.aliyuncs.com/dukang_office
|
||||
@@ -2,5 +2,8 @@ node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.local
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
_tmp_*.txt
|
||||
.vite
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# 百度搜索资源平台操作清单(部署后执行)
|
||||
|
||||
目标站点:https://www.dukanghaoke.com
|
||||
|
||||
## 1. 验证站点
|
||||
|
||||
1. 打开 https://ziyuan.baidu.com/ 登录
|
||||
2. 添加站点:`https://www.dukanghaoke.com`(若裸域跳转到 www,可一并添加)
|
||||
3. 验证归属(二选一):
|
||||
- **HTML 文件**:把平台下发的 `baidu_verify_xxxx.html` 放到 `public/` 后重新部署
|
||||
- **Meta 标签**:把验证码填入 `index.html` 中 `baidu-site-verification` 的 content(替换 REPLACE_WITH_BAIDU_CODE)后重新部署
|
||||
|
||||
## 2. 提交资源
|
||||
|
||||
1. Sitemap:提交 `https://www.dukanghaoke.com/sitemap.xml`
|
||||
2. 普通收录:优先推送
|
||||
- https://www.dukanghaoke.com/
|
||||
- https://www.dukanghaoke.com/brand
|
||||
- https://www.dukanghaoke.com/qingxiang
|
||||
- https://www.dukanghaoke.com/haoke
|
||||
- https://www.dukanghaoke.com/contact
|
||||
3. 有 API Token 时可用普通收录 API 每日推送核心 URL
|
||||
|
||||
## 3. 自检
|
||||
|
||||
- 浏览器打开 `https://www.dukanghaoke.com/robots.txt`、`/sitemap.xml`
|
||||
- 百度搜索:`site:www.dukanghaoke.com`
|
||||
- 品牌词「杜康好客」通常需数天到数周才稳定展示新站
|
||||
|
||||
## 4. OSS 图片(与收录并行)
|
||||
|
||||
1. 将 `public/assets`(及如有 `public/audio`)上传到 `oss://dukang-prod/dukang_office/assets/`(音频到 `.../audio/`)
|
||||
2. 确认 `.env.production` 中 `VITE_OSS_BASE` 与公网前缀一致后 `npm run build` 再部署
|
||||
3. 抽查:`{VITE_OSS_BASE}/assets/logo.png` 可公网打开
|
||||
|
||||
## 5. 缓存
|
||||
|
||||
- 站点 HTML:短校验(见 `deploy/nginx-cache.conf` / `public/_headers`)
|
||||
- Vite 带 hash 的 JS/CSS:长缓存
|
||||
- OSS 图片:建议 30 天 Cache-Control;发版后一般只需 purge 首页 HTML
|
||||
@@ -0,0 +1,37 @@
|
||||
# Nginx / 宝塔:分层缓存片段(并入 server {} 后 reload)
|
||||
# Vite 构建产物在 /assets/ 下且文件名含 content hash → 长缓存
|
||||
# 注意:若 public 静态图也挂在 /assets/,与构建产物同前缀时用扩展名区分
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
|
||||
location ~* \.html$ {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
|
||||
# Vite 打出的带 hash 的 JS/CSS(部署后路径通常为 /assets/index-xxxxx.js)
|
||||
location ~* ^/assets/.*\.(?:js|css)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# 同域兜底图片(生产主图应走 OSS;若仍有本地图则 7 天)
|
||||
location ~* ^/assets/.*\.(?:png|jpe?g|gif|webp|svg|ico)$ {
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
|
||||
location = /sitemap.xml {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
default_type application/xml;
|
||||
}
|
||||
|
||||
# SPA 路由回退(按实际根目录调整)
|
||||
# location / {
|
||||
# try_files $uri $uri/ /index.html;
|
||||
# }
|
||||
+63
-4
@@ -4,18 +4,77 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。" />
|
||||
<meta
|
||||
name="description"
|
||||
content="杜康好客官网——你请客,我买单。酒祖杜康清香白酒,买酒送等价好客权益,到合作门店抵餐。官方网站 www.dukanghaoke.com。"
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="杜康好客,酒祖杜康,杜康好客官网,清香白酒,你请客我买单,城市合伙人,酒祖文化"
|
||||
/>
|
||||
<!-- 百度站长验证:在 ziyuan.baidu.com 获取后填入 content,或改用 public/baidu_verify_*.html 文件验证 -->
|
||||
<meta name="baidu-site-verification" content="REPLACE_WITH_BAIDU_CODE" />
|
||||
<meta http-equiv="Cache-Control" content="max-age=0, must-revalidate" />
|
||||
<link rel="canonical" href="https://www.dukanghaoke.com/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://www.dukanghaoke.com/" />
|
||||
<meta property="og:title" content="杜康好客 · 酒祖杜康" />
|
||||
<meta property="og:description" content="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。" />
|
||||
<title>杜康好客 · 酒祖杜康</title>
|
||||
<meta property="og:title" content="杜康好客官网|酒祖杜康清香 · 你请客我买单" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。官方网站。"
|
||||
/>
|
||||
<meta property="og:locale" content="zh_CN" />
|
||||
<title>杜康好客官网|酒祖杜康清香 · 你请客我买单</title>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "杜康好客",
|
||||
"alternateName": "酒祖杜康",
|
||||
"url": "https://www.dukanghaoke.com/",
|
||||
"logo": "https://www.dukanghaoke.com/assets/logo.png",
|
||||
"description": "杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。"
|
||||
},
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"name": "杜康好客官网",
|
||||
"url": "https://www.dukanghaoke.com/",
|
||||
"inLanguage": "zh-CN",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "杜康好客"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<link rel="preconnect" href="https://dukang-prod.oss-cn-hangzhou.aliyuncs.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://dukang-prod.oss-cn-hangzhou.aliyuncs.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>
|
||||
<header>
|
||||
<h1>杜康好客官网|酒祖杜康</h1>
|
||||
<p>你请客,我买单。酒祖杜康清香,买酒送等价好客权益,到合作门店抵餐。</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="https://www.dukanghaoke.com/">首页</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/brand">品牌传承</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/poetry">诗酒文化</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/qingxiang">清香</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/haoke">好客模式</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/partner">城市合伙人</a></li>
|
||||
<li><a href="https://www.dukanghaoke.com/contact">联系我们</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</noscript>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+1017
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -5,9 +5,11 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build": "tsc -b && vite build && node scripts/prerender-routes.mjs",
|
||||
"preview": "vite preview",
|
||||
"optimize:stage": "node scripts/optimize-stage-images.mjs"
|
||||
"optimize:stage": "node scripts/optimize-stage-images.mjs",
|
||||
"oss:sync": "node scripts/sync-assets-to-oss.mjs",
|
||||
"prerender": "node scripts/prerender-routes.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"peerjs": "^1.5.5",
|
||||
@@ -20,6 +22,7 @@
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"ali-oss": "^6.23.0",
|
||||
"sharp": "^0.34.2",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
/
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
|
||||
/*.html
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
|
||||
/assets/*.js
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/assets/*.css
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=604800
|
||||
|
||||
/robots.txt
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
|
||||
/sitemap.xml
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.dukanghaoke.com/sitemap.xml
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/brand</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/poetry</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/qingxiang</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/haoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/system</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/tech</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/subsidy</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/partner</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/tech-detail</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.dukanghaoke.com/contact</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* After vite build: copy dist/index.html to each main route folder for crawler/static hosting.
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const outDir = path.resolve(__dirname, '../dist')
|
||||
|
||||
const ROUTES = [
|
||||
'/brand',
|
||||
'/poetry',
|
||||
'/qingxiang',
|
||||
'/haoke',
|
||||
'/system',
|
||||
'/tech',
|
||||
'/subsidy',
|
||||
'/partner',
|
||||
'/tech-detail',
|
||||
'/contact',
|
||||
]
|
||||
|
||||
const indexHtml = path.join(outDir, 'index.html')
|
||||
if (!fs.existsSync(indexHtml)) {
|
||||
console.error('dist/index.html not found; run vite build first')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(indexHtml, 'utf8')
|
||||
for (const route of ROUTES) {
|
||||
const dir = path.join(outDir, route.replace(/^\//, ''))
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(path.join(dir, 'index.html'), html)
|
||||
console.log(`prerender ${route}/index.html`)
|
||||
}
|
||||
console.log('Done.')
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Upload public/assets (and optional public/audio) to Aliyun OSS.
|
||||
*
|
||||
* Target layout (matches VITE_OSS_BASE + /assets/...):
|
||||
* oss://{bucket}/dukang_office/assets/...
|
||||
* oss://{bucket}/dukang_office/audio/...
|
||||
*
|
||||
* Required env:
|
||||
* OSS_REGION e.g. oss-cn-hangzhou
|
||||
* OSS_BUCKET e.g. dukang-prod
|
||||
* OSS_ACCESS_KEY_ID
|
||||
* OSS_ACCESS_KEY_SECRET
|
||||
* Optional:
|
||||
* OSS_PREFIX default dukang_office
|
||||
* OSS_ENDPOINT custom endpoint if needed
|
||||
*/
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import OSS from 'ali-oss'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
const region = process.env.OSS_REGION
|
||||
const bucket = process.env.OSS_BUCKET
|
||||
const accessKeyId = process.env.OSS_ACCESS_KEY_ID
|
||||
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET
|
||||
const prefix = (process.env.OSS_PREFIX || 'dukang_office').replace(/^\/|\/$/g, '')
|
||||
const endpoint = process.env.OSS_ENDPOINT
|
||||
|
||||
if (!region || !bucket || !accessKeyId || !accessKeySecret) {
|
||||
console.error(
|
||||
'Missing OSS env. Set OSS_REGION, OSS_BUCKET, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const client = new OSS({
|
||||
region,
|
||||
bucket,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
...(endpoint ? { endpoint } : {}),
|
||||
})
|
||||
|
||||
async function walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = []
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walk(full)))
|
||||
} else {
|
||||
files.push(full)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
async function uploadDir(localDir, remoteSubdir) {
|
||||
try {
|
||||
await fs.access(localDir)
|
||||
} catch {
|
||||
console.warn(`skip (missing): ${localDir}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
const files = await walk(localDir)
|
||||
let count = 0
|
||||
for (const file of files) {
|
||||
const rel = path.relative(localDir, file).split(path.sep).join('/')
|
||||
const key = `${prefix}/${remoteSubdir}/${rel}`
|
||||
await client.put(key, file, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=2592000',
|
||||
},
|
||||
})
|
||||
count += 1
|
||||
console.log(`put ${key}`)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const n1 = await uploadDir(path.join(root, 'public/assets'), 'assets')
|
||||
const n2 = await uploadDir(path.join(root, 'public/audio'), 'audio')
|
||||
console.log(`\nDone. uploaded ${n1 + n2} files under oss://${bucket}/${prefix}/`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
+21
-19
@@ -1,28 +1,30 @@
|
||||
import { assetUrl } from '../../lib/assetUrl'
|
||||
|
||||
export const deepImages = {
|
||||
brand: [
|
||||
'/assets/brand/emb_p6_5.png',
|
||||
'/assets/brand/emb_p31_1.png',
|
||||
'/assets/brand/emb_p15_3.png',
|
||||
'/assets/brand/emb_p33_1.png',
|
||||
'/assets/brand/photo_p11_1.jpg',
|
||||
'/assets/brand/photo_p8_4.jpg',
|
||||
assetUrl('/assets/brand/emb_p6_5.png'),
|
||||
assetUrl('/assets/brand/emb_p31_1.png'),
|
||||
assetUrl('/assets/brand/emb_p15_3.png'),
|
||||
assetUrl('/assets/brand/emb_p33_1.png'),
|
||||
assetUrl('/assets/brand/photo_p11_1.jpg'),
|
||||
assetUrl('/assets/brand/photo_p8_4.jpg'),
|
||||
],
|
||||
poetry: [
|
||||
'/assets/poetry/emb_p33_4.png',
|
||||
'/assets/poetry/emb_p49_4.png',
|
||||
'/assets/poetry/emb_p59_4.png',
|
||||
'/assets/poetry/emb_p67_4.png',
|
||||
'/assets/poetry/emb_p25_4.png',
|
||||
'/assets/poetry/emb_p66_4.png',
|
||||
assetUrl('/assets/poetry/emb_p33_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p49_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p59_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p67_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p25_4.png'),
|
||||
assetUrl('/assets/poetry/emb_p66_4.png'),
|
||||
],
|
||||
qingxiang: [
|
||||
'/assets/qingxiang/emb_p7_5.png',
|
||||
'/assets/qingxiang/emb_p6_8.png',
|
||||
'/assets/qingxiang/emb_p7_4.png',
|
||||
'/assets/qingxiang/emb_p12_2.png',
|
||||
'/assets/qingxiang/emb_p7_6.png',
|
||||
'/assets/qingxiang/emb_p6_2.png',
|
||||
'/assets/t30-bottle.png',
|
||||
assetUrl('/assets/qingxiang/emb_p7_5.png'),
|
||||
assetUrl('/assets/qingxiang/emb_p6_8.png'),
|
||||
assetUrl('/assets/qingxiang/emb_p7_4.png'),
|
||||
assetUrl('/assets/qingxiang/emb_p12_2.png'),
|
||||
assetUrl('/assets/qingxiang/emb_p7_6.png'),
|
||||
assetUrl('/assets/qingxiang/emb_p6_2.png'),
|
||||
assetUrl('/assets/t30-bottle.png'),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
+18
-16
@@ -302,27 +302,29 @@ export const moduleEntries = [
|
||||
{ to: '/contact', label: '联系我们', desc: '扫码对接负责人' },
|
||||
] as const
|
||||
|
||||
/** Pure visuals only (no PDF page screenshots with baked-in text) */
|
||||
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: '/assets/logo.png',
|
||||
qr: '/assets/qr-contact.png',
|
||||
t30: '/assets/t30-bottle.png',
|
||||
logo: assetUrl('/assets/logo.png'),
|
||||
qr: assetUrl('/assets/qr-contact.png'),
|
||||
t30: assetUrl('/assets/t30-bottle.png'),
|
||||
brand: {
|
||||
manor: '/assets/brand/emb_p6_5.png',
|
||||
monument: '/assets/brand/emb_p31_1.png',
|
||||
landscape: '/assets/brand/emb_p15_3.png',
|
||||
relic: '/assets/brand/emb_p33_1.png',
|
||||
manor: assetUrl('/assets/brand/emb_p6_5.png'),
|
||||
monument: assetUrl('/assets/brand/emb_p31_1.png'),
|
||||
landscape: assetUrl('/assets/brand/emb_p15_3.png'),
|
||||
relic: assetUrl('/assets/brand/emb_p33_1.png'),
|
||||
},
|
||||
poetry: {
|
||||
inkA: '/assets/poetry/emb_p33_4.png',
|
||||
inkB: '/assets/poetry/emb_p49_4.png',
|
||||
inkC: '/assets/poetry/emb_p59_4.png',
|
||||
inkD: '/assets/poetry/emb_p67_4.png',
|
||||
inkA: assetUrl('/assets/poetry/emb_p33_4.png'),
|
||||
inkB: assetUrl('/assets/poetry/emb_p49_4.png'),
|
||||
inkC: assetUrl('/assets/poetry/emb_p59_4.png'),
|
||||
inkD: assetUrl('/assets/poetry/emb_p67_4.png'),
|
||||
},
|
||||
qingxiang: {
|
||||
hall: '/assets/qingxiang/emb_p7_4.png',
|
||||
interior: '/assets/qingxiang/emb_p6_2.png',
|
||||
storefront: '/assets/qingxiang/emb_p7_6.png',
|
||||
scene: '/assets/qingxiang/emb_p12_2.png',
|
||||
hall: assetUrl('/assets/qingxiang/emb_p7_4.png'),
|
||||
interior: assetUrl('/assets/qingxiang/emb_p6_2.png'),
|
||||
storefront: assetUrl('/assets/qingxiang/emb_p7_6.png'),
|
||||
scene: assetUrl('/assets/qingxiang/emb_p12_2.png'),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Prefix local static paths with OSS/CDN base when VITE_OSS_BASE is set.
|
||||
* Example: VITE_OSS_BASE=https://dukang-prod.oss-cn-hangzhou.aliyuncs.com/dukang_office
|
||||
* /assets/logo.png → https://.../dukang_office/assets/logo.png
|
||||
* Leave unset in local dev to keep serving from /assets on the Vite host.
|
||||
*/
|
||||
const OSS_BASE = (import.meta.env.VITE_OSS_BASE as string | undefined)?.replace(/\/$/, '') ?? ''
|
||||
|
||||
export function assetUrl(path: string): string {
|
||||
const p = path.startsWith('/') ? path : `/${path}`
|
||||
return OSS_BASE ? `${OSS_BASE}${p}` : p
|
||||
}
|
||||
|
||||
export function assetPair(pair: { thumb: string; full: string }): { thumb: string; full: string } {
|
||||
return {
|
||||
thumb: assetUrl(pair.thumb),
|
||||
full: assetUrl(pair.full),
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
defaultIntervals,
|
||||
type RemoteToHost,
|
||||
} from './remote/protocol'
|
||||
import { assetUrl } from '../lib/assetUrl'
|
||||
import './StageShell.css'
|
||||
|
||||
const BGM_SRC = '/assets/stage/bgm.wav'
|
||||
const BGM_SRC = assetUrl('/assets/stage/bgm.wav')
|
||||
const BGM_START = 4
|
||||
const BGM_END = 35
|
||||
|
||||
@@ -32,7 +33,7 @@ export function StageShell() {
|
||||
const bgmRef = useRef<HTMLAudioElement | null>(null)
|
||||
const audioWantedRef = useRef(true)
|
||||
const [isAutoPlay, setIsAutoPlay] = useState(true)
|
||||
const [audioPlaying, setAudioPlaying] = useState(true)
|
||||
const [audioPlaying, setAudioPlaying] = useState(false)
|
||||
const [remoteOpen, setRemoteOpen] = useState(false)
|
||||
const [intervals, setIntervals] = useState(() => defaultIntervals(stageNavItems.length))
|
||||
|
||||
@@ -89,20 +90,12 @@ export function StageShell() {
|
||||
audio.currentTime = BGM_START
|
||||
void audio.play().catch(() => setAudioPlaying(false))
|
||||
}
|
||||
// Retry after user gesture when autoplay policy blocked start (not after intentional pause)
|
||||
const onGesture = () => {
|
||||
if (audioWantedRef.current && audio.paused) void playBgm()
|
||||
}
|
||||
audio.addEventListener('timeupdate', onTimeUpdate)
|
||||
audio.addEventListener('ended', onEnded)
|
||||
window.addEventListener('pointerdown', onGesture)
|
||||
window.addEventListener('keydown', onGesture)
|
||||
void playBgm()
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', onTimeUpdate)
|
||||
audio.removeEventListener('ended', onEnded)
|
||||
window.removeEventListener('pointerdown', onGesture)
|
||||
window.removeEventListener('keydown', onGesture)
|
||||
audio.pause()
|
||||
bgmRef.current = null
|
||||
}
|
||||
@@ -274,7 +267,10 @@ export function StageShell() {
|
||||
<button
|
||||
type="button"
|
||||
className={`stage-autoplay-btn ${audioPlaying ? 'is-playing' : ''}`}
|
||||
onClick={() => setBgmPlaying(!audioPlaying)}
|
||||
onClick={() => {
|
||||
const paused = bgmRef.current?.paused ?? true
|
||||
setBgmPlaying(paused)
|
||||
}}
|
||||
title={audioPlaying ? '点击暂停音乐' : '点击播放音乐'}
|
||||
>
|
||||
<span className="stage-play-icon">{audioPlaying ? '♪' : '♫'}</span>
|
||||
|
||||
+11
-20
@@ -1,28 +1,19 @@
|
||||
import { assetUrl } from '../lib/assetUrl'
|
||||
|
||||
export type StageImagePair = { thumb: string; full: string }
|
||||
|
||||
function pair(thumb: string, full: string): StageImagePair {
|
||||
return { thumb: assetUrl(thumb), full: assetUrl(full) }
|
||||
}
|
||||
|
||||
export const stageImages = {
|
||||
logo: {
|
||||
thumb: '/assets/stage/thumb/logo.webp',
|
||||
full: '/assets/stage/full/logo.webp',
|
||||
},
|
||||
t30: {
|
||||
thumb: '/assets/stage/thumb/t30-bottle.webp',
|
||||
full: '/assets/stage/full/t30-bottle.webp',
|
||||
},
|
||||
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'),
|
||||
brand: {
|
||||
manor: {
|
||||
thumb: '/assets/stage/thumb/emb_p6_5.webp',
|
||||
full: '/assets/stage/full/emb_p6_5.webp',
|
||||
},
|
||||
relic: {
|
||||
thumb: '/assets/stage/thumb/emb_p33_1.webp',
|
||||
full: '/assets/stage/full/emb_p33_1.webp',
|
||||
},
|
||||
},
|
||||
qr: {
|
||||
thumb: '/assets/stage/thumb/qr-contact.webp',
|
||||
full: '/assets/stage/full/qr-contact.webp',
|
||||
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'),
|
||||
},
|
||||
qr: pair('/assets/stage/thumb/qr-contact.webp', '/assets/stage/full/qr-contact.webp'),
|
||||
} as const
|
||||
|
||||
/** Flat list of all stage image pairs for preload helpers */
|
||||
|
||||
Vendored
+8
@@ -1 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_OSS_BASE?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user