This commit is contained in:
Rao 2025-04-10 18:59:11 +08:00
parent 9f1bc38acd
commit c6e92cb876
15 changed files with 126 additions and 68 deletions

View File

@ -20,6 +20,7 @@ export interface ShareCode {
isUsed: boolean;
fileName?: string | null;
canUseTimes: number | null;
uploadIp?: string;
}
export interface GenerateShareCodeResponse {
id?: string;
@ -154,7 +155,9 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
filetype,
size,
}
}
},
uploadIp,
createdAt: new Date()
};
} catch (error) {
this.logger.error('Failed to generate share code', error);
@ -172,6 +175,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
code,
isUsed: false,
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
deletedAt: null
},
});
if (shareCode.canUseTimes <= 0) {
@ -280,7 +284,8 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
this.logger.log('验证分享码结果:', shareCode);
if (!shareCode) {
throw new NotFoundException('分享码无效或已过期');
this.logger.log('分享码无效或已过期');
return null
}
// 获取文件信息
@ -308,7 +313,9 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
meta: {
filename, filetype, size
}
}
},
uploadIp: shareCode.uploadIp,
createdAt: shareCode.createdAt
};
this.logger.log('返回给前端的数据:', response); // 添加日志

View File

@ -45,7 +45,8 @@ export const CodeManageProvider = ({ children }: { children: React.ReactNode })
...(searchKeyword ? {
OR: [
{ fileName: { contains: searchKeyword } },
{ code: { contains: searchKeyword } }
{ code: { contains: searchKeyword } },
{ uploadIp: { contains: searchKeyword } }
]
} : {})
};

View File

@ -1,11 +1,13 @@
import { Button, Form, Input } from 'antd';
import { useCodeManageContext } from '../CodeManageContext';
import { ChangeEvent, useEffect, useRef } from 'react';
import { useSearchParams } from "react-router-dom";
export default function CodeManageSearchBase({ keyword }: { keyword?: string }) {
const { setCurrentPage, searchRefetch, setSearchKeyword, searchKeyword } = useCodeManageContext();
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
const formRef = Form.useForm()[0]; // 获取表单实例
const [searchParams, setSearchParams] = useSearchParams();
// 当 keyword 属性变化时更新表单值
useEffect(() => {
@ -14,6 +16,17 @@ export default function CodeManageSearchBase({ keyword }: { keyword?: string })
}
}, [keyword, formRef]);
// 监听 searchKeyword 变化,如果 URL 参数中有 keyword 且 searchKeyword 发生变化,则清空 URL 参数
useEffect(() => {
const urlKeyword = searchParams.get('keyword');
if (urlKeyword && searchKeyword !== urlKeyword) {
// 创建一个新的 URLSearchParams 对象,不包含 keyword 参数
const newParams = new URLSearchParams(searchParams);
newParams.delete('keyword');
setSearchParams(newParams);
}
}, [searchKeyword, searchParams, setSearchParams]);
const onSearch = (value: string) => {
console.log(value);
setSearchKeyword(value);
@ -41,9 +54,10 @@ export default function CodeManageSearchBase({ keyword }: { keyword?: string })
<Form form={formRef}>
<Form.Item name="search" label="关键字搜索">
<Input.Search
placeholder="输入分享码或文件名"
placeholder="输入分享码、文件名或用户IP"
enterButton
value={searchKeyword}
allowClear
onSearch={onSearch}
onChange={onChange}
/>

View File

@ -25,8 +25,11 @@ export default function Activate() {
<ActivityItem
key={item.id}
icon={<FileOutlined className="text-blue-500" />}
title={`来自${item.uploadIp}的用户上传了文件:"${item.fileName}",文件大小:${Math.max(Number(item.resource.meta.size) / 1024 / 1024, 0.01).toFixed(2)}MB分享码${item.code}`}
time={item.createdAt?.toLocaleString()}
ip={item.uploadIp}
filename={item.fileName}
filesize={Math.max(Number(item.resource.meta.size) / 1024 / 1024, 0.01).toFixed(2)}
code={item.code}
/>
</div>
))}

