This commit is contained in:
Rao 2025-03-27 13:26:13 +08:00
parent 23daab3b3b
commit 1fc1aa368c
13 changed files with 175 additions and 144 deletions

View File

@ -1,5 +1,5 @@
# 基础镜像
FROM node:18.17-alpine as base
FROM node:18-alpine as base
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/v3.18/community" >> /etc/apk/repositories

View File

@ -125,5 +125,14 @@ export class PostRouter {
const { staff } = ctx;
return await this.postService.updateOrderByIds(input.ids);
}),
softDeletePostDescendant:this.trpc.protectProcedure
.input(
z.object({
ancestorId:z.string()
})
)
.mutation(async ({ input })=>{
return await this.postService.softDeletePostDescendant(input)
})
});
}

View File

@ -101,6 +101,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
},
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
) {
const { courseDetail } = args;
// If no transaction is provided, create a new one
if (!params.tx) {
@ -295,40 +296,33 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
staff?.id && {
authorId: staff.id,
},
// staff?.id && {
// watchableStaffs: {
// some: {
// id: staff.id,
// },
// },
// },
// deptId && {
// watchableDepts: {
// some: {
// id: {
// in: parentDeptIds,
// },
// },
// },
// },
// {
// AND: [
// {
// watchableStaffs: {
// none: {}, // 匹配 watchableStaffs 为空
// },
// },
// {
// watchableDepts: {
// none: {}, // 匹配 watchableDepts 为空
// },
// },
// ],
// },
].filter(Boolean);
if (orCondition?.length > 0) return orCondition;
return undefined;
}
async softDeletePostDescendant(args:{ancestorId?:string}){
const { ancestorId } = args
const descendantIds = []
await db.postAncestry.findMany({
where:{
ancestorId,
},
select:{
descendantId:true
}
}).then(res=>{
res.forEach(item=>{
descendantIds.push(item.descendantId)
})
})
console.log(descendantIds)
const result = super.softDeleteByIds([...descendantIds,ancestorId])
EventBus.emit('dataChanged', {
type: ObjectType.POST,
operation: CrudOperation.DELETED,
data: result,
});
return result
}
}

View File

