rht
This commit is contained in:
parent
9edce44b32
commit
34dde48aaf
|
@ -9,6 +9,7 @@ import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
|
import { url } from 'inspector';
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
export interface ShareCode {
|
export interface ShareCode {
|
||||||
|
@ -25,12 +26,19 @@ export interface GenerateShareCodeResponse {
|
||||||
code: string;
|
code: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
canUseTimes: number;
|
canUseTimes: number;
|
||||||
|
fileName?: string;
|
||||||
|
resource: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
meta: ResourceMeta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceMeta {
|
interface ResourceMeta {
|
||||||
filename: string;
|
filename: string;
|
||||||
filetype: string;
|
filetype: string;
|
||||||
filesize: string;
|
size: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -61,7 +69,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
throw new NotFoundException('文件不存在');
|
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();
|
const code = this.generateCode();
|
||||||
// 查找是否已有分享码记录
|
// 查找是否已有分享码记录
|
||||||
|
@ -78,7 +86,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
||||||
expiresAt,
|
expiresAt,
|
||||||
canUseTimes,
|
canUseTimes,
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
fileName: filename|| "downloaded_file",
|
fileName: filename || "downloaded_file",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,6 +107,17 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
||||||
code,
|
code,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
canUseTimes,
|
canUseTimes,
|
||||||
|
fileName: filename || "downloaded_file",
|
||||||
|
resource: {
|
||||||
|
id: resource.id,
|
||||||
|
type: resource.type,
|
||||||
|
url: resource.url,
|
||||||
|
meta: {
|
||||||
|
filename,
|
||||||
|
filetype,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to generate share code', 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 },
|
where: { fileId: shareCode.fileId },
|
||||||
});
|
});
|
||||||
this.logger.log('获取到的资源信息:', resource);
|
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
|
const fileUrl = resource?.url
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
throw new NotFoundException('文件不存在');
|
throw new NotFoundException('文件不存在');
|
||||||
|
@ -282,12 +301,19 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
||||||
|
|
||||||
// 直接返回正确的数据结构
|
// 直接返回正确的数据结构
|
||||||
const response = {
|
const response = {
|
||||||
fileId: shareCode.fileId,
|
id:shareCode.id,
|
||||||
fileName: filename || 'downloaded_file',
|
|
||||||
code: shareCode.code,
|
code: shareCode.code,
|
||||||
|
fileName: filename || 'downloaded_file',
|
||||||
expiresAt: shareCode.expiresAt,
|
expiresAt: shareCode.expiresAt,
|
||||||
url: fileUrl,
|
|
||||||
canUseTimes: shareCode.canUseTimes - 1,
|
canUseTimes: shareCode.canUseTimes - 1,
|
||||||
|
resource:{
|
||||||
|
id:resource.id,
|
||||||
|
type:resource.type,
|
||||||
|
url:resource.url,
|
||||||
|
meta:{
|
||||||
|
filename,filetype,size
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log('返回给前端的数据:', response); // 添加日志
|
this.logger.log('返回给前端的数据:', response); // 添加日志
|
||||||
|
@ -316,11 +342,10 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
console.log('args:', args.where.OR);
|
|
||||||
// 使用include直接关联查询Resource
|
// 使用include直接关联查询Resource
|
||||||
const { items, totalPages } = await super.findManyWithPagination({
|
const { items, totalPages } = await super.findManyWithPagination({
|
||||||
...args,
|
...args,
|
||||||
select:{
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
code: true,
|
code: true,
|
||||||
fileId: true,
|
fileId: true,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { api } from "@nice/client";
|
||||||
import { createContext, useContext, useState } from "react";
|
import { createContext, useContext, useState } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { getQueryKey } from "@trpc/react-query";
|
import { getQueryKey } from "@trpc/react-query";
|
||||||
|
import { ShareCodeResponse } from "../quick-file/quickFileContext";
|
||||||
|
|
||||||
interface CodeManageContextType {
|
interface CodeManageContextType {
|
||||||
editForm: FormInstance<any>;
|
editForm: FormInstance<any>;
|
||||||
|
@ -22,21 +23,7 @@ interface CodeManageContextType {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareCodeWithResource {
|
interface ShareCodeWithResource {
|
||||||
items: {
|
items: ShareCodeResponse[],
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
fileName: string;
|
|
||||||
fileSize: number;
|
|
||||||
expiresAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
canUseTimes: number;
|
|
||||||
resource: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
url: string;
|
|
||||||
meta: any;
|
|
||||||
}
|
|
||||||
}[],
|
|
||||||
totalPages: number
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,29 +4,14 @@ import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import ShareCodeListCard from './ShareCodeListCard';
|
import ShareCodeListCard from './ShareCodeListCard';
|
||||||
|
import { ShareCodeResponse } from '../../quick-file/quickFileContext';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
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 {
|
interface ShareCodeListProps {
|
||||||
data: ShareCodeItem[];
|
data: ShareCodeResponse[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onEdit?: (id: string,expiresAt:Date,canUseTimes:number,code:string) => void;
|
onEdit?: (id: string,expiresAt:Date,canUseTimes:number,code:string) => void;
|
||||||
onDelete?: (id: string) => void;
|
onDelete?: (id: string) => void;
|
||||||
|
@ -46,7 +31,7 @@ const ShareCodeList: React.FC<ShareCodeListProps> = ({
|
||||||
loading={loading}
|
loading={loading}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<ShareCodeListCard item={item} onEdit={onEdit} onDelete={onDelete} />
|
<ShareCodeListCard item={item} onEdit={onEdit} onDelete={onDelete} styles='w-[262px]' />
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 { Button, Card, Typography } from "antd";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ShareCodeItem } from "./ShareCodeList";
|
|
||||||
import { useEffect } from "react";
|
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(() => {
|
useEffect(() => {
|
||||||
console.log('item:', item);
|
console.log('item:', item);
|
||||||
}, [item]);
|
}, [item]);
|
||||||
return <div>
|
return <div className={`${styles}`}>
|
||||||
<Card
|
<Card
|
||||||
className="shadow-md hover:shadow-lg transition-shadow duration-300 space-x-4"
|
className="shadow-md hover:shadow-lg transition-shadow duration-300 space-x-4"
|
||||||
title={
|
title={
|
||||||
|
@ -21,28 +28,38 @@ export default function ShareCodeListCard({ item, onEdit, onDelete }: { item: Sh
|
||||||
}
|
}
|
||||||
hoverable
|
hoverable
|
||||||
actions={[
|
actions={[
|
||||||
|
onEdit && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => onEdit?.(item.id,dayjs(item.expiresAt).toDate(),item.canUseTimes,item.code)}
|
onClick={() => onEdit?.(item.id, dayjs(item.expiresAt).toDate(), item.canUseTimes, item.code)}
|
||||||
className="text-blue-500 hover:text-blue-700"
|
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
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => onDelete?.(item.id)}
|
onClick={() => onDelete?.(item.id)}
|
||||||
className="text-red-500 hover:text-red-700"
|
className="text-red-500 hover:text-red-700"
|
||||||
/>
|
/>
|
||||||
]}
|
].filter(Boolean)}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
<span className="font-medium">文件名:</span> {item.fileName}
|
<span className="font-medium">文件名:</span> {item.fileName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700">
|
<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>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700 ">
|
||||||
<span className="font-medium">过期时间:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')}
|
<span className="font-medium">过期时间:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
|
|
|
@ -5,47 +5,26 @@ import { message, Tabs, Form, Spin, Alert } from "antd";
|
||||||
import { env } from '../../../env'
|
import { env } from '../../../env'
|
||||||
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import CodeRecord from "../sharecode/components/CodeRecord";
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
export default function QuickUploadPage() {
|
export default function QuickUploadPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const uploadFileId = Form.useWatch(["file"], form)?.[0]
|
const uploadFileId = Form.useWatch(["file"], form)?.[0]
|
||||||
const [isGetingFileId, setIsGetingFileId] = useState(false);
|
|
||||||
const handleShareSuccess = (code: string) => {
|
const handleShareSuccess = (code: string) => {
|
||||||
message.success('分享码生成成功:' + code);
|
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 (
|
return (
|
||||||
<>
|
<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
|
||||||
{
|
|
||||||
isGetingFileId ?
|
|
||||||
(<Spin spinning={isGetingFileId} fullscreen />) :
|
|
||||||
(<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img src="/logo.svg" className="h-16 w-16 rounded-xl" />
|
<img src="/logo.svg" className="h-16 w-16 rounded-xl" />
|
||||||
<span className="text-4xl py-4 mx-4">烽火快传</span>
|
<span className="text-4xl py-4 mx-4">烽火快传</span>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultActiveKey="upload">
|
<Tabs defaultActiveKey="upload">
|
||||||
<TabPane tab="上传分享" key="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>
|
<div>
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
<Form.Item name="file">
|
<Form.Item name="file">
|
||||||
|
@ -59,22 +38,20 @@ export default function QuickUploadPage() {
|
||||||
<div style={{ marginBottom: '40px' }}>
|
<div style={{ marginBottom: '40px' }}>
|
||||||
<ShareCodeGenerator
|
<ShareCodeGenerator
|
||||||
fileId={uploadFileId}
|
fileId={uploadFileId}
|
||||||
onSuccess={handleShareSuccess}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tab="下载文件" key="download">
|
<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>
|
<span className="text-lg block text-zinc-700 py-4">使用分享码下载文件</span>
|
||||||
<ShareCodeValidator
|
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true}/>
|
||||||
onValidSuccess={handleValidSuccess}
|
</div>
|
||||||
/>
|
<div>
|
||||||
|
<ShareCodeValidator />
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>)
|
</div>
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { api } from '@nice/client';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
interface ShareCodeGeneratorProps {
|
interface ShareCodeGeneratorProps {
|
||||||
|
@ -14,11 +15,7 @@ interface ShareCodeGeneratorProps {
|
||||||
onSuccess?: (code: string) => void;
|
onSuccess?: (code: string) => void;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}
|
}
|
||||||
interface ShareCodeResponse {
|
|
||||||
code?: string;
|
|
||||||
expiresAt?: Date;
|
|
||||||
canUseTimes?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyToClipboard(text) {
|
export function copyToClipboard(text) {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
|
@ -35,7 +32,6 @@ export function copyToClipboard(text) {
|
||||||
}
|
}
|
||||||
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
fileId,
|
fileId,
|
||||||
onSuccess,
|
|
||||||
fileName,
|
fileName,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
@ -47,6 +43,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
const [isGenerate, setIsGenerate] = useState(false);
|
const [isGenerate, setIsGenerate] = useState(false);
|
||||||
const [currentFileId, setCurrentFileId] = useState<string>('');
|
const [currentFileId, setCurrentFileId] = useState<string>('');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { saveCodeRecord } = useQuickFileContext();
|
||||||
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
|
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
@ -74,10 +71,10 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
console.log('data', data)
|
console.log('data', data)
|
||||||
setShareCode(data.code);
|
setShareCode(data.code);
|
||||||
setIsGenerate(true);
|
setIsGenerate(true);
|
||||||
onSuccess?.(data.code);
|
|
||||||
setExpiresAt(dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'));
|
setExpiresAt(dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'));
|
||||||
setCanUseTimes(data.canUseTimes);
|
setCanUseTimes(data.canUseTimes);
|
||||||
//message.success('分享码生成成功');
|
saveCodeRecord(data,'shareCodeGeneratorRecords');
|
||||||
|
message.success('分享码生成成功'+data.code);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成分享码错误:', error);
|
console.error('生成分享码错误:', error);
|
||||||
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
|
|
@ -1,50 +1,42 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
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 styles from './ShareCodeValidator.module.css';
|
||||||
import { api } from '@nice/client';
|
import { api } from '@nice/client';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
interface ShareCodeValidatorProps {
|
|
||||||
onValidSuccess: (fileId: string, fileName: string) => void;
|
|
||||||
}
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import { env } from '@web/src/env';
|
import { env } from '@web/src/env';
|
||||||
import { copyToClipboard } from './sharecodegenerator';
|
import { copyToClipboard } from './sharecodegenerator';
|
||||||
|
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
export const ShareCodeValidator: React.FC = ({ }) => {
|
||||||
onValidSuccess,
|
const { saveCodeRecord,handleValidSuccess,isGetingFileId,downloadCode,setDownloadCode,refetchShareCodeWithResource,isLoading,downloadResult } = useQuickFileContext();
|
||||||
}) => {
|
|
||||||
const [code, setCode] = useState('');
|
|
||||||
const { data: result, isLoading, refetch } = api.shareCode.getFileByShareCode.useQuery(
|
|
||||||
{ code: code.trim() },
|
|
||||||
{
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const validateCode = useCallback(async () => {
|
const validateCode = useCallback(async () => {
|
||||||
|
|
||||||
if (!code.trim()) {
|
if (!downloadCode?.trim()) {
|
||||||
message.warning('请输入正确的分享码');
|
message.warning('请输入正确的分享码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data: latestResult } = await refetch();
|
const { data: latestResult } = await refetchShareCodeWithResource();
|
||||||
console.log('验证分享码返回数据:', latestResult);
|
console.log('验证分享码返回数据:', latestResult);
|
||||||
onValidSuccess(latestResult.url, latestResult.fileName);
|
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
|
||||||
|
handleValidSuccess(latestResult.resource.url, latestResult.fileName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('验证分享码失败:', error);
|
console.error('验证分享码失败:', error);
|
||||||
message.error('分享码无效或已过期');
|
message.error('分享码无效或已过期');
|
||||||
}
|
}
|
||||||
}, [refetch, code, onValidSuccess]);
|
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
|
||||||
const getDownloadUrl = useCallback(async () => {
|
const getDownloadUrl = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { data: latestResult } = await refetch();
|
const { data: latestResult } = await refetchShareCodeWithResource();
|
||||||
console.log('验证分享码返回数据:', latestResult);
|
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)
|
copyToClipboard(downloadUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
message.success(`${latestResult.fileName}的下载链接已复制`,6)
|
message.success(`${latestResult.fileName}的下载链接已复制`, 6)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
message.error('复制失败')
|
message.error('复制失败')
|
||||||
|
@ -53,14 +45,19 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
console.error('验证分享码失败:', error);
|
console.error('验证分享码失败:', error);
|
||||||
message.error('分享码无效或已过期');
|
message.error('分享码无效或已过期');
|
||||||
}
|
}
|
||||||
}, [refetch, code, onValidSuccess]);
|
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
isGetingFileId ?
|
||||||
|
(<Spin spinning={isGetingFileId} fullscreen />) :
|
||||||
|
(
|
||||||
<>
|
<>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Input
|
<Input
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
value={code}
|
value={downloadCode}
|
||||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
onChange={(e) => setDownloadCode(e.target.value.toUpperCase())}
|
||||||
placeholder="请输入分享码"
|
placeholder="请输入分享码"
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
onPressEnter={validateCode}
|
onPressEnter={validateCode}
|
||||||
|
@ -69,7 +66,7 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={getDownloadUrl}
|
onClick={getDownloadUrl}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={!code.trim()}
|
disabled={!downloadCode?.trim()}
|
||||||
>
|
>
|
||||||
获取下载链接
|
获取下载链接
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -77,21 +74,24 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={validateCode}
|
onClick={validateCode}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={!code.trim()}
|
disabled={!downloadCode?.trim()}
|
||||||
>
|
>
|
||||||
下载
|
下载
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
!isLoading && result && (
|
!isLoading && downloadResult && (
|
||||||
<div className='w-full flex justify-between my-2 p-1 antialiased text-secondary-600'>
|
<div className='w-full flex justify-between my-2 p-1 antialiased text-secondary-600'>
|
||||||
<span >{`分享码:${result?.code ? result.code : ''}`}</span>
|
<span >{`分享码:${downloadResult?.code ? downloadResult.code : ''}`}</span>
|
||||||
<span >{`文件名:${result?.fileName ? result.fileName : ''}`}</span>
|
<span >{`文件名:${downloadResult?.fileName ? downloadResult.fileName : ''}`}</span>
|
||||||
<span >{`过期时间:${result?.expiresAt ? dayjs(result.expiresAt).format('YYYY-MM-DD HH:mm:ss') : ''}`}</span>
|
<span >{`过期时间:${downloadResult?.expiresAt ? dayjs(downloadResult.expiresAt).format('YYYY-MM-DD HH:mm:ss') : ''}`}</span>
|
||||||
<span >{`剩余使用次数:${result?.canUseTimes ? result.canUseTimes : ''}`}</span>
|
<span >{`剩余使用次数:${downloadResult?.canUseTimes ? downloadResult.canUseTimes : ''}`}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -11,6 +11,7 @@ import WithAuth from "../components/utils/with-auth";
|
||||||
import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
|
import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
|
||||||
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
|
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
|
||||||
import QuickUploadPage from "../app/admin/quick-file/page";
|
import QuickUploadPage from "../app/admin/quick-file/page";
|
||||||
|
import { QuickFileProvider } from "../app/admin/quick-file/quickFileContext";
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
breadcrumb?: string;
|
breadcrumb?: string;
|
||||||
|
@ -34,7 +35,11 @@ export type CustomRouteObject =
|
||||||
export const routes: CustomRouteObject[] = [
|
export const routes: CustomRouteObject[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <QuickUploadPage></QuickUploadPage>,
|
element: <>
|
||||||
|
<QuickFileProvider>
|
||||||
|
<QuickUploadPage></QuickUploadPage>
|
||||||
|
</QuickFileProvider>
|
||||||
|
</>,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue