This commit is contained in:
ditiqi 2025-01-26 16:10:31 +08:00
parent 1266d076b1
commit ed4b328047
17 changed files with 204 additions and 116 deletions

View File

@ -4,44 +4,48 @@ import { AppConfigService } from './app-config.service';
import { z, ZodType } from 'zod';
import { Prisma } from '@nice/common';
import { RealtimeServer } from '@server/socket/realtime/realtime.server';
const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> = z.any()
const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any()
const AppConfigDeleteManyArgsSchema: ZodType<Prisma.AppConfigDeleteManyArgs> = z.any()
const AppConfigFindFirstArgsSchema: ZodType<Prisma.AppConfigFindFirstArgs> = z.any()
const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> =
z.any();
const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any();
const AppConfigDeleteManyArgsSchema: ZodType<Prisma.AppConfigDeleteManyArgs> =
z.any();
const AppConfigFindFirstArgsSchema: ZodType<Prisma.AppConfigFindFirstArgs> =
z.any();
@Injectable()
export class AppConfigRouter {
constructor(
private readonly trpc: TrpcService,
private readonly appConfigService: AppConfigService,
private readonly realtimeServer: RealtimeServer
) { }
router = this.trpc.router({
create: this.trpc.protectProcedure
.input(AppConfigUncheckedCreateInputSchema)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.appConfigService.create({ data: input });
}),
update: this.trpc.protectProcedure
.input(AppConfigUpdateArgsSchema)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.appConfigService.update(input);
}),
deleteMany: this.trpc.protectProcedure.input(AppConfigDeleteManyArgsSchema).mutation(async ({ input }) => {
return await this.appConfigService.deleteMany(input)
}),
findFirst: this.trpc.protectProcedure.input(AppConfigFindFirstArgsSchema).
query(async ({ input }) => {
return await this.appConfigService.findFirst(input)
}),
clearRowCache: this.trpc.protectProcedure.mutation(async () => {
return await this.appConfigService.clearRowCache()
}),
getClientCount: this.trpc.protectProcedure.query(() => {
return this.realtimeServer.getClientCount()
})
});
constructor(
private readonly trpc: TrpcService,
private readonly appConfigService: AppConfigService,
private readonly realtimeServer: RealtimeServer,
) {}
router = this.trpc.router({
create: this.trpc.protectProcedure
.input(AppConfigUncheckedCreateInputSchema)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.appConfigService.create({ data: input });
}),
update: this.trpc.protectProcedure
.input(AppConfigUpdateArgsSchema)
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.appConfigService.update(input);
}),
deleteMany: this.trpc.protectProcedure
.input(AppConfigDeleteManyArgsSchema)
.mutation(async ({ input }) => {
return await this.appConfigService.deleteMany(input);
}),
findFirst: this.trpc.protectProcedure
.input(AppConfigFindFirstArgsSchema)
.query(async ({ input }) => {
return await this.appConfigService.findFirst(input);
}),
clearRowCache: this.trpc.protectProcedure.mutation(async () => {
return await this.appConfigService.clearRowCache();
}),
getClientCount: this.trpc.protectProcedure.query(() => {
return this.realtimeServer.getClientCount();
}),
});
}

View File

@ -1,10 +1,5 @@
import { Injectable } from '@nestjs/common';
import {
db,
ObjectType,
Prisma,
} from '@nice/common';
import { db, ObjectType, Prisma } from '@nice/common';
import { BaseService } from '../base/base.service';
import { deleteByPattern } from '@server/utils/redis/utils';
@ -12,10 +7,10 @@ import { deleteByPattern } from '@server/utils/redis/utils';
@Injectable()
export class AppConfigService extends BaseService<Prisma.AppConfigDelegate> {
constructor() {
super(db, "appConfig");
super(db, 'appConfig');
}
async clearRowCache() {
await deleteByPattern("row-*")
return true
await deleteByPattern('row-*');
return true;
}
}

