This commit is contained in:
Rao 2025-04-09 17:01:21 +08:00
parent 9edce44b32
commit 34dde48aaf
10 changed files with 375 additions and 183 deletions

View File

@ -9,6 +9,7 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { BaseService } from '../base/base.service';
import { url } from 'inspector';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface ShareCode {
@ -25,12 +26,19 @@ export interface GenerateShareCodeResponse {
code: string;
expiresAt: Date;
canUseTimes: number;
fileName?: string;
resource: {
id: string;
type: string;
url: string;
meta: ResourceMeta
}
}
interface ResourceMeta {
filename: string;
filetype: string;
filesize: string;
size: string;
}
@Injectable()
@ -61,7 +69,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
if (!resource) {
throw new NotFoundException('文件不存在');
}
const { filename } = resource.meta as any as ResourceMeta
const { filename, filetype, size } = resource.meta as any as ResourceMeta
// 生成分享码
const code = this.generateCode();
// 查找是否已有分享码记录
@ -99,6 +107,17 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
code,
expiresAt,
canUseTimes,
fileName: filename || "downloaded_file",
resource: {
id: resource.id,
type: resource.type,
url: resource.url,
meta: {
filename,
filetype,
size,
}
}
};
} catch (error) {
this.logger.error('Failed to generate share code', error);
@ -274,7 +293,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
where: { fileId: shareCode.fileId },
});
this.logger.log('获取到的资源信息:', resource);
const { filename } = resource.meta as any as ResourceMeta
const { filename,filetype,size } = resource.meta as any as ResourceMeta
const fileUrl = resource?.url
if (!resource) {
throw new NotFoundException('文件不存在');
@ -282,12 +301,19 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
// 直接返回正确的数据结构
const response = {
fileId: shareCode.fileId,
fileName: filename || 'downloaded_file',
id:shareCode.id,
code: shareCode.code,
fileName: filename || 'downloaded_file',
expiresAt: shareCode.expiresAt,
url: fileUrl,
canUseTimes: shareCode.canUseTimes - 1,
resource:{
id:resource.id,
type:resource.type,
url:resource.url,
meta:{
filename,filetype,size
}
}
};
this.logger.log('返回给前端的数据:', response); // 添加日志
@ -316,7 +342,6 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
totalPages: number;
}> {
try {
console.log('args:', args.where.OR);
// 使用include直接关联查询Resource
const { items, totalPages } = await super.findManyWithPagination({
...args,

View File

@ -3,6 +3,7 @@ import { api } from "@nice/client";
import { createContext, useContext, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
import { ShareCodeResponse } from "../quick-file/quickFileContext";
interface CodeManageContextType {
editForm: FormInstance<any>;
@ -22,21 +23,7 @@ interface CodeManageContextType {
}
interface ShareCodeWithResource {
items: {
id: string;
code: string;
fileName: string;
fileSize: number;
expiresAt: string;
createdAt: string;
canUseTimes: number;
resource: {
id: string;
type: string;
url: string;
meta: any;
}
}[],
items: ShareCodeResponse[],
totalPages: number
}

View File

@ -4,29 +4,14 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import ShareCodeListCard from './ShareCodeListCard';
import { ShareCodeResponse } from '../../quick-file/quickFileContext';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface ShareCodeItem {
id: string;
code: string;
expiresAt: string;
fileName: string;
canUseTimes: number;
resource: {
id: string;
type: string;
url: string;
meta: {
size: number;
filename: string;
};
}
}
interface ShareCodeListProps {
data: ShareCodeItem[];
data: ShareCodeResponse[];
loading?: boolean;
onEdit?: (id: string,expiresAt:Date,canUseTimes:number,code:string) => void;
onDelete?: (id: string) => void;
@ -46,7 +31,7 @@ const ShareCodeList: React.FC<ShareCodeListProps> = ({
loading={loading}
renderItem={(item) => (
<List.Item>
<ShareCodeListCard item={item} onEdit={onEdit} onDelete={onDelete} />
<ShareCodeListCard item={item} onEdit={onEdit} onDelete={onDelete} styles='w-[262px]' />
</List.Item>
)}
/>

View File

@ -1,14 +1,21 @@
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { DeleteOutlined, DownloadOutlined, EditOutlined } from "@ant-design/icons";
import { Button, Card, Typography } from "antd";
import dayjs from "dayjs";
import { ShareCodeItem } from "./ShareCodeList";
import { useEffect } from "react";
import { ShareCodeResponse } from "../../quick-file/quickFileContext";
export default function ShareCodeListCard({ item, onEdit, onDelete }: { item: ShareCodeItem, onEdit: (id: string,expiresAt:Date,canUseTimes:number,code:string) => void, onDelete: (id: string) => void }) {
export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDownload }:
{
item: ShareCodeResponse,
styles?: string,
onDelete: (id: string) => void,
onEdit?: (id: string, expiresAt: Date, canUseTimes: number, code: string) => void,
onDownload?: (id: string) => void
}) {
useEffect(() => {
console.log('item:', item);
}, [item]);
return <div>
return <div className={`${styles}`}>
<Card
className="shadow-md hover:shadow-lg transition-shadow duration-300 space-x-4"
title={
@ -21,26 +28,36 @@ export default function ShareCodeListCard({ item, onEdit, onDelete }: { item: Sh
}
hoverable
actions={[
onEdit && (
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEdit?.(item.id, dayjs(item.expiresAt).toDate(), item.canUseTimes, item.code)}
className="text-blue-500 hover:text-blue-700"
/>,
/>
),
onDownload && (
<Button
type="text"
icon={<DownloadOutlined />}
onClick={() => onDownload?.(item.code)}
className="text-blue-500 hover:text-blue-700"
/>
),
<Button
type="text"
icon={<DeleteOutlined />}
onClick={() => onDelete?.(item.id)}
className="text-red-500 hover:text-red-700"
/>
]}
].filter(Boolean)}
>
<div className="space-y-2">
<p className="text-gray-700">
<span className="font-medium">:</span> {item.fileName}
</p>
<p className="text-gray-700">
<span className="font-medium">:</span> {Math.max(0.01, (item?.resource?.meta?.size / 1024 / 1024)).toFixed(2)} MB
<span className="font-medium">:</span> {Math.max(0.01, (Number(item?.resource?.meta?.size) / 1024 / 1024)).toFixed(2)} MB
</p>
<p className="text-gray-700 ">
<span className="font-medium">:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')}

View File

@ -5,47 +5,26 @@ import { message, Tabs, Form, Spin, Alert } from "antd";
import { env } from '../../../env'
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { useState } from "react";
import CodeRecord from "../sharecode/components/CodeRecord";
const { TabPane } = Tabs;
export default function QuickUploadPage() {
const [form] = Form.useForm();
const uploadFileId = Form.useWatch(["file"], form)?.[0]
const [isGetingFileId, setIsGetingFileId] = useState(false);
const handleShareSuccess = (code: string) => {
message.success('分享码生成成功:' + code);
}
const handleValidSuccess = async (fileUrl: string, fileName: string) => {
setIsGetingFileId(true);
try {
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('文件下载开始');
} catch (error) {
console.error('下载失败:', error);
message.error('文件下载失败');
} finally {
setIsGetingFileId(false);
}
};
return (
<>
{
isGetingFileId ?
(<Spin spinning={isGetingFileId} fullscreen />) :
(<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
<div className="flex items-center">
<img src="/logo.svg" className="h-16 w-16 rounded-xl" />
<span className="text-4xl py-4 mx-4"></span>
</div>
<Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
<div className="flex justify-between items-center mb-4">
<span className="text-lg text-zinc-700"></span>
<CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" />
</div>
<div>
<Form form={form}>
<Form.Item name="file">
@ -59,22 +38,20 @@ export default function QuickUploadPage() {
<div style={{ marginBottom: '40px' }}>
<ShareCodeGenerator
fileId={uploadFileId}
onSuccess={handleShareSuccess}
/>
</div>
</TabPane>
<TabPane tab="下载文件" key="download">
<div>
<div className="flex justify-between items-center mb-4">
<span className="text-lg block text-zinc-700 py-4">使</span>
<ShareCodeValidator
onValidSuccess={handleValidSuccess}
/>
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true}/>
</div>
<div>
<ShareCodeValidator />
</div>
</TabPane>
</Tabs>
</div>)
}
</>
</div>
)
}

View File

@ -0,0 +1,111 @@
import { message } from "antd";
import dayjs from "dayjs";
import { createContext, useContext, useState } from "react";
import { env } from "@web/src/env";
import { api } from "@nice/client";
interface QuickFileContextType {
saveCodeRecord: (data: ShareCodeResponse, recordName: string) => void;
handleValidSuccess: (fileUrl: string, fileName: string) => void;
isGetingFileId: boolean;
downloadCode: string | null;
setDownloadCode: (code: string | null) => void;
refetchShareCodeWithResource: () => Promise<{ data: any }>;
isLoading: boolean;
downloadResult: ShareCodeResponse | null;
}
export interface ShareCodeResponse {
id?: string;
code?: string;
fileName?: string;
expiresAt?: Date;
canUseTimes?: number;
resource?: {
id: string;
type: string;
url: string;
meta: ResourceMeta
}
}
interface ResourceMeta {
filename: string;
filetype: string;
size: string;
}
export const QuickFileContext = createContext<QuickFileContextType | null>(null);
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) => {
const newRecord = {
id: `${Date.now()}`, // 生成唯一ID
code: data.code,
expiresAt: dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'),
fileName: data.fileName || `文件_${data.code}`,
canUseTimes: data.canUseTimes,
resource: {
id: data.resource.id,
type: data.resource.type,
url: data.resource.url,
meta: {
size: data.resource.meta.size,
filename: data.resource.meta.filename,
filetype: data.resource.meta.filetype
}
}
};
// 获取已有记录并添加新记录
const existingGeneratorRecords = localStorage.getItem(recordName);
const generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : [];
generatorRecords.unshift(newRecord); // 添加到最前面
localStorage.setItem(recordName, JSON.stringify(generatorRecords));
}
const handleValidSuccess = async (fileUrl: string, fileName: string) => {
setIsGetingFileId(true);
try {
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('文件下载开始');
} catch (error) {
console.error('下载失败:', error);
message.error('文件下载失败');
} finally {
setIsGetingFileId(false);
}
};
const { data: downloadResult, isLoading, refetch: refetchShareCodeWithResource } = api.shareCode.getFileByShareCode.useQuery(
{ code: downloadCode?.trim() },
{
enabled: false
}
)
return <>
<QuickFileContext.Provider value={{
saveCodeRecord,
handleValidSuccess,
isGetingFileId,
downloadCode,
setDownloadCode,
refetchShareCodeWithResource,
isLoading,
downloadResult: downloadResult as any as ShareCodeResponse
}}>
{children}
</QuickFileContext.Provider>
</>
};
export const useQuickFileContext = () => {
const context = useContext(QuickFileContext);
if (!context) {
throw new Error("useQuickFileContext must be used within a QuickFileProvider");
}
return context;
};

View File

@ -0,0 +1,88 @@
import { useState, useEffect } from "react";
import { Button, Drawer, Empty } from "antd";
import { HistoryOutlined } from "@ant-design/icons";
import ShareCodeListCard from "../../code-manage/components/ShareCodeListCard";
import { ShareCodeResponse, useQuickFileContext } from "../../quick-file/quickFileContext";
export default function CodeRecord({ title, btnContent, recordName ,styles,isDownload}:
{ title: string, btnContent: string , recordName: string, styles?:string,isDownload?:boolean}) {
const [open, setOpen] = useState(false);
const [records, setRecords] = useState<ShareCodeResponse[]>([]);
const {handleValidSuccess,saveCodeRecord,refetchShareCodeWithResource,setDownloadCode} = useQuickFileContext();
const loadRecordsFromStorage = () => {
const storedRecords = localStorage.getItem(recordName);
if (storedRecords) {
setRecords(JSON.parse(storedRecords));
}
};
useEffect(() => {
loadRecordsFromStorage();
}, [recordName]);
const saveAndUpdateRecord = (data: ShareCodeResponse, name: string) => {
saveCodeRecord(data, name);
if (name === recordName) {
loadRecordsFromStorage();
}
};
const handleOpen = () => {
loadRecordsFromStorage();
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleDelete = (id: string) => {
const updatedRecords = records.filter(record => record.id !== id);
setRecords(updatedRecords);
localStorage.setItem(recordName, JSON.stringify(updatedRecords));
};
const handleDownload = async (code:string) => {
await setDownloadCode(code)
const {data:result} = await refetchShareCodeWithResource()
console.log('下载', result);
handleValidSuccess(result.resource.url, result.fileName);
saveAndUpdateRecord(result as any as ShareCodeResponse, 'shareCodeDownloadRecords');
};
return (
<>
<Button
type="primary"
icon={<HistoryOutlined />}
onClick={handleOpen}
style={{ marginBottom: '16px' }}
>
{btnContent}
</Button>
<Drawer
title={title}
placement="right"
width={420}
onClose={handleClose}
open={open}
>
{records.length > 0 ? (
<div className="space-y-4">
{records.map(item => (
<ShareCodeListCard
key={item.code}
item={item}
onDelete={handleDelete}
onDownload={isDownload ? handleDownload : undefined}
/>
))}
</div>
) : (
<Empty description="暂无分享码记录" />
)}
</Drawer>
</>
);
}

View File

@ -7,6 +7,7 @@ import { api } from '@nice/client';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
dayjs.extend(utc);
dayjs.extend(timezone);
interface ShareCodeGeneratorProps {
@ -14,11 +15,7 @@ interface ShareCodeGeneratorProps {
onSuccess?: (code: string) => void;
fileName?: string;
}
interface ShareCodeResponse {
code?: string;
expiresAt?: Date;
canUseTimes?: number;
}
export function copyToClipboard(text) {
if (navigator.clipboard) {
@ -35,7 +32,6 @@ export function copyToClipboard(text) {
}
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
fileId,
onSuccess,
fileName,
}) => {
const [loading, setLoading] = useState(false);
@ -47,6 +43,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
const [isGenerate, setIsGenerate] = useState(false);
const [currentFileId, setCurrentFileId] = useState<string>('');
const [form] = Form.useForm();
const { saveCodeRecord } = useQuickFileContext();
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey });
@ -74,10 +71,10 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
console.log('data', data)
setShareCode(data.code);
setIsGenerate(true);
onSuccess?.(data.code);
setExpiresAt(dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'));
setCanUseTimes(data.canUseTimes);
//message.success('分享码生成成功');
saveCodeRecord(data,'shareCodeGeneratorRecords');
message.success('分享码生成成功'+data.code);
} catch (error) {
console.error('生成分享码错误:', error);
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));

View File

@ -1,47 +1,39 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Input, Button, message } from 'antd';
import { Input, Button, message, Spin } from 'antd';
import styles from './ShareCodeValidator.module.css';
import { api } from '@nice/client';
import dayjs from 'dayjs';
interface ShareCodeValidatorProps {
onValidSuccess: (fileId: string, fileName: string) => void;
}
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { env } from '@web/src/env';
import { copyToClipboard } from './sharecodegenerator';
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
dayjs.extend(utc);
dayjs.extend(timezone);
export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
onValidSuccess,
}) => {
const [code, setCode] = useState('');
const { data: result, isLoading, refetch } = api.shareCode.getFileByShareCode.useQuery(
{ code: code.trim() },
{
enabled: false
}
)
export const ShareCodeValidator: React.FC = ({ }) => {
const { saveCodeRecord,handleValidSuccess,isGetingFileId,downloadCode,setDownloadCode,refetchShareCodeWithResource,isLoading,downloadResult } = useQuickFileContext();
const validateCode = useCallback(async () => {
if (!code.trim()) {
if (!downloadCode?.trim()) {
message.warning('请输入正确的分享码');
return;
}
try {
const { data: latestResult } = await refetch();
const { data: latestResult } = await refetchShareCodeWithResource();
console.log('验证分享码返回数据:', latestResult);
onValidSuccess(latestResult.url, latestResult.fileName);
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
handleValidSuccess(latestResult.resource.url, latestResult.fileName);
} catch (error) {
console.error('验证分享码失败:', error);
message.error('分享码无效或已过期');
}
}, [refetch, code, onValidSuccess]);
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
const getDownloadUrl = useCallback(async () => {
try {
const { data: latestResult } = await refetch();
const { data: latestResult } = await refetchShareCodeWithResource();
console.log('验证分享码返回数据:', latestResult);
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${latestResult.url}`;
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${latestResult.resource.url}`;
copyToClipboard(downloadUrl)
.then(() => {
message.success(`${latestResult.fileName}的下载链接已复制`, 6)
@ -53,14 +45,19 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
console.error('验证分享码失败:', error);
message.error('分享码无效或已过期');
}
}, [refetch, code, onValidSuccess]);
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
return (
<>
{
isGetingFileId ?
(<Spin spinning={isGetingFileId} fullscreen />) :
(
<>
<div className={styles.container}>
<Input
className={styles.input}
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())}
value={downloadCode}
onChange={(e) => setDownloadCode(e.target.value.toUpperCase())}
placeholder="请输入分享码"
maxLength={8}
onPressEnter={validateCode}
@ -69,7 +66,7 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
type="primary"
onClick={getDownloadUrl}
loading={isLoading}
disabled={!code.trim()}
disabled={!downloadCode?.trim()}
>
</Button>
@ -77,21 +74,24 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
type="primary"
onClick={validateCode}
loading={isLoading}
disabled={!code.trim()}
disabled={!downloadCode?.trim()}
>
</Button>
</div>
{
!isLoading && result && (
!isLoading && downloadResult && (
<div className='w-full flex justify-between my-2 p-1 antialiased text-secondary-600'>
<span >{`分享码:${result?.code ? result.code : ''}`}</span>
<span >{`文件名:${result?.fileName ? result.fileName : ''}`}</span>
<span >{`过期时间:${result?.expiresAt ? dayjs(result.expiresAt).format('YYYY-MM-DD HH:mm:ss') : ''}`}</span>
<span >{`剩余使用次数:${result?.canUseTimes ? result.canUseTimes : ''}`}</span>
<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>
</div>
)
}
</>
)
}
</>
);
};

View File

@ -11,6 +11,7 @@ import WithAuth from "../components/utils/with-auth";
import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
import QuickUploadPage from "../app/admin/quick-file/page";
import { QuickFileProvider } from "../app/admin/quick-file/quickFileContext";
interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
@ -34,7 +35,11 @@ export type CustomRouteObject =
export const routes: CustomRouteObject[] = [
{
path: "/",
element: <QuickUploadPage></QuickUploadPage>,
element: <>
<QuickFileProvider>
<QuickUploadPage></QuickUploadPage>
</QuickFileProvider>
</>,
errorElement: <ErrorPage />,
},
{