From ed4b328047259b58e406ee079c56516b073a517e Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 16:10:31 +0800 Subject: [PATCH 1/5] add --- .../models/app-config/app-config.router.ts | 80 +++++++++--------- .../models/app-config/app-config.service.ts | 13 +-- apps/server/src/models/visit/visit.service.ts | 7 ++ apps/server/src/queue/models/post/utils.ts | 11 ++- apps/server/src/queue/worker/processor.ts | 1 - apps/server/src/tasks/init/gendev.service.ts | 24 +++--- apps/web/src/app/admin/base-setting/page.tsx | 82 ++++++++++--------- apps/web/src/app/auth/register.tsx | 2 +- .../common/uploader/AvatarUploader.tsx | 7 +- .../src/components/models/post/LetterCard.tsx | 2 + .../models/post/detail/PostCommentCard.tsx | 10 ++- .../post/detail/PostHeader/PostHateButton.tsx | 52 ++++++++++++ .../post/detail/PostHeader/StatsSection.tsx | 2 + packages/client/src/api/hooks/useAppConfig.ts | 12 +-- packages/client/src/api/hooks/useVisitor.ts | 2 + packages/common/src/select.ts | 1 + packages/common/src/types.ts | 12 +-- 17 files changed, 204 insertions(+), 116 deletions(-) create mode 100644 apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx diff --git a/apps/server/src/models/app-config/app-config.router.ts b/apps/server/src/models/app-config/app-config.router.ts index ece1b35..882fa16 100644 --- a/apps/server/src/models/app-config/app-config.router.ts +++ b/apps/server/src/models/app-config/app-config.router.ts @@ -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 = z.any() -const AppConfigUpdateArgsSchema: ZodType = z.any() -const AppConfigDeleteManyArgsSchema: ZodType = z.any() -const AppConfigFindFirstArgsSchema: ZodType = z.any() +const AppConfigUncheckedCreateInputSchema: ZodType = + z.any(); +const AppConfigUpdateArgsSchema: ZodType = z.any(); +const AppConfigDeleteManyArgsSchema: ZodType = + z.any(); +const AppConfigFindFirstArgsSchema: ZodType = + 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(); + }), + }); } diff --git a/apps/server/src/models/app-config/app-config.service.ts b/apps/server/src/models/app-config/app-config.service.ts index 733e620..bd003d7 100644 --- a/apps/server/src/models/app-config/app-config.service.ts +++ b/apps/server/src/models/app-config/app-config.service.ts @@ -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 { constructor() { - super(db, "appConfig"); + super(db, 'appConfig'); } async clearRowCache() { - await deleteByPattern("row-*") - return true + await deleteByPattern('row-*'); + return true; } } diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index 3d3a49a..6e0fb44 100644 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -187,6 +187,13 @@ export class VisitService extends BaseService { 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; } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 6d43be2..4c620a2 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -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 + }, + }); } } diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index 74fe414..76c4b89 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -8,7 +8,6 @@ import { updatePostViewCount } from '../models/post/utils'; const logger = new Logger('QueueWorker'); export default async function processJob(job: Job) { try { - if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) { await updatePostViewCount(job.data.id, job.data.type); } diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index 313c004..5bc2cc0 100644 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -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 = {}; terms: Record = { [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); diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index 693b11b..7c6fe27 100644 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -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 ( -
+
{isFormChanged && @@ -101,7 +89,6 @@ export default function BaseSettingPage() { !hasSomePermissions(RolePerms.MANAGE_BASE_SETTING) } onFinish={onSubmit} - onFieldsChange={handleFieldsChange} layout="vertical"> {/*
+
+ + + +
{/*
- {
- app在线人数 -
- {clientCount && clientCount > 0 ? `${clientCount}人在线` : '无人在线'} + { +
+ app在线人数 +
+ {clientCount && clientCount > 0 + ? `${clientCount}人在线` + : "无人在线"} +
-
} + }
); diff --git a/apps/web/src/app/auth/register.tsx b/apps/web/src/app/auth/register.tsx index 3337d32..eb2791a 100644 --- a/apps/web/src/app/auth/register.tsx +++ b/apps/web/src/app/auth/register.tsx @@ -156,7 +156,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => { message: "请输入有效的证件号(5-12位数字)", }, ]}> - + = ({ }) => { const { handleFileUpload, uploadProgress } = useTusUpload(); const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(value || ""); const [uploading, setUploading] = useState(false); const inputRef = useRef(null); @@ -56,7 +58,9 @@ const AvatarUploader: React.FC = ({ 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 = ({ 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 = ({ background: token.colorBgContainer, ...style, // 应用外部传入的样式 }}> +
{previewUrl}
+
diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 3af549e..4442c08 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -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({ {`#${index + 1}`} + diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx new file mode 100644 index 0000000..3d5cb18 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx @@ -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 ( + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx index 64c7380..005da03 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx @@ -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() { 回复数{post?.commentsCount} + diff --git a/packages/client/src/api/hooks/useAppConfig.ts b/packages/client/src/api/hooks/useAppConfig.ts index 0b00ffd..3e468cb 100644 --- a/packages/client/src/api/hooks/useAppConfig.ts +++ b/packages/client/src/api/hooks/useAppConfig.ts @@ -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(); 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, }; } diff --git a/packages/client/src/api/hooks/useVisitor.ts b/packages/client/src/api/hooks/useVisitor.ts index 37ce83e..86cdc58 100644 --- a/packages/client/src/api/hooks/useVisitor.ts +++ b/packages/client/src/api/hooks/useVisitor.ts @@ -171,5 +171,7 @@ export function useVisitor() { deleteStar, like, unLike, + hate, + unHate, }; } diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 0e9e0c6..32271f6 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -8,6 +8,7 @@ export const postDetailSelect: Prisma.PostSelect = { content: true, views: true, likes: true, + hates: true, isPublic: true, resources: true, createdAt: true, diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index c1f8795..d754ae6 100755 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -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 { From 483058090d92031031b83d41fe2050a8e15aa604 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 18:24:16 +0800 Subject: [PATCH 2/5] add --- apps/server/src/models/rbac/rolemap.router.ts | 12 +- .../server/src/models/rbac/rolemap.service.ts | 74 +++-- .../src/models/resource/resource.module.ts | 6 +- .../src/models/resource/resource.router.ts | 127 ++++---- apps/server/src/models/staff/staff.module.ts | 2 +- apps/server/src/models/staff/staff.service.ts | 2 +- apps/server/src/trpc/trpc.module.ts | 3 + apps/server/src/trpc/trpc.router.ts | 9 +- apps/server/src/trpc/types.ts | 2 +- apps/web/src/app/main/letter/editor/page.tsx | 2 +- apps/web/src/app/main/letter/write/page.tsx | 273 ++++++++++-------- .../common/uploader/AvatarUploader.tsx | 7 +- .../common/uploader/TusUploader.tsx | 5 +- .../web/src/components/layout/main/Header.tsx | 101 ++++--- .../src/components/layout/main/navigation.tsx | 2 +- .../models/post/detail/PostCommentCard.tsx | 7 +- .../models/post/detail/PostCommentEditor.tsx | 15 +- .../post/detail/PostHeader/PostHateButton.tsx | 3 +- .../post/detail/PostHeader/PostSendButton.tsx | 32 ++ .../models/post/detail/PostResources.tsx | 6 +- .../post/editor/form/LetterBasicForm.tsx | 10 +- .../models/role/role-editor/role-modal.tsx | 38 ++- apps/web/src/hooks/useTusUpload.ts | 17 +- packages/client/src/api/hooks/useVisitor.ts | 1 + packages/common/src/enum.ts | 7 + packages/common/src/schema.ts | 3 + 26 files changed, 459 insertions(+), 307 deletions(-) create mode 100644 apps/web/src/components/models/post/detail/PostHeader/PostSendButton.tsx diff --git a/apps/server/src/models/rbac/rolemap.router.ts b/apps/server/src/models/rbac/rolemap.router.ts index 72ae5a4..e610cc2 100755 --- a/apps/server/src/models/rbac/rolemap.router.ts +++ b/apps/server/src/models/rbac/rolemap.router.ts @@ -1,9 +1,6 @@ import { Injectable } from '@nestjs/common'; import { TrpcService } from '@server/trpc/trpc.service'; -import { - ObjectType, - RoleMapMethodSchema, -} from '@nice/common'; +import { ObjectType, RoleMapMethodSchema } from '@nice/common'; import { RoleMapService } from './rolemap.service'; @Injectable() @@ -11,7 +8,7 @@ export class RoleMapRouter { constructor( private readonly trpc: TrpcService, private readonly roleMapService: RoleMapService, - ) { } + ) {} router = this.trpc.router({ deleteAllRolesForObject: this.trpc.protectProcedure .input(RoleMapMethodSchema.deleteWithObject) @@ -67,5 +64,10 @@ export class RoleMapRouter { .query(async ({ input }) => { return this.roleMapService.getStaffsNotMap(input); }), + getStaffIdsByRoleNames: this.trpc.procedure + .input(RoleMapMethodSchema.getStaffIdsByRoleNames) + .query(async ({ input }) => { + return this.roleMapService.getStaffIdsByRoleNames(input); + }), }); } diff --git a/apps/server/src/models/rbac/rolemap.service.ts b/apps/server/src/models/rbac/rolemap.service.ts index d3c971a..9b2c7db 100755 --- a/apps/server/src/models/rbac/rolemap.service.ts +++ b/apps/server/src/models/rbac/rolemap.service.ts @@ -44,7 +44,7 @@ export class RoleMapService extends RowModelService { ) { const { roleId, domainId } = request; // Base conditions - let condition = super.createGetRowsFilters(request, staff); + const condition = super.createGetRowsFilters(request, staff); if (isFieldCondition(condition)) return; // Adding conditions based on parameters existence if (roleId) { @@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService { return condition; } - protected async getRowDto( - row: any, - staff?: UserProfile, - ): Promise { + protected async getRowDto(row: any, staff?: UserProfile): Promise { if (!row.id) return row; return row; } @@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService { data: roleMaps, }); }); - const wrapResult = Promise.all(result.map(async item => { - const staff = await db.staff.findMany({ - include: { department: true }, - where: { - id: item.objectId - } - }) - return { ...item, staff } - })) + const wrapResult = Promise.all( + result.map(async (item) => { + const staff = await db.staff.findMany({ + include: { department: true }, + where: { + id: item.objectId, + }, + }); + return { ...item, staff }; + }), + ); return wrapResult; } async addRoleForObjects( @@ -187,11 +186,11 @@ export class RoleMapService extends RowModelService { { objectId: staffId, objectType: ObjectType.STAFF }, ...(deptId || ancestorDeptIds.length > 0 ? [ - { - objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) }, - objectType: ObjectType.DEPARTMENT, - }, - ] + { + objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) }, + objectType: ObjectType.DEPARTMENT, + }, + ] : []), ]; // Helper function to fetch roles based on domain ID. @@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService { // const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item))); return { items, totalCount }; } - async getStaffsNotMap(data: z.infer) { + async getStaffsNotMap( + data: z.infer, + ) { const { domainId, roleId } = data; let staffs = await db.staff.findMany({ where: { @@ -280,6 +281,35 @@ export class RoleMapService extends RowModelService { ); return staffs; } + async getStaffIdsByRoleNames( + data: z.infer, + ) { + const { roleNames } = data; + const roles = await db.role.findMany({ + where: { + name: { + in: roleNames, + }, + }, + select: { + id: true, + }, + }); + const roleMaps = await db.roleMap.findMany({ + where: { + roleId: { + in: roles.map((role) => role.id), + }, + objectType: ObjectType.STAFF, + }, + select: { + id: true, + objectId: true, + }, + }); + const staffIds = roleMaps.map((roleMap) => roleMap.objectId); + return staffIds; + } /** * 更新角色映射 * @param data 包含更新信息的数据 @@ -300,7 +330,9 @@ export class RoleMapService extends RowModelService { * @param data 包含角色ID和域ID的数据 * @returns 角色映射详情,包含部门ID和员工ID列表 */ - async getRoleMapDetail(data: z.infer) { + async getRoleMapDetail( + data: z.infer, + ) { const { roleId, domainId } = data; const res = await db.roleMap.findMany({ where: { roleId, domainId } }); diff --git a/apps/server/src/models/resource/resource.module.ts b/apps/server/src/models/resource/resource.module.ts index 153bc6e..a2b9dbf 100644 --- a/apps/server/src/models/resource/resource.module.ts +++ b/apps/server/src/models/resource/resource.module.ts @@ -4,7 +4,7 @@ import { ResourceService } from './resource.service'; import { TrpcService } from '@server/trpc/trpc.service'; @Module({ - exports: [ResourceRouter, ResourceService], - providers: [ResourceRouter, ResourceService, TrpcService], + exports: [ResourceRouter, ResourceService], + providers: [ResourceRouter, ResourceService, TrpcService], }) -export class ResourceModule { } +export class ResourceModule {} diff --git a/apps/server/src/models/resource/resource.router.ts b/apps/server/src/models/resource/resource.router.ts index 6d4290f..30af7b5 100644 --- a/apps/server/src/models/resource/resource.router.ts +++ b/apps/server/src/models/resource/resource.router.ts @@ -3,68 +3,75 @@ import { TrpcService } from '@server/trpc/trpc.service'; import { Prisma, UpdateOrderSchema } from '@nice/common'; import { ResourceService } from './resource.service'; import { z, ZodType } from 'zod'; -const ResourceCreateArgsSchema: ZodType = z.any() -const ResourceCreateManyInputSchema: ZodType = z.any() -const ResourceDeleteManyArgsSchema: ZodType = z.any() -const ResourceFindManyArgsSchema: ZodType = z.any() -const ResourceFindFirstArgsSchema: ZodType = z.any() -const ResourceWhereInputSchema: ZodType = z.any() -const ResourceSelectSchema: ZodType = z.any() +const ResourceCreateArgsSchema: ZodType = z.any(); +const ResourceCreateManyInputSchema: ZodType = + z.any(); +const ResourceDeleteManyArgsSchema: ZodType = + z.any(); +const ResourceFindManyArgsSchema: ZodType = + z.any(); +const ResourceFindFirstArgsSchema: ZodType = + z.any(); +const ResourceWhereInputSchema: ZodType = z.any(); +const ResourceSelectSchema: ZodType = z.any(); @Injectable() export class ResourceRouter { - constructor( - private readonly trpc: TrpcService, - private readonly resourceService: ResourceService, - ) { } - router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(ResourceCreateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.resourceService.create(input, { staff }); - }), - createMany: this.trpc.protectProcedure.input(z.array(ResourceCreateManyInputSchema)) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; + constructor( + private readonly trpc: TrpcService, + private readonly resourceService: ResourceService, + ) {} + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(ResourceCreateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.resourceService.create(input, { staff }); + }), + createMany: this.trpc.protectProcedure + .input(z.array(ResourceCreateManyInputSchema)) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; - return await this.resourceService.createMany({ data: input }, staff); - }), - deleteMany: this.trpc.procedure - .input(ResourceDeleteManyArgsSchema) - .mutation(async ({ input }) => { - return await this.resourceService.deleteMany(input); - }), - findFirst: this.trpc.procedure - .input(ResourceFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.resourceService.findFirst(input); - }), - softDeleteByIds: this.trpc.protectProcedure - .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema - .mutation(async ({ input }) => { - return this.resourceService.softDeleteByIds(input.ids); - }), - updateOrder: this.trpc.protectProcedure - .input(UpdateOrderSchema) - .mutation(async ({ input }) => { - return this.resourceService.updateOrder(input); - }), - findMany: this.trpc.procedure - .input(ResourceFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.resourceService.findMany(input); - }), - findManyWithCursor: this.trpc.protectProcedure - .input(z.object({ - cursor: z.any().nullish(), - take: z.number().nullish(), - where: ResourceWhereInputSchema.nullish(), - select: ResourceSelectSchema.nullish() - })) - .query(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.resourceService.findManyWithCursor(input); - }), - }); + return await this.resourceService.createMany({ data: input }, staff); + }), + deleteMany: this.trpc.procedure + .input(ResourceDeleteManyArgsSchema) + .mutation(async ({ input }) => { + return await this.resourceService.deleteMany(input); + }), + findFirst: this.trpc.procedure + .input(ResourceFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.resourceService.findFirst(input); + }), + softDeleteByIds: this.trpc.protectProcedure + .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema + .mutation(async ({ input }) => { + return this.resourceService.softDeleteByIds(input.ids); + }), + updateOrder: this.trpc.protectProcedure + .input(UpdateOrderSchema) + .mutation(async ({ input }) => { + return this.resourceService.updateOrder(input); + }), + findMany: this.trpc.procedure + .input(ResourceFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.resourceService.findMany(input); + }), + findManyWithCursor: this.trpc.protectProcedure + .input( + z.object({ + cursor: z.any().nullish(), + take: z.number().nullish(), + where: ResourceWhereInputSchema.nullish(), + select: ResourceSelectSchema.nullish(), + }), + ) + .query(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.resourceService.findManyWithCursor(input); + }), + }); } diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts index fa681dc..c9e787f 100755 --- a/apps/server/src/models/staff/staff.module.ts +++ b/apps/server/src/models/staff/staff.module.ts @@ -12,4 +12,4 @@ import { StaffRowService } from './staff.row.service'; exports: [StaffService, StaffRouter, StaffRowService], controllers: [StaffController], }) -export class StaffModule { } +export class StaffModule {} diff --git a/apps/server/src/models/staff/staff.service.ts b/apps/server/src/models/staff/staff.service.ts index a5868aa..9844c37 100755 --- a/apps/server/src/models/staff/staff.service.ts +++ b/apps/server/src/models/staff/staff.service.ts @@ -44,7 +44,7 @@ export class StaffService extends BaseService { ...data, password: await argon2.hash((data.password || '123456') as string), }; - + const result = await super.create({ ...args, data: createData }); this.emitDataChangedEvent(result, CrudOperation.CREATED); return result; diff --git a/apps/server/src/trpc/trpc.module.ts b/apps/server/src/trpc/trpc.module.ts index 978c0af..222c9c9 100755 --- a/apps/server/src/trpc/trpc.module.ts +++ b/apps/server/src/trpc/trpc.module.ts @@ -15,6 +15,8 @@ import { WebSocketModule } from '@server/socket/websocket.module'; import { RoleMapModule } from '@server/models/rbac/rbac.module'; import { TransformModule } from '@server/models/transform/transform.module'; +import { ResourceModule } from '@server/models/resource/resource.module'; + @Module({ imports: [ AuthModule, @@ -30,6 +32,7 @@ import { TransformModule } from '@server/models/transform/transform.module'; PostModule, VisitModule, WebSocketModule, + ResourceModule, ], controllers: [], providers: [TrpcService, TrpcRouter, Logger], diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index dd0190c..7550867 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -13,10 +13,10 @@ import { VisitRouter } from '@server/models/visit/visit.router'; import { RoleMapRouter } from '@server/models/rbac/rolemap.router'; import { TransformRouter } from '@server/models/transform/transform.router'; import { RoleRouter } from '@server/models/rbac/role.router'; +import { ResourceRouter } from '../models/resource/resource.router'; @Injectable() export class TrpcRouter { - logger = new Logger(TrpcRouter.name); constructor( private readonly trpc: TrpcService, @@ -31,10 +31,10 @@ export class TrpcRouter { private readonly app_config: AppConfigRouter, private readonly message: MessageRouter, private readonly visitor: VisitRouter, - - ) { } + private readonly resource: ResourceRouter, + ) {} getRouter() { - return + return; } appRouter = this.trpc.router({ transform: this.transform.router, @@ -48,6 +48,7 @@ export class TrpcRouter { message: this.message.router, app_config: this.app_config.router, visitor: this.visitor.router, + resource: this.resource.router, }); wss: WebSocketServer = undefined; diff --git a/apps/server/src/trpc/types.ts b/apps/server/src/trpc/types.ts index cd7e491..fc6ce2c 100644 --- a/apps/server/src/trpc/types.ts +++ b/apps/server/src/trpc/types.ts @@ -1,3 +1,3 @@ -import { TrpcRouter } from "./trpc.router"; +import { TrpcRouter } from './trpc.router'; export type AppRouter = TrpcRouter[`appRouter`]; diff --git a/apps/web/src/app/main/letter/editor/page.tsx b/apps/web/src/app/main/letter/editor/page.tsx index 4caa91f..f624693 100644 --- a/apps/web/src/app/main/letter/editor/page.tsx +++ b/apps/web/src/app/main/letter/editor/page.tsx @@ -9,7 +9,7 @@ export default function LetterEditorPage() { const termId = searchParams.get("termId"); return ( -
+
diff --git a/apps/web/src/app/main/letter/write/page.tsx b/apps/web/src/app/main/letter/write/page.tsx index c7da2f7..de44eae 100644 --- a/apps/web/src/app/main/letter/write/page.tsx +++ b/apps/web/src/app/main/letter/write/page.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useSearchParams } from "react-router-dom"; @@ -9,136 +9,157 @@ import DepartmentSelect from "@web/src/components/models/department/department-s import debounce from "lodash/debounce"; import { SearchOutlined } from "@ant-design/icons"; import WriteHeader from "./WriteHeader"; +import { ObjectType, RoleName } from "@nice/common"; export default function WriteLetterPage() { - const [searchParams] = useSearchParams(); - const termId = searchParams.get("termId"); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedDept, setSelectedDept] = useState(); - const [currentPage, setCurrentPage] = useState(1); - const pageSize = 10; - const { getTerm } = useTerm(); + const [searchParams] = useSearchParams(); + const termId = searchParams.get("termId"); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedDept, setSelectedDept] = useState(); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 10; + const { getTerm } = useTerm(); + const { data: enabledStaffIds, isLoading: roleMapIsLoading } = + api.rolemap.getStaffIdsByRoleNames.useQuery({ + roleNames: [RoleName.Leader, RoleName.Organization], + }); + // eslint-disable-next-line react-hooks/exhaustive-deps - const { data, isLoading, error } = - api.staff.findManyWithPagination.useQuery({ - page: currentPage, - pageSize, - where: { - deptId: selectedDept, - OR: [ - { - showname: { - contains: searchQuery, - }, - }, - { - username: { - contains: searchQuery, - }, - }, + const { data, isLoading, error } = + api.staff.findManyWithPagination.useQuery( + { + page: currentPage, + pageSize, + where: { + id: enabledStaffIds + ? { + in: enabledStaffIds, + } + : undefined, + deptId: selectedDept, + OR: [ + { + showname: { + contains: searchQuery, + }, + }, + { + username: { + contains: searchQuery, + }, + }, + { + meta: { + path: ["rank"], // 指定 JSON 字段的路径 + string_contains: searchQuery, // 对 rank 字段进行模糊搜索 + }, + }, + ], + }, + orderBy: { + order: "asc", + }, + }, + { + enabled: !roleMapIsLoading, + } + ); - ], - }, - orderBy: { - order: "desc", - } - }); + const resetPage = useCallback(() => { + setCurrentPage(1); + }, []); - const resetPage = useCallback(() => { - setCurrentPage(1); - }, []); + // Reset page when search or department changes + useEffect(() => { + resetPage(); + }, [searchQuery, selectedDept, resetPage]); - // Reset page when search or department changes - useEffect(() => { - resetPage(); - }, [searchQuery, selectedDept, resetPage]); + return ( +
+ +
+
+
+ + + } + placeholder="搜索领导姓名或职级..." + onChange={debounce( + (e) => setSearchQuery(e.target.value), + 300 + )} + size="large" + /> +
+ {error && ( + + )} +
+ + {isLoading ? ( +
+ +
+ ) : data?.items.length > 0 ? ( + + {data?.items.map((item: any) => ( + + ))} + + ) : ( + + + + )} +
- return ( -
- -
-
-
- - - } - placeholder="搜索领导姓名或职级..." - onChange={debounce( - (e) => setSearchQuery(e.target.value), - 300 - )} - size="large" - /> -
- {error && ( - - )} -
- - {isLoading ? ( -
- -
- ) : data?.items.length > 0 ? ( - - {data?.items.map((item: any) => ( - - ))} - - ) : ( - - - - )} -
- - {/* Pagination */} - {data?.items.length > 0 && ( -
- { - setCurrentPage(page); - window.scrollTo(0, 0); - }} - showSizeChanger={false} - showTotal={(total) => `共 ${total} 条记录`} - /> -
- )} -
-
- ); + {/* Pagination */} + {data?.items.length > 0 && ( +
+ { + setCurrentPage(page); + window.scrollTo(0, 0); + }} + showSizeChanger={false} + showTotal={(total) => `共 ${total} 条记录`} + /> +
+ )} +
+
+ ); } diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index 5082427..ae831a3 100644 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -2,6 +2,7 @@ import { env } from "@web/src/env"; import { message, Progress, Spin, theme } from "antd"; import React, { useState, useEffect, useRef } from "react"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; +import { Avatar } from "antd/lib"; export interface AvatarUploaderProps { value?: string; @@ -69,7 +70,6 @@ const AvatarUploader: React.FC = ({ file?.fileKey ); }); - setPreviewUrl(`http://${env.SERVER_IP}/uploads/${fileId}`); onChange?.(fileId); message.success("头像上传成功"); } catch (error) { @@ -94,7 +94,6 @@ const AvatarUploader: React.FC = ({ background: token.colorBgContainer, ...style, // 应用外部传入的样式 }}> -
{previewUrl}
= ({ style={{ display: "none" }} /> {previewUrl ? ( - Avatar ) : ( diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 47034e6..12f8f9d 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -130,9 +130,8 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {

@@ -143,7 +142,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {

支持单个或批量上传文件

{/* 正在上传的文件 */} {(uploadingFiles.length > 0 || completedFiles.length > 0) && ( -
+
{uploadingFiles.map((file) => (
-
-
-
-
- 首长机关信箱 -

聆怀若水,应语如风;纾难化困,践诺成春

-
-
- -
-
- {!isAuthenticated ? ( - +
+
+
+
+ {/** 在这里放置logo */} + {isLoading ? ( +
+ ) : ( + logoUrl && ( + Logo + ) + )} +
+
+ +
+
+ {!isAuthenticated ? ( + - - + - 登录 - - ) : ( - - )} -
-
-
- -
- - ); + 登录 + + ) : ( + + )} +
+
+
+ +
+ + ); }); diff --git a/apps/web/src/components/layout/main/navigation.tsx b/apps/web/src/components/layout/main/navigation.tsx index 27a1302..66517b6 100644 --- a/apps/web/src/components/layout/main/navigation.tsx +++ b/apps/web/src/components/layout/main/navigation.tsx @@ -28,7 +28,7 @@ export default function Navigation({ className }: NavigationProps) { return (
- { - setFileIds(value); - }} - /> +
+ { + setFileIds(value); + }} + /> +
diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx index 3d5cb18..a19dfec 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx @@ -36,8 +36,9 @@ export default function PostHateButton({ post }: { post: PostDto }) { type={post?.hated ? "primary" : "default"} style={{ backgroundColor: post?.hated ? "#ff4d4f" : "#fff", - borderColor: "#ff4d4f", + borderColor: post?.hated ? "transparent" : "#ff4d4f", color: post?.hated ? "#fff" : "#ff4d4f", + boxShadow: "none", // 去除阴影 }} shape="round" icon={post?.hated ? : } diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostSendButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostSendButton.tsx new file mode 100644 index 0000000..e0a7298 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/PostSendButton.tsx @@ -0,0 +1,32 @@ +import { PostDto, VisitType } from "@nice/common"; +import { useVisitor } from "@nice/client"; +import { Button, Tooltip } from "antd"; +import { + DislikeFilled, + DislikeOutlined, + SendOutlined, +} from "@ant-design/icons"; +import { useAuth } from "@web/src/providers/auth-provider"; + +export default function PostSendButton({ post }: { post: PostDto }) { + const { user } = useAuth(); + const { hate, unHate } = useVisitor(); + function sendPost() { + if (post?.authorId) { + window.open(`/editor?receiverId=${post?.authorId}`, "_blank"); + } + } + return ( + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostResources.tsx b/apps/web/src/components/models/post/detail/PostResources.tsx index 328bae4..714b549 100644 --- a/apps/web/src/components/models/post/detail/PostResources.tsx +++ b/apps/web/src/components/models/post/detail/PostResources.tsx @@ -4,7 +4,7 @@ import { DownloadOutlined } from "@ant-design/icons"; import { PostDto } from "@nice/common"; import { env } from "@web/src/env"; import { getFileIcon } from "./utils"; -import { formatFileSize } from '@nice/utils'; +import { formatFileSize } from "@nice/utils"; export default function PostResources({ post }: { post: PostDto }) { const { resources } = useMemo(() => { if (!post?.resources) return { resources: [] }; @@ -99,7 +99,9 @@ export default function PostResources({ post }: { post: PostDto }) { {resource.meta.size && - formatFileSize(resource.meta.size)} + formatFileSize( + resource.meta.size + )}
diff --git a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index d3b6cd2..4252c4e 100644 --- a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -65,7 +65,7 @@ export function LetterBasicForm() { name="content" rules={[{ required: true, message: "请输入内容" }]} required={false}> -
+
-
+
+ form.setFieldValue( @@ -88,12 +89,13 @@ export function LetterBasicForm() { ) } /> -
+ +
{/* Footer Actions */} -
+
是否公开 diff --git a/apps/web/src/components/models/role/role-editor/role-modal.tsx b/apps/web/src/components/models/role/role-editor/role-modal.tsx index e8b4411..1897af7 100644 --- a/apps/web/src/components/models/role/role-editor/role-modal.tsx +++ b/apps/web/src/components/models/role/role-editor/role-modal.tsx @@ -4,28 +4,24 @@ import { RoleEditorContext } from "./role-editor"; import RoleForm from "./role-form"; export default function RoleModal() { - const { - roleForm, - editRoleId, - roleModalOpen, setRoleModalOpen - } = useContext(RoleEditorContext); + const { roleForm, editRoleId, roleModalOpen, setRoleModalOpen } = + useContext(RoleEditorContext); - const handleOk = async () => { - roleForm.submit() - }; + const handleOk = async () => { + roleForm.submit(); + }; - const handleCancel = () => setRoleModalOpen(false); + const handleCancel = () => setRoleModalOpen(false); - return ( - - - - ); + return ( + + + + ); } diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index b3f0b35..a1ddf1d 100644 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -1,5 +1,6 @@ import { useState } from "react"; import * as tus from "tus-js-client"; +import { env } from "../env"; // useTusUpload.ts interface UploadProgress { @@ -27,7 +28,16 @@ export function useTusUpload() { } return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/"); }; - + const getResourceUrl = (url: string) => { + const parts = url.split("/"); + const uploadIndex = parts.findIndex((part) => part === "upload"); + if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { + throw new Error("Invalid upload URL format"); + } + const resUrl = `http://${env.SERVER_IP}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; + console.log(resUrl); + return resUrl; + }; const handleFileUpload = async ( file: File, onSuccess: (result: UploadResult) => void, @@ -52,7 +62,7 @@ export function useTusUpload() { metadata: { filename: file.name, filetype: file.type, - size: file.size as any + size: file.size as any, }, onProgress: (bytesUploaded, bytesTotal) => { const progress = Number( @@ -67,13 +77,14 @@ export function useTusUpload() { try { if (upload.url) { const fileId = getFileId(upload.url); + const url = getResourceUrl(upload.url); setIsUploading(false); setUploadProgress((prev) => ({ ...prev, [fileKey]: 100, })); onSuccess({ - url: upload.url, + url, fileId, }); } diff --git a/packages/client/src/api/hooks/useVisitor.ts b/packages/client/src/api/hooks/useVisitor.ts index 86cdc58..30e4e5e 100644 --- a/packages/client/src/api/hooks/useVisitor.ts +++ b/packages/client/src/api/hooks/useVisitor.ts @@ -8,6 +8,7 @@ export function useVisitor() { const create = api.visitor.create.useMutation({ onSuccess() { utils.visitor.invalidate(); + // utils.post.invalidate(); }, }); diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 035c58a..d4701ea 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -201,3 +201,10 @@ export const PostStateLabels = { [PostState.PROCESSING]: "处理中", [PostState.RESOLVED]: "已完成", }; +export enum RoleName { + Basic = "基层", // 基层 + Organization = "机关", // 机关 + Leader = "领导", // 领导 + DomainAdmin = "域管理员", // 域管理员 + RootAdmin = "根管理员", // 根管理员 +} diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index ab44cd9..7ff9417 100755 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -320,6 +320,9 @@ export const RoleMapMethodSchema = { domainId: z.string().nullish(), roleId: z.string().nullish(), }), + getStaffIdsByRoleNames: z.object({ + roleNames: z.array(z.string()), + }), }; export const RoleMethodSchema = { create: z.object({ From 262dfade4e9c0c345873b23fa860e750aa998ff2 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 18:32:47 +0800 Subject: [PATCH 3/5] add 0125 --- .../src/components/common/uploader/TusUploader.tsx | 8 ++++---- apps/web/src/components/models/post/LetterCard.tsx | 2 +- .../models/post/detail/PostCommentCard.tsx | 4 ++-- .../models/post/detail/PostHeader/PostLikeButton.tsx | 12 ++++++------ .../models/post/detail/PostHeader/StatsSection.tsx | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 12f8f9d..c41ba95 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -4,9 +4,10 @@ import { CheckCircleOutlined, DeleteOutlined, } from "@ant-design/icons"; -import { Upload, message, Progress, Button } from "antd"; +import { Upload, Progress, Button } from "antd"; import type { UploadFile } from "antd"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; +import toast from "react-hot-toast"; export interface TusUploaderProps { value?: string[]; @@ -52,7 +53,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { console.log("文件", files); if (!files.every((f) => f instanceof File)) { - message.error("无效的文件格式"); + toast.error("无效的文件格式"); return false; } @@ -111,10 +112,9 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { ); setUploadResults(newValue); onChange?.(newValue); - message.success(`${files.length} 个文件上传成功`); } catch (error) { console.error("上传错误详情:", error); - message.error( + toast.error( `上传失败: ${error instanceof Error ? error.message : "未知错误"}` ); setUploadingFiles((prev) => diff --git a/apps/web/src/components/models/post/LetterCard.tsx b/apps/web/src/components/models/post/LetterCard.tsx index 27ca0b2..61b8393 100644 --- a/apps/web/src/components/models/post/LetterCard.tsx +++ b/apps/web/src/components/models/post/LetterCard.tsx @@ -117,8 +117,8 @@ export function LetterCard({ letter }: LetterCardProps) { 浏览量 {letter.views} - +
diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 7a0cfd3..7cef4fd 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -65,10 +65,10 @@ export default function PostCommentCard({ )} - + {`#${index + 1}`}
diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx index 93fdea6..a1d723b 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx @@ -35,16 +35,16 @@ export default function PostLikeButton({ post }: { post: PostDto }) { title={post?.liked ? "取消点赞" : "点赞"} type={post?.liked ? "primary" : "default"} shape="round" - + style={{ + boxShadow: "none", // 去除阴影 + }} icon={post?.liked ? : } onClick={(e) => { - e.stopPropagation() - likeThisPost() - }} - > + e.stopPropagation(); + likeThisPost(); + }}> 有帮助 {post?.likes} - ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx index 005da03..7e49dde 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx @@ -26,8 +26,8 @@ export function StatsSection() { - +
From 8ce5d689c29c6f31912cfdcb3f1bb4c949a16219 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sun, 26 Jan 2025 19:33:45 +0800 Subject: [PATCH 4/5] add 0126 --- .../common/uploader/TusUploader.tsx | 247 ++++++++---------- .../web/src/components/layout/main/Header.tsx | 44 +--- .../src/components/models/post/LetterCard.tsx | 4 +- .../models/post/detail/PostCommentCard.tsx | 8 +- .../models/post/detail/PostCommentEditor.tsx | 25 +- .../models/post/detail/PostHeader/Header.tsx | 15 +- .../post/detail/PostHeader/PostHateButton.tsx | 4 +- .../post/editor/form/LetterBasicForm.tsx | 34 ++- .../components/presentation/CustomAvatar.tsx | 6 +- 9 files changed, 185 insertions(+), 202 deletions(-) diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index c41ba95..3aa7ad0 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -4,7 +4,7 @@ import { CheckCircleOutlined, DeleteOutlined, } from "@ant-design/icons"; -import { Upload, Progress, Button } from "antd"; +import { Upload, Progress, Button } from "antd"; import type { UploadFile } from "antd"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; import toast from "react-hot-toast"; @@ -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(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; - } + setUploadingFiles((prev) => [ + ...prev, + { + name: file.name, + progress: 0, + status: "uploading", + fileKey, + }, + ]); - const newFiles: UploadingFile[] = files.map((f) => ({ - name: f.name, - progress: 0, - status: "uploading" as const, - fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识 - })); + handleFileUpload( + file, + (result) => { + setCompletedFiles((prev) => [ + ...prev, + { + name: file.name, + progress: 100, + status: "done", + fileId: result.fileId, + }, + ]); - 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( - (resolve, reject) => { - handleFileUpload( - f as File, - (result) => { - console.log("上传成功:", result); - const completedFile = { - name: f.name, - progress: 100, - status: "done" as const, - fileId: result.fileId, - }; - setCompletedFiles((prev) => [ - ...prev, - completedFile, - ]); - setUploadingFiles((prev) => - prev.filter( - (file) => file.fileKey !== fileKey - ) - ); - resolve(result.fileId); - }, - (error) => { - console.error("上传错误:", error); - reject(error); - }, - fileKey - ); - } + setUploadingFiles((prev) => + prev.filter((f) => f.fileKey !== 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" })) - ); - } + // 正确的状态更新方式 + setUploadResults((prev) => { + const newValue = [...prev, result.fileId]; + onChange?.(newValue); // 传递值而非函数 + return newValue; + }); + }, + (error) => { + console.error("上传错误:", error); + toast.error( + `上传失败: ${ + error instanceof Error ? error.message : "未知错误" + }` + ); + setUploadingFiles((prev) => + prev.map((f) => + f.fileKey === fileKey + ? { ...f, status: "error" } + : f + ) + ); + }, + fileKey + ); + return false; }, - [uploadResults, onChange, handleFileUpload] + [handleFileUpload, onChange] ); return (
+ beforeUpload={handleBeforeUpload}>

@@ -140,64 +127,58 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { 点击或拖拽文件到此区域进行上传

支持单个或批量上传文件

- {/* 正在上传的文件 */} - {(uploadingFiles.length > 0 || completedFiles.length > 0) && ( -
- {uploadingFiles.map((file) => ( -
-
-
{file.name}
-
- + {/* 上传状态展示 */} +
+ {/* 上传中的文件 */} + {uploadingFiles.map((file) => ( +
+
+ {file.name}
- ))} - {completedFiles.length > 0 && - completedFiles.map((file, index) => ( -
-
- -
- {file.name} -
-
-
- ))} -
- )} + +
+ ))} + + {/* 已完成的文件 */} + {completedFiles.map((file) => ( +
+
+ + {file.name} +
+
+ ))} +
); diff --git a/apps/web/src/components/layout/main/Header.tsx b/apps/web/src/components/layout/main/Header.tsx index fe05d4f..8c5ae80 100644 --- a/apps/web/src/components/layout/main/Header.tsx +++ b/apps/web/src/components/layout/main/Header.tsx @@ -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 (
-
+
- {/** 在这里放置logo */} - {isLoading ? ( -
- ) : ( - logoUrl && ( - Logo - ) - )} + + 首长机关信箱 + +

+ 聆怀若水,应语如风;纾难化困,践诺成春 +

diff --git a/apps/web/src/components/models/post/LetterCard.tsx b/apps/web/src/components/models/post/LetterCard.tsx index 61b8393..39715e7 100644 --- a/apps/web/src/components/models/post/LetterCard.tsx +++ b/apps/web/src/components/models/post/LetterCard.tsx @@ -49,7 +49,9 @@ export function LetterCard({ letter }: LetterCardProps) {
- {letter.author?.showname || "匿名用户"} + {letter?.meta?.signature || + letter.author?.showname || + "匿名用户"}
diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 7cef4fd..5a7bc30 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -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}> + randomString={ + post?.meta?.signature || post?.meta?.ip + }>
- {post.author?.showname || "匿名用户"} + {post?.meta?.signature || + post.author?.showname || + "匿名用户"} {dayjs(post?.createdAt).format( diff --git a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx index 322f9d6..74df927 100644 --- a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx @@ -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(undefined); const [fileIds, setFileIds] = useState([]); 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() {
{ + console.log("ids", value); setFileIds(value); }} /> @@ -90,12 +96,23 @@ export default function PostCommentEditor() { {!isContentEmpty(content) && ( -
+
+ + { + setSignature(e.target.value); + }} + showCount + placeholder="签名" + />
- - 发件人 - {post?.author?.showname || "匿名用户"} + {post?.meta?.signature || + post?.author?.showname || + "匿名用户"} - 收件人 {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 || "匿名用户"} ))} {/* Date Info Badge */} - - 创建于 - {dayjs(post?.createdAt).format("YYYY-MM-DD")} {/* Last Updated Badge */} - 最后更新于 - {dayjs(post?.updatedAt).format("YYYY-MM-DD")} diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx index a19dfec..f93603e 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/PostHateButton.tsx @@ -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" diff --git a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index 4252c4e..5b3312b 100644 --- a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -79,8 +79,7 @@ export function LetterBasicForm() { -
- +
form.setFieldValue( @@ -89,8 +88,7 @@ export function LetterBasicForm() { ) } /> - -
+
@@ -101,14 +99,26 @@ export function LetterBasicForm() { 是否公开 - +
+ + + + +
diff --git a/apps/web/src/components/presentation/CustomAvatar.tsx b/apps/web/src/components/presentation/CustomAvatar.tsx index 53c931f..5883497 100644 --- a/apps/web/src/components/presentation/CustomAvatar.tsx +++ b/apps/web/src/components/presentation/CustomAvatar.tsx @@ -6,14 +6,14 @@ import multiavatar from "@multiavatar/multiavatar"; interface CustomAvatarProps extends Omit { 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 ( Date: Sun, 26 Jan 2025 19:36:47 +0800 Subject: [PATCH 5/5] adsd --- apps/web/src/app/auth/register.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/auth/register.tsx b/apps/web/src/app/auth/register.tsx index eb2791a..bfbfeca 100644 --- a/apps/web/src/app/auth/register.tsx +++ b/apps/web/src/app/auth/register.tsx @@ -134,12 +134,12 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => { rules={[ { required: true, message: "请输入密码" }, { min: 8, message: "密码至少需要8个字符" }, - { - pattern: - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, - message: - "密码必须包含大小写字母、数字和特殊字符", - }, + // { + // pattern: + // /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, + // message: + // "密码必须包含大小写字母、数字和特殊字符", + // }, ]}> @@ -150,7 +150,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => { label="证件号" noStyle rules={[ - { required: true, message: "请输入证件号" }, + { message: "请输入证件号" }, { pattern: /^\d{5,12}$/, message: "请输入有效的证件号(5-12位数字)",