Merge pull request #49 from oiov/dev

Add clickable tooltip
This commit is contained in:
oiov
2025-07-12 11:35:34 +08:00
committed by GitHub
8 changed files with 139 additions and 65 deletions
+18
View File
@@ -2,10 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { getPlanQuota } from "@/lib/dto/plan";
import { getMultipleConfigs } from "@/lib/dto/system-config";
import { checkUserStatus } from "@/lib/dto/user";
import { createS3Client } from "@/lib/r2";
import { getCurrentUser } from "@/lib/session";
import { restrictByTimeRange } from "@/lib/team";
import { generateFileKey } from "@/lib/utils";
export async function POST(request: NextRequest) {
@@ -42,6 +44,22 @@ export async function POST(request: NextRequest) {
});
}
const plan = await getPlanQuota(user.team!);
for (const file of files) {
if (Number(file.size) > Number(plan.stMaxFileSize)) {
return Response.json(`File (${file.name}) size limit exceeded`, {
status: 403,
});
}
}
const limit = await restrictByTimeRange({
model: "userFile",
userId: user.id,
limit: Number(plan.stMaxFileCount),
rangeType: "month",
});
if (limit) return Response.json(limit.statusText, { status: limit.status });
const R2 = createS3Client(
configs.s3_config_01.endpoint,
configs.s3_config_01.access_key_id,
+59 -59
View File
@@ -6,10 +6,12 @@ import { User } from "@prisma/client";
import {
Archive,
Download,
FileAudio,
FileCode,
FileSpreadsheet,
FileText,
FileType2,
FileVideo,
Folder,
ImageOff,
Trash2,
@@ -25,20 +27,13 @@ import {
formatFileSize,
truncateMiddle,
} from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ClickableTooltip } from "@/components/ui/tooltip";
import { BucketInfo, DisplayType, FileListData } from "@/components/file";
import { UrlForm } from "../forms/url-form";
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";
@@ -52,6 +47,7 @@ import {
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Modal } from "../ui/modal";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Skeleton } from "../ui/skeleton";
import { Switch } from "../ui/switch";
import { TableCell, TableRow } from "../ui/table";
@@ -92,7 +88,7 @@ export default function UserFileList({
const [currentSelectFile, setCurrentSelectFile] =
useState<UserFileData | null>();
const isAdmin = action.includes("/admin");
// const isAdmin = action.includes("/admin");
const getFileUrl = (key: string) => {
return `${bucketInfo.custom_domain}/${key}`;
@@ -155,7 +151,7 @@ export default function UserFileList({
const handleGenerateShortLink = async (urlId: string) => {
if (!shortTarget) return;
try {
const response = await fetch(`${action}/r2/short`, {
const response = await fetch(`${action}/r2/files/short`, {
method: "PUT",
body: JSON.stringify({ urlId, fileId: shortTarget?.id }),
});
@@ -208,7 +204,14 @@ export default function UserFileList({
const renderFileLinks = (file: UserFileData, index: number) => (
<>
{!isAdmin && file.shortUrlId && (
<div className="flex items-center gap-2">
<Icons.fileText className="size-3 flex-shrink-0" />
<p className="line-clamp-1 truncate rounded-md bg-neutral-100 p-1.5 text-xs dark:bg-neutral-800">
{file.path}
</p>
<CopyButton className="size-6" value={file.path} />
</div>
{file.shortUrlId && (
<div className="flex items-center gap-2">
<Icons.unLink className="size-3 flex-shrink-0 text-blue-500" />
<Link
@@ -308,30 +311,13 @@ export default function UserFileList({
</div>
)}
<div className={cn("col-span-3 items-center space-x-3 text-sm")}>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger
className={cn(
"flex items-center justify-start gap-1 break-all text-start",
file.status !== 1 && "text-muted-foreground",
)}
>
{truncateMiddle(file.path)}
{file.status === 1 && (
<CopyButton
className="size-6"
value={getFileUrl(file.path)}
/>
)}
</TooltipTrigger>
<TooltipContent
side="right"
className="w-72 space-y-1 text-wrap p-3 text-start"
>
<ClickableTooltip
content={
<div className="w-72 space-y-1 text-wrap p-3 text-start">
{file.mimeType.startsWith("image/") &&
file.status === 1 && (
<img
className="mb-2 max-h-[70vh] w-72 rounded shadow"
className="mb-2 max-h-[300px] w-fit rounded shadow"
width={300}
height={300}
src={getFileUrl(file.path)}
@@ -339,9 +325,24 @@ export default function UserFileList({
/>
)}
{renderFileLinks(file, index)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<div
className={cn(
"flex items-center justify-start gap-1 break-all text-start",
file.status !== 1 && "text-muted-foreground",
)}
>
{truncateMiddle(file.path)}
{file.status === 1 && (
<CopyButton
className="size-6"
value={getFileUrl(file.path)}
/>
)}
</div>
</ClickableTooltip>
</div>
<div className="col-span-2 hidden items-center text-xs sm:flex">
<Badge className="truncate" variant="outline">
@@ -352,17 +353,18 @@ export default function UserFileList({
{formatFileSize(file.size || 0)}
</div>
<div className="col-span-1 hidden items-center text-xs sm:flex">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="truncate">
{file.user.name ?? file.user.email}
</TooltipTrigger>
<TooltipContent>
<ClickableTooltip
content={
<>
<p>{file.user.name}</p>
<p>{file.user.email}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
}
>
<div className="truncate">
{file.user.name ?? file.user.email}
</div>
</ClickableTooltip>
</div>
<div className="col-span-1 hidden items-center text-nowrap text-xs sm:flex">
<TimeAgoIntl date={file.updatedAt as Date} />
@@ -492,19 +494,13 @@ export default function UserFileList({
)}
{React.cloneElement(getFileIcon(file, bucketInfo), { size: 40 })}
<div className="w-full text-center">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger className="mx-auto line-clamp-2 max-w-[60px] break-all px-2 pb-1 text-left text-xs font-medium text-muted-foreground group-hover:text-blue-500 sm:max-w-[100px]">
{truncateMiddle(file.path || "")}
</TooltipTrigger>
<TooltipContent
side="right"
className="max-w-[300px] space-y-1 p-3 text-start"
>
<ClickableTooltip
content={
<div className="max-w-[300px] space-y-1 p-3 text-start">
{file.mimeType.startsWith("image/") &&
file.status === 1 && (
<img
className="mb-2 max-h-[70vh] w-fit rounded shadow"
className="mb-2 max-h-[300px] w-fit rounded shadow"
width={300}
height={300}
src={getFileUrl(file.path)}
@@ -574,9 +570,13 @@ export default function UserFileList({
</Button>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<div className="mx-auto line-clamp-2 max-w-[60px] break-all px-2 pb-1 text-left text-xs font-medium text-muted-foreground group-hover:text-blue-500 sm:max-w-[100px]">
{truncateMiddle(file.path || "")}
</div>
</ClickableTooltip>
</div>
</div>
</div>
@@ -768,12 +768,12 @@ const getFileIcon = (file: UserFileData, bucketInfo: BucketInfo) => {
// 音频文件
if (mimeType.startsWith("audio/")) {
return <FileText {...iconProps} className="text-purple-500" />;
return <FileAudio {...iconProps} className="text-purple-500" />;
}
// 视频文件
if (mimeType.startsWith("video/")) {
return <FileText {...iconProps} className="text-pink-500" />;
return <FileVideo {...iconProps} className="text-pink-500" />;
}
// 默认文件图标
+17 -2
View File
@@ -12,6 +12,7 @@ import {
X,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { cn, formatFileSize } from "@/lib/utils";
import { useFileUpload } from "@/hooks/use-file-upload";
@@ -62,7 +63,20 @@ export const FileUploader = ({
useEffect(() => {
if (selectedFile) {
addFiles(selectedFile);
const outOfLimitSizeFiles = selectedFile.some(
(file) => file.size > Number(plan?.stMaxFileSize ?? 0),
);
const notOutOfLimitSizeFiles = selectedFile.filter(
(file) => file.size <= Number(plan?.stMaxFileSize ?? 0),
);
addFiles(notOutOfLimitSizeFiles);
if (outOfLimitSizeFiles) {
toast.warning(
`File size exceeds the limit of ${formatFileSize(
Number(plan?.stMaxFileSize ?? 0),
)}`,
);
}
}
}, [selectedFile]);
@@ -99,6 +113,7 @@ export const FileUploader = ({
</div>
</div>
<Badge className="text-xs">
{t("Limit")}:{" "}
{formatFileSize(Number(plan?.stMaxFileSize || "0"), {
precision: 0,
})}{" "}
@@ -327,7 +342,7 @@ export const FileUploader = ({
className="flex items-center gap-2 rounded-md bg-green-500 px-4 py-2 text-white transition-colors hover:bg-green-600 disabled:cursor-not-allowed disabled:bg-gray-400"
>
<Play className="h-4 w-4" />
Start Upload
{t("Start Upload")}
</Button>
<Button
variant="destructive"
+37
View File
@@ -1,6 +1,7 @@
"use client";
import * as React from "react";
import { useState } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
@@ -39,3 +40,39 @@ export {
TooltipProvider,
TooltipArrow,
};
export const ClickableTooltip = ({ children, content }) => {
const [open, setOpen] = useState(false);
const handleClick = (e) => {
e.preventDefault();
setOpen(!open);
};
return (
<TooltipProvider>
<Tooltip
delayDuration={0}
open={open}
onOpenChange={setOpen}
disableHoverableContent
>
<TooltipTrigger
asChild
onPointerEnter={(e) => e.preventDefault()} // 阻止指针进入事件
onPointerLeave={(e) => e.preventDefault()} // 阻止指针离开事件
onPointerMove={(e) => e.preventDefault()} // 阻止指针移动事件
onFocus={(e) => e.preventDefault()} // 阻止焦点事件
onBlur={(e) => e.preventDefault()}
>
<div onClick={handleClick} className="cursor-pointer truncate">
{children}
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="p-1">{content}</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
);
};
+3 -1
View File
@@ -370,7 +370,9 @@
"Upload Files": "Upload Files",
"Uploud channel": "Uploud channel",
"Do not close the window until the upload is complete": "Do not close the window until the upload is complete",
"Pending Upload": "Pending Upload"
"Pending Upload": "Pending Upload",
"Start Upload": "Start Upload",
"Limit": "Limit"
},
"Landing": {
"settings": "Settings",
+3 -1
View File
@@ -370,7 +370,9 @@
"Upload Files": "上传文件",
"Uploud channel": "渠道",
"Do not close the window until the upload is complete": "在上传完成之前不要浏览器关闭窗口",
"Pending Upload": "等待上传"
"Pending Upload": "等待上传",
"Start Upload": "开始上传",
"Limit": "限制"
},
"Landing": {
"settings": "设置",
@@ -9,7 +9,7 @@ INSERT INTO "system_configs"
VALUES
(
's3_config_01',
'{"enabled":true,"platform":"cloudflare","channel":"r2","provider_name":"Cloudflare R2","account_id":"","access_key_id":"","secret_access_key":"","endpoint":"https://<account_id>.r2.cloudflarestorage.com","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"auto","public":true}]}',
'{"enabled":true,"platform":"cloudflare","channel":"r2","provider_name":"Cloudflare R2","account_id":"","access_key_id":"","secret_access_key":"","endpoint":"https://<account_id>.r2.cloudflarestorage.com","buckets":[{"bucket":"","prefix":"","file_types":"","region":"auto","custom_domain":"","file_size":"26214400","public":true}]}',
'OBJECT',
'R2 存储桶配置'
);
+1 -1
View File
File diff suppressed because one or more lines are too long