Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
003208a5f5
|
@ -169,12 +169,16 @@ export class AuthService {
|
||||||
password,
|
password,
|
||||||
officerId,
|
officerId,
|
||||||
showname,
|
showname,
|
||||||
department: deptId && {
|
department: deptId
|
||||||
|
? {
|
||||||
connect: { id: deptId },
|
connect: { id: deptId },
|
||||||
},
|
}
|
||||||
domain: {
|
: undefined,
|
||||||
connect: deptId && { id: deptId },
|
domain: deptId
|
||||||
},
|
? {
|
||||||
|
connect: { id: deptId },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
// domainId: data.deptId,
|
// domainId: data.deptId,
|
||||||
meta: {
|
meta: {
|
||||||
photoUrl,
|
photoUrl,
|
||||||
|
|
|
@ -31,7 +31,6 @@ export async function setPostRelation(params: {
|
||||||
visitorId: staff?.id,
|
visitorId: staff?.id,
|
||||||
},
|
},
|
||||||
})) > 0;
|
})) > 0;
|
||||||
|
|
||||||
const liked =
|
const liked =
|
||||||
(await db.visit.count({
|
(await db.visit.count({
|
||||||
where: {
|
where: {
|
||||||
|
@ -50,6 +49,24 @@ export async function setPostRelation(params: {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
})) > 0;
|
})) > 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({
|
const readedCount = await db.visit.count({
|
||||||
where: {
|
where: {
|
||||||
postId: data.id,
|
postId: data.id,
|
||||||
|
@ -61,6 +78,7 @@ export async function setPostRelation(params: {
|
||||||
readed,
|
readed,
|
||||||
readedCount,
|
readedCount,
|
||||||
liked,
|
liked,
|
||||||
|
hated,
|
||||||
commentsCount,
|
commentsCount,
|
||||||
});
|
});
|
||||||
// console.log('data', data);
|
// console.log('data', data);
|
||||||
|
|
|
@ -78,6 +78,13 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
visitType: VisitType.LIKE,
|
visitType: VisitType.LIKE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (args.data.type === VisitType.HATE) {
|
||||||
|
EventBus.emit('updateVisitCount', {
|
||||||
|
objectType: ObjectType.POST,
|
||||||
|
id: postId,
|
||||||
|
visitType: VisitType.HATE,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,19 +37,20 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={onSubmit}
|
onFinish={onSubmit}
|
||||||
scrollToFirstError>
|
scrollToFirstError>
|
||||||
<div className=" flex items-center gap-4 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<Form.Item name="photoUrl" label="头像" noStyle>
|
<Form.Item name="photoUrl" label="头像" noStyle>
|
||||||
<AvatarUploader
|
<AvatarUploader
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
placeholder="点击上传头像"
|
placeholder="点击上传头像"
|
||||||
style={{
|
style={{
|
||||||
height: 150,
|
width: `100%`,
|
||||||
width: 120,
|
height: 210,
|
||||||
}}></AvatarUploader>
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-2 flex-1">
|
<div className="flex-1 grid grid-cols-1 gap-3">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
label="用户名"
|
label="用户名"
|
||||||
|
@ -88,12 +89,8 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
noStyle
|
noStyle
|
||||||
label="部门"
|
label="部门"
|
||||||
rules={[{ required: true, message: "请选择部门" }]}>
|
rules={[{ required: true, message: "请选择部门" }]}>
|
||||||
<DepartmentSelect
|
<DepartmentSelect rootId={domainId} />
|
||||||
rootId={domainId}></DepartmentSelect>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-2 flex-1 mb-2">
|
|
||||||
<Form.Item noStyle name={"rank"}>
|
<Form.Item noStyle name={"rank"}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入职级(可选)"
|
placeholder="请输入职级(可选)"
|
||||||
|
@ -102,19 +99,10 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
</div>
|
||||||
name="officerId"
|
</div>
|
||||||
label="证件号"
|
<div className="flex items-center gap-2">
|
||||||
noStyle
|
<div className="flex-1 grid grid-cols-1 gap-2">
|
||||||
rules={[
|
|
||||||
{ required: true, message: "请输入证件号" },
|
|
||||||
{
|
|
||||||
pattern: /^\d{5,12}$/,
|
|
||||||
message: "请输入有效的证件号(5-12位数字)",
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Input placeholder="证件号(可选)" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
noStyle
|
noStyle
|
||||||
rules={[
|
rules={[
|
||||||
|
@ -133,17 +121,9 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
placeholder="请输入手机号(可选)"
|
placeholder="请输入手机号(可选)"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item noStyle name={"email"}>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入邮箱(可选)"
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</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,8 +131,6 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
label="密码"
|
|
||||||
noStyle
|
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入密码" },
|
{ required: true, message: "请输入密码" },
|
||||||
{ min: 8, message: "密码至少需要8个字符" },
|
{ min: 8, message: "密码至少需要8个字符" },
|
||||||
|
@ -165,11 +143,31 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
]}>
|
]}>
|
||||||
<Input.Password placeholder="密码" />
|
<Input.Password placeholder="密码" />
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
name="repeatPass"
|
name="repeatPass"
|
||||||
label="确认密码"
|
|
||||||
noStyle
|
|
||||||
dependencies={["password"]}
|
dependencies={["password"]}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请确认密码" },
|
{ required: true, message: "请确认密码" },
|
||||||
|
@ -190,6 +188,9 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
<Input.Password placeholder="确认密码" />
|
<Input.Password placeholder="确认密码" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 flex-1 my-2"></div>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
|
@ -24,13 +24,12 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/${letter.id}/detail`)
|
window.open(`/${letter.id}/detail`);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer p-6 bg-slate-100/80 rounded-xl hover:ring-white hover:ring-1 transition-all
|
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
|
duration-300 ease-in-out hover:-translate-y-0.5
|
||||||
active:scale-[0.98] border border-white
|
active:scale-[0.98] border border-white
|
||||||
group relative overflow-hidden"
|
group relative overflow-hidden">
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className=" text-xl text-primary font-bold">
|
<div className=" text-xl text-primary font-bold">
|
||||||
{letter.title}
|
{letter.title}
|
||||||
|
@ -49,21 +48,27 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserOutlined className="text-primary text-base" />
|
<UserOutlined className="text-primary text-base" />
|
||||||
<Text className="text-primary font-medium">
|
<Text className="text-primary font-medium">
|
||||||
{letter.author?.showname || '匿名用户'}
|
{letter.author?.showname || "匿名用户"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{letter.receivers.some(item => item.showname) && (
|
{letter.receivers.some((item) => item.showname) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MailOutlined className="text-primary-400 text-base" />
|
<MailOutlined className="text-primary-400 text-base" />
|
||||||
<Tooltip title={letter.receivers.map(item => item.showname).filter(Boolean).join(', ')}>
|
<Tooltip
|
||||||
|
title={letter?.receivers
|
||||||
|
?.map((item) => item.showname)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ")}>
|
||||||
<Text className="text-primary-400">
|
<Text className="text-primary-400">
|
||||||
{letter.receivers
|
{letter.receivers
|
||||||
.map(item => item.showname)
|
.map((item) => item.showname)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.join('、')}
|
.join("、")}
|
||||||
{letter.receivers.filter(item => item.showname).length > 2 && ' 等'}
|
{letter.receivers.filter(
|
||||||
|
(item) => item.showname
|
||||||
|
).length > 2 && " 等"}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,11 +96,15 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<div className="flex justify-between items-center ">
|
<div className="flex justify-between items-center ">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<LetterBadge type="state" value={letter.state} />
|
<LetterBadge type="state" value={letter.state} />
|
||||||
{letter.meta.tags.map(tag => (
|
{letter?.meta?.tags?.map((tag) => (
|
||||||
<LetterBadge key={tag} type="tag" value={tag} />
|
<LetterBadge key={tag} type="tag" value={tag} />
|
||||||
))}
|
))}
|
||||||
{letter.terms.map(term => (
|
{letter.terms.map((term) => (
|
||||||
<LetterBadge key={term.name} type="category" value={term.name} />
|
<LetterBadge
|
||||||
|
key={term.name}
|
||||||
|
type="category"
|
||||||
|
value={term.name}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -103,9 +112,9 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
shape="round"
|
shape="round"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}>
|
||||||
>
|
<span className="mr-1">浏览量</span>
|
||||||
<span className="mr-1">浏览量</span>{letter.views}
|
{letter.views}
|
||||||
</Button>
|
</Button>
|
||||||
<PostLikeButton post={letter as any}></PostLikeButton>
|
<PostLikeButton post={letter as any}></PostLikeButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -137,7 +137,7 @@ export default function PostCommentList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -177,11 +177,9 @@ export default function PostCommentList() {
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="flex flex-col items-center py-4 space-y-2">
|
className="flex flex-col items-center py-4 space-y-2">
|
||||||
|
|
||||||
<span className="text-sm text-gray-500 font-medium">
|
<span className="text-sm text-gray-500 font-medium">
|
||||||
已加载全部回复
|
已加载全部回复
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,7 +8,10 @@ import { formatFileSize } from '@nice/utils';
|
||||||
export default function PostResources({ post }: { post: PostDto }) {
|
export default function PostResources({ post }: { post: PostDto }) {
|
||||||
const { resources } = useMemo(() => {
|
const { resources } = useMemo(() => {
|
||||||
if (!post?.resources) return { resources: [] };
|
if (!post?.resources) return { resources: [] };
|
||||||
const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
|
||||||
|
const isImage = (url: string) =>
|
||||||
|
/\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
||||||
|
|
||||||
const sortedResources = post.resources
|
const sortedResources = post.resources
|
||||||
.map((resource) => ({
|
.map((resource) => ({
|
||||||
...resource,
|
...resource,
|
||||||
|
@ -36,8 +39,7 @@ export default function PostResources({ post }: { post: PostDto }) {
|
||||||
md={6}
|
md={6}
|
||||||
lg={6}
|
lg={6}
|
||||||
xl={4}
|
xl={4}
|
||||||
className="relative"
|
className="relative">
|
||||||
>
|
|
||||||
<div className="relative aspect-square rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 bg-gray-100">
|
<div className="relative aspect-square rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 bg-gray-100">
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<Image
|
<Image
|
||||||
|
@ -78,17 +80,22 @@ export default function PostResources({ post }: { post: PostDto }) {
|
||||||
{fileResources.map((resource) => (
|
{fileResources.map((resource) => (
|
||||||
<div
|
<div
|
||||||
key={resource.url}
|
key={resource.url}
|
||||||
className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-md transition-colors duration-200"
|
className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-md transition-colors duration-200">
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3 min-w-0">
|
<div className="flex items-center space-x-3 min-w-0">
|
||||||
<span className="text-xl">{getFileIcon(resource.url)}</span>
|
<span className="text-xl">
|
||||||
|
{getFileIcon(resource.url)}
|
||||||
|
</span>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-gray-800 truncate">
|
<p className="text-gray-800 truncate">
|
||||||
{resource.title || "未命名文件"}
|
{resource.title || "未命名文件"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{resource.url.split(".").pop()?.toUpperCase()}文件
|
{resource.url
|
||||||
|
.split(".")
|
||||||
|
.pop()
|
||||||
|
?.toUpperCase()}
|
||||||
|
文件
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{resource.meta.size &&
|
{resource.meta.size &&
|
||||||
|
@ -100,8 +107,7 @@ export default function PostResources({ post }: { post: PostDto }) {
|
||||||
<Button
|
<Button
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
href={resource.url}
|
href={resource.url}
|
||||||
download
|
download>
|
||||||
>
|
|
||||||
下载
|
下载
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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(
|
const addStar = api.visitor.create.useMutation(
|
||||||
createOptimisticMutation((item) => ({
|
createOptimisticMutation((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|
|
@ -205,6 +205,7 @@ model Post {
|
||||||
visits Visit[] // 访问记录,关联 Visit 模型
|
visits Visit[] // 访问记录,关联 Visit 模型
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
likes Int @default(0)
|
likes Int @default(0)
|
||||||
|
hates Int @default(0)
|
||||||
receivers Staff[] @relation("post_receiver")
|
receivers Staff[] @relation("post_receiver")
|
||||||
parentId String? @map("parent_id")
|
parentId String? @map("parent_id")
|
||||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||||
|
|
|
@ -14,6 +14,7 @@ export enum VisitType {
|
||||||
STAR = "star",
|
STAR = "star",
|
||||||
READED = "read",
|
READED = "read",
|
||||||
LIKE = "like",
|
LIKE = "like",
|
||||||
|
HATE = "hate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StorageProvider {
|
export enum StorageProvider {
|
||||||
|
|
Loading…
Reference in New Issue