rht
This commit is contained in:
parent
23daab3b3b
commit
1fc1aa368c
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
@ -133,7 +140,7 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
containerRef.current.hidden = true;
|
||||
//挂载实例
|
||||
setInstance(mind);
|
||||
|
||||
|
||||
}, [canEdit]);
|
||||
useEffect(() => {
|
||||
handleIcon()
|
||||
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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]);
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -100,6 +100,7 @@ export const courseDetailSelect: Prisma.PostSelect = {
|
|||
// isFeatured: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
// 关联表选择
|
||||
terms: {
|
||||
select: {
|
||||
|
|
Loading…
Reference in New Issue