feat: add qrcode for files

This commit is contained in:
oiov
2025-07-07 11:22:11 +08:00
parent 4382b0b045
commit d5acf4f34d
9 changed files with 142 additions and 37 deletions
@@ -30,6 +30,7 @@ export default async function DashboardPage() {
apiKey: user.apiKey || "",
email: user.email || "",
role: user.role,
team: user.team,
}}
action="/api/storage"
/>
+114 -30
View File
@@ -2,6 +2,7 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { User } from "@prisma/client";
import {
Archive,
Download,
@@ -17,7 +18,14 @@ import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { UserFileData } from "@/lib/dto/files";
import { cn, formatDate, formatFileSize, truncateMiddle } from "@/lib/utils";
import {
cn,
downloadFile,
downloadFileFromUrl,
formatDate,
formatFileSize,
truncateMiddle,
} from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import {
Tooltip,
@@ -32,6 +40,7 @@ import { CopyButton } from "../shared/copy-button";
import { EmptyPlaceholder } from "../shared/empty-placeholder";
import { Icons } from "../shared/icons";
import { PaginationWrapper } from "../shared/pagination";
import QRCodeEditor from "../shared/qr";
import { TimeAgoIntl } from "../shared/time-ago";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
@@ -47,22 +56,8 @@ import { Modal } from "../ui/modal";
import { Skeleton } from "../ui/skeleton";
import { TableCell, TableRow } from "../ui/table";
export default function UserFileList({
files,
isLoading,
bucketInfo,
action,
view,
showMutiCheckBox,
currentPage,
pageSize,
setCurrentPage,
setPageSize,
selectedFiles,
setSelectedFiles,
onRefresh,
onSelectAll,
}: {
interface Props {
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role" | "team">;
files?: FileListData;
isLoading: boolean;
bucketInfo: BucketInfo;
@@ -78,12 +73,33 @@ export default function UserFileList({
onRefresh: () => void;
onSelectAll: () => void;
onDeleteAll: () => void;
}) {
}
export default function UserFileList({
user,
files,
isLoading,
bucketInfo,
action,
view,
showMutiCheckBox,
currentPage,
pageSize,
setCurrentPage,
setPageSize,
selectedFiles,
setSelectedFiles,
onRefresh,
onSelectAll,
}: Props) {
const t = useTranslations("List");
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [shortTarget, setShortTarget] = useState<UserFileData | null>(null);
const [shortLinks, setShortLinks] = useState<string[]>([]);
const [isShowQrcode, setShowQrcode] = useState(false);
const [currentSelectFile, setCurrentSelectFile] =
useState<UserFileData | null>();
const getFileUrl = (key: string) => {
return `${bucketInfo.custom_domain}/${key}`;
@@ -97,7 +113,11 @@ export default function UserFileList({
}
};
const handleDownload = async (key: string) => {
const handleDownload = async (file: UserFileData) => {
downloadFileFromUrl(getFileUrl(file.path), file.name);
};
const handlePreviewRawFile = async (key: string) => {
try {
const response = await fetch(`${action}/r2/files`, {
method: "POST",
@@ -341,6 +361,21 @@ export default function UserFileList({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild>
<Button
className="flex w-full items-center gap-2"
size="sm"
variant="ghost"
onClick={() => {
setCurrentSelectFile(file);
setShowQrcode(!isShowQrcode);
}}
>
<Icons.qrcode className="size-4" />
{t("QR Code")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
className="flex w-full items-center gap-2"
@@ -363,7 +398,19 @@ export default function UserFileList({
className="flex w-full items-center gap-2"
size="sm"
variant="ghost"
onClick={() => handleDownload(file.path)}
onClick={() => handlePreviewRawFile(file.path)}
>
<Icons.eye className="size-4" />
{t("Raw Data")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
className="flex w-full items-center gap-2"
size="sm"
variant="ghost"
onClick={() => handleDownload(file)}
>
<Icons.download className="size-4" />
{t("Download")}
@@ -439,7 +486,7 @@ export default function UserFileList({
>
{file.mimeType.startsWith("image/") && (
<img
className="mb-2 max-h-[70vh] rounded shadow"
className="mb-2 max-h-[70vh] w-fit rounded shadow"
width={300}
height={300}
src={getFileUrl(file.path)}
@@ -482,24 +529,44 @@ export default function UserFileList({
<strong>Modified:</strong>{" "}
{formatDate(file.lastModified?.toString() || "")}
</p>
<div className="flex justify-end space-x-1">
<div className="flex items-center justify-end space-x-1 pt-2">
<Button
onClick={() => file.path && handleDownload(file.path)}
className="size-7"
className="flex h-7 w-full items-center gap-2"
size="sm"
variant="outline"
onClick={() => handlePreviewRawFile(file.path)}
>
<Icons.eye className="size-4" />
{t("Raw Data")}
</Button>
<Button
className="h-7 px-1.5 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground"
size="sm"
variant={"outline"}
onClick={() => {
setCurrentSelectFile(file);
setShowQrcode(!isShowQrcode);
}}
>
<Icons.qrcode className="size-4" />
</Button>
<Button
onClick={() => handlePreviewRawFile(file.path)}
className="h-7 px-1.5"
title="下载"
size="icon"
size="sm"
variant={"blue"}
>
<Download size={14} />
<Download className="size-4" />
</Button>
<Button
onClick={() => handleDeleteSingle(file)}
className="size-7"
className="h-7 px-1.5"
title="删除"
size="icon"
size="sm"
variant={"destructive"}
>
<Trash2 size={14} />
<Trash2 className="size-4" />
</Button>
</div>
</TooltipContent>
@@ -551,6 +618,23 @@ export default function UserFileList({
onRefresh={handleGenerateShortLink}
/>
</Modal>
<Modal
className="md:max-w-lg"
showModal={isShowQrcode}
setShowModal={setShowQrcode}
>
{currentSelectFile && (
<QRCodeEditor
user={{
id: user.id,
apiKey: user.apiKey || "",
team: user.team || "free",
}}
url={getFileUrl(currentSelectFile.path)}
/>
)}
</Modal>
</>
);
}
@@ -606,7 +690,7 @@ const getFileIcon = (
// 其他图片格式显示缩略图
return (
<img
className="max-h-12 max-w-24 rounded shadow"
className="max-h-12 w-fit max-w-24 rounded shadow"
height={60}
width={60}
src={
+2 -1
View File
@@ -36,7 +36,7 @@ import {
} from "../ui/dropdown-menu";
export interface FileListProps {
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role">;
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role" | "team">;
action: string;
}
@@ -310,6 +310,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
{!isLoading && r2Configs?.buckets && r2Configs.buckets.length > 0 && (
<UserFileList
user={user}
files={files}
isLoading={isLoadingFiles}
view={displayType}
+16 -1
View File
@@ -7,6 +7,7 @@ import { BucketInfo } from "@/components/file";
import { CopyButton } from "../shared/copy-button";
import { Icons } from "../shared/icons";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { UploadPendingItemType, UploadProgressType } from "./uploader";
@@ -24,7 +25,21 @@ const UploadPending = ({
const t = useTranslations("Components");
return (
<div className="space-y-2 rounded-lg">
{pendingUpload && <h2 className="font-semibold">{t("Upload List")}</h2>}
{progressList && (
<div className="flex items-center justify-between gap-3">
<h2 className="font-semibold">{t("Upload List")}</h2>
<Badge className="flex items-center gap-1">
{progressList.filter((i) => i.progress === 100).length}
<span>/</span>
{progressList.length}
</Badge>
</div>
)}
{pendingUpload && (
<p className="rounded-md border border-dashed bg-yellow-100 p-2 text-sm text-muted-foreground dark:bg-neutral-600">
Do not close the window until the upload is complete
</p>
)}
{pendingUpload &&
pendingUpload.map((item) => {
const progress =
+2 -1
View File
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { formatFileSize } from "@/lib/utils";
import { BucketInfo } from "@/components/file";
import { Icons } from "../shared/icons";
@@ -182,7 +183,7 @@ export default function Uploader({
try {
if (file.size > Number(bucketInfo.file_size || "26214400")) {
toast.warning("Upload Failed", {
description: `File '${file.name}' size exceeds the maximum allowed size of ${bucketInfo.file_size} bytes.`,
description: `File '${file.name}' size exceeds the maximum allowed size of ${formatFileSize(Number(bucketInfo.file_size || "0"))} bytes.`,
});
return;
}
-1
View File
@@ -12,7 +12,6 @@ import Link from "next/link";
import { debounce } from "lodash";
import { useTranslations } from "next-intl";
import { HexColorPicker } from "react-colorful";
import { toast } from "sonner";
import { getQRAsCanvas, getQRAsSVGDataUri, getQRData } from "@/lib/qr";
import { WRDO_QR_LOGO } from "@/lib/qr/constants";
+3 -1
View File
@@ -198,7 +198,9 @@
"Delete File": "Delete",
"Select all": "Select all",
"Delete selected": "Delete selected",
"Update short link": "Update short link"
"Update short link": "Update short link",
"QR Code": "QR Code",
"Raw Data": "Raw Data"
},
"Components": {
"Dashboard": "Dashboard",
+3 -1
View File
@@ -198,7 +198,9 @@
"Delete File": "删除文件",
"Select all": "全部选中",
"Delete selected": "删除选中",
"Update short link": "更新短链"
"Update short link": "更新短链",
"QR Code": "二维码",
"Raw Data": "在线预览"
},
"Components": {
"Dashboard": "用户面板",
+1 -1
View File
File diff suppressed because one or more lines are too long