diff --git a/.env.example b/.env.example
index b75a79b..099a81d 100644
--- a/.env.example
+++ b/.env.example
@@ -19,7 +19,7 @@ TOS_REGION="cn-beijing"
TOS_BUCKET="cyber-star"
TOS_ACCESS_KEY="CHANGE_ME"
TOS_SECRET_KEY="CHANGE_ME"
-NEXT_PUBLIC_TOS_DOMAIN="https://cyber-star.tos-cn-beijing.volces.com"
+NEXT_PUBLIC_TOS_DOMAIN="https://cyberstar.tos-cn-shanghai.volces.com/cyber-star"
# ── Auth.js 鉴权 ──
# 用 `openssl rand -base64 32` 生成
diff --git a/next.config.ts b/next.config.ts
index 3020ec5..42227ca 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -5,6 +5,14 @@ const nextConfig: NextConfig = {
devIndicators: false,
// 容器化部署:产出精简的 standalone 包(node server.js 启动)
output: "standalone",
+ // next/image 远程域名白名单:火山 TOS 桶 + 后续 CDN 域名
+ images: {
+ remotePatterns: [
+ { protocol: "https", hostname: "*.tos-cn-shanghai.volces.com" },
+ { protocol: "https", hostname: "*.tos-cn-beijing.volces.com" },
+ { protocol: "https", hostname: "*.volccdn.com" },
+ ],
+ },
};
export default nextConfig;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index db875bc..20780a4 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -11,6 +11,7 @@ import { getActivityEndTime, sortArtists, type SortKey } from "@/lib/mock-data";
import { useVoteStore } from "@/lib/store";
import { useVoteAction } from "@/hooks/useVoteAction";
import { cn } from "@/lib/cn";
+import { tosUrl } from "@/lib/tos";
export default function Home() {
const artists = useVoteStore((s) => s.artists);
@@ -70,7 +71,7 @@ export default function Home() {
scrollMarginTop: "80px",
}}
>
-
+
{/* Top12 出道位 · 作为第二个 snap 点:滚动结束后自然落到这里,标题贴近顶部 */}
diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts
index 83ad92e..863945b 100644
--- a/src/lib/mock-data.ts
+++ b/src/lib/mock-data.ts
@@ -1,11 +1,12 @@
import type { Artist } from "@/types/artist";
import { ARTIST_SEEDS } from "./artist-bios";
+import { tosUrl } from "./tos";
/**
* 真实艺人数据装配层。
* 所有人物字段(姓名 / 性别 / 年龄 / 身高 / 性格 / 技能 / 赛道 / 口头禅 /
* 座右铭 / 长简介)来自《36 位虚拟艺人人物小传.docx》。
- * 立绘 / 三视图 / 氛围图 / solo 视频 来自 public/portraits/ 和 public/videos/artists/。
+ * 立绘 / 三视图 / 氛围图 / solo 视频 走火山 TOS 桶(webp + mp4 已压缩到 1/10)。
*
* 票数 / 排名是运行时计算(store 投票后会更新)。除此之外不再有任何虚构数据。
*/
@@ -18,9 +19,12 @@ const MISSING_ATMOSPHERE_3: ReadonlySet = new Set(["036"]);
/** 画廊 = 三张氛围图(1/2/3)。不包含三视图,因为长宽比与卡片不一致。 */
function buildGallery(no: string): string[] {
- const items = [`/portraits/${no}.png`, `/portraits/${no}-2.png`];
+ const items = [
+ tosUrl(`portraits/${no}.webp`),
+ tosUrl(`portraits/${no}-2.webp`),
+ ];
if (!MISSING_ATMOSPHERE_3.has(no)) {
- items.push(`/portraits/${no}-3.png`);
+ items.push(tosUrl(`portraits/${no}-3.webp`));
}
return items;
}
@@ -35,12 +39,12 @@ function buildArtists(): Artist[] {
age: seed.age,
gender: seed.gender,
bio: seed.bio,
- portrait: `/portraits/${seed.no}.png`,
+ portrait: tosUrl(`portraits/${seed.no}.webp`),
avatar: "",
gallery: buildGallery(seed.no),
videoUrl: MISSING_VIDEO.has(seed.no)
? undefined
- : `/videos/artists/${seed.no}.mp4`,
+ : tosUrl(`videos/artists/${seed.no}.mp4`),
// 不设置 poster,由播放器运行时 seek 到 0.001s 渲染首帧作为封面
videoPoster: "",
tags: seed.tags,
diff --git a/src/lib/tos.ts b/src/lib/tos.ts
new file mode 100644
index 0000000..5409b46
--- /dev/null
+++ b/src/lib/tos.ts
@@ -0,0 +1,18 @@
+/**
+ * TOS 资源 URL 拼接工具
+ *
+ * 用法:
+ * tosUrl("portraits/001.webp")
+ * → https://cyberstar.tos-cn-shanghai.volces.com/cyber-star/portraits/001.webp
+ *
+ * 环境变量 NEXT_PUBLIC_TOS_DOMAIN 配置:
+ * .env.local / .env.production → 完整的桶 + 路径前缀 (含 scheme, 不含末尾 /)
+ * 未设置时 fallback 到相对路径 (/path/...), 适合本地用 public/ 静态文件托管的场景。
+ */
+const TOS_BASE = (process.env.NEXT_PUBLIC_TOS_DOMAIN ?? "").replace(/\/+$/, "");
+
+export function tosUrl(path: string): string {
+ const clean = path.replace(/^\/+/, "");
+ if (!TOS_BASE) return `/${clean}`;
+ return `${TOS_BASE}/${clean}`;
+}