feat(web): 产品视频静态优先,缺失时回退 promotion-media API

Made-with: Cursor
This commit is contained in:
whm
2026-03-20 22:33:16 +08:00
parent 7336c42af0
commit d6767c2c5c
3 changed files with 84 additions and 12 deletions

View File

@@ -44,16 +44,79 @@ export const PROMOTION_VIDEOS_BASE = [
} }
] ]
/** @param {string} siteId 空串时走本地/静态 /promotion/… */ /**
export function buildPromotionVideos(siteId) { * 检测同域静态 /promotion/ 文件是否可访问(优先 HEAD405 时用 Range GET
* @param {string} url
* @returns {Promise<boolean>}
*/
export async function promotionStaticUrlExists(url) {
try {
const head = await fetch(url, {
method: 'HEAD',
mode: 'same-origin',
credentials: 'same-origin',
cache: 'default'
})
if (head.ok) return true
if (head.status === 405) {
const r = await fetch(url, {
method: 'GET',
headers: { Range: 'bytes=0-0' },
mode: 'same-origin',
credentials: 'same-origin',
cache: 'default'
})
return r.ok || r.status === 206
}
return false
} catch {
return false
}
}
/**
* 有静态则静态,否则(需 siteId走 promotion-media API
* @param {string} siteId
* @param {string} relPath promotion 下相对路径,如 social/xxx.mov
*/
export async function pickPromotionAssetUrl(siteId, relPath) {
const staticUrl = promotionUrl(relPath)
const hasStatic = await promotionStaticUrlExists(staticUrl)
if (hasStatic) return staticUrl
if (siteId) return promotionMediaApiUrl(siteId, relPath)
return staticUrl
}
/**
* 同步列表:全部静态 URL首屏占位、无 siteId 时与异步结果一致场景)
* @param {string} [_siteId] 保留兼容,当前忽略
*/
export function buildPromotionVideos(_siteId) {
return PROMOTION_VIDEOS_BASE.map((v) => ({ return PROMOTION_VIDEOS_BASE.map((v) => ({
id: v.id, id: v.id,
title: v.title, title: v.title,
desc: v.desc, desc: v.desc,
cover: promotionMediaApiUrl(siteId, v.relCover), cover: promotionUrl(v.relCover),
src: promotionMediaApiUrl(siteId, v.relVideo) src: promotionUrl(v.relVideo)
})) }))
} }
/** @deprecated 请用 buildPromotionVideos(siteId);保留兼容旧引用 */ /**
export const PROMOTION_VIDEOS = buildPromotionVideos('') * 静态优先:存在 /promotion/... 则用静态,否则用 uploads API需 siteId
* @param {string} siteId
*/
export async function buildPromotionVideosAsync(siteId) {
const id = siteId || ''
return Promise.all(
PROMOTION_VIDEOS_BASE.map(async (v) => {
const [cover, src] = await Promise.all([
pickPromotionAssetUrl(id, v.relCover),
pickPromotionAssetUrl(id, v.relVideo)
])
return { id: v.id, title: v.title, desc: v.desc, cover, src }
})
)
}
/** @deprecated 请用 buildPromotionVideos() 或 buildPromotionVideosAsync */
export const PROMOTION_VIDEOS = buildPromotionVideos()

View File

@@ -1,8 +1,8 @@
import { apiBase } from '../config' import { apiBase } from '../config'
/** /**
* 推广素材根路径。开发时由 Vite 插件映射到 web/promotion * 推广素材根路径。开发时由 Vite 映射到 web/promotion
* 生产环境视频默认走后台上传:`/api/web/sites/{siteId}/promotion-media/...`(见 buildPromotionVideos * 首页产品视频见 `buildPromotionVideosAsync`:同域静态存在则 `/promotion/...`,否则 `promotionMediaApiUrl`
*/ */
export function promotionUrl(relativePath) { export function promotionUrl(relativePath) {
const parts = String(relativePath).split('/').filter(Boolean) const parts = String(relativePath).split('/').filter(Boolean)

View File

@@ -312,7 +312,7 @@
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue' import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { apiBase } from '../config' import { apiBase } from '../config'
import BlockRenderer from '../components/blocks/BlockRenderer.vue' import BlockRenderer from '../components/blocks/BlockRenderer.vue'
import { buildPromotionVideos } from '../data/promotionVideos' import { buildPromotionVideos, buildPromotionVideosAsync } from '../data/promotionVideos'
import { PROMOTION_SOCIAL_FOLLOW } from '../data/promotionSocial' import { PROMOTION_SOCIAL_FOLLOW } from '../data/promotionSocial'
import { getCachedWebSiteId, fetchWebRoutes } from '../api/webPages' import { getCachedWebSiteId, fetchWebRoutes } from '../api/webPages'
@@ -322,9 +322,18 @@ const starsEl = ref(null)
let cometTimer = null let cometTimer = null
const scrollY = ref(0) const scrollY = ref(0)
const openFaq = ref(0) const openFaq = ref(0)
/** 官网 site_idbootstrap 已拉路由时此处已有值;为空则产品视频走本地 /promotion */ /** 官网 site_idbootstrap 已拉路由时此处已有值;产品视频:静态优先,否则 promotion-media API */
const webSiteId = ref(getCachedWebSiteId() || '') const webSiteId = ref(getCachedWebSiteId() || '')
const promoVideos = computed(() => buildPromotionVideos(webSiteId.value)) const promoVideos = ref(buildPromotionVideos(webSiteId.value))
async function refreshPromoVideos() {
const id = webSiteId.value || ''
try {
promoVideos.value = await buildPromotionVideosAsync(id)
} catch {
promoVideos.value = buildPromotionVideos(id)
}
}
watch(webSiteId, refreshPromoVideos, { immediate: true })
const activeVideo = ref(null) const activeVideo = ref(null)
watch( watch(
promoVideos, promoVideos,
@@ -333,7 +342,7 @@ watch(
const cur = activeVideo.value const cur = activeVideo.value
if (!cur || !list.some((v) => v.id === cur.id)) activeVideo.value = list[0] if (!cur || !list.some((v) => v.id === cur.id)) activeVideo.value = list[0]
}, },
{ immediate: true } { immediate: true, deep: true }
) )
const videoModalOpen = ref(false) const videoModalOpen = ref(false)
const modalVideoSrc = ref('') const modalVideoSrc = ref('')