@ -25,6 +25,8 @@ import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButto
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { useQueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
export default function MindEditor({ id }: { id?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const {
@ -36,6 +38,7 @@ export default function MindEditor({ id }: { id?: string }) {
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
const { isAuthenticated, user, hasSomePermissions } = useAuth();
const { read } = useVisitor();
const queryClient = useQueryClient();
// const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
// api.post.findFirst.useQuery(
// {
@ -46,7 +49,11 @@ export default function MindEditor({ id }: { id?: string }) {
// },
// { enabled: Boolean(id) }
// );
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
onSuccess:()=>{
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
}
})
const canEdit: boolean = useMemo(() => {
const isAuth = isAuthenticated && user?.id === post?.author?.id;
return (
@ -63,7 +70,7 @@ export default function MindEditor({ id }: { id?: string }) {
const { handleFileUpload } = useTusUpload();
const [form] = Form.useForm();
const handleIcon = () => {
const hyperLinkElement =document.querySelectorAll(".hyper-link");
const hyperLinkElement = document.querySelectorAll(".hyper-link");
console.log("hyperLinkElement", hyperLinkElement);
hyperLinkElement.forEach((item) => {
const hyperLinkDom = createRoot(item);
@ -212,6 +219,12 @@ export default function MindEditor({ id }: { id?: string }) {
`mind-thumb-${new Date().toString()}`
);
};
const handleDelete = async () => {
await softDeletePostDescendant.mutateAsync({
ancestorId: id,
});
navigate("/path");
}
useEffect(() => {
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
}, []);
@ -252,14 +265,28 @@ export default function MindEditor({ id }: { id?: string }) {
</div>
<div>
{canEdit && (
<Button
ghost
type="primary"
icon={<SaveOutlined></SaveOutlined>}
onSubmit={(e) => e.preventDefault()}
onClick={handleSave}>
{id ? "更新" : "保存"}
</Button>
<>
{
id && (
<Button
danger
icon={<SaveOutlined></SaveOutlined>}
onSubmit={(e) => e.preventDefault()}
onClick={handleDelete}>
</Button>
)
}
<Button
className="ml-4"
ghost
type="primary"
icon={<SaveOutlined></SaveOutlined>}
onSubmit={(e) => e.preventDefault()}
onClick={handleSave}>
{id ? "更新" : "保存"}
</Button>
</>
)}
</div>
</div>

View File

@ -41,7 +41,7 @@ export const CourseDetailDisplayArea: React.FC = () => {
}}
className="w-full bg-black rounded-lg ">
<div className=" w-full cursor-pointer">
<ReactPlayer
{/* <ReactPlayer
url={lecture?.meta?.videoUrl}
controls={true}
width="100%"
@ -49,8 +49,8 @@ export const CourseDetailDisplayArea: React.FC = () => {
onError={(error) => {
console.log(error);
}}
/>
{/* <VideoPlayer src={lecture?.meta?.videoUrl} /> */}
/> */}
<VideoPlayer src={lecture?.meta?.videoUrl} />
</div>
</motion.div>
</div>

View File

@ -1,97 +1,40 @@
import { useAuth } from "@web/src/providers/auth-provider";
import { useContext, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { CourseDetailContext } from "../PostDetailContext";
import { useStaff } from "@nice/client";
import { api } from "@nice/client";
import {
CheckCircleOutlined,
CloseCircleOutlined,
DeleteTwoTone,
EditTwoTone,
LoginOutlined,
ExclamationCircleFilled,
} from "@ant-design/icons";
import toast from "react-hot-toast";
import JoinButton from "./JoinButton";
import { Modal } from "antd";
import { useQueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
export default function CourseOperationBtns() {
// const { isAuthenticated, user } = useAuth();
const navigate = useNavigate();
const { post, canEdit, userIsLearning, setUserIsLearning } =
useContext(CourseDetailContext);
// const { update } = useStaff();
// const [isHovered, setIsHovered] = useState(false);
// const toggleLearning = async () => {
// if (!userIsLearning) {
// await update.mutateAsync({
// where: { id: user?.id },
// data: {
// learningPosts: {
// connect: { id: course.id },
// },
// },
// });
// setUserIsLearning(true);
// toast.success("加入学习成功");
// } else {
// await update.mutateAsync({
// where: { id: user?.id },
// data: {
// learningPosts: {
// disconnect: {
// id: course.id,
// },
// },
// },
// });
// toast.success("退出学习成功");
// setUserIsLearning(false);
// }
// };
const { post, canEdit } = useContext(CourseDetailContext);
return (
<>
{/* {isAuthenticated && (
<div
onClick={toggleLearning}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`flex px-1 py-0.5 gap-1 hover:cursor-pointer transition-all ${
userIsLearning
? isHovered
? "text-red-500 border-red-500 rounded-md "
: "text-green-500 "
: "text-primary "
}`}>
{userIsLearning ? (
isHovered ? (
<CloseCircleOutlined />
) : (
<CheckCircleOutlined />
)
) : (
<LoginOutlined />
)}
<span>
{userIsLearning
? isHovered
? "退出学习"
: "正在学习"
: "加入学习"}
</span>
</div>
)} */}
<JoinButton></JoinButton>
{canEdit && (
<div
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
onClick={() => {
const url = post?.id
? `/course/${post?.id}/editor`
: "/course/editor";
navigate(url);
}}>
<EditTwoTone></EditTwoTone>
{"编辑课程"}
</div>
<>
<div
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
onClick={() => {
const url = post?.id
? `/course/${post?.id}/editor`
: "/course/editor";
navigate(url);
}}>
<EditTwoTone></EditTwoTone>
{"编辑课程"}
</div>
</>
)}
</>
);

View File

@ -13,6 +13,8 @@ import { api, usePost } from "@nice/client";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useAuth } from "@web/src/providers/auth-provider";
import { getQueryKey } from "@trpc/react-query";
import { useQueryClient } from "@tanstack/react-query";
export type CourseFormData = {
title: string;
@ -26,6 +28,7 @@ export type CourseFormData = {
interface CourseEditorContextType {
onSubmit: (values: CourseFormData) => Promise<void>;
handleDeleteCourse: () => Promise<void>;
editId?: string;
course?: CourseDto;
taxonomies?: Taxonomy[]; // 根据实际类型调整
@ -45,6 +48,7 @@ export function CourseFormProvider({
}: CourseFormProviderProps) {
const [form] = Form.useForm<CourseFormData>();
const { create, update, createCourse } = usePost();
const queryClient = useQueryClient();
const { user } = useAuth();
const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery(
{
@ -60,6 +64,11 @@ export function CourseFormProvider({
} = api.taxonomy.getAll.useQuery({
type: ObjectType.COURSE,
});
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
onSuccess:()=>{
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
}
})
const navigate = useNavigate();
useEffect(() => {
@ -92,7 +101,15 @@ export function CourseFormProvider({
form.setFieldsValue(formData);
}
}, [course, form]);
const handleDeleteCourse = async () => {
if(editId){
await softDeletePostDescendant.mutateAsync({
ancestorId: editId,
});
navigate("/courses");
}
}
const onSubmit = async (values: any) => {
const sections = values?.sections || [];
const deptIds = values?.deptIds || [];
@ -172,6 +189,7 @@ export function CourseFormProvider({
course,
taxonomies,
form,
handleDeleteCourse
}}>
<Form
// requiredMark="optional"

View File

@ -25,8 +25,11 @@ import { CourseSectionEmpty } from "./CourseSectionEmpty";
import { SortableSection } from "./SortableSection";
import { LectureList } from "./LectureList";
import toast from "react-hot-toast";
import { useQueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
const CourseContentForm: React.FC = () => {
const queryClient = useQueryClient();
const { editId } = useCourseEditor();
const sensors = useSensors(
useSensor(PointerSensor),
@ -71,7 +74,11 @@ const CourseContentForm: React.FC = () => {
ids: newItems.map((item) => item.id),
});
};
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
onSuccess:()=>{
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
}
})
return (
<div className="max-w-4xl mx-auto p-6">
<CourseContentFormHeader />
@ -93,8 +100,8 @@ const CourseContentForm: React.FC = () => {
field={section}
remove={async () => {
if (section?.id) {
await softDeleteByIds.mutateAsync({
ids: [section.id],
await softDeletePostDescendant.mutateAsync({
ancestorId: section.id,
});
}
setItems(sections);

View File

@ -1,10 +1,11 @@
import { ArrowLeftOutlined, ClockCircleOutlined } from "@ant-design/icons";
import { Button, Tag, Typography } from "antd";
import { ArrowLeftOutlined, ClockCircleOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import { Button, Modal, Tag, Typography } from "antd";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { CourseStatus, CourseStatusLabel } from "@nice/common";
import { useCourseEditor } from "../context/CourseEditorContext";
import { useAuth } from "@web/src/providers/auth-provider";
import toast from "react-hot-toast";
const { Title } = Typography;
@ -18,8 +19,8 @@ const courseStatusVariant: Record<CourseStatus, string> = {
export default function CourseEditorHeader() {
const navigate = useNavigate();
const { user, hasSomePermissions } = useAuth();
const { onSubmit, course, form } = useCourseEditor();
const { confirm } = Modal;
const { onSubmit, course, form, handleDeleteCourse, editId } = useCourseEditor();
const handleSave = () => {
try {
@ -30,7 +31,26 @@ export default function CourseEditorHeader() {
console.log(err);
}
};
const showDeleteConfirm = () => {
confirm({
title: '确定删除该课程吗',
icon: <ExclamationCircleFilled />,
content: '',
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
console.log('OK');
console.log(editId)
await handleDeleteCourse()
toast.success('课程已删除')
navigate("/courses");
},
onCancel() {
console.log('Cancel');
},
});
};
return (
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 z-10">
<div className="h-full flex items-center justify-between px-3 md:px-4">
@ -70,16 +90,27 @@ export default function CourseEditorHeader() {
)} */}
</div>
</div>
<Button
type="primary"
// size="small"
onClick={handleSave}
<div>
{editId &&
<Button
danger
onClick={showDeleteConfirm}
>
</Button>
}
<Button
className="ml-4"
type="primary"
// size="small"
onClick={handleSave}
// disabled={form
// .getFieldsError()
// .some(({ errors }) => errors.length)}
>
</Button>
>
</Button>
</div>
</div>
</header>
);

View File

@ -44,7 +44,8 @@ export default function PostList({
const posts = useMemo(() => {
if (data && !isLoading) {
return data?.items;
console.log(data?.items)
return data?.items.filter(item=>item.deletedAt === null);
}
return [];
}, [data, isLoading]);

BIN
apps/web/web-dist.zip Normal file

Binary file not shown.

View File

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

View File

@ -100,6 +100,7 @@ export const courseDetailSelect: Prisma.PostSelect = {
// isFeatured: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
// 关联表选择
terms: {
select: {