feat: update image and video sending
This commit is contained in:
parent
8d0c187bbf
commit
eb32e31cc1
@ -237,6 +237,7 @@
|
|||||||
:streaming-message="composerBusy"
|
:streaming-message="composerBusy"
|
||||||
:input-locked="displayLockEngaged"
|
:input-locked="displayLockEngaged"
|
||||||
:uploading="uploading"
|
:uploading="uploading"
|
||||||
|
:media-uploading="mediaUploading"
|
||||||
:thinking-mode="thinkingMode"
|
:thinking-mode="thinkingMode"
|
||||||
:run-mode="resolvedRunMode"
|
:run-mode="resolvedRunMode"
|
||||||
:model-menu-open="modelMenuOpen"
|
:model-menu-open="modelMenuOpen"
|
||||||
@ -310,8 +311,10 @@
|
|||||||
:entries="imageEntries"
|
:entries="imageEntries"
|
||||||
:initial-selected="selectedImages"
|
:initial-selected="selectedImages"
|
||||||
:loading="imageLoading"
|
:loading="imageLoading"
|
||||||
|
:uploading="mediaUploading"
|
||||||
@close="closeImagePicker"
|
@close="closeImagePicker"
|
||||||
@confirm="handleImagesConfirmed"
|
@confirm="handleImagesConfirmed"
|
||||||
|
@local-files="handleLocalImageFiles"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="overlay-fade">
|
<transition name="overlay-fade">
|
||||||
@ -321,8 +324,10 @@
|
|||||||
:entries="videoEntries"
|
:entries="videoEntries"
|
||||||
:initial-selected="selectedVideos"
|
:initial-selected="selectedVideos"
|
||||||
:loading="videoLoading"
|
:loading="videoLoading"
|
||||||
|
:uploading="mediaUploading"
|
||||||
@close="closeVideoPicker"
|
@close="closeVideoPicker"
|
||||||
@confirm="handleVideosConfirmed"
|
@confirm="handleVideosConfirmed"
|
||||||
|
@local-files="handleLocalVideoFiles"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="overlay-fade">
|
<transition name="overlay-fade">
|
||||||
|
|||||||
@ -510,7 +510,7 @@ const appOptions = {
|
|||||||
'usageQuota'
|
'usageQuota'
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useFocusStore, ['focusedFiles']),
|
...mapWritableState(useFocusStore, ['focusedFiles']),
|
||||||
...mapWritableState(useUploadStore, ['uploading'])
|
...mapWritableState(useUploadStore, ['uploading', 'mediaUploading'])
|
||||||
,
|
,
|
||||||
...mapState(useMonitorStore, {
|
...mapState(useMonitorStore, {
|
||||||
monitorIsLocked: (store) => store.isLocked
|
monitorIsLocked: (store) => store.isLocked
|
||||||
@ -838,10 +838,12 @@ const appOptions = {
|
|||||||
inputSetMultiline: 'setInputMultiline',
|
inputSetMultiline: 'setInputMultiline',
|
||||||
inputSetImagePickerOpen: 'setImagePickerOpen',
|
inputSetImagePickerOpen: 'setImagePickerOpen',
|
||||||
inputSetSelectedImages: 'setSelectedImages',
|
inputSetSelectedImages: 'setSelectedImages',
|
||||||
|
inputAddSelectedImage: 'addSelectedImage',
|
||||||
inputClearSelectedImages: 'clearSelectedImages',
|
inputClearSelectedImages: 'clearSelectedImages',
|
||||||
inputRemoveSelectedImage: 'removeSelectedImage',
|
inputRemoveSelectedImage: 'removeSelectedImage',
|
||||||
inputSetVideoPickerOpen: 'setVideoPickerOpen',
|
inputSetVideoPickerOpen: 'setVideoPickerOpen',
|
||||||
inputSetSelectedVideos: 'setSelectedVideos',
|
inputSetSelectedVideos: 'setSelectedVideos',
|
||||||
|
inputAddSelectedVideo: 'addSelectedVideo',
|
||||||
inputClearSelectedVideos: 'clearSelectedVideos',
|
inputClearSelectedVideos: 'clearSelectedVideos',
|
||||||
inputRemoveSelectedVideo: 'removeSelectedVideo'
|
inputRemoveSelectedVideo: 'removeSelectedVideo'
|
||||||
}),
|
}),
|
||||||
@ -877,7 +879,8 @@ const appOptions = {
|
|||||||
resourceSetUsageQuota: 'setUsageQuota'
|
resourceSetUsageQuota: 'setUsageQuota'
|
||||||
}),
|
}),
|
||||||
...mapActions(useUploadStore, {
|
...mapActions(useUploadStore, {
|
||||||
uploadHandleSelected: 'handleSelectedFiles'
|
uploadHandleSelected: 'handleSelectedFiles',
|
||||||
|
uploadBatchFiles: 'uploadFiles'
|
||||||
}),
|
}),
|
||||||
...mapActions(useFileStore, {
|
...mapActions(useFileStore, {
|
||||||
fileFetchTree: 'fetchFileTree',
|
fileFetchTree: 'fetchFileTree',
|
||||||
@ -2579,6 +2582,164 @@ const appOptions = {
|
|||||||
this.uploadHandleSelected(files);
|
this.uploadHandleSelected(files);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
normalizeLocalFiles(files) {
|
||||||
|
if (!files) return [];
|
||||||
|
const list = Array.isArray(files) ? files : Array.from(files);
|
||||||
|
return list.filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
isImageFile(file) {
|
||||||
|
const name = file?.name || '';
|
||||||
|
const type = file?.type || '';
|
||||||
|
return type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
isVideoFile(file) {
|
||||||
|
const name = file?.name || '';
|
||||||
|
const type = file?.type || '';
|
||||||
|
return type.startsWith('video/') || /\.(mp4|mov|m4v|webm|avi|mkv|flv|mpg|mpeg)$/i.test(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertImageEntry(path, filename) {
|
||||||
|
if (!path) return;
|
||||||
|
const name = filename || path.split('/').pop() || path;
|
||||||
|
const list = Array.isArray(this.imageEntries) ? this.imageEntries : [];
|
||||||
|
if (list.some((item) => item.path === path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.imageEntries = [{ name, path }, ...list];
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertVideoEntry(path, filename) {
|
||||||
|
if (!path) return;
|
||||||
|
const name = filename || path.split('/').pop() || path;
|
||||||
|
const list = Array.isArray(this.videoEntries) ? this.videoEntries : [];
|
||||||
|
if (list.some((item) => item.path === path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.videoEntries = [{ name, path }, ...list];
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLocalImageFiles(files) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mediaUploading) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传中',
|
||||||
|
message: '请等待当前图片上传完成',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.normalizeLocalFiles(files);
|
||||||
|
if (!list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingCount = Array.isArray(this.selectedImages) ? this.selectedImages.length : 0;
|
||||||
|
const remaining = Math.max(0, 9 - existingCount);
|
||||||
|
if (!remaining) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已达上限',
|
||||||
|
message: '最多只能选择 9 张图片',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valid = list.filter((file) => this.isImageFile(file));
|
||||||
|
if (!valid.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法上传',
|
||||||
|
message: '仅支持图片文件',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (valid.length < list.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已忽略',
|
||||||
|
message: '已跳过非图片文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const limited = valid.slice(0, remaining);
|
||||||
|
if (valid.length > remaining) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已超出数量',
|
||||||
|
message: `最多还能添加 ${remaining} 张图片,已自动截断`,
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const uploaded = await this.uploadBatchFiles(limited, {
|
||||||
|
markUploading: true,
|
||||||
|
markMediaUploading: true
|
||||||
|
});
|
||||||
|
if (!uploaded.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploaded.forEach((item) => {
|
||||||
|
if (!item?.path) return;
|
||||||
|
this.inputAddSelectedImage(item.path);
|
||||||
|
this.upsertImageEntry(item.path, item.filename);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLocalVideoFiles(files) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mediaUploading) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传中',
|
||||||
|
message: '请等待当前视频上传完成',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.normalizeLocalFiles(files);
|
||||||
|
if (!list.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valid = list.filter((file) => this.isVideoFile(file));
|
||||||
|
if (!valid.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '无法上传',
|
||||||
|
message: '仅支持视频文件',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (valid.length < list.length) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '已忽略',
|
||||||
|
message: '已跳过非视频文件',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (valid.length > 1) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '视频数量过多',
|
||||||
|
message: '一次只能选择 1 个视频,已使用第一个',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [file] = valid;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uploaded = await this.uploadBatchFiles([file], {
|
||||||
|
markUploading: true,
|
||||||
|
markMediaUploading: true
|
||||||
|
});
|
||||||
|
const [item] = uploaded;
|
||||||
|
if (!item?.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inputSetSelectedVideos([item.path]);
|
||||||
|
this.inputClearSelectedImages();
|
||||||
|
this.upsertVideoEntry(item.path, item.filename);
|
||||||
|
},
|
||||||
|
|
||||||
handleSendOrStop() {
|
handleSendOrStop() {
|
||||||
if (this.composerBusy) {
|
if (this.composerBusy) {
|
||||||
this.stopTask();
|
this.stopTask();
|
||||||
@ -2591,6 +2752,14 @@ const appOptions = {
|
|||||||
if (this.streamingUi || !this.isConnected) {
|
if (this.streamingUi || !this.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.mediaUploading) {
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '上传中',
|
||||||
|
message: '请等待图片/视频上传完成后再发送',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = (this.inputMessage || '').trim();
|
const text = (this.inputMessage || '').trim();
|
||||||
const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : [];
|
const images = Array.isArray(this.selectedImages) ? this.selectedImages.slice(0, 9) : [];
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
:disabled="
|
:disabled="
|
||||||
!isConnected ||
|
!isConnected ||
|
||||||
(inputLocked && !streamingMessage) ||
|
(inputLocked && !streamingMessage) ||
|
||||||
|
(mediaUploading && !streamingMessage) ||
|
||||||
((!(inputMessage || '').trim() && (!selectedImages?.length && !selectedVideos?.length)) && !streamingMessage)
|
((!(inputMessage || '').trim() && (!selectedImages?.length && !selectedVideos?.length)) && !streamingMessage)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -167,6 +168,7 @@ const props = defineProps<{
|
|||||||
currentModelKey: string;
|
currentModelKey: string;
|
||||||
selectedImages?: string[];
|
selectedImages?: string[];
|
||||||
selectedVideos?: string[];
|
selectedVideos?: string[];
|
||||||
|
mediaUploading?: boolean;
|
||||||
blockUpload?: boolean;
|
blockUpload?: boolean;
|
||||||
blockToolToggle?: boolean;
|
blockToolToggle?: boolean;
|
||||||
blockRealtimeTerminal?: boolean;
|
blockRealtimeTerminal?: boolean;
|
||||||
|
|||||||
@ -1,9 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="overlay-fade">
|
<transition name="overlay-fade">
|
||||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||||
<div class="image-picker-panel">
|
<div class="image-picker-panel">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="title">选择图片(最多9张)</div>
|
<div class="header-left">
|
||||||
|
<div class="title">选择图片(最多9张)</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="local-btn"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="triggerLocal"
|
||||||
|
>
|
||||||
|
{{ uploading ? '上传中...' : '从本地发送' }}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="localInput"
|
||||||
|
type="file"
|
||||||
|
class="file-input-hidden"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
@change="onLocalChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button class="close-btn" @click="close">×</button>
|
<button class="close-btn" @click="close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
@ -36,26 +54,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted } from 'vue';
|
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
||||||
|
|
||||||
interface ImageEntry {
|
interface ImageEntry {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
open: boolean;
|
defineProps<{
|
||||||
entries: ImageEntry[];
|
open: boolean;
|
||||||
initialSelected: string[];
|
entries: ImageEntry[];
|
||||||
loading: boolean;
|
initialSelected: string[];
|
||||||
}>();
|
loading: boolean;
|
||||||
|
uploading?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
uploading: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'confirm', list: string[]): void;
|
(e: 'confirm', list: string[]): void;
|
||||||
|
(e: 'local-files', files: FileList | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const selectedSet = ref<Set<string>>(new Set(props.initialSelected || []));
|
const selectedSet = ref<Set<string>>(new Set(props.initialSelected || []));
|
||||||
|
const localInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.initialSelected,
|
() => props.initialSelected,
|
||||||
@ -82,6 +108,19 @@ const close = () => emit('close');
|
|||||||
|
|
||||||
const confirm = () => emit('confirm', Array.from(selectedSet.value));
|
const confirm = () => emit('confirm', Array.from(selectedSet.value));
|
||||||
|
|
||||||
|
const triggerLocal = () => {
|
||||||
|
if (props.uploading) return;
|
||||||
|
localInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLocalChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit('local-files', target?.files || null);
|
||||||
|
if (target) {
|
||||||
|
target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const previewUrl = (path: string) => `/api/gui/files/download?path=${encodeURIComponent(path)}`;
|
const previewUrl = (path: string) => `/api/gui/files/download?path=${encodeURIComponent(path)}`;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -117,9 +156,27 @@ onMounted(() => {
|
|||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid #1f2430;
|
border-bottom: 1px solid #1f2430;
|
||||||
}
|
}
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.local-btn {
|
||||||
|
border: 1px solid #2f3645;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1b202c;
|
||||||
|
color: #c5ccda;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.local-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.close-btn {
|
.close-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #9aa3b5;
|
color: #9aa3b5;
|
||||||
|
|||||||
@ -1,9 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="overlay-fade">
|
<transition name="overlay-fade">
|
||||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||||
<div class="image-picker-panel">
|
<div class="image-picker-panel">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="title">选择视频(一次最多 1 个)</div>
|
<div class="header-left">
|
||||||
|
<div class="title">选择视频(一次最多 1 个)</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="local-btn"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="triggerLocal"
|
||||||
|
>
|
||||||
|
{{ uploading ? '上传中...' : '从本地发送' }}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="localInput"
|
||||||
|
type="file"
|
||||||
|
class="file-input-hidden"
|
||||||
|
accept="video/*"
|
||||||
|
@change="onLocalChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button class="close-btn" @click="close">×</button>
|
<button class="close-btn" @click="close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
@ -39,26 +56,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted } from 'vue';
|
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
||||||
|
|
||||||
interface VideoEntry {
|
interface VideoEntry {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
open: boolean;
|
defineProps<{
|
||||||
entries: VideoEntry[];
|
open: boolean;
|
||||||
initialSelected: string[];
|
entries: VideoEntry[];
|
||||||
loading: boolean;
|
initialSelected: string[];
|
||||||
}>();
|
loading: boolean;
|
||||||
|
uploading?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
uploading: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'confirm', list: string[]): void;
|
(e: 'confirm', list: string[]): void;
|
||||||
|
(e: 'local-files', files: FileList | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const selectedSet = ref<Set<string>>(new Set(props.initialSelected || []));
|
const selectedSet = ref<Set<string>>(new Set(props.initialSelected || []));
|
||||||
|
const localInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.initialSelected,
|
() => props.initialSelected,
|
||||||
@ -85,6 +110,19 @@ const close = () => emit('close');
|
|||||||
|
|
||||||
const confirm = () => emit('confirm', Array.from(selectedSet.value));
|
const confirm = () => emit('confirm', Array.from(selectedSet.value));
|
||||||
|
|
||||||
|
const triggerLocal = () => {
|
||||||
|
if (props.uploading) return;
|
||||||
|
localInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLocalChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit('local-files', target?.files || null);
|
||||||
|
if (target) {
|
||||||
|
target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fileExt = (name: string) => {
|
const fileExt = (name: string) => {
|
||||||
if (!name || !name.includes('.')) return '';
|
if (!name || !name.includes('.')) return '';
|
||||||
return name.split('.').pop()?.toLowerCase();
|
return name.split('.').pop()?.toLowerCase();
|
||||||
@ -123,9 +161,27 @@ onMounted(() => {
|
|||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid #1f2430;
|
border-bottom: 1px solid #1f2430;
|
||||||
}
|
}
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.local-btn {
|
||||||
|
border: 1px solid #2f3645;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1b202c;
|
||||||
|
color: #c5ccda;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.local-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.close-btn {
|
.close-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #9aa3b5;
|
color: #9aa3b5;
|
||||||
|
|||||||
@ -3,18 +3,28 @@ import { useUiStore } from './ui';
|
|||||||
import { useFileStore } from './file';
|
import { useFileStore } from './file';
|
||||||
import { usePolicyStore } from './policy';
|
import { usePolicyStore } from './policy';
|
||||||
|
|
||||||
|
interface UploadResult {
|
||||||
|
path: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface UploadState {
|
interface UploadState {
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
|
mediaUploading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUploadStore = defineStore('upload', {
|
export const useUploadStore = defineStore('upload', {
|
||||||
state: (): UploadState => ({
|
state: (): UploadState => ({
|
||||||
uploading: false
|
uploading: false,
|
||||||
|
mediaUploading: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setUploading(value: boolean) {
|
setUploading(value: boolean) {
|
||||||
this.uploading = value;
|
this.uploading = value;
|
||||||
},
|
},
|
||||||
|
setMediaUploading(value: boolean) {
|
||||||
|
this.mediaUploading = value;
|
||||||
|
},
|
||||||
async handleSelectedFiles(files: FileList | File[] | null) {
|
async handleSelectedFiles(files: FileList | File[] | null) {
|
||||||
if (!files) {
|
if (!files) {
|
||||||
return;
|
return;
|
||||||
@ -26,10 +36,18 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
}
|
}
|
||||||
await this.uploadFile(file);
|
await this.uploadFile(file);
|
||||||
},
|
},
|
||||||
async uploadFile(file: File) {
|
async uploadFiles(
|
||||||
if (!file || this.uploading) {
|
files: FileList | File[] | null,
|
||||||
return;
|
options: { markUploading?: boolean; markMediaUploading?: boolean } = {}
|
||||||
|
): Promise<UploadResult[]> {
|
||||||
|
if (!files) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
const list = Array.isArray(files) ? files : Array.from(files);
|
||||||
|
if (!list.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const { markUploading = true, markMediaUploading = false } = options;
|
||||||
const policyStore = usePolicyStore();
|
const policyStore = usePolicyStore();
|
||||||
if (policyStore.uiBlocks?.block_upload) {
|
if (policyStore.uiBlocks?.block_upload) {
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
@ -38,7 +56,57 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
message: '已被管理员禁用上传功能',
|
message: '已被管理员禁用上传功能',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
});
|
});
|
||||||
return;
|
return [];
|
||||||
|
}
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const results: UploadResult[] = [];
|
||||||
|
if (markUploading) {
|
||||||
|
this.setUploading(true);
|
||||||
|
}
|
||||||
|
if (markMediaUploading) {
|
||||||
|
this.setMediaUploading(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const file of list) {
|
||||||
|
const uploaded = await this.uploadFile(file, {
|
||||||
|
manageUploading: false,
|
||||||
|
refreshFileTree: false,
|
||||||
|
skipPolicyCheck: true
|
||||||
|
});
|
||||||
|
if (uploaded?.path) {
|
||||||
|
results.push(uploaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (results.length) {
|
||||||
|
await fileStore.fetchFileTree();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (markMediaUploading) {
|
||||||
|
this.setMediaUploading(false);
|
||||||
|
}
|
||||||
|
if (markUploading) {
|
||||||
|
this.setUploading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
async uploadFile(
|
||||||
|
file: File,
|
||||||
|
options: { manageUploading?: boolean; refreshFileTree?: boolean; skipPolicyCheck?: boolean } = {}
|
||||||
|
): Promise<UploadResult | null> {
|
||||||
|
const { manageUploading = true, refreshFileTree = true, skipPolicyCheck = false } = options;
|
||||||
|
if (!file || (manageUploading && this.uploading)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const policyStore = usePolicyStore();
|
||||||
|
if (!skipPolicyCheck && policyStore.uiBlocks?.block_upload) {
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
uiStore.pushToast({
|
||||||
|
title: '上传被禁用',
|
||||||
|
message: '已被管理员禁用上传功能',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
@ -53,7 +121,9 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.setUploading(true);
|
if (manageUploading) {
|
||||||
|
this.setUploading(true);
|
||||||
|
}
|
||||||
const toastId = uiStore.pushToast({
|
const toastId = uiStore.pushToast({
|
||||||
title: '上传文件',
|
title: '上传文件',
|
||||||
message: `正在上传 ${file.name}...`,
|
message: `正在上传 ${file.name}...`,
|
||||||
@ -84,7 +154,9 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
const message = result.error || result.message || '上传失败';
|
const message = result.error || result.message || '上传失败';
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
await fileStore.fetchFileTree();
|
if (refreshFileTree) {
|
||||||
|
await fileStore.fetchFileTree();
|
||||||
|
}
|
||||||
const successMessage = `上传成功:${result.path || file.name}`;
|
const successMessage = `上传成功:${result.path || file.name}`;
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
uiStore.updateToast(toastId, {
|
uiStore.updateToast(toastId, {
|
||||||
@ -100,6 +172,13 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (result?.path) {
|
||||||
|
return {
|
||||||
|
path: result.path,
|
||||||
|
filename: result.filename || file.name || result.path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const originalMessage = error && error.message ? String(error.message) : '';
|
const originalMessage = error && error.message ? String(error.message) : '';
|
||||||
const fileLabel = file && file.name ? file.name : '文件';
|
const fileLabel = file && file.name ? file.name : '文件';
|
||||||
@ -128,8 +207,11 @@ export const useUploadStore = defineStore('upload', {
|
|||||||
type: 'error'
|
type: 'error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
this.setUploading(false);
|
if (manageUploading) {
|
||||||
|
this.setUploading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user