diff --git a/.gitignore b/.gitignore index 7d4585e..2e6bac7 100755 --- a/.gitignore +++ b/.gitignore @@ -67,5 +67,6 @@ yarn-error.log* # Ignore .idea files in the Expo monorepo **/.idea/ - -uploads \ No newline at end of file +uploads +packages/mind-elixir-core +config/nginx/conf.d/web.conf \ No newline at end of file diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index aaed815..d4a4efa 100755 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -56,7 +56,7 @@ export default function BaseSettingPage() { meta: { ...baseSetting, appConfig: { - ...baseSetting.appConfig, + ...(baseSetting?.appConfig || {}), ...appConfig, }, }, diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 40aa8da..d830c4d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -16,6 +16,7 @@ export default function CourseCard({ course }: CourseCardProps) { const navigate = useNavigate(); const handleClick = (course: CourseDto) => { navigate(`/course/${course.id}/detail`); + window.scrollTo({top: 0,behavior: "smooth",}) }; return ( @@ -33,6 +34,7 @@ export default function CourseCard({ course }: CourseCardProps) { backgroundImage: `url(${course?.meta?.thumbnail})`, }} /> +
@@ -46,10 +48,10 @@ export default function CourseCard({ course }: CourseCardProps) { // color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"} color={ term?.taxonomy?.slug === - TaxonomySlug.CATEGORY + TaxonomySlug.CATEGORY ? "blue" : term?.taxonomy?.slug === - TaxonomySlug.LEVEL + TaxonomySlug.LEVEL ? "green" : "orange" } @@ -59,6 +61,7 @@ export default function CourseCard({ course }: CourseCardProps) { ); })} + @@ -69,13 +72,17 @@ export default function CourseCard({ course }: CourseCardProps) { <TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" /> <div className="ml-2 flex items-center flex-grow"> <Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]"> - { - course?.depts.length > 1 ?`${course.depts[0].name}等`:course.depts[0].name - } + {course?.depts?.length > 1 + ? `${course.depts[0].name}等` + : course?.depts?.[0]?.name} + {/* {course?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */} + {/* {course?.depts?.map((dept)=>{return dept.name})} */} </Text> </div> <span className="text-xs font-medium text-gray-500"> - {course?.meta?.views ? `观看次数 ${course?.meta?.views}` : null} + {course?.meta?.views + ? `观看次数 ${course?.meta?.views}` + : null} </span> </div> <div className="pt-4 border-t border-gray-100 text-center"> diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 203e1d1..01ffba6 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -7,6 +7,7 @@ import { api } from "@nice/client"; import { useSearchParams } from "react-router-dom"; import TermSelect from "@web/src/components/models/term/term-select"; import { useMainContext } from "../../layout/MainProvider"; +import TermParentSelector from "@web/src/components/models/term/term-parent-selector"; export default function FilterSection() { const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); @@ -28,7 +29,19 @@ export default function FilterSection() { <h3 className="text-lg font-medium mb-4"> {tax?.name} </h3> - <TermSelect + <TermParentSelector + value={items} + slug = {tax?.slug} + className="w-70 max-h-[500px] overscroll-contain overflow-x-hidden" + onChange={(selected) => + handleTermChange( + tax?.slug, + selected as string[] + ) + } + taxonomyId={tax?.id} + ></TermParentSelector> + {/* <TermSelect // open className="w-72" value={items} @@ -46,7 +59,7 @@ export default function FilterSection() { }></TermSelect> {index < taxonomies.length - 1 && ( <Divider className="my-6" /> - )} + )} */} </div> ); })} diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index e78bc48..902e979 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -38,66 +38,67 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ description, initialVisibleCoursesCount = 8, }) => { - const [selectedCategory, setSelectedCategory] = useState<string>("全部"); - const gateGory: GetTaxonomyProps = useGetTaxonomy({ - type: TaxonomySlug.CATEGORY, - }); - return ( - <section className="relative py-20 overflow-hidden bg-gradient-to-b from-blue-50 to-gray-50 "> - <div className="max-w-screen-2xl mx-auto px-6 relative"> - <div className="flex justify-between items-end mb-16 bg-blue-100 rounded-lg p-6"> - <div> - <Title - level={2} - className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> - {title} - - - {description} - - - -
- {gateGory.isLoading ? ( - - ) : ( - <> - {["全部", ...gateGory.categories].map( - (category, idx) => ( - - ) - )} - - )} -
- - - - - ); + const [selectedCategory, setSelectedCategory] = useState("全部"); + const gateGory: GetTaxonomyProps = useGetTaxonomy({ + type: TaxonomySlug.CATEGORY, + }); + return ( +
+
+
+
+ + {title} + + + + {description} + +
+
+
+ {gateGory.isLoading ? ( + + ) : ( + <> + {["全部", ...gateGory.categories].map( + (category, idx) => ( + + ) + )} + + )} +
+ + +
+
+ ); }; export default CoursesSection; diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 404ea27..17aa228 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -1,10 +1,9 @@ -import React, { useRef, useCallback, useEffect } from "react"; -import { Button, Carousel, Typography } from "antd"; +import React, { useRef, useCallback, useEffect, useMemo, useState } from "react"; +import { Carousel, Typography } from "antd"; import { TeamOutlined, BookOutlined, StarOutlined, - ClockCircleOutlined, LeftOutlined, RightOutlined, EyeOutlined, @@ -24,36 +23,22 @@ interface CarouselItem { interface PlatformStat { icon: React.ReactNode; - value: string; + value: number; label: string; } -const carouselItems: CarouselItem[] = [ - { - title: "探索编程世界", - desc: "从零开始学习编程,开启你的技术之旅", - image: "/images/banner1.jpg", - action: "立即开始", - color: "from-blue-600/90", - }, - { - title: "人工智能课程", - desc: "掌握AI技术,引领未来发展", - image: "/images/banner2.jpg", - action: "了解更多", - color: "from-purple-600/90", - }, -]; - const HeroSection = () => { const carouselRef = useRef(null); - const { statistics, baseSetting } = useAppConfig(); - const platformStats: PlatformStat[] = [ - { icon: , value: statistics.staffs.toString(), label: "注册学员" }, - { icon: , value: statistics.courses.toString(), label: "精品课程" }, - { icon: , value: statistics.lectures.toString(), label: '课程章节' }, - { icon: , value: statistics.reads.toString(), label: "观看次数" }, - ]; + const { statistics, slides } = useAppConfig(); + const [countStatistics, setCountStatistics] = useState(0) + const platformStats: PlatformStat[] = useMemo(() => { + return [ + { icon: , value: statistics.staffs, label: "注册学员" }, + { icon: , value: statistics.courses, label: "精品课程" }, + { icon: , value: statistics.lectures, label: '课程章节' }, + { icon: , value: statistics.reads, label: "观看次数" }, + ]; + }, [statistics]); const handlePrev = useCallback(() => { carouselRef.current?.prev(); }, []); @@ -61,10 +46,16 @@ const HeroSection = () => { const handleNext = useCallback(() => { carouselRef.current?.next(); }, []); - const { slides } = useAppConfig() + + const countNonZeroValues = (statistics: Record): number => { + return Object.values(statistics).filter(value => value !== 0).length; + }; + useEffect(() => { - console.log(statistics) - }, [statistics]) + const count = countNonZeroValues(statistics); + console.log(count); + setCountStatistics(count); + }, [statistics]); return (
@@ -76,7 +67,7 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {Array.isArray(slides)? + {Array.isArray(slides) ? (slides.map((item, index) => (
{
)) - ) : ( -
- )} + ) : ( +
+ )} {/* Navigation Buttons */} @@ -117,25 +108,31 @@ const HeroSection = () => {
{/* Stats Container */} -
-
- {platformStats.map((stat, index) => ( -
-
- {stat.icon} -
-
- {stat.value} -
-
- {stat.label} -
+ { + countStatistics > 1 && ( +
+
+ {platformStats.map((stat, index) => { + return stat.value + ? (
+
+ {stat.icon} +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ) : null + })}
- ))} -
-
+
+ ) + }
); }; diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index 47503b5..1461b4e 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -2,7 +2,7 @@ import { useContext, useState } from "react"; import { Input, Layout, Avatar, Button, Dropdown } from "antd"; import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { UserMenu } from "./UserMenu/UserMenu"; import { NavigationMenu } from "./NavigationMenu"; import { useMainContext } from "./MainProvider"; @@ -10,6 +10,7 @@ const { Header } = Layout; export function MainHeader() { const { isAuthenticated, user } = useAuth(); + const { id } = useParams(); const navigate = useNavigate(); const { searchValue, setSearchValue } = useMainContext(); return ( @@ -52,10 +53,15 @@ export function MainHeader() { {isAuthenticated && ( <> )} diff --git a/apps/web/src/components/common/container/CollapsibleContent.tsx b/apps/web/src/components/common/container/CollapsibleContent.tsx index 831bdad..9352183 100644 --- a/apps/web/src/components/common/container/CollapsibleContent.tsx +++ b/apps/web/src/components/common/container/CollapsibleContent.tsx @@ -28,9 +28,15 @@ const CollapsibleContent: React.FC = ({ {/* 包装整个内容区域的容器 */}
{/* 内容区域 */} diff --git a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx index 1ff09b5..16f389d 100644 --- a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx @@ -3,66 +3,81 @@ import { useEffect, useState } from "react"; import { Upload, Progress, Button, Image, Form } from "antd"; import { DeleteOutlined } from "@ant-design/icons"; import AvatarUploader from "./AvatarUploader"; -import { isEqual } from 'lodash'; +import { isEqual } from "lodash"; interface MultiAvatarUploaderProps { - value?: string[]; - onChange?: (value: string[]) => void; + value?: string[]; + onChange?: (value: string[]) => void; } export function MultiAvatarUploader({ - value, - onChange, + value, + onChange, }: MultiAvatarUploaderProps) { - const [imageList, setImageList] = useState(value || []) - const [previewImage, setPreviewImage] = useState(""); - useEffect(() => { - if (!isEqual(value, imageList)) { - setImageList(value || []); - } - }, [value]); - useEffect(() => { - onChange?.(imageList) - }, [imageList]) - return <> -
- {imageList.map((image, index) => { - return ( -
- - setPreviewImage(visible ? image || "" : "") - }} > - -
- ) - })} -
-
- { - console.log(value); - setImageList([...imageList, value]) - }}> -
- - ; + const [imageList, setImageList] = useState(value || []); + const [previewImage, setPreviewImage] = useState(""); + useEffect(() => { + if (!isEqual(value, imageList)) { + setImageList(value || []); + } + }, [value]); + useEffect(() => { + onChange?.(imageList); + }, [imageList]); + return ( + <> +
+ {(imageList || [])?.map((image, index) => { + return ( +
+ + setPreviewImage( + visible ? image || "" : "" + ), + }}> +
+ ); + })} +
+
+ { + console.log(value); + setImageList([...imageList, value]); + }}> +
+ + ); } -export default MultiAvatarUploader; \ No newline at end of file +export default MultiAvatarUploader; diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 8702d19..6787e13 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -7,7 +7,7 @@ import { } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; import React, { createContext, ReactNode, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; interface CourseDetailContextType { editId?: string; // 添加 editId @@ -33,6 +33,7 @@ export function CourseDetailProvider({ const navigate = useNavigate(); const { read } = useVisitor(); const { user } = useAuth(); + const { lectureId } = useParams(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { @@ -47,7 +48,7 @@ export function CourseDetailProvider({ const [selectedLectureId, setSelectedLectureId] = useState< string | undefined - >(undefined); + >(lectureId || undefined); const { data: lecture, isLoading: lectureIsLoading } = ( api.post as any ).findFirst.useQuery( diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx index feba41d..6573753 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx @@ -61,7 +61,6 @@ export const CourseDetailDescription: React.FC = () => { expandable: true, symbol: "展开", onExpand: () => console.log("展开"), - // collapseText: "收起", }}> {course?.content} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index 142abde..447ecda 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -51,7 +51,7 @@ export const CourseDetailDisplayArea: React.FC = () => {
diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 7e708e1..acb8d70 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -7,7 +7,7 @@ import { UserOutlined, } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; @@ -15,7 +15,8 @@ const { Header } = Layout; export function CourseDetailHeader() { const [searchValue, setSearchValue] = useState(""); - const { isAuthenticated, user } = useAuth(); + const { id } = useParams(); + const { isAuthenticated, user, hasSomePermissions } = useAuth(); const navigate = useNavigate(); const { course } = useContext(CourseDetailContext); @@ -51,10 +52,15 @@ export function CourseDetailHeader() { {isAuthenticated && ( <> )} diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index ed73745..f4276bf 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -1,7 +1,7 @@ // components/CourseSyllabus/LectureItem.tsx -import { Lecture, LectureType } from "@nice/common"; -import React from "react"; +import { Lecture, LectureType, LessonTypeLabel } from "@nice/common"; +import React, { useMemo } from "react"; import { ClockCircleOutlined, FileTextOutlined, @@ -19,15 +19,24 @@ export const LectureItem: React.FC = ({ onClick, }) => { const { lectureId } = useParams(); + const isReading = useMemo(() => { + return lecture?.id === lectureId; + }, [lectureId, lecture]); return (
onClick(lecture.id)}> - {lecture.type === LectureType.VIDEO && ( - + {lecture?.meta?.type === LectureType.VIDEO && ( +
+ + {LessonTypeLabel[lecture?.meta?.type]} +
)} - {lecture.type === LectureType.ARTICLE && ( - // 为文章类型添加图标 + {lecture?.meta?.type === LectureType.ARTICLE && ( +
+ {" "} + {LessonTypeLabel[lecture?.meta?.type]} +
)}

{lecture.title}

@@ -37,10 +46,6 @@ export const LectureItem: React.FC = ({

)}
- {/*
- - {lecture.duration}分钟 -
*/}
); }; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx index 6cd1627..3c0a4c5 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx @@ -1,10 +1,9 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { SectionDto } from "@nice/common"; import { AnimatePresence, motion } from "framer-motion"; -import React from "react"; +import React, { useMemo } from "react"; import { LectureItem } from "./LectureItem"; - -// components/CourseSyllabus/SectionItem.tsx +import { useParams } from "react-router-dom"; interface SectionItemProps { section: SectionDto; index?: number; @@ -13,57 +12,68 @@ interface SectionItemProps { onLectureClick: (lectureId: string) => void; ref: React.RefObject; } - export const SectionItem = React.forwardRef( - ({ section, index, isExpanded, onToggle, onLectureClick }, ref) => ( -
- - - {isExpanded && ( - - {section.lectures.map((lecture) => ( - - ))} - - )} - -
- ) + + {isExpanded && ( + + {section.lectures.map((lecture) => ( + + ))} + + )} + + + ); + } ); diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 6a12e2c..6b61854 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -83,6 +83,7 @@ export function CourseFormProvider({ const onSubmit = async (values: any) => { console.log(values); const sections = values?.sections || []; + const deptIds = values?.deptIds || []; const termIds = taxonomies .map((tax) => values[tax.id]) // 获取每个 taxonomy 对应的选中值 .filter((id) => id); // 过滤掉空值 @@ -95,12 +96,16 @@ export function CourseFormProvider({ terms: { connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 }, + depts: { + connect: deptIds.map((id) => ({ id })), + }, }; // 删除原始的 taxonomy 字段 taxonomies.forEach((tax) => { delete formattedValues[tax.id]; }); delete formattedValues.sections; + delete formattedValues.deptIds; if (course) { formattedValues.meta = { ...(course?.meta as CourseMeta), diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index b05579b..cf683f1 100755 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -4,6 +4,7 @@ import { convertToOptions } from "@nice/client"; import TermSelect from "../../../term/term-select"; import { useCourseEditor } from "../context/CourseEditorContext"; import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; +import DepartmentSelect from "../../../department/department-select"; const { TextArea } = Input; @@ -48,6 +49,9 @@ export function CourseBasicForm() { autoSize={{ minRows: 3, maxRows: 6 }} /> + + + {taxonomies && taxonomies.map((tax, index) => ( = ({ {isContentVisible && !editing && // Conditionally render content based on type (field?.meta?.type === LectureType.ARTICLE ? ( - + ) : ( ))} diff --git a/apps/web/src/components/models/course/editor/layout/CourseEditorLayout.tsx b/apps/web/src/components/models/course/editor/layout/CourseEditorLayout.tsx index 39ace5d..aaa823d 100755 --- a/apps/web/src/components/models/course/editor/layout/CourseEditorLayout.tsx +++ b/apps/web/src/components/models/course/editor/layout/CourseEditorLayout.tsx @@ -25,7 +25,7 @@ export default function CourseEditorLayout() { const navigate = useNavigate(); const handleNavigation = (item: NavItem, index: number) => { setSelectedSection(index); - navigate(item.path, { replace: true }); + navigate(item.path); }; return ( diff --git a/apps/web/src/components/models/course/list/CourseList.tsx b/apps/web/src/components/models/course/list/CourseList.tsx index fb1f74e..3ba3eaf 100755 --- a/apps/web/src/components/models/course/list/CourseList.tsx +++ b/apps/web/src/components/models/course/list/CourseList.tsx @@ -54,6 +54,9 @@ export default function CourseList({ setCurrentPage(page); window.scrollTo({ top: 0, behavior: "smooth" }); } + if (isLoading) { + return ; + } return (
{courses.length > 0 ? ( diff --git a/apps/web/src/components/models/term/term-parent-selector.tsx b/apps/web/src/components/models/term/term-parent-selector.tsx new file mode 100644 index 0000000..f4b365d --- /dev/null +++ b/apps/web/src/components/models/term/term-parent-selector.tsx @@ -0,0 +1,55 @@ +import { api } from "@nice/client/"; +import { Checkbox, Form } from "antd"; +import { TermDto } from "@nice/common"; +import { useCallback, useEffect, useState } from "react"; + +export default function TermParentSelector({ + value, + onChange, + className, + placeholder = "选择分类", + multiple = true, + taxonomyId, + domainId, + style, +}: any) { + const utils = api.useUtils(); + const [selectedValues, setSelectedValues] = useState([]); // 用于存储选中的值 + const [termsData, setTermsData] = useState([]); + + + const { + data, + isLoading, + }: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({ + where: { + taxonomy: { + id: taxonomyId, + }, + parentId: null + }, + }); + const handleCheckboxChange = (checkedValues: string[]) => { + setSelectedValues(checkedValues); // 更新选中的值 + if (onChange) { + onChange(checkedValues); // 调用外部传入的 onChange 回调 + } + }; + return ( +
+
+ + + {data?.map((category) => ( +
+ + {category.name} + +
+ ))} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index 3896d8e..95990dd 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -48,7 +48,7 @@ export default function TermSelect({ const [listTreeData, setListTreeData] = useState< Omit[] >([]); - + const fetchParentTerms = useCallback( async (termIds: string | string[], taxonomyId?: string) => { const idsArray = Array.isArray(termIds) diff --git a/apps/web/src/components/models/term/term-tree.tsx b/apps/web/src/components/models/term/term-tree.tsx deleted file mode 100644 index ae88a68..0000000 --- a/apps/web/src/components/models/term/term-tree.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import React, { useEffect, useState, useCallback } from "react"; -import { Tree } from "antd"; -import type { DataNode, TreeProps } from "antd/es/tree"; -import { getUniqueItems } from "@nice/common"; -import { api } from "@nice/client"; - -interface TermData { - value?: string; - children?: TermData[]; - key?: string; - hasChildren?: boolean; - isLeaf?: boolean; - pId?: string; - title?: React.ReactNode; - data?: any; - order?: string; - id?: string; -} - -interface TermTreeProps { - defaultValue?: string | string[]; - value?: string | string[]; - onChange?: (value: string | string[]) => void; - multiple?: boolean; - taxonomyId?: string; - disabled?: boolean; - className?: string; - domainId?: string; - style?: React.CSSProperties; -} - -const TermTree: React.FC = ({ - defaultValue, - value, - onChange, - className, - multiple = false, - taxonomyId, - domainId, - disabled = false, - style, -}) => { - const utils = api.useUtils(); - const [treeData, setTreeData] = useState([]); - - const processTermData = (terms: TermData[]): TermData[] => { - return terms.map((term) => ({ - ...term, - key: term.key || term.id || "", - title: term.title || term.value, - children: term.children - ? processTermData(term.children) - : undefined, - })); - }; - - const fetchParentTerms = useCallback( - async (termIds: string | string[], taxonomyId?: string) => { - const idsArray = Array.isArray(termIds) - ? termIds - : [termIds].filter(Boolean); - try { - const result = await utils.term.getParentSimpleTree.fetch({ - termIds: idsArray, - taxonomyId, - domainId, - }); - return processTermData(result); - } catch (error) { - console.error( - "Error fetching parent terms for termIds", - idsArray, - ":", - error - ); - throw error; - } - }, - [utils, domainId] - ); - - const fetchTerms = useCallback(async () => { - try { - const rootTerms = await utils.term.getChildSimpleTree.fetch({ - taxonomyId, - domainId, - }); - let combinedTerms = processTermData(rootTerms); - if (defaultValue) { - const defaultTerms = await fetchParentTerms( - defaultValue, - taxonomyId - ); - combinedTerms = getUniqueItems( - [...treeData, ...combinedTerms, ...defaultTerms], - "key" - ); - } - if (value) { - const valueTerms = await fetchParentTerms(value, taxonomyId); - combinedTerms = getUniqueItems( - [...treeData, ...combinedTerms, ...valueTerms], - "key" - ); - } - - setTreeData(combinedTerms); - } catch (error) { - console.error("Error fetching terms:", error); - } - }, [ - defaultValue, - value, - taxonomyId, - utils, - fetchParentTerms, - domainId, - treeData, - ]); - - useEffect(() => { - fetchTerms(); - }, [fetchTerms]); - - const onLoadData = async ({ key }: any) => { - try { - const result = await utils.term.getChildSimpleTree.fetch({ - termIds: [key], - taxonomyId, - domainId, - }); - const processedResult = processTermData(result); - const newItems = getUniqueItems( - [...treeData, ...processedResult], - "key" - ); - setTreeData(newItems); - } catch (error) { - console.error( - "Error loading data for node with key", - key, - ":", - error - ); - } - }; - - const handleCheck: TreeProps["onCheck"] = (checkedKeys, info) => { - if (onChange) { - if (multiple) { - onChange(checkedKeys as string[]); - } else { - onChange((checkedKeys as string[])[0] || ""); - } - } - }; - - const handleExpand = async (expandedKeys: React.Key[]) => { - try { - const allKeyIds = expandedKeys - .map((key) => key.toString()) - .filter(Boolean); - const expandedNodes = await utils.term.getChildSimpleTree.fetch({ - termIds: allKeyIds, - taxonomyId, - domainId, - }); - const processedNodes = processTermData(expandedNodes); - const newItems = getUniqueItems( - [...treeData, ...processedNodes], - "key" - ); - setTreeData(newItems); - } catch (error) { - console.error( - "Error expanding nodes with keys", - expandedKeys, - ":", - error - ); - } - }; - - return ( - - ); -}; - -export default TermTree; diff --git a/apps/web/src/components/presentation/collapse-section.tsx b/apps/web/src/components/presentation/collapse-section.tsx index 072e596..d0a9798 100755 --- a/apps/web/src/components/presentation/collapse-section.tsx +++ b/apps/web/src/components/presentation/collapse-section.tsx @@ -14,7 +14,7 @@ interface CollapsibleSectionProps { interface MenuItem { key: string; link?: string; - blank?: boolean + blank?: boolean; icon?: React.ReactNode; label: string; children?: Array; @@ -69,7 +69,10 @@ const CollapsibleSection: React.FC = ({ const isChildCollapsed = !expandedSections[item.key]; return ( -
+
{ @@ -78,7 +81,7 @@ const CollapsibleSection: React.FC = ({ } if (item.link) { if (!item.blank) { - navigate(item.link, { replace: true }); + navigate(item.link); } else { window.open(item.link, "_blank"); } @@ -86,12 +89,20 @@ const CollapsibleSection: React.FC = ({ }} initial={false} animate={{ - backgroundColor: isActive ? token.colorPrimaryBorder : token.colorPrimary, + backgroundColor: isActive + ? token.colorPrimaryBorder + : token.colorPrimary, }} - whileHover={{ backgroundColor: token.colorPrimaryHover }} - transition={{ type: "spring", stiffness: 300, damping: 25, duration: 0.3 }} - style={{ marginLeft: `${level * 16}px` }} - > + whileHover={{ + backgroundColor: token.colorPrimaryHover, + }} + transition={{ + type: "spring", + stiffness: 300, + damping: 25, + duration: 0.3, + }} + style={{ marginLeft: `${level * 16}px` }}>
{item.icon && {item.icon}} @@ -100,8 +111,7 @@ const CollapsibleSection: React.FC = ({ {hasChildren && ( + className={`ml-1 transition-transform duration-300 ${!isChildCollapsed ? "rotate-90" : ""}`}> )}
{item.extra &&
{item.extra}
} @@ -115,11 +125,10 @@ const CollapsibleSection: React.FC = ({ // stiffness: 200, // damping: 20, type: "tween", - duration: 0.2 + duration: 0.2, }} style={{ overflow: "hidden" }} - className="mt-1" - > + className="mt-1"> {renderItems(item.children, level + 1)} )} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 07c7c9c..7592489 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -68,11 +68,11 @@ export const routes: CustomRouteObject[] = [ path: "profiles", }, - // 课程预览页面 - { - path: "coursePreview/:id?", - element: , - }, + // // 课程预览页面 + // { + // path: "coursePreview/:id?", + // element: , + // }, ], }, {