This commit is contained in:
ditiqi 2025-01-26 19:33:45 +08:00
parent 262dfade4e
commit 8ce5d689c2
9 changed files with 185 additions and 202 deletions

View File

@ -4,7 +4,7 @@ import {
CheckCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { Upload, Progress, Button } from "antd";
import { Upload, Progress, Button } from "antd";
import type { UploadFile } from "antd";
import { useTusUpload } from "@web/src/hooks/useTusUpload";
import toast from "react-hot-toast";
@ -13,6 +13,7 @@ export interface TusUploaderProps {
value?: string[];
onChange?: (value: string[]) => void;
}
interface UploadingFile {
name: string;
progress: number;
@ -33,106 +34,92 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
fileId,
})) || []
);
// 恢复使用 uploadResults 状态跟踪最新结果
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
const handleRemoveFile = useCallback(
(fileId: string) => {
setCompletedFiles((prev) =>
prev.filter((f) => f.fileId !== fileId)
);
const newResults = uploadResults.filter((id) => id !== fileId);
setUploadResults(newResults);
onChange?.(newResults);
// 使用函数式更新保证获取最新状态
setUploadResults((prev) => {
const newValue = prev.filter((id) => id !== fileId);
onChange?.(newValue); // 同步更新父组件
return newValue;
});
},
[uploadResults, onChange]
[onChange]
);
const handleChange = useCallback(
async (fileList: UploadFile | UploadFile[]) => {
const files = Array.isArray(fileList) ? fileList : [fileList];
console.log("文件", files);
const handleBeforeUpload = useCallback(
(file: File) => {
const fileKey = `${file.name}-${Date.now()}`;
if (!files.every((f) => f instanceof File)) {
toast.error("无效的文件格式");
return false;
}
setUploadingFiles((prev) => [
...prev,
{
name: file.name,
progress: 0,
status: "uploading",
fileKey,
},
]);
const newFiles: UploadingFile[] = files.map((f) => ({
name: f.name,
progress: 0,
status: "uploading" as const,
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
}));
handleFileUpload(
file,
(result) => {
setCompletedFiles((prev) => [
...prev,
{
name: file.name,
progress: 100,
status: "done",
fileId: result.fileId,
},
]);
setUploadingFiles((prev) => [...prev, ...newFiles]);
const newUploadResults: string[] = [];
try {
for (const [index, f] of files.entries()) {
if (!f) {
throw new Error(`文件 ${f.name} 无效`);
}
const fileKey = newFiles[index].fileKey!;
const fileId = await new Promise<string>(
(resolve, reject) => {
handleFileUpload(
f as File,
(result) => {
console.log("上传成功:", result);
const completedFile = {
name: f.name,
progress: 100,
status: "done" as const,
fileId: result.fileId,
};
setCompletedFiles((prev) => [
...prev,
completedFile,
]);
setUploadingFiles((prev) =>
prev.filter(
(file) => file.fileKey !== fileKey
)
);
resolve(result.fileId);
},
(error) => {
console.error("上传错误:", error);
reject(error);
},
fileKey
);
}
setUploadingFiles((prev) =>
prev.filter((f) => f.fileKey !== fileKey)
);
newUploadResults.push(fileId);
}
const newValue = Array.from(
new Set([...uploadResults, ...newUploadResults])
);
setUploadResults(newValue);
onChange?.(newValue);
} catch (error) {
console.error("上传错误详情:", error);
toast.error(
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
);
setUploadingFiles((prev) =>
prev.map((f) => ({ ...f, status: "error" }))
);
}
// 正确的状态更新方式
setUploadResults((prev) => {
const newValue = [...prev, result.fileId];
onChange?.(newValue); // 传递值而非函数
return newValue;
});
},
(error) => {
console.error("上传错误:", error);
toast.error(
`上传失败: ${
error instanceof Error ? error.message : "未知错误"
}`
);
setUploadingFiles((prev) =>
prev.map((f) =>
f.fileKey === fileKey
? { ...f, status: "error" }
: f
)
);
},
fileKey
);
return false;
},
[uploadResults, onChange, handleFileUpload]
[handleFileUpload, onChange]
);
return (
<div className="space-y-1">
<Upload.Dragger
name="files"
multiple
showUploadList={false}
style={{ background: "transparent", borderStyle: "none" }}
beforeUpload={handleChange}>
beforeUpload={handleBeforeUpload}>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
@ -140,64 +127,58 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
</p>
<p className="ant-upload-hint"></p>
{/* 正在上传的文件 */}
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
<div className=" px-2 py-0 rounded mt-1 ">
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="text-sm">{file.name}</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 className="px-2 py-0 rounded mt-1">
{/* 上传中的文件 */}
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
className="flex flex-col gap-1 mb-2">
<div className="flex items-center gap-2">
<span className="text-sm">{file.name}</span>
</div>
))}
{completedFiles.length > 0 &&
completedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" />
<div className="text-sm">
{file.name}
</div>
</div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
if (file.fileId) {
handleRemoveFile(file.fileId); // 只删除文件
}
}}
/>
</div>
))}
</div>
)}
<Progress
percent={
file.status === "done"
? 100
: Math.round(
uploadProgress?.[
file.fileKey!
] || 0
)
}
status={
file.status === "error"
? "exception"
: "active"
}
/>
</div>
))}
{/* 已完成的文件 */}
{completedFiles.map((file) => (
<div
key={file.fileId}
className="flex items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" />
<span className="text-sm">{file.name}</span>
</div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
if (file.fileId)
handleRemoveFile(file.fileId);
}}
/>
</div>
))}
</div>
</Upload.Dragger>
</div>
);

