|
|
@@ -21,10 +21,12 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 内容区域 -->
|
|
|
+ <!-- 内容区域:isFixed 为「视频」的相册走视频链路,其余相册走原图片链路 -->
|
|
|
<div class="content-section">
|
|
|
- <!-- 环境/相册 Tab:图片上传说明 -->
|
|
|
- <div v-if="isImageTab" class="upload-tips">
|
|
|
+ <div v-if="(isAlbumTab && currentAlbumIsVideo) || activeTab === 'video'" class="upload-tips">
|
|
|
+ <span>单个视频上传大小不超过50M,时长建议小于1分钟</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="isAlbumTab && currentAlbumRef && !currentAlbumIsVideo" class="upload-tips">
|
|
|
<span>照片宽高不小于500像素</span>
|
|
|
<span>大小不超过10M</span>
|
|
|
<span>可拖拽排序</span>
|
|
|
@@ -32,21 +34,11 @@
|
|
|
<span>不可上传手机截屏、含清晰人脸/电话/二维码/第三方水印/LOGO等图片。</span>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 视频 Tab:视频上传说明 -->
|
|
|
- <div v-if="activeTab === 'video'" class="upload-tips">
|
|
|
- <span>单个视频上传大小不超过500MB</span>
|
|
|
- <span>可拖拽排序</span>
|
|
|
- <span>时长建议小于1分钟</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 环境/相册 Tab:图片上传和展示区域 -->
|
|
|
- <div v-if="isImageTab" class="media-grid">
|
|
|
- <!-- 上传按钮 -->
|
|
|
+ <!-- 相册 Tab:图片(非视频相册) -->
|
|
|
+ <div v-if="isAlbumTab && currentAlbumRef && !currentAlbumIsVideo" class="media-grid">
|
|
|
<div class="upload-item upload-box">
|
|
|
- <!-- 环境tab:直接绑定 environmentImages -->
|
|
|
<UploadImgs
|
|
|
- v-if="activeTab === 'environment'"
|
|
|
- v-model:file-list="environmentImages"
|
|
|
+ v-model:file-list="currentAlbumRef.images"
|
|
|
:limit="50"
|
|
|
:file-size="10"
|
|
|
:file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
|
|
|
@@ -56,48 +48,75 @@
|
|
|
:api="customImageUploadApi"
|
|
|
>
|
|
|
<template #tip>
|
|
|
- <div class="upload-tip-text">上传图片 ({{ environmentImages.length }}/50)</div>
|
|
|
+ <div class="upload-tip-text">上传图片 ({{ currentAlbumRef.images?.length || 0 }}/50)</div>
|
|
|
</template>
|
|
|
</UploadImgs>
|
|
|
- <!-- 相册tab:根据当前相册动态绑定 -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-for="(image, index) in displayImages" :key="image.uid || index" class="media-item">
|
|
|
+ <div class="media-wrapper">
|
|
|
+ <img :src="image.url || ''" alt="相册图片" />
|
|
|
+ <div class="media-overlay">
|
|
|
+ <el-button type="primary" link @click="viewImage(image.url || '')"> 查看 </el-button>
|
|
|
+ <el-button type="primary" link @click="deleteImage(index)"> 删除 </el-button>
|
|
|
+ </div>
|
|
|
+ <el-tag v-if="isFirstImage(index)" type="danger" class="cover-tag"> 封面 </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 相册 Tab:视频(isFixed 为视频) -->
|
|
|
+ <div v-else-if="isAlbumTab && currentAlbumRef && currentAlbumIsVideo" class="media-grid">
|
|
|
+ <div class="upload-item upload-box">
|
|
|
<UploadImgs
|
|
|
- v-else-if="currentAlbumRef"
|
|
|
- v-model:file-list="currentAlbumRef.images"
|
|
|
- :limit="50"
|
|
|
- :file-size="10"
|
|
|
- :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
|
|
|
+ v-model:file-list="currentAlbumRef.videos"
|
|
|
+ :limit="10"
|
|
|
+ :file-size="50"
|
|
|
+ :file-type="['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'] as any"
|
|
|
:width="'100%'"
|
|
|
:height="'200px'"
|
|
|
:border-radius="'8px'"
|
|
|
- :api="customImageUploadApi"
|
|
|
+ :api="customVideoUploadApi"
|
|
|
+ :on-success="handleVideoUploadSuccess"
|
|
|
>
|
|
|
<template #tip>
|
|
|
- <div class="upload-tip-text">上传图片 ({{ currentAlbumRef.images?.length || 0 }}/50)</div>
|
|
|
+ <div class="upload-tip-text">上传视频 ({{ currentAlbumRef.videos?.length || 0 }}/10)</div>
|
|
|
</template>
|
|
|
</UploadImgs>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 已上传的图片 -->
|
|
|
- <div v-for="(image, index) in displayImages" :key="image.uid || index" class="media-item">
|
|
|
+ <div v-for="(video, index) in displayVideos" :key="video.uid || index" class="media-item">
|
|
|
<div class="media-wrapper">
|
|
|
- <img :src="image.url || ''" alt="相册图片" />
|
|
|
+ <video
|
|
|
+ v-if="video.url"
|
|
|
+ :src="video.url"
|
|
|
+ :poster="video.coverUrl || undefined"
|
|
|
+ class="media-preview"
|
|
|
+ muted
|
|
|
+ preload="metadata"
|
|
|
+ playsinline
|
|
|
+ />
|
|
|
+ <!-- imgUrl JSON 内 cover 为封面图,叠在 video 上保证缩略图稳定展示 -->
|
|
|
+ <img v-if="video.coverUrl" :src="video.coverUrl" class="media-cover-thumb" alt="视频封面" />
|
|
|
+ <div v-else-if="!video.url" class="media-placeholder">
|
|
|
+ <el-icon><VideoPlay /></el-icon>
|
|
|
+ <span>视频预览</span>
|
|
|
+ </div>
|
|
|
<div class="media-overlay">
|
|
|
- <el-button type="primary" link @click="viewImage(image.url || '')"> 查看 </el-button>
|
|
|
- <el-button type="primary" link @click="deleteImage(index)"> 删除 </el-button>
|
|
|
+ <el-button type="primary" link @click="viewVideo(video.url || '')"> 查看 </el-button>
|
|
|
+ <el-button type="primary" link @click="deleteVideo(index)"> 删除 </el-button>
|
|
|
</div>
|
|
|
- <el-tag v-if="isFirstImage(index)" type="danger" class="cover-tag"> 封面 </el-tag>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 视频 Tab:视频上传和展示区域 -->
|
|
|
- <div v-if="activeTab === 'video'" class="media-grid">
|
|
|
- <!-- 上传按钮 -->
|
|
|
+ <!-- 独立「视频」Tab(若启用头部 Tab) -->
|
|
|
+ <div v-else-if="activeTab === 'video'" class="media-grid">
|
|
|
<div class="upload-item upload-box">
|
|
|
<UploadImgs
|
|
|
v-model:file-list="videoList"
|
|
|
:limit="10"
|
|
|
- :file-size="500"
|
|
|
+ :file-size="50"
|
|
|
:file-type="['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'] as any"
|
|
|
:width="'100%'"
|
|
|
:height="'200px'"
|
|
|
@@ -111,11 +130,19 @@
|
|
|
</UploadImgs>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 已上传的视频 -->
|
|
|
<div v-for="(video, index) in displayVideos" :key="video.uid || index" class="media-item">
|
|
|
<div class="media-wrapper">
|
|
|
- <video v-if="video.url" :src="video.url" class="media-preview" muted preload="metadata" playsinline />
|
|
|
- <div v-else class="media-placeholder">
|
|
|
+ <video
|
|
|
+ v-if="video.url"
|
|
|
+ :src="video.url"
|
|
|
+ :poster="video.coverUrl || undefined"
|
|
|
+ class="media-preview"
|
|
|
+ muted
|
|
|
+ preload="metadata"
|
|
|
+ playsinline
|
|
|
+ />
|
|
|
+ <img v-if="video.coverUrl" :src="video.coverUrl" class="media-cover-thumb" alt="视频封面" />
|
|
|
+ <div v-else-if="!video.url" class="media-placeholder">
|
|
|
<el-icon><VideoPlay /></el-icon>
|
|
|
<span>视频预览</span>
|
|
|
</div>
|
|
|
@@ -159,11 +186,11 @@
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
- <!-- 图片/视频预览对话框 -->
|
|
|
+ <!-- 视频预览对话框(相册/视频 Tab) -->
|
|
|
<el-dialog v-model="previewVisible" width="600px" align-center class="preview-dialog">
|
|
|
<div class="preview-content">
|
|
|
- <img v-if="isImageTab" :src="previewImageUrl" alt="预览图片" class="preview-image" />
|
|
|
- <video v-else-if="activeTab === 'video'" :src="previewVideoUrl" controls class="preview-video" />
|
|
|
+ <video v-if="previewVideoUrl" :src="previewVideoUrl" controls class="preview-video" />
|
|
|
+ <img v-else-if="previewImageUrl" :src="previewImageUrl" alt="预览" class="preview-image" />
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
@@ -183,9 +210,11 @@ import {
|
|
|
deleteOfficialAlbum,
|
|
|
saveOfficialImg,
|
|
|
getOfficialImgList,
|
|
|
+ getOfficialVideoByStoreId,
|
|
|
deleteOfficialImg
|
|
|
} from "@/api/modules/storeDecoration";
|
|
|
import { uploadFilesToOss } from "@/api/upload.js";
|
|
|
+import { uploadFileViaDevSimpleEndpoint } from "@/api/modules/aiImageUpload";
|
|
|
|
|
|
/** 普通官方相册图 */
|
|
|
const OFFICIAL_IMG_TYPE_ALBUM = 2;
|
|
|
@@ -208,23 +237,41 @@ interface AlbumImage extends UploadUserFile {
|
|
|
|
|
|
// 扩展视频类型
|
|
|
interface AlbumVideo extends UploadUserFile {
|
|
|
- businessId?: number; // 相册ID
|
|
|
+ businessId?: number; // 相册ID(接口 businessId)
|
|
|
imgSort?: number; // 排序
|
|
|
imgType?: number; // 视频类型,可能是3或其他值
|
|
|
imgId?: number; // 视频ID(从服务器返回)
|
|
|
videoDuration?: number; // 视频时长(秒)
|
|
|
+ coverUrl?: string; // 封面:接口 imgUrl 为 JSON 字符串时的 cover 字段
|
|
|
+ imgDescription?: string; // 接口说明文案
|
|
|
}
|
|
|
|
|
|
-// 相册接口
|
|
|
+// 相册接口(getOfficialAlbumList:isFixed 为「视频」时该 Tab 走视频上传与 alienStore/video 列表)
|
|
|
interface Album {
|
|
|
id: string | number;
|
|
|
name: string;
|
|
|
albumName?: string; // API返回的字段名
|
|
|
+ isFixed?: string | number | boolean;
|
|
|
images: AlbumImage[];
|
|
|
videos: AlbumVideo[];
|
|
|
coverImageId?: string | number;
|
|
|
}
|
|
|
|
|
|
+/** 是否视频相册:与后端 isFixed 约定,支持文案「视频」或数字 1 */
|
|
|
+const albumIsVideoAlbum = (a: Album | null | undefined): boolean => {
|
|
|
+ if (!a) return false;
|
|
|
+ const v = a.isFixed;
|
|
|
+ if (v === undefined || v === null) return false;
|
|
|
+ if (typeof v === "string") {
|
|
|
+ const t = v.trim();
|
|
|
+ return t === "视频" || /^video$/i.test(t);
|
|
|
+ }
|
|
|
+ if (typeof v === "number") {
|
|
|
+ return v === 1;
|
|
|
+ }
|
|
|
+ return v === true;
|
|
|
+};
|
|
|
+
|
|
|
const createDialogVisible = ref(false);
|
|
|
const createFormRef = ref<FormInstance>();
|
|
|
// 环境 / 视频 / 相册 id(字符串);初始为空,相册列表加载后默认选中第一项
|
|
|
@@ -244,13 +291,8 @@ const isLoadingAlbumImages = ref(false);
|
|
|
// 相册列表
|
|
|
const albumList = ref<Album[]>([]);
|
|
|
|
|
|
-// 判断当前是否为图片tab(环境或相册)
|
|
|
-const isImageTab = computed(() => {
|
|
|
- return (
|
|
|
- activeTab.value === "environment" ||
|
|
|
- (activeTab.value !== "video" && albumList.value.some(album => String(album.id) === activeTab.value))
|
|
|
- );
|
|
|
-});
|
|
|
+// 当前是否为相册 Tab(动态相册列表中的某一项)
|
|
|
+const isAlbumTab = computed(() => albumList.value.some(album => String(album.id) === activeTab.value));
|
|
|
|
|
|
// 当前选中的相册或环境数据
|
|
|
const currentAlbum = computed(() => {
|
|
|
@@ -265,7 +307,6 @@ const currentAlbum = computed(() => {
|
|
|
// 否则是相册tab,查找对应的相册
|
|
|
const album = albumList.value.find(album => String(album.id) === activeTab.value);
|
|
|
if (album) {
|
|
|
- // 确保 videos 数组存在(相册只有图片,没有视频)
|
|
|
if (!album.videos) {
|
|
|
album.videos = [];
|
|
|
}
|
|
|
@@ -287,30 +328,19 @@ const currentAlbumRef = computed(() => {
|
|
|
}
|
|
|
const album = albumList.value.find(album => String(album.id) === activeTab.value);
|
|
|
if (album) {
|
|
|
- // 确保 images 数组存在且是响应式的
|
|
|
if (!album.images) {
|
|
|
album.images = reactive<AlbumImage[]>([]);
|
|
|
}
|
|
|
+ if (!album.videos) {
|
|
|
+ album.videos = reactive<AlbumVideo[]>([]);
|
|
|
+ }
|
|
|
return album;
|
|
|
}
|
|
|
return null;
|
|
|
});
|
|
|
|
|
|
-// 当前相册的图片列表(用于计算显示)
|
|
|
-const currentAlbumImages = computed(() => {
|
|
|
- if (activeTab.value === "environment") {
|
|
|
- return environmentImages.value;
|
|
|
- }
|
|
|
- return currentAlbumRef.value?.images || [];
|
|
|
-});
|
|
|
-
|
|
|
-// 当前媒体列表(根据tab类型返回图片或视频)
|
|
|
-const currentMediaList = computed(() => {
|
|
|
- if (activeTab.value === "video") {
|
|
|
- return videoList.value;
|
|
|
- }
|
|
|
- return currentAlbumImages.value || [];
|
|
|
-});
|
|
|
+// 当前选中的相册是否为「视频」类型(由 getOfficialAlbumList.isFixed 决定)
|
|
|
+const currentAlbumIsVideo = computed(() => albumIsVideoAlbum(currentAlbumRef.value));
|
|
|
|
|
|
// 分页数据
|
|
|
const pageable = reactive({
|
|
|
@@ -319,38 +349,31 @@ const pageable = reactive({
|
|
|
total: 0
|
|
|
});
|
|
|
|
|
|
-// 显示图片列表(分页后)
|
|
|
+// 图片相册:分页后的图片列表
|
|
|
const displayImages = computed(() => {
|
|
|
- // 如果不在图片tab,返回空数组
|
|
|
- if (!isImageTab.value) {
|
|
|
+ if (!isAlbumTab.value || currentAlbumIsVideo.value) {
|
|
|
return [];
|
|
|
}
|
|
|
-
|
|
|
- let images: AlbumImage[] = [];
|
|
|
- if (activeTab.value === "environment") {
|
|
|
- images = environmentImages.value || [];
|
|
|
- } else if (activeTab.value !== "video") {
|
|
|
- images = currentAlbumRef.value?.images || [];
|
|
|
- }
|
|
|
-
|
|
|
- // 如果没有图片,直接返回空数组
|
|
|
+ const images = currentAlbumRef.value?.images || [];
|
|
|
if (images.length === 0) {
|
|
|
return [];
|
|
|
}
|
|
|
-
|
|
|
const start = (pageable.pageNum - 1) * pageable.pageSize;
|
|
|
const end = start + pageable.pageSize;
|
|
|
return images.slice(start, end);
|
|
|
});
|
|
|
|
|
|
-// 显示视频列表(分页后)
|
|
|
+// 显示视频列表(分页后):独立「视频」Tab 或 isFixed 为视频的相册 Tab
|
|
|
const displayVideos = computed(() => {
|
|
|
- if (activeTab.value !== "video") {
|
|
|
+ let videos: AlbumVideo[] = [];
|
|
|
+ if (activeTab.value === "video") {
|
|
|
+ videos = videoList.value || [];
|
|
|
+ } else if (isAlbumTab.value && currentAlbumIsVideo.value) {
|
|
|
+ videos = currentAlbumRef.value?.videos || [];
|
|
|
+ } else {
|
|
|
return [];
|
|
|
}
|
|
|
- const videos = videoList.value || [];
|
|
|
|
|
|
- // 如果没有视频,直接返回空数组
|
|
|
if (videos.length === 0) {
|
|
|
return [];
|
|
|
}
|
|
|
@@ -360,6 +383,11 @@ const displayVideos = computed(() => {
|
|
|
return videos.slice(start, end);
|
|
|
});
|
|
|
|
|
|
+const isFirstImage = (displayIndex: number): boolean => {
|
|
|
+ const actualIndex = (pageable.pageNum - 1) * pageable.pageSize + displayIndex;
|
|
|
+ return actualIndex === 0;
|
|
|
+};
|
|
|
+
|
|
|
// 新建相册表单
|
|
|
const createForm = reactive({
|
|
|
name: ""
|
|
|
@@ -462,8 +490,13 @@ const getVideoDuration = (file: File): Promise<number> => {
|
|
|
|
|
|
// 视频上传成功回调,用于保存视频时长
|
|
|
const handleVideoUploadSuccess = async (url: string) => {
|
|
|
- // 找到对应的视频文件
|
|
|
- const video = videoList.value.find((v: AlbumVideo) => v.url === url && !v.videoDuration);
|
|
|
+ const list: AlbumVideo[] =
|
|
|
+ activeTab.value === "video"
|
|
|
+ ? videoList.value
|
|
|
+ : currentAlbumRef.value && albumIsVideoAlbum(currentAlbumRef.value)
|
|
|
+ ? currentAlbumRef.value.videos || []
|
|
|
+ : [];
|
|
|
+ const video = list.find((v: AlbumVideo) => v.url === url && !v.videoDuration);
|
|
|
if (!video || !video.raw) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -488,20 +521,20 @@ const customVideoUploadApi = async (formData: FormData): Promise<any> => {
|
|
|
}
|
|
|
|
|
|
const fileSizeMB = file.size / 1024 / 1024;
|
|
|
- if (fileSizeMB > 500) {
|
|
|
- ElMessage.warning(`视频大小不能超过 500MB,当前为 ${fileSizeMB.toFixed(2)}MB`);
|
|
|
+ if (fileSizeMB > 50) {
|
|
|
+ ElMessage.warning(`视频大小不能超过 50MB,当前为 ${fileSizeMB.toFixed(2)}MB`);
|
|
|
throw new Error("视频大小超过限制");
|
|
|
}
|
|
|
|
|
|
- const urls = await uploadFilesToOss(file, "video");
|
|
|
- const fileUrl = urls[0];
|
|
|
+ const { fileUrl, coverUrl } = await uploadFileViaDevSimpleEndpoint(file);
|
|
|
if (!fileUrl) {
|
|
|
throw new Error("上传失败,未返回地址");
|
|
|
}
|
|
|
|
|
|
const response = {
|
|
|
- data: { fileUrl },
|
|
|
- fileUrl
|
|
|
+ data: { fileUrl, ...(coverUrl ? { coverUrl } : {}) },
|
|
|
+ fileUrl,
|
|
|
+ ...(coverUrl ? { coverUrl } : {})
|
|
|
};
|
|
|
await nextTick();
|
|
|
return response;
|
|
|
@@ -530,8 +563,8 @@ const saveImageToServer = async (imgUrl: string) => {
|
|
|
businessId = Number(storeId);
|
|
|
currentImages = environmentImages.value || [];
|
|
|
} else {
|
|
|
- // 相册图片:使用相册ID作为 businessId
|
|
|
- if (!currentAlbumRef.value) {
|
|
|
+ // 相册图片:仅非视频相册(isFixed 非视频)
|
|
|
+ if (!currentAlbumRef.value || albumIsVideoAlbum(currentAlbumRef.value)) {
|
|
|
return;
|
|
|
}
|
|
|
businessId = Number(currentAlbumRef.value.id);
|
|
|
@@ -593,11 +626,22 @@ const saveVideoToServer = async (videoUrl: string) => {
|
|
|
try {
|
|
|
const userInfo: any = localGet("geeker-user")?.userInfo || {};
|
|
|
const storeId = userInfo.storeId;
|
|
|
- if (!storeId || activeTab.value !== "video") {
|
|
|
+ if (!storeId) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const currentVideos = videoList.value || [];
|
|
|
+ let currentVideos: AlbumVideo[];
|
|
|
+ let businessId: number;
|
|
|
+
|
|
|
+ if (activeTab.value === "video") {
|
|
|
+ currentVideos = videoList.value || [];
|
|
|
+ businessId = Number(storeId);
|
|
|
+ } else if (currentAlbumRef.value && albumIsVideoAlbum(currentAlbumRef.value)) {
|
|
|
+ currentVideos = currentAlbumRef.value.videos || [];
|
|
|
+ businessId = Number(currentAlbumRef.value.id);
|
|
|
+ } else {
|
|
|
+ return;
|
|
|
+ }
|
|
|
// 找到对应的视频(通过URL匹配)
|
|
|
const targetVideo = currentVideos.find((video: AlbumVideo) => video.url === videoUrl && !video.imgId);
|
|
|
if (!targetVideo) {
|
|
|
@@ -609,10 +653,9 @@ const saveVideoToServer = async (videoUrl: string) => {
|
|
|
|
|
|
const imgSort = currentVideos.filter((video: AlbumVideo) => video.imgId).length + 1; // 排序为已保存视频数量+1
|
|
|
|
|
|
- // 视频:使用 storeId 作为 businessId(需要根据实际API调整)
|
|
|
const params = [
|
|
|
{
|
|
|
- businessId: Number(storeId), // 视频使用 storeId 作为 businessId
|
|
|
+ businessId,
|
|
|
imgSort: imgSort,
|
|
|
imgType: 3, // 视频类型为3
|
|
|
imgUrl: videoUrl,
|
|
|
@@ -751,7 +794,7 @@ const handleDeleteAlbum = async () => {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- await ElMessageBox.confirm("确认删除该相册吗?删除后相册内的所有图片也将被删除。", "提示", {
|
|
|
+ await ElMessageBox.confirm("确认删除该相册吗?删除后相册内的图片或视频也将一并删除。", "提示", {
|
|
|
confirmButtonText: "确定",
|
|
|
cancelButtonText: "取消",
|
|
|
type: "warning"
|
|
|
@@ -842,7 +885,13 @@ const deleteImage = async (index: number) => {
|
|
|
// 删除视频
|
|
|
const deleteVideo = async (index: number) => {
|
|
|
const actualIndex = (pageable.pageNum - 1) * pageable.pageSize + index;
|
|
|
- const video = videoList.value[actualIndex] as AlbumVideo;
|
|
|
+ const list: AlbumVideo[] =
|
|
|
+ activeTab.value === "video"
|
|
|
+ ? videoList.value
|
|
|
+ : currentAlbumRef.value && albumIsVideoAlbum(currentAlbumRef.value)
|
|
|
+ ? currentAlbumRef.value.videos || []
|
|
|
+ : [];
|
|
|
+ const video = list[actualIndex] as AlbumVideo;
|
|
|
if (!video) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -855,7 +904,7 @@ const deleteVideo = async (index: number) => {
|
|
|
|
|
|
const res: any = await deleteOfficialImg(params);
|
|
|
if (res && (res.code === 200 || res.code === "200")) {
|
|
|
- videoList.value.splice(actualIndex, 1);
|
|
|
+ list.splice(actualIndex, 1);
|
|
|
updatePagination();
|
|
|
ElMessage.success("删除成功");
|
|
|
} else {
|
|
|
@@ -863,7 +912,7 @@ const deleteVideo = async (index: number) => {
|
|
|
}
|
|
|
} else {
|
|
|
// 如果视频还没有保存到服务器,直接删除本地数据
|
|
|
- videoList.value.splice(actualIndex, 1);
|
|
|
+ list.splice(actualIndex, 1);
|
|
|
updatePagination();
|
|
|
ElMessage.success("删除成功");
|
|
|
}
|
|
|
@@ -873,23 +922,18 @@ const deleteVideo = async (index: number) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 判断是否是第一张图片(封面)
|
|
|
-const isFirstImage = (displayIndex: number): boolean => {
|
|
|
- // 计算在整个图片列表中的实际索引
|
|
|
- const actualIndex = (pageable.pageNum - 1) * pageable.pageSize + displayIndex;
|
|
|
- // 第一张图片(索引为0)是封面
|
|
|
- return actualIndex === 0;
|
|
|
-};
|
|
|
-
|
|
|
// 更新分页总数
|
|
|
const updatePagination = () => {
|
|
|
if (activeTab.value === "video") {
|
|
|
pageable.total = videoList.value?.length || 0;
|
|
|
} else if (activeTab.value === "environment") {
|
|
|
pageable.total = environmentImages.value?.length || 0;
|
|
|
+ } else if (isAlbumTab.value) {
|
|
|
+ pageable.total = currentAlbumIsVideo.value
|
|
|
+ ? currentAlbumRef.value?.videos?.length || 0
|
|
|
+ : currentAlbumRef.value?.images?.length || 0;
|
|
|
} else {
|
|
|
- const album = albumList.value.find(album => String(album.id) === activeTab.value);
|
|
|
- pageable.total = album?.images?.length || 0;
|
|
|
+ pageable.total = 0;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -904,21 +948,20 @@ const handleCurrentChange = (page: number) => {
|
|
|
pageable.pageNum = page;
|
|
|
};
|
|
|
|
|
|
-// 监听图片列表变化,更新分页并保存新上传的图片
|
|
|
+// 监听环境 Tab 或「图片相册」Tab 的图片列表
|
|
|
watch(
|
|
|
() => {
|
|
|
- // 根据当前tab返回对应的图片列表
|
|
|
if (activeTab.value === "environment") {
|
|
|
return environmentImages.value || [];
|
|
|
- } else if (activeTab.value !== "video") {
|
|
|
- // 相册tab
|
|
|
- return currentAlbumRef.value?.images || [];
|
|
|
+ }
|
|
|
+ if (isAlbumTab.value && currentAlbumRef.value && !albumIsVideoAlbum(currentAlbumRef.value)) {
|
|
|
+ return currentAlbumRef.value.images || [];
|
|
|
}
|
|
|
return [];
|
|
|
},
|
|
|
async (newImages, oldImages) => {
|
|
|
- // 如果不在图片 tab,不处理
|
|
|
- if (!isImageTab.value) {
|
|
|
+ const isImageAlbumTab = isAlbumTab.value && currentAlbumRef.value && !albumIsVideoAlbum(currentAlbumRef.value);
|
|
|
+ if (activeTab.value !== "environment" && !isImageAlbumTab) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -970,12 +1013,20 @@ watch(
|
|
|
{ deep: true }
|
|
|
);
|
|
|
|
|
|
-// 监听视频列表变化,更新分页并保存新上传的视频
|
|
|
+// 监听视频列表:独立「视频」Tab 或 isFixed 为视频的相册 Tab
|
|
|
watch(
|
|
|
- () => videoList.value || [],
|
|
|
+ () => {
|
|
|
+ if (activeTab.value === "video") {
|
|
|
+ return videoList.value || [];
|
|
|
+ }
|
|
|
+ if (isAlbumTab.value && currentAlbumRef.value && albumIsVideoAlbum(currentAlbumRef.value)) {
|
|
|
+ return currentAlbumRef.value.videos || [];
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ },
|
|
|
async (newVideos, oldVideos) => {
|
|
|
- // 如果不在视频 tab,不处理
|
|
|
- if (activeTab.value !== "video") {
|
|
|
+ const isVideoAlbumTab = isAlbumTab.value && currentAlbumRef.value && albumIsVideoAlbum(currentAlbumRef.value);
|
|
|
+ if (activeTab.value !== "video" && !isVideoAlbumTab) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -1082,6 +1133,7 @@ const getAlbumList = async () => {
|
|
|
id: album.id,
|
|
|
name: album.albumName || album.name,
|
|
|
albumName: album.albumName,
|
|
|
+ isFixed: album.isFixed,
|
|
|
images: reactive<AlbumImage[]>([]), // 使用 reactive 确保响应式
|
|
|
videos: reactive<AlbumVideo[]>([]),
|
|
|
coverImageId: album.coverImageId
|
|
|
@@ -1152,7 +1204,112 @@ const loadEnvironmentImages = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 加载视频列表
|
|
|
+const parseOfficialVideoListPayload = (payload: any): any[] => {
|
|
|
+ if (Array.isArray(payload)) return payload;
|
|
|
+ if (payload && Array.isArray(payload.list)) return payload.list;
|
|
|
+ if (payload && Array.isArray(payload.records)) return payload.records;
|
|
|
+ return [];
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析接口 `imgUrl`:
|
|
|
+ * - 若为对象或 JSON 字符串:`cover` = 封面图 URL,`video` = 视频 URL(与 getByStoreId 返回一致)
|
|
|
+ * - 若为普通字符串:视为单一视频直链,无封面
|
|
|
+ * - 支持外层多套 JSON 字符串引号包裹(多次序列化)
|
|
|
+ */
|
|
|
+const parseOfficialVideoImgUrlField = (imgUrlField: unknown): { videoUrl: string; coverUrl: string } => {
|
|
|
+ if (imgUrlField == null || imgUrlField === "") {
|
|
|
+ return { videoUrl: "", coverUrl: "" };
|
|
|
+ }
|
|
|
+ if (typeof imgUrlField === "object" && imgUrlField !== null) {
|
|
|
+ const o = imgUrlField as Record<string, unknown>;
|
|
|
+ return {
|
|
|
+ videoUrl: String(o.video ?? o.videoUrl ?? o.url ?? ""),
|
|
|
+ coverUrl: String(o.cover ?? o.coverUrl ?? o.poster ?? "")
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (typeof imgUrlField === "string") {
|
|
|
+ let s = imgUrlField.trim();
|
|
|
+ for (let i = 0; i < 5; i++) {
|
|
|
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
|
+ try {
|
|
|
+ const next = JSON.parse(s);
|
|
|
+ if (typeof next === "string") {
|
|
|
+ s = next.trim();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (s.startsWith("{")) {
|
|
|
+ try {
|
|
|
+ const o = JSON.parse(s) as Record<string, unknown>;
|
|
|
+ if (o && typeof o === "object") {
|
|
|
+ let videoUrl = String(o.video ?? o.videoUrl ?? o.url ?? "").trim();
|
|
|
+ let coverUrl = String(o.cover ?? o.coverUrl ?? o.poster ?? "").trim();
|
|
|
+ if (videoUrl.startsWith("{")) {
|
|
|
+ try {
|
|
|
+ const o2 = JSON.parse(videoUrl) as Record<string, unknown>;
|
|
|
+ videoUrl = String(o2.video ?? o2.videoUrl ?? o2.url ?? "").trim();
|
|
|
+ if (!coverUrl) {
|
|
|
+ coverUrl = String(o2.cover ?? o2.coverUrl ?? o2.poster ?? "").trim();
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { videoUrl, coverUrl };
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ /* 非 JSON 则当作直链 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { videoUrl: s, coverUrl: "" };
|
|
|
+ }
|
|
|
+ return { videoUrl: "", coverUrl: "" };
|
|
|
+};
|
|
|
+
|
|
|
+const mapOfficialVideoRowToAlbumVideo = (video: any): AlbumVideo => {
|
|
|
+ const parsed = parseOfficialVideoImgUrlField(video.imgUrl);
|
|
|
+ const fallback = video.videoUrl || video.url || video.fileUrl || "";
|
|
|
+ const url = (parsed.videoUrl || fallback).trim();
|
|
|
+ let coverUrl = (parsed.coverUrl || "").trim();
|
|
|
+ if (!coverUrl) {
|
|
|
+ coverUrl = String(
|
|
|
+ video.cover || video.coverUrl || video.poster || video.thumbnailUrl || video.thumbnail || video.videoCover || ""
|
|
|
+ ).trim();
|
|
|
+ }
|
|
|
+ const id = video.id;
|
|
|
+ const desc = video.imgDescription != null ? String(video.imgDescription).trim() : "";
|
|
|
+ const name = desc || (url ? url.split("/").pop()?.split("?")[0] || "video" : "video");
|
|
|
+ return {
|
|
|
+ uid: id || Date.now() + Math.random(),
|
|
|
+ name,
|
|
|
+ url,
|
|
|
+ status: "success",
|
|
|
+ imgId: id,
|
|
|
+ businessId: video.businessId,
|
|
|
+ imgSort: video.imgSort ?? video.sort,
|
|
|
+ imgType: video.imgType ?? 3,
|
|
|
+ videoDuration: video.videoDuration ?? video.duration ?? 0,
|
|
|
+ coverUrl: coverUrl || undefined,
|
|
|
+ imgDescription: desc || undefined
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+/** GET /alienStore/video/getByStoreId */
|
|
|
+const fetchStoreOfficialVideosRaw = async (storeId: number | string) => {
|
|
|
+ const videoRes: any = await getOfficialVideoByStoreId(storeId);
|
|
|
+ if (videoRes && (videoRes.code === 200 || videoRes.code === "200") && videoRes.data != null) {
|
|
|
+ return parseOfficialVideoListPayload(videoRes.data);
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+};
|
|
|
+
|
|
|
+// 加载视频列表(独立「视频」Tab)
|
|
|
const loadVideoList = async () => {
|
|
|
try {
|
|
|
isLoadingAlbumImages.value = true;
|
|
|
@@ -1162,32 +1319,12 @@ const loadVideoList = async () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 视频:使用 storeId 作为 businessId(需要根据实际API调整)
|
|
|
- // 或者如果视频不需要 businessId,可能需要使用其他API
|
|
|
- // 这里先使用 storeId 作为 businessId
|
|
|
- const videoRes: any = await getOfficialImgList(Number(storeId), Number(storeId), 3);
|
|
|
-
|
|
|
- if (videoRes && (videoRes.code === 200 || videoRes.code === "200") && videoRes.data) {
|
|
|
- const videoListData = Array.isArray(videoRes.data) ? videoRes.data : [];
|
|
|
- const videos: AlbumVideo[] = videoListData
|
|
|
- .filter((video: any) => video.deleteFlag !== 1)
|
|
|
- .map((video: any) => ({
|
|
|
- uid: video.id || Date.now() + Math.random(),
|
|
|
- name: video.imgUrl?.split("/").pop() || "video",
|
|
|
- url: video.imgUrl,
|
|
|
- status: "success",
|
|
|
- imgId: video.id,
|
|
|
- businessId: video.businessId,
|
|
|
- imgSort: video.imgSort,
|
|
|
- imgType: video.imgType,
|
|
|
- videoDuration: video.videoDuration || 0
|
|
|
- }))
|
|
|
- .sort((a: AlbumVideo, b: AlbumVideo) => (a.imgSort || 0) - (b.imgSort || 0));
|
|
|
- // 使用数组替换确保响应式更新
|
|
|
- videoList.value.splice(0, videoList.value.length, ...videos);
|
|
|
- } else {
|
|
|
- videoList.value.splice(0, videoList.value.length);
|
|
|
- }
|
|
|
+ const rawList = await fetchStoreOfficialVideosRaw(storeId);
|
|
|
+ const videos: AlbumVideo[] = rawList
|
|
|
+ .filter((video: any) => video.deleteFlag !== 1)
|
|
|
+ .map(mapOfficialVideoRowToAlbumVideo)
|
|
|
+ .sort((a: AlbumVideo, b: AlbumVideo) => (a.imgSort || 0) - (b.imgSort || 0));
|
|
|
+ videoList.value.splice(0, videoList.value.length, ...videos);
|
|
|
|
|
|
processedImages.value.clear();
|
|
|
} catch (error) {
|
|
|
@@ -1200,72 +1337,131 @@ const loadVideoList = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 加载相册图片列表(相册只有图片,没有视频)
|
|
|
-const loadAlbumImages = async (albumId: string) => {
|
|
|
- try {
|
|
|
- isLoadingAlbumImages.value = true;
|
|
|
+// 图片相册:getOfficialImgList(与改造前一致)
|
|
|
+const loadAlbumPicturesForAlbum = async (albumId: string) => {
|
|
|
+ const userInfo: any = localGet("geeker-user")?.userInfo || {};
|
|
|
+ const storeId = userInfo.storeId;
|
|
|
+ if (!storeId || !albumId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- const userInfo: any = localGet("geeker-user")?.userInfo || {};
|
|
|
- const storeId = userInfo.storeId;
|
|
|
- if (!storeId || !albumId) {
|
|
|
- return;
|
|
|
+ const current = albumList.value.find(album => String(album.id) === albumId);
|
|
|
+ if (!current) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const listImgType = isAlbumTabImgType4(albumId) ? OFFICIAL_IMG_TYPE_ENV_OR_SPECIAL_ALBUM : OFFICIAL_IMG_TYPE_ALBUM;
|
|
|
+ const imageRes: any = await getOfficialImgList(Number(albumId), Number(storeId), listImgType);
|
|
|
+
|
|
|
+ if (imageRes && (imageRes.code === 200 || imageRes.code === "200") && imageRes.data) {
|
|
|
+ const imageList = Array.isArray(imageRes.data) ? imageRes.data : [];
|
|
|
+ const images: AlbumImage[] = imageList
|
|
|
+ .filter((img: any) => img.deleteFlag !== 1)
|
|
|
+ .map((img: any) => ({
|
|
|
+ uid: img.id || Date.now() + Math.random(),
|
|
|
+ name: img.imgUrl?.split("/").pop() || "image",
|
|
|
+ url: img.imgUrl,
|
|
|
+ status: "success",
|
|
|
+ imgId: img.id,
|
|
|
+ businessId: img.businessId,
|
|
|
+ imgSort: img.imgSort,
|
|
|
+ imgType: img.imgType,
|
|
|
+ isCover: false
|
|
|
+ }))
|
|
|
+ .sort((a: AlbumImage, b: AlbumImage) => (a.imgSort || 0) - (b.imgSort || 0));
|
|
|
+ if (!current.images) {
|
|
|
+ current.images = reactive<AlbumImage[]>([]);
|
|
|
}
|
|
|
+ current.images.splice(0, current.images.length, ...images);
|
|
|
+ } else {
|
|
|
+ if (!current.images) {
|
|
|
+ current.images = reactive<AlbumImage[]>([]);
|
|
|
+ } else {
|
|
|
+ current.images.splice(0, current.images.length);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!current.videos) {
|
|
|
+ current.videos = reactive<AlbumVideo[]>([]);
|
|
|
+ } else {
|
|
|
+ current.videos.splice(0, current.videos.length);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 视频相册:getOfficialVideoByStoreId,按 businessId 与相册 id 匹配
|
|
|
+const loadAlbumVideosForAlbum = async (albumId: string) => {
|
|
|
+ const userInfo: any = localGet("geeker-user")?.userInfo || {};
|
|
|
+ const storeId = userInfo.storeId;
|
|
|
+ if (!storeId || !albumId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const current = albumList.value.find(album => String(album.id) === albumId);
|
|
|
+ if (!current) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rawList = await fetchStoreOfficialVideosRaw(storeId);
|
|
|
+ const albumKey = String(albumId);
|
|
|
+ const forAlbum = rawList.filter((v: any) => {
|
|
|
+ if (v.deleteFlag === 1) return false;
|
|
|
+ const bid = v.businessId ?? v.albumId ?? v.officialAlbumId;
|
|
|
+ return bid != null && String(bid) === albumKey;
|
|
|
+ });
|
|
|
+ const videos: AlbumVideo[] = forAlbum
|
|
|
+ .map(mapOfficialVideoRowToAlbumVideo)
|
|
|
+ .sort((a: AlbumVideo, b: AlbumVideo) => (a.imgSort || 0) - (b.imgSort || 0));
|
|
|
|
|
|
- const listImgType = isAlbumTabImgType4(albumId) ? OFFICIAL_IMG_TYPE_ENV_OR_SPECIAL_ALBUM : OFFICIAL_IMG_TYPE_ALBUM;
|
|
|
- const imageRes: any = await getOfficialImgList(Number(albumId), Number(storeId), listImgType);
|
|
|
+ if (!current.videos) {
|
|
|
+ current.videos = reactive<AlbumVideo[]>([]);
|
|
|
+ }
|
|
|
+ current.videos.splice(0, current.videos.length, ...videos);
|
|
|
+
|
|
|
+ if (!current.images) {
|
|
|
+ current.images = reactive<AlbumImage[]>([]);
|
|
|
+ } else {
|
|
|
+ current.images.splice(0, current.images.length);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const loadAlbumImages = async (albumId: string) => {
|
|
|
+ try {
|
|
|
+ isLoadingAlbumImages.value = true;
|
|
|
|
|
|
const current = albumList.value.find(album => String(album.id) === albumId);
|
|
|
if (!current) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 处理图片列表
|
|
|
- if (imageRes && (imageRes.code === 200 || imageRes.code === "200") && imageRes.data) {
|
|
|
- const imageList = Array.isArray(imageRes.data) ? imageRes.data : [];
|
|
|
- const images: AlbumImage[] = imageList
|
|
|
- .filter((img: any) => img.deleteFlag !== 1)
|
|
|
- .map((img: any) => ({
|
|
|
- uid: img.id || Date.now() + Math.random(),
|
|
|
- name: img.imgUrl?.split("/").pop() || "image",
|
|
|
- url: img.imgUrl,
|
|
|
- status: "success",
|
|
|
- imgId: img.id,
|
|
|
- businessId: img.businessId,
|
|
|
- imgSort: img.imgSort,
|
|
|
- imgType: img.imgType,
|
|
|
- isCover: false
|
|
|
- }))
|
|
|
- .sort((a: AlbumImage, b: AlbumImage) => (a.imgSort || 0) - (b.imgSort || 0));
|
|
|
- // 使用 splice 方法替换数组内容,确保响应式更新
|
|
|
- if (!current.images) {
|
|
|
- current.images = reactive<AlbumImage[]>([]);
|
|
|
- }
|
|
|
- current.images.splice(0, current.images.length, ...images);
|
|
|
+ if (albumIsVideoAlbum(current)) {
|
|
|
+ await loadAlbumVideosForAlbum(albumId);
|
|
|
} else {
|
|
|
- if (!current.images) {
|
|
|
- current.images = reactive<AlbumImage[]>([]);
|
|
|
- } else {
|
|
|
- current.images.splice(0, current.images.length);
|
|
|
- }
|
|
|
+ await loadAlbumPicturesForAlbum(albumId);
|
|
|
}
|
|
|
|
|
|
- // 清空已处理图片集合,因为加载的是服务器数据,不需要再保存
|
|
|
processedImages.value.clear();
|
|
|
} catch (error) {
|
|
|
- console.error("获取相册图片列表失败:", error);
|
|
|
+ console.error("获取相册媒体列表失败:", error);
|
|
|
const current = albumList.value.find(album => String(album.id) === albumId);
|
|
|
if (current) {
|
|
|
processedImages.value.clear();
|
|
|
- if (!current.images) {
|
|
|
- current.images = reactive<AlbumImage[]>([]);
|
|
|
+ if (albumIsVideoAlbum(current)) {
|
|
|
+ if (!current.videos) {
|
|
|
+ current.videos = reactive<AlbumVideo[]>([]);
|
|
|
+ } else {
|
|
|
+ current.videos.splice(0, current.videos.length);
|
|
|
+ }
|
|
|
} else {
|
|
|
- current.images.splice(0, current.images.length);
|
|
|
+ if (!current.images) {
|
|
|
+ current.images = reactive<AlbumImage[]>([]);
|
|
|
+ } else {
|
|
|
+ current.images.splice(0, current.images.length);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
} finally {
|
|
|
isLoadingAlbumImages.value = false;
|
|
|
await nextTick();
|
|
|
- // 更新分页信息,确保视图刷新
|
|
|
updatePagination();
|
|
|
}
|
|
|
};
|
|
|
@@ -1402,6 +1598,16 @@ onMounted(async () => {
|
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
|
}
|
|
|
+ .media-cover-thumb {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 1;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ pointer-events: none;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
.media-placeholder {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
@@ -1418,6 +1624,7 @@ onMounted(async () => {
|
|
|
.media-overlay {
|
|
|
position: absolute;
|
|
|
inset: 0;
|
|
|
+ z-index: 2;
|
|
|
display: flex;
|
|
|
gap: 12px;
|
|
|
align-items: center;
|