feat: 新增视频床与文件床,视频支持 HLS 转码
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
文件床 - 新增 files/ 存储前缀与 media_type 索引列,图片、视频、文件三类资源独立 - 新增文件上传、列表、删除、批量删除接口,禁止 html/htm/svg - 支持公开直链与 Token 访问两种模式,后台可切换,直链强制附件下载 视频床 - 新增 videos/ 存储前缀与视频上传接口,单文件上限 500MB - 接入 ffmpeg 转码队列,并发数与单任务核心数可配,默认串行 - 五档转码质量:直接封装 / 速度 / 平衡 / 质量 / 压缩(压缩档可调码率比例) - 码率控制避免转码后体积反超源文件,音频跟随源音轨不放大不压缩 - 转码进度实时上报,成功后删除源文件,失败时保留源文件与可用链接 - 上传时自动抽帧生成封面,视频与封面各有独立链接 - 删除视频时中断转码并清理切片、封面与源文件 媒体库 - 图库、视频库、文件库三套独立统计,含趋势、占用与来源分布 - 统一筛选、多选、批量删除、批量标签、网格/列表切换,显示方式按类型分别记忆 - 视频详情对齐图片详情,视频库支持在线播放(hls.js,兼容回退原文件) 修复 - 清理无主的切片、封面与开发期遗留的 ffmpeg 进程 - 源文件按设计删除后,索引不再被启动孤儿清理误删 - 视频统计口径与列表一致,转码中的视频不计入统计 - 转码失败时回收已写入的部分切片,避免残留 - 设置页停用检查更新(与上游版本号已不对应) 其他 - Docker 镜像安装 ffmpeg - git remote 指向内网仓库
This commit is contained in:
1 parent
05cc3895f1
commit
89ea2fb25d
61 files changed
+6637
-154
No files matched your search
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
import uploadCardBg from '~/assets/image/upload-card-bg.webp'
|
||||
import uploadCardHero from '~/assets/image/upload-card-hero.webp'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
hint?: string
|
||||
formats?: string
|
||||
accept?: string
|
||||
heroImage?: string
|
||||
backgroundImage?: string
|
||||
showSettings?: boolean
|
||||
pasteEnabled?: boolean
|
||||
/** 拖拽/选择时的类型限制;图片页与视频页各自限定,文件页放开 */
|
||||
mediaKind?: 'image' | 'video' | 'any'
|
||||
}>(), {
|
||||
accept: 'image/jpeg,image/png,image/webp,image/gif,image/svg+xml,image/x-icon,.svg,.ico',
|
||||
showSettings: true,
|
||||
pasteEnabled: true,
|
||||
mediaKind: 'image'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'upload': [files: File[]]
|
||||
'open-settings': []
|
||||
}>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const dragging = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
const { bindPaste } = useClipboardImage(ref(null))
|
||||
|
||||
bindPaste((files) => {
|
||||
if (props.disabled || !props.pasteEnabled) return
|
||||
emit('upload', files)
|
||||
})
|
||||
|
||||
const heroSource = computed(() => props.heroImage ?? uploadCardHero)
|
||||
const backgroundSource = computed(() => props.backgroundImage ?? uploadCardBg)
|
||||
const titleText = computed(() => props.title ?? t('upload.title'))
|
||||
const hintText = computed(() => props.hint ?? t('upload.hint', { ctrl: 'Ctrl', v: 'V' }))
|
||||
const formatsText = computed(() => props.formats ?? t('upload.formats'))
|
||||
|
||||
function matchesMediaKind(file: File): boolean {
|
||||
if (props.mediaKind === 'any') return true
|
||||
if (!file.type) return true
|
||||
return file.type.startsWith(`${props.mediaKind}/`)
|
||||
}
|
||||
|
||||
function openFilePicker() {
|
||||
if (props.disabled) return
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
function onFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = input.files ? Array.from(input.files) : []
|
||||
if (files.length) emit('upload', files)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
dragging.value = false
|
||||
if (props.disabled) return
|
||||
|
||||
const files = Array.from(event.dataTransfer?.files ?? []).filter(matchesMediaKind)
|
||||
if (files.length) emit('upload', files)
|
||||
}
|
||||
|
||||
function onDragOver(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
if (!props.disabled) dragging.value = true
|
||||
}
|
||||
|
||||
function onDragLeave() {
|
||||
dragging.value = false
|
||||
}
|
||||
|
||||
function openSettings(event: Event) {
|
||||
event.stopPropagation()
|
||||
if (props.disabled) return
|
||||
emit('open-settings')
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (props.disabled) return
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
openFilePicker()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="upload-card-surface group/upload relative cursor-pointer overflow-hidden text-center outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
:class="{ 'upload-card-surface--active': dragging }"
|
||||
:aria-disabled="disabled"
|
||||
@click="openFilePicker"
|
||||
@keydown="onKeydown"
|
||||
@drop="onDrop"
|
||||
@dragover="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
multiple
|
||||
class="hidden"
|
||||
:disabled="disabled"
|
||||
@change="onFileChange"
|
||||
>
|
||||
|
||||
<img
|
||||
:src="backgroundSource"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute inset-0 size-full object-cover opacity-100 dark:opacity-[0.12]"
|
||||
decoding="async"
|
||||
draggable="false"
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="showSettings"
|
||||
class="absolute top-3 right-3 z-20"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="upload-settings-btn inline-flex size-8 items-center justify-center rounded-md bg-default/70 text-muted backdrop-blur-sm transition-colors hover:bg-elevated hover:text-highlighted disabled:pointer-events-none disabled:opacity-40"
|
||||
:aria-label="t('upload.settings')"
|
||||
:disabled="disabled"
|
||||
@click="openSettings"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-settings"
|
||||
class="upload-settings-icon size-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 mx-auto flex max-w-lg flex-col items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<img
|
||||
:src="heroSource"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="h-24 w-auto -mb-3 select-none transition-transform duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] sm:-mb-4 sm:h-28 md:h-32"
|
||||
:class="dragging ? 'scale-105' : 'group-hover/upload:scale-[1.02]'"
|
||||
decoding="async"
|
||||
draggable="false"
|
||||
>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xl font-semibold tracking-tight text-highlighted sm:text-2xl">
|
||||
{{ titleText }}
|
||||
</p>
|
||||
<p class="text-sm text-muted sm:text-base">
|
||||
{{ hintText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 max-w-md text-xs leading-relaxed text-dimmed sm:text-sm">
|
||||
{{ formatsText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user