add 20250126-1558
This commit is contained in:
parent
11a1a7610b
commit
e8b23533d7
|
@ -169,12 +169,16 @@ export class AuthService {
|
|||
password,
|
||||
officerId,
|
||||
showname,
|
||||
department: {
|
||||
connect: { id: deptId },
|
||||
},
|
||||
domain: {
|
||||
connect: { id: deptId },
|
||||
},
|
||||
department: deptId
|
||||
? {
|
||||
connect: { id: deptId },
|
||||
}
|
||||
: undefined,
|
||||
domain: deptId
|
||||
? {
|
||||
connect: { id: deptId },
|
||||
}
|
||||
: undefined,
|
||||
// domainId: data.deptId,
|
||||
meta: {
|
||||
photoUrl,
|
||||
|
|
|
@ -31,7 +31,6 @@ export async function setPostRelation(params: {
|
|||
visitorId: staff?.id,
|
||||
},
|
||||
})) > 0;
|
||||
|
||||
const liked =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
|
@ -50,6 +49,24 @@ export async function setPostRelation(params: {
|
|||
}),
|
||||
},
|
||||
})) > 0;
|
||||
const hated =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
type: VisitType?.HATE,
|
||||
...(staff?.id
|
||||
? // 如果有 staff,查找对应的 visitorId
|
||||
{ visitorId: staff.id }
|
||||
: // 如果没有 staff,查找相同 IP 且 visitorId 为 null 且 30 分钟内的记录
|
||||
{
|
||||
visitorId: null,
|
||||
meta: { path: ['ip'], equals: clientIp },
|
||||
updatedAt: {
|
||||
gte: thirtyMinutesAgo,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})) > 0;
|
||||
const readedCount = await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
|
@ -61,6 +78,7 @@ export async function setPostRelation(params: {
|
|||
readed,
|
||||
readedCount,
|
||||
liked,
|
||||
hated,
|
||||
commentsCount,
|
||||
});
|
||||
// console.log('data', data);
|
||||
|
|
|
@ -78,6 +78,13 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
visitType: VisitType.LIKE,
|
||||
});
|
||||
}
|
||||
if (args.data.type === VisitType.HATE) {
|
||||
EventBus.emit('updateVisitCount', {
|
||||
objectType: ObjectType.POST,
|
||||
id: postId,
|
||||
visitType: VisitType.HATE,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -37,19 +37,20 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
|||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
scrollToFirstError>
|
||||
<div className=" flex items-center gap-4 mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex-1">
|
||||
<Form.Item name="photoUrl" label="头像" noStyle>
|
||||
<AvatarUploader
|
||||
className="rounded-lg"
|
||||
placeholder="点击上传头像"
|
||||
style={{
|
||||
height: 150,
|
||||
width: 120,
|
||||
}}></AvatarUploader>
|
||||
width: `100%`,
|
||||
height: 210,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 flex-1">
|
||||
<div className="flex-1 grid grid-cols-1 gap-3">
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
|
@ -88,108 +89,108 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
|||
noStyle
|
||||
label="部门"
|
||||
rules={[{ required: true, message: "请选择部门" }]}>
|
||||
<DepartmentSelect
|
||||
rootId={domainId}></DepartmentSelect>
|
||||
<DepartmentSelect rootId={domainId} />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"rank"}>
|
||||
<Input
|
||||
placeholder="请输入职级(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 flex-1 mb-2">
|
||||
<Form.Item noStyle name={"rank"}>
|
||||
<Input
|
||||
placeholder="请输入职级(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="officerId"
|
||||
label="证件号"
|
||||
noStyle
|
||||
rules={[
|
||||
{ required: true, message: "请输入证件号" },
|
||||
{
|
||||
pattern: /^\d{5,12}$/,
|
||||
message: "请输入有效的证件号(5-12位数字)",
|
||||
},
|
||||
]}>
|
||||
<Input placeholder="证件号(可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
pattern: /^\d{6,11}$/,
|
||||
message: "请输入正确的手机号(数字)",
|
||||
},
|
||||
]}
|
||||
name={"phoneNumber"}
|
||||
label="手机号">
|
||||
<Input
|
||||
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
placeholder="请输入手机号(可选)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"email"}>
|
||||
<Input
|
||||
placeholder="请输入邮箱(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"office"}>
|
||||
<Input
|
||||
placeholder="请输入办公室地点(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
noStyle
|
||||
rules={[
|
||||
{ required: true, message: "请输入密码" },
|
||||
{ min: 8, message: "密码至少需要8个字符" },
|
||||
{
|
||||
pattern:
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
||||
message:
|
||||
"密码必须包含大小写字母、数字和特殊字符",
|
||||
},
|
||||
]}>
|
||||
<Input.Password placeholder="密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="repeatPass"
|
||||
label="确认密码"
|
||||
noStyle
|
||||
dependencies={["password"]}
|
||||
rules={[
|
||||
{ required: true, message: "请确认密码" },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue("password") === value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error("两次输入的密码不一致")
|
||||
);
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 grid grid-cols-1 gap-2">
|
||||
<Form.Item
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
pattern: /^\d{6,11}$/,
|
||||
message: "请输入正确的手机号(数字)",
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<Input.Password placeholder="确认密码" />
|
||||
</Form.Item>
|
||||
]}
|
||||
name={"phoneNumber"}
|
||||
label="手机号">
|
||||
<Input
|
||||
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
placeholder="请输入手机号(可选)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"office"}>
|
||||
<Input
|
||||
placeholder="请输入办公室地点"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: "请输入密码" },
|
||||
{ min: 8, message: "密码至少需要8个字符" },
|
||||
{
|
||||
pattern:
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
||||
message:
|
||||
"密码必须包含大小写字母、数字和特殊字符",
|
||||
},
|
||||
]}>
|
||||
<Input.Password placeholder="密码" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="flex-1 grid grid-cols-1 gap-2">
|
||||
<Form.Item
|
||||
name="officerId"
|
||||
label="证件号"
|
||||
noStyle
|
||||
rules={[
|
||||
{ required: true, message: "请输入证件号" },
|
||||
{
|
||||
pattern: /^\d{5,12}$/,
|
||||
message: "请输入有效的证件号(5-12位数字)",
|
||||
},
|
||||
]}>
|
||||
<Input placeholder="证件号(可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name={"email"}>
|
||||
<Input
|
||||
placeholder="请输入邮箱(可选)"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="repeatPass"
|
||||
dependencies={["password"]}
|
||||
rules={[
|
||||
{ required: true, message: "请确认密码" },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue("password") === value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error("两次输入的密码不一致")
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<Input.Password placeholder="确认密码" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 flex-1 my-2"></div>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {
|
||||
EyeOutlined,
|
||||
LikeOutlined,
|
||||
LikeFilled,
|
||||
UserOutlined,
|
||||
BankOutlined,
|
||||
CalendarOutlined,
|
||||
FileTextOutlined,
|
||||
SendOutlined,
|
||||
MailOutlined,
|
||||
EyeOutlined,
|
||||
LikeOutlined,
|
||||
LikeFilled,
|
||||
UserOutlined,
|
||||
BankOutlined,
|
||||
CalendarOutlined,
|
||||
FileTextOutlined,
|
||||
SendOutlined,
|
||||
MailOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Typography, Space, Tooltip } from "antd";
|
||||
import { PostDto, PostStateLabels } from "@nice/common";
|
||||
|
@ -17,100 +17,109 @@ import { LetterBadge } from "./LetterBadge";
|
|||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface LetterCardProps {
|
||||
letter: PostDto;
|
||||
letter: PostDto;
|
||||
}
|
||||
|
||||
export function LetterCard({ letter }: LetterCardProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(`/${letter.id}/detail`)
|
||||
}}
|
||||
className="cursor-pointer p-6 bg-slate-100/80 rounded-xl hover:ring-white hover:ring-1 transition-all
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(`/${letter.id}/detail`);
|
||||
}}
|
||||
className="cursor-pointer p-6 bg-slate-100/80 rounded-xl hover:ring-white hover:ring-1 transition-all
|
||||
duration-300 ease-in-out hover:-translate-y-0.5
|
||||
active:scale-[0.98] border border-white
|
||||
group relative overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className=" text-xl text-primary font-bold">
|
||||
{letter.title}
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-gray-600 gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-[300px]">
|
||||
{letter.author?.department?.name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<BankOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-600 font-medium">
|
||||
{letter.author?.department?.name}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<UserOutlined className="text-primary text-base" />
|
||||
<Text className="text-primary font-medium">
|
||||
{letter.author?.showname || '匿名用户'}
|
||||
</Text>
|
||||
</div>
|
||||
group relative overflow-hidden">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className=" text-xl text-primary font-bold">
|
||||
{letter.title}
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-gray-600 gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-[300px]">
|
||||
{letter.author?.department?.name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<BankOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-600 font-medium">
|
||||
{letter.author?.department?.name}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<UserOutlined className="text-primary text-base" />
|
||||
<Text className="text-primary font-medium">
|
||||
{letter.author?.showname || "匿名用户"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{letter.receivers.some(item => item.showname) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<MailOutlined className="text-primary-400 text-base" />
|
||||
<Tooltip title={letter.receivers.map(item => item.showname).filter(Boolean).join(', ')}>
|
||||
<Text className="text-primary-400">
|
||||
{letter.receivers
|
||||
.map(item => item.showname)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join('、')}
|
||||
{letter.receivers.filter(item => item.showname).length > 2 && ' 等'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-500">
|
||||
{dayjs(letter.createdAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{letter.receivers.some((item) => item.showname) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<MailOutlined className="text-primary-400 text-base" />
|
||||
<Tooltip
|
||||
title={letter?.receivers
|
||||
?.map((item) => item.showname)
|
||||
.filter(Boolean)
|
||||
.join(", ")}>
|
||||
<Text className="text-primary-400">
|
||||
{letter.receivers
|
||||
.map((item) => item.showname)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join("、")}
|
||||
{letter.receivers.filter(
|
||||
(item) => item.showname
|
||||
).length > 2 && " 等"}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-500">
|
||||
{dayjs(letter.createdAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Preview */}
|
||||
{letter.content && (
|
||||
<div className="flex-1 leading-relaxed text-sm">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: letter.content }}
|
||||
className="line-clamp-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Content Preview */}
|
||||
{letter.content && (
|
||||
<div className="flex-1 leading-relaxed text-sm">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: letter.content }}
|
||||
className="line-clamp-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center ">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LetterBadge type="state" value={letter.state} />
|
||||
{letter.meta.tags.map(tag => (
|
||||
<LetterBadge key={tag} type="tag" value={tag} />
|
||||
))}
|
||||
{letter.terms.map(term => (
|
||||
<LetterBadge key={term.name} type="category" value={term.name} />
|
||||
))}
|
||||
</div>
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center ">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LetterBadge type="state" value={letter.state} />
|
||||
{letter?.meta?.tags?.map((tag) => (
|
||||
<LetterBadge key={tag} type="tag" value={tag} />
|
||||
))}
|
||||
{letter.terms.map((term) => (
|
||||
<LetterBadge
|
||||
key={term.name}
|
||||
type="category"
|
||||
value={term.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="default"
|
||||
shape="round"
|
||||
icon={<EyeOutlined />}
|
||||
>
|
||||
<span className="mr-1">浏览量</span>{letter.views}
|
||||
</Button>
|
||||
<PostLikeButton post={letter as any}></PostLikeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="default"
|
||||
shape="round"
|
||||
icon={<EyeOutlined />}>
|
||||
<span className="mr-1">浏览量</span>
|
||||
{letter.views}
|
||||
</Button>
|
||||
<PostLikeButton post={letter as any}></PostLikeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ export default function PostCommentList() {
|
|||
|
||||
return (
|
||||
<div className=" space-y-2 p-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{items.map((comment, index) => (
|
||||
<motion.div
|
||||
key={comment.id}
|
||||
|
|
|
@ -118,6 +118,21 @@ export function useVisitor() {
|
|||
}))
|
||||
);
|
||||
|
||||
const hate = api.visitor.create.useMutation(
|
||||
createOptimisticMutation((item) => ({
|
||||
...item,
|
||||
hates: (item.hates || 0) + 1,
|
||||
hated: true,
|
||||
}))
|
||||
);
|
||||
const unHate = api.visitor.deleteMany.useMutation(
|
||||
createOptimisticMutation((item) => ({
|
||||
...item,
|
||||
hates: item.hates - 1 || 0,
|
||||
hated: false,
|
||||
}))
|
||||
);
|
||||
|
||||
const addStar = api.visitor.create.useMutation(
|
||||
createOptimisticMutation((item) => ({
|
||||
...item,
|
||||
|
|
|
@ -75,7 +75,7 @@ model Staff {
|
|||
username String @unique @map("username")
|
||||
avatar String? @map("avatar")
|
||||
password String? @map("password")
|
||||
|
||||
|
||||
phoneNumber String? @unique @map("phone_number")
|
||||
|
||||
|
||||
|
@ -206,6 +206,7 @@ model Post {
|
|||
visits Visit[] // 访问记录,关联 Visit 模型
|
||||
views Int @default(0)
|
||||
likes Int @default(0)
|
||||
hates Int @default(0)
|
||||
receivers Staff[] @relation("post_receiver")
|
||||
parentId String? @map("parent_id")
|
||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum VisitType {
|
|||
STAR = "star",
|
||||
READED = "read",
|
||||
LIKE = "like",
|
||||
HATE = "hate",
|
||||
}
|
||||
|
||||
export enum StorageProvider {
|
||||
|
|
Loading…
Reference in New Issue