This commit is contained in:
longdayi 2025-03-05 21:55:55 +08:00
commit dc7917f75d
15 changed files with 243 additions and 172 deletions

View File

@ -1,21 +1,29 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { DepartmentService } from './department.service'; // assuming it's in the same directory import { DepartmentService } from './department.service'; // assuming it's in the same directory
import { DepartmentMethodSchema, Prisma, UpdateOrderSchema } from '@nice/common'; import {
DepartmentMethodSchema,
Prisma,
UpdateOrderSchema,
} from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { DepartmentRowService } from './department.row.service'; import { DepartmentRowService } from './department.row.service';
const DepartmentCreateArgsSchema: ZodType<Prisma.DepartmentCreateArgs> = z.any() const DepartmentCreateArgsSchema: ZodType<Prisma.DepartmentCreateArgs> =
const DepartmentUpdateArgsSchema: ZodType<Prisma.DepartmentUpdateArgs> = z.any() z.any();
const DepartmentFindFirstArgsSchema: ZodType<Prisma.DepartmentFindFirstArgs> = z.any() const DepartmentUpdateArgsSchema: ZodType<Prisma.DepartmentUpdateArgs> =
const DepartmentFindManyArgsSchema: ZodType<Prisma.DepartmentFindManyArgs> = z.any() z.any();
const DepartmentFindFirstArgsSchema: ZodType<Prisma.DepartmentFindFirstArgs> =
z.any();
const DepartmentFindManyArgsSchema: ZodType<Prisma.DepartmentFindManyArgs> =
z.any();
@Injectable() @Injectable()
export class DepartmentRouter { export class DepartmentRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly departmentService: DepartmentService, // 注入 DepartmentService private readonly departmentService: DepartmentService, // 注入 DepartmentService
private readonly departmentRowService: DepartmentRowService private readonly departmentRowService: DepartmentRowService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
// 创建部门 // 创建部门
create: this.trpc.protectProcedure create: this.trpc.protectProcedure
@ -36,8 +44,10 @@ export class DepartmentRouter {
return this.departmentService.softDeleteByIds(input.ids); return this.departmentService.softDeleteByIds(input.ids);
}), }),
// 更新部门顺序 // 更新部门顺序
updateOrder: this.trpc.protectProcedure.input(UpdateOrderSchema).mutation(async ({ input }) => { updateOrder: this.trpc.protectProcedure
return this.departmentService.updateOrder(input) .input(UpdateOrderSchema)
.mutation(async ({ input }) => {
return this.departmentService.updateOrder(input);
}), }),
// 查询多个部门 // 查询多个部门
findMany: this.trpc.procedure findMany: this.trpc.procedure
@ -53,13 +63,15 @@ export class DepartmentRouter {
}), }),
// 获取子部门的简单树结构 // 获取子部门的简单树结构
getChildSimpleTree: this.trpc.procedure getChildSimpleTree: this.trpc.procedure
.input(DepartmentMethodSchema.getSimpleTree).query(async ({ input }) => { .input(DepartmentMethodSchema.getSimpleTree)
return await this.departmentService.getChildSimpleTree(input) .query(async ({ input }) => {
return await this.departmentService.getChildSimpleTree(input);
}), }),
// 获取父部门的简单树结构 // 获取父部门的简单树结构
getParentSimpleTree: this.trpc.procedure getParentSimpleTree: this.trpc.procedure
.input(DepartmentMethodSchema.getSimpleTree).query(async ({ input }) => { .input(DepartmentMethodSchema.getSimpleTree)
return await this.departmentService.getParentSimpleTree(input) .query(async ({ input }) => {
return await this.departmentService.getParentSimpleTree(input);
}), }),
// 获取部门行数据 // 获取部门行数据
getRows: this.trpc.protectProcedure getRows: this.trpc.protectProcedure

View File

