Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
17633bcbd3
|
@ -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 && \
|
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
|
echo "https://mirrors.aliyun.com/alpine/v3.18/community" >> /etc/apk/repositories
|
||||||
|
|
||||||
|
|
|
@ -125,5 +125,14 @@ export class PostRouter {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.postService.updateOrderByIds(input.ids);
|
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 },
|
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { courseDetail } = args;
|
const { courseDetail } = args;
|
||||||
// If no transaction is provided, create a new one
|
// If no transaction is provided, create a new one
|
||||||
if (!params.tx) {
|
if (!params.tx) {
|
||||||
|
@ -295,40 +296,33 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
staff?.id && {
|
staff?.id && {
|
||||||
authorId: 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);
|
].filter(Boolean);
|
||||||
|
|
||||||
if (orCondition?.length > 0) return orCondition;
|
if (orCondition?.length > 0) return orCondition;
|
||||||
return undefined;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-player": "^2.16.0",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.24.1",
|
"react-router-dom": "^6.24.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default function PathEditorPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
return (
|
return (
|
||||||
<PostDetailProvider editId={id}>
|
<PostDetailProvider editId={id}>
|
||||||
<MindEditor id={id}></MindEditor>;
|
<MindEditor id={id}></MindEditor>
|
||||||
</PostDetailProvider>
|
</PostDetailProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButto
|
||||||
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
|
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { createRoot } from "react-dom/client";
|
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 }) {
|
export default function MindEditor({ id }: { id?: string }) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const {
|
const {
|
||||||
|
@ -36,6 +38,7 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
||||||
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
||||||
const { read } = useVisitor();
|
const { read } = useVisitor();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
// const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
|
// const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
|
||||||
// api.post.findFirst.useQuery(
|
// api.post.findFirst.useQuery(
|
||||||
// {
|
// {
|
||||||
|
@ -46,7 +49,11 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
// },
|
// },
|
||||||
// { enabled: Boolean(id) }
|
// { enabled: Boolean(id) }
|
||||||
// );
|
// );
|
||||||
|
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||||
|
onSuccess:()=>{
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||||
|
}
|
||||||
|
})
|
||||||
const canEdit: boolean = useMemo(() => {
|
const canEdit: boolean = useMemo(() => {
|
||||||
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
||||||
return (
|
return (
|
||||||
|
@ -62,16 +69,16 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
});
|
});
|
||||||
const { handleFileUpload } = useTusUpload();
|
const { handleFileUpload } = useTusUpload();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const CustomLinkIconPlugin = (mind) => {
|
const handleIcon = () => {
|
||||||
mind.bus.addListener("operation", async () => {
|
const hyperLinkElement = document.querySelectorAll(".hyper-link");
|
||||||
const hyperLinkElement =
|
console.log("hyperLinkElement", hyperLinkElement);
|
||||||
await document.querySelectorAll(".hyper-link");
|
hyperLinkElement.forEach((item) => {
|
||||||
console.log("hyperLinkElement", hyperLinkElement);
|
const hyperLinkDom = createRoot(item);
|
||||||
hyperLinkElement.forEach((item) => {
|
hyperLinkDom.render(<LinkOutlined />);
|
||||||
const hyperLinkDom = createRoot(item);
|
|
||||||
hyperLinkDom.render(<LinkOutlined />);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
const CustomLinkIconPlugin = (mind) => {
|
||||||
|
mind.bus.addListener("operation", handleIcon)
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (post?.id && id) {
|
if (post?.id && id) {
|
||||||
|
@ -133,7 +140,11 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
containerRef.current.hidden = true;
|
containerRef.current.hidden = true;
|
||||||
//挂载实例
|
//挂载实例
|
||||||
setInstance(mind);
|
setInstance(mind);
|
||||||
|
|
||||||
}, [canEdit]);
|
}, [canEdit]);
|
||||||
|
useEffect(() => {
|
||||||
|
handleIcon()
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((!id || post) && instance) {
|
if ((!id || post) && instance) {
|
||||||
containerRef.current.hidden = false;
|
containerRef.current.hidden = false;
|
||||||
|
@ -204,10 +215,16 @@ 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()}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await softDeletePostDescendant.mutateAsync({
|
||||||
|
ancestorId: id,
|
||||||
|
});
|
||||||
|
navigate("/path");
|
||||||
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
|
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -248,14 +265,28 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button
|
<>
|
||||||
ghost
|
{
|
||||||
type="primary"
|
id && (
|
||||||
icon={<SaveOutlined></SaveOutlined>}
|
<Button
|
||||||
onSubmit={(e) => e.preventDefault()}
|
danger
|
||||||
onClick={handleSave}>
|
icon={<SaveOutlined></SaveOutlined>}
|
||||||
{id ? "更新" : "保存"}
|
onSubmit={(e) => e.preventDefault()}
|
||||||
</Button>
|
onClick={handleDelete}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
ghost
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined></SaveOutlined>}
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
onClick={handleSave}>
|
||||||
|
{id ? "更新" : "保存"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import PostSelect from "../../models/post/PostSelect/PostSelect";
|
||||||
import { Lecture, PostType } from "@nice/common";
|
import { Lecture, PostType } from "@nice/common";
|
||||||
import { xmindColorPresets } from "./constant";
|
import { xmindColorPresets } from "./constant";
|
||||||
import { api } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import { env } from "@web/src/env";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
|
||||||
interface NodeMenuProps {
|
interface NodeMenuProps {
|
||||||
mind: MindElixirInstance;
|
mind: MindElixirInstance;
|
||||||
|
@ -20,12 +20,13 @@ interface NodeMenuProps {
|
||||||
|
|
||||||
//管理节点样式状态
|
//管理节点样式状态
|
||||||
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectedFontColor, setSelectedFontColor] = useState<string>("");
|
const [selectedFontColor, setSelectedFontColor] = useState<string>("");
|
||||||
const [selectedBgColor, setSelectedBgColor] = useState<string>("");
|
const [selectedBgColor, setSelectedBgColor] = useState<string>("");
|
||||||
const [selectedSize, setSelectedSize] = useState<string>("");
|
const [selectedSize, setSelectedSize] = useState<string>("");
|
||||||
const [isBold, setIsBold] = useState(false);
|
const [isBold, setIsBold] = useState(false);
|
||||||
|
const { user} = useAuth();
|
||||||
const [urlMode, setUrlMode] = useState<"URL" | "POSTURL">("POSTURL");
|
const [urlMode, setUrlMode] = useState<"URL" | "POSTURL">("POSTURL");
|
||||||
const [url, setUrl] = useState<string>("");
|
const [url, setUrl] = useState<string>("");
|
||||||
const [postId, setPostId] = useState<string>("");
|
const [postId, setPostId] = useState<string>("");
|
||||||
|
@ -238,13 +239,15 @@ const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
||||||
{urlMode === "POSTURL" ? (
|
{urlMode === "POSTURL" ? (
|
||||||
<PostSelect
|
<PostSelect
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string" ) {
|
||||||
setPostId(value);
|
setPostId(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
params={{
|
params={{
|
||||||
where: {
|
where: {
|
||||||
type: PostType.LECTURE,
|
type: PostType.LECTURE,
|
||||||
|
deletedAt: null,
|
||||||
|
authorId: user?.id,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { formatFileSize, getCompressedImageUrl } from "@nice/utils";
|
||||||
|
|
||||||
export default function ResourcesShower({
|
export default function ResourcesShower({
|
||||||
resources = [],
|
resources = [],
|
||||||
|
isShowImage
|
||||||
}: {
|
}: {
|
||||||
resources: ResourceDto[];
|
resources: ResourceDto[];
|
||||||
|
isShowImage?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { resources: dealedResources } = useMemo(() => {
|
const { resources: dealedResources } = useMemo(() => {
|
||||||
if (!resources) return { resources: [] };
|
if (!resources) return { resources: [] };
|
||||||
|
@ -36,7 +38,7 @@ export default function ResourcesShower({
|
||||||
const fileResources = dealedResources.filter((res) => !res.isImage);
|
const fileResources = dealedResources.filter((res) => !res.isImage);
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{imageResources.length > 0 && (
|
{imageResources.length > 0 && isShowImage && (
|
||||||
<Row gutter={[16, 16]} className="mb-6">
|
<Row gutter={[16, 16]} className="mb-6">
|
||||||
<Image.PreviewGroup>
|
<Image.PreviewGroup>
|
||||||
{imageResources.map((resource) => (
|
{imageResources.map((resource) => (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useState } from "react";
|
import { ReactNode, useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
|
@ -12,6 +12,9 @@ export interface TusUploaderProps {
|
||||||
onChange?: (value: string[]) => void;
|
onChange?: (value: string[]) => void;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
allowTypes?: string[];
|
allowTypes?: string[];
|
||||||
|
style?:string
|
||||||
|
icon?:ReactNode,
|
||||||
|
description?:string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadingFile {
|
interface UploadingFile {
|
||||||
|
@ -27,6 +30,9 @@ export const TusUploader = ({
|
||||||
onChange,
|
onChange,
|
||||||
multiple = true,
|
multiple = true,
|
||||||
allowTypes = undefined,
|
allowTypes = undefined,
|
||||||
|
style="",
|
||||||
|
icon = <UploadOutlined />,
|
||||||
|
description = "点击或拖拽文件到此区域进行上传",
|
||||||
}: TusUploaderProps) => {
|
}: TusUploaderProps) => {
|
||||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
||||||
|
@ -137,7 +143,7 @@ export const TusUploader = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className={`space-y-1 ${style}`}>
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
accept={allowTypes?.join(",")}
|
accept={allowTypes?.join(",")}
|
||||||
name="files"
|
name="files"
|
||||||
|
@ -145,18 +151,18 @@ export const TusUploader = ({
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
beforeUpload={handleBeforeUpload}>
|
beforeUpload={handleBeforeUpload}>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
{icon}
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">
|
<p className="ant-upload-text">
|
||||||
点击或拖拽文件到此区域进行上传
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-hint">
|
<p className="ant-upload-hint">
|
||||||
{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"}
|
{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"}
|
||||||
{allowTypes && (
|
{/* {allowTypes && (
|
||||||
<span className="block text-xs text-gray-500">
|
<span className="block text-xs text-gray-500">
|
||||||
允许类型: {allowTypes.join(", ")}
|
允许类型: {allowTypes.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)} */}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="px-2 py-0 rounded mt-1">
|
<div className="px-2 py-0 rounded mt-1">
|
||||||
|
|
|
@ -10,11 +10,11 @@ import { Skeleton } from "antd";
|
||||||
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import CourseDetailTitle from "./CourseDetailTitle";
|
import CourseDetailTitle from "./CourseDetailTitle";
|
||||||
|
import ReactPlayer from "react-player";
|
||||||
export const CourseDetailDisplayArea: React.FC = () => {
|
export const CourseDetailDisplayArea: React.FC = () => {
|
||||||
// 创建滚动动画效果
|
// 创建滚动动画效果
|
||||||
const {
|
const {
|
||||||
|
|
||||||
isLoading,
|
isLoading,
|
||||||
canEdit,
|
canEdit,
|
||||||
lecture,
|
lecture,
|
||||||
|
@ -41,6 +41,15 @@ export const CourseDetailDisplayArea: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
className="w-full bg-black rounded-lg ">
|
className="w-full bg-black rounded-lg ">
|
||||||
<div className=" w-full cursor-pointer">
|
<div className=" w-full cursor-pointer">
|
||||||
|
{/* <ReactPlayer
|
||||||
|
url={lecture?.meta?.videoUrl}
|
||||||
|
controls={true}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
onError={(error) => {
|
||||||
|
console.log(error);
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
<VideoPlayer src={lecture?.meta?.videoUrl} />
|
<VideoPlayer src={lecture?.meta?.videoUrl} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
@ -48,18 +57,22 @@ export const CourseDetailDisplayArea: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
{!lectureIsLoading &&
|
{!lectureIsLoading &&
|
||||||
selectedLectureId &&
|
selectedLectureId &&
|
||||||
lecture?.meta?.type === LectureType.ARTICLE && (
|
(
|
||||||
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 ">
|
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 ">
|
||||||
<div className="w-full rounded-lg ">
|
<div className="w-full rounded-lg ">
|
||||||
<CollapsibleContent
|
{lecture?.meta?.type === LectureType.ARTICLE && (
|
||||||
content={lecture?.content || ""}
|
<CollapsibleContent
|
||||||
maxHeight={500} // Optional, defaults to 150
|
content={lecture?.content || ""}
|
||||||
/>
|
maxHeight={500} // Optional, defaults to 150
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="px-6">
|
<div className="px-6">
|
||||||
<ResourcesShower
|
<ResourcesShower
|
||||||
resources={
|
resources={
|
||||||
lecture?.resources
|
lecture?.resources
|
||||||
}></ResourcesShower>
|
}
|
||||||
|
isShowImage = {lecture?.meta?.type === LectureType.ARTICLE}
|
||||||
|
></ResourcesShower>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,15 +23,9 @@ export default function CourseDetailTitle() {
|
||||||
{!selectedLectureId ? course?.title : lecture?.title}
|
{!selectedLectureId ? course?.title : lecture?.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
||||||
{course?.author?.showname && (
|
|
||||||
<div>
|
|
||||||
发布者:
|
|
||||||
{course?.author?.showname}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{course?.depts && course?.depts?.length > 0 && (
|
{course?.depts && course?.depts?.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
发布单位:
|
发布单位:
|
||||||
{course?.depts?.map((dept) => dept.name)}
|
{course?.depts?.map((dept) => dept.name)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,97 +1,40 @@
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { useContext, useState } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { CourseDetailContext } from "../PostDetailContext";
|
import { CourseDetailContext } from "../PostDetailContext";
|
||||||
import { useStaff } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import {
|
import {
|
||||||
CheckCircleOutlined,
|
DeleteTwoTone,
|
||||||
CloseCircleOutlined,
|
|
||||||
EditTwoTone,
|
EditTwoTone,
|
||||||
LoginOutlined,
|
ExclamationCircleFilled,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import JoinButton from "./JoinButton";
|
import JoinButton from "./JoinButton";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getQueryKey } from "@trpc/react-query";
|
||||||
|
|
||||||
export default function CourseOperationBtns() {
|
export default function CourseOperationBtns() {
|
||||||
// const { isAuthenticated, user } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { post, canEdit, userIsLearning, setUserIsLearning } =
|
const { post, canEdit } = useContext(CourseDetailContext);
|
||||||
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);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
return (
|
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>
|
<JoinButton></JoinButton>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div
|
<>
|
||||||
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
|
<div
|
||||||
onClick={() => {
|
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
|
||||||
const url = post?.id
|
onClick={() => {
|
||||||
? `/course/${post?.id}/editor`
|
const url = post?.id
|
||||||
: "/course/editor";
|
? `/course/${post?.id}/editor`
|
||||||
navigate(url);
|
: "/course/editor";
|
||||||
}}>
|
navigate(url);
|
||||||
<EditTwoTone></EditTwoTone>
|
}}>
|
||||||
{"编辑课程"}
|
<EditTwoTone></EditTwoTone>
|
||||||
</div>
|
{"编辑课程"}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { api, usePost } from "@nice/client";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
import { getQueryKey } from "@trpc/react-query";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export type CourseFormData = {
|
export type CourseFormData = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -26,6 +28,7 @@ export type CourseFormData = {
|
||||||
|
|
||||||
interface CourseEditorContextType {
|
interface CourseEditorContextType {
|
||||||
onSubmit: (values: CourseFormData) => Promise<void>;
|
onSubmit: (values: CourseFormData) => Promise<void>;
|
||||||
|
handleDeleteCourse: () => Promise<void>;
|
||||||
editId?: string;
|
editId?: string;
|
||||||
course?: CourseDto;
|
course?: CourseDto;
|
||||||
taxonomies?: Taxonomy[]; // 根据实际类型调整
|
taxonomies?: Taxonomy[]; // 根据实际类型调整
|
||||||
|
@ -45,6 +48,7 @@ export function CourseFormProvider({
|
||||||
}: CourseFormProviderProps) {
|
}: CourseFormProviderProps) {
|
||||||
const [form] = Form.useForm<CourseFormData>();
|
const [form] = Form.useForm<CourseFormData>();
|
||||||
const { create, update, createCourse } = usePost();
|
const { create, update, createCourse } = usePost();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery(
|
const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery(
|
||||||
{
|
{
|
||||||
|
@ -60,6 +64,11 @@ export function CourseFormProvider({
|
||||||
} = api.taxonomy.getAll.useQuery({
|
} = api.taxonomy.getAll.useQuery({
|
||||||
type: ObjectType.COURSE,
|
type: ObjectType.COURSE,
|
||||||
});
|
});
|
||||||
|
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||||
|
onSuccess:()=>{
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||||
|
}
|
||||||
|
})
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -92,7 +101,15 @@ export function CourseFormProvider({
|
||||||
form.setFieldsValue(formData);
|
form.setFieldsValue(formData);
|
||||||
}
|
}
|
||||||
}, [course, form]);
|
}, [course, form]);
|
||||||
|
const handleDeleteCourse = async () => {
|
||||||
|
if(editId){
|
||||||
|
await softDeletePostDescendant.mutateAsync({
|
||||||
|
ancestorId: editId,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate("/courses");
|
||||||
|
}
|
||||||
|
}
|
||||||
const onSubmit = async (values: any) => {
|
const onSubmit = async (values: any) => {
|
||||||
const sections = values?.sections || [];
|
const sections = values?.sections || [];
|
||||||
const deptIds = values?.deptIds || [];
|
const deptIds = values?.deptIds || [];
|
||||||
|
@ -172,6 +189,7 @@ export function CourseFormProvider({
|
||||||
course,
|
course,
|
||||||
taxonomies,
|
taxonomies,
|
||||||
form,
|
form,
|
||||||
|
handleDeleteCourse
|
||||||
}}>
|
}}>
|
||||||
<Form
|
<Form
|
||||||
// requiredMark="optional"
|
// requiredMark="optional"
|
||||||
|
|
|
@ -25,8 +25,11 @@ import { CourseSectionEmpty } from "./CourseSectionEmpty";
|
||||||
import { SortableSection } from "./SortableSection";
|
import { SortableSection } from "./SortableSection";
|
||||||
import { LectureList } from "./LectureList";
|
import { LectureList } from "./LectureList";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getQueryKey } from "@trpc/react-query";
|
||||||
|
|
||||||
const CourseContentForm: React.FC = () => {
|
const CourseContentForm: React.FC = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { editId } = useCourseEditor();
|
const { editId } = useCourseEditor();
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
|
@ -71,7 +74,11 @@ const CourseContentForm: React.FC = () => {
|
||||||
ids: newItems.map((item) => item.id),
|
ids: newItems.map((item) => item.id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||||
|
onSuccess:()=>{
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||||
|
}
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
<CourseContentFormHeader />
|
<CourseContentFormHeader />
|
||||||
|
@ -93,8 +100,8 @@ const CourseContentForm: React.FC = () => {
|
||||||
field={section}
|
field={section}
|
||||||
remove={async () => {
|
remove={async () => {
|
||||||
if (section?.id) {
|
if (section?.id) {
|
||||||
await softDeleteByIds.mutateAsync({
|
await softDeletePostDescendant.mutateAsync({
|
||||||
ids: [section.id],
|
ancestorId: section.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setItems(sections);
|
setItems(sections);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
CaretDownOutlined,
|
CaretDownOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
PlaySquareOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Form, Button, Input, Select, Space } from "antd";
|
import { Form, Button, Input, Select, Space } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
@ -86,12 +88,12 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
resources:
|
resources:
|
||||||
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||||
? {
|
? {
|
||||||
connect: [videoUrlId, ...fileIds]
|
connect: [videoUrlId, ...fileIds]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((fileId) => ({
|
.map((fileId) => ({
|
||||||
fileId,
|
fileId,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
content: values?.content,
|
content: values?.content,
|
||||||
},
|
},
|
||||||
|
@ -115,12 +117,12 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
resources:
|
resources:
|
||||||
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||||
? {
|
? {
|
||||||
set: [videoUrlId, ...fileIds]
|
set: [videoUrlId, ...fileIds]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((fileId) => ({
|
.map((fileId) => ({
|
||||||
fileId,
|
fileId,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
content: values?.content,
|
content: values?.content,
|
||||||
},
|
},
|
||||||
|
@ -175,22 +177,42 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-1 ">
|
<div className="mt-4 flex flex-2 flex-row gap-x-5 ">
|
||||||
{lectureType === LectureType.VIDEO ? (
|
{lectureType === LectureType.VIDEO ? (
|
||||||
<Form.Item
|
<>
|
||||||
name={["meta", "videoIds"]}
|
<div className="mb-0 flex-1">
|
||||||
className="mb-0 flex-1"
|
{/* <span className="inline-block w-full h-7 my-1 rounded-lg bg-slate-100 text-center leading-7">添加视频</span> */}
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
name={["meta", "videoIds"]}
|
||||||
required: true,
|
rules={[
|
||||||
message: "请传入视频",
|
{
|
||||||
},
|
required: true,
|
||||||
]}>
|
message: "请传入视频",
|
||||||
<TusUploader
|
},
|
||||||
allowTypes={videoMimeTypes}
|
]}>
|
||||||
multiple={false}
|
<TusUploader
|
||||||
/>
|
allowTypes={videoMimeTypes}
|
||||||
</Form.Item>
|
multiple={false}
|
||||||
|
style={"h-64"}
|
||||||
|
icon={<PlaySquareOutlined />}
|
||||||
|
description="点击或拖拽视频到此区域进行上传"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div className="mb-0 flex-1">
|
||||||
|
{/* <span className="inline-block w-full h-7 my-1 rounded-lg bg-slate-100 text-center leading-7">添加附件</span> */}
|
||||||
|
<Form.Item
|
||||||
|
name={["meta", "fileIds"]}>
|
||||||
|
<TusUploader
|
||||||
|
style={"h-64"}
|
||||||
|
multiple={true}
|
||||||
|
icon={<PaperClipOutlined />}
|
||||||
|
description="点击或拖拽附件到此区域进行上传"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -204,7 +226,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
]}>
|
]}>
|
||||||
<QuillEditor
|
<QuillEditor
|
||||||
style={{
|
style={{
|
||||||
width:"700px",
|
width: "700px",
|
||||||
}}
|
}}
|
||||||
></QuillEditor>
|
></QuillEditor>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { ArrowLeftOutlined, ClockCircleOutlined } from "@ant-design/icons";
|
import { ArrowLeftOutlined, ClockCircleOutlined, ExclamationCircleFilled } from "@ant-design/icons";
|
||||||
import { Button, Tag, Typography } from "antd";
|
import { Button, Modal, Tag, Typography } from "antd";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CourseStatus, CourseStatusLabel } from "@nice/common";
|
import { CourseStatus, CourseStatusLabel } from "@nice/common";
|
||||||
import { useCourseEditor } from "../context/CourseEditorContext";
|
import { useCourseEditor } from "../context/CourseEditorContext";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
@ -18,8 +19,8 @@ const courseStatusVariant: Record<CourseStatus, string> = {
|
||||||
export default function CourseEditorHeader() {
|
export default function CourseEditorHeader() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, hasSomePermissions } = useAuth();
|
const { user, hasSomePermissions } = useAuth();
|
||||||
|
const { confirm } = Modal;
|
||||||
const { onSubmit, course, form } = useCourseEditor();
|
const { onSubmit, course, form, handleDeleteCourse, editId } = useCourseEditor();
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
try {
|
try {
|
||||||
|
@ -30,7 +31,26 @@ export default function CourseEditorHeader() {
|
||||||
console.log(err);
|
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 (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 z-10">
|
<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">
|
<div className="h-full flex items-center justify-between px-3 md:px-4">
|
||||||
|
@ -70,16 +90,27 @@ export default function CourseEditorHeader() {
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div>
|
||||||
type="primary"
|
{editId &&
|
||||||
// size="small"
|
<Button
|
||||||
onClick={handleSave}
|
danger
|
||||||
|
onClick={showDeleteConfirm}
|
||||||
|
>
|
||||||
|
删除课程
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
type="primary"
|
||||||
|
// size="small"
|
||||||
|
onClick={handleSave}
|
||||||
// disabled={form
|
// disabled={form
|
||||||
// .getFieldsError()
|
// .getFieldsError()
|
||||||
// .some(({ errors }) => errors.length)}
|
// .some(({ errors }) => errors.length)}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,7 +44,8 @@ export default function PostList({
|
||||||
|
|
||||||
const posts = useMemo(() => {
|
const posts = useMemo(() => {
|
||||||
if (data && !isLoading) {
|
if (data && !isLoading) {
|
||||||
return data?.items;
|
console.log(data?.items)
|
||||||
|
return data?.items.filter(item=>item.deletedAt === null);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading]);
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default function PostSelect({
|
||||||
placeholder = "请选择课时",
|
placeholder = "请选择课时",
|
||||||
params = { where: {}, select: {} },
|
params = { where: {}, select: {} },
|
||||||
className,
|
className,
|
||||||
|
createdById,
|
||||||
}: {
|
}: {
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
onChange?: (value: string | string[]) => void;
|
onChange?: (value: string | string[]) => void;
|
||||||
|
@ -22,6 +23,7 @@ export default function PostSelect({
|
||||||
select?: Prisma.PostSelect<DefaultArgs>;
|
select?: Prisma.PostSelect<DefaultArgs>;
|
||||||
};
|
};
|
||||||
className?: string;
|
className?: string;
|
||||||
|
createdById?: string;
|
||||||
}) {
|
}) {
|
||||||
const [searchValue, setSearch] = useState("");
|
const [searchValue, setSearch] = useState("");
|
||||||
const searchCondition: Prisma.PostWhereInput = useMemo(() => {
|
const searchCondition: Prisma.PostWhereInput = useMemo(() => {
|
||||||
|
|
|
@ -128,7 +128,7 @@ export const routes: CustomRouteObject[] = [
|
||||||
path: "course",
|
path: "course",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: ":id?/editor",
|
path: ":id?/editor",
|
||||||
element: (
|
element: (
|
||||||
<WithAuth>
|
<WithAuth>
|
||||||
<CourseEditorLayout></CourseEditorLayout>
|
<CourseEditorLayout></CourseEditorLayout>
|
||||||
|
|
Binary file not shown.
|
@ -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.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
|
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
|
||||||
|
|
|
@ -100,6 +100,7 @@ export const courseDetailSelect: Prisma.PostSelect = {
|
||||||
// isFeatured: true,
|
// isFeatured: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
|
deletedAt: true,
|
||||||
// 关联表选择
|
// 关联表选择
|
||||||
terms: {
|
terms: {
|
||||||
select: {
|
select: {
|
||||||
|
|
|
@ -386,6 +386,9 @@ importers:
|
||||||
react-hot-toast:
|
react-hot-toast:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 2.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
react-player:
|
||||||
|
specifier: ^2.16.0
|
||||||
|
version: 2.16.0(react@18.2.0)
|
||||||
react-resizable:
|
react-resizable:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
@ -5603,6 +5606,9 @@ packages:
|
||||||
enquirer:
|
enquirer:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
load-script@1.0.0:
|
||||||
|
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||||
|
|
||||||
load-tsconfig@0.2.5:
|
load-tsconfig@0.2.5:
|
||||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
@ -5818,6 +5824,9 @@ packages:
|
||||||
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
|
memoize-one@5.2.1:
|
||||||
|
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||||
|
|
||||||
meow@8.1.2:
|
meow@8.1.2:
|
||||||
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -6645,6 +6654,9 @@ packages:
|
||||||
react: '>= 16.3.0'
|
react: '>= 16.3.0'
|
||||||
react-dom: '>= 16.3.0'
|
react-dom: '>= 16.3.0'
|
||||||
|
|
||||||
|
react-fast-compare@3.2.2:
|
||||||
|
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||||
|
|
||||||
react-hook-form@7.54.2:
|
react-hook-form@7.54.2:
|
||||||
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
|
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
@ -6664,6 +6676,11 @@ packages:
|
||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
|
react-player@2.16.0:
|
||||||
|
resolution: {integrity: sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.6.0'
|
||||||
|
|
||||||
react-refresh@0.14.2:
|
react-refresh@0.14.2:
|
||||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -13723,6 +13740,8 @@ snapshots:
|
||||||
rfdc: 1.4.1
|
rfdc: 1.4.1
|
||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
|
|
||||||
|
load-script@1.0.0: {}
|
||||||
|
|
||||||
load-tsconfig@0.2.5: {}
|
load-tsconfig@0.2.5: {}
|
||||||
|
|
||||||
loader-runner@4.3.0: {}
|
loader-runner@4.3.0: {}
|
||||||
|
@ -13899,6 +13918,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey: 1.0.6
|
fs-monkey: 1.0.6
|
||||||
|
|
||||||
|
memoize-one@5.2.1: {}
|
||||||
|
|
||||||
meow@8.1.2:
|
meow@8.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimist': 1.2.5
|
'@types/minimist': 1.2.5
|
||||||
|
@ -14774,6 +14795,8 @@ snapshots:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
|
react-fast-compare@3.2.2: {}
|
||||||
|
|
||||||
react-hook-form@7.54.2(react@18.2.0):
|
react-hook-form@7.54.2(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
@ -14789,6 +14812,15 @@ snapshots:
|
||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
|
react-player@2.16.0(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
load-script: 1.0.0
|
||||||
|
memoize-one: 5.2.1
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-fast-compare: 3.2.2
|
||||||
|
|
||||||
react-refresh@0.14.2: {}
|
react-refresh@0.14.2: {}
|
||||||
|
|
||||||
react-resizable@3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
react-resizable@3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
|
|
Loading…
Reference in New Issue