mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-07-12 16:11:27 +08:00
✨ Feature(custom): add upload task system
This commit is contained in:
@@ -27,7 +27,6 @@
|
||||
<span>{{ t('pages.upload.imageProcessNameSingle') }}</span>
|
||||
</button>
|
||||
<button class="segmented-button" :title="t('pages.upload.imageProcessName')" @click="handleImageProcess">
|
||||
<Settings :size="16" />
|
||||
<span>{{ t('pages.upload.imageProcessName') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,6 +97,17 @@
|
||||
<LinkIcon :size="20" />
|
||||
<span>{{ t('pages.upload.urlUpload') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="quick-action-button"
|
||||
:class="{ 'has-badge': taskQueueStatus.tasks.length > 0 }"
|
||||
@click="openTaskDialog"
|
||||
>
|
||||
<ListTodoIcon :size="20" />
|
||||
<span>{{ t('pages.upload.taskUpload') }}</span>
|
||||
<span v-if="taskQueueStatus.tasks.length > 0" class="task-count-badge">
|
||||
{{ taskQueueStatus.tasks.length }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,20 +174,379 @@
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Task Queue Manager Modal -->
|
||||
<transition name="modal">
|
||||
<div v-if="taskDialogVisible" class="modal-overlay" @click="taskDialogVisible = false">
|
||||
<div class="modal-container task-queue-modal" @click.stop>
|
||||
<div class="modal-header">
|
||||
<div class="modal-header-text">
|
||||
<h3 class="modal-title">
|
||||
<ListTodoIcon :size="22" />
|
||||
{{ t('pages.upload.taskQueue.title') }}
|
||||
</h3>
|
||||
<span class="modal-subtitle">
|
||||
{{
|
||||
t('pages.upload.taskQueue.stats', {
|
||||
completed: taskQueueStatus.stats.completed,
|
||||
total: taskQueueStatus.stats.total,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<button class="modal-close" @click="taskDialogVisible = false">
|
||||
<XIcon :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-content task-queue-content">
|
||||
<!-- Action Bar -->
|
||||
<div class="task-action-bar">
|
||||
<div class="action-bar-left">
|
||||
<button class="action-btn primary" @click="addFilesToTask">
|
||||
<PlusIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.addFiles') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!taskQueueStatus.config.isRunning && taskQueueStatus.stats.pending > 0"
|
||||
class="action-btn success"
|
||||
@click="startTaskQueue"
|
||||
>
|
||||
<PlayIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.start') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="taskQueueStatus.config.isRunning && !taskQueueStatus.config.isPaused"
|
||||
class="action-btn warning"
|
||||
@click="pauseTaskQueue"
|
||||
>
|
||||
<PauseIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.pause') }}
|
||||
</button>
|
||||
<button v-if="taskQueueStatus.config.isPaused" class="action-btn success" @click="resumeTaskQueue">
|
||||
<PlayIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.resume') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-bar-right">
|
||||
<button v-if="taskQueueStatus.stats.failed > 0" class="action-btn" @click="retryAllFailedTasks">
|
||||
<RefreshCwIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.retryAllFailed') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="taskQueueStatus.config.isRunning || taskQueueStatus.stats.pending > 0"
|
||||
class="action-btn danger"
|
||||
@click="cancelAllTasks"
|
||||
>
|
||||
<XIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.cancelAll') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
taskQueueStatus.stats.completed > 0 ||
|
||||
taskQueueStatus.stats.failed > 0 ||
|
||||
taskQueueStatus.stats.cancelled > 0
|
||||
"
|
||||
class="action-btn"
|
||||
@click="clearFinishedTasks"
|
||||
>
|
||||
<Trash2Icon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.clearFinished') }}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
:class="{ active: showTaskSettings }"
|
||||
@click="showTaskSettings = !showTaskSettings"
|
||||
>
|
||||
<SettingsIcon :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Progress -->
|
||||
<div v-if="taskQueueStatus.stats.total > 0" class="overall-progress">
|
||||
<div class="progress-info">
|
||||
<span class="progress-label">{{ t('pages.upload.taskQueue.overallProgress') }}</span>
|
||||
<span class="progress-percentage">{{ overallProgressPercent }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-fill" :style="{ width: `${overallProgressPercent}%` }" />
|
||||
</div>
|
||||
<div class="progress-details">
|
||||
<span v-if="taskQueueStatus.stats.avgSpeed > 0" class="progress-detail-item">
|
||||
<ZapIcon :size="14" />
|
||||
{{ formatSpeed(taskQueueStatus.stats.avgSpeed) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="taskQueueStatus.stats.estimatedTimeMs > 0 && taskQueueStatus.config.isRunning"
|
||||
class="progress-detail-item"
|
||||
>
|
||||
<ClockIcon :size="14" />
|
||||
{{ formatTime(taskQueueStatus.stats.estimatedTimeMs) }}
|
||||
</span>
|
||||
<span class="progress-detail-item">
|
||||
<HardDriveIcon :size="14" />
|
||||
{{ formatSize(taskQueueStatus.stats.completedSize) }} /
|
||||
{{ formatSize(taskQueueStatus.stats.totalSize) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<transition name="settings-slide">
|
||||
<div v-if="showTaskSettings" class="settings-panel">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">
|
||||
<TimerIcon :size="14" />
|
||||
{{ t('pages.upload.taskQueue.interval') }}
|
||||
</label>
|
||||
<div class="input-with-unit">
|
||||
<input
|
||||
v-model.number="uploadInterval"
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="99999"
|
||||
step="0.1"
|
||||
class="setting-input"
|
||||
:disabled="taskQueueStatus.config.isRunning"
|
||||
@change="updateInterval"
|
||||
/>
|
||||
<span class="input-unit">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">{{ t('pages.upload.taskQueue.maxRetry') }}</label>
|
||||
<input
|
||||
v-model.number="maxRetryCount"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
class="setting-input"
|
||||
@change="updateSettings"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item toggle-item">
|
||||
<label class="setting-label" for="task-auto-start">
|
||||
{{ t('pages.upload.taskQueue.autoStart') }}
|
||||
</label>
|
||||
<input
|
||||
id="task-auto-start"
|
||||
v-model="autoStart"
|
||||
type="checkbox"
|
||||
class="setting-checkbox"
|
||||
@change="updateSettings"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item toggle-item">
|
||||
<label class="setting-label" for="task-pause-on-error">
|
||||
{{ t('pages.upload.taskQueue.pauseOnError') }}
|
||||
</label>
|
||||
<input
|
||||
id="task-pause-on-error"
|
||||
v-model="pauseOnError"
|
||||
type="checkbox"
|
||||
class="setting-checkbox"
|
||||
@change="updateSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Filter & Search Bar -->
|
||||
<div v-if="taskQueueStatus.tasks.length > 0" class="filter-search-bar">
|
||||
<div class="search-box">
|
||||
<SearchIcon :size="16" />
|
||||
<input
|
||||
v-model="taskSearchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
:placeholder="t('pages.upload.taskQueue.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-tabs">
|
||||
<button class="filter-tab" :class="{ active: taskFilter === 'all' }" @click="taskFilter = 'all'">
|
||||
{{ t('pages.upload.taskQueue.filterAll') }}
|
||||
</button>
|
||||
<button
|
||||
class="filter-tab"
|
||||
:class="{ active: taskFilter === 'pending' }"
|
||||
@click="taskFilter = 'pending'"
|
||||
>
|
||||
{{ t('pages.upload.taskQueue.filterPending') }}
|
||||
</button>
|
||||
<button
|
||||
class="filter-tab"
|
||||
:class="{ active: taskFilter === 'completed' }"
|
||||
@click="taskFilter = 'completed'"
|
||||
>
|
||||
{{ t('pages.upload.taskQueue.filterCompleted') }}
|
||||
</button>
|
||||
<button class="filter-tab" :class="{ active: taskFilter === 'failed' }" @click="taskFilter = 'failed'">
|
||||
{{ t('pages.upload.taskQueue.filterFailed') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task List -->
|
||||
<div v-if="taskQueueStatus.tasks.length > 0" class="task-list-container">
|
||||
<TransitionGroup name="task" tag="div" class="task-list">
|
||||
<div
|
||||
v-for="task in filteredTasks"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
:class="getTaskStatusClass(task.status)"
|
||||
>
|
||||
<div class="task-content">
|
||||
<div class="task-header-row">
|
||||
<div class="task-name">
|
||||
<span class="task-filename" :title="task.filePath">{{ task.fileName }}</span>
|
||||
<span v-if="task.priority === 2" class="priority-badge">
|
||||
<StarIcon :size="12" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="task-status-badge" :class="getTaskStatusClass(task.status)">
|
||||
{{ getTaskStatusText(task.status) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-meta-row">
|
||||
<span v-if="task.fileSize > 0" class="task-meta-item">
|
||||
<HardDriveIcon :size="12" />
|
||||
{{ formatSize(task.fileSize) }}
|
||||
</span>
|
||||
<span v-if="task.uploadSpeed && task.status === 'uploading'" class="task-meta-item">
|
||||
<ZapIcon :size="12" />
|
||||
{{ formatSpeed(task.uploadSpeed) }}
|
||||
</span>
|
||||
<span v-if="task.retryCount > 0" class="task-meta-item retry">
|
||||
{{ t('pages.upload.taskQueue.retryCount', { count: task.retryCount }) }}
|
||||
</span>
|
||||
<span v-if="task.error" class="task-meta-item error" :title="task.error">
|
||||
{{ task.error }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
<!-- Pending task actions -->
|
||||
<template v-if="task.status === 'pending'">
|
||||
<button
|
||||
class="task-icon-btn"
|
||||
:title="t('pages.upload.taskQueue.moveUp')"
|
||||
@click="moveTaskUp(task.id)"
|
||||
>
|
||||
<ChevronUpIcon :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="task-icon-btn"
|
||||
:title="t('pages.upload.taskQueue.moveDown')"
|
||||
@click="moveTaskDown(task.id)"
|
||||
>
|
||||
<ChevronDownIcon :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="task-icon-btn priority"
|
||||
:class="{ 'is-high': task.priority === 2 }"
|
||||
:title="t('pages.upload.taskQueue.togglePriority')"
|
||||
@click="toggleTaskPriority(task.id, task.priority)"
|
||||
>
|
||||
<StarIcon :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="task-icon-btn danger"
|
||||
:title="t('pages.upload.taskQueue.cancelTask')"
|
||||
@click="cancelTask(task.id)"
|
||||
>
|
||||
<XIcon :size="16" />
|
||||
</button>
|
||||
</template>
|
||||
<!-- Failed task actions -->
|
||||
<template v-if="task.status === 'failed'">
|
||||
<button
|
||||
class="task-icon-btn"
|
||||
:title="t('pages.upload.taskQueue.retryTask')"
|
||||
@click="retryTask(task.id)"
|
||||
>
|
||||
<RefreshCwIcon :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="task-icon-btn danger"
|
||||
:title="t('pages.upload.taskQueue.removeTask')"
|
||||
@click="removeTask(task.id)"
|
||||
>
|
||||
<Trash2Icon :size="16" />
|
||||
</button>
|
||||
</template>
|
||||
<!-- Completed/Cancelled task actions -->
|
||||
<template v-if="task.status === 'completed' || task.status === 'cancelled'">
|
||||
<button
|
||||
class="task-icon-btn"
|
||||
:title="t('pages.upload.taskQueue.removeTask')"
|
||||
@click="removeTask(task.id)"
|
||||
>
|
||||
<Trash2Icon :size="16" />
|
||||
</button>
|
||||
</template>
|
||||
<!-- Status icon -->
|
||||
<div class="task-status-icon">
|
||||
<CheckCircleIcon v-if="task.status === 'completed'" :size="18" class="icon-success" />
|
||||
<XCircleIcon v-if="task.status === 'failed'" :size="18" class="icon-error" />
|
||||
<LoaderIcon v-if="task.status === 'uploading'" :size="18" class="icon-loading spinning" />
|
||||
<ClockIcon v-if="task.status === 'pending'" :size="18" class="icon-pending" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<ListTodoIcon :size="48" />
|
||||
<h4>{{ t('pages.upload.taskQueue.empty') }}</h4>
|
||||
<p>{{ t('pages.upload.taskQueue.emptyHint') }}</p>
|
||||
<button class="action-btn primary" @click="addFilesToTask">
|
||||
<PlusIcon :size="16" />
|
||||
{{ t('pages.upload.taskQueue.selectFiles') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
ClipboardIcon,
|
||||
ClockIcon,
|
||||
EditIcon,
|
||||
HardDriveIcon,
|
||||
LinkIcon,
|
||||
ListTodoIcon,
|
||||
LoaderIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
Settings,
|
||||
SettingsIcon,
|
||||
StarIcon,
|
||||
TimerIcon,
|
||||
Trash2Icon,
|
||||
UploadCloudIcon,
|
||||
XCircleIcon,
|
||||
XIcon,
|
||||
ZapIcon,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -193,6 +562,49 @@ import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { useDragEventListeners } from '@/utils/drag'
|
||||
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||
|
||||
// Task queue types
|
||||
interface IUploadTaskItem {
|
||||
id: string
|
||||
fileName: string
|
||||
filePath: string
|
||||
fileSize: number
|
||||
status: string
|
||||
progress: number
|
||||
error?: string
|
||||
result?: any
|
||||
createdAt: number
|
||||
startedAt?: number
|
||||
completedAt?: number
|
||||
retryCount: number
|
||||
priority: number
|
||||
uploadSpeed?: number
|
||||
uploadDuration?: number
|
||||
}
|
||||
|
||||
interface IUploadTaskQueueStatus {
|
||||
tasks: IUploadTaskItem[]
|
||||
config: {
|
||||
intervalS: number
|
||||
isRunning: boolean
|
||||
isPaused: boolean
|
||||
autoStart: boolean
|
||||
pauseOnError: boolean
|
||||
maxRetryCount: number
|
||||
}
|
||||
stats: {
|
||||
total: number
|
||||
pending: number
|
||||
completed: number
|
||||
failed: number
|
||||
cancelled: number
|
||||
uploading: number
|
||||
totalSize: number
|
||||
completedSize: number
|
||||
avgSpeed: number
|
||||
estimatedTimeMs: number
|
||||
}
|
||||
}
|
||||
|
||||
useDragEventListeners()
|
||||
const $router = useRouter()
|
||||
const { t } = useI18n()
|
||||
@@ -200,6 +612,7 @@ const message = useMessage()
|
||||
const { picBedG, defaultPicBedG, defaultConfigNameG, defaultIdG, updatePicBeds } = usePicBed()
|
||||
|
||||
const imageProcessDialogVisible = ref(false)
|
||||
const taskDialogVisible = ref(false)
|
||||
const useShortUrl = ref(false)
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
@@ -208,6 +621,65 @@ const showError = ref(false)
|
||||
const pasteStyle = ref(IPasteStyle.MARKDOWN)
|
||||
const PicBedId = ref('')
|
||||
const fileInput = useTemplateRef('fileInput')
|
||||
const uploadInterval = ref(1000)
|
||||
|
||||
// New task queue settings
|
||||
const showTaskSettings = useStorage('upload-task-queue-show-settings', true)
|
||||
const taskSearchQuery = ref('')
|
||||
const taskFilter = ref<'all' | 'pending' | 'completed' | 'failed'>('all')
|
||||
const autoStart = ref(false)
|
||||
const pauseOnError = ref(false)
|
||||
const maxRetryCount = ref(3)
|
||||
|
||||
// Task queue status
|
||||
const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
||||
tasks: [],
|
||||
config: {
|
||||
intervalS: 1,
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
autoStart: false,
|
||||
pauseOnError: false,
|
||||
maxRetryCount: 3,
|
||||
},
|
||||
stats: {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
cancelled: 0,
|
||||
uploading: 0,
|
||||
totalSize: 0,
|
||||
completedSize: 0,
|
||||
avgSpeed: 0,
|
||||
estimatedTimeMs: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Computed properties
|
||||
const filteredTasks = computed(() => {
|
||||
let tasks = taskQueueStatus.tasks
|
||||
|
||||
// Filter by status
|
||||
if (taskFilter.value !== 'all') {
|
||||
tasks = tasks.filter(t => t.status === taskFilter.value)
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (taskSearchQuery.value) {
|
||||
const query = taskSearchQuery.value.toLowerCase()
|
||||
tasks = tasks.filter(t => t.fileName.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
return tasks
|
||||
})
|
||||
|
||||
const overallProgressPercent = computed(() => {
|
||||
if (taskQueueStatus.stats.total === 0) return 0
|
||||
const completed = taskQueueStatus.stats.completed
|
||||
const total = taskQueueStatus.stats.total - taskQueueStatus.stats.cancelled
|
||||
return total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
})
|
||||
|
||||
const picBedName = computed(() => {
|
||||
if (!picBedG.value || picBedG.value.length === 0) {
|
||||
@@ -421,18 +893,193 @@ async function handleChangePicBed() {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
|
||||
}
|
||||
|
||||
function openTaskDialog() {
|
||||
taskDialogVisible.value = true
|
||||
refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function refreshTaskStatus() {
|
||||
const status = await window.electron.triggerRPC<IUploadTaskQueueStatus>(IRPCActionType.UPLOAD_TASK_GET_STATUS)
|
||||
if (status) {
|
||||
Object.assign(taskQueueStatus, status)
|
||||
uploadInterval.value = status.config.intervalS
|
||||
autoStart.value = status.config.autoStart
|
||||
pauseOnError.value = status.config.pauseOnError
|
||||
maxRetryCount.value = status.config.maxRetryCount
|
||||
}
|
||||
}
|
||||
|
||||
async function addFilesToTask() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.onchange = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (target.files && target.files.length > 0) {
|
||||
const files: IFileWithPath[] = Array.from(target.files).map(file => ({
|
||||
name: file.name,
|
||||
path: window.electron.showFilePath(file),
|
||||
}))
|
||||
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_ADD, files)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.filesAdded', { count: files.length }))
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
async function startTaskQueue() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_START, uploadInterval.value)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.started'))
|
||||
}
|
||||
|
||||
async function pauseTaskQueue() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_PAUSE)
|
||||
await refreshTaskStatus()
|
||||
message.info(t('pages.upload.taskQueue.paused'))
|
||||
}
|
||||
|
||||
async function resumeTaskQueue() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_RESUME)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.resumed'))
|
||||
}
|
||||
|
||||
async function cancelAllTasks() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CANCEL_ALL)
|
||||
await refreshTaskStatus()
|
||||
message.info(t('pages.upload.taskQueue.allCancelled'))
|
||||
}
|
||||
|
||||
async function cancelTask(taskId: string) {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CANCEL_ONE, taskId)
|
||||
await refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function removeTask(taskId: string) {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_REMOVE_ONE, taskId)
|
||||
await refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function clearFinishedTasks() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CLEAR_FINISHED)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.cleared'))
|
||||
}
|
||||
|
||||
async function updateInterval() {
|
||||
uploadInterval.value = Math.max(100, Math.min(60000, uploadInterval.value))
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_SET_INTERVAL, uploadInterval.value)
|
||||
}
|
||||
|
||||
async function retryTask(taskId: string) {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_RETRY_ONE, taskId)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.taskRetried'))
|
||||
}
|
||||
|
||||
async function retryAllFailedTasks() {
|
||||
const count = await window.electron.triggerRPC<number>(IRPCActionType.UPLOAD_TASK_RETRY_ALL_FAILED)
|
||||
await refreshTaskStatus()
|
||||
message.success(t('pages.upload.taskQueue.retriedAllFailed', { count }))
|
||||
}
|
||||
|
||||
async function moveTaskUp(taskId: string) {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_MOVE_UP, taskId)
|
||||
await refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function moveTaskDown(taskId: string) {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_MOVE_DOWN, taskId)
|
||||
await refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function toggleTaskPriority(taskId: string, currentPriority: number) {
|
||||
const newPriority = currentPriority === 2 ? 1 : 2
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_SET_PRIORITY, taskId, newPriority)
|
||||
await refreshTaskStatus()
|
||||
}
|
||||
|
||||
async function updateSettings() {
|
||||
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_UPDATE_SETTINGS, {
|
||||
intervalS: uploadInterval.value,
|
||||
autoStart: autoStart.value,
|
||||
pauseOnError: pauseOnError.value,
|
||||
maxRetryCount: maxRetryCount.value,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
function formatSpeed(bytesPerSecond: number): string {
|
||||
return formatSize(bytesPerSecond) + '/s'
|
||||
}
|
||||
|
||||
function formatTime(ms: number): string {
|
||||
if (ms < 1000) return '< 1s'
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainingMinutes = minutes % 60
|
||||
return `${hours}h ${remainingMinutes}m`
|
||||
}
|
||||
|
||||
function getTaskStatusClass(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: 'status-pending',
|
||||
uploading: 'status-uploading',
|
||||
completed: 'status-completed',
|
||||
failed: 'status-failed',
|
||||
cancelled: 'status-cancelled',
|
||||
}
|
||||
return statusMap[status] || ''
|
||||
}
|
||||
|
||||
function getTaskStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: t('pages.upload.taskQueue.statusPending'),
|
||||
uploading: t('pages.upload.taskQueue.statusUploading'),
|
||||
completed: t('pages.upload.taskQueue.statusCompleted'),
|
||||
failed: t('pages.upload.taskQueue.statusFailed'),
|
||||
cancelled: t('pages.upload.taskQueue.statusCancelled'),
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
function taskQueueUpdateHandler(status: IUploadTaskQueueStatus) {
|
||||
Object.assign(taskQueueStatus, status)
|
||||
uploadInterval.value = status.config.intervalS
|
||||
}
|
||||
|
||||
let removeTaskQueueUpdateListenerCallback: () => void = () => {}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
$bus.off(SHOW_INPUT_BOX_RESPONSE)
|
||||
removeUploadProgressListenerCallback()
|
||||
removeSyncPicBedListenerCallback()
|
||||
removeTaskQueueUpdateListenerCallback()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
removeUploadProgressListenerCallback = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
|
||||
removeSyncPicBedListenerCallback = window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
|
||||
removeTaskQueueUpdateListenerCallback = window.electron.ipcRendererOn('uploadTaskQueueUpdate', taskQueueUpdateHandler)
|
||||
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
|
||||
getUseShortUrl()
|
||||
getPasteStyle()
|
||||
refreshTaskStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user