mirror of
https://github.com/keven1024/015.git
synced 2026-05-26 07:08:02 +00:00
feat(front): implement ImageConvertResult component for image format conversion and update layout in ImageCompressResult
This commit is contained in:
@@ -157,8 +157,8 @@ watch(
|
||||
<div class="flex items-center justify-center" v-if="taskResults?.[index]?.data?.status === 'archived'">
|
||||
<div class="text-sm text-red-500 px-2 py-1 rounded-md bg-red-100">{{ t('page.result.imageCompress.failed') }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center" v-if="taskResults?.[index]?.data?.status === 'success'">
|
||||
<div class="flex flex-col gap-1 items-center shrink-0">
|
||||
<div class="flex flex-row gap-2 items-center shrink-0" v-if="taskResults?.[index]?.data?.status === 'success'">
|
||||
<div class="flex flex-col gap-1 items-center">
|
||||
<div class="rounded flex flex-row items-center bg-green-100 text-green-600 px-1 text-xs">
|
||||
<LucideArrowDown class="size-4" />
|
||||
{{
|
||||
|
||||
190
front/components/Result/ImageConvertResult.vue
Normal file
190
front/components/Result/ImageConvertResult.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { useQueries, useQuery } from '@tanstack/vue-query'
|
||||
import { AsyncButton } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { filesize } from 'filesize'
|
||||
import useMyAppShare from '@/composables/useMyAppShare'
|
||||
import { toast } from 'vue-sonner'
|
||||
import type { handleFileComponentProps } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
const props = defineProps<handleFileComponentProps>()
|
||||
const fileIds = computed(() => props?.data?.files?.map((item) => item.id))
|
||||
const targetExt = computed(() => props?.data?.config?.target_ext)
|
||||
const { data: taskIds } = useQuery({
|
||||
queryKey: ['create-image-convert', fileIds.value, targetExt.value],
|
||||
queryFn: async () => {
|
||||
return await Promise.all(
|
||||
props?.data?.files?.map(async (file) => {
|
||||
const { id } = file || {}
|
||||
if (!id) return
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
id?: string
|
||||
}
|
||||
}>(`/api/task/image:convert`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
file_id: id,
|
||||
target_ext: targetExt.value,
|
||||
},
|
||||
})
|
||||
return data?.data?.id
|
||||
})
|
||||
)
|
||||
},
|
||||
staleTime: Infinity,
|
||||
enabled: !!fileIds.value && fileIds.value?.length > 0,
|
||||
})
|
||||
|
||||
const taskResults = useQueries({
|
||||
queries: computed(
|
||||
() =>
|
||||
taskIds?.value?.filter(Boolean).map((taskId) => {
|
||||
return {
|
||||
queryKey: ['task-image-convert', taskId],
|
||||
queryFn: async () => {
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
result: {
|
||||
old_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
new_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
}[]
|
||||
status: 'success' | 'retry' | 'archived'
|
||||
err?: {
|
||||
message?: string
|
||||
retry?: number
|
||||
max_retry?: number
|
||||
}
|
||||
}
|
||||
}>(`/api/task/${taskId}`)
|
||||
return data?.data
|
||||
},
|
||||
enabled: !!taskId,
|
||||
}
|
||||
}) ?? []
|
||||
),
|
||||
})
|
||||
|
||||
const { downloadFileByShareId, createFileShare } = useMyAppShare()
|
||||
|
||||
const { counter, pause } = useInterval(2000, { controls: true })
|
||||
|
||||
watch(
|
||||
() => counter.value,
|
||||
() => {
|
||||
taskResults.value.forEach((item) => {
|
||||
if (['success', 'archived'].includes(item.data?.status ?? '')) return
|
||||
item.refetch()
|
||||
})
|
||||
|
||||
if (taskResults.value.every((item) => ['success', 'archived'].includes(item.data?.status ?? ''))) {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const totalExt = computed(() => {
|
||||
return props?.data?.files?.reduce<string[]>((acc, item) => {
|
||||
const [, originalName, originalExt] = item?.file?.name?.match(/(.*?)\.([^.]+(?:\.[^.]+)*)$/) ?? []
|
||||
if (originalExt) acc.push(originalExt)
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<BaseCard class="flex flex-col gap-3" :title="t('page.result.imageConvert.title')" :showBackButton="true">
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-2/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageConvert.convert') }}</div>
|
||||
<div class="text-2xl font-light flex flex-row items-center gap-1">
|
||||
<div v-for="ext in totalExt" class="rounded flex flex-row items-center bg-primary/20 text-primary p-1 py-0.5 text-sm">
|
||||
{{ ext?.toUpperCase() }}
|
||||
</div>
|
||||
<LucideChevronsRight class="size-6" />
|
||||
<div class="rounded flex flex-row items-center bg-primary/20 text-primary p-1 py-0.5 text-sm">
|
||||
{{ targetExt?.toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-1/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageConvert.task') }}</div>
|
||||
<div class="text-3xl font-light">{{ taskResults.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in props?.data?.files" class="flex flex-row rounded-xl bg-white/70 p-3 justify-between w-full gap-3">
|
||||
<div class="flex flex-row gap-2 items-center w-full overflow-hidden">
|
||||
<div class="*:h-12 overflow-hidden">
|
||||
<FileIcon :file="item?.file" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 flex-1 overflow-hidden">
|
||||
<div class="truncate w-auto">{{ item?.file?.name }}</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(item?.file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="!taskResults?.[index]?.data">
|
||||
<Skeleton class="w-16 h-12" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 items-center text-sm" v-if="taskResults?.[index]?.data?.status === 'retry'">
|
||||
<LucideLoader2 class="size-4 animate-spin" />
|
||||
{{
|
||||
t('page.result.imageConvert.retry', [
|
||||
taskResults?.[index]?.data?.err?.retry ?? 0,
|
||||
taskResults?.[index]?.data?.err?.max_retry ?? 0,
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="taskResults?.[index]?.data?.status === 'archived'">
|
||||
<div class="text-sm text-red-500 px-2 py-1 rounded-md bg-red-100">{{ t('page.result.imageConvert.failed') }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center shrink-0" v-if="taskResults?.[index]?.data?.status === 'success'">
|
||||
<div class="flex flex-col gap-1 items-center">
|
||||
<div class="rounded flex flex-row items-center bg-primary/20 text-primary px-1 text-xs">
|
||||
{{ targetExt.toUpperCase() }}
|
||||
</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(taskResults?.[index]?.data?.result?.[0]?.new_file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
<AsyncButton
|
||||
size="icon"
|
||||
@click="
|
||||
async () => {
|
||||
const { new_file } = taskResults?.[index]?.data?.result?.[0] || {}
|
||||
if (!new_file?.id) return
|
||||
const [, originalName, originalExt] = item?.file?.name?.match(/(.*?)\.([^.]+(?:\.[^.]+)*)$/) ?? []
|
||||
const data = await createFileShare({
|
||||
files: [{ id: new_file?.id as string, name: `${originalName}.${targetExt}` }],
|
||||
config: {
|
||||
download_nums: 1,
|
||||
expire_time: 60,
|
||||
has_pickup_code: false,
|
||||
has_password: false,
|
||||
},
|
||||
})
|
||||
const { id } = data?.[0]?.data || {}
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await downloadFileByShareId(id)
|
||||
} catch (error: any) {
|
||||
toast.error(error?.data?.message || error?.message || error)
|
||||
}
|
||||
}
|
||||
"
|
||||
><LucideArrowDown
|
||||
/></AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
@@ -144,6 +144,13 @@
|
||||
"retry": "Retry {0}/{1}",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "Image Convert",
|
||||
"convert": "Convert",
|
||||
"task": "Task",
|
||||
"retry": "Retry {0}/{1}",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"text": {
|
||||
"title": "Share Successful",
|
||||
"info": "Info",
|
||||
|
||||
@@ -144,6 +144,13 @@
|
||||
"retry": "重试 {0}/{1}",
|
||||
"failed": "失败"
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "图片转换",
|
||||
"convert": "转换",
|
||||
"task": "任务",
|
||||
"retry": "重试 {0}/{1}",
|
||||
"failed": "失败"
|
||||
},
|
||||
"text": {
|
||||
"title": "分享成功",
|
||||
"info": "信息",
|
||||
|
||||
Reference in New Issue
Block a user