diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts old mode 100644 new mode 100755 diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/couresPreviewTabmsg.tsx b/apps/web/src/app/main/course/preview/components/couresPreviewTabmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/courseCatalog.tsx b/apps/web/src/app/main/course/preview/components/courseCatalog.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx b/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/page.tsx b/apps/web/src/app/main/course/preview/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/type.ts b/apps/web/src/app/main/course/preview/type.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 597caf0..fa4b0d5 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -87,11 +87,11 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) {
- + {`观看次数 ${course?.meta?.views || 0}`} - + {`学习人数 ${course?.studentIds?.length || 0}`} @@ -102,7 +102,7 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { size="large" className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)] transform hover:translate-y-[-2px] transition-all duration-500 ease-out"> - {edit ? "进行编辑" : "立即学习"} + {edit ? "编辑" : "立即学习"}
diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx old mode 100644 new mode 100755 index 944620f..d3b7562 --- a/apps/web/src/app/main/courses/components/CoursesContainer.tsx +++ b/apps/web/src/app/main/courses/components/CoursesContainer.tsx @@ -6,16 +6,7 @@ import CourseCard from "./CourseCard"; import PostCard from "@web/src/components/models/course/card/PostCard"; 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, // 使用类型断言 - }; + const {searchCondition, termsCondition } = useMainContext(); return ( <> ({ - terms: { - some: { - id: { - in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 - }, - }, - }, - })), - OR: [ - { title: searchCondition }, - { subTitle: searchCondition }, - { content: searchCondition }, - { - terms: { - some: { - name: searchCondition, - }, - }, - }, - ], + ...termsCondition, + ...searchCondition, }, }} cols={4}> diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx deleted file mode 100755 index b437058..0000000 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Checkbox, Divider, Radio, Space, Spin } from "antd"; - -import { TaxonomySlug, TermDto } from "@nice/common"; - -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"; -import TermParentSelector from "@web/src/components/models/term/term-parent-selector"; - -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} -

- - handleTermChange( - tax?.slug, - selected as string[] - ) - } - taxonomyId={tax?.id} - > - {/* ( -
{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 deleted file mode 100644 index 5062731..0000000 --- a/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import FilterSection from "../components/FilterSection"; -import CoursesContainer from "../components/CoursesContainer"; -export function AllCoursesLayout() { - return ( - <> -
-
-
- -
-
- -
-
-
- - ); -} -export default AllCoursesLayout; diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index 9c9225e..79eda89 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -1,8 +1,18 @@ -import AllCoursesLayout from "./layout/AllCoursesLayout"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import CoursesContainer from "./components/CoursesContainer"; +import { useEffect } from "react"; +import { useMainContext } from "../layout/MainProvider"; +import { PostType } from "@nice/common"; export default function CoursesPage() { + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.COURSE); + }, [setSearchMode]); return ( <> - + + + ); } diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index d589d35..4c8141e 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -19,11 +19,11 @@ const CategorySection = () => { taxonomy: { slug: TaxonomySlug.CATEGORY, }, - parentId : null + parentId: null, }, take: 8, }); - const navigate = useNavigate() + const navigate = useNavigate(); const handleMouseEnter = useCallback((index: number) => { setHoveredIndex(index); @@ -33,13 +33,13 @@ const CategorySection = () => { setHoveredIndex(null); }, []); - const handleMouseClick = useCallback((categoryId:string) => { + const handleMouseClick = useCallback((categoryId: string) => { setSelectedTerms({ - [TaxonomySlug.CATEGORY] : [categoryId] - }) - navigate('/courses') - window.scrollTo({top: 0,behavior: "smooth",}) - },[]); + [TaxonomySlug.CATEGORY]: [categoryId], + }); + navigate("/courses"); + window.scrollTo({ top: 0, behavior: "smooth" }); + }, []); return (
@@ -57,7 +57,7 @@ const CategorySection = () => { {isLoading ? ( ) : ( - courseCategoriesData.map((category, index) => { + courseCategoriesData?.map((category, index) => { const categoryColor = stringToColor(category.name); const isHovered = hoveredIndex === index; diff --git a/apps/web/src/app/main/home/components/CategorySectionCard.tsx b/apps/web/src/app/main/home/components/CategorySectionCard.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/home/components/CoursesSectionTag.tsx b/apps/web/src/app/main/home/components/CoursesSectionTag.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/home/components/LookForMore.tsx b/apps/web/src/app/main/home/components/LookForMore.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx b/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx new file mode 100644 index 0000000..f93ea11 --- /dev/null +++ b/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx @@ -0,0 +1,29 @@ +import { ReactNode, useEffect } from "react"; +import FilterSection from "./FilterSection"; +import { useMainContext } from "../MainProvider"; + +export function BasePostLayout({ + children, + showSearchMode = false, +}: { + children: ReactNode; + showSearchMode?: boolean; +}) { + const { setShowSearchMode } = useMainContext(); + useEffect(() => { + setShowSearchMode(showSearchMode); + }, [showSearchMode]); + return ( + <> +
+
+
+ +
+
{children}
+
+
+ + ); +} +export default BasePostLayout; diff --git a/apps/web/src/app/main/path/components/PathFilter.tsx b/apps/web/src/app/main/layout/BasePost/FilterSection.tsx similarity index 58% rename from apps/web/src/app/main/path/components/PathFilter.tsx rename to apps/web/src/app/main/layout/BasePost/FilterSection.tsx index 01442c8..25575d1 100755 --- a/apps/web/src/app/main/path/components/PathFilter.tsx +++ b/apps/web/src/app/main/layout/BasePost/FilterSection.tsx @@ -1,11 +1,12 @@ - +import { Divider } from "antd"; import { api } from "@nice/client"; -import { useMainContext } from "../../layout/MainProvider"; +import { useMainContext } from "../MainProvider"; import TermParentSelector from "@web/src/components/models/term/term-parent-selector"; - -export default function PathFilter() { +import SearchModeRadio from "./SearchModeRadio"; +export default function FilterSection() { const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); - const { selectedTerms, setSelectedTerms } = useMainContext(); + const { selectedTerms, setSelectedTerms, showSearchMode } = + useMainContext(); const handleTermChange = (slug: string, selected: string[]) => { setSelectedTerms({ ...selectedTerms, @@ -13,7 +14,8 @@ export default function PathFilter() { }); }; return ( -
+
+ {showSearchMode && } {taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( ([key, items]) => key === tax.slug @@ -25,17 +27,16 @@ export default function PathFilter() { handleTermChange( tax?.slug, selected as string[] ) } - taxonomyId={tax?.id} - > - + taxonomyId={tax?.id}> +
); })} diff --git a/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx b/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx new file mode 100644 index 0000000..692bfe2 --- /dev/null +++ b/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx @@ -0,0 +1,25 @@ +import { useMainContext } from "../MainProvider"; +import { Radio, Space, Typography } from "antd"; +import { PostType } from "@nice/common"; // Assuming PostType is defined in this path + +export default function SearchModeRadio() { + const { searchMode, setSearchMode } = useMainContext(); + + const handleModeChange = (e) => { + setSearchMode(e.target.value); + }; + + return ( + +

搜索模式

+ + 课程 + 路径 + 全部 + +
+ ); +} diff --git a/apps/web/src/app/main/layout/MainFooter.tsx b/apps/web/src/app/main/layout/MainFooter.tsx index 9d060dc..b232752 100755 --- a/apps/web/src/app/main/layout/MainFooter.tsx +++ b/apps/web/src/app/main/layout/MainFooter.tsx @@ -1,68 +1,76 @@ -import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons'; +import { + CloudOutlined, + FileSearchOutlined, + HomeOutlined, + MailOutlined, + PhoneOutlined, +} from "@ant-design/icons"; export function MainFooter() { - return ( -
-
-
- {/* 开发组织信息 */} -
-

- 软件与数据小组 -

-

