rht
This commit is contained in:
parent
0dad0e18ec
commit
ca079e0096
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"marscode.chatLanguage": "cn"
|
||||
"marscode.chatLanguage": "cn",
|
||||
"marscode.codeCompletionPro": {
|
||||
"enableCodeCompletionPro": true
|
||||
}
|
||||
}
|
|
@ -11,9 +11,9 @@ export class ShareCodeRouter {
|
|||
|
||||
router = this.trpc.router({
|
||||
generateShareCode: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string() }))
|
||||
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.shareCodeService.generateShareCode(input.fileId);
|
||||
return this.shareCodeService.generateShareCode(input.fileId, input.expiresAt, input.canUseTimes);
|
||||
}),
|
||||
validateAndUseCode: this.trpc.procedure
|
||||
.input(z.object({ code: z.string() }))
|
||||
|
@ -41,9 +41,9 @@ export class ShareCodeRouter {
|
|||
return this.shareCodeService.getFileByShareCode(input.code);
|
||||
}),
|
||||
generateShareCodeByFileId: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string() }))
|
||||
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.shareCodeService.generateShareCodeByFileId(input.fileId);
|
||||
return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -5,8 +5,11 @@ import { Cron, CronExpression } from '@nestjs/schedule';
|
|||
import { ResourceService } from '@server/models/resource/resource.service';
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
export interface ShareCode {
|
||||
id: string;
|
||||
code: string;
|
||||
|
@ -15,10 +18,12 @@ export interface ShareCode {
|
|||
expiresAt: Date;
|
||||
isUsed: boolean;
|
||||
fileName?: string | null;
|
||||
canUseTimes: number | null;
|
||||
}
|
||||
export interface GenerateShareCodeResponse {
|
||||
code: string;
|
||||
expiresAt: Date;
|
||||
canUseTimes: number;
|
||||
}
|
||||
|
||||
interface ResourceMeta {
|
||||
|
@ -40,6 +45,8 @@ export class ShareCodeService {
|
|||
|
||||
async generateShareCode(
|
||||
fileId: string,
|
||||
expiresAt: Date,
|
||||
canUseTimes: number,
|
||||
fileName?: string,
|
||||
): Promise<GenerateShareCodeResponse> {
|
||||
try {
|
||||
|
@ -55,8 +62,6 @@ export class ShareCodeService {
|
|||
|
||||
// 生成分享码
|
||||
const code = this.generateCode();
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
|
||||
//const expiresAt = new Date(Date.now() + 10 * 1000); // 24小时后过期
|
||||
// 查找是否已有分享码记录
|
||||
const existingShareCode = await db.shareCode.findUnique({
|
||||
where: { fileId },
|
||||
|
@ -69,6 +74,7 @@ export class ShareCodeService {
|
|||
data: {
|
||||
code,
|
||||
expiresAt,
|
||||
canUseTimes,
|
||||
isUsed: false,
|
||||
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
||||
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
|
||||
|
@ -81,15 +87,17 @@ export class ShareCodeService {
|
|||
code,
|
||||
fileId,
|
||||
expiresAt,
|
||||
canUseTimes,
|
||||
isUsed: false,
|
||||
fileName: fileName || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.logger.log(`Generated share code ${code} for file ${fileId}`);
|
||||
this.logger.log(`Generated share code ${code} for file ${fileId} canUseTimes: ${canUseTimes}`);
|
||||
return {
|
||||
code,
|
||||
expiresAt,
|
||||
canUseTimes,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate share code', error);
|
||||
|
@ -106,26 +114,26 @@ export class ShareCodeService {
|
|||
where: {
|
||||
code,
|
||||
isUsed: false,
|
||||
expiresAt: { gt: new Date() },
|
||||
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
||||
},
|
||||
});
|
||||
|
||||
if (shareCode.canUseTimes <= 0) {
|
||||
this.logger.log('分享码已使用次数超过限制');
|
||||
return null;
|
||||
}
|
||||
//更新已使用次数
|
||||
await db.shareCode.update({
|
||||
where: { id: shareCode.id },
|
||||
data: { canUseTimes: shareCode.canUseTimes - 1 },
|
||||
});
|
||||
this.logger.log('查询结果:', shareCode);
|
||||
|
||||
if (!shareCode) {
|
||||
this.logger.log('分享码无效或已过期');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 标记分享码为已使用
|
||||
// await db.shareCode.update({
|
||||
// where: { id: shareCode.id },
|
||||
// data: { isUsed: true },
|
||||
// });
|
||||
|
||||
// 记录使用日志
|
||||
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
|
||||
|
||||
// 返回完整的分享码信息,包括文件名
|
||||
return shareCode;
|
||||
} catch (error) {
|
||||
|
@ -141,7 +149,11 @@ export class ShareCodeService {
|
|||
try {
|
||||
const shareCodes = await db.shareCode.findMany({
|
||||
where: {
|
||||
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
|
||||
OR: [
|
||||
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
|
||||
{ isUsed: true },
|
||||
{ canUseTimes: { lte: 0 } }
|
||||
],
|
||||
}
|
||||
})
|
||||
this.logger.log('需要清理的分享码:', shareCodes);
|
||||
|
@ -222,7 +234,7 @@ export class ShareCodeService {
|
|||
where: {
|
||||
fileId,
|
||||
isUsed: false,
|
||||
expiresAt: { gt: new Date() },
|
||||
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -249,7 +261,6 @@ export class ShareCodeService {
|
|||
// 根据分享码获取文件
|
||||
async getFileByShareCode(code: string) {
|
||||
this.logger.log('收到验证分享码请求,code:', code);
|
||||
|
||||
const shareCode = await this.validateAndUseCode(code);
|
||||
this.logger.log('验证分享码结果:', shareCode);
|
||||
|
||||
|
@ -275,6 +286,7 @@ export class ShareCodeService {
|
|||
code: shareCode.code,
|
||||
expiresAt: shareCode.expiresAt,
|
||||
url: fileUrl,
|
||||
canUseTimes: shareCode.canUseTimes - 1,
|
||||
};
|
||||
|
||||
this.logger.log('返回给前端的数据:', response); // 添加日志
|
||||
|
@ -282,10 +294,10 @@ export class ShareCodeService {
|
|||
}
|
||||
|
||||
// 根据文件ID生成分享码
|
||||
async generateShareCodeByFileId(fileId: string) {
|
||||
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number) {
|
||||
try {
|
||||
this.logger.log('收到生成分享码请求,fileId:', fileId);
|
||||
const result = await this.generateShareCode(fileId);
|
||||
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes);
|
||||
this.logger.log('生成分享码结果:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
"quill": "2.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-player": "^2.16.0",
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 35 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 35 KiB |
|
@ -1,7 +1,7 @@
|
|||
|
||||
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
||||
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
||||
import { message, Tabs, Form, Spin } from "antd";
|
||||
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";
|
||||
|
@ -10,30 +10,20 @@ export default function DeptSettingPage() {
|
|||
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 {
|
||||
// 构建下载URL(包含文件名参数)
|
||||
console.log('文件url:', fileUrl);
|
||||
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
|
||||
console.log('下载URL:', downloadUrl);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
|
||||
// 直接使用传入的 fileName
|
||||
link.download = fileName;
|
||||
link.target = '_blank'; // 在新标签页中打开
|
||||
// 触发下载
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
message.success('文件下载开始');
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
|
@ -49,35 +39,30 @@ export default function DeptSettingPage() {
|
|||
{
|
||||
isGetingFileId ?
|
||||
(<Spin spinning={isGetingFileId} fullscreen />) :
|
||||
(<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
||||
<span className="text-2xl py-4">文件分享中心</span>
|
||||
(<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
|
||||
<div className="flex items-center">
|
||||
<img src="/logo.svg" className="h-20 w-20 rounded-xl" />
|
||||
<span className="text-4xl py-4 mx-4">烽火快传</span>
|
||||
</div>
|
||||
<Tabs defaultActiveKey="upload">
|
||||
<TabPane tab="上传分享" key="upload">
|
||||
{/* 文件上传区域 */}
|
||||
<div style={{ marginBottom: '40px' }}>
|
||||
<span className="text-lg block text-zinc-700 py-2">第一步:上传文件</span>
|
||||
{/* 如果没有已上传文件,显示上传区域 */}
|
||||
<div>
|
||||
<Form form={form}>
|
||||
<Form.Item name="file">
|
||||
<TusUploader
|
||||
multiple={false}
|
||||
style={"w-full py-4"}
|
||||
style={"w-full py-4 mb-0 h-64"}
|
||||
></TusUploader>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* 生成分享码区域 */}
|
||||
<div style={{ marginBottom: '40px' }}>
|
||||
<span className="text-lg block text-zinc-700 py-4">第二步:生成分享码</span>
|
||||
<ShareCodeGenerator
|
||||
fileId={uploadFileId}
|
||||
onSuccess={handleShareSuccess}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
{/* 使用分享码区域 */}
|
||||
<TabPane tab="下载文件" key="download">
|
||||
<div>
|
||||
<span className="text-lg block text-zinc-700 py-4">使用分享码下载文件</span>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, message } from 'antd';
|
||||
import { Button, DatePicker, Form, message, Select } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { env } from '../../../env'
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getQueryKey } from '@trpc/react-query';
|
||||
import { api } from '@nice/client';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
interface ShareCodeGeneratorProps {
|
||||
fileId: string;
|
||||
onSuccess?: (code: string) => void;
|
||||
|
@ -14,6 +17,7 @@ interface ShareCodeGeneratorProps {
|
|||
interface ShareCodeResponse {
|
||||
code?: string;
|
||||
expiresAt?: Date;
|
||||
canUseTimes?: number;
|
||||
}
|
||||
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||
fileId,
|
||||
|
@ -23,10 +27,12 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [shareCode, setShareCode] = useState<string>('');
|
||||
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
|
||||
const [canUseTimes, setCanUseTimes] = useState<number>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = getQueryKey(api.term);
|
||||
const [isGenerate, setIsGenerate] = useState(false);
|
||||
const [currentFileId, setCurrentFileId] = useState<string>('');
|
||||
const [form] = Form.useForm();
|
||||
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
|
@ -46,12 +52,17 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|||
setLoading(true);
|
||||
console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName);
|
||||
try {
|
||||
const data: ShareCodeResponse = await generateShareCode.mutateAsync({ fileId });
|
||||
console.log('生成分享码结果:', data);
|
||||
const data: ShareCodeResponse = await generateShareCode.mutateAsync({
|
||||
fileId,
|
||||
expiresAt: form.getFieldsValue()?.expiresAt ? form.getFieldsValue().expiresAt.toDate() : dayjs().add(1, 'day').tz('Asia/Shanghai').toDate(),
|
||||
canUseTimes: form.getFieldsValue()?.canUseTimes ? form.getFieldsValue().canUseTimes : 10,
|
||||
});
|
||||
console.log('data', data)
|
||||
setShareCode(data.code);
|
||||
setIsGenerate(true);
|
||||
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
|
||||
onSuccess?.(data.code);
|
||||
setExpiresAt(dayjs(data.expiresAt).tz('Asia/Shanghai').toDate());
|
||||
setCanUseTimes(data.canUseTimes);
|
||||
//message.success('分享码生成成功');
|
||||
} catch (error) {
|
||||
console.error('生成分享码错误:', error);
|
||||
|
@ -73,30 +84,61 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// 组件使用
|
||||
const handleCopy = (code) => {
|
||||
copyToClipboard(code)
|
||||
.then(() => console.log('复制成功'))
|
||||
.catch(() => console.error('复制失败'));
|
||||
};
|
||||
useEffect(() => {
|
||||
const date = dayjs().add(1, 'day').tz('Asia/Shanghai');
|
||||
form.setFieldsValue({
|
||||
expiresAt: date,
|
||||
canUseTimes: 10
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
useEffect(()=>{
|
||||
if (fileId) {
|
||||
generateCode()
|
||||
}
|
||||
}, [fileId])
|
||||
return (
|
||||
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{/* 添加调试信息 */}
|
||||
<small style={{ color: '#666' }}>文件ID: {fileId ? fileId : '未选择文件'}</small>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<small style={{ color: '#666' }}>文件ID: {fileId ? fileId : '暂未上传文件'}</small>
|
||||
</div>
|
||||
|
||||
{!isGenerate ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={generateCode}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
生成分享码
|
||||
</Button>
|
||||
<>
|
||||
<Form form={form}>
|
||||
<div className='w-4/5 h-16 flex flex-row justify-between items-center'>
|
||||
<small style={{ color: '#666' }}>
|
||||
{"分享码的有效期"}
|
||||
</small>
|
||||
<Form.Item name="expiresAt" className='mt-5'>
|
||||
<DatePicker
|
||||
showTime
|
||||
/>
|
||||
</Form.Item>
|
||||
<small style={{ color: '#666' }}>
|
||||
{"分享码的使用次数"}
|
||||
</small>
|
||||
<Form.Item name="canUseTimes" className='mt-5'>
|
||||
<Select
|
||||
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' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
|
@ -126,9 +168,9 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{expiresAt ? (
|
||||
{isGenerate && expiresAt ? (
|
||||
<div style={{ color: '#666' }}>
|
||||
有效期至: {expiresAt.toLocaleString()}
|
||||
有效期至: {expiresAt.toLocaleString()} 可使用次数: {canUseTimes}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: 'red' }}>
|
||||
|
|
|
@ -1,63 +1,75 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Input, Button, message } from 'antd';
|
||||
import styles from './ShareCodeValidator.module.css';
|
||||
import {env} from '../../../env'
|
||||
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';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||
onValidSuccess,
|
||||
}) => {
|
||||
const [code, setCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { data: result, isLoading } = api.shareCode.getFileByShareCode.useQuery(
|
||||
const { data: result, isLoading,refetch } = api.shareCode.getFileByShareCode.useQuery(
|
||||
{ code: code.trim() },
|
||||
{
|
||||
enabled: !!code.trim()
|
||||
enabled: false
|
||||
}
|
||||
)
|
||||
const validateCode = useCallback(async () => {
|
||||
|
||||
|
||||
|
||||
const validateCode = useCallback(() => {
|
||||
|
||||
if (!code.trim()) {
|
||||
message.warning('请输入正确的分享码');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('验证分享码返回数据:', result);
|
||||
onValidSuccess(result.url, result.fileName);
|
||||
message.success(`验证成功,文件名:${result.fileName},请等待下载...`);
|
||||
const { data: latestResult } = await refetch();
|
||||
console.log('验证分享码返回数据:', latestResult);
|
||||
onValidSuccess(latestResult.url, latestResult.fileName);
|
||||
} catch (error) {
|
||||
console.error('验证分享码失败:', error);
|
||||
message.error('分享码无效或已过期');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},[result,code, onValidSuccess])
|
||||
}, [refetch, code, onValidSuccess]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
placeholder="请输入分享码"
|
||||
maxLength={8}
|
||||
onPressEnter={validateCode}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={validateCode}
|
||||
loading={loading}
|
||||
disabled={!code.trim()}
|
||||
>
|
||||
验证并下载
|
||||
</Button>
|
||||
</div>
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
placeholder="请输入分享码"
|
||||
maxLength={8}
|
||||
onPressEnter={validateCode}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={validateCode}
|
||||
loading={loading}
|
||||
disabled={!code.trim()}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
!loading && result && (
|
||||
<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).tz('Asia/Shanghai').toDate().toLocaleString() : ''}`}</span>
|
||||
<span >{`剩余使用次数:${result?.canUseTimes ? result.canUseTimes : ''}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -4,7 +4,7 @@ import {
|
|||
CheckCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Upload, Progress, Button } from "antd";
|
||||
import { Upload, Progress, Button, message } from "antd";
|
||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||
import toast from "react-hot-toast";
|
||||
export interface TusUploaderProps {
|
||||
|
@ -12,9 +12,9 @@ export interface TusUploaderProps {
|
|||
onChange?: (value: string[]) => void;
|
||||
multiple?: boolean;
|
||||
allowTypes?: string[];
|
||||
style?:string
|
||||
icon?:ReactNode,
|
||||
description?:string
|
||||
style?: string
|
||||
icon?: ReactNode,
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface UploadingFile {
|
||||
|
@ -30,7 +30,7 @@ export const TusUploader = ({
|
|||
onChange,
|
||||
multiple = true,
|
||||
allowTypes = undefined,
|
||||
style="",
|
||||
style = "",
|
||||
icon = <UploadOutlined />,
|
||||
description = "点击或拖拽文件到此区域进行上传",
|
||||
}: TusUploaderProps) => {
|
||||
|
@ -67,8 +67,14 @@ export const TusUploader = ({
|
|||
|
||||
const handleBeforeUpload = useCallback(
|
||||
(file: File) => {
|
||||
console.log('File object:',file)
|
||||
// 判断是否为文件
|
||||
if (!file.type) {
|
||||
message.error('请选择正确的文件');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
if (allowTypes && !allowTypes.includes(file.type)) {
|
||||
toast.error(`文件类型 ${file.type} 不在允许范围内`);
|
||||
message.error(`文件类型 ${file.type} 不在允许范围内`);
|
||||
return Upload.LIST_IGNORE; // 使用 antd 的官方阻止方式
|
||||
}
|
||||
const fileKey = `${file.name}-${Date.now()}`;
|
||||
|
@ -150,7 +156,9 @@ export const TusUploader = ({
|
|||
name="files"
|
||||
multiple={multiple}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleBeforeUpload}>
|
||||
beforeUpload={handleBeforeUpload}
|
||||
directory={false}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
{icon}
|
||||
</p>
|
||||
|
@ -180,10 +188,10 @@ export const TusUploader = ({
|
|||
file.status === "done"
|
||||
? 100
|
||||
: Math.round(
|
||||
uploadProgress?.[
|
||||
file.fileKey!
|
||||
] || 0
|
||||
)
|
||||
uploadProgress?.[
|
||||
file.fileKey!
|
||||
] || 0
|
||||
)
|
||||
}
|
||||
status={
|
||||
file.status === "error"
|
||||
|
|
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://192.168.43.206:3006/auth/file;
|
||||
proxy_pass http://192.168.43.206:3001/auth/file;
|
||||
|
||||
# 请求优化:不传递请求体
|
||||
proxy_pass_request_body off;
|
||||
|
|
|
@ -89,11 +89,10 @@ model Staff {
|
|||
officerId String? @map("officer_id")
|
||||
|
||||
// watchedPost Post[] @relation("post_watch_staff")
|
||||
visits Visit[]
|
||||
posts Post[]
|
||||
visits Visit[]
|
||||
posts Post[]
|
||||
|
||||
|
||||
learningPosts Post[] @relation("post_student")
|
||||
learningPosts Post[] @relation("post_student")
|
||||
sentMsgs Message[] @relation("message_sender")
|
||||
receivedMsgs Message[] @relation("message_receiver")
|
||||
registerToken String?
|
||||
|
@ -113,7 +112,7 @@ model Department {
|
|||
id String @id @default(cuid())
|
||||
name String
|
||||
order Float?
|
||||
posts Post[] @relation("post_dept")
|
||||
posts Post[] @relation("post_dept")
|
||||
ancestors DeptAncestry[] @relation("DescendantToAncestor")
|
||||
descendants DeptAncestry[] @relation("AncestorToDescendant")
|
||||
parentId String? @map("parent_id")
|
||||
|
@ -191,45 +190,45 @@ model AppConfig {
|
|||
|
||||
model Post {
|
||||
// 字符串类型字段
|
||||
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
|
||||
type String? // Post类型,课程、章节、小节、讨论都用Post实现
|
||||
level String?
|
||||
state String?
|
||||
title String? // 帖子标题,可为空
|
||||
subTitle String?
|
||||
content String? // 帖子内容,可为空
|
||||
important Boolean? //是否重要/精选/突出
|
||||
domainId String? @map("domain_id")
|
||||
terms Term[] @relation("post_term")
|
||||
order Float? @default(0) @map("order")
|
||||
duration Int?
|
||||
rating Int? @default(0)
|
||||
students Staff[] @relation("post_student")
|
||||
depts Department[] @relation("post_dept")
|
||||
views Int @default(0) @map("views")
|
||||
hates Int @default(0) @map("hates")
|
||||
likes Int @default(0) @map("likes")
|
||||
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
|
||||
type String? // Post类型,课程、章节、小节、讨论都用Post实现
|
||||
level String?
|
||||
state String?
|
||||
title String? // 帖子标题,可为空
|
||||
subTitle String?
|
||||
content String? // 帖子内容,可为空
|
||||
important Boolean? //是否重要/精选/突出
|
||||
domainId String? @map("domain_id")
|
||||
terms Term[] @relation("post_term")
|
||||
order Float? @default(0) @map("order")
|
||||
duration Int?
|
||||
rating Int? @default(0)
|
||||
students Staff[] @relation("post_student")
|
||||
depts Department[] @relation("post_dept")
|
||||
views Int @default(0) @map("views")
|
||||
hates Int @default(0) @map("hates")
|
||||
likes Int @default(0) @map("likes")
|
||||
// 索引
|
||||
// 日期时间类型字段
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
publishedAt DateTime? @map("published_at") // 发布时间
|
||||
updatedAt DateTime @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at") // 删除时间,可为空
|
||||
instructors PostInstructor[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
publishedAt DateTime? @map("published_at") // 发布时间
|
||||
updatedAt DateTime @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at") // 删除时间,可为空
|
||||
instructors PostInstructor[]
|
||||
// 关系类型字段
|
||||
authorId String? @map("author_id")
|
||||
author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
||||
enrollments Enrollment[] // 学生报名记录
|
||||
visits Visit[] // 访问记录,关联 Visit 模型
|
||||
parentId String? @map("parent_id")
|
||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
||||
hasChildren Boolean? @default(false) @map("has_children")
|
||||
authorId String? @map("author_id")
|
||||
author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
||||
enrollments Enrollment[] // 学生报名记录
|
||||
visits Visit[] // 访问记录,关联 Visit 模型
|
||||
parentId String? @map("parent_id")
|
||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
||||
hasChildren Boolean? @default(false) @map("has_children")
|
||||
// 闭包表关系
|
||||
ancestors PostAncestry[] @relation("DescendantPosts")
|
||||
descendants PostAncestry[] @relation("AncestorPosts")
|
||||
resources Resource[] // 附件列表
|
||||
meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int
|
||||
ancestors PostAncestry[] @relation("DescendantPosts")
|
||||
descendants PostAncestry[] @relation("AncestorPosts")
|
||||
resources Resource[] // 附件列表
|
||||
meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int
|
||||
|
||||
// 索引
|
||||
@@index([type, domainId])
|
||||
|
@ -247,12 +246,13 @@ model Post {
|
|||
}
|
||||
|
||||
model PostAncestry {
|
||||
id String @id @default(cuid())
|
||||
ancestorId String? @map("ancestor_id")
|
||||
descendantId String @map("descendant_id")
|
||||
relDepth Int @map("rel_depth")
|
||||
ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id])
|
||||
descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id])
|
||||
id String @id @default(cuid())
|
||||
ancestorId String? @map("ancestor_id")
|
||||
descendantId String @map("descendant_id")
|
||||
relDepth Int @map("rel_depth")
|
||||
ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id])
|
||||
descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id])
|
||||
|
||||
// 复合索引优化
|
||||
// 索引建议
|
||||
@@index([ancestorId]) // 针对祖先的查询
|
||||
|
@ -276,23 +276,24 @@ model Message {
|
|||
visits Visit[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||
|
||||
@@index([type, createdAt])
|
||||
@@map("message")
|
||||
}
|
||||
|
||||
model Visit {
|
||||
id String @id @default(cuid()) @map("id")
|
||||
id String @id @default(cuid()) @map("id")
|
||||
type String?
|
||||
views Int @default(1) @map("views")
|
||||
views Int @default(1) @map("views")
|
||||
// sourceIP String? @map("source_ip")
|
||||
// 关联关系
|
||||
visitorId String? @map("visitor_id")
|
||||
visitor Staff? @relation(fields: [visitorId], references: [id])
|
||||
postId String? @map("post_id")
|
||||
post Post? @relation(fields: [postId], references: [id])
|
||||
message Message? @relation(fields: [messageId], references: [id])
|
||||
messageId String? @map("message_id")
|
||||
lectureId String? @map("lecture_id") // 课时ID
|
||||
postId String? @map("post_id")
|
||||
post Post? @relation(fields: [postId], references: [id])
|
||||
message Message? @relation(fields: [messageId], references: [id])
|
||||
messageId String? @map("message_id")
|
||||
lectureId String? @map("lecture_id") // 课时ID
|
||||
createdAt DateTime @default(now()) @map("created_at") // 创建时间
|
||||
updatedAt DateTime @updatedAt @map("updated_at") // 更新时间
|
||||
deletedAt DateTime? @map("deleted_at") // 删除时间,可为空
|
||||
|
@ -403,14 +404,16 @@ model NodeEdge {
|
|||
@@index([targetId])
|
||||
@@map("node_edge")
|
||||
}
|
||||
|
||||
model Animal {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
age Int
|
||||
gender Boolean
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
age Int
|
||||
gender Boolean
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id])
|
||||
person Person? @relation(fields: [personId], references: [id])
|
||||
}
|
||||
|
||||
model Person {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
|
@ -418,16 +421,17 @@ model Person {
|
|||
gender Boolean
|
||||
animals Animal[]
|
||||
}
|
||||
model ShareCode {
|
||||
id String @id @default(cuid())
|
||||
code String? @unique
|
||||
fileId String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
isUsed Boolean? @default(false)
|
||||
fileName String? @map("file_name")
|
||||
|
||||
model ShareCode {
|
||||
id String @id @default(cuid())
|
||||
code String? @unique
|
||||
fileId String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
isUsed Boolean? @default(false)
|
||||
fileName String? @map("file_name")
|
||||
canUseTimes Int?
|
||||
@@index([code])
|
||||
@@index([fileId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -380,6 +380,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-fast-marquee:
|
||||
specifier: ^1.6.5
|
||||
version: 1.6.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.54.2
|
||||
version: 7.54.2(react@18.2.0)
|
||||
|
@ -6657,6 +6660,12 @@ packages:
|
|||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-fast-marquee@1.6.5:
|
||||
resolution: {integrity: sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==}
|
||||
peerDependencies:
|
||||
react: '>= 16.8.0 || ^18.0.0'
|
||||
react-dom: '>= 16.8.0 || ^18.0.0'
|
||||
|
||||
react-hook-form@7.54.2:
|
||||
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
@ -14797,6 +14806,11 @@ snapshots:
|
|||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-fast-marquee@1.6.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
react-hook-form@7.54.2(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
|
BIN
web-dist.zip
BIN
web-dist.zip
Binary file not shown.
Loading…
Reference in New Issue