@ -61,6 +61,18 @@ export class StaffRouter {
.query(async ({ input }) => { .query(async ({ input }) => {
return await this.staffService.findMany(input); return await this.staffService.findMany(input);
}), }),
findManyWithPagination: this.trpc.procedure
.input(
z.object({
page: z.number().optional(),
pageSize: z.number().optional(),
where: StaffWhereInputSchema.optional(),
select: StaffSelectSchema.optional(),
}),
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => {
return await this.staffService.findManyWithPagination(input);
}),
getRows: this.trpc.protectProcedure getRows: this.trpc.protectProcedure
.input(StaffMethodSchema.getRows) .input(StaffMethodSchema.getRows)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {

View File

@ -15,13 +15,13 @@ import { z } from 'zod';
import { isFieldCondition } from '../base/sql-builder'; import { isFieldCondition } from '../base/sql-builder';
@Injectable() @Injectable()
export class StaffRowService extends RowCacheService { export class StaffRowService extends RowCacheService {
constructor( constructor(private readonly departmentService: DepartmentService) {
private readonly departmentService: DepartmentService,
) {
super(ObjectType.STAFF, false); super(ObjectType.STAFF, false);
} }
createUnGroupingRowSelect(request?: RowModelRequest): string[] { createUnGroupingRowSelect(request?: RowModelRequest): string[] {
const result = super.createUnGroupingRowSelect(request).concat([ const result = super
.createUnGroupingRowSelect(request)
.concat([
`${this.tableName}.id AS id`, `${this.tableName}.id AS id`,
`${this.tableName}.username AS username`, `${this.tableName}.username AS username`,
`${this.tableName}.showname AS showname`, `${this.tableName}.showname AS showname`,
@ -33,7 +33,7 @@ export class StaffRowService extends RowCacheService {
'dept.name AS dept_name', 'dept.name AS dept_name',
'domain.name AS domain_name', 'domain.name AS domain_name',
]); ]);
return result return result;
} }
createJoinSql(request?: RowModelRequest): string[] { createJoinSql(request?: RowModelRequest): string[] {
return [ return [
@ -94,17 +94,13 @@ export class StaffRowService extends RowCacheService {
const deptId = data?.deptId; const deptId = data?.deptId;
const isFromSameDept = staff.deptIds?.includes(deptId); const isFromSameDept = staff.deptIds?.includes(deptId);
const domainChildDeptIds = await this.departmentService.getDescendantIds( const domainChildDeptIds = await this.departmentService.getDescendantIds(
staff.domainId, true staff.domainId,
); true,
const belongsToDomain = domainChildDeptIds.includes(
deptId,
); );
const belongsToDomain = domainChildDeptIds.includes(deptId);
return { isFromSameDept, belongsToDomain }; return { isFromSameDept, belongsToDomain };
} }
protected async setResPermissions( protected async setResPermissions(data: Staff, staff: UserProfile) {
data: Staff,
staff: UserProfile,
) {
const permissions: ResPerm = {}; const permissions: ResPerm = {};
const { isFromSameDept, belongsToDomain } = await this.getPermissionContext( const { isFromSameDept, belongsToDomain } = await this.getPermissionContext(
data.id, data.id,
@ -131,5 +127,4 @@ export class StaffRowService extends RowCacheService {
}); });
return { ...data, perm: permissions }; return { ...data, perm: permissions };
} }
} }

View File

@ -14,7 +14,6 @@ import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()
export class StaffService extends BaseService<Prisma.StaffDelegate> { export class StaffService extends BaseService<Prisma.StaffDelegate> {
constructor(private readonly departmentService: DepartmentService) { constructor(private readonly departmentService: DepartmentService) {
super(db, ObjectType.STAFF, true); super(db, ObjectType.STAFF, true);
} }
@ -25,7 +24,10 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
*/ */
async findByDept(data: z.infer<typeof StaffMethodSchema.findByDept>) { async findByDept(data: z.infer<typeof StaffMethodSchema.findByDept>) {
const { deptId, domainId } = data; const { deptId, domainId } = data;
const childDepts = await this.departmentService.getDescendantIds(deptId, true); const childDepts = await this.departmentService.getDescendantIds(
deptId,
true,
);
const result = await db.staff.findMany({ const result = await db.staff.findMany({
where: { where: {
deptId: { in: childDepts }, deptId: { in: childDepts },
@ -50,7 +52,9 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
await this.validateUniqueFields(data, where.id); await this.validateUniqueFields(data, where.id);
const updateData = { const updateData = {
...data, ...data,
...(data.password && { password: await argon2.hash(data.password as string) }) ...(data.password && {
password: await argon2.hash(data.password as string),
}),
}; };
const result = await super.update({ ...args, data: updateData }); const result = await super.update({ ...args, data: updateData });
this.emitDataChangedEvent(result, CrudOperation.UPDATED); this.emitDataChangedEvent(result, CrudOperation.UPDATED);
@ -58,17 +62,26 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
} }
private async validateUniqueFields(data: any, excludeId?: string) { private async validateUniqueFields(data: any, excludeId?: string) {
const uniqueFields = [ const uniqueFields = [
{ field: 'officerId', errorMsg: (val: string) => `证件号为${val}的用户已存在` }, {
{ field: 'phoneNumber', errorMsg: (val: string) => `手机号为${val}的用户已存在` }, field: 'officerId',
{ field: 'username', errorMsg: (val: string) => `帐号为${val}的用户已存在` } errorMsg: (val: string) => `证件号为${val}的用户已存在`,
},
{
field: 'phoneNumber',
errorMsg: (val: string) => `手机号为${val}的用户已存在`,
},
{
field: 'username',
errorMsg: (val: string) => `帐号为${val}的用户已存在`,
},
]; ];
for (const { field, errorMsg } of uniqueFields) { for (const { field, errorMsg } of uniqueFields) {
if (data[field]) { if (data[field]) {
const count = await db.staff.count({ const count = await db.staff.count({
where: { where: {
[field]: data[field], [field]: data[field],
...(excludeId && { id: { not: excludeId } }) ...(excludeId && { id: { not: excludeId } }),
} },
}); });
if (count > 0) { if (count > 0) {
throw new Error(errorMsg(data[field])); throw new Error(errorMsg(data[field]));
@ -77,9 +90,8 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
} }
} }
private emitDataChangedEvent(data: any, operation: CrudOperation) { private emitDataChangedEvent(data: any, operation: CrudOperation) {
EventBus.emit("dataChanged", { EventBus.emit('dataChanged', {
type: this.objectType, type: this.objectType,
operation, operation,
data, data,
@ -92,7 +104,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
* @returns * @returns
*/ */
async updateUserDomain(data: { domainId?: string }, staff?: UserProfile) { async updateUserDomain(data: { domainId?: string }, staff?: UserProfile) {
let { domainId } = data; const { domainId } = data;
if (staff.domainId !== domainId) { if (staff.domainId !== domainId) {
const result = await this.update({ const result = await this.update({
where: { id: staff.id }, where: { id: staff.id },
@ -107,7 +119,6 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
} }
} }
// /** // /**
// * 根据关键词或ID集合查找员工 // * 根据关键词或ID集合查找员工
// * @param data 包含关键词、域ID和ID集合的对象 // * @param data 包含关键词、域ID和ID集合的对象
@ -176,5 +187,4 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
// return combinedResults; // return combinedResults;
// } // }
} }

View File

@ -107,10 +107,10 @@ export async function updatePostViewCount(id: string, type: VisitType) {
where: { id: course.id }, where: { id: course.id },
data: { data: {
[metaFieldMap[type]]: courseViews._sum.views || 0, [metaFieldMap[type]]: courseViews._sum.views || 0,
meta: { // meta: {
...((post?.meta as any) || {}), // ...((post?.meta as any) || {}),
[metaFieldMap[type]]: courseViews._sum.views || 0, // [metaFieldMap[type]]: courseViews._sum.views || 0,
}, // },
}, },
}); });
} }
@ -127,10 +127,10 @@ export async function updatePostViewCount(id: string, type: VisitType) {
where: { id }, where: { id },
data: { data: {
[metaFieldMap[type]]: totalViews._sum.views || 0, [metaFieldMap[type]]: totalViews._sum.views || 0,
meta: { // meta: {
...((post?.meta as any) || {}), // ...((post?.meta as any) || {}),
[metaFieldMap[type]]: totalViews._sum.views || 0, // [metaFieldMap[type]]: totalViews._sum.views || 0,
}, // },
}, },
}); });
} }

View File

@ -20,9 +20,11 @@ import { useTusUpload } from "@web/src/hooks/useTusUpload";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { MIND_OPTIONS } from "./constant"; import { MIND_OPTIONS } from "./constant";
import { SaveOutlined } from "@ant-design/icons"; import { LinkOutlined, SaveOutlined } from "@ant-design/icons";
import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButton"; import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButton";
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext"; import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
export default function MindEditor({ id }: { id?: string }) { export default function MindEditor({ id }: { id?: string }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { const {
@ -57,6 +59,17 @@ export default function MindEditor({ id }: { id?: string }) {
}); });
const { handleFileUpload } = useTusUpload(); const { handleFileUpload } = useTusUpload();
const [form] = Form.useForm(); const [form] = Form.useForm();
const CustomLinkIconPlugin = (mind) => {
mind.bus.addListener('operation', async () => {
const hyperLinkElement = await document.querySelectorAll('.hyper-link');
console.log('hyperLinkElement', hyperLinkElement);
hyperLinkElement.forEach((item) => {
const hyperLinkDom = createRoot(item)
hyperLinkDom.render(<LinkOutlined />)
});
});
};
useEffect(() => { useEffect(() => {
if (post?.id && id) { if (post?.id && id) {
read.mutateAsync({ read.mutateAsync({
@ -98,6 +111,7 @@ export default function MindEditor({ id }: { id?: string }) {
nodeMenu: canEdit, // 禁用节点右键菜单 nodeMenu: canEdit, // 禁用节点右键菜单
keypress: canEdit, // 禁用键盘快捷键 keypress: canEdit, // 禁用键盘快捷键
}); });
mind.install(CustomLinkIconPlugin);
mind.init(MindElixir.new("新思维导图")); mind.init(MindElixir.new("新思维导图"));
containerRef.current.hidden = true; containerRef.current.hidden = true;
//挂载实例 //挂载实例
@ -173,16 +187,13 @@ export default function MindEditor({ id }: { id?: string }) {
} }
console.log(result); console.log(result);
}, },
(error) => {}, (error) => { },
`mind-thumb-${new Date().toString()}` `mind-thumb-${new Date().toString()}`
); );
}; };
useEffect(() => { useEffect(() => {
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
}, []); }, []);
useEffect(()=>{
console.log(canEdit,user?.id,post?.author?.id)
})
return ( return (
<div className={` flex-col flex `}> <div className={` flex-col flex `}>
{taxonomies && ( {taxonomies && (

View File

@ -1,6 +1,6 @@
// components/CourseDetailDisplayArea.tsx // components/CourseDetailDisplayArea.tsx
import { motion, useScroll, useTransform } from "framer-motion"; import { motion, useScroll, useTransform } from "framer-motion";
import React, { useContext, useRef, useState } from "react"; import React, { useContext, useEffect, useRef, useState } from "react";
import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer"; import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer";
import { CourseDetailDescription } from "./CourseDetailDescription"; import { CourseDetailDescription } from "./CourseDetailDescription";
import { Course, LectureType, PostType } from "@nice/common"; import { Course, LectureType, PostType } from "@nice/common";
@ -40,7 +40,7 @@ export const CourseDetailDisplayArea: React.FC = () => {
opacity: videoOpacity, opacity: videoOpacity,
}} }}
className="w-full bg-black rounded-lg "> className="w-full bg-black rounded-lg ">
<div className=" w-full "> <div className=" w-full cursor-pointer">
<VideoPlayer src={lecture?.meta?.videoUrl} /> <VideoPlayer src={lecture?.meta?.videoUrl} />
</div> </div>
</motion.div> </motion.div>

View File

@ -28,6 +28,7 @@ export default function PostList({
renderItem, renderItem,
}: PostListProps) { }: PostListProps) {
const [currentPage, setCurrentPage] = useState<number>(params?.page || 1); const [currentPage, setCurrentPage] = useState<number>(params?.page || 1);
const { data, isLoading }: PostPagnationProps = const { data, isLoading }: PostPagnationProps =
api.post.findManyWithPagination.useQuery({ api.post.findManyWithPagination.useQuery({
select: courseDetailSelect, select: courseDetailSelect,

View File

@ -91,6 +91,7 @@ export default function PostSelect({
dropdownStyle={{ dropdownStyle={{
minWidth: 200, // 设置合适的最小宽度 minWidth: 200, // 设置合适的最小宽度
}} }}
autoClearSearchValue
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
filterOption={false} filterOption={false}

View File

@ -25,6 +25,7 @@ export const VideoDisplay: React.FC<VideoDisplayProps> = ({
isDragging, isDragging,
setIsDragging, setIsDragging,
progressRef, progressRef,
isPlaying
} = useContext(VideoPlayerContext); } = useContext(VideoPlayerContext);
// 处理进度条拖拽 // 处理进度条拖拽
@ -191,9 +192,20 @@ export const VideoDisplay: React.FC<VideoDisplayProps> = ({
}; };
}, [src, onError, autoPlay]); }, [src, onError, autoPlay]);
const handleVideoClick = () => {
if (videoRef.current && isPlaying) {
videoRef.current.pause();
setIsPlaying(false);
}else if (videoRef.current && !isPlaying) {
videoRef.current.play();
setIsPlaying(true);
}
};
return ( return (
<div className="relative w-full aspect-video"> <div className="relative w-full aspect-video" >
<video <video
onClick={handleVideoClick}
ref={videoRef} ref={videoRef}
className="w-full h-full" className="w-full h-full"
poster={poster} poster={poster}

View File

@ -0,0 +1,13 @@
import { Staff } from "packages/common/dist";
import { api } from "packages/client/dist";
export const StaffCard = (staff: Staff) => {
const { data } = api.staff.findManyWithPagination.useQuery({
page
});
return(
<div className="staff-card">
<div className="staff-card__username">${username}</div>
</div>
)
}

View File

@ -0,0 +1,6 @@
import { api } from "packages/client/dist";
import { ReactNode } from "react";
export default function StaffList({ renderItem }: { renderItem: ReactNode }) {
const { data } = api.staff.findManyWithPagination.useQuery({});
data.items.map((staff) => renderItem);
}

View File

@ -72,9 +72,7 @@ export const routes: CustomRouteObject[] = [
{ {
path: "editor/:id?", path: "editor/:id?",
element: ( element: (
<WithAuth>
<PathEditorPage></PathEditorPage> <PathEditorPage></PathEditorPage>
</WithAuth>
), ),
}, },
], ],

View File

@ -2,7 +2,7 @@ server {
# 监听80端口 # 监听80端口
listen 80; listen 80;
# 服务器域名/IP地址使用环境变量 # 服务器域名/IP地址使用环境变量
server_name 192.168.252.77; server_name host.docker.internal;
# 基础性能优化配置 # 基础性能优化配置
# 启用tcp_nopush以优化数据发送 # 启用tcp_nopush以优化数据发送
@ -100,7 +100,7 @@ server {
# 仅供内部使用 # 仅供内部使用
internal; internal;
# 代理到认证服务 # 代理到认证服务
proxy_pass http://192.168.252.77:3000/auth/file; proxy_pass http://host.docker.internal:3000/auth/file;
# 请求优化:不传递请求体 # 请求优化:不传递请求体
proxy_pass_request_body off; proxy_pass_request_body off;

View File

@ -15,7 +15,7 @@ done
if [ -f "/usr/share/nginx/html/index.html" ]; then if [ -f "/usr/share/nginx/html/index.html" ]; then
# Use envsubst to replace environment variable placeholders # Use envsubst to replace environment variable placeholders
echo "Processing /usr/share/nginx/html/index.html" echo "Processing /usr/share/nginx/html/index.html"
envsubst < /usr/share/nginx/html/index.template > /usr/share/nginx/html/index.html.tmp envsubst < /usr/share/nginx/html/index.temp > /usr/share/nginx/html/index.html.tmp
mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html
echo "Processed content:" echo "Processed content:"
cat /usr/share/nginx/html/index.html cat /usr/share/nginx/html/index.html