- 提供技术支持 -

-
+ return ( +
+
+
+ {/* 开发组织信息 */} +
+

+ 软件与数据小组 +

+

+ 提供技术支持 +

+
- {/* 联系方式 */} -
-
- - 628118 -
-
- - gcsjs6@tx3l.nb.kj -
-
+ {/* 联系方式 */} +
+
+ + + 628118 + +
+
+ + + gcsjs6@tx3l.nb.kj + +
+
- {/* 系统链接 */} -
-
- - - - - - + {/* 系统链接 */} +
+ -
-
+ + + +
+
+
- {/* 版权信息 */} -
-

- © {new Date().getFullYear()} 南天烽火. All rights reserved. -

-
-
-
- ); + {/* 版权信息 */} +
+

+ © {new Date().getFullYear()} 南天烽火. All rights + reserved. +

+
+
+ + ); } diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index dfbf5e2..f0d27c1 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -1,10 +1,16 @@ import { Input, Layout, Avatar, Button, Dropdown } from "antd"; -import { EditFilled, PlusOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; +import { + EditFilled, + PlusOutlined, + SearchOutlined, + UserOutlined, +} from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { UserMenu } from "./UserMenu/UserMenu"; import { NavigationMenu } from "./NavigationMenu"; import { useMainContext } from "./MainProvider"; +import { Header } from "antd/es/layout/layout"; export function MainHeader() { const { isAuthenticated, user } = useAuth(); @@ -12,76 +18,83 @@ export function MainHeader() { const navigate = useNavigate(); const { searchValue, setSearchValue } = useMainContext(); - return ( -
- -
-
navigate("/")} - className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> - 烽火慕课 +
+
+
+
navigate("/")} + className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> + 烽火慕课 +
+
-
- - } - placeholder="搜索课程" - className="w-96 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if ( - !window.location.pathname.startsWith( - "/courses/" - ) - ) { - navigate(`/courses/`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); +
+ } - }} - /> -
- {isAuthenticated && ( - <> + placeholder="搜索课程" + className="w-96 rounded-full" + value={searchValue} + onClick={(e) => { + if (!window.location.pathname.startsWith("/search")) { + navigate(`/search`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + onChange={(e) => setSearchValue(e.target.value)} + onPressEnter={(e) => { + if (!window.location.pathname.startsWith("/search")) { + navigate(`/search`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + /> +
+ {isAuthenticated && ( + <> + + + )} + {isAuthenticated && ( - - )} - { - isAuthenticated && - } - {isAuthenticated ? ( - - ) : ( - - )} + )} + {isAuthenticated ? ( + + ) : ( + + )} +
); diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index f5b462b..fa6627a 100755 --- a/apps/web/src/app/main/layout/MainLayout.tsx +++ b/apps/web/src/app/main/layout/MainLayout.tsx @@ -11,7 +11,7 @@ export function MainLayout() {
- + diff --git a/apps/web/src/app/main/layout/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx old mode 100644 new mode 100755 index 0952268..2fc4c5f --- a/apps/web/src/app/main/layout/MainProvider.tsx +++ b/apps/web/src/app/main/layout/MainProvider.tsx @@ -1,4 +1,11 @@ -import React, { createContext, ReactNode, useContext, useState } from "react"; +import { PostType, Prisma } from "packages/common/dist"; +import React, { + createContext, + ReactNode, + useContext, + useMemo, + useState, +} from "react"; interface SelectedTerms { [key: string]: string[]; // 每个 slug 对应一个 string 数组 } @@ -8,6 +15,14 @@ interface MainContextType { selectedTerms?: SelectedTerms; setSearchValue?: React.Dispatch>; setSelectedTerms?: React.Dispatch>; + searchCondition?: Prisma.PostWhereInput; + termsCondition?: Prisma.PostWhereInput; + searchMode?: PostType.COURSE | PostType.PATH | "both"; + setSearchMode?: React.Dispatch< + React.SetStateAction + >; + showSearchMode?: boolean; + setShowSearchMode?: React.Dispatch>; } const MainContext = createContext(null); @@ -16,8 +31,55 @@ interface MainProviderProps { } export function MainProvider({ children }: MainProviderProps) { + const [searchMode, setSearchMode] = useState< + PostType.COURSE | PostType.PATH | "both" + >("both"); + const [showSearchMode, setShowSearchMode] = useState(false); const [searchValue, setSearchValue] = useState(""); const [selectedTerms, setSelectedTerms] = useState({}); // 初始化状态 + const termFilters = useMemo(() => { + return Object.entries(selectedTerms) + .filter(([, terms]) => terms.length > 0) + ?.map(([, terms]) => terms); + }, [selectedTerms]); + + const termsCondition: Prisma.PostWhereInput = useMemo(() => { + return termFilters && termFilters?.length > 0 + ? { + AND: termFilters.map((termFilter) => ({ + terms: { + some: { + id: { + in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 + }, + }, + }, + })), + } + : {}; + }, [termFilters]); + const searchCondition: Prisma.PostWhereInput = useMemo(() => { + const containTextCondition: Prisma.StringNullableFilter = { + contains: searchValue, + mode: "insensitive" as Prisma.QueryMode, // 使用类型断言 + }; + return searchValue + ? { + OR: [ + { title: containTextCondition }, + { subTitle: containTextCondition }, + { content: containTextCondition }, + { + terms: { + some: { + name: containTextCondition, + }, + }, + }, + ], + } + : {}; + }, [searchValue]); return ( {children} diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 63ab7d1..dfc8c23 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -14,13 +14,15 @@ export const NavigationMenu = () => { { key: "courses", path: "/courses", label: "全部课程" }, { key: "path", path: "/path", label: "学习路径" }, ]; + if (!isAuthenticated) { return baseItems; } else { return [ ...baseItems, - { key: "my-duty", path: "/my-duty", label: "我创建的" }, - { key: "my-learning", path: "/my-learning", label: "我学习的" }, + { key: "my-duty", path: "/my-duty", label: "我的授课" }, + { key: "my-learning", path: "/my-learning", label: "我的课程" }, + { key: "my-path", path: "/my-path", label: "我的路径" }, ]; } }, [isAuthenticated]); @@ -31,6 +33,7 @@ export const NavigationMenu = () => { { const selectedItem = menuItems.find((item) => item.key === key); diff --git a/apps/web/src/app/main/layout/UserMenu/UserEditModal.tsx b/apps/web/src/app/main/layout/UserMenu/UserEditModal.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/UserMenu/UserForm.tsx b/apps/web/src/app/main/layout/UserMenu/UserForm.tsx old mode 100644 new mode 100755 index efbf12c..a496ed0 --- a/apps/web/src/app/main/layout/UserMenu/UserForm.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserForm.tsx @@ -12,15 +12,7 @@ import toast from "react-hot-toast"; export default function StaffForm() { const { user } = useAuth(); const { create, update } = useStaff(); // Ensure you have these methods in your hooks - const { - formLoading, - modalOpen, - setModalOpen, - domainId, - setDomainId, - form, - setFormLoading, - } = useContext(UserEditorContext); + const {formLoading,modalOpen,setModalOpen,domainId,setDomainId,form,setFormLoading,} = useContext(UserEditorContext); const { data, isLoading, @@ -76,6 +68,8 @@ export default function StaffForm() { } useEffect(() => { form.resetFields(); + console.log('cc',data); + if (data) { form.setFieldValue("username", data.username); form.setFieldValue("showname", data.showname); diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index e3ef21f..17c3413 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -86,20 +86,7 @@ export function UserMenu() { setModalOpen(true); }, }, - { - icon: , - label: "我创建的课程", - action: () => { - navigate("/my/duty"); - }, - }, - { - icon: , - label: "我学习的课程", - action: () => { - navigate("/my/learning"); - }, - }, + canManageAnyStaff && { icon: , label: "设置", diff --git a/apps/web/src/app/main/layout/UserMenu/types.ts b/apps/web/src/app/main/layout/UserMenu/types.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx b/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx new file mode 100644 index 0000000..f656398 --- /dev/null +++ b/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx @@ -0,0 +1,29 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useAuth } from "@web/src/providers/auth-provider"; + +import CourseCard from "../../courses/components/CourseCard"; +import { PostType } from "@nice/common"; +import { useMainContext } from "../../layout/MainProvider"; + +export default function MyDutyListContainer() { + const { user } = useAuth(); + const { searchCondition, termsCondition } = useMainContext(); + return ( + <> + ( + + )} + params={{ + pageSize: 12, + where: { + type: PostType.COURSE, + authorId: user.id, + ...termsCondition, + ...searchCondition, + }, + }} + cols={4}> + + ); +} diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx old mode 100644 new mode 100755 index a22e105..fff4c91 --- a/apps/web/src/app/main/my-duty/page.tsx +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -1,24 +1,16 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; -import CourseCard from "../courses/components/CourseCard"; -import PostCard from "@web/src/components/models/course/card/PostCard"; - +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import MyDutyListContainer from "./components/MyDutyListContainer"; +import { useEffect } from "react"; +import { useMainContext } from "../layout/MainProvider"; +import { PostType } from "@nice/common"; export default function MyDutyPage() { - const { user } = useAuth(); + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.COURSE); + }, [setSearchMode]); return ( - <> -
- } - - params={{ - pageSize: 12, - where: { - authorId: user.id, - }, - }} - cols={4}> -
- + + + ); } diff --git a/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx b/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx new file mode 100644 index 0000000..a6e8226 --- /dev/null +++ b/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx @@ -0,0 +1,32 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { useMainContext } from "../../layout/MainProvider"; +import CourseCard from "../../courses/components/CourseCard"; +import { PostType } from "@nice/common"; + +export default function MyLearningListContainer() { + const { user } = useAuth(); + const { searchCondition, termsCondition } = useMainContext(); + return ( + <> + ( + + )} + params={{ + pageSize: 12, + where: { + type: PostType.COURSE, + students: { + some: { + id: user?.id, + }, + }, + ...termsCondition, + ...searchCondition, + }, + }} + cols={4}> + + ); +} diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx old mode 100644 new mode 100755 index 14278e8..cee2c94 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -1,26 +1,17 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; -import CourseCard from "../courses/components/CourseCard"; +import { useEffect } from "react"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import { useMainContext } from "../layout/MainProvider"; +import MyLearningListContainer from "./components/MyLearningListContainer"; +import { PostType } from "@nice/common"; export default function MyLearningPage() { - const { user } = useAuth(); + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.COURSE); + }, [setSearchMode]); return ( - <> -
- } - params={{ - pageSize: 12, - where: { - students: { - some: { - id: user?.id, - }, - }, - }, - }} - cols={4}> -
- + + + ); } diff --git a/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx b/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx new file mode 100644 index 0000000..ebbba92 --- /dev/null +++ b/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx @@ -0,0 +1,28 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useAuth } from "@web/src/providers/auth-provider"; + + +import { PostType } from "@nice/common"; +import { useMainContext } from "../../layout/MainProvider"; +import PathCard from "../../path/components/PathCard"; + +export default function MyPathListContainer() { + const { user } = useAuth(); + const { searchCondition, termsCondition } = useMainContext(); + return ( + <> + } + params={{ + pageSize: 12, + where: { + type: PostType.PATH, + authorId: user.id, + ...termsCondition, + ...searchCondition, + }, + }} + cols={4}> + + ); +} diff --git a/apps/web/src/app/main/my-path/page.tsx b/apps/web/src/app/main/my-path/page.tsx new file mode 100755 index 0000000..81f7298 --- /dev/null +++ b/apps/web/src/app/main/my-path/page.tsx @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import { useMainContext } from "../layout/MainProvider"; +import MyPathListContainer from "./components/MyPathListContainer"; +import { PostType } from "@nice/common"; + +export default function MyPathPage() { + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.PATH); + }, [setSearchMode]); + return ( + + + + ); +} diff --git a/apps/web/src/app/main/path/components/DeptInfo.tsx b/apps/web/src/app/main/path/components/DeptInfo.tsx new file mode 100644 index 0000000..2cec836 --- /dev/null +++ b/apps/web/src/app/main/path/components/DeptInfo.tsx @@ -0,0 +1,22 @@ +import { TeamOutlined } from "@ant-design/icons"; +import { Typography } from "antd"; + +const { Title, Text } = Typography; +const DeptInfo = ({ path }) => { + return ( +
+ + {path?.depts && path?.depts?.length > 0 ? ( + + {path?.depts?.length > 1 ? `${path.depts[0].name}等` : path?.depts?.[0]?.name} + + ) : ( + + 未设置单位 + + )} +
+ ); +}; + +export default DeptInfo; \ No newline at end of file diff --git a/apps/web/src/app/main/path/components/PathCard.tsx b/apps/web/src/app/main/path/components/PathCard.tsx index 8ccb01f..32f5d43 100755 --- a/apps/web/src/app/main/path/components/PathCard.tsx +++ b/apps/web/src/app/main/path/components/PathCard.tsx @@ -1,10 +1,11 @@ -import { Card, Rate, Tag, Typography, Button } from "antd"; +import { Card, Tag, Typography, Button } from "antd"; import { - PlayCircleOutlined, - TeamOutlined, + EyeOutlined } from "@ant-design/icons"; import { PostDto, TaxonomySlug } from "@nice/common"; import { useNavigate } from "react-router-dom"; +import DeptInfo from "./DeptInfo"; +import TermInfo from "./TermInfo"; interface pathCardProps { path: PostDto; } @@ -13,15 +14,14 @@ export default function PathCard({ path }: pathCardProps) { const navigate = useNavigate(); const handleClick = (path: PostDto) => { navigate(`/path/editor/${path.id}`); - window.scrollTo({ top: 0, behavior: "smooth", }) + window.scrollTo({ top: 0, behavior: "smooth" }); }; return ( handleClick(path)} key={path.id} hoverable - className="group overflow-hidden rounded-xl border border-gray-200 bg-white - shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" + className=" group overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" cover={
}>
-
- {path?.terms?.map((term) => { - return ( - - {term.name} - - ); - })} -
- + @@ -63,20 +42,12 @@ export default function PathCard({ path }: pathCardProps) {
- -
- - {path?.depts?.length > 1 - ? `${path.depts[0].name}等` - : path?.depts?.[0]?.name} - {/* {path?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */} - {/* {path?.depts?.map((dept)=>{return dept.name})} */} - -
- + + + {path?.meta?.views ? `观看次数 ${path?.meta?.views}` - : null} + : 0}
diff --git a/apps/web/src/app/main/path/components/PathListContainer.tsx b/apps/web/src/app/main/path/components/PathListContainer.tsx old mode 100644 new mode 100755 index 41f67c5..57dfcb9 --- a/apps/web/src/app/main/path/components/PathListContainer.tsx +++ b/apps/web/src/app/main/path/components/PathListContainer.tsx @@ -1,21 +1,11 @@ import PostList from "@web/src/components/models/course/list/PostList"; import { useMainContext } from "../../layout/MainProvider"; import { PostType, Prisma } from "@nice/common"; -import { useMemo } from "react"; import PathCard from "./PathCard"; import PostCard from "@web/src/components/models/course/card/PostCard"; export function PathListContainer() { - 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, // 使用类型断言 - }; + const { searchCondition, termsCondition } = useMainContext(); return ( <> ({ - terms: { - some: { - id: { - in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 - }, - }, - }, - })), - OR: [ - { title: searchCondition }, - { subTitle: searchCondition }, - { content: searchCondition }, - { - terms: { - some: { - name: searchCondition, - }, - }, - }, - ], + ...termsCondition, + ...searchCondition, }, }} cols={4}> ); } - export default PathListContainer; diff --git a/apps/web/src/app/main/path/components/TermInfo.tsx b/apps/web/src/app/main/path/components/TermInfo.tsx new file mode 100644 index 0000000..421909b --- /dev/null +++ b/apps/web/src/app/main/path/components/TermInfo.tsx @@ -0,0 +1,41 @@ +import { Tag } from "antd"; +import { TaxonomySlug } from "@nice/common"; + +const TermInfo = ({ path }) => { + console.log('xx',path?.terms); + + return <> + {path?.terms && path?.terms?.length > 0 ? ( +
+ {path?.terms?.map((term:any) => { + return ( + + {term.name} + + ); + })} +
+ ) : ( +
+ + {"未设置分类"} + +
+ )} + +}; + +export default TermInfo; \ No newline at end of file diff --git a/apps/web/src/app/main/path/editor/page.tsx b/apps/web/src/app/main/path/editor/page.tsx old mode 100644 new mode 100755 index bffdec2..5ce3a20 --- a/apps/web/src/app/main/path/editor/page.tsx +++ b/apps/web/src/app/main/path/editor/page.tsx @@ -2,9 +2,11 @@ import MindEditor from "@web/src/components/common/editor/MindEditor"; import { useParams } from "react-router-dom"; export default function PathEditorPage() { - const { id } = useParams(); + const { id } = useParams(); - return
- -
+ return ( +
+ +
+ ); } diff --git a/apps/web/src/app/main/path/layout/PathListLayout.tsx b/apps/web/src/app/main/path/layout/PathListLayout.tsx deleted file mode 100644 index 19ebe94..0000000 --- a/apps/web/src/app/main/path/layout/PathListLayout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import PathFilter from "../components/PathFilter"; -import PathListContainer from "../components/PathListContainer"; - -export function PathListLayout() { - return ( - <> -
-
-
- -
-
- -
-
-
- - ); -} -export default PathListLayout; diff --git a/apps/web/src/app/main/path/page.tsx b/apps/web/src/app/main/path/page.tsx index 825f3f7..65178db 100755 --- a/apps/web/src/app/main/path/page.tsx +++ b/apps/web/src/app/main/path/page.tsx @@ -1,5 +1,17 @@ -import PathListLayout from "./layout/PathListLayout"; +import { useEffect } from "react"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import { useMainContext } from "../layout/MainProvider"; +import PathListContainer from "./components/PathListContainer"; +import { PostType } from "@nice/common"; export default function PathPage() { - return + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.PATH); + }, [setSearchMode]); + return ( + + + + ); } diff --git a/apps/web/src/app/main/search/components/SearchContainer.tsx b/apps/web/src/app/main/search/components/SearchContainer.tsx new file mode 100644 index 0000000..cbfbf56 --- /dev/null +++ b/apps/web/src/app/main/search/components/SearchContainer.tsx @@ -0,0 +1,23 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useMainContext } from "../../layout/MainProvider"; +import PathCard from "../../path/components/PathCard"; +import { useEffect } from "react"; +export default function SearchListContainer() { + const { searchCondition, termsCondition, searchMode } = useMainContext(); + + return ( + <> + } + params={{ + pageSize: 12, + where: { + type: searchMode === "both" ? undefined : searchMode, + ...termsCondition, + ...searchCondition, + }, + }} + cols={4}> + + ); +} diff --git a/apps/web/src/app/main/search/page.tsx b/apps/web/src/app/main/search/page.tsx new file mode 100755 index 0000000..cfb6153 --- /dev/null +++ b/apps/web/src/app/main/search/page.tsx @@ -0,0 +1,20 @@ +import { useEffect } from "react"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import SearchListContainer from "./components/SearchContainer"; +import { useMainContext } from "../layout/MainProvider"; + +export default function SearchPage() { + const { setShowSearchMode, setSearchValue } = useMainContext(); + useEffect(() => { + setShowSearchMode(true); + return () => { + setShowSearchMode(false); + setSearchValue(""); + }; + }, [setShowSearchMode]); + return ( + + + + ); +} diff --git a/apps/web/src/components/common/container/CollapsibleContent.tsx b/apps/web/src/components/common/container/CollapsibleContent.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index dd6306e..4528b29 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -1,15 +1,22 @@ -import { Button, Card, Empty, Form, Space, Spin, message, theme } from 'antd'; -import NodeMenu from './NodeMenu'; -import { useEntity, api, usePost } from '@nice/client'; -import { ObjectType, postDetailSelect, PostDto, PostType, Prisma, Taxonomy } from '@nice/common'; -import TermSelect from '../../models/term/term-select'; -import DepartmentSelect from '../../models/department/department-select'; -import { useEffect, useRef, useState } from 'react'; -import toast from 'react-hot-toast'; -import { MindElixirInstance } from 'mind-elixir'; -import MindElixir from 'mind-elixir'; -import { useTusUpload } from '@web/src/hooks/useTusUpload'; -import { useNavigate } from 'react-router-dom'; +import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd"; +import NodeMenu from "./NodeMenu"; +import { api, usePost } from "@nice/client"; +import { + ObjectType, + postDetailSelect, + PostDto, + PostType, + Prisma, + Taxonomy, +} from "@nice/common"; +import TermSelect from "../../models/term/term-select"; +import DepartmentSelect from "../../models/department/department-select"; +import { useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { MindElixirInstance } from "mind-elixir"; +import MindElixir from "mind-elixir"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; +import { useNavigate } from "react-router-dom"; const MIND_OPTIONS = { direction: MindElixir.SIDE, draggable: true, @@ -17,53 +24,54 @@ const MIND_OPTIONS = { toolBar: true, nodeMenu: true, keypress: true, - locale: 'zh_CN' as const, + locale: "zh_CN" as const, theme: { - name: 'Latte', + name: "Latte", palette: [ - '#dd7878', - '#ea76cb', - '#8839ef', - '#e64553', - '#fe640b', - '#df8e1d', - '#40a02b', - '#209fb5', - '#1e66f5', - '#7287fd', + "#dd7878", + "#ea76cb", + "#8839ef", + "#e64553", + "#fe640b", + "#df8e1d", + "#40a02b", + "#209fb5", + "#1e66f5", + "#7287fd", ], cssVar: { - '--main-color': '#444446', - '--main-bgcolor': '#ffffff', - '--color': '#777777', - '--bgcolor': '#f6f6f6', - '--panel-color': '#444446', - '--panel-bgcolor': '#ffffff', - '--panel-border-color': '#eaeaea', + "--main-color": "#444446", + "--main-bgcolor": "#ffffff", + "--color": "#777777", + "--bgcolor": "#f6f6f6", + "--panel-color": "#444446", + "--panel-bgcolor": "#ffffff", + "--panel-border-color": "#eaeaea", }, - } + }, }; export default function MindEditor({ id }: { id?: string }) { const containerRef = useRef(null); const [instance, setInstance] = useState(null); - const { data: post, isLoading }: { data: PostDto, isLoading: boolean } = api.post.findFirst.useQuery({ - where: { - id - }, - select: postDetailSelect - }) - const navigate = useNavigate() + const { data: post, isLoading }: { data: PostDto; isLoading: boolean } = + api.post.findFirst.useQuery({ + where: { + id, + }, + select: postDetailSelect, + }); + const navigate = useNavigate(); const { create, update } = usePost(); const { data: taxonomies } = api.taxonomy.getAll.useQuery({ type: ObjectType.COURSE, }); - const { handleFileUpload } = useTusUpload() - const [form] = Form.useForm() + const { handleFileUpload } = useTusUpload(); + const [form] = Form.useForm(); useEffect(() => { if (post && form && instance && id) { - console.log(post) - instance.refresh((post as any).meta) + console.log(post); + instance.refresh((post as any).meta); const deptIds = (post?.depts || [])?.map((dept) => dept.id); const formData = { title: post.title, @@ -82,116 +90,137 @@ export default function MindEditor({ id }: { id?: string }) { ...MIND_OPTIONS, el: containerRef.current, }); - mind.init(MindElixir.new('新学习路径')); + mind.init(MindElixir.new("新学习路径")); containerRef.current.hidden = true; setInstance(mind); }, []); useEffect(() => { if ((!id || post) && instance) { - containerRef.current.hidden = false - instance.toCenter() - instance.refresh((post as any)?.meta) + containerRef.current.hidden = false; + instance.toCenter(); + instance.refresh((post as any)?.meta); } - }, [id, post, instance]) + }, [id, post, instance]); const handleSave = async () => { if (!instance) return; - const values = form.getFieldsValue() - const imgBlob = await instance?.exportPng() - handleFileUpload(imgBlob, async (result) => { - const termIds = taxonomies.map((tax) => values[tax.id]).filter((id) => id); - const deptIds = (values?.deptIds || []) as string[]; - const { theme, ...data } = instance.getData(); - try { - if (post && id) { - const params: Prisma.PostUpdateArgs = { - where: { - id - }, - data: { + const values = form.getFieldsValue(); + const imgBlob = await instance?.exportPng(); + handleFileUpload( + imgBlob, + async (result) => { + const termIds = taxonomies + .map((tax) => values[tax.id]) + .filter((id) => id); + const deptIds = (values?.deptIds || []) as string[]; + const { theme, ...data } = instance.getData(); + try { + if (post && id) { + const params: Prisma.PostUpdateArgs = { + where: { + id, + }, + data: { + title: data.nodeData.topic, + meta: { + ...data, + thumbnail: result.compressedUrl, + }, + terms: { + set: termIds.map((id) => ({ id })), + }, + depts: { + set: deptIds.map((id) => ({ id })), + }, + updatedAt: new Date(), + }, + }; + await update.mutateAsync(params); + toast.success("更新成功"); + } else { + const params: Prisma.PostCreateInput = { + type: PostType.PATH, title: data.nodeData.topic, meta: { ...data, thumbnail: result.compressedUrl }, terms: { - set: termIds.map((id) => ({ id })) + connect: termIds.map((id) => ({ id })), }, depts: { - set: deptIds.map((id) => ({ id })), + connect: deptIds.map((id) => ({ id })), }, - updatedAt: new Date() - } + updatedAt: new Date(), + }; + + const res = await create.mutateAsync({ data: params }); + navigate(`/path/editor/${res.id}`, { replace: true }); + toast.success("创建成功"); } - await update.mutateAsync(params); - toast.success('更新成功'); - } else { - const params: Prisma.PostCreateInput = { - type: PostType.PATH, - title: data.nodeData.topic, - meta: { ...data, thumbnail: result.compressedUrl }, - terms: { - connect: termIds.map((id) => ({ id })) - }, - depts: { - connect: deptIds.map((id) => ({ id })), - }, - updatedAt: new Date() - }; - - const res = await create.mutateAsync({ data: params }); - navigate(`/path/editor/${res.id}`, { replace: true }) - toast.success('创建成功'); + } catch (error) { + toast.error("保存失败"); + throw error; } - } catch (error) { - toast.error('保存失败'); - throw error; - } - console.log(result) - }, (error) => { }, `mind-thumb-${new Date().toString()}`) - - + console.log(result); + }, + (error) => {}, + `mind-thumb-${new Date().toString()}` + ); }; return ( -
+
{taxonomies && ( -
{ - console.log(values) - }} form={form} className=' bg-white p-2 '> -
-
+ { + console.log(values); + }} + form={form} + className=" bg-white p-2 "> +
+
{taxonomies.map((tax, index) => ( + // rules={[{ required: true }]} + noStyle> - ))} - - + +
- +
- ) - } -
- {instance && ( )} - {isLoading &&
- -
} - {!post && id && !isLoading &&
- -
} +
+ {instance && } + {isLoading && ( +
+ +
+ )} + {!post && id && !isLoading && ( +
+ +
+ )}
); } diff --git a/apps/web/src/components/common/editor/NodeMenu.tsx b/apps/web/src/components/common/editor/NodeMenu.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/i18n.ts b/apps/web/src/components/common/editor/i18n.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/types.ts b/apps/web/src/components/common/editor/types.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/input/InputList.tsx b/apps/web/src/components/common/input/InputList.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/ResourceShower.tsx b/apps/web/src/components/common/uploader/ResourceShower.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/utils.tsx b/apps/web/src/components/common/uploader/utils.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 0da1702..34ca11c 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -36,12 +36,8 @@ interface CourseFormProviderProps { editId?: string; // 添加 editId 参数 } -export const CourseDetailContext = - createContext(null); -export function CourseDetailProvider({ - children, - editId, -}: CourseFormProviderProps) { +export const CourseDetailContext =createContext(null); +export function CourseDetailProvider({children,editId}: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); const { user, hasSomePermissions, isAuthenticated } = useAuth(); @@ -64,6 +60,7 @@ export function CourseDetailProvider({ const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); return isAuthor || isRoot; }, [user, course]); + const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(lectureId || undefined); diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx index 05a3126..6fdabf6 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx @@ -15,14 +15,13 @@ import dayjs from "dayjs"; import { useNavigate, useParams } from "react-router-dom"; export const CourseDetailDescription: React.FC = () => { - const { course, isLoading, selectedLectureId, setSelectedLectureId } = + const { course,canEdit, isLoading, selectedLectureId, setSelectedLectureId } = useContext(CourseDetailContext); const { Paragraph, Title } = Typography; const firstLectureId = useMemo(() => { return course?.sections?.[0]?.lectures?.[0]?.id; }, [course]); const navigate = useNavigate(); - const { canEdit } = useContext(CourseDetailContext); const { id } = useParams(); return ( //
diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 6154bbe..56831e4 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -19,12 +19,12 @@ export default function CourseDetailLayout() { const [isSyllabusOpen, setIsSyllabusOpen] = useState(true); return (
- + {/* */} {/* 添加 Header 组件 */} {/* 主内容区域 */} {/* 为了防止 Header 覆盖内容,添加上边距 */} -
+
{" "} {/* 添加这个包装 div */} {course?.title}
-
+
{"创建于:"} {dayjs(course?.createdAt).format("YYYY年M月D日")}
- + {"更新于:"} {dayjs(course?.updatedAt).format("YYYY年M月D日")}
@@ -38,19 +45,7 @@ export default function CourseDetailTitle() {
{`学习人数${course?.studentIds?.length || 0}`}
- {canEdit && ( -
{ - const url = course?.id - ? `/course/${course?.id}/editor` - : "/course/editor"; - navigate(url); - }}> - - {"点击编辑课程"} -
- )} +
); diff --git a/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx b/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx b/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx b/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx index 10b756a..84463cf 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -58,7 +58,7 @@ export const CourseSyllabus: React.FC = ({ style={{ width: isOpen ? "25%" : "0", right: 0, - top: isHeaderVisible ? "64px" : "0", + top: isHeaderVisible ? "56px" : "0", }} className="fixed top-0 bottom-0 z-20 bg-white shadow-xl"> {isOpen && ( diff --git a/apps/web/src/components/models/course/detail/JoinLearingButton.tsx b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx new file mode 100644 index 0000000..68c4243 --- /dev/null +++ b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx @@ -0,0 +1,94 @@ +import { useAuth } from "@web/src/providers/auth-provider"; +import { useContext, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { CourseDetailContext } from "./CourseDetailContext"; +import { useStaff } from "@nice/client"; +import { + CheckCircleFilled, + CheckCircleOutlined, + CloseCircleFilled, + CloseCircleOutlined, + EditFilled, + EditTwoTone, + LoginOutlined, +} from "@ant-design/icons"; + +export default function CourseOperationBtns() { + const { id } = useParams(); + const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = + useAuth(); + const navigate = useNavigate(); + const { course, canEdit, userIsLearning } = 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 }, + }, + }, + }); + } else { + await update.mutateAsync({ + where: { id: user?.id }, + data: { + learningPosts: { + disconnect: { + id: course.id, + }, + }, + }, + }); + } + }; + return ( + <> + {isAuthenticated && ( +
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 ? ( + + ) : ( + + ) + ) : ( + + )} + + {userIsLearning + ? isHovered + ? "退出学习" + : "正在学习" + : "加入学习"} + +
+ )} + {canEdit && ( +
{ + const url = course?.id + ? `/course/${course?.id}/editor` + : "/course/editor"; + navigate(url); + }}> + + {"编辑课程"} +
+ )} + + ); +} diff --git a/apps/web/src/components/models/course/list/PostList.tsx b/apps/web/src/components/models/course/list/PostList.tsx index e9e51f4..665e30b 100755 --- a/apps/web/src/components/models/course/list/PostList.tsx +++ b/apps/web/src/components/models/course/list/PostList.tsx @@ -2,7 +2,7 @@ import { Pagination, Empty, Skeleton } from "antd"; import { courseDetailSelect, CourseDto, Prisma } from "@nice/common"; import { api } from "@nice/client"; import { DefaultArgs } from "@prisma/client/runtime/library"; -import { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; interface PostListProps { params?: { page?: number; @@ -12,8 +12,7 @@ interface PostListProps { }; cols?: number; showPagination?: boolean; - renderItem: (post: any) => React.ReactNode - + renderItem: (post: any) => React.ReactNode; } interface PostPagnationProps { data: { @@ -21,13 +20,12 @@ interface PostPagnationProps { totalPages: number; }; isLoading: boolean; - } export default function PostList({ params, cols = 3, showPagination = true, - renderItem + renderItem, }: PostListProps) { const [currentPage, setCurrentPage] = useState(params?.page || 1); const { data, isLoading }: PostPagnationProps = @@ -72,9 +70,9 @@ export default function PostList({ {isLoading ? ( ) : ( - posts.map((post) =>
- {renderItem(post)} -
) + posts.map((post) => ( +
{renderItem(post)}
+ )) )}
{showPagination && ( @@ -91,7 +89,6 @@ export default function PostList({ ) : (
-
)} diff --git a/apps/web/src/components/models/term/term-parent-selector.tsx b/apps/web/src/components/models/term/term-parent-selector.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index f011db6..0899fd2 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -22,6 +22,8 @@ import PathEditorPage from "../app/main/path/editor/page"; import { CoursePreview } from "../app/main/course/preview/page"; import MyLearningPage from "../app/main/my-learning/page"; import MyDutyPage from "../app/main/my-duty/page"; +import MyPathPage from "../app/main/my-path/page"; +import SearchPage from "../app/main/search/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -68,14 +70,22 @@ export const routes: CustomRouteObject[] = [ }, { path: "editor/:id?", - element: - } - ] + element: , + }, + ], }, { path: "courses", element: , }, + { + path: "my-path", + element: ( + + + + ), + }, { path: "my-duty", element: ( @@ -92,6 +102,14 @@ export const routes: CustomRouteObject[] = [ ), }, + { + path: "search", + element: , + }, + { + path: "course/:id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 + element: , + }, ], }, @@ -125,10 +143,6 @@ export const routes: CustomRouteObject[] = [ }, ], }, - { - path: ":id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 - element: , - }, ], }, adminRoute, diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index a12c727..170d0d8 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -42,12 +42,12 @@ export type PostDto = Post & { }; watchableDepts: Department[]; watchableStaffs: Staff[]; - terms: TermDto[] - depts: DepartmentDto[] + terms: TermDto[]; + depts: DepartmentDto[]; meta?: { - thumbnail?: string - views?: number - } + thumbnail?: string; + views?: number; + }; }; export type LectureMeta = { @@ -81,7 +81,7 @@ export type CourseMeta = { likes?: number; hates?: number; }; -export type Course = Post & { +export type Course = PostDto & { meta?: CourseMeta; }; export type CourseDto = Course & { diff --git a/packages/common/src/models/resource.ts b/packages/common/src/models/resource.ts old mode 100644 new mode 100755 diff --git a/scripts/git_stats.py b/scripts/git_stats.py new file mode 100755 index 0000000..374834c --- /dev/null +++ b/scripts/git_stats.py @@ -0,0 +1,503 @@ +import statistics +from git import Repo +from collections import defaultdict, Counter +from datetime import datetime, timedelta +import os + + +def get_contributor_stats(repo_path, start_date=None, end_date=None, branch='HEAD'): + """ + 获取仓库贡献者的详细统计信息 + + Args: + repo_path: Git仓库路径 + start_date: 开始日期(可选) + end_date: 结束日期(可选) + branch: 要分析的分支(默认为HEAD) + + Returns: + 包含每个贡献者详细统计信息的字典 + """ + # 初始化仓库对象 + repo = Repo(repo_path) + + # 存储统计结果 + stats = defaultdict(lambda: { + 'additions': 0, # 添加的行数 + 'deletions': 0, # 删除的行数 + 'commits': 0, # 提交次数 + 'files_modified': set(), # 修改过的文件集合 + 'file_types': defaultdict(int),# 各类型文件的修改次数 + 'commit_dates': set(), # 提交日期集合 + 'commit_hours': defaultdict(int), # 提交小时分布 + 'commit_weekdays': defaultdict(int), # 提交工作日分布 + 'largest_commit': 0, # 最大单次提交修改量 + 'first_commit': None, # 首次提交时间 + 'last_commit': None, # 最近提交时间 + 'commit_sizes': [], # 每次提交的大小,用于计算平均值和中位数 + 'commit_messages': [], # 提交消息列表 + 'commit_message_lengths': [], # 提交消息长度列表 + 'directories_modified': set(), # 修改过的目录集合 + 'co_authors': set(), # 合作者集合 + 'impact_score': 0, # 影响力得分 + 'complexity_score': 0, # 复杂度得分 + 'commit_by_month': defaultdict(int), # 按月份统计的提交次数 + 'commit_by_quarter': defaultdict(int), # 按季度统计的提交次数 + 'commit_by_year': defaultdict(int), # 按年份统计的提交次数 + 'commit_by_week': defaultdict(int), # 按周统计的提交次数 + 'file_operations': { # 文件操作统计 + 'created': set(), # 创建的文件 + 'deleted': set(), # 删除的文件 + 'modified': set(), # 修改的文件 + }, + 'review_comments': 0, # 代码审查评论数(如果可用) + 'merge_commits': 0, # 合并提交数 + 'commit_streak': 0, # 最长连续提交天数 + 'current_streak': 0, # 当前连续提交天数 + 'contribution_days': [], # 所有贡献的日期列表(用于热图) + 'code_churn': 0, # 代码周转率(添加后又删除的代码) + 'file_ownership': {}, # 文件所有权百分比 + 'key_files_modified': set(), # 修改过的关键文件 + 'refactoring_commits': 0, # 重构提交数(基于提交消息分析) + 'bug_fix_commits': 0, # 修复bug的提交数 + 'feature_commits': 0, # 新功能提交数 + 'documentation_commits': 0, # 文档相关提交数 + 'commit_size_distribution': defaultdict(int), # 提交大小分布 + 'collaboration_score': 0, # 协作得分 + 'consistency_score': 0, # 一致性得分 + 'expertise_areas': defaultdict(float), # 专业领域(目录/语言) + }) + + # 存储所有文件的修改者,用于计算协作指标 + file_authors = defaultdict(set) + + # 存储项目文件的重要性权重 (基于修改频率) + file_importance = Counter() + + # 存储用于检测关键词的正则表达式 + import re + refactor_pattern = re.compile(r'refactor|重构', re.IGNORECASE) + bugfix_pattern = re.compile(r'fix|修复|bug|问题|issue|错误', re.IGNORECASE) + feature_pattern = re.compile(r'feature|功能|新增|add|实现', re.IGNORECASE) + docs_pattern = re.compile(r'doc|文档|注释|comment', re.IGNORECASE) + + # 记录每位贡献者的提交日期,用于计算连续贡献天数 + author_commit_days = defaultdict(set) + + # 定义关键文件路径模式 (可以根据项目自定义) + key_file_patterns = [ + re.compile(r'package\.json$'), + re.compile(r'docker-compose\.yml$'), + re.compile(r'Dockerfile$'), + re.compile(r'tsconfig\..*\.json$'), + re.compile(r'/src/index\.[jt]s$'), + re.compile(r'README\.md$'), + re.compile(r'\.env'), + re.compile(r'/main\.[jt]s$'), + re.compile(r'/app\.[jt]s$'), + ] + + # 遍历所有提交 + for commit in repo.iter_commits(branch): + # 过滤日期 + commit_date = datetime.fromtimestamp(commit.committed_date) + if start_date and commit_date < start_date: + continue + if end_date and commit_date > end_date: + continue + + author = commit.author.name + stats[author]['commits'] += 1 + + # 记录提交日期和时间 + commit_day = commit_date.date() + stats[author]['commit_dates'].add(commit_day) + stats[author]['contribution_days'].append(commit_day) # 用于热图 + stats[author]['commit_hours'][commit_date.hour] += 1 + stats[author]['commit_weekdays'][commit_date.weekday()] += 1 + + # 添加到作者的提交日集合 + author_commit_days[author].add(commit_day) + + # 按时间段统计 + year = commit_date.year + month = commit_date.month + quarter = (month - 1) // 3 + 1 + week_num = commit_date.isocalendar()[1] + stats[author]['commit_by_year'][year] += 1 + stats[author]['commit_by_month'][f"{year}-{month:02d}"] += 1 + stats[author]['commit_by_quarter'][f"{year}-Q{quarter}"] += 1 + stats[author]['commit_by_week'][f"{year}-W{week_num:02d}"] += 1 + + # 分析提交消息,对提交进行分类 + commit_message = commit.message.strip() + if refactor_pattern.search(commit_message): + stats[author]['refactoring_commits'] += 1 + if bugfix_pattern.search(commit_message): + stats[author]['bug_fix_commits'] += 1 + if feature_pattern.search(commit_message): + stats[author]['feature_commits'] += 1 + if docs_pattern.search(commit_message): + stats[author]['documentation_commits'] += 1 + + # 记录首次和最近提交 + if stats[author]['first_commit'] is None or commit_date < stats[author]['first_commit']: + stats[author]['first_commit'] = commit_date + if stats[author]['last_commit'] is None or commit_date > stats[author]['last_commit']: + stats[author]['last_commit'] = commit_date + + # 记录提交消息 + commit_message = commit.message.strip() + stats[author]['commit_messages'].append(commit_message) + stats[author]['commit_message_lengths'].append(len(commit_message)) + + # 检测是否为合并提交 + if len(commit.parents) > 1: + stats[author]['merge_commits'] += 1 + + # 统计添加和删除的行数 + total_changes = 0 + modified_files = set() + created_files = set() + deleted_files = set() + directories = set() + + # 尝试获取提交前后的差异,以确定文件操作类型 + try: + if commit.parents: + parent = commit.parents[0] + diffs = parent.diff(commit) + for diff_item in diffs: + if diff_item.new_file: + if diff_item.b_path: + created_files.add(diff_item.b_path) + elif diff_item.deleted_file: + if diff_item.a_path: + deleted_files.add(diff_item.a_path) + else: + if diff_item.a_path: + modified_files.add(diff_item.a_path) + else: + # 对于首次提交,所有文件都是新创建的 + for file_path in commit.stats.files: + created_files.add(file_path) + except Exception as e: + # 如果获取差异失败,退回到简单的文件修改统计 + modified_files = set(commit.stats.files.keys()) + + for file_path, item in commit.stats.files.items(): + # 统计文件类型 + _, ext = os.path.splitext(file_path) + if ext: # 确保扩展名不为空 + stats[author]['file_types'][ext] += 1 + else: + stats[author]['file_types']['no_extension'] += 1 + + # 记录目录 + directory = os.path.dirname(file_path) + if directory: + directories.add(directory) + + # 记录修改的文件 + modified_files.add(file_path) + + # 记录文件的修改者,用于计算协作指标 + file_authors[file_path].add(author) + + # 统计添加和删除的行数 + stats[author]['additions'] += item['insertions'] + stats[author]['deletions'] += item['deletions'] + total_changes += item['insertions'] + item['deletions'] + + # 更新修改过的文件和目录集合 + stats[author]['files_modified'].update(modified_files) + stats[author]['directories_modified'].update(directories) + stats[author]['file_operations']['created'].update(created_files) + stats[author]['file_operations']['deleted'].update(deleted_files) + stats[author]['file_operations']['modified'].update(modified_files - created_files - deleted_files) + + # 记录本次提交的修改量 + stats[author]['commit_sizes'].append(total_changes) + + # 记录提交大小分布 + commit_size_category = "小型(1-10行)" if total_changes <= 10 else \ + "中型(11-100行)" if total_changes <= 100 else \ + "大型(101-500行)" if total_changes <= 500 else \ + "超大型(500+行)" + stats[author]['commit_size_distribution'][commit_size_category] += 1 + + # 更新最大单次提交修改量 + if total_changes > stats[author]['largest_commit']: + stats[author]['largest_commit'] = total_changes + + # 检查修改的文件是否为关键文件 + for file_path in modified_files: + for pattern in key_file_patterns: + if pattern.search(file_path): + stats[author]['key_files_modified'].add(file_path) + break + + # 更新文件重要性权重 + for file_path in modified_files: + file_importance[file_path] += 1 + + # 计算影响力得分 (基于修改的文件数和总修改行数) + impact = total_changes * len(modified_files) / 100 if modified_files else 0 + stats[author]['impact_score'] += impact + + # 计算文件协作度和文件所有权 + for file_path, authors in file_authors.items(): + # 如果只有一个作者修改了文件,则该作者100%拥有此文件 + if len(authors) == 1: + author = next(iter(authors)) + if 'file_ownership' not in stats[author]: + stats[author]['file_ownership'] = {} + stats[author]['file_ownership'][file_path] = 100.0 + else: + # 如果多个作者修改了文件,则按照每个作者的修改比例计算所有权 + for author in authors: + # 简化处理:平均分配所有权 + ownership_percent = 100.0 / len(authors) + if 'file_ownership' not in stats[author]: + stats[author]['file_ownership'] = {} + stats[author]['file_ownership'][file_path] = ownership_percent + + # 计算每个作者的连续提交天数 + for author, commit_days in author_commit_days.items(): + if not commit_days: + continue + + # 按日期排序 + sorted_days = sorted(commit_days) + + # 计算最长提交连续天数 + current_streak = 1 + max_streak = 1 + + for i in range(1, len(sorted_days)): + # 如果当前日期与前一天相差正好一天,则增加连续计数 + if (sorted_days[i] - sorted_days[i-1]).days == 1: + current_streak += 1 + else: + # 重置当前连续计数 + current_streak = 1 + + max_streak = max(max_streak, current_streak) + + # 记录最长连续提交天数 + stats[author]['commit_streak'] = max_streak + + # 计算当前连续提交天数 (到最后一个日期) + if sorted_days: + today = datetime.now().date() + days_since_last = (today - sorted_days[-1]).days + + if days_since_last <= 1: # 如果最后提交是今天或昨天 + current_streak = 1 + for i in range(len(sorted_days) - 1, 0, -1): + if (sorted_days[i] - sorted_days[i-1]).days == 1: + current_streak += 1 + else: + break + stats[author]['current_streak'] = current_streak + + # 后处理:计算派生指标并转换集合为计数 + for author, data in stats.items(): + # 将文件集合转换为数量 + data['files_count'] = len(data['files_modified']) + data['active_days'] = len(data['commit_dates']) + data['key_files_count'] = len(data['key_files_modified']) + + # 计算平均每次提交的修改量 + if data['commits'] > 0: + data['avg_commit_size'] = sum(data['commit_sizes']) / data['commits'] + data['median_commit_size'] = statistics.median(data['commit_sizes']) if data['commit_sizes'] else 0 + + # 计算代码复杂度得分 (基于修改量、文件数和一致性) + variability = statistics.stdev(data['commit_sizes']) if len(data['commit_sizes']) > 1 else 0 + data['complexity_score'] = (data['avg_commit_size'] * data['files_count'] * (1 + variability / 1000)) / 100 + + # 计算一致性得分 (提交大小和频率的一致性) + if variability > 0: + data['consistency_score'] = 100 * (1 - min(1, variability / data['avg_commit_size'])) + else: + data['consistency_score'] = 100 + else: + data['avg_commit_size'] = 0 + data['median_commit_size'] = 0 + data['complexity_score'] = 0 + data['consistency_score'] = 0 + + # 计算总修改量 + data['total_changes'] = data['additions'] + data['deletions'] + + # 计算代码周转率 (code churn) - 估算值 + if data['additions'] > 0 and data['deletions'] > 0: + data['code_churn'] = min(data['additions'], data['deletions']) / max(data['additions'], data['deletions']) * 100 + + # 计算活跃时长(天) + if data['first_commit'] and data['last_commit']: + delta = data['last_commit'] - data['first_commit'] + data['active_period_days'] = delta.days + 1 + + # 计算活跃密度 (提交数/活跃天数) + if delta.days > 0: + data['activity_density'] = data['commits'] / delta.days + else: + data['activity_density'] = data['commits'] + else: + data['active_period_days'] = 0 + data['activity_density'] = 0 + + # 计算协作得分 (基于参与修改的共享文件比例) + total_files = len(data['files_modified']) + shared_files = sum(1 for f in data['files_modified'] if len(file_authors[f]) > 1) + if total_files > 0: + data['collaboration_score'] = (shared_files / total_files) * 100 + + # 计算专业领域 (基于文件类型和目录) + if data['file_types']: + primary_type = max(data['file_types'].items(), key=lambda x: x[1])[0] + data['primary_file_type'] = primary_type + data['primary_file_type_percent'] = (data['file_types'][primary_type] / sum(data['file_types'].values())) * 100 + + # 统计目录专业度 + if data['directories_modified']: + dir_counts = Counter() + for directory in data['directories_modified']: + dir_counts[directory] += 1 + + # 检查父目录 + parent = os.path.dirname(directory) + while parent: + dir_counts[parent] += 0.5 # 对父目录给予较低的权重 + parent = os.path.dirname(parent) + + # 找出专业领域(最常修改的目录) + if dir_counts: + primary_dir = max(dir_counts.items(), key=lambda x: x[1])[0] + data['primary_directory'] = primary_dir + data['expertise_areas'][primary_dir] = dir_counts[primary_dir] / sum(dir_counts.values()) + + return stats + +def print_stats(stats): + """打印贡献者统计信息的详细报告""" + # 基本信息表头 + print("\n===== 贡献者基本统计 =====") + print("{:<20} {:<10} {:<10} {:<10} {:<10} {:<15} {:<15}".format( + "作者", "提交数", "添加行数", "删除行数", "总修改行数", "修改文件数", "活跃天数")) + print("-" * 90) + + # 按总修改量排序 + for author, data in sorted(stats.items(), key=lambda x: x[1]['total_changes'], reverse=True): + print("{:<20} {:<10} {:<10} {:<10} {:<10} {:<15} {:<15}".format( + author, + data['commits'], + data['additions'], + data['deletions'], + data['total_changes'], + data['files_count'], + data['active_days'] + )) + + # 为每个贡献者打印详细信息 + for author, data in sorted(stats.items(), key=lambda x: x[1]['total_changes'], reverse=True): + print(f"\n\n===== {author} 的详细贡献统计 =====") + + # 活跃时间信息 + if data['first_commit'] and data['last_commit']: + print(f"首次提交时间: {data['first_commit'].strftime('%Y-%m-%d %H:%M:%S')}") + print(f"最近提交时间: {data['last_commit'].strftime('%Y-%m-%d %H:%M:%S')}") + print(f"活跃时长: {data['active_period_days']} 天") + + # 提交规模信息 + print(f"平均每次提交修改: {data['avg_commit_size']:.2f} 行") + print(f"最大单次提交修改: {data['largest_commit']} 行") + + # 文件类型分布 + if data['file_types']: + print("\n文件类型分布:") + for ext, count in sorted(data['file_types'].items(), key=lambda x: x[1], reverse=True): + print(f" {ext}: {count} 次修改") + + # 提交时间分布 + if data['commit_hours']: + print("\n提交时间分布:") + for hour in range(24): + count = data['commit_hours'].get(hour, 0) + if count > 0: + print(f" {hour:02d}:00-{hour+1:02d}:00: {count} 次提交") + + # 工作日分布 + if data['commit_weekdays']: + weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] + print("\n工作日分布:") + for day in range(7): + count = data['commit_weekdays'].get(day, 0) + if count > 0: + print(f" {weekday_names[day]}: {count} 次提交") + +def get_team_summary(stats): + """生成团队整体统计摘要""" + summary = { + 'total_commits': 0, + 'total_additions': 0, + 'total_deletions': 0, + 'total_files': set(), + 'contributors': len(stats), + 'first_commit': None, + 'last_commit': None, + } + + for author, data in stats.items(): + summary['total_commits'] += data['commits'] + summary['total_additions'] += data['additions'] + summary['total_deletions'] += data['deletions'] + summary['total_files'].update(data['files_modified']) + + # 更新首次和最近提交 + if data['first_commit']: + if summary['first_commit'] is None or data['first_commit'] < summary['first_commit']: + summary['first_commit'] = data['first_commit'] + + if data['last_commit']: + if summary['last_commit'] is None or data['last_commit'] > summary['last_commit']: + summary['last_commit'] = data['last_commit'] + + return summary + +def print_team_summary(summary): + """打印团队整体统计摘要""" + print("\n===== 团队整体统计 =====") + print(f"贡献者数量: {summary['contributors']}") + print(f"总提交次数: {summary['total_commits']}") + print(f"总添加行数: {summary['total_additions']}") + print(f"总删除行数: {summary['total_deletions']}") + print(f"总修改行数: {summary['total_additions'] + summary['total_deletions']}") + print(f"修改的文件数: {len(summary['total_files'])}") + + if summary['first_commit'] and summary['last_commit']: + print(f"项目起始时间: {summary['first_commit'].strftime('%Y-%m-%d')}") + print(f"最近活动时间: {summary['last_commit'].strftime('%Y-%m-%d')}") + delta = summary['last_commit'] - summary['first_commit'] + print(f"项目活跃时长: {delta.days + 1} 天") +if __name__ == "__main__": + # 设置仓库路径(当前目录) + repo_path = '.' + + # 设置日期范围(示例) + # 注意:这里使用的是2025年的日期,可能需要根据实际情况调整 + start_date = datetime(2025, 1, 1) # 修改为更合理的日期范围 + end_date = datetime(2025, 12, 31) + + print(f"分析Git仓库: {os.path.abspath(repo_path)}") + print(f"时间范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}") + + # 获取统计信息 + stats = get_contributor_stats(repo_path, start_date, end_date) + + # 打印团队摘要 + team_summary = get_team_summary(stats) + print_team_summary(team_summary) + print(stats) \ No newline at end of file diff --git a/web-dist/index.html b/web-dist/index.html old mode 100644 new mode 100755