新增对视频的支持
This commit is contained in:
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -1 +1 @@
|
|||||||
version=1.0.0-SNAPSHOT
|
version=1.5.1-1
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user