View File

@ -1,52 +1,26 @@
import { Link, NavLink, useNavigate } from "react-router-dom";
import { memo, useMemo } from "react";
import { memo } from "react";
import { SearchBar } from "./SearchBar";
import Navigation from "./navigation";
import { useAuth } from "@web/src/providers/auth-provider";
import { UserOutlined } from "@ant-design/icons";
import { UserMenu } from "../element/usermenu/usermenu";
import { api, useAppConfig } from "@nice/client";
import { env } from "@web/src/env";
export const Header = memo(function Header() {
const { isAuthenticated } = useAuth();
const { logo } = useAppConfig();
const { data: logoRes, isLoading } = api.resource.findFirst.useQuery(
{
where: {
fileId: logo,
},
select: {
id: true,
url: true,
},
},
{
enabled: !!logo,
}
);
const logoUrl: string = useMemo(() => {
return `http://${env.SERVER_IP}/uploads/${logoRes?.url}`;
}, [logoRes]);
return (
<header className="sticky top-0 z-50 bg-gradient-to-br from-primary-500 to-primary-800 text-white shadow-lg">
<div className=" mx-auto px-4">
<div className="py-2">
<div className="py-4">
<div className="flex items-center justify-between gap-4">
<div className="">
{/** 在这里放置logo */}
{isLoading ? (
<div className="w-48 h-24 bg-gray-300 animate-pulse"></div>
) : (
logoUrl && (
<img
src={logoUrl}
alt="Logo"
style={{ width: 192, height: 72 }}
className=" w-full h-full"
/>
)
)}
<span className="text-xl font-bold">
</span>
<p className=" text-sm text-secondary-50">
怀
</p>
</div>
<div className="flex-grow max-w-2xl">
<SearchBar />

View File

@ -49,7 +49,9 @@ export function LetterCard({ letter }: LetterCardProps) {
<div className="flex items-center gap-2">
<UserOutlined className="text-primary text-base" />
<Text className="text-primary font-medium">
{letter.author?.showname || "匿名用户"}
{letter?.meta?.signature ||
letter.author?.showname ||
"匿名用户"}
</Text>
</div>

View File

@ -37,14 +37,18 @@ export default function PostCommentCard({
src={post.author?.avatar}
size={50}
name={!post.author?.avatar && post.author?.showname}
ip={post?.meta?.ip}></CustomAvatar>
randomString={
post?.meta?.signature || post?.meta?.ip
}></CustomAvatar>
</div>
<div className="flex-1">
<div className={`flex-1 min-w-0 `}>
<div className="flex flex-1 justify-between ">
<div className="flex items-center space-x-2">
<span className="flex font-medium text-slate-900">
{post.author?.showname || "匿名用户"}
{post?.meta?.signature ||
post.author?.showname ||
"匿名用户"}
</span>
<span className="flex text-sm text-slate-500 ">
{dayjs(post?.createdAt).format(

View File

@ -1,6 +1,6 @@
import React, { useContext, useState } from "react";
import { motion } from "framer-motion";
import { Button, Tabs } from "antd";
import { Button, Input, Tabs } from "antd";
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
import { PostDetailContext } from "./context/PostDetailContext";
import { usePost } from "@nice/client";
@ -9,13 +9,14 @@ import toast from "react-hot-toast";
import { isContentEmpty } from "./utils";
import { SendOutlined } from "@ant-design/icons";
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
const { TabPane } = Tabs;
export default function PostCommentEditor() {
const { post } = useContext(PostDetailContext);
const [content, setContent] = useState("");
const [isPreview, setIsPreview] = useState(false);
const [signature, setSignature] = useState<string | undefined>(undefined);
const [fileIds, setFileIds] = useState<string[]>([]);
const { create } = usePost();
@ -38,10 +39,14 @@ export default function PostCommentEditor() {
fileId: id,
})),
},
meta: {
signature,
},
},
});
toast.success("发布成功!");
setContent("");
setFileIds([]);
} catch (error) {
toast.error("发布失败,请稍后重试");
console.error("Error posting comment:", error);
@ -82,6 +87,7 @@ export default function PostCommentEditor() {
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
<TusUploader
onChange={(value) => {
console.log("ids", value);
setFileIds(value);
}}
/>
@ -90,12 +96,23 @@ export default function PostCommentEditor() {
</Tabs>
{!isContentEmpty(content) && (
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
<CustomAvatar randomString={signature}></CustomAvatar>
<Input
maxLength={10}
style={{
width: 150,
}}
onChange={(e) => {
setSignature(e.target.value);
}}
showCount
placeholder="签名"
/>
<div>
<Button
type="primary"
htmlType="submit"
size="large"
disabled={isContentEmpty(content)}
className="flex items-center space-x-2 bg-primary"
icon={<SendOutlined />}>

View File

@ -26,16 +26,15 @@ export default function Header() {
</h1>
</div>
<div className="space-y-1">
<Space className="mr-4">
<span className="text-white"></span>
<Text className="text-white" strong>
{post?.author?.showname || "匿名用户"}
{post?.meta?.signature ||
post?.author?.showname ||
"匿名用户"}
</Text>
</Space>
<Space className="mr-4">
<span className="text-white"> </span>
{post?.receivers?.map((receiver, index) => (
@ -43,27 +42,23 @@ export default function Header() {
strong
className="text-white"
key={`${index}`}>
{receiver?.meta?.rank} {receiver?.showname || '匿名用户'}
{receiver?.meta?.rank}{" "}
{receiver?.showname || "匿名用户"}
</Text>
))}
</Space>
{/* Date Info Badge */}
<Space className="mr-4">
<span className="text-white"></span>
<Text className="text-white">
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
</Text>
</Space>
{/* Last Updated Badge */}
<Space className="mr-4">
<span className="text-white"></span>
<Text className="text-white">
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
</Text>
</Space>

View File

@ -36,8 +36,8 @@ export default function PostHateButton({ post }: { post: PostDto }) {
type={post?.hated ? "primary" : "default"}
style={{
backgroundColor: post?.hated ? "#ff4d4f" : "#fff",
borderColor: post?.hated ? "transparent" : "#ff4d4f",
color: post?.hated ? "#fff" : "#ff4d4f",
borderColor: post?.hated ? "transparent" : "",
color: post?.hated ? "#fff" : "#000",
boxShadow: "none", // 去除阴影
}}
shape="round"

View File

@ -79,8 +79,7 @@ export function LetterBasicForm() {
</TabPane>
<TabPane tab="附件" key="2">
<Form.Item name="resources" required={false}>
<div className="rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
<div className="rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
<TusUploader
onChange={(resources) =>
form.setFieldValue(
@ -89,8 +88,7 @@ export function LetterBasicForm() {
)
}
/>
</div>
</div>
</Form.Item>
</TabPane>
</Tabs>
@ -101,14 +99,26 @@ export function LetterBasicForm() {
</Checkbox>
</Form.Item>
<Button
type="primary"
onClick={() => form.submit()}
size="large"
icon={<SendOutlined />}
className="w-full sm:w-40">
</Button>
<div className="flex gap-2 ">
<Form.Item name={["meta", "signature"]}>
<Input
maxLength={10}
style={{
width: 150,
}}
showCount
placeholder="签名"
/>
</Form.Item>
<Button
type="primary"
onClick={() => form.submit()}
size="large"
icon={<SendOutlined />}
className="w-full sm:w-40">
</Button>
</div>
</div>
</Form>
</div>

View File

@ -6,14 +6,14 @@ import multiavatar from "@multiavatar/multiavatar";
interface CustomAvatarProps extends Omit<AvatarProps, "children"> {
src?: string;
name?: string;
ip?: string;
randomString?: string;
}
export function CustomAvatar({
src,
name,
className = "",
ip,
randomString,
...props
}: CustomAvatarProps) {
// 获取名字的第一个字符,如果没有名字则显示"匿"
@ -39,7 +39,7 @@ export function CustomAvatar({
const avatarSrc =
src || (name && name !== "匿名用户")
? src
: generateAvatarFromIp(ip || "default");
: generateAvatarFromIp(randomString || "default");
return (
<Avatar