View File

@ -187,6 +187,13 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
visitType: VisitType.LIKE,
});
}
if (args.where.type === VisitType.HATE) {
EventBus.emit('updateVisitCount', {
objectType: ObjectType.POST,
id: args?.where?.postId as string,
visitType: VisitType.HATE,
});
}
}
return superDetele;
}

View File

@ -1,5 +1,6 @@
import { db, PostState, PostType, VisitType } from '@nice/common';
export async function updatePostViewCount(id: string, type: VisitType) {
console.log('updatePostViewCount', type);
const totalViews = await db.visit.aggregate({
_sum: {
views: true,
@ -19,7 +20,6 @@ export async function updatePostViewCount(id: string, type: VisitType) {
},
});
} else if (type === VisitType.LIKE) {
console.log('totalViews._sum.view', totalViews._sum.views);
await db.post.update({
where: {
id: id,
@ -28,5 +28,14 @@ export async function updatePostViewCount(id: string, type: VisitType) {
likes: totalViews._sum.views || 0, // Use 0 if no visits exist
},
});
} else if (type === VisitType.HATE) {
await db.post.update({
where: {
id: id,
},
data: {
hates: totalViews._sum.views || 0, // Use 0 if no visits exist
},
});
}
}

View File

@ -8,7 +8,6 @@ import { updatePostViewCount } from '../models/post/utils';
const logger = new Logger('QueueWorker');
export default async function processJob(job: Job<any, any, QueueJobType>) {
try {
if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
await updatePostViewCount(job.data.id, job.data.type);
}

View File

@ -12,12 +12,7 @@ import {
Term,
} from '@nice/common';
import EventBus from '@server/utils/event-bus';
import {
capitalizeFirstLetter,
DevDataCounts,
getCounts,
} from './utils';
import { capitalizeFirstLetter, DevDataCounts, getCounts } from './utils';
import { StaffService } from '@server/models/staff/staff.service';
@Injectable()
export class GenDevService {
@ -26,7 +21,7 @@ export class GenDevService {
deptStaffRecord: Record<string, Staff[]> = {};
terms: Record<TaxonomySlug, Term[]> = {
[TaxonomySlug.CATEGORY]: [],
[TaxonomySlug.TAG]: []
[TaxonomySlug.TAG]: [],
};
depts: Department[] = [];
domains: Department[] = [];
@ -39,7 +34,7 @@ export class GenDevService {
private readonly departmentService: DepartmentService,
private readonly staffService: StaffService,
private readonly termService: TermService,
) { }
) {}
async genDataEvent() {
EventBus.emit('genDataEvent', { type: 'start' });
try {
@ -47,7 +42,6 @@ export class GenDevService {
await this.generateDepartments(3, 6);
await this.generateTerms(1, 3);
await this.generateStaffs(4);
} catch (err) {
this.logger.error(err);
}
@ -164,8 +158,8 @@ export class GenDevService {
showname: username,
username: username,
deptId: dept.id,
domainId: domain.id
}
domainId: domain.id,
},
});
// Update both deptStaffRecord and staffs array
this.deptStaffRecord[dept.id].push(staff);
@ -190,7 +184,7 @@ export class GenDevService {
name,
isDomain: currentDepth === 1 ? true : false,
parentId,
}
},
});
return department;
}
@ -208,7 +202,9 @@ export class GenDevService {
throw new Error(`Taxonomy with slug ${taxonomySlug} not found`);
}
this.logger.log(`Creating terms for taxonomy: ${taxonomy.name} (${taxonomy.slug})`);
this.logger.log(
`Creating terms for taxonomy: ${taxonomy.name} (${taxonomy.slug})`,
);
let counter = 1;
const createTermTree = async (
parentId: string | null,
@ -223,7 +219,7 @@ export class GenDevService {
taxonomyId: taxonomy!.id,
domainId: domain?.id,
parentId,
}
},
});
this.terms[taxonomySlug].push(newTerm);
await createTermTree(newTerm.id, currentDepth + 1);

