add 0126
This commit is contained in:
parent
262dfade4e
commit
8ce5d689c2
|
|
@ -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;
|
||||
}
|
||||
|
||||
const newFiles: UploadingFile[] = files.map((f) => ({
|
||||
name: f.name,
|
||||
setUploadingFiles((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: file.name,
|
||||
progress: 0,
|
||||
status: "uploading" as const,
|
||||
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
|
||||
}));
|
||||
status: "uploading",
|
||||
fileKey,
|
||||
},
|
||||
]);
|
||||
|
||||
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,
|
||||
file,
|
||||
(result) => {
|
||||
console.log("上传成功:", result);
|
||||
const completedFile = {
|
||||
name: f.name,
|
||||
progress: 100,
|
||||
status: "done" as const,
|
||||
fileId: result.fileId,
|
||||
};
|
||||
setCompletedFiles((prev) => [
|
||||
...prev,
|
||||
completedFile,
|
||||
{
|
||||
name: file.name,
|
||||
progress: 100,
|
||||
status: "done",
|
||||
fileId: result.fileId,
|
||||
},
|
||||
]);
|
||||
|
||||
setUploadingFiles((prev) =>
|
||||
prev.filter(
|
||||
(file) => file.fileKey !== fileKey
|
||||
)
|
||||
prev.filter((f) => f.fileKey !== fileKey)
|
||||
);
|
||||
resolve(result.fileId);
|
||||
|
||||
// 正确的状态更新方式
|
||||
setUploadResults((prev) => {
|
||||
const newValue = [...prev, result.fileId];
|
||||
onChange?.(newValue); // 传递值而非函数
|
||||
return newValue;
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
console.error("上传错误:", error);
|
||||
reject(error);
|
||||
toast.error(
|
||||
`上传失败: ${
|
||||
error instanceof Error ? error.message : "未知错误"
|
||||
}`
|
||||
);
|
||||
setUploadingFiles((prev) =>
|
||||
prev.map((f) =>
|
||||
f.fileKey === fileKey
|
||||
? { ...f, status: "error" }
|
||||
: f
|
||||
)
|
||||
);
|
||||
},
|
||||
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" }))
|
||||
);
|
||||
}
|
||||
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">
|
||||
className="flex flex-col gap-1 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm">{file.name}</div>
|
||||
<span className="text-sm">{file.name}</span>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
className="flex-1 w-full"
|
||||
percent={
|
||||
file.status === "done"
|
||||
? 100
|
||||
: Math.round(
|
||||
uploadProgress?.[
|
||||
file?.fileKey
|
||||
file.fileKey!
|
||||
] || 0
|
||||
)
|
||||
}
|
||||
status={
|
||||
file.status === "error"
|
||||
? "exception"
|
||||
: file.status === "done"
|
||||
? "success"
|
||||
: "active"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{completedFiles.length > 0 &&
|
||||
completedFiles.map((file, index) => (
|
||||
|
||||
{/* 已完成的文件 */}
|
||||
{completedFiles.map((file) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between gap-2">
|
||||
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" />
|
||||
<div className="text-sm">
|
||||
{file.name}
|
||||
</div>
|
||||
<span className="text-sm">{file.name}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
if (file.fileId) {
|
||||
handleRemoveFile(file.fileId); // 只删除文件
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (file.fileId)
|
||||
handleRemoveFile(file.fileId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Upload.Dragger>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 />}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ export function LetterBasicForm() {
|
|||
<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">
|
||||
|
||||
<TusUploader
|
||||
onChange={(resources) =>
|
||||
form.setFieldValue(
|
||||
|
|
@ -89,7 +88,6 @@ export function LetterBasicForm() {
|
|||
)
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</Form.Item>
|
||||
</TabPane>
|
||||
|
|
@ -101,6 +99,17 @@ export function LetterBasicForm() {
|
|||
是否公开
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<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()}
|
||||
|
|
@ -110,6 +119,7 @@ export function LetterBasicForm() {
|
|||
发送信件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue