新增对视频的支持

This commit is contained in:
anian
2026-03-09 22:13:36 +08:00
parent 65174cb3df
commit bb72928738
6 changed files with 93 additions and 35 deletions
+11
View File
@@ -1,3 +1,14 @@
# anian-plugin-photos
> 基于原作者v1.5.1代码修改
#### v1.5.1-1
1. 新增对视频的支持
# plugin-photos # plugin-photos
Halo 2.0 的相册管理插件, 支持在 Console 进行管理以及为主题端提供 `/photos` 页面路由。 Halo 2.0 的相册管理插件, 支持在 Console 进行管理以及为主题端提供 `/photos` 页面路由。
+1 -1
View File
@@ -18,7 +18,7 @@ repositories {
} }
dependencies { dependencies {
implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT') implementation platform('run.halo.tools.platform:plugin:2.20.11')
compileOnly 'run.halo.app:api' compileOnly 'run.halo.app:api'
testImplementation 'run.halo.app:api' testImplementation 'run.halo.app:api'
+56 -10
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from "vue"; import { onMounted, ref, computed, watch } from "vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -17,34 +17,80 @@ const props = withDefaults(
const isLoading = ref(false); const isLoading = ref(false);
const error = ref(false); const error = ref(false);
const loadImage = async () => { // 更加严谨的后缀判断
const image = new Image(); const isVideo = computed(() => {
image.src = props.src; if (!props.src) return false;
const videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'avi'];
try {
const url = new URL(props.src, window.location.origin);
const pathname = url.pathname;
const extension = pathname.split('.').pop()?.toLowerCase();
return extension ? videoExtensions.includes(extension) : false;
} catch (e) {
// 如果 URL 格式不规范,回退到简单字符串判断
return videoExtensions.some(ext => props.src.toLowerCase().includes(`.${ext}`));
}
});
const loadMedia = () => {
if (!props.src) return Promise.reject("Empty src");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
image.onload = () => resolve(image); if (isVideo.value) {
image.onerror = (err) => reject(err); const video = document.createElement('video');
video.src = props.src;
video.preload = 'metadata';
video.onloadeddata = () => resolve(true);
video.onerror = () => reject(new Error("Video load error"));
} else {
const image = new Image();
image.src = props.src;
image.onload = () => resolve(true);
image.onerror = () => reject(new Error("Image load error"));
}
}); });
}; };
onMounted(async () => { const init = async () => {
if (!props.src) return;
isLoading.value = true; isLoading.value = true;
error.value = false;
try { try {
await loadImage(); await loadMedia();
} catch (e) { } catch (e) {
console.warn("Media preview load failed:", props.src);
error.value = true; error.value = true;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
}); };
onMounted(init);
// 如果 src 发生变化(比如在弹窗中切换上一张/下一张),重新加载
watch(() => props.src, init);
</script> </script>
<template> <template>
<div :class="classes"> <div :class="classes">
<template v-if="isLoading"> <template v-if="isLoading">
<slot name="loading"> loading... </slot> <slot name="loading"> loading... </slot>
</template> </template>
<template v-else-if="error"> <template v-else-if="error">
<slot name="error"> error </slot> <slot name="error">
<div class=":uno: flex items-center justify-center text-red-400 text-xs">加载失败</div>
</slot>
</template> </template>
<video
v-else-if="isVideo"
class=":uno: size-full object-cover"
:src="`${src}#t=0.1`"
muted
playsinline
preload="metadata"
></video>
<img v-else class=":uno: size-full object-cover" :src="src" :alt="alt" /> <img v-else class=":uno: size-full object-cover" :src="src" :alt="alt" />
</div> </div>
</template> </template>
+21 -19
View File
@@ -240,26 +240,28 @@ const onAttachmentsSelect = async (attachments: AttachmentLike[]) => {
type?: string; type?: string;
}[]; }[];
for (const photo of photos) { for (const photo of photos) {
const type = photo.type; const type = photo.type;
if (!type) {
Toast.error("只支持选择图片");
nextTick(() => {
attachmentModal.value = true;
});
return; if (!type) {
} Toast.error("只支持选择图片或视频");
const fileType = type.split("/")[0]; nextTick(() => {
if (fileType !== "image") { attachmentModal.value = true;
Toast.error("只支持选择图片"); });
nextTick(() => { return;
attachmentModal.value = true;
});
return;
}
} }
const fileType = type.split("/")[0];
if (fileType !== "image" && fileType !== "video") {
Toast.error("只支持选择图片或视频");
nextTick(() => {
attachmentModal.value = true;
});
return;
}
}
const createRequests = photos.map((photo) => { const createRequests = photos.map((photo) => {
return axiosInstance.post<Photo>("/apis/core.halo.run/v1alpha1/photos", { return axiosInstance.post<Photo>("/apis/core.halo.run/v1alpha1/photos", {
metadata: { metadata: {
@@ -310,7 +312,7 @@ const onEditingModalClose = () => {
</span> </span>
</template> </template>
</PhotoEditingModal> </PhotoEditingModal>
<AttachmentSelectorModal v-model:visible="attachmentModal" :accepts="['image/*']" @select="onAttachmentsSelect" /> <AttachmentSelectorModal v-model:visible="attachmentModal" :accepts="['image/*', 'video/*']" @select="onAttachmentsSelect" />
<VPageHeader title="图库"> <VPageHeader title="图库">
<template #icon> <template #icon>
<RiImage2Line /> <RiImage2Line />
@@ -383,7 +385,7 @@ const onEditingModalClose = () => {
@click="handleOpenEditingModal(photo)" @click="handleOpenEditingModal(photo)"
> >
<div class=":uno: group relative bg-white"> <div class=":uno: group relative bg-white">
<div class=":uno: block aspect-16/9 size-full cursor-pointer overflow-hidden bg-gray-100"> <div class=":uno: block aspect-1/1 size-full cursor-pointer overflow-hidden bg-gray-100">
<LazyImage <LazyImage
:key="photo.metadata.name" :key="photo.metadata.name"
:alt="photo.spec.displayName" :alt="photo.spec.displayName"
+1 -1
View File
@@ -1 +1 @@
version=1.0.0-SNAPSHOT version=1.5.1-1
-1
View File
@@ -8,7 +8,6 @@ metadata:
"store.halo.run/app-id": "app-BmQJW" "store.halo.run/app-id": "app-BmQJW"
spec: spec:
enabled: true enabled: true
version: 1.0.0
requires: ">=2.21.0" requires: ">=2.21.0"
author: author:
name: Halo name: Halo