diff --git a/apps/server/src/models/app-config/app-config.router.ts b/apps/server/src/models/app-config/app-config.router.ts index 882fa16..350d047 100755 --- a/apps/server/src/models/app-config/app-config.router.ts +++ b/apps/server/src/models/app-config/app-config.router.ts @@ -36,7 +36,7 @@ export class AppConfigRouter { .mutation(async ({ input }) => { return await this.appConfigService.deleteMany(input); }), - findFirst: this.trpc.protectProcedure + findFirst: this.trpc.procedure .input(AppConfigFindFirstArgsSchema) .query(async ({ input }) => { return await this.appConfigService.findFirst(input); diff --git a/apps/server/src/models/term/term.router.ts b/apps/server/src/models/term/term.router.ts index 548526c..1b15a3d 100755 --- a/apps/server/src/models/term/term.router.ts +++ b/apps/server/src/models/term/term.router.ts @@ -61,7 +61,7 @@ export class TermRouter { .input(TermMethodSchema.getSimpleTree) .query(async ({ input, ctx }) => { const { staff } = ctx; - return await this.termService.getChildSimpleTree(staff, input); + return await this.termService.getChildSimpleTree(input, staff); }), getParentSimpleTree: this.trpc.procedure .input(TermMethodSchema.getSimpleTree) diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts index 7f21041..7cbcc5f 100755 --- a/apps/server/src/models/term/term.service.ts +++ b/apps/server/src/models/term/term.service.ts @@ -269,10 +269,10 @@ export class TermService extends BaseTreeService { } async getChildSimpleTree( - staff: UserProfile, data: z.infer, + staff?: UserProfile, ) { - const { domainId = null, permissions } = staff; + const domainId = staff?.domainId || null; const hasAnyPerms = staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) || staff?.permissions?.includes(RolePerms.READ_ANY_TERM); @@ -352,7 +352,9 @@ export class TermService extends BaseTreeService { staff: UserProfile, data: z.infer, ) { - const { domainId = null, permissions } = staff; + // const { domainId = null, permissions } = staff; + const permissions = staff?.permissions || []; + const domainId = staff?.domainId || null; const hasAnyPerms = permissions.includes(RolePerms.READ_ANY_TERM) || permissions.includes(RolePerms.MANAGE_ANY_TERM); diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx new file mode 100644 index 0000000..9e2d871 --- /dev/null +++ b/apps/web/src/app/main/courses/components/CoursesContainer.tsx @@ -0,0 +1,52 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useMainContext } from "../../layout/MainProvider"; +import { PostType, Prisma } from "@nice/common"; +import { useMemo } from "react"; + +export function CoursesContainer() { + const { searchValue, selectedTerms } = useMainContext(); + const termFilters = useMemo(() => { + return Object.entries(selectedTerms) + .filter(([, terms]) => terms.length > 0) + .map(([, terms]) => terms); + }, [selectedTerms]); + const searchCondition: Prisma.StringNullableFilter = { + contains: searchValue, + mode: "insensitive" as Prisma.QueryMode, // 使用类型断言 + }; + return ( + <> + ({ + terms: { + some: { + id: { + in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 + }, + }, + }, + })), + OR: [ + { title: searchCondition }, + { subTitle: searchCondition }, + { content: searchCondition }, + { + terms: { + some: { + name: searchCondition, + }, + }, + }, + ], + }, + }} + cols={4}> + + ); +} + +export default CoursesContainer; diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index f7a6ced..203e1d1 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -2,23 +2,48 @@ import { Checkbox, Divider, Radio, Space, Spin } from "antd"; import { TaxonomySlug, TermDto } from "@nice/common"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; 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"; export default function FilterSection() { const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); - + const { selectedTerms, setSelectedTerms } = useMainContext(); + const handleTermChange = (slug: string, selected: string[]) => { + setSelectedTerms({ + ...selectedTerms, + [slug]: selected, // 更新对应 slug 的选择 + }); + }; return (
{taxonomies?.map((tax, index) => { + const items = Object.entries(selectedTerms).find( + ([key, items]) => key === tax.slug + )?.[1]; return (

{tax?.name}

- + ( +
{menu}
+ )} + dropdownStyle={{ maxHeight: 400, overflow: "auto" }} + multiple + taxonomyId={tax?.id} + onChange={(selected) => + handleTermChange( + tax?.slug, + selected as string[] + ) + }>
{index < taxonomies.length - 1 && ( )} diff --git a/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx b/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx index b3089f1..5062731 100644 --- a/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx +++ b/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx @@ -1,9 +1,6 @@ -import { useMainContext } from "../../layout/MainProvider"; -import CourseList from "../components/CourseList"; import FilterSection from "../components/FilterSection"; - +import CoursesContainer from "../components/CoursesContainer"; export function AllCoursesLayout() { - const { searchValue, setSearchValue } = useMainContext(); return ( <>
@@ -12,12 +9,7 @@ export function AllCoursesLayout() {
- +
diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index 0b0fb43..9c9225e 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -1,22 +1,4 @@ -import { useState, useMemo, useEffect } from "react"; -import FilterSection from "./components/FilterSection"; -import CourseList from "./components/CourseList"; -import { api } from "@nice/client"; -import { - courseDetailSelect, - CourseDto, - LectureType, - PostType, -} from "@nice/common"; -import { useSearchParams } from "react-router-dom"; -import { set } from "idb-keyval"; -import { useMainContext } from "../layout/MainProvider"; import AllCoursesLayout from "./layout/AllCoursesLayout"; - -interface paginationData { - items: CourseDto[]; - totalPages: number; -} export default function CoursesPage() { return ( <> diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 1c0b8a6..f3ed9ec 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -1,15 +1,16 @@ import React, { useState, useCallback, useEffect, useMemo } from "react"; -import { Typography, Button, Spin, Skeleton } from "antd"; +import { Typography, Skeleton } from "antd"; import { stringToColor, TaxonomySlug, TermDto } from "@nice/common"; import { api } from "@nice/client"; import LookForMore from "./LookForMore"; import CategorySectionCard from "./CategorySectionCard"; +import { useNavigate } from "react-router-dom"; +import { useMainContext } from "../../layout/MainProvider"; const { Title, Text } = Typography; const CategorySection = () => { const [hoveredIndex, setHoveredIndex] = useState(null); - const [showAll, setShowAll] = useState(false); - //获得分类 + const { selectedTerms, setSelectedTerms } = useMainContext(); const { data: courseCategoriesData, isLoading, @@ -18,28 +19,12 @@ const CategorySection = () => { taxonomy: { slug: TaxonomySlug.CATEGORY, }, - }, - include: { - children: true, - }, - orderBy: { - createdAt: "desc", // 按创建时间降序排列 + parentId : null }, take: 8, }); - // 分类展示 - const [displayedCategories, setDisplayedCategories] = useState( - [] - ); - useEffect(() => { - if (!isLoading) { - if (showAll) { - setDisplayedCategories(courseCategoriesData); - } else { - setDisplayedCategories(courseCategoriesData.slice(0, 8)); - } - } - }, [courseCategoriesData, showAll]); + const navigate = useNavigate() + const handleMouseEnter = useCallback((index: number) => { setHoveredIndex(index); }, []); @@ -48,6 +33,13 @@ const CategorySection = () => { setHoveredIndex(null); }, []); + const handleMouseClick = useCallback((categoryId:string) => { + setSelectedTerms({ + [TaxonomySlug.CATEGORY] : [categoryId] + }) + navigate('/courses') + window.scrollTo({top: 0,behavior: "smooth",}) + },[]); return (
@@ -63,21 +55,22 @@ const CategorySection = () => {
{isLoading ? ( - + ) : ( - displayedCategories.map((category, index) => { + courseCategoriesData.map((category, index) => { const categoryColor = stringToColor(category.name); const isHovered = hoveredIndex === index; return ( ); }) diff --git a/apps/web/src/app/main/home/components/CategorySectionCard.tsx b/apps/web/src/app/main/home/components/CategorySectionCard.tsx index a91525c..e3e1273 100644 --- a/apps/web/src/app/main/home/components/CategorySectionCard.tsx +++ b/apps/web/src/app/main/home/components/CategorySectionCard.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; import { Typography } from "antd"; -export default function CategorySectionCard({index,handleMouseEnter,handleMouseLeave,category,categoryColor,isHovered,}) { +export default function CategorySectionCard({handleMouseClick, index,handleMouseEnter,handleMouseLeave,category,categoryColor,isHovered,}) { const navigate = useNavigate() const { Title, Text } = Typography; return ( @@ -12,16 +12,10 @@ export default function CategorySectionCard({index,handleMouseEnter,handleMouseL role="button" tabIndex={0} aria-label={`查看${category.name}课程类别`} - onClick={() => { - console.log(category.name); - navigate( - `/courses?category=${category.name}` - ); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - }}> + onClick={()=>{ + handleMouseClick(category.id) + }} + >
>; + setSelectedTerms?: React.Dispatch>; } const MainContext = createContext(null); @@ -12,11 +17,14 @@ interface MainProviderProps { export function MainProvider({ children }: MainProviderProps) { const [searchValue, setSearchValue] = useState(""); + const [selectedTerms, setSelectedTerms] = useState({}); // 初始化状态 return ( {children} diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index b1a4fb6..53efdb3 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -1,31 +1,37 @@ -import { Menu } from 'antd'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { Menu } from "antd"; +import { useNavigate, useLocation } from "react-router-dom"; const menuItems = [ - { key: 'home', path: '/', label: '首页' }, - { key: 'courses', path: '/courses', label: '全部课程' }, - { key: 'paths', path: '/paths', label: '学习路径' } + { key: "home", path: "/", label: "首页" }, + { key: "courses", path: "/courses", label: "全部课程" }, + { key: "paths", path: "/paths", label: "学习路径" }, ]; export const NavigationMenu = () => { - const navigate = useNavigate(); - const { pathname } = useLocation(); - const selectedKey = menuItems.find(item => item.path === pathname)?.key || ''; - return ( - { - const selectedItem = menuItems.find(item => item.key === key); - if (selectedItem) navigate(selectedItem.path); - }} - > - {menuItems.map(({ key, label }) => ( - - {label} - - ))} - - ); -}; \ No newline at end of file + const navigate = useNavigate(); + const { pathname } = useLocation(); + const selectedKey = + menuItems.find((item) => item.path === pathname)?.key || ""; + return ( + { + const selectedItem = menuItems.find((item) => item.key === key); + if (selectedItem) navigate(selectedItem.path); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }}> + {menuItems.map(({ key, label }) => ( + + {label} + + ))} + + ); +}; diff --git a/apps/web/src/app/main/courses/components/CourseList.tsx b/apps/web/src/components/models/course/list/CourseList.tsx similarity index 93% rename from apps/web/src/app/main/courses/components/CourseList.tsx rename to apps/web/src/components/models/course/list/CourseList.tsx index 2c1ce08..fb1f74e 100755 --- a/apps/web/src/app/main/courses/components/CourseList.tsx +++ b/apps/web/src/components/models/course/list/CourseList.tsx @@ -1,5 +1,5 @@ import { Pagination, Empty, Skeleton } from "antd"; -import CourseCard from "./CourseCard"; +import CourseCard from "../../../../app/main/courses/components/CourseCard"; import { courseDetailSelect, CourseDto, Prisma } from "@nice/common"; import { api } from "@nice/client"; import { DefaultArgs } from "@prisma/client/runtime/library"; @@ -49,7 +49,7 @@ export default function CourseList({ useEffect(() => { setCurrentPage(params?.page || 1); - }, [params?.page]); + }, [params?.page, params]); function onPageChange(page: number, pageSize: number) { setCurrentPage(page); window.scrollTo({ top: 0, behavior: "smooth" }); @@ -71,7 +71,7 @@ export default function CourseList({
React.ReactNode; - emptyComponent?: React.ReactNode; - // 新增分页相关属性 - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; -} - -const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.05, - duration: 0.3, - }, - }, -}; -export const CourseList = ({ - courses, - renderItem, - emptyComponent: EmptyComponent, - currentPage, - totalPages, - onPageChange, -}: CourseListProps) => { - if (!courses || courses.length === 0) { - return EmptyComponent || ; - } - - return ( -
- - {courses.map((course) => ( - - {renderItem(course)} - - ))} - - - -
- ); -}; diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index 07b4741..3896d8e 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -1,5 +1,12 @@ import { TreeSelect, TreeSelectProps } from "antd"; -import React, { useEffect, useState, useCallback, useRef } from "react"; +import React, { + useEffect, + useState, + useCallback, + useRef, + ReactElement, + JSXElementConstructor, +} from "react"; import { getUniqueItems } from "@nice/common"; import { api } from "@nice/client"; import { DefaultOptionType } from "antd/es/select"; @@ -8,25 +15,35 @@ interface TermSelectProps { defaultValue?: string | string[]; value?: string | string[]; onChange?: (value: string | string[]) => void; - placeholder?: string; multiple?: boolean; taxonomyId?: string; disabled?: boolean; - className?: string; domainId?: string; + dropdownStyle?: React.CSSProperties; + style?: React.CSSProperties; + open?: boolean; + showSearch?: boolean; + dropdownRender?: ( + menu: ReactElement> + ) => ReactElement>; } export default function TermSelect({ defaultValue, value, onChange, - className, placeholder = "选择分类", multiple = false, taxonomyId, + open = undefined, + showSearch = true, domainId, + dropdownStyle, + style, disabled = false, -}: TermSelectProps) { + dropdownRender, + ...treeSelectProps +}: TermSelectProps & TreeSelectProps) { const utils = api.useUtils(); const [listTreeData, setListTreeData] = useState< Omit[] @@ -53,7 +70,7 @@ export default function TermSelect({ throw error; } }, - [utils] + [utils, value] ); const fetchTerms = useCallback(async () => { @@ -120,25 +137,35 @@ export default function TermSelect({ } }; - const handleExpand = async (keys: React.Key[]) => { - try { - const allKeyIds = - keys.map((key) => key.toString()).filter(Boolean) || []; - const expandedNodes = await utils.term.getChildSimpleTree.fetch({ - termIds: allKeyIds, - taxonomyId, - domainId, - }); - const flattenedNodes = expandedNodes.flat(); - const newItems = getUniqueItems( - [...listTreeData, ...flattenedNodes], - "id" - ); - setListTreeData(newItems); - } catch (error) { - console.error("Error expanding nodes with keys", keys, ":", error); - } - }; + const handleExpand = useCallback( + async (keys: React.Key[]) => { + try { + const allKeyIds = + keys.map((key) => key.toString()).filter(Boolean) || []; + const expandedNodes = await utils.term.getChildSimpleTree.fetch( + { + termIds: allKeyIds, + taxonomyId, + domainId, + } + ); + const flattenedNodes = expandedNodes.flat(); + const newItems = getUniqueItems( + [...listTreeData, ...flattenedNodes], + "id" + ); + setListTreeData(newItems); + } catch (error) { + console.error( + "Error expanding nodes with keys", + keys, + ":", + error + ); + } + }, + [value] + ); const handleDropdownVisibleChange = async (open: boolean) => { if (open) { // This will attempt to expand all nodes and fetch their children when the dropdown opens @@ -150,17 +177,15 @@ export default function TermSelect({ handleChange(multiple ? [] : undefined)} onTreeExpand={handleExpand} onDropdownVisibleChange={handleDropdownVisibleChange} + {...treeSelectProps} /> ); } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 9a36b03..34aca4f 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -69,10 +69,10 @@ export const InitTaxonomies: { // name: "研判单元", // slug: TaxonomySlug.UNIT, // }, - { - name: "标签", - slug: TaxonomySlug.TAG, - }, + // { + // name: "标签", + // slug: TaxonomySlug.TAG, + // }, ]; export const InitAppConfigs: Prisma.AppConfigCreateInput[] = [ {