add
This commit is contained in:
parent
7e1942fa07
commit
741ff22489
|
@ -12,22 +12,22 @@ export interface TusUploaderProps {
|
||||||
value?: string[];
|
value?: string[];
|
||||||
onChange?: (value: string[]) => void;
|
onChange?: (value: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadingFile {
|
interface UploadingFile {
|
||||||
name: string;
|
name: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
status: "uploading" | "done" | "error";
|
status: "uploading" | "done" | "error";
|
||||||
fileId?: string;
|
fileId?: string;
|
||||||
|
fileKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
const { handleFileUpload } = useTusUpload();
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
||||||
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
|
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
|
||||||
() =>
|
() =>
|
||||||
value?.map((fileId) => ({
|
value?.map((fileId) => ({
|
||||||
name: `文件 ${fileId}`, // 可以根据需要获取实际文件名
|
name: `文件 ${fileId}`,
|
||||||
progress: 1,
|
progress: 100,
|
||||||
status: "done" as const,
|
status: "done" as const,
|
||||||
fileId,
|
fileId,
|
||||||
})) || []
|
})) || []
|
||||||
|
@ -60,7 +60,9 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: "uploading" as const,
|
status: "uploading" as const,
|
||||||
|
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setUploadingFiles((prev) => [...prev, ...newFiles]);
|
setUploadingFiles((prev) => [...prev, ...newFiles]);
|
||||||
|
|
||||||
const newUploadResults: string[] = [];
|
const newUploadResults: string[] = [];
|
||||||
|
@ -69,6 +71,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw new Error(`文件 ${f.name} 无效`);
|
throw new Error(`文件 ${f.name} 无效`);
|
||||||
}
|
}
|
||||||
|
const fileKey = newFiles[index].fileKey!;
|
||||||
const fileId = await new Promise<string>(
|
const fileId = await new Promise<string>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
|
@ -77,7 +80,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
console.log("上传成功:", result);
|
console.log("上传成功:", result);
|
||||||
const completedFile = {
|
const completedFile = {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
progress: 1,
|
progress: 100,
|
||||||
status: "done" as const,
|
status: "done" as const,
|
||||||
fileId: result.fileId,
|
fileId: result.fileId,
|
||||||
};
|
};
|
||||||
|
@ -86,14 +89,17 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
completedFile,
|
completedFile,
|
||||||
]);
|
]);
|
||||||
setUploadingFiles((prev) =>
|
setUploadingFiles((prev) =>
|
||||||
prev.filter((_, i) => i !== index)
|
prev.filter(
|
||||||
|
(file) => file.fileKey !== fileKey
|
||||||
|
)
|
||||||
);
|
);
|
||||||
resolve(result.fileId);
|
resolve(result.fileId);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error("上传错误:", error);
|
console.error("上传错误:", error);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
},
|
||||||
|
fileKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -126,7 +132,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
name="files"
|
name="files"
|
||||||
multiple
|
multiple
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
style={{ background: "white" }}
|
style={{ background: "white", borderStyle: "solid" }}
|
||||||
beforeUpload={handleChange}>
|
beforeUpload={handleChange}>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
|
@ -138,30 +144,35 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
{/* 正在上传的文件 */}
|
{/* 正在上传的文件 */}
|
||||||
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
|
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
|
||||||
<div className=" p-2 border rounded bg-white mt-1">
|
<div className=" p-2 border rounded bg-white mt-1">
|
||||||
{uploadingFiles.length > 0 &&
|
{uploadingFiles.map((file) => (
|
||||||
uploadingFiles.map((file, index) => (
|
<div
|
||||||
<div
|
key={file.fileKey}
|
||||||
key={index}
|
className="flex flex-col gap-1">
|
||||||
className="flex flex-col gap-1">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="text-sm">{file.name}</div>
|
||||||
<div className="text-sm">
|
|
||||||
{file.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
percent={Math.round(
|
|
||||||
file.progress * 100
|
|
||||||
)}
|
|
||||||
status={
|
|
||||||
file.status === "error"
|
|
||||||
? "exception"
|
|
||||||
: file.status === "done"
|
|
||||||
? "success"
|
|
||||||
: "active"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
<Progress
|
||||||
|
className="flex-1 w-full"
|
||||||
|
percent={
|
||||||
|
file.status === "done"
|
||||||
|
? 100
|
||||||
|
: Math.round(
|
||||||
|
uploadProgress?.[
|
||||||
|
file?.fileKey
|
||||||
|
] || 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
status={
|
||||||
|
file.status === "error"
|
||||||
|
? "exception"
|
||||||
|
: file.status === "done"
|
||||||
|
? "success"
|
||||||
|
: "active"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{completedFiles.length > 0 &&
|
{completedFiles.length > 0 &&
|
||||||
completedFiles.map((file, index) => (
|
completedFiles.map((file, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -95,18 +95,20 @@ export default function PostCommentEditor() {
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
{!isContentEmpty(content) && (
|
||||||
<div>
|
<div className="flex items-center justify-end">
|
||||||
<Button
|
<div>
|
||||||
type="primary"
|
<Button
|
||||||
htmlType="submit"
|
type="primary"
|
||||||
disabled={isContentEmpty(content)}
|
htmlType="submit"
|
||||||
className="flex items-center space-x-2 bg-primary"
|
disabled={isContentEmpty(content)}
|
||||||
icon={<SendOutlined />}>
|
className="flex items-center space-x-2 bg-primary"
|
||||||
提交
|
icon={<SendOutlined />}>
|
||||||
</Button>
|
提交
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,23 +10,23 @@ export default function Content() {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const contentWrapperRef = useRef(null);
|
const contentWrapperRef = useRef(null);
|
||||||
const [shouldCollapse, setShouldCollapse] = useState(false);
|
const [shouldCollapse, setShouldCollapse] = useState(false);
|
||||||
|
const maxHeight = 125;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentWrapperRef.current) {
|
if (contentWrapperRef.current) {
|
||||||
const shouldCollapse = contentWrapperRef.current.scrollHeight > 100;
|
const shouldCollapse = contentWrapperRef.current.scrollHeight > 150;
|
||||||
setShouldCollapse(shouldCollapse);
|
setShouldCollapse(shouldCollapse);
|
||||||
}
|
}
|
||||||
}, [post?.content]);
|
}, [post?.content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-white rounded-b-xl p-4 pt-2 shadow-lg border border-[#97A9C4]/30">
|
<div className="relative bg-white rounded-b-xl p-4 pt-2 border border-[#97A9C4]/30">
|
||||||
<div className="text-secondary-700">
|
<div className="text-secondary-700">
|
||||||
{/* 包装整个内容区域的容器 */}
|
{/* 包装整个内容区域的容器 */}
|
||||||
<div
|
<div
|
||||||
ref={contentWrapperRef}
|
ref={contentWrapperRef}
|
||||||
className={`duration-300 ${
|
className={`duration-300 ${
|
||||||
shouldCollapse && !isExpanded
|
shouldCollapse && !isExpanded
|
||||||
? "max-h-[100px] overflow-hidden relative"
|
? `max-h-[150px] overflow-hidden relative`
|
||||||
: ""
|
: ""
|
||||||
}`}>
|
}`}>
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { CornerBadge } from "../badge/CornerBadeg";
|
import { CornerBadge } from "../badge/CornerBadeg";
|
||||||
|
import { LetterBadge } from "../../LetterBadge";
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph, Text } = Typography;
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { post, user } = useContext(PostDetailContext);
|
const { post, user } = useContext(PostDetailContext);
|
||||||
|
@ -88,22 +89,23 @@ export default function Header() {
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{/* Tags Badges */}
|
{/* Tags Badges */}
|
||||||
|
|
||||||
<Space>
|
<LetterBadge type="state" value={post?.state} />
|
||||||
<PostBadge type="state" value={post?.state} />
|
{(post?.terms || [])?.map((term, index) => {
|
||||||
</Space>
|
return (
|
||||||
<Space>
|
<LetterBadge
|
||||||
<PostBadge
|
key={`${term.name}-${index}`}
|
||||||
type="category"
|
type="category"
|
||||||
value={post?.term?.name}
|
value={term.name}
|
||||||
/>
|
/>
|
||||||
</Space>
|
);
|
||||||
|
})}
|
||||||
{post.meta.tags.length > 0 &&
|
{post.meta.tags.length > 0 &&
|
||||||
post.meta.tags.map((tag, index) => (
|
post.meta.tags.map((tag, index) => (
|
||||||
<Space key={index}>
|
<LetterBadge
|
||||||
<PostBadge
|
key={`${tag}-${index}`}
|
||||||
type="tag"
|
type="tag"
|
||||||
value={`${tag}`}></PostBadge>
|
value={tag}
|
||||||
</Space>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -12,8 +12,7 @@ import { useVisitor } from "@nice/client";
|
||||||
import { VisitType } from "packages/common/dist";
|
import { VisitType } from "packages/common/dist";
|
||||||
import PostLikeButton from "./PostLikeButton";
|
import PostLikeButton from "./PostLikeButton";
|
||||||
export function StatsSection() {
|
export function StatsSection() {
|
||||||
const { post, user } = useContext(PostDetailContext);
|
const { post } = useContext(PostDetailContext);
|
||||||
const { like, unLike } = useVisitor();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
@ -22,14 +21,12 @@ export function StatsSection() {
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.7 }}
|
||||||
className="mt-6 flex flex-wrap gap-4 justify-between items-center">
|
className="mt-6 flex flex-wrap gap-4 justify-between items-center">
|
||||||
<div className=" flex gap-2">
|
<div className=" flex gap-2">
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<Button type="default" shape="round" icon={<EyeOutlined />}>
|
||||||
<EyeOutlined className="text-lg" />
|
{post?.views}
|
||||||
<span className="text-sm">{post?.views}</span>
|
</Button>
|
||||||
</div>
|
<Button type="default" shape="round" icon={<CommentOutlined />}>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
{post?.commentsCount}
|
||||||
<CommentOutlined className="text-lg" />
|
</Button>
|
||||||
<span className="text-sm">{post?.commentsCount}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<PostLikeButton post={post}></PostLikeButton>
|
<PostLikeButton post={post}></PostLikeButton>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -1,89 +1,101 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
|
|
||||||
|
// useTusUpload.ts
|
||||||
|
interface UploadProgress {
|
||||||
|
fileId: string;
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface UploadResult {
|
interface UploadResult {
|
||||||
url: string;
|
url: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
// resource: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTusUpload() {
|
export function useTusUpload() {
|
||||||
const [progress, setProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState<
|
||||||
|
Record<string, number>
|
||||||
|
>({});
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
|
||||||
const getFileId = (url: string) => {
|
const getFileId = (url: string) => {
|
||||||
const parts = url.split("/");
|
const parts = url.split("/");
|
||||||
// Find the index of the 'upload' segment
|
|
||||||
const uploadIndex = parts.findIndex((part) => part === "upload");
|
const uploadIndex = parts.findIndex((part) => part === "upload");
|
||||||
if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) {
|
if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) {
|
||||||
throw new Error("Invalid upload URL format");
|
throw new Error("Invalid upload URL format");
|
||||||
}
|
}
|
||||||
// Get the date parts and file ID (4 segments after 'upload')
|
|
||||||
return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/");
|
return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = async (
|
const handleFileUpload = async (
|
||||||
file: File,
|
file: File,
|
||||||
onSuccess: (result: UploadResult) => void,
|
onSuccess: (result: UploadResult) => void,
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void,
|
||||||
|
fileKey: string // 添加文件唯一标识
|
||||||
) => {
|
) => {
|
||||||
if (!file || !file.name || !file.type) {
|
if (!file || !file.name || !file.type) {
|
||||||
const error = new Error('Invalid file provided');
|
const error = new Error("不可上传该类型文件");
|
||||||
setUploadError(error.message);
|
setUploadError(error.message);
|
||||||
onError(error);
|
onError(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setProgress(0);
|
setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 }));
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const upload = new tus.Upload(file, {
|
const upload = new tus.Upload(file, {
|
||||||
endpoint: "http://localhost:3000/upload",
|
endpoint: "http://localhost:3000/upload",
|
||||||
retryDelays: [0, 1000, 3000, 5000],
|
retryDelays: [0, 1000, 3000, 5000],
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
filetype: file.type,
|
filetype: file.type,
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
const uploadProgress = (
|
const progress = Number(
|
||||||
(bytesUploaded / bytesTotal) *
|
((bytesUploaded / bytesTotal) * 100).toFixed(2)
|
||||||
100
|
);
|
||||||
).toFixed(2);
|
setUploadProgress((prev) => ({
|
||||||
setProgress(Number(uploadProgress));
|
...prev,
|
||||||
},
|
[fileKey]: progress,
|
||||||
onSuccess: async () => {
|
}));
|
||||||
try {
|
},
|
||||||
if (upload.url) {
|
onSuccess: async () => {
|
||||||
const fileId = getFileId(upload.url);
|
try {
|
||||||
// const resource = await pollResourceStatus(fileId);
|
if (upload.url) {
|
||||||
|
const fileId = getFileId(upload.url);
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[fileKey]: 100,
|
||||||
|
}));
|
||||||
|
onSuccess({
|
||||||
|
url: upload.url,
|
||||||
|
fileId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const err =
|
||||||
|
error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error("Unknown error");
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setProgress(100);
|
setUploadError(err.message);
|
||||||
onSuccess({
|
onError(err);
|
||||||
url: upload.url,
|
|
||||||
fileId,
|
|
||||||
// resource,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
const err =
|
onError: (error) => {
|
||||||
error instanceof Error
|
|
||||||
? error
|
|
||||||
: new Error("Unknown error");
|
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadError(err.message);
|
setUploadError(error.message);
|
||||||
onError(err);
|
onError(error);
|
||||||
}
|
},
|
||||||
},
|
});
|
||||||
onError: (error) => {
|
upload.start();
|
||||||
setIsUploading(false);
|
|
||||||
setUploadError(error.message);
|
|
||||||
onError(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
upload.start();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error instanceof Error ? error : new Error("Upload failed");
|
const err =
|
||||||
|
error instanceof Error ? error : new Error("Upload failed");
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadError(err.message);
|
setUploadError(err.message);
|
||||||
onError(err);
|
onError(err);
|
||||||
|
@ -91,7 +103,7 @@ export function useTusUpload() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
progress,
|
uploadProgress,
|
||||||
isUploading,
|
isUploading,
|
||||||
uploadError,
|
uploadError,
|
||||||
handleFileUpload,
|
handleFileUpload,
|
||||||
|
|
Loading…
Reference in New Issue