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
|
dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.local
|
*.local
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
_tmp_*.txt
|
_tmp_*.txt
|
||||||
.vite
|
.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" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/assets/logo.png" />
|
<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="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/" />
|
<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:url" content="https://www.dukanghaoke.com/" />
|
||||||
<meta property="og:title" content="杜康好客 · 酒祖杜康" />
|
<meta property="og:title" content="杜康好客官网|酒祖杜康清香 · 你请客我买单" />
|
||||||
<meta property="og:description" content="杜康好客——你请客,我买单。酒祖杜康清香,买酒送等价好客权益。" />
|
<meta
|
||||||
<title>杜康好客 · 酒祖杜康</title>
|
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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+1017
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -5,9 +5,11 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build && node scripts/prerender-routes.mjs",
|
||||||
"preview": "vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"peerjs": "^1.5.5",
|
"peerjs": "^1.5.5",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"ali-oss": "^6.23.0",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.3.5"
|
"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 = {
|
export const deepImages = {
|
||||||
brand: [
|
brand: [
|
||||||
'/assets/brand/emb_p6_5.png',
|
assetUrl('/assets/brand/emb_p6_5.png'),
|
||||||
'/assets/brand/emb_p31_1.png',
|
assetUrl('/assets/brand/emb_p31_1.png'),
|
||||||
'/assets/brand/emb_p15_3.png',
|
assetUrl('/assets/brand/emb_p15_3.png'),
|
||||||
'/assets/brand/emb_p33_1.png',
|
assetUrl('/assets/brand/emb_p33_1.png'),
|
||||||
'/assets/brand/photo_p11_1.jpg',
|
assetUrl('/assets/brand/photo_p11_1.jpg'),
|
||||||
'/assets/brand/photo_p8_4.jpg',
|
assetUrl('/assets/brand/photo_p8_4.jpg'),
|
||||||
],
|
],
|
||||||
poetry: [
|
poetry: [
|
||||||
'/assets/poetry/emb_p33_4.png',
|
assetUrl('/assets/poetry/emb_p33_4.png'),
|
||||||
'/assets/poetry/emb_p49_4.png',
|
assetUrl('/assets/poetry/emb_p49_4.png'),
|
||||||
'/assets/poetry/emb_p59_4.png',
|
assetUrl('/assets/poetry/emb_p59_4.png'),
|
||||||
'/assets/poetry/emb_p67_4.png',
|
assetUrl('/assets/poetry/emb_p67_4.png'),
|
||||||
'/assets/poetry/emb_p25_4.png',
|
assetUrl('/assets/poetry/emb_p25_4.png'),
|
||||||
'/assets/poetry/emb_p66_4.png',
|
assetUrl('/assets/poetry/emb_p66_4.png'),
|
||||||
],
|
],
|
||||||
qingxiang: [
|
qingxiang: [
|
||||||
'/assets/qingxiang/emb_p7_5.png',
|
assetUrl('/assets/qingxiang/emb_p7_5.png'),
|
||||||
'/assets/qingxiang/emb_p6_8.png',
|
assetUrl('/assets/qingxiang/emb_p6_8.png'),
|
||||||
'/assets/qingxiang/emb_p7_4.png',
|
assetUrl('/assets/qingxiang/emb_p7_4.png'),
|
||||||
'/assets/qingxiang/emb_p12_2.png',
|
assetUrl('/assets/qingxiang/emb_p12_2.png'),
|
||||||
'/assets/qingxiang/emb_p7_6.png',
|
assetUrl('/assets/qingxiang/emb_p7_6.png'),
|
||||||
'/assets/qingxiang/emb_p6_2.png',
|
assetUrl('/assets/qingxiang/emb_p6_2.png'),
|
||||||
'/assets/t30-bottle.png',
|
assetUrl('/assets/t30-bottle.png'),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -302,27 +302,29 @@ export const moduleEntries = [
|
|||||||
{ to: '/contact', label: '联系我们', desc: '扫码对接负责人' },
|
{ to: '/contact', label: '联系我们', desc: '扫码对接负责人' },
|
||||||
] as const
|
] 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 = {
|
export const images = {
|
||||||
logo: '/assets/logo.png',
|
logo: assetUrl('/assets/logo.png'),
|
||||||
qr: '/assets/qr-contact.png',
|
qr: assetUrl('/assets/qr-contact.png'),
|
||||||
t30: '/assets/t30-bottle.png',
|
t30: assetUrl('/assets/t30-bottle.png'),
|
||||||
brand: {
|
brand: {
|
||||||
manor: '/assets/brand/emb_p6_5.png',
|
manor: assetUrl('/assets/brand/emb_p6_5.png'),
|
||||||
monument: '/assets/brand/emb_p31_1.png',
|
monument: assetUrl('/assets/brand/emb_p31_1.png'),
|
||||||
landscape: '/assets/brand/emb_p15_3.png',
|
landscape: assetUrl('/assets/brand/emb_p15_3.png'),
|
||||||
relic: '/assets/brand/emb_p33_1.png',
|
relic: assetUrl('/assets/brand/emb_p33_1.png'),
|
||||||
},
|
},
|
||||||
poetry: {
|
poetry: {
|
||||||
inkA: '/assets/poetry/emb_p33_4.png',
|
inkA: assetUrl('/assets/poetry/emb_p33_4.png'),
|
||||||
inkB: '/assets/poetry/emb_p49_4.png',
|
inkB: assetUrl('/assets/poetry/emb_p49_4.png'),
|
||||||
inkC: '/assets/poetry/emb_p59_4.png',
|
inkC: assetUrl('/assets/poetry/emb_p59_4.png'),
|
||||||
inkD: '/assets/poetry/emb_p67_4.png',
|
inkD: assetUrl('/assets/poetry/emb_p67_4.png'),
|
||||||
},
|
},
|
||||||
qingxiang: {
|
qingxiang: {
|
||||||
hall: '/assets/qingxiang/emb_p7_4.png',
|
hall: assetUrl('/assets/qingxiang/emb_p7_4.png'),
|
||||||
interior: '/assets/qingxiang/emb_p6_2.png',
|
interior: assetUrl('/assets/qingxiang/emb_p6_2.png'),
|
||||||
storefront: '/assets/qingxiang/emb_p7_6.png',
|
storefront: assetUrl('/assets/qingxiang/emb_p7_6.png'),
|
||||||
scene: '/assets/qingxiang/emb_p12_2.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,
|
defaultIntervals,
|
||||||
type RemoteToHost,
|
type RemoteToHost,
|
||||||
} from './remote/protocol'
|
} from './remote/protocol'
|
||||||
|
import { assetUrl } from '../lib/assetUrl'
|
||||||
import './StageShell.css'
|
import './StageShell.css'
|
||||||
|
|
||||||
const BGM_SRC = '/assets/stage/bgm.wav'
|
const BGM_SRC = assetUrl('/assets/stage/bgm.wav')
|
||||||
const BGM_START = 4
|
const BGM_START = 4
|
||||||
const BGM_END = 35
|
const BGM_END = 35
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export function StageShell() {
|
|||||||
const bgmRef = useRef<HTMLAudioElement | null>(null)
|
const bgmRef = useRef<HTMLAudioElement | null>(null)
|
||||||
const audioWantedRef = useRef(true)
|
const audioWantedRef = useRef(true)
|
||||||
const [isAutoPlay, setIsAutoPlay] = useState(true)
|
const [isAutoPlay, setIsAutoPlay] = useState(true)
|
||||||
const [audioPlaying, setAudioPlaying] = useState(true)
|
const [audioPlaying, setAudioPlaying] = useState(false)
|
||||||
const [remoteOpen, setRemoteOpen] = useState(false)
|
const [remoteOpen, setRemoteOpen] = useState(false)
|
||||||
const [intervals, setIntervals] = useState(() => defaultIntervals(stageNavItems.length))
|
const [intervals, setIntervals] = useState(() => defaultIntervals(stageNavItems.length))
|
||||||
|
|
||||||
@@ -89,20 +90,12 @@ export function StageShell() {
|
|||||||
audio.currentTime = BGM_START
|
audio.currentTime = BGM_START
|
||||||
void audio.play().catch(() => setAudioPlaying(false))
|
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('timeupdate', onTimeUpdate)
|
||||||
audio.addEventListener('ended', onEnded)
|
audio.addEventListener('ended', onEnded)
|
||||||
window.addEventListener('pointerdown', onGesture)
|
|
||||||
window.addEventListener('keydown', onGesture)
|
|
||||||
void playBgm()
|
void playBgm()
|
||||||
return () => {
|
return () => {
|
||||||
audio.removeEventListener('timeupdate', onTimeUpdate)
|
audio.removeEventListener('timeupdate', onTimeUpdate)
|
||||||
audio.removeEventListener('ended', onEnded)
|
audio.removeEventListener('ended', onEnded)
|
||||||
window.removeEventListener('pointerdown', onGesture)
|
|
||||||
window.removeEventListener('keydown', onGesture)
|
|
||||||
audio.pause()
|
audio.pause()
|
||||||
bgmRef.current = null
|
bgmRef.current = null
|
||||||
}
|
}
|
||||||
@@ -274,7 +267,10 @@ export function StageShell() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`stage-autoplay-btn ${audioPlaying ? 'is-playing' : ''}`}
|
className={`stage-autoplay-btn ${audioPlaying ? 'is-playing' : ''}`}
|
||||||
onClick={() => setBgmPlaying(!audioPlaying)}
|
onClick={() => {
|
||||||
|
const paused = bgmRef.current?.paused ?? true
|
||||||
|
setBgmPlaying(paused)
|
||||||
|
}}
|
||||||
title={audioPlaying ? '点击暂停音乐' : '点击播放音乐'}
|
title={audioPlaying ? '点击暂停音乐' : '点击播放音乐'}
|
||||||
>
|
>
|
||||||
<span className="stage-play-icon">{audioPlaying ? '♪' : '♫'}</span>
|
<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 }
|
export type StageImagePair = { thumb: string; full: string }
|
||||||
|
|
||||||
|
function pair(thumb: string, full: string): StageImagePair {
|
||||||
|
return { thumb: assetUrl(thumb), full: assetUrl(full) }
|
||||||
|
}
|
||||||
|
|
||||||
export const stageImages = {
|
export const stageImages = {
|
||||||
logo: {
|
logo: pair('/assets/stage/thumb/logo.webp', '/assets/stage/full/logo.webp'),
|
||||||
thumb: '/assets/stage/thumb/logo.webp',
|
t30: pair('/assets/stage/thumb/t30-bottle.webp', '/assets/stage/full/t30-bottle.webp'),
|
||||||
full: '/assets/stage/full/logo.webp',
|
|
||||||
},
|
|
||||||
t30: {
|
|
||||||
thumb: '/assets/stage/thumb/t30-bottle.webp',
|
|
||||||
full: '/assets/stage/full/t30-bottle.webp',
|
|
||||||
},
|
|
||||||
brand: {
|
brand: {
|
||||||
manor: {
|
manor: pair('/assets/stage/thumb/emb_p6_5.webp', '/assets/stage/full/emb_p6_5.webp'),
|
||||||
thumb: '/assets/stage/thumb/emb_p6_5.webp',
|
relic: pair('/assets/stage/thumb/emb_p33_1.webp', '/assets/stage/full/emb_p33_1.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',
|
|
||||||
},
|
},
|
||||||
|
qr: pair('/assets/stage/thumb/qr-contact.webp', '/assets/stage/full/qr-contact.webp'),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Flat list of all stage image pairs for preload helpers */
|
/** Flat list of all stage image pairs for preload helpers */
|
||||||
|
|||||||
Vendored
+8
@@ -1 +1,9 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_OSS_BASE?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user