View File

@ -2,11 +2,14 @@ import { motion } from 'framer-motion';
// 活动项组件
interface ActivityItemProps {
icon: React.ReactNode;
title: string;
time: string;
ip: string;
filename: string;
filesize: string;
code: string;
}
export default function ActivityItem({ icon, title, time }: ActivityItemProps) {
export default function ActivityItem({ icon, time, ip ,filename, filesize, code }: ActivityItemProps) {
return (
<motion.div
className="flex items-center py-2"
@ -18,7 +21,12 @@ export default function ActivityItem({ icon, title, time }: ActivityItemProps) {
{icon}
</div>
<div className="flex-grow">
<div className="text-gray-800">{title}</div>
<div className='text-gray-500'>
<span className='text-red-500'>{ip}</span>
<span className='text-blue-500'>"{filename}"</span>
<span className='text-gray-500'>{filesize}MB</span>
<span className='text-primary-900'>{code}</span>
</div>
<div className="text-gray-400 text-xs">{time}</div>
</div>
</motion.div>

View File

@ -74,7 +74,7 @@ export default function Board() {
className="h-full"
>
<div className="flex flex-col h-full justify-between py-2">
<Statistic value={isShareCodeAllLoading ? 0 : `${(shareCodeAll?.totalSize / 1024 / 1024).toFixed(2)}MB`} />
<Statistic value={isShareCodeAllLoading ? 0 : `${(shareCodeAll?.totalSize / 1024 / 1024 / 1024).toFixed(2)}GB`} />
{
isShareCodeTodayLoading || isShareCodeYesterdayLoading
? <Spin />

View File

@ -13,7 +13,7 @@ export const Header: React.FC<HeaderProps> = ({ showLoginButton = true }) => {
const isAdmin = hasEveryPermissions(RolePerms.MANAGE_ANY_POST) && isAuthenticated;
return (
<div className="w-[1000px] mx-auto flex justify-between items-center mt-2">
<div className="w-[1000px] mx-auto flex justify-between items-center pt-3">
<div className="flex items-center">
<img src="/logo.svg" className="h-16 w-16" />
<span className="text-4xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm"></span>

View File

@ -1,7 +1,8 @@
import { useState, useEffect } from "react";
import { Layout, Menu } from "antd";
import { FileOutlined, DashboardOutlined } from "@ant-design/icons";
import { Layout, Menu, Button, Space } from "antd";
import { FileOutlined, DashboardOutlined, HomeOutlined, LogoutOutlined } from "@ant-design/icons";
import { NavLink, Outlet, useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "@web/src/providers/auth-provider";
const { Sider, Content } = Layout;
@ -10,6 +11,7 @@ export default function QuickFileManage() {
const navigate = useNavigate();
const location = useLocation();
const [currentKey, setCurrentKey] = useState("1");
const { logout } = useAuth();
useEffect(() => {
// 根据当前URL路径设置currentKey
@ -28,15 +30,12 @@ export default function QuickFileManage() {
onCollapse={(value) => setCollapsed(value)}
theme="light"
style={{
borderRight: "1px solid #f0f0f0"
borderRight: "1px solid #f0f0f0",
display: "flex",
flexDirection: "column"
}}
>
<div
className="flex items-center justify-center cursor-pointer w-full"
onClick={() => {
navigate('/');
}}
>
<div className="flex items-center justify-center cursor-pointer w-full">
<img src="/logo.svg" className="h-10 w-10" />
{!collapsed && <span className="text-2xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm"></span>}
</div>
@ -57,6 +56,27 @@ export default function QuickFileManage() {
},
]}
/>
<div className="mt-auto p-2 flex justify-between" style={{marginTop: "auto"}}>
<Button
type="link"
icon={<HomeOutlined />}
onClick={() => navigate('/')}
style={{flex: 1}}
>
{!collapsed && "首页"}
</Button>
<Button
type="link"
icon={<LogoutOutlined />}
onClick={async () => {
navigate('/');
await logout();
}}
style={{flex: 1}}
>
{!collapsed && "登出"}
</Button>
</div>
</Sider>
<Content style={{ padding: "24px" }}>
<Outlet></Outlet>

View File

@ -10,48 +10,38 @@ export default function QuickUploadPage() {
const [form] = Form.useForm();
const uploadFileId = Form.useWatch(["file"], form)?.[0]
return (
<div className="w-full ">
<div className="w-full min-h-screen bg-gray-50">
<Header />
<div className="max-w-[1000px] mx-auto">
<div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-4">
<span className="text-lg block text-zinc-700 py-4">使</span>
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true} />
<div className="max-w-[1000px] mx-auto px-4 py-6">
<div className="my-1 p-6 bg-white border border-gray-100 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-5 pb-4 border-b border-gray-100">
<span className="text-xl font-medium text-gray-800">使</span>
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" />
</div>
<div>
<div className="bg-gray-50 bg-opacity-70 rounded-lg p-4">
<ShareCodeValidator />
</div>
</div>
<div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-4">
<span className="text-lg text-zinc-700"></span>
<div className="my-6 p-6 bg-white border border-gray-100 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-5 pb-4 border-b border-gray-100">
<span className="text-xl font-medium text-gray-800"></span>
<CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" />
</div>
<div>
<div className="mb-8">
<ShareCodeGenerator fileId={uploadFileId} />
</div>
<div className="mt-4">
<Form form={form}>
<Form.Item name="file">
<TusUploader
multiple={false}
style={"w-full py-4 mb-0 h-64"}
></TusUploader>
style="w-full py-8 px-4 mb-0 h-72 border-2 border-dashed border-gray-200 hover:border-blue-400 bg-gray-50 bg-opacity-50 rounded-lg transition-colors"
/>
</Form.Item>
</Form>
</div>
<div style={{ marginBottom: '40px' }}>
<ShareCodeGenerator
fileId={uploadFileId}
/>
</div>
</div>
{/* <Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
</TabPane>
<TabPane tab="下载文件" key="download">
</TabPane>
</Tabs> */}
</div>
</div>
)

View File

@ -40,6 +40,15 @@ export const QuickFileProvider = ({ children }: { children: React.ReactNode }) =
const [isGetingFileId, setIsGetingFileId] = useState(false);
const [downloadCode, setDownloadCode] = useState<string | null>(null);
const saveCodeRecord = (data: ShareCodeResponse, recordName: string) => {
if (data.canUseTimes == 0) {
const existingGeneratorRecords = localStorage.getItem(recordName);
if (existingGeneratorRecords && data.code) {
const generatorRecords = JSON.parse(existingGeneratorRecords);
const filteredRecords = generatorRecords.filter((item: ShareCodeResponse) => item.code !== data.code);
localStorage.setItem(recordName, JSON.stringify(filteredRecords));
}
return;
}
const newRecord = {
id: `${Date.now()}`, // 生成唯一ID
code: data.code,
@ -55,7 +64,9 @@ export const QuickFileProvider = ({ children }: { children: React.ReactNode }) =
filename: data.resource.meta.filename,
filetype: data.resource.meta.filetype
}
}
},
uploadIp: data.uploadIp,
createdAt: data.createdAt
};
// 获取已有记录并添加新记录
const existingGeneratorRecords = localStorage.getItem(recordName);

View File

@ -43,11 +43,12 @@ export default function CodeRecord({ title, btnContent, recordName ,styles,isDow
localStorage.setItem(recordName, JSON.stringify(updatedRecords));
};
const handleDownload = async (code:string) => {
const handleDownload = async (code:string,recordName:string) => {
await setDownloadCode(code)
const {data:result} = await refetchShareCodeWithResource()
console.log('下载', result);
handleValidSuccess(result.resource.url, result.fileName);
saveAndUpdateRecord(result as any as ShareCodeResponse, 'shareCodeUploadRecords');
saveAndUpdateRecord(result as any as ShareCodeResponse, 'shareCodeDownloadRecords');
};
@ -75,7 +76,7 @@ export default function CodeRecord({ title, btnContent, recordName ,styles,isDow
key={item.id}
item={item}
onDelete={handleDelete}
onDownload={isDownload ? handleDownload : undefined}
onDownload={() => handleDownload(item.code, recordName)}
/>
))}
</div>

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, DatePicker, Form, message, Select } from 'antd';
import { Button, DatePicker, Form, InputNumber, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { useQueryClient } from '@tanstack/react-query';
import { getQueryKey } from '@trpc/react-query';
@ -73,8 +73,8 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
setIsGenerate(true);
setExpiresAt(dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'));
setCanUseTimes(data.canUseTimes);
saveCodeRecord(data,'shareCodeGeneratorRecords');
message.success('分享码生成成功'+data.code);
saveCodeRecord(data, 'shareCodeGeneratorRecords');
message.success('分享码生成成功' + data.code);
} catch (error) {
console.error('生成分享码错误:', error);
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
@ -92,7 +92,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
const date = dayjs().add(1, 'day').tz('Asia/Shanghai');
form.setFieldsValue({
expiresAt: date,
canUseTimes: 10
canUseTimes: 5
});
}, [form]);
@ -116,7 +116,10 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
<Form.Item name="expiresAt" className='mt-5'>
<DatePicker
showTime
disabledDate={(current) => current && current < dayjs().startOf('day')}
disabledDate={(current) =>
(current && current < dayjs().startOf('day')) ||
(current && current > dayjs().add(7, 'day').endOf('day'))
}
disabledTime={(current) => {
if (current && current.isSame(dayjs(), 'day')) {
return {
@ -137,16 +140,11 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
{"分享码的使用次数"}
</small>
<Form.Item name="canUseTimes" className='mt-5'>
<Select
<InputNumber
style={{ width: 120 }}
//onChange={handleChange}
options={[
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 30, label: '30' },
{ value: 40, label: '40' },
{ value: 50, label: '50' },
]}
min={1}
max={10}
defaultValue={5}
/>
</Form.Item>
</div>

View File

@ -22,8 +22,12 @@ export const ShareCodeValidator: React.FC = ({ }) => {
try {
const { data: latestResult } = await refetchShareCodeWithResource();
console.log('验证分享码返回数据:', latestResult);
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
handleValidSuccess(latestResult.resource.url, latestResult.fileName);
if (latestResult) {
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
handleValidSuccess(latestResult.resource.url, latestResult.fileName);
} else {
message.error('分享码无效或已过期');
}
} catch (error) {
console.error('验证分享码失败:', error);
message.error('分享码无效或已过期');
@ -32,6 +36,7 @@ export const ShareCodeValidator: React.FC = ({ }) => {
const getDownloadUrl = useCallback(async () => {
try {
const { data: latestResult } = await refetchShareCodeWithResource();
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
console.log('验证分享码返回数据:', latestResult);
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${latestResult.resource.url}`;
copyToClipboard(downloadUrl)
@ -85,7 +90,7 @@ export const ShareCodeValidator: React.FC = ({ }) => {
<span >{`分享码:${downloadResult?.code ? downloadResult.code : ''}`}</span>
<span >{`文件名:${downloadResult?.fileName ? downloadResult.fileName : ''}`}</span>
<span >{`过期时间:${downloadResult?.expiresAt ? dayjs(downloadResult.expiresAt).format('YYYY-MM-DD HH:mm:ss') : ''}`}</span>
<span >{`剩余使用次数:${downloadResult?.canUseTimes ? downloadResult.canUseTimes : ''}`}</span>
<span >{`剩余使用次数:${downloadResult?.canUseTimes ? downloadResult.canUseTimes : 0}`}</span>
</div>
)
}

View File

@ -100,7 +100,7 @@ server {
# 仅供内部使用
internal;
# 代理到认证服务
proxy_pass http://192.168.43.206:3001/auth/file;
proxy_pass http://192.168.43.206:3006/auth/file;
# 请求优化:不传递请求体
proxy_pass_request_body off;

Binary file not shown.