diff --git a/scripts/transcode-promotion-videos.ps1 b/scripts/transcode-promotion-videos.ps1 new file mode 100644 index 0000000..8c4dfe3 --- /dev/null +++ b/scripts/transcode-promotion-videos.ps1 @@ -0,0 +1,40 @@ +# 将 web\promotion\social 下产品视频从 .mov 转为网页通用 .mp4(H.264 + AAC,faststart) +# 依赖:已安装 ffmpeg 并在 PATH 中(Windows: https://www.gyan.dev/ffmpeg/builds/ 或 winget install ffmpeg) + +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $PSScriptRoot +$Dir = Join-Path $Root "web\promotion\social" + +$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue +if (-not $ffmpeg) { + Write-Host "未找到 ffmpeg。请安装并加入 PATH:https://ffmpeg.org/download.html" -ForegroundColor Red + exit 1 +} + +$files = @( + "video-calc-demo-1.mov", + "video-calc-demo-2.mov", + "video-aiword.mov", + "video-voice-office.mov", + "video-invoice-ai.mov" +) + +foreach ($f in $files) { + $src = Join-Path $Dir $f + $base = [System.IO.Path]::GetFileNameWithoutExtension($f) + $dst = Join-Path $Dir "$base.mp4" + if (-not (Test-Path -LiteralPath $src)) { + Write-Host "[跳过] 无源文件: $src" -ForegroundColor Yellow + continue + } + Write-Host "[转码] $src -> $dst" + & ffmpeg -y -i $src ` + -c:v libx264 -profile:v high -pix_fmt yuv420p ` + -c:a aac -b:a 128k ` + -movflags +faststart ` + $dst + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "[完成] $dst" -ForegroundColor Green +} + +Write-Host "全部处理结束。请部署生成的 .mp4;确认后可删除本地 .mov(可选)。" -ForegroundColor Cyan diff --git a/scripts/transcode-promotion-videos.sh b/scripts/transcode-promotion-videos.sh new file mode 100644 index 0000000..ca00d8d --- /dev/null +++ b/scripts/transcode-promotion-videos.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# 将 web/promotion/social 下产品视频从 .mov 转为网页通用 .mp4(H.264 + AAC,faststart) +# 依赖:已安装 ffmpeg(macOS: brew install ffmpeg;Ubuntu: apt install ffmpeg) +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DIR="$ROOT/web/promotion/social" + +if ! command -v ffmpeg >/dev/null 2>&1; then + echo "未找到 ffmpeg,请先安装:https://ffmpeg.org/download.html" + exit 1 +fi + +# 与 web/src/data/promotionVideos.js 中 relVideo 基名一致(输出为同名 .mp4) +FILES=( + "video-calc-demo-1.mov" + "video-calc-demo-2.mov" + "video-aiword.mov" + "video-voice-office.mov" + "video-invoice-ai.mov" +) + +for f in "${FILES[@]}"; do + src="$DIR/$f" + base="${f%.mov}" + dst="$DIR/${base}.mp4" + if [[ ! -f "$src" ]]; then + echo "[跳过] 无源文件: $src" + continue + fi + echo "[转码] $src -> $dst" + ffmpeg -y -i "$src" \ + -c:v libx264 -profile:v high -pix_fmt yuv420p \ + -c:a aac -b:a 128k \ + -movflags +faststart \ + "$dst" + echo "[完成] $dst" +done + +echo "全部处理结束。请部署生成的 .mp4;确认后可删除本地 .mov 以减小体积(可选)。" diff --git a/server/.env.example b/server/.env.example index 217bf13..42acb80 100644 --- a/server/.env.example +++ b/server/.env.example @@ -9,6 +9,9 @@ GIN_MODE=release # 对外域名(CORS、日志),与 nginx 反代域名一致(官网 https://yuheng.yuxindazhineng.com/) ALLOWED_ORIGINS=https://yuheng.yuxindazhineng.com +# 设为 1 时跳过 uploads 下 promotion 视频的 .mov→.mp4 转码(无 ffmpeg 时可临时关闭) +# SKIP_PROMOTION_TRANSCODE=1 + # 部署时自动导入「视频发布」到 data/uploads + site_assets(compose up 后执行) # 官网站点 MongoDB _id;pull-and-restart 会把本文件里「.env 缺失的键」自动追加到 server/.env,一般无需手改 YH_IMPORT_PROMOTION_SITE_ID=69ba1f1f41aeb82acfd609ef diff --git a/server/Dockerfile b/server/Dockerfile index 0c3ad84..7e3e105 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -13,7 +13,8 @@ RUN CGO_ENABLED=0 go build -mod=vendor -o /app/server . ARG REGISTRY_MIRROR=docker.m.daocloud.io/library/ FROM ${REGISTRY_MIRROR}alpine:3.19 WORKDIR /app -RUN apk add --no-cache ca-certificates tzdata +# 产品视频 .mov → .mp4 服务端转码(handlers/promotion_transcode.go) +RUN apk add --no-cache ca-certificates tzdata ffmpeg ENV TZ=Asia/Shanghai COPY --from=builder /app/server . EXPOSE 8088 diff --git a/server/Dockerfile.run b/server/Dockerfile.run index 3ea7551..78dd095 100644 --- a/server/Dockerfile.run +++ b/server/Dockerfile.run @@ -1,7 +1,8 @@ # 仅运行时:不包含二进制,启动时挂载 deploy/api 到 /app,/app/server 由宿主机构建 ARG REGISTRY_MIRROR=docker.m.daocloud.io/library/ FROM ${REGISTRY_MIRROR}alpine:3.19 -RUN apk add --no-cache ca-certificates tzdata +# 与编译镜像一致:挂载的二进制需在容器内调用 ffmpeg 做推广视频转码 +RUN apk add --no-cache ca-certificates tzdata ffmpeg ENV TZ=Asia/Shanghai WORKDIR /app EXPOSE 8088 diff --git a/server/README.md b/server/README.md index 90f364d..d7655aa 100644 --- a/server/README.md +++ b/server/README.md @@ -22,3 +22,8 @@ go run main.go ``` 默认端口 8080 + +## 推广视频转码(promotion 目录) + +上传到 `sites/{site_id}/promotion/**.mov` 后,服务会异步转 **MP4**(需本机安装 **ffmpeg**,与 Docker 镜像一致)。启动时也会扫描遗留 `.mov` 补转码。详见 `handlers/promotion_transcode.go`。 +关闭:`SKIP_PROMOTION_TRANSCODE=1`。 diff --git a/server/handlers/module_upload.go b/server/handlers/module_upload.go index a18f604..cddb344 100644 --- a/server/handlers/module_upload.go +++ b/server/handlers/module_upload.go @@ -306,6 +306,8 @@ func UploadSiteAsset(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "file_path": doc.FilePath, "message": "上传成功"}) + // promotion 目录下 .mov 由服务端异步转 .mp4 并更新本条资源(需容器/主机安装 ffmpeg) + ScheduleTranscodeAfterUpload(siteID, relPath, destPath, res.InsertedID) } // DeleteSiteAsset 删除站点资源 diff --git a/server/handlers/promotion_transcode.go b/server/handlers/promotion_transcode.go new file mode 100644 index 0000000..386b0da --- /dev/null +++ b/server/handlers/promotion_transcode.go @@ -0,0 +1,197 @@ +package handlers + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + + "yh_web/server/config" +) + +func skipPromotionTranscode() bool { + v := strings.TrimSpace(os.Getenv("SKIP_PROMOTION_TRANSCODE")) + return v == "1" || strings.EqualFold(v, "true") +} + +func ffmpegAvailable() bool { + _, err := exec.LookPath("ffmpeg") + return err == nil +} + +func isMOVUnderPromotion(relPath string, ext string) bool { + if strings.ToLower(ext) != ".mov" { + return false + } + return strings.Contains(filepath.ToSlash(relPath), "/promotion/") +} + +func mp4PathForMOV(movPath string) string { + return strings.TrimSuffix(movPath, filepath.Ext(movPath)) + ".mp4" +} + +func needsTranscode(movPath, mp4Path string) bool { + mi, err1 := os.Stat(movPath) + if err1 != nil || mi.IsDir() { + return false + } + pi, err2 := os.Stat(mp4Path) + if err2 != nil { + return true + } + return mi.ModTime().After(pi.ModTime()) +} + +// runFFmpegMOVToMP4 将 mov 转为浏览器通用 mp4(与前端/脚本参数一致) +func runFFmpegMOVToMP4(ctx context.Context, movPath, mp4Path string) error { + cmd := exec.CommandContext(ctx, "ffmpeg", + "-y", "-i", movPath, + "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + mp4Path, + ) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, string(out)) + } + return nil +} + +func relPathFromUploadRoot(uploadRoot, fullPath string) (string, error) { + r, err := filepath.Rel(uploadRoot, fullPath) + if err != nil { + return "", err + } + return filepath.ToSlash(r), nil +} + +// replaceMOVWithMP4InDB 将 site_assets 中对应 .mov 记录更新为 .mp4(转码成功后调用) +func replaceMOVWithMP4InDB(siteID, oldRelPath, mp4FullPath string, insertedID any) { + if config.MongoClient == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + coll := config.GetDB(config.DBName).Collection("site_assets") + + newRel := strings.TrimSuffix(oldRelPath, filepath.Ext(oldRelPath)) + ".mp4" + fi, err := os.Stat(mp4FullPath) + if err != nil { + log.Printf("[promotion-transcode] stat mp4: %v", err) + return + } + + set := bson.M{ + "file_path": newRel, + "name": filepath.Base(newRel), + "size": fi.Size(), + "content_type": "video/mp4", + } + + filter := bson.M{"site_id": siteID, "file_path": oldRelPath} + if oid, ok := insertedID.(bson.ObjectID); ok && !oid.IsZero() { + filter = bson.M{"_id": oid, "site_id": siteID} + } + _, err = coll.UpdateOne(ctx, filter, bson.M{"$set": set}) + if err != nil { + log.Printf("[promotion-transcode] 更新数据库失败: %v", err) + } +} + +// ScheduleTranscodeAfterUpload 上传保存成功后异步:promotion 下 .mov -> .mp4,并更新本条 site_assets +func ScheduleTranscodeAfterUpload(siteID, relPath, movFullPath string, insertedID any) { + if skipPromotionTranscode() || !isMOVUnderPromotion(relPath, filepath.Ext(movFullPath)) { + return + } + go func() { + if !ffmpegAvailable() { + log.Printf("[promotion-transcode] 已上传 .mov 但未安装 ffmpeg,无法转码: %s", relPath) + return + } + mp4Full := mp4PathForMOV(movFullPath) + if !needsTranscode(movFullPath, mp4Full) { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) + defer cancel() + log.Printf("[promotion-transcode] 开始转码: %s -> %s", movFullPath, mp4Full) + if err := runFFmpegMOVToMP4(ctx, movFullPath, mp4Full); err != nil { + log.Printf("[promotion-transcode] 转码失败 %s: %v", relPath, err) + return + } + if err := os.Remove(movFullPath); err != nil { + log.Printf("[promotion-transcode] 删除原 .mov 失败(可手动删): %v", err) + } + replaceMOVWithMP4InDB(siteID, relPath, mp4Full, insertedID) + log.Printf("[promotion-transcode] 完成: %s", newRelLog(relPath)) + }() +} + +func newRelLog(oldRel string) string { + return strings.TrimSuffix(oldRel, filepath.Ext(oldRel)) + ".mp4" +} + +// SweepPromotionTranscodeOnStartup 扫描 uploads/sites/*/promotion/**.mov,补转码并同步数据库(已有文件) +func SweepPromotionTranscodeOnStartup() { + time.Sleep(3 * time.Second) + if skipPromotionTranscode() { + log.Println("[promotion-transcode] 启动扫描已跳过 SKIP_PROMOTION_TRANSCODE=1") + return + } + if !ffmpegAvailable() { + log.Println("[promotion-transcode] 启动扫描跳过:未找到 ffmpeg(安装后可重启服务)") + return + } + root := getUploadDir() + sitesDir := filepath.Join(root, "sites") + fi, err := os.Stat(sitesDir) + if err != nil || !fi.IsDir() { + return + } + entries, err := os.ReadDir(sitesDir) + if err != nil { + return + } + for _, e := range entries { + if !e.IsDir() { + continue + } + siteID := e.Name() + promoRoot := filepath.Join(sitesDir, siteID, "promotion") + _ = filepath.WalkDir(promoRoot, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + if strings.ToLower(filepath.Ext(path)) != ".mov" { + return nil + } + mp4Full := mp4PathForMOV(path) + if !needsTranscode(path, mp4Full) { + return nil + } + rel, err := relPathFromUploadRoot(root, path) + if err != nil { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) + log.Printf("[promotion-transcode] [启动补转] %s", rel) + err = runFFmpegMOVToMP4(ctx, path, mp4Full) + cancel() + if err != nil { + log.Printf("[promotion-transcode] [启动补转] 失败 %s: %v", rel, err) + return nil + } + _ = os.Remove(path) + replaceMOVWithMP4InDB(siteID, rel, mp4Full, nil) + log.Printf("[promotion-transcode] [启动补转] 完成 %s", newRelLog(rel)) + return nil + }) + } +} diff --git a/server/main.go b/server/main.go index 0e2ea04..64bc887 100644 --- a/server/main.go +++ b/server/main.go @@ -225,5 +225,9 @@ func main() { if port == "" { port = "8080" } + + // 启动后扫描 uploads 下各站点 promotion 中遗留的 .mov 并补转码(与上传后异步转码逻辑一致) + go handlers.SweepPromotionTranscodeOnStartup() + r.Run(":" + port) } diff --git a/web/package.json b/web/package.json index 6ab2936..5ca05f7 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "transcode": "node scripts/auto-transcode-promotion.mjs", "build": "vite build", "preview": "vite preview" }, diff --git a/web/promotion/social/README.md b/web/promotion/social/README.md index bd74c6c..b85540f 100644 --- a/web/promotion/social/README.md +++ b/web/promotion/social/README.md @@ -15,11 +15,28 @@ | 文件 | 说明 | |------|------| -| `video-calc-demo-1-cover.jpg` / `video-calc-demo-1.mov` | 操作与计算(一) | -| `video-calc-demo-2-cover.jpg` / `video-calc-demo-2.mov` | 操作与计算(二) | -| `video-aiword-cover.jpg` / `video-aiword.mov` | AI Word | -| `video-voice-office-cover.jpg` / `video-voice-office.mov` | 语音办公 | -| `video-invoice-ai-cover.jpg` / `video-invoice-ai.mov` | 办发票 | +| `video-calc-demo-1-cover.jpg` / `video-calc-demo-1.mp4` | 操作与计算(一) | +| `video-calc-demo-2-cover.jpg` / `video-calc-demo-2.mp4` | 操作与计算(二) | +| `video-aiword-cover.jpg` / `video-aiword.mp4` | AI Word | +| `video-voice-office-cover.jpg` / `video-voice-office.mp4` | 语音办公 | +| `video-invoice-ai-cover.jpg` / `video-invoice-ai.mp4` | 办发票 | + +### 从 .mov 转码为 .mp4(推荐) + +剪辑软件常导出 **.mov**,浏览器兼容性差;前台统一使用 **`.mp4`(H.264+AAC)**(见 `src/data/promotionVideos.js`)。 + +**服务端自动转码(推荐,线上)** +上传到 **`promotion/...`** 的 **`.mov`** 时,API 容器/主机需在 PATH 中有 **`ffmpeg`**(`server/Dockerfile`、`Dockerfile.run` 已 `apk add ffmpeg`)。服务会: + +1. **上传后**:异步将 `.mov` 转为同名 `.mp4`,删除原 `.mov`,并更新 `site_assets` 中路径与大小。 +2. **启动后约 3 秒**:扫描 `uploads/sites/*/promotion/**/*.mov`,对遗留文件补转码并写库。 + +环境变量 **`SKIP_PROMOTION_TRANSCODE=1`** 可关闭转码(无 ffmpeg 时避免报错日志)。 + +部署 API 镜像更新后请执行 **`docker compose build api`**(或等价命令),确保运行环境含 ffmpeg。 + +**仅静态站、无 API 时(本地可选)** +可在 `web/` 执行 `npm run transcode`(见 `scripts/auto-transcode-promotion.mjs`),或仓库根目录 `scripts/transcode-promotion-videos.sh` / `.ps1`。 线上访问示例:`https://你的域名/promotion/social/douyin.png` (须将 `web/promotion` 同步到 **`deploy/web/dist/promotion`**,见 `pull-and-restart.sh`。) diff --git a/web/scripts/auto-transcode-promotion.mjs b/web/scripts/auto-transcode-promotion.mjs new file mode 100644 index 0000000..5d78ec6 --- /dev/null +++ b/web/scripts/auto-transcode-promotion.mjs @@ -0,0 +1,113 @@ +/** + * 构建前自动:若 promotion/social 存在 .mov 且比对应 .mp4 新(或尚无 .mp4),则用 ffmpeg 转码。 + * - 无 .mov:直接通过 + * - 有 .mov 需更新但无 ffmpeg:失败(避免打出缺 mp4 的包);可设 SKIP_PROMOTION_TRANSCODE=1 跳过 + */ +import { existsSync, statSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import { spawnSync } from 'child_process' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SOCIAL = join(__dirname, '..', 'promotion', 'social') + +/** 与 src/data/promotionVideos.js 中视频基名一致(不含扩展名) */ +const VIDEO_BASES = [ + 'video-calc-demo-1', + 'video-calc-demo-2', + 'video-aiword', + 'video-voice-office', + 'video-invoice-ai' +] + +const FFMPEG_ARGS = (src, dst) => [ + '-y', + '-i', + src, + '-c:v', + 'libx264', + '-profile:v', + 'high', + '-pix_fmt', + 'yuv420p', + '-c:a', + 'aac', + '-b:a', + '128k', + '-movflags', + '+faststart', + dst +] + +function mtime(p) { + try { + return statSync(p).mtimeMs + } catch { + return 0 + } +} + +function needsTranscode(movPath, mp4Path) { + if (!existsSync(movPath)) return false + if (!existsSync(mp4Path)) return true + return mtime(movPath) > mtime(mp4Path) +} + +function ffmpegWorks() { + const r = spawnSync('ffmpeg', ['-hide_banner', '-version'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }) + return r.status === 0 +} + +function runTranscode(movPath, mp4Path) { + console.log(`[transcode] ${movPath} -> ${mp4Path}`) + const r = spawnSync('ffmpeg', FFMPEG_ARGS(movPath, mp4Path), { + stdio: 'inherit', + env: process.env + }) + return r.status === 0 +} + +function main() { + if (process.env.SKIP_PROMOTION_TRANSCODE === '1' || process.env.SKIP_PROMOTION_TRANSCODE === 'true') { + console.log('[transcode] 已跳过(SKIP_PROMOTION_TRANSCODE)') + process.exit(0) + } + + const todo = [] + for (const base of VIDEO_BASES) { + const movPath = join(SOCIAL, `${base}.mov`) + const mp4Path = join(SOCIAL, `${base}.mp4`) + if (needsTranscode(movPath, mp4Path)) { + todo.push({ movPath, mp4Path, base }) + } + } + + if (todo.length === 0) { + console.log('[transcode] 无需转码(无 .mov 或 .mp4 已最新)') + process.exit(0) + } + + if (!ffmpegWorks()) { + console.error( + '[transcode] 需要转码但未找到 ffmpeg。请安装并加入 PATH:https://ffmpeg.org/download.html\n' + + '或设置 SKIP_PROMOTION_TRANSCODE=1 跳过(将依赖已存在的 .mp4)' + ) + process.exit(1) + } + + for (const { movPath, mp4Path, base } of todo) { + if (!runTranscode(movPath, mp4Path)) { + console.error(`[transcode] 失败: ${base}`) + process.exit(1) + } + console.log(`[transcode] 完成: ${base}.mp4`) + } + + console.log('[transcode] 全部完成') + process.exit(0) +} + +main() diff --git a/web/src/components/HomeBrochurePreview.vue b/web/src/components/HomeBrochurePreview.vue new file mode 100644 index 0000000..96d7f96 --- /dev/null +++ b/web/src/components/HomeBrochurePreview.vue @@ -0,0 +1,466 @@ + + + + + diff --git a/web/src/data/promotionVideos.js b/web/src/data/promotionVideos.js index fd9bc6a..06ff166 100644 --- a/web/src/data/promotionVideos.js +++ b/web/src/data/promotionVideos.js @@ -5,7 +5,8 @@ const SOCIAL = 'social' /** * 产品视频与封面统一使用 promotion/social/ 下英文文件名(无空格、无中文路径),便于线上 URL 与 Nginx。 - * 静态:/promotion/social/xxx.mov;后台上传路径:promotion/social/ + 下列文件名,勾选「保留原文件名」。 + * 视频请使用 **.mp4(H.264+AAC)** 以保证各系统浏览器可播;剪辑导出多为 .mov 时,用仓库 `scripts/transcode-promotion-videos.*` 转码后再部署。 + * 静态:/promotion/social/xxx.mp4;后台上传路径:promotion/social/ + 下列文件名,勾选「保留原文件名」。 */ export const PROMOTION_VIDEOS_BASE = [ { @@ -13,35 +14,35 @@ export const PROMOTION_VIDEOS_BASE = [ title: '操作与计算软件实例(一)', desc: '宇恒一号宣传片', relCover: `${SOCIAL}/video-calc-demo-1-cover.jpg`, - relVideo: `${SOCIAL}/video-calc-demo-1.mov` + relVideo: `${SOCIAL}/video-calc-demo-1.mp4` }, { id: 'calc-demo-2', title: '操作与计算软件实例(二)', desc: '进阶操作与计算演示', relCover: `${SOCIAL}/video-calc-demo-2-cover.jpg`, - relVideo: `${SOCIAL}/video-calc-demo-2.mov` + relVideo: `${SOCIAL}/video-calc-demo-2.mp4` }, { id: 'aiword', title: '宇恒一号 AI Word 简介', desc: 'AI Word 能力介绍', relCover: `${SOCIAL}/video-aiword-cover.jpg`, - relVideo: `${SOCIAL}/video-aiword.mov` + relVideo: `${SOCIAL}/video-aiword.mp4` }, { id: 'voice', title: '语音办公实例', desc: '语音驱动办公流程', relCover: `${SOCIAL}/video-voice-office-cover.jpg`, - relVideo: `${SOCIAL}/video-voice-office.mov` + relVideo: `${SOCIAL}/video-voice-office.mp4` }, { id: 'invoice', title: 'AI 全自动办发票', desc: '发票场景自动化演示', relCover: `${SOCIAL}/video-invoice-ai-cover.jpg`, - relVideo: `${SOCIAL}/video-invoice-ai.mov` + relVideo: `${SOCIAL}/video-invoice-ai.mp4` } ] @@ -58,7 +59,7 @@ function responseIsUsableAsset(res) { /** * 检测同域静态 /promotion/ 文件是否真实存在。 * - 需配合 Nginx:`location ^~ /promotion/ { try_files $uri =404; }`,避免缺失时返回 index.html 误判为 200。 - * - HEAD 非 2xx 或疑似 SPA 回退时,再用 Range GET 探测(部分环境对 .mov 的 HEAD 不友好)。 + * - HEAD 非 2xx 或疑似 SPA 回退时,再用 Range GET 探测(部分环境对大视频 HEAD 不友好)。 * @param {string} url * @returns {Promise} */ @@ -94,7 +95,7 @@ const apiOnly = /** * 有静态则静态,否则(需 siteId)走 promotion-media API(单资源;列表请用 buildPromotionVideosAsync 批量逻辑) * @param {string} siteId - * @param {string} relPath promotion 下相对路径,如 social/xxx.mov + * @param {string} relPath promotion 下相对路径,如 social/xxx.mp4 */ export async function pickPromotionAssetUrl(siteId, relPath) { const sid = String(siteId || defaultWebSiteId || '').trim() diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 9f349ce..4ed22a0 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -68,35 +68,13 @@
- +
-