mirror of
https://github.com/keven1024/015.git
synced 2026-05-26 15:13:30 +00:00
feat(front): add AboutChartTooltip and ImageCompressResult components for enhanced file upload statistics and visualization
This commit is contained in:
38
front/components/AboutChartTooltip.vue
Normal file
38
front/components/AboutChartTooltip.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: { name: string; value: string; color: string }[];
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
const dataKeyMap = {
|
||||||
|
file_size: "文件大小",
|
||||||
|
file_num: "文件数量",
|
||||||
|
processed: "处理数量",
|
||||||
|
failed: "失败数量",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-md bg-white p-2 flex flex-col gap-2">
|
||||||
|
<div class="text-sm font-medium">{{ title }}</div>
|
||||||
|
<div v-for="(item, index) in data" :key="index">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="h-5 w-2 rounded-full"
|
||||||
|
:style="{ backgroundColor: item.color ?? '#222' }"
|
||||||
|
></div>
|
||||||
|
<div class="text-xs font-medium">
|
||||||
|
{{ dataKeyMap?.[item.name as keyof typeof dataKeyMap] ?? item.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{{
|
||||||
|
["file_size"]?.includes(item?.name)
|
||||||
|
? filesize(item.value)
|
||||||
|
: item.value
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
146
front/components/Result/ImageCompressResult.vue
Normal file
146
front/components/Result/ImageCompressResult.vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { AsyncButton, Button } from "@/components/ui/button";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
import useAppShare from "~/composables/useShare";
|
||||||
|
const props = defineProps<{
|
||||||
|
data: { file: File; config: any; handle_type: string; file_id: string };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["create-image-compress", props?.data?.file_id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { file_id } = props?.data || {};
|
||||||
|
const data = await $fetch<{
|
||||||
|
code: number;
|
||||||
|
data: {
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
}>(`/api/image/compress`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
file_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data?.data;
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskId = computed(() => data?.value?.id);
|
||||||
|
|
||||||
|
const { data: taskData, refetch } = useQuery({
|
||||||
|
queryKey: ["image-compress-task", 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" | "processing" | "failed";
|
||||||
|
};
|
||||||
|
}>(`/api/image/compress/${taskId.value}`);
|
||||||
|
return data?.data;
|
||||||
|
},
|
||||||
|
enabled: !!taskId.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { downloadFile, createFileShare } = useAppShare();
|
||||||
|
|
||||||
|
const { counter, pause } = useInterval(2000, { controls: true });
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => counter.value,
|
||||||
|
() => {
|
||||||
|
if (taskData.value?.status === "success") {
|
||||||
|
pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<h2 class="text-lg">上传成功</h2>
|
||||||
|
<div class="flex flex-col gap-1 items-center">
|
||||||
|
<div class="flex flex-col h-30 items-center justify-center">
|
||||||
|
<FilePreviewView :value="props?.data?.file" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="taskData?.status === 'success'"
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
v-for="item in taskData?.result"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white/80 p-2 rounded-md w-full flex flex-row items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-center rounded-md bg-black/5 p-2"
|
||||||
|
>
|
||||||
|
<LucideImage />
|
||||||
|
</div>
|
||||||
|
{{ props?.data?.file?.name }}
|
||||||
|
<div class="flex flex-row gap-2 items-center text-sm">
|
||||||
|
<span class="opacity-75">{{
|
||||||
|
filesize(item.new_file.size ?? 0)
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
class="bg-green-200 text-green-600 rounded-md px-1 py-0.5 flex flex-row gap-1 items-center text-xs"
|
||||||
|
>
|
||||||
|
<LucideChevronDown class="size-4" />
|
||||||
|
{{
|
||||||
|
((1 - item.new_file.size / item.old_file.size) * 100).toFixed(
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AsyncButton
|
||||||
|
variant="outline"
|
||||||
|
class="bg-black/5"
|
||||||
|
size="icon"
|
||||||
|
@click="
|
||||||
|
async () => {
|
||||||
|
const data = await createFileShare({
|
||||||
|
file_id: item.new_file.id,
|
||||||
|
config: {
|
||||||
|
download_nums: 1,
|
||||||
|
expire_time: 60,
|
||||||
|
has_pickup_code: false,
|
||||||
|
has_password: false,
|
||||||
|
},
|
||||||
|
file_name: props?.data?.file?.name,
|
||||||
|
});
|
||||||
|
const { id } = data?.data || {};
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await downloadFile(id);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<LucideDownload />
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<Skeleton
|
||||||
|
class="w-full h-16 flex flex-row items-center justify-between"
|
||||||
|
v-for="i in 3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
233
front/pages/about.vue
Normal file
233
front/pages/about.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CurveType } from "@unovis/ts";
|
||||||
|
import { AreaChart } from "@/components/ui/chart-area";
|
||||||
|
import { cx } from "class-variance-authority";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import AboutChartTooltip from "@/components/AboutChartTooltip.vue";
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
import SparkMD5 from "spark-md5";
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["stat"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const data = await $fetch<{ data: any }>("/api/stat");
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartTabs = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "文件",
|
||||||
|
value: "storage",
|
||||||
|
total:
|
||||||
|
data.value?.chart?.storage?.reduce(
|
||||||
|
(acc: number, curr: { file_size: number; file_num: number }) =>
|
||||||
|
acc + curr.file_num,
|
||||||
|
0,
|
||||||
|
) ?? 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "任务",
|
||||||
|
value: "queue",
|
||||||
|
total:
|
||||||
|
data.value?.chart?.queue?.reduce(
|
||||||
|
(acc: number, curr: { processed: number; failed: number }) =>
|
||||||
|
acc + curr.processed + curr.failed,
|
||||||
|
0,
|
||||||
|
) ?? 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentFileSize = computed(() => {
|
||||||
|
return (
|
||||||
|
data.value?.chart?.storage?.reduce(
|
||||||
|
(acc: number, curr: { file_size: number; file_num: number }) =>
|
||||||
|
acc + curr.file_size,
|
||||||
|
0,
|
||||||
|
) ?? 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentChartTab = ref<"storage" | "queue">("storage");
|
||||||
|
const currentChartData = computed(() => {
|
||||||
|
const { storage, queue } = data.value?.chart || {};
|
||||||
|
if (currentChartTab.value === "storage") {
|
||||||
|
return {
|
||||||
|
data: storage,
|
||||||
|
index: "date",
|
||||||
|
categories: ["file_size", "file_num"],
|
||||||
|
colors: ["#22d3ee", "#c084fc"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: queue,
|
||||||
|
index: "date",
|
||||||
|
categories: ["processed", "failed"],
|
||||||
|
colors: ["#4ade80", "#f87171"],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const genUserAvatar = ({ email }: { email: string }) => {
|
||||||
|
if (!email) {
|
||||||
|
return "/logo.png";
|
||||||
|
}
|
||||||
|
return `https://www.gravatar.com/avatar/${SparkMD5.hash(email)}?d=retro`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserClick = ({ url, email }: { url: string; email: string }) => {
|
||||||
|
if (url) {
|
||||||
|
return navigateTo(url, { external: true });
|
||||||
|
}
|
||||||
|
if (email) {
|
||||||
|
return navigateTo(`mailto:${email}`, { external: true });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = computed(() => {
|
||||||
|
const { email, name, url } = data.value?.admin || {};
|
||||||
|
return [
|
||||||
|
...(!!name
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "站长",
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
url: url ?? (email ? `mailto:${email}` : null),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: "作者",
|
||||||
|
name: "keven1024",
|
||||||
|
email: "keven@fudaoyuan.icu",
|
||||||
|
url: "https://github.com/keven1024",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 my-5 flex flex-col gap-5"
|
||||||
|
>
|
||||||
|
<div class="text-xl font-normal">关于</div>
|
||||||
|
<div class="flex flex-col gap-2 items-center">
|
||||||
|
<NuxtImg src="/logo.png" class="size-20 rounded-xl" />
|
||||||
|
<div class="text-xl">015</div>
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
015
|
||||||
|
是一个开源的临时文件分享平台项目,支持临时大文件切片上传,临时文本上传、下载、分享
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold">系统信息</div>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Skeleton class="w-full h-20 rounded-xl" v-for="i in 2" :key="i" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="rounded-xl bg-white/50 flex-1 flex flex-col p-3">
|
||||||
|
<div class="opacity-75 text-xs">系统版本</div>
|
||||||
|
<div class="text-xl font-semibold">
|
||||||
|
{{ data?.version }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl bg-white/50 flex-1 flex flex-col p-3">
|
||||||
|
<div class="opacity-75 text-xs">存储空间</div>
|
||||||
|
<div class="text-right flex flex-row items-baseline">
|
||||||
|
<span class="text-lg font-semibold">{{
|
||||||
|
filesize(currentFileSize ?? 0)
|
||||||
|
}}</span>
|
||||||
|
<span class="text-md opacity-75"
|
||||||
|
>/ {{ filesize(data?.max_limit?.file_size ?? 0) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full w-full h-1 bg-black/10">
|
||||||
|
<div
|
||||||
|
class="rounded-full h-full bg-blue-500"
|
||||||
|
:style="{
|
||||||
|
width: `${(currentFileSize / (data?.max_limit?.file_size ?? 0)) * 100}%`,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="font-semibold">分析</div>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Skeleton class="w-full h-96 rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-col gap-2 bg-white/50 w-full rounded-xl py-5">
|
||||||
|
<div class="flex flex-row gap-2 px-5">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cx(
|
||||||
|
'rounded-md min-w-30 flex flex-col px-3 py-1.5 cursor-pointer',
|
||||||
|
currentChartTab === tab.value && 'bg-black/10',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-for="tab in chartTabs"
|
||||||
|
:key="tab.value"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
currentChartTab = tab.value as 'storage' | 'queue';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="opacity-75 text-xs">{{ tab.label }}</div>
|
||||||
|
<div class="text-lg font-semibold">{{ tab.total }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AreaChart
|
||||||
|
v-if="currentChartData"
|
||||||
|
class="h-64 w-full"
|
||||||
|
:key="currentChartTab"
|
||||||
|
:index="currentChartData.index"
|
||||||
|
:data="currentChartData.data"
|
||||||
|
:categories="currentChartData.categories"
|
||||||
|
:show-grid-line="false"
|
||||||
|
:show-legend="false"
|
||||||
|
:colors="currentChartData.colors"
|
||||||
|
:custom-tooltip="AboutChartTooltip"
|
||||||
|
:curve-type="CurveType.CatmullRom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Skeleton class="w-full h-20 rounded-xl" v-for="i in 2" :key="i" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<div class="flex flex-col gap-3" v-for="user in users" :key="user.name">
|
||||||
|
<div class="font-semibold">{{ user.title }}</div>
|
||||||
|
<div
|
||||||
|
class="rounded-xl bg-white/50 hover:bg-white/40 flex-1 flex flex-row items-center gap-2 p-3 cursor-pointer"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
handleUserClick(user);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="size-10 rounded-full bg-white/50">
|
||||||
|
<NuxtImg
|
||||||
|
:src="genUserAvatar(user)"
|
||||||
|
class="size-full rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-md font-semibold">{{ user.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user