file列表页预览窗口支持预览常见在线视频服务商的视频,比如youtube,vimeo,哔哩哔哩,优酷,搜狐,爱奇艺等
This commit is contained in:
parent
d4b80a334c
commit
fa00f39550
@ -10,6 +10,7 @@ import { computed } from 'vue'
|
||||
import { NodeViewWrapper } from '@tiptap/vue-3'
|
||||
import type { NodeViewProps } from '@tiptap/vue-3'
|
||||
import VideoPreview from '@/views/pagetype/file/VideoPreview.vue'
|
||||
import { detectVideoProvider } from '@/shared/utils/videoProviders'
|
||||
|
||||
const props = defineProps<NodeViewProps>()
|
||||
|
||||
@ -18,34 +19,8 @@ const hasSrc = computed(() => !!src.value)
|
||||
|
||||
// ── URL 检测 ──
|
||||
|
||||
function getYouTubeId(url: string): string | null {
|
||||
const patterns = [
|
||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
||||
/^([a-zA-Z0-9_-]{11})$/,
|
||||
]
|
||||
for (const p of patterns) {
|
||||
const m = url.match(p)
|
||||
if (m?.[1]) return m[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getVimeoId(url: string): string | null {
|
||||
const m = url.match(/(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/)
|
||||
return m?.[1] || null
|
||||
}
|
||||
|
||||
const isYouTube = computed(() => !!getYouTubeId(src.value))
|
||||
const isVimeo = computed(() => !!getVimeoId(src.value))
|
||||
const isEmbedUrl = computed(() => isYouTube.value || isVimeo.value)
|
||||
|
||||
const embedSrc = computed(() => {
|
||||
const ytId = getYouTubeId(src.value)
|
||||
if (ytId) return `https://www.youtube-nocookie.com/embed/${ytId}`
|
||||
const vId = getVimeoId(src.value)
|
||||
if (vId) return `https://player.vimeo.com/video/${vId}`
|
||||
return ''
|
||||
})
|
||||
const videoProvider = computed(() => detectVideoProvider(src.value))
|
||||
const isEmbedUrl = computed(() => !!videoProvider.value)
|
||||
|
||||
function getFileName(url: string): string {
|
||||
const parts = url.split('/')
|
||||
@ -59,13 +34,13 @@ function getFileName(url: string): string {
|
||||
data-type="videoBlock"
|
||||
class="video-block-nodeview"
|
||||
>
|
||||
<!-- YouTube / Vimeo 嵌入 -->
|
||||
<!-- 在线视频嵌入(YouTube / Vimeo / Bilibili 等) -->
|
||||
<div
|
||||
v-if="hasSrc && isEmbedUrl"
|
||||
class="video-block-embed"
|
||||
>
|
||||
<iframe
|
||||
:src="embedSrc"
|
||||
:src="videoProvider!.embedUrl"
|
||||
class="video-block-iframe"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowfullscreen
|
||||
|
||||
97
frontend/src/shared/utils/videoProviders.ts
Normal file
97
frontend/src/shared/utils/videoProviders.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* videoProviders.ts — 在线视频服务商 URL 检测与嵌入
|
||||
*
|
||||
* 与后端 File.is_remote_file 配合:当 file_url 以 http/https 开头,
|
||||
* 后端不下载文件直接存为远程记录。此工具检测 URL 域名并生成对应的
|
||||
* iframe embed URL,供文件预览和 Block Editor 视频块共享使用。
|
||||
*/
|
||||
|
||||
export interface VideoProvider {
|
||||
name: string
|
||||
embedUrl: string
|
||||
}
|
||||
|
||||
// ── 各平台 ID 提取与 embed URL 构建 ──
|
||||
|
||||
const YOUTUBE_PATTERNS = [
|
||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
||||
/^([a-zA-Z0-9_-]{11})$/,
|
||||
]
|
||||
|
||||
function detectYouTube(url: string): VideoProvider | null {
|
||||
for (const p of YOUTUBE_PATTERNS) {
|
||||
const m = url.match(p)
|
||||
if (m?.[1]) {
|
||||
return { name: 'YouTube', embedUrl: `https://www.youtube-nocookie.com/embed/${m[1]}` }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectVimeo(url: string): VideoProvider | null {
|
||||
const m = url.match(/(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/)
|
||||
if (m?.[1]) {
|
||||
return { name: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${m[1]}` }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectBilibili(url: string): VideoProvider | null {
|
||||
// bilibili.com/video/BVxxx/ 或 b23.tv/BVxxx
|
||||
const m = url.match(/(?:bilibili\.com\/video\/|b23\.tv\/)(BV[a-zA-Z0-9]+)/)
|
||||
if (m?.[1]) {
|
||||
return { name: 'Bilibili', embedUrl: `https://player.bilibili.com/player.html?bvid=${m[1]}` }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectYouku(url: string): VideoProvider | null {
|
||||
// v.youku.com/v_show/id_Xxxxxxxxxx.html
|
||||
const m = url.match(/v\.youku\.com\/v_show\/id_([a-zA-Z0-9=]+)\.html/)
|
||||
if (m?.[1]) {
|
||||
return { name: 'Youku', embedUrl: `https://player.youku.com/embed/${m[1]}` }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectIQiyi(url: string): VideoProvider | null {
|
||||
// www.iqiyi.com/v_videoid.html
|
||||
const m = url.match(/iqiyi\.com\/(?:v_|)([a-zA-Z0-9]+)\.html/)
|
||||
if (m?.[1]) {
|
||||
return { name: 'iQiyi', embedUrl: `https://player.iqiyi.com/player/player.html?vid=${m[1]}` }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function detectSohu(url: string): VideoProvider | null {
|
||||
// tv.sohu.com/v/...html 或 my.tv.sohu.com/...
|
||||
if (/tv\.sohu\.com/.test(url)) {
|
||||
// Sohu embed URL 格式不固定,返回原始 URL 直接 iframe
|
||||
return { name: 'Sohu', embedUrl: url }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ── 统一检测入口 ──
|
||||
|
||||
const DETECTORS: Array<(url: string) => VideoProvider | null> = [
|
||||
detectYouTube,
|
||||
detectVimeo,
|
||||
detectBilibili,
|
||||
detectYouku,
|
||||
detectIQiyi,
|
||||
detectSohu,
|
||||
]
|
||||
|
||||
/**
|
||||
* 检测 URL 是否匹配已知在线视频服务商。
|
||||
* 匹配成功返回 { name, embedUrl },失败返回 null。
|
||||
*/
|
||||
export function detectVideoProvider(url: string): VideoProvider | null {
|
||||
if (!url || !/^https?:\/\//.test(url)) return null
|
||||
for (const detect of DETECTORS) {
|
||||
const result = detect(url)
|
||||
if (result) return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -72,6 +72,15 @@
|
||||
@error="onPresentationError"
|
||||
/>
|
||||
</div>
|
||||
<!-- 在线视频嵌入 -->
|
||||
<div v-else-if="onlineVideo" class="visual-online-video-wrapper">
|
||||
<iframe
|
||||
:src="onlineVideo.embedUrl"
|
||||
class="visual-online-video-iframe"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowfullscreen
|
||||
/>
|
||||
</div>
|
||||
<!-- 视频预览 -->
|
||||
<div v-else-if="isVideoFile" class="visual-video-wrapper">
|
||||
<VideoPreview
|
||||
@ -199,6 +208,7 @@
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { detectVideoProvider } from '@/shared/utils/videoProviders'
|
||||
import axios from 'axios'
|
||||
|
||||
const SpreadsheetPreview = defineAsyncComponent(() => import('./SpreadsheetPreview.vue'))
|
||||
@ -237,6 +247,11 @@ const isPresentationFile = computed(() => PRESENTATION_EXTENSIONS.includes((prop
|
||||
const isVideoFile = computed(() => VIDEO_EXTENSIONS.includes((props.row?.file_type || '').toLowerCase()))
|
||||
const isAudioFile = computed(() => AUDIO_EXTENSIONS.includes((props.row?.file_type || '').toLowerCase()))
|
||||
|
||||
const onlineVideo = computed(() => {
|
||||
const url = props.row?.file_url
|
||||
return url ? detectVideoProvider(url) : null
|
||||
})
|
||||
|
||||
const canPreview = computed(() => isImageFile.value || isPdfFile.value || isDocxFile.value || isSpreadsheetFile.value || isTextFile.value || isPresentationFile.value || isVideoFile.value || isAudioFile.value)
|
||||
|
||||
const EDITABLE_EXTENSIONS = ['docx', 'xls', 'xlsx', 'csv', 'txt']
|
||||
@ -530,6 +545,7 @@ onUnmounted(() => {
|
||||
.preview-visual:has(.visual-pdf-wrapper),
|
||||
.preview-visual:has(.visual-document-wrapper),
|
||||
.preview-visual:has(.visual-presentation-wrapper),
|
||||
.preview-visual:has(.visual-online-video-wrapper),
|
||||
.preview-visual:has(.visual-video-wrapper),
|
||||
.preview-visual:has(.visual-audio-wrapper) {
|
||||
min-height: 400px;
|
||||
@ -539,6 +555,7 @@ onUnmounted(() => {
|
||||
.preview-visual:has(.visual-spreadsheet-wrapper),
|
||||
.preview-visual:has(.visual-text-wrapper),
|
||||
.preview-visual:has(.visual-presentation-wrapper),
|
||||
.preview-visual:has(.visual-online-video-wrapper),
|
||||
.preview-visual:has(.visual-video-wrapper),
|
||||
.preview-visual:has(.visual-audio-wrapper) {
|
||||
align-items: stretch;
|
||||
@ -632,6 +649,20 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 在线视频嵌入 */
|
||||
.visual-online-video-wrapper {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.visual-online-video-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 音频预览 — 允许音量弹窗向上溢出 */
|
||||
.visual-audio-wrapper {
|
||||
width: 100%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user