This commit is contained in:
longdayi 2025-01-27 00:29:36 +08:00
commit 9abc34c031
15 changed files with 159 additions and 56 deletions

View File

@ -26,7 +26,6 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
} }
onModuleInit() { onModuleInit() {
EventBus.on('updatePostState', ({ id }) => { EventBus.on('updatePostState', ({ id }) => {
console.log('updatePostState');
updatePostState(id); updatePostState(id);
}); });
} }
@ -34,7 +33,6 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
args: Prisma.PostCreateArgs, args: Prisma.PostCreateArgs,
params: { staff?: UserProfile; tx?: Prisma.PostDelegate }, params: { staff?: UserProfile; tx?: Prisma.PostDelegate },
) { ) {
console.log('params?.staff?.id', params?.staff?.id);
args.data.authorId = params?.staff?.id; args.data.authorId = params?.staff?.id;
args.data.updatedAt = new Date(); args.data.updatedAt = new Date();
// args.data.resources // args.data.resources

View File

@ -10,7 +10,6 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) { async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
const { postId, messageId } = args.data; const { postId, messageId } = args.data;
const clientIp = (args.data.meta as any)?.ip; const clientIp = (args.data.meta as any)?.ip;
console.log('visit create');
const visitorId = args.data.visitorId || staff?.id; const visitorId = args.data.visitorId || staff?.id;
let result; let result;
const existingVisit = await db.visit.findFirst({ const existingVisit = await db.visit.findFirst({

View File

@ -15,13 +15,11 @@ export class PostQueueService implements OnModuleInit {
constructor(@InjectQueue('general') private generalQueue: Queue) {} constructor(@InjectQueue('general') private generalQueue: Queue) {}
onModuleInit() { onModuleInit() {
EventBus.on('updateVisitCount', ({ id, objectType, visitType }) => { EventBus.on('updateVisitCount', ({ id, objectType, visitType }) => {
console.log('updateVisitCount');
if (objectType === ObjectType.POST) { if (objectType === ObjectType.POST) {
this.addUpdateVisitCountJob({ id, type: visitType }); this.addUpdateVisitCountJob({ id, type: visitType });
} }
}); });
EventBus.on('updatePostState', ({ id }) => { EventBus.on('updatePostState', ({ id }) => {
console.log('updatePostState');
this.addUpdatePostState({ id }); this.addUpdatePostState({ id });
}); });
} }

View File

@ -88,7 +88,6 @@ export class TusService implements OnModuleInit {
upload: Upload, upload: Upload,
) { ) {
try { try {
console.log('upload.id', upload.id);
const resource = await this.resourceService.update({ const resource = await this.resourceService.update({
where: { fileId: this.getFileId(upload.id) }, where: { fileId: this.getFileId(upload.id) },
data: { status: ResourceStatus.UPLOADED }, data: { status: ResourceStatus.UPLOADED },

View File

@ -43,6 +43,11 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
<AvatarUploader <AvatarUploader
className="rounded-lg" className="rounded-lg"
placeholder="点击上传头像" placeholder="点击上传头像"
onChange={(url) => {
if (url) {
console.log(url);
}
}}
style={{ style={{
width: `100%`, width: `100%`,
height: 210, height: 210,
@ -123,7 +128,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
</Form.Item> </Form.Item>
<Form.Item noStyle name={"office"}> <Form.Item noStyle name={"office"}>
<Input <Input
placeholder="请输入办公地点" placeholder="请输入办公地点"
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
allowClear allowClear
@ -151,10 +156,10 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
noStyle noStyle
rules={[ rules={[
{ message: "请输入证件号" }, { message: "请输入证件号" },
{ // {
pattern: /^\d{5,12}$/, // pattern: /^\d{5,12}$/,
message: "请输入有效的证件号5-12位数字", // message: "请输入有效的证件号5-12位数字",
}, // },
]}> ]}>
<Input placeholder="证件号" /> <Input placeholder="证件号" />
</Form.Item> </Form.Item>

View File

@ -1,11 +1,16 @@
import PostDetail from "@web/src/components/models/post/detail/PostDetail"; import PostDetail from "@web/src/components/models/post/detail/PostDetail";
import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
export default function LetterDetailPage() { export default function LetterDetailPage() {
const { id } = useParams(); const { id } = useParams();
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return <div className="shadow-elegant border-2 border-white rounded-xl bg-slate-200"> return (
<div className="shadow-elegant border-2 border-white rounded-xl bg-slate-200">
<PostDetail id={id}></PostDetail> <PostDetail id={id}></PostDetail>
</div>; </div>
);
} }

View File

@ -10,3 +10,16 @@ export const defaultModules = {
["clean"], ["clean"],
], ],
}; };
// 如果需要自定义 tooltip
const customTooltip = {
header: {
1: "标题 1",
2: "标题 2",
3: "标题 3",
4: "标题 4",
5: "标题 5",
6: "标题 6",
false: "正文",
},
};

View File

@ -36,6 +36,8 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
const [file, setFile] = useState<UploadingFile | null>(null); const [file, setFile] = useState<UploadingFile | null>(null);
const avatarRef = useRef<HTMLImageElement>(null); const avatarRef = useRef<HTMLImageElement>(null);
const [previewUrl, setPreviewUrl] = useState<string>(value || ""); const [previewUrl, setPreviewUrl] = useState<string>(value || "");
const [compressedUrl, setCompressedUrl] = useState<string>(value || "");
const [url, setUrl] = useState<string>(value || ""); const [url, setUrl] = useState<string>(value || "");
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -46,7 +48,9 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0]; const selectedFile = event.target.files?.[0];
if (!selectedFile) return; if (!selectedFile) return;
// Create an object URL for the selected file
const objectUrl = URL.createObjectURL(selectedFile);
setPreviewUrl(objectUrl);
setFile({ setFile({
name: selectedFile.name, name: selectedFile.name,
progress: 0, progress: 0,
@ -70,7 +74,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
})); }));
setUrl(result.url); setUrl(result.url);
setPreviewUrl(result.compressedUrl); setCompressedUrl(result.compressedUrl);
// 直接使用 result 中的最新值 // 直接使用 result 中的最新值
resolve(compressed ? result.compressedUrl : result.url); resolve(compressed ? result.compressedUrl : result.url);
}, },
@ -80,6 +84,9 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
file?.fileKey file?.fileKey
); );
}); });
// await new Promise((resolve) => setTimeout(resolve,4999)); // 方法1使用 await 暂停执行
// 使用 resolved 的最新值调用 onChange
// 强制刷新 Avatar 组件
setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载 setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载
onChange?.(uploadedUrl); onChange?.(uploadedUrl);
console.log(uploadedUrl) console.log(uploadedUrl)

View File

@ -47,7 +47,6 @@ export default function StaffForm() {
rank, rank,
office, office,
} = values; } = values;
console.log("photoUrl", photoUrl);
setFormLoading(true); setFormLoading(true);
try { try {
if (data && user?.id) { if (data && user?.id) {
@ -185,13 +184,13 @@ export default function StaffForm() {
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
rules={[ // rules={[
{ // {
required: false, // required: false,
pattern: /^\d{5,18}$/, // pattern: /^\d{5,18}$/,
message: "请输入正确的证件号(数字)", // message: "请输入正确的证件号(数字)",
}, // },
]} // ]}
noStyle noStyle
name={"officerId"}> name={"officerId"}>
<Input <Input
@ -229,7 +228,7 @@ export default function StaffForm() {
</Form.Item> </Form.Item>
<Form.Item noStyle name={"office"}> <Form.Item noStyle name={"office"}>
<Input <Input
placeholder="请输入办公地点(可选)" placeholder="请输入办公地点(可选)"
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
allowClear allowClear

View File

@ -48,6 +48,7 @@ export default function PostCommentEditor() {
toast.success("发布成功!"); toast.success("发布成功!");
setContent(""); setContent("");
setFileIds([]); // 重置上传组件状态 setFileIds([]); // 重置上传组件状态
setSignature("");
setUploaderKey(uploaderKey + 1); setUploaderKey(uploaderKey + 1);
} catch (error) { } catch (error) {
toast.error("发布失败,请稍后重试"); toast.error("发布失败,请稍后重试");
@ -91,7 +92,6 @@ export default function PostCommentEditor() {
key={uploaderKey} key={uploaderKey}
value={fileIds} value={fileIds}
onChange={(value) => { onChange={(value) => {
console.log("ids", value);
setFileIds(value); setFileIds(value);
}} }}
/> />

View File

@ -45,6 +45,7 @@ export function LetterFormProvider({
const onSubmit = async (data: LetterFormData) => { const onSubmit = async (data: LetterFormData) => {
try { try {
console.log(data);
const receivers = data?.receivers; const receivers = data?.receivers;
const terms = data?.terms; const terms = data?.terms;
delete data.receivers; delete data.receivers;
@ -78,14 +79,14 @@ export function LetterFormProvider({
: undefined, : undefined,
}, },
}); });
const formattedDateTime = dayjs().format('YYYY-MM-DD HH:mm:ss') const formattedDateTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
// 创建包含信件编号和提交时间的文本 // 创建包含信件编号和提交时间的文本
const fileContent = `信件编号: ${result.id}\n投递时间: ${formattedDateTime}`; const fileContent = `信件编号: ${result.id}\n投递时间: ${formattedDateTime}`;
// 创建包含信件编号和提交时间的Blob对象 // 创建包含信件编号和提交时间的Blob对象
const blob = new Blob([fileContent], { type: 'text/plain' }); const blob = new Blob([fileContent], { type: "text/plain" });
// 创建下载链接 // 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob); const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement("a");
link.href = downloadUrl; link.href = downloadUrl;
link.download = `信件编号-${result.id}.txt`; // 设置下载文件名 link.download = `信件编号-${result.id}.txt`; // 设置下载文件名
document.body.appendChild(link); document.body.appendChild(link);
@ -93,10 +94,16 @@ export function LetterFormProvider({
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl);
toast.success(`信件投递成功!信件编号已保存到本地,请妥善保管用于进度查询`, { toast.success(
duration: 5000 // 10秒 `信件投递成功!信件编号已保存到本地,请妥善保管用于进度查询`,
}); {
navigate(`/${result.id}/detail`, { replace: true }); duration: 5000, // 10秒
}
);
// navigate(`/${result.id}/detail`, {
// replace: true,
// state: { scrollToTop: true },
// });
form.resetFields(); form.resetFields();
} catch (error) { } catch (error) {
console.error("Error submitting form:", error); console.error("Error submitting form:", error);

View File

@ -6,12 +6,39 @@ import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import StaffSelect from "../../../staff/staff-select"; import StaffSelect from "../../../staff/staff-select";
import TermSelect from "../../../term/term-select"; import TermSelect from "../../../term/term-select";
import TabPane from "antd/es/tabs/TabPane"; import TabPane from "antd/es/tabs/TabPane";
import toast from "react-hot-toast";
export function LetterBasicForm() { export function LetterBasicForm() {
const { onSubmit, receiverId, termId, form } = useLetterEditor(); const { onSubmit, receiverId, termId, form } = useLetterEditor();
const handleFinish = async (values: any) => { const handleFinish = async (values: any) => {
await onSubmit(values); await onSubmit(values);
}; };
const handleSubmit = async () => {
try {
await form.validateFields();
form.submit();
// toast.success("提交成功!");
} catch (error) {
// 提取所有错误信息
const errorMessages = (error as any).errorFields
.map((field) => field.errors[0])
.filter(Boolean);
// 显示 toast 错误提示
toast.error(
<div className="flex flex-col gap-1">
<b></b>
{errorMessages.map((msg, i) => (
<span key={i}>· {msg}</span>
))}
</div>,
{
duration: 5000, // 显示 5 秒
position: "top-center",
}
);
}
};
return ( return (
<div className=" p-6 "> <div className=" p-6 ">
<Form <Form
@ -63,13 +90,15 @@ export function LetterBasicForm() {
<TabPane tab="正文" key="1"> <TabPane tab="正文" key="1">
<Form.Item <Form.Item
name="content" name="content"
rules={[{ required: true, message: "请输入内容" }]} rules={[
{ required: true, message: "请输入正文内容" },
]}
required={false}> 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">
<QuillEditor <QuillEditor
maxLength={10000} maxLength={10000}
placeholder="请输入内容" placeholder="请输入内容"
minRows={16} minRows={6}
onChange={(content) => onChange={(content) =>
form.setFieldValue("content", content) form.setFieldValue("content", content)
} }
@ -78,22 +107,30 @@ export function LetterBasicForm() {
</Form.Item> </Form.Item>
</TabPane> </TabPane>
<TabPane tab="附件" key="2"> <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"> <Form.Item name="resources" required={false}>
<TusUploader <TusUploader
onChange={async (resources) => { onChange={async (resources) => {
// console.log(resources);
form.setFieldValue( form.setFieldValue(
"resources", "resources",
resources resources
); );
// console.log(
// form.getFieldValue("resources")
// );
}} }}
/> />
</div> </Form.Item>
</Form.Item> </div>
</TabPane> </TabPane>
</Tabs> </Tabs>
{/* <Button
onClick={() => {
console.log(form.getFieldValue("resources"));
}}></Button> */}
{/* Footer Actions */} {/* Footer Actions */}
<div className="flex flex-col-reverse sm:flex-row items-center justify-between gap-4 "> <div className="flex flex-col-reverse sm:flex-row items-center justify-between gap-4 mt-2 ">
<Form.Item name="isPublic" valuePropName="checked"> <Form.Item name="isPublic" valuePropName="checked">
<Checkbox className="text-gray-600 hover:text-gray-900 transition-colors text-sm"> <Checkbox className="text-gray-600 hover:text-gray-900 transition-colors text-sm">
@ -112,7 +149,7 @@ export function LetterBasicForm() {
</Form.Item> </Form.Item>
<Button <Button
type="primary" type="primary"
onClick={() => form.submit()} onClick={handleSubmit}
size="large" size="large"
icon={<SendOutlined />} icon={<SendOutlined />}
className="w-full sm:w-40"> className="w-full sm:w-40">

View File

@ -204,13 +204,15 @@ export default function StaffForm() {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
noStyle noStyle
rules={[ rules={
{ [
required: false, // {
pattern: /^\d{5,18}$/, // required: false,
message: "请输入正确的证件号(数字)", // pattern: /^\d{5,18}$/,
}, // message: "请输入正确的证件号(数字)",
]} // },
]
}
name={"officerId"} name={"officerId"}
label="证件号"> label="证件号">
<Input <Input
@ -248,7 +250,7 @@ export default function StaffForm() {
</Form.Item> </Form.Item>
<Form.Item noStyle name={"office"}> <Form.Item noStyle name={"office"}>
<Input <Input
placeholder="请输入办公地点(可选)" placeholder="请输入办公地点(可选)"
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
allowClear allowClear

View File

@ -13,7 +13,7 @@
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
border: none; border: none;
@apply text-base @apply text-base;
} }
.ag-custom-dragging-class { .ag-custom-dragging-class {
@ -41,8 +41,6 @@
/* 垂直居中 */ /* 垂直居中 */
} }
/* 滚动条轨道 */ /* 滚动条轨道 */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
border-radius: 10px; border-radius: 10px;
@ -69,7 +67,9 @@
} }
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/ /* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before { .ant-radio-button-wrapper-checked:not(
.ant-radio-button-wrapper-disabled
)::before {
background-color: unset !important; background-color: unset !important;
} }
@ -82,6 +82,38 @@
display: none !important; display: none !important;
} }
.no-wrap-header .ant-table-thead>tr>th { .no-wrap-header .ant-table-thead > tr > th {
white-space: nowrap; white-space: nowrap;
} }
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题 1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题 2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题 3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题 4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题 5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题 6";
}
/* 针对下拉菜单中的选项 */
.ql-snow .ql-picker.ql-header .ql-picker-item:not([data-value])::before,
.ql-snow .ql-picker.ql-header .ql-picker-label:not([data-value])::before {
content: "正文" !important;
}

View File

@ -59,6 +59,8 @@ server {
# 文件上传处理位置 # 文件上传处理位置
location /uploads/ { location /uploads/ {
# 文件实际存储路径 # 文件实际存储路径
alias /data/uploads/; alias /data/uploads/;
# 文件传输性能优化 # 文件传输性能优化