fix:提交更新

This commit is contained in:
ljy
2026-08-26 00:44:25 +08:00
parent 83c52614be
commit b822d21eae
20 changed files with 1491 additions and 72 deletions
+3
View File
@@ -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
+19
View File
@@ -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=
+3
View File
@@ -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
+3
View File
@@ -2,5 +2,8 @@ node_modules
dist
.DS_Store
*.local
.env
.env.local
.env.*.local
_tmp_*.txt
.vite
+40
View File
@@ -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
+37
View File
@@ -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
View File
@@ -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>
+1017
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -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"
+24
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://www.dukanghaoke.com/sitemap.xml
+58
View File
@@ -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>
+37
View File
@@ -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.')
+94
View File
@@ -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
View File
@@ -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
View File
@@ -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'),
},
}
+19
View File
@@ -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),
}
}
+7 -11
View File
@@ -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
View File
@@ -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 */
+8
View File
@@ -1 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_OSS_BASE?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}