feat: 产品视频改后台上传,promotion-media 公开访问;gitignore 大文件;保留原文件名上传
Made-with: Cursor
This commit is contained in:
@@ -78,6 +78,7 @@ export const uploadSiteAsset = (siteId, file, opts = {}) => {
|
||||
form.append('file', file)
|
||||
if (opts.folder != null) form.append('folder', opts.folder)
|
||||
form.append('downloadable', opts.downloadable ? 'true' : 'false')
|
||||
if (opts.preserveFilename) form.append('preserve_filename', 'true')
|
||||
return request.post(`/admin/sites/${siteId}/assets`, form, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
}
|
||||
export const createSiteFolder = (siteId, path) => request.post(`/admin/sites/${siteId}/folders`, { path })
|
||||
|
||||
@@ -56,11 +56,15 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 上传前选择是否可下载 -->
|
||||
<el-dialog v-model="uploadDialogVisible" title="上传文件" width="400px" :close-on-click-modal="false">
|
||||
<el-form label-width="100px">
|
||||
<el-dialog v-model="uploadDialogVisible" title="上传文件" width="440px" :close-on-click-modal="false">
|
||||
<el-form label-width="112px">
|
||||
<el-form-item label="当前目录">
|
||||
<span>{{ currentPath || '根目录' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="保留原文件名">
|
||||
<el-switch v-model="uploadPreserveFilename" />
|
||||
<span class="form-hint">开启后覆盖同路径同名文件;首页「产品视频」须上传到 <code>promotion/视频发布/…</code> 并开启此项</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="允许下载">
|
||||
<el-switch v-model="uploadDownloadable" />
|
||||
</el-form-item>
|
||||
@@ -100,7 +104,8 @@ const loading = ref(false)
|
||||
const currentPath = ref('')
|
||||
const uploading = ref(false)
|
||||
const uploadDialogVisible = ref(false)
|
||||
const uploadDownloadable = ref(true)
|
||||
const uploadDownloadable = ref(false)
|
||||
const uploadPreserveFilename = ref(false)
|
||||
const pendingFile = ref(null)
|
||||
const showNewFolder = ref(false)
|
||||
const newFolderName = ref('')
|
||||
@@ -152,7 +157,9 @@ watch([siteId, currentPath], fetchList)
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
pendingFile.value = file
|
||||
uploadDownloadable.value = true
|
||||
const p = (currentPath.value || '').replace(/^\//, '')
|
||||
uploadPreserveFilename.value = p.startsWith('promotion/')
|
||||
uploadDownloadable.value = !uploadPreserveFilename.value
|
||||
uploadDialogVisible.value = true
|
||||
return false
|
||||
}
|
||||
@@ -163,7 +170,8 @@ const doUpload = async () => {
|
||||
try {
|
||||
await uploadSiteAsset(siteId.value, pendingFile.value, {
|
||||
folder: currentPath.value || undefined,
|
||||
downloadable: uploadDownloadable.value
|
||||
downloadable: uploadDownloadable.value,
|
||||
preserveFilename: uploadPreserveFilename.value
|
||||
})
|
||||
ElMessage.success('上传成功')
|
||||
uploadDialogVisible.value = false
|
||||
@@ -216,6 +224,8 @@ onMounted(() => fetchSites().then(() => fetchList()))
|
||||
|
||||
<style scoped>
|
||||
.file-manage .tip { color: #666; font-size: 14px; }
|
||||
.form-hint { display: block; margin-top: 6px; font-size: 12px; color: #909399; line-height: 1.4; }
|
||||
.form-hint code { font-size: 11px; }
|
||||
.module-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.breadcrumb-wrap { margin-top: 12px; }
|
||||
.subdirs { margin-top: 8px; font-size: 13px; color: #666; }
|
||||
|
||||
@@ -4,7 +4,7 @@ server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name yuheng.yuxindazhineng.com;
|
||||
client_max_body_size 200m;
|
||||
client_max_body_size 800m;
|
||||
|
||||
ssl_certificate /etc/ssl/yh_web/yuheng.yuxindazhineng.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/ssl/yh_web/yuheng.yuxindazhineng.com/privkey.pem;
|
||||
|
||||
@@ -153,7 +153,61 @@ func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// UploadSiteAsset 上传功能模块/文件;form 可选:folder(当前目录相对路径)、downloadable(true/false)
|
||||
func promotionMimeType(ext string) string {
|
||||
switch strings.ToLower(ext) {
|
||||
case ".mov":
|
||||
return "video/quicktime"
|
||||
case ".mp4":
|
||||
return "video/mp4"
|
||||
case ".webm":
|
||||
return "video/webm"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ServePromotionMedia 公开读取站点 uploads/sites/{site_id}/promotion/ 下文件(与后台上传到 promotion/… 目录对应)
|
||||
func ServePromotionMedia(c *gin.Context) {
|
||||
siteID := c.Param("site_id")
|
||||
raw := strings.TrimPrefix(c.Param("filepath"), "/")
|
||||
if siteID == "" || raw == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
rel := filepath.ToSlash(filepath.Clean(raw))
|
||||
if rel == "." || strings.HasPrefix(rel, "../") || strings.Contains(rel, "/../") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效路径"})
|
||||
return
|
||||
}
|
||||
baseDir := filepath.Join(getUploadDir(), "sites", siteID, "promotion")
|
||||
fullPath := filepath.Join(baseDir, filepath.FromSlash(rel))
|
||||
relBack, err := filepath.Rel(baseDir, fullPath)
|
||||
if err != nil || strings.HasPrefix(relBack, "..") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "禁止访问"})
|
||||
return
|
||||
}
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil || fi.IsDir() {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(fullPath)
|
||||
ct := promotionMimeType(ext)
|
||||
if ct == "" {
|
||||
ct = "application/octet-stream"
|
||||
}
|
||||
c.Header("Content-Type", ct)
|
||||
c.Header("Cache-Control", "public, max-age=86400")
|
||||
c.File(fullPath)
|
||||
}
|
||||
|
||||
// UploadSiteAsset 上传功能模块/文件;form 可选:folder(当前目录相对路径)、downloadable(true/false)、preserve_filename(true 时保持原名并覆盖同路径旧文件,用于首页推广视频固定 URL)
|
||||
func UploadSiteAsset(c *gin.Context) {
|
||||
siteID := c.Param("site_id")
|
||||
if siteID == "" {
|
||||
@@ -167,22 +221,59 @@ func UploadSiteAsset(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
folder := c.PostForm("folder")
|
||||
folder := strings.TrimSpace(c.PostForm("folder"))
|
||||
downloadable := c.PostForm("downloadable") == "true" || c.PostForm("downloadable") == "1"
|
||||
baseDir := filepath.Join(getUploadDir(), "sites", siteID, filepath.Clean(folder))
|
||||
preserve := c.PostForm("preserve_filename") == "true" || c.PostForm("preserve_filename") == "1"
|
||||
|
||||
name := file.Filename
|
||||
ext := filepath.Ext(name)
|
||||
nameNoExt := strings.TrimSuffix(name, ext)
|
||||
var saveName string
|
||||
if preserve {
|
||||
saveName = filepath.Base(name)
|
||||
if saveName == "." || saveName == string(filepath.Separator) || saveName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件名"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(ext) == 0 {
|
||||
saveName = nameNoExt + "_" + time.Now().Format("20060102150405")
|
||||
} else {
|
||||
saveName = nameNoExt + "_" + time.Now().Format("20060102150405") + ext
|
||||
}
|
||||
}
|
||||
|
||||
folderClean := ""
|
||||
if folder != "" {
|
||||
folderClean = filepath.ToSlash(filepath.Clean(folder))
|
||||
if strings.HasPrefix(folderClean, "../") || strings.Contains(folderClean, "/../") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的目录路径"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var relPath string
|
||||
if folderClean != "" {
|
||||
relPath = filepath.ToSlash(filepath.Join("sites", siteID, folderClean, saveName))
|
||||
} else {
|
||||
relPath = filepath.ToSlash(filepath.Join("sites", siteID, saveName))
|
||||
}
|
||||
destPath := filepath.Join(getUploadDir(), filepath.FromSlash(relPath))
|
||||
|
||||
if preserve {
|
||||
ctxDel, cancelDel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancelDel()
|
||||
coll := config.GetDB(config.DBName).Collection("site_assets")
|
||||
_, _ = coll.DeleteMany(ctxDel, bson.M{"site_id": siteID, "file_path": relPath})
|
||||
_ = os.Remove(destPath)
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(destPath)
|
||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
name := file.Filename
|
||||
ext := filepath.Ext(name)
|
||||
nameNoExt := name[:len(name)-len(ext)]
|
||||
saveName := nameNoExt + "_" + time.Now().Format("20060102150405") + ext
|
||||
relPath := filepath.Join("sites", siteID, filepath.Clean(folder), saveName)
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
destPath := filepath.Join(getUploadDir(), filepath.FromSlash(relPath))
|
||||
|
||||
if err := c.SaveUploadedFile(file, destPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
|
||||
return
|
||||
|
||||
@@ -79,7 +79,7 @@ func main() {
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
r.MaxMultipartMemory = 200 << 20 // 200MB,与 Nginx client_max_body_size 一致,避免上传 413
|
||||
r.MaxMultipartMemory = 32 << 20 // 大单片先落临时文件;整体体积受 Nginx client_max_body_size 限制
|
||||
r.Use(middleware.ErrorLogger())
|
||||
|
||||
// CORS(ALLOWED_ORIGINS 为空则允许所有来源;否则仅允许配置的域名)
|
||||
@@ -215,6 +215,8 @@ func main() {
|
||||
web.GET("/info", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "web api"})
|
||||
})
|
||||
// 推广/产品视频:站点 uploads 下 promotion 目录(无需鉴权,与后台上传到 promotion/… 对应)
|
||||
web.GET("/sites/:site_id/promotion-media/*filepath", handlers.ServePromotionMedia)
|
||||
// 可下载资源公开下载(首页等链接指向此路径)
|
||||
web.GET("/sites/:site_id/assets/:asset_id/download", handlers.DownloadSiteAsset)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# 推广素材(首页与视频源)
|
||||
|
||||
- `index.html`:静态完整落地页参考;线上 Vue 首页已与之对齐,素材路径以本目录为准。
|
||||
- `视频发布/`:产品视频与封面,首页「产品视频」区块使用下列相对路径(经 `promotionUrl` 转为 URL):
|
||||
- `/promotion/视频发布/...`
|
||||
- `视频发布/`:产品视频与封面。**`.mov` 等大文件默认不入 Git**;生产环境请在 **后台 → 文件管理** 上传到 `promotion/视频发布/…`,勾选 **保留原文件名**(路径与文件名见 `视频发布/README.md`)。官网解析到站点后,首页通过 `/api/web/sites/{site_id}/promotion-media/视频发布/...` 拉取。本地开发仍可将文件放在本目录,走 `/promotion/视频发布/...`。
|
||||
- `social/`:**关注我们** 统一资源包(建议只用此目录上线),首页读取:
|
||||
- `social/xiaohongshu.png`、`social/douyin.png`、`social/wechat-official.png`、`social/wechat-channels.jpg`
|
||||
- 源文件可从根目录 `小红书.png`、`抖音.png`、`公众号.png`、`视频号.jpg` 同步复制进来(ASCII 文件名利于网关与日志)。
|
||||
@@ -10,7 +9,7 @@
|
||||
|
||||
## 生产部署
|
||||
|
||||
1. **前端构建产物**(`dist/`)不包含本目录。部署时请把本文件夹 **完整复制** 到站点根下,与 `index.html` 同级,目录名为 `promotion`(即能通过 `https://你的域名/promotion/视频发布/...` 访问视频)。
|
||||
1. **前端构建产物**(`dist/`)不包含本目录。除视频外请把需要的素材 **复制** 到站点根下 `promotion/`(如 `social/`、宣传册相关),或通过 Nginx alias。**视频**推荐仅通过后台上传到 API 存储目录,无需再拷 `.mov` 到静态服务器。
|
||||
2. 或使用 Nginx:
|
||||
|
||||
```nginx
|
||||
|
||||
26
web/promotion/视频发布/README.md
Normal file
26
web/promotion/视频发布/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 产品视频目录
|
||||
|
||||
大体积 **`.mov` 视频不入 Git**。线上由官网 **后台 → 文件管理** 上传到站点下的 `promotion/视频发布/...`,与首页「产品视频」使用的路径一致。
|
||||
|
||||
## 上传步骤
|
||||
|
||||
1. 打开 **后台**,进入 **文件管理 → 功能模块**。
|
||||
2. 选择 **官网站点**(与系统设置中的「官网」站点一致)。
|
||||
3. 新建或使用目录:`promotion/视频发布/<子目录名>/`(与下列清单一致)。
|
||||
4. 上传对应 **封面 `.jpg`** 与 **视频 `.mov`** 时,请勾选 **「保留原文件名」**,文件名必须与下列清单完全一致(否则首页无法匹配)。
|
||||
|
||||
## 文件清单(相对 `promotion/`)
|
||||
|
||||
| 子目录 | 封面文件 | 视频文件 |
|
||||
|--------|----------|----------|
|
||||
| `视频发布/宇恒一号操作计算软件实例(一)` | `宣传片-封面.jpg` | `宣传片.mov` |
|
||||
| `视频发布/宇恒一号操作计算软件实例(二)` | `宇恒一号操作计算软件实例(二)-封面.jpg` | `宇恒一号操作计算软件实例(二).mov` |
|
||||
| `视频发布/宇恒一号AIWord简介` | `宇恒一号AIWord简介-封面.jpg` | `宇恒一号AIWord简介.mov` |
|
||||
| `视频发布/宇恒一号语音办公实例` | `宇恒一号语音办公实例-封面.jpg` | `宇恒一号语音办公实例.mov` |
|
||||
| `视频发布/宇恒一号,AI 全自动办发票` | `宇恒一号,AI 全自动办发票-封面.jpg` | `宇恒一号,AI 全自动办发票.mov` |
|
||||
|
||||
(若表内「封面」文件名与代码中 `web/src/data/promotionVideos.js` 不一致,以代码中的 `relCover` / `relVideo` 为准。)
|
||||
|
||||
## 本地开发
|
||||
|
||||
若仅在本地调试,仍可将视频放在本目录,Vite 开发服务器会通过 `/promotion/...` 直接读本地文件。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 148 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
@@ -1,42 +1,56 @@
|
||||
import { promotionUrl } from '../utils/promotionAssets'
|
||||
import { promotionUrl, promotionMediaApiUrl } from '../utils/promotionAssets'
|
||||
|
||||
const ROOT = '视频发布'
|
||||
|
||||
/** 与 web/promotion/视频发布 目录结构一致 */
|
||||
export const PROMOTION_VIDEOS = [
|
||||
/** 相对 `promotion/` 的路径;与后台上传目录 promotion/视频发布/… +「保留原文件名」一致 */
|
||||
export const PROMOTION_VIDEOS_BASE = [
|
||||
{
|
||||
id: 'calc-demo-1',
|
||||
title: '操作与计算软件实例(一)',
|
||||
desc: '宇恒一号宣传片',
|
||||
cover: promotionUrl(`${ROOT}/宇恒一号操作计算软件实例(一)/宣传片-封面.jpg`),
|
||||
src: promotionUrl(`${ROOT}/宇恒一号操作计算软件实例(一)/宣传片.mov`)
|
||||
relCover: `${ROOT}/宇恒一号操作计算软件实例(一)/宣传片-封面.jpg`,
|
||||
relVideo: `${ROOT}/宇恒一号操作计算软件实例(一)/宣传片.mov`
|
||||
},
|
||||
{
|
||||
id: 'calc-demo-2',
|
||||
title: '操作与计算软件实例(二)',
|
||||
desc: '进阶操作与计算演示',
|
||||
cover: promotionUrl(`${ROOT}/宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg`),
|
||||
src: promotionUrl(`${ROOT}/宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov`)
|
||||
relCover: `${ROOT}/宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg`,
|
||||
relVideo: `${ROOT}/宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov`
|
||||
},
|
||||
{
|
||||
id: 'aiword',
|
||||
title: '宇恒一号 AI Word 简介',
|
||||
desc: 'AI Word 能力介绍',
|
||||
cover: promotionUrl(`${ROOT}/宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg`),
|
||||
src: promotionUrl(`${ROOT}/宇恒一号AIWord简介/宇恒一号AIWord简介.mov`)
|
||||
relCover: `${ROOT}/宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg`,
|
||||
relVideo: `${ROOT}/宇恒一号AIWord简介/宇恒一号AIWord简介.mov`
|
||||
},
|
||||
{
|
||||
id: 'voice',
|
||||
title: '语音办公实例',
|
||||
desc: '语音驱动办公流程',
|
||||
cover: promotionUrl(`${ROOT}/宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg`),
|
||||
src: promotionUrl(`${ROOT}/宇恒一号语音办公实例/宇恒一号语音办公实例.mov`)
|
||||
relCover: `${ROOT}/宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg`,
|
||||
relVideo: `${ROOT}/宇恒一号语音办公实例/宇恒一号语音办公实例.mov`
|
||||
},
|
||||
{
|
||||
id: 'invoice',
|
||||
title: 'AI 全自动办发票',
|
||||
desc: '发票场景自动化演示',
|
||||
cover: promotionUrl(`${ROOT}/宇恒一号,AI 全自动办发票/宇恒一号,AI 全自动办发票-封面.jpg`),
|
||||
src: promotionUrl(`${ROOT}/宇恒一号,AI 全自动办发票/宇恒一号,AI 全自动办发票.mov`)
|
||||
relCover: `${ROOT}/宇恒一号,AI 全自动办发票/宇恒一号,AI 全自动办发票-封面.jpg`,
|
||||
relVideo: `${ROOT}/宇恒一号,AI 全自动办发票/宇恒一号,AI 全自动办发票.mov`
|
||||
}
|
||||
]
|
||||
|
||||
/** @param {string} siteId 空串时走本地/静态 /promotion/… */
|
||||
export function buildPromotionVideos(siteId) {
|
||||
return PROMOTION_VIDEOS_BASE.map((v) => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
desc: v.desc,
|
||||
cover: promotionMediaApiUrl(siteId, v.relCover),
|
||||
src: promotionMediaApiUrl(siteId, v.relVideo)
|
||||
}))
|
||||
}
|
||||
|
||||
/** @deprecated 请用 buildPromotionVideos(siteId);保留兼容旧引用 */
|
||||
export const PROMOTION_VIDEOS = buildPromotionVideos('')
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { apiBase } from '../config'
|
||||
|
||||
/**
|
||||
* 推广素材根路径。开发时由 Vite 插件映射到 web/promotion;
|
||||
* 生产构建请把 web/promotion 整目录同步到站点 /promotion(或配置 Nginx alias)。
|
||||
* 生产环境视频默认走后台上传:`/api/web/sites/{siteId}/promotion-media/...`(见 buildPromotionVideos)。
|
||||
*/
|
||||
export function promotionUrl(relativePath) {
|
||||
const parts = String(relativePath).split('/').filter(Boolean)
|
||||
return '/promotion/' + parts.map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 官网产品视频/封面:读取站点 uploads 下 `promotion/` 目录(与后台上传路径一致)
|
||||
* @param {string} siteId Mongo 站点 id(与 /api/web/routes 返回的 site_id 一致)
|
||||
* @param {string} relativePath 相对 promotion/ 的路径,如 `视频发布/xxx/yyy.mov`
|
||||
*/
|
||||
export function promotionMediaApiUrl(siteId, relativePath) {
|
||||
if (!siteId) return promotionUrl(relativePath)
|
||||
const root = (apiBase || '').replace(/\/$/, '')
|
||||
const prefix = root ? `${root}/api` : '/api'
|
||||
const parts = String(relativePath).split(/[/\\]/).filter(Boolean).map(encodeURIComponent).join('/')
|
||||
return `${prefix}/web/sites/${siteId}/promotion-media/${parts}`
|
||||
}
|
||||
|
||||
@@ -213,11 +213,11 @@
|
||||
<section class="video-section" id="videos">
|
||||
<div class="section-title">
|
||||
<h2>产品视频</h2>
|
||||
<p>内容为「视频发布」文件夹中的正式素材,点击即可播放</p>
|
||||
<p>视频由后台上传至 <code class="social-code">promotion/视频发布/</code>,需勾选「保留原文件名」;未上传时本地开发可走 <code class="social-code">/promotion/…</code></p>
|
||||
</div>
|
||||
<div class="video-container">
|
||||
<div class="video-wrapper">
|
||||
<div class="video-card main-video">
|
||||
<div v-if="activeVideo" class="video-card main-video">
|
||||
<div
|
||||
class="video-thumbnail main-thumb"
|
||||
:style="mainVideoCoverStyle"
|
||||
@@ -238,7 +238,7 @@
|
||||
v-for="v in promoVideos"
|
||||
:key="v.id"
|
||||
class="video-card small-video"
|
||||
:class="{ 'is-active': activeVideo.id === v.id }"
|
||||
:class="{ 'is-active': activeVideo && activeVideo.id === v.id }"
|
||||
@click="selectVideo(v)"
|
||||
>
|
||||
<div
|
||||
@@ -309,11 +309,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { apiBase } from '../config'
|
||||
import BlockRenderer from '../components/blocks/BlockRenderer.vue'
|
||||
import { PROMOTION_VIDEOS } from '../data/promotionVideos'
|
||||
import { buildPromotionVideos } from '../data/promotionVideos'
|
||||
import { PROMOTION_SOCIAL_FOLLOW } from '../data/promotionSocial'
|
||||
import { getCachedWebSiteId, fetchWebRoutes } from '../api/webPages'
|
||||
|
||||
const socialFollowItems = PROMOTION_SOCIAL_FOLLOW
|
||||
|
||||
@@ -321,8 +322,19 @@ const starsEl = ref(null)
|
||||
let cometTimer = null
|
||||
const scrollY = ref(0)
|
||||
const openFaq = ref(0)
|
||||
const promoVideos = PROMOTION_VIDEOS
|
||||
const activeVideo = ref(promoVideos[0])
|
||||
/** 官网 site_id(bootstrap 已拉路由时此处已有值;为空则产品视频走本地 /promotion) */
|
||||
const webSiteId = ref(getCachedWebSiteId() || '')
|
||||
const promoVideos = computed(() => buildPromotionVideos(webSiteId.value))
|
||||
const activeVideo = ref(null)
|
||||
watch(
|
||||
promoVideos,
|
||||
(list) => {
|
||||
if (!list.length) return
|
||||
const cur = activeVideo.value
|
||||
if (!cur || !list.some((v) => v.id === cur.id)) activeVideo.value = list[0]
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const videoModalOpen = ref(false)
|
||||
const modalVideoSrc = ref('')
|
||||
const modalCaption = ref('')
|
||||
@@ -385,11 +397,14 @@ const navbarStyle = computed(() => {
|
||||
return { background: 'linear-gradient(180deg, rgba(10,10,18,0.95) 0%, transparent 100%)', boxShadow: 'none' }
|
||||
})
|
||||
|
||||
const mainVideoCoverStyle = computed(() => ({
|
||||
backgroundImage: 'url(' + activeVideo.value.cover + ')',
|
||||
const mainVideoCoverStyle = computed(() => {
|
||||
const v = activeVideo.value
|
||||
return {
|
||||
backgroundImage: v ? 'url(' + v.cover + ')' : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const bodyBuilderBlocks = computed(() => {
|
||||
const raw = data.body_builder
|
||||
@@ -569,6 +584,11 @@ function onDocKeydown(e) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!webSiteId.value) {
|
||||
fetchWebRoutes().then(() => {
|
||||
webSiteId.value = getCachedWebSiteId() || ''
|
||||
})
|
||||
}
|
||||
fetchHomepage()
|
||||
document.title = (data.title || '宇恒一号') + ' - 星际探索版'
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
|
||||
Reference in New Issue
Block a user