rht
This commit is contained in:
parent
9f1bc38acd
commit
c6e92cb876
|
@ -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); // 添加日志
|
||||
|
|
|
@ -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 } }
|
||||
]
|
||||
} : {})
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center cursor-pointer w-full"
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
borderRight: "1px solid #f0f0f0",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
|
|
@ -10,49 +10,39 @@ 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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -22,8 +22,12 @@ export const ShareCodeValidator: React.FC = ({ }) => {
|
|||
try {
|
||||
const { data: latestResult } = await refetchShareCodeWithResource();
|
||||
console.log('验证分享码返回数据:', latestResult);
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
BIN
web-dist.zip
BIN
web-dist.zip
Binary file not shown.
Loading…
Reference in New Issue