View File

@ -1,32 +1,25 @@
import {
AppConfigSlug,
BaseSetting,
RolePerms,
} from "@nice/common";
import { AppConfigSlug, BaseSetting, RolePerms } from "@nice/common";
import { useContext, useEffect, useState } from "react";
import {
Button,
Form,
Input,
message,
theme,
} from "antd";
import { Button, Form, Input, message, theme } from "antd";
import { useAppConfig } from "@nice/client";
import { useAuth } from "@web/src/providers/auth-provider";
import { useForm } from "antd/es/form/Form";
import { api } from "@nice/client"
import { api } from "@nice/client";
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
export default function BaseSettingPage() {
const { update, baseSetting } = useAppConfig();
const utils = api.useUtils()
const [form] = useForm()
const utils = api.useUtils();
const [form] = useForm();
const { token } = theme.useToken();
const { data: clientCount } = api.app_config.getClientCount.useQuery(undefined, {
refetchInterval: 3000,
refetchIntervalInBackground: true
})
const { data: clientCount } = api.app_config.getClientCount.useQuery(
undefined,
{
refetchInterval: 3000,
refetchIntervalInBackground: true,
}
);
const [isFormChanged, setIsFormChanged] = useState(false);
const [loading, setLoading] = useState(false);
const { user, hasSomePermissions } = useAuth();
@ -34,31 +27,27 @@ export default function BaseSettingPage() {
setIsFormChanged(true);
}
function onResetClick() {
if (!form)
return
if (!form) return;
if (!baseSetting) {
form.resetFields();
} else {
form.resetFields();
form.setFieldsValue(baseSetting);
}
setIsFormChanged(false);
}
function onSaveClick() {
if (form)
form.submit();
if (form) form.submit();
}
async function onSubmit(values: BaseSetting) {
setLoading(true);
try {
await update.mutateAsync({
where: {
slug: AppConfigSlug.BASE_SETTING,
},
data: { meta: JSON.stringify(values) }
data: { meta: JSON.stringify(values) },
});
setIsFormChanged(false);
message.success("已保存");
@ -70,12 +59,11 @@ export default function BaseSettingPage() {
}
useEffect(() => {
if (baseSetting && form) {
form.setFieldsValue(baseSetting);
}
}, [baseSetting, form]);
return (
<div >
<div>
<AdminHeader>
<div className="flex items-center gap-2">
{isFormChanged &&
@ -101,7 +89,6 @@ export default function BaseSettingPage() {
!hasSomePermissions(RolePerms.MANAGE_BASE_SETTING)
}
onFinish={onSubmit}
onFieldsChange={handleFieldsChange}
layout="vertical">
{/* <div
@ -127,6 +114,17 @@ export default function BaseSettingPage() {
<Input></Input>
</Form.Item>
</div>
<div className="p-2 grid grid-cols-8 gap-2 border-b">
<Form.Item
label="网站logo"
name={["appConfig", "logo"]}>
<AvatarUploader
style={{
width: 192,
height: 108,
}}></AvatarUploader>
</Form.Item>
</div>
{/* <div
className="p-2 border-b flex items-center justify-between"
style={{
@ -171,17 +169,21 @@ export default function BaseSettingPage() {
</Button>
</div>
{<div
className="p-2 border-b text-primary flex justify-between items-center"
style={{
fontSize: token.fontSize,
fontWeight: "bold",
}}>
<span>app在线人数</span>
<div>
{clientCount && clientCount > 0 ? `${clientCount}人在线` : '无人在线'}
{
<div
className="p-2 border-b text-primary flex justify-between items-center"
style={{
fontSize: token.fontSize,
fontWeight: "bold",
}}>
<span>app在线人数</span>
<div>
{clientCount && clientCount > 0
? `${clientCount}人在线`
: "无人在线"}
</div>
</div>
</div>}
}
</div>
</div>
);

View File

@ -156,7 +156,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
message: "请输入有效的证件号5-12位数字",
},
]}>
<Input placeholder="证件号(可选)" />
<Input placeholder="证件号" />
</Form.Item>
<Form.Item noStyle name={"email"}>
<Input

View File

@ -16,6 +16,7 @@ interface UploadingFile {
progress: number;
status: "uploading" | "done" | "error";
fileId?: string;
url?: string;
fileKey?: string;
}
@ -28,6 +29,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
}) => {
const { handleFileUpload, uploadProgress } = useTusUpload();
const [file, setFile] = useState<UploadingFile | null>(null);
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
const [uploading, setUploading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
@ -56,7 +58,9 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
progress: 100,
status: "done",
fileId: result.fileId,
url: result?.url,
}));
setPreviewUrl(result?.url);
resolve(result.fileId);
},
(error) => {
@ -65,7 +69,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
file?.fileKey
);
});
setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`);
setPreviewUrl(`http://${env.SERVER_IP}/uploads/${fileId}`);
onChange?.(fileId);
message.success("头像上传成功");
} catch (error) {
@ -90,6 +94,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
background: token.colorBgContainer,
...style, // 应用外部传入的样式
}}>
<div>{previewUrl}</div>
<input
type="file"
ref={inputRef}

View File

@ -14,6 +14,7 @@ import { PostDto, PostStateLabels } from "@nice/common";
import dayjs from "dayjs";
import PostLikeButton from "./detail/PostHeader/PostLikeButton";
import { LetterBadge } from "./LetterBadge";
import PostHateButton from "./detail/PostHeader/PostHateButton";
const { Title, Paragraph, Text } = Typography;
interface LetterCardProps {
@ -117,6 +118,7 @@ export function LetterCard({ letter }: LetterCardProps) {
{letter.views}
</Button>
<PostLikeButton post={letter as any}></PostLikeButton>
<PostHateButton post={letter as any}></PostHateButton>
</div>
</div>
</div>

View File

@ -6,10 +6,16 @@ import { Avatar } from "antd";
import { useVisitor } from "@nice/client";
import { useContext, useEffect, useRef, useState } from "react";
import { PostDetailContext } from "./context/PostDetailContext";
import { CheckCircleOutlined, CheckOutlined, LikeFilled, LikeOutlined } from "@ant-design/icons";
import {
CheckCircleOutlined,
CheckOutlined,
LikeFilled,
LikeOutlined,
} from "@ant-design/icons";
import PostLikeButton from "./PostHeader/PostLikeButton";
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
import PostResources from "./PostResources";
import PostHateButton from "./PostHeader/PostHateButton";
export default function PostCommentCard({
post,
@ -57,6 +63,8 @@ export default function PostCommentCard({
<span className=" text-sm text-slate-500">{`#${index + 1}`}</span>
<PostLikeButton
post={post}></PostLikeButton>
<PostHateButton
post={post}></PostHateButton>
</div>
</div>
</div>

View File

@ -0,0 +1,52 @@
import { PostDto, VisitType } from "@nice/common";
import { useVisitor } from "@nice/client";
import { Button, Tooltip } from "antd";
import { DislikeFilled, DislikeOutlined } from "@ant-design/icons";
import { useAuth } from "@web/src/providers/auth-provider";
export default function PostHateButton({ post }: { post: PostDto }) {
const { user } = useAuth();
const { hate, unHate } = useVisitor();
function hateThisPost() {
if (!post?.hated) {
post.hates += 1;
post.hated = true;
hate.mutateAsync({
data: {
visitorId: user?.id || null,
postId: post.id,
type: VisitType.HATE,
},
});
} else {
post.hates -= 1;
post.hated = false;
unHate.mutateAsync({
where: {
visitorId: user?.id || null,
postId: post.id,
type: VisitType.HATE,
},
});
}
}
return (
<Button
title={post?.hated ? "取消点踩" : "点踩"}
type={post?.hated ? "primary" : "default"}
style={{
backgroundColor: post?.hated ? "#ff4d4f" : "#fff",
borderColor: "#ff4d4f",
color: post?.hated ? "#fff" : "#ff4d4f",
}}
shape="round"
icon={post?.hated ? <DislikeFilled /> : <DislikeOutlined />}
onClick={(e) => {
e.stopPropagation();
hateThisPost();
}}>
<span className="mr-1"></span>
{post?.hates || 0}
</Button>
);
}

View File

@ -10,6 +10,7 @@ import { Button, Tooltip } from "antd/lib";
import { PostDetailContext } from "../context/PostDetailContext";
import PostLikeButton from "./PostLikeButton";
import PostResources from "../PostResources";
import PostHateButton from "./PostHateButton";
export function StatsSection() {
const { post } = useContext(PostDetailContext);
@ -26,6 +27,7 @@ export function StatsSection() {
<span className="mr-1"></span>{post?.commentsCount}
</Button>
<PostLikeButton post={post}></PostLikeButton>
<PostHateButton post={post}></PostHateButton>
</div>
</div>

View File

@ -3,15 +3,15 @@ import { AppConfigSlug, BaseSetting } from "@nice/common";
import { useCallback, useEffect, useMemo, useState } from "react";
export function useAppConfig() {
const utils = api.useUtils()
const utils = api.useUtils();
const [baseSetting, setBaseSetting] = useState<BaseSetting | undefined>();
const { data, isLoading }: { data: any; isLoading: boolean } =
api.app_config.findFirst.useQuery({
where: { slug: AppConfigSlug.BASE_SETTING }
where: { slug: AppConfigSlug.BASE_SETTING },
});
const handleMutationSuccess = useCallback(() => {
utils.app_config.invalidate()
utils.app_config.invalidate();
}, [utils]);
// Use the generic success handler in mutations
@ -28,7 +28,6 @@ export function useAppConfig() {
if (data?.meta) {
setBaseSetting(JSON.parse(data?.meta));
}
}, [data, isLoading]);
const splashScreen = useMemo(() => {
return baseSetting?.appConfig?.splashScreen;
@ -36,8 +35,10 @@ export function useAppConfig() {
const devDept = useMemo(() => {
return baseSetting?.appConfig?.devDept;
}, [baseSetting]);
const logo = useMemo(() => {
return baseSetting?.appConfig?.logo;
}, [baseSetting]);
return {
create,
deleteMany,
update,
@ -45,5 +46,6 @@ export function useAppConfig() {
splashScreen,
devDept,
isLoading,
logo,
};
}

View File

@ -171,5 +171,7 @@ export function useVisitor() {
deleteStar,
like,
unLike,
hate,
unHate,
};
}

View File

@ -8,6 +8,7 @@ export const postDetailSelect: Prisma.PostSelect = {
content: true,
views: true,
likes: true,
hates: true,
isPublic: true,
resources: true,
createdAt: true,

View File

@ -39,11 +39,11 @@ export type StaffDto = Staff & {
domain?: Department;
department?: Department;
meta?: {
photoUrl?: string
office?: string
email?: string
rank?: string
}
photoUrl?: string;
office?: string;
email?: string;
rank?: string;
};
};
export interface AuthDto {
token: string;
@ -133,6 +133,7 @@ export type PostComment = {
export type PostDto = Post & {
readed: boolean;
liked: boolean;
hated: boolean;
readedCount: number;
commentsCount: number;
terms: TermDto[];
@ -167,6 +168,7 @@ export interface BaseSetting {
appConfig?: {
splashScreen?: string;
devDept?: string;
logo?: string;
};
}
export interface PostMeta {