lin
This commit is contained in:
parent
7fd2173dd2
commit
29340b8bb2
|
@ -1,7 +0,0 @@
|
||||||
import CourseDetail from "@web/src/components/models/course/detail/CourseDetail";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
export function CourseDetailPage() {
|
|
||||||
const { id, lectureId } = useParams();
|
|
||||||
return <CourseDetail id={id} lectureId={lectureId}></CourseDetail>;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Checkbox, List } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export function CoursePreviewTabmsg({data}){
|
|
||||||
|
|
||||||
|
|
||||||
const renderItem = (item) => (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={item.title}
|
|
||||||
description={item.description}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
return(
|
|
||||||
<div className='my-2'>
|
|
||||||
<List
|
|
||||||
dataSource={data}
|
|
||||||
split={false}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import { Menu } from 'antd';
|
|
||||||
|
|
||||||
type MenuItem = Required<MenuProps>['items'][number];
|
|
||||||
|
|
||||||
export function CourseCatalog(){
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import { CoursePreviewMsg } from "@web/src/app/main/course/preview/type.ts";
|
|
||||||
import { Button, Tabs, Image, Skeleton } from "antd";
|
|
||||||
import type { TabsProps } from "antd";
|
|
||||||
import { PlayCircleOutlined } from "@ant-design/icons";
|
|
||||||
export function CoursePreviewAllmsg({
|
|
||||||
previewMsg,
|
|
||||||
items,
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
previewMsg?: CoursePreviewMsg;
|
|
||||||
items: TabsProps["items"];
|
|
||||||
isLoading: boolean;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(previewMsg);
|
|
||||||
});
|
|
||||||
const TapOnChange = (key: string) => {
|
|
||||||
console.log(key);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen max-w-7xl mx-auto px-6 lg:px-8">
|
|
||||||
<div className="overflow-auto flex justify-around align-items-center w-full mx-auto my-8">
|
|
||||||
<div className="relative w-[600px] h-[340px] m-4 overflow-hidden flex justify-center items-center">
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
previewMsg.isLoading
|
|
||||||
? "error"
|
|
||||||
: previewMsg.videoPreview
|
|
||||||
}
|
|
||||||
alt="example"
|
|
||||||
preview={false}
|
|
||||||
className="w-full h-full object-cover z-0"
|
|
||||||
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="
|
|
||||||
/>
|
|
||||||
<div className="w-[600px] h-[360px] absolute top-0 z-10 bg-black opacity-30 transition-opacity duration-300 ease-in-out hover:opacity-70 cursor-pointer">
|
|
||||||
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-between w-2/5 content-start h-[340px] my-4 overflow-hidden">
|
|
||||||
{isLoading ? (
|
|
||||||
<Skeleton className="my-5" active />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="text-3xl font-bold my-3 ">
|
|
||||||
{previewMsg.Title}
|
|
||||||
</span>
|
|
||||||
<span className="text-xl font-semibold my-3 text-gray-700">
|
|
||||||
{previewMsg.SubTitle}
|
|
||||||
</span>
|
|
||||||
<span className="text-lg font-light my-3 text-gray-500 text-clip">
|
|
||||||
{previewMsg.Description}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button block type="primary" size="large">
|
|
||||||
{" "}
|
|
||||||
查看课程{" "}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto w-11/12 mx-auto my-8">
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="1"
|
|
||||||
tabBarGutter={100}
|
|
||||||
items={items}
|
|
||||||
onChange={TapOnChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { Skeleton, type TabsProps } from 'antd';
|
|
||||||
import { CoursePreviewAllmsg } from "./components/coursePreviewAllmsg";
|
|
||||||
import { CoursePreviewTabmsg } from "./components/couresPreviewTabmsg";
|
|
||||||
import { CoursePreviewMsg } from "./type";
|
|
||||||
import { api } from '@nice/client'
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { courseDetailSelect, CourseDto } from '@nice/common';
|
|
||||||
|
|
||||||
export function CoursePreview(){
|
|
||||||
const { id } = useParams()
|
|
||||||
const { data:course,isLoading:courseIsLoading}:{data:CourseDto,isLoading:boolean}= api.post.findFirst.useQuery({
|
|
||||||
where:{
|
|
||||||
id
|
|
||||||
},
|
|
||||||
select:courseDetailSelect
|
|
||||||
})
|
|
||||||
// course.sections[0].lectures[0]
|
|
||||||
// `/course/${course.id}/detail/${Lecture.id}`
|
|
||||||
useEffect(() => {
|
|
||||||
if(!courseIsLoading){
|
|
||||||
setPreviewMsg({
|
|
||||||
videoPreview: course?.meta?.thumbnail,
|
|
||||||
Title: course?.title,
|
|
||||||
SubTitle:course?.subTitle,
|
|
||||||
Description:course?.content,
|
|
||||||
ToCourseUrl:`/course/${id}`,
|
|
||||||
isLoading:courseIsLoading
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
},[courseIsLoading])
|
|
||||||
const [previewMsg,setPreviewMsg] = useState({
|
|
||||||
videoPreview: '',
|
|
||||||
Title: '',
|
|
||||||
SubTitle:'',
|
|
||||||
Description:'',
|
|
||||||
ToCourseUrl:'',
|
|
||||||
isLoading:courseIsLoading
|
|
||||||
})
|
|
||||||
const tapData = [
|
|
||||||
{
|
|
||||||
title: '掌握R语言的基本概念语法',
|
|
||||||
description: '学生将学习R语言和RStudio的基本知识',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '掌握R语言的基本概念语法',
|
|
||||||
description: '学生将学习R语言的变量、数据类型、循环和条件语句等',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '掌握R语言的数据导入管理',
|
|
||||||
description: '学生将学会如何将数据导入R环境,并且使用各种类型的数据',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '掌握R语言的基本数据清洗',
|
|
||||||
description: '学生将学会使用R语言进行数据清洗、整理和管理',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '掌握R语言的基本数据统计',
|
|
||||||
description: '学生将学会使用R语言进行基本的数据统计',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '掌握R语言的基本绘图功能',
|
|
||||||
description: '学生将学会使用R语言基本的绘图功能和ggplot2的应用',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const isLoading = false
|
|
||||||
const items: TabsProps['items'] = [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
label: '课程学习目标',
|
|
||||||
children: isLoading ? <Skeleton className='my-5' active />: <CoursePreviewTabmsg data={tapData}/>,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return(
|
|
||||||
<div className="min-h-screen">
|
|
||||||
<CoursePreviewAllmsg previewMsg= {previewMsg} isLoading={courseIsLoading } items = {items}></CoursePreviewAllmsg>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export interface CoursePreviewMsg {
|
|
||||||
videoPreview: string;
|
|
||||||
Title: string;
|
|
||||||
SubTitle: string;
|
|
||||||
Description: string;
|
|
||||||
ToCourseUrl: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import { PostType, Prisma } from "@nice/common";
|
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
|
||||||
|
|
||||||
export function CoursesContainer() {
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CoursesContainer;
|
|
|
@ -1,18 +0,0 @@
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<BasePostLayout>
|
|
||||||
<CoursesContainer></CoursesContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
|
||||||
import { Typography, Skeleton } from "antd";
|
|
||||||
import { stringToColor, TaxonomySlug, TermDto } from "@nice/common";
|
|
||||||
import { api, useTrainSituation } from "@nice/client";
|
|
||||||
import LookForMore from "./LookForMore";
|
|
||||||
import CategorySectionCard from "./CategorySectionCard";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
const CategorySection = () => {
|
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
||||||
const { selectedTerms, setSelectedTerms } = useMainContext();
|
|
||||||
const {
|
|
||||||
data: courseCategoriesData,
|
|
||||||
isLoading,
|
|
||||||
}: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({
|
|
||||||
where: {
|
|
||||||
taxonomy: {
|
|
||||||
slug: TaxonomySlug.CATEGORY,
|
|
||||||
},
|
|
||||||
parentId: null,
|
|
||||||
},
|
|
||||||
take: 8,
|
|
||||||
});
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleMouseEnter = useCallback((index: number) => {
|
|
||||||
setHoveredIndex(index);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
|
||||||
setHoveredIndex(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseClick = useCallback((categoryId: string) => {
|
|
||||||
setSelectedTerms({
|
|
||||||
...selectedTerms,
|
|
||||||
[TaxonomySlug.CATEGORY]: [categoryId],
|
|
||||||
});
|
|
||||||
navigate("/courses");
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const {user} = useAuth()
|
|
||||||
const {create} = useTrainSituation()
|
|
||||||
useEffect(() => {
|
|
||||||
if (user?.id) {
|
|
||||||
create.mutate({
|
|
||||||
data: {
|
|
||||||
//staffId: user.id, // 确保类型匹配
|
|
||||||
mustTrainTime: "1",
|
|
||||||
alreadyTrainTime: "1",
|
|
||||||
//trainContentId: "cm83w52dr00ff3jc8ep4uf4a4",
|
|
||||||
staff: { connect: { id: user.id } },
|
|
||||||
trainContent: { connect: { id: "cm847lcc805ne123v81l5h13e" } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("create score time")
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="py-8 relative overflow-hidden">
|
|
||||||
<div className="max-w-screen-2xl mx-auto px-4 relative">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<Title
|
|
||||||
level={2}
|
|
||||||
className="font-bold text-5xl mb-6 bg-gradient-to-r from-gray-900 via-gray-700 to-gray-800 bg-clip-text text-transparent motion-safe:animate-gradient-x">
|
|
||||||
探索课程分类
|
|
||||||
</Title>
|
|
||||||
<Text type="secondary" className="text-xl font-light">
|
|
||||||
选择你感兴趣的方向,开启学习之旅
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{isLoading ? (
|
|
||||||
<Skeleton paragraph={{ rows: 4 }}></Skeleton>
|
|
||||||
) : (
|
|
||||||
courseCategoriesData?.filter(c=>!c.deletedAt)?.map((category, index) => {
|
|
||||||
const categoryColor = stringToColor(category.name);
|
|
||||||
const isHovered = hoveredIndex === index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CategorySectionCard
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
category={category}
|
|
||||||
categoryColor={categoryColor}
|
|
||||||
isHovered={isHovered}
|
|
||||||
handleMouseEnter={handleMouseEnter}
|
|
||||||
handleMouseLeave={handleMouseLeave}
|
|
||||||
handleMouseClick={handleMouseClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<LookForMore to={"/courses"}></LookForMore>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CategorySection;
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { Typography } from "antd";
|
|
||||||
export default function CategorySectionCard({handleMouseClick, index,handleMouseEnter,handleMouseLeave,category,categoryColor,isHovered,}) {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="group relative min-h-[130px] rounded-2xl transition-all duration-700 ease-out cursor-pointer will-change-transform hover:-translate-y-2"
|
|
||||||
onMouseEnter={() => handleMouseEnter(index)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`查看${category.name}课程类别`}
|
|
||||||
onClick={()=>{
|
|
||||||
handleMouseClick(category.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-gray-200 to-gray-300 opacity-50 rounded-2xl transition-all duration-700 group-hover:opacity-75" />
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 rounded-2xl bg-gradient-to-br from-white to-gray-50 shadow-lg transition-all duration-700 ease-out ${isHovered
|
|
||||||
? "scale-[1.02] bg-opacity-95"
|
|
||||||
: "scale-100 bg-opacity-90"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 rounded-2xl transition-all duration-700 ease-out ${isHovered
|
|
||||||
? "shadow-[0_8px_30px_rgb(0,0,0,0.12)]"
|
|
||||||
: "shadow-none opacity-0"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`absolute w-1/2 top-0 left-1/2 -translate-x-1/2 h-1 rounded-full transition-all duration-500 ease-out `}
|
|
||||||
style={{
|
|
||||||
backgroundColor: categoryColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="relative w-full h-full p-6">
|
|
||||||
<div className="flex w-2/3 absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 flex-col space-y-4 mb-4">
|
|
||||||
<Text
|
|
||||||
strong
|
|
||||||
className="text-xl font-medium tracking-wide">
|
|
||||||
{category.name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={` mt-6 absolute bottom-4 right-6 text-sm font-medium flex items-center space-x-2 transition-all duration-500 ease-out `}
|
|
||||||
style={{ color: categoryColor }}>
|
|
||||||
<span>了解更多</span>
|
|
||||||
<span
|
|
||||||
className={`transform transition-all duration-500 ease-out `}>
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import React, { useState, useMemo, ReactNode } from "react";
|
|
||||||
import { Typography, Skeleton } from "antd";
|
|
||||||
import { TaxonomySlug, TermDto } from "@nice/common";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
import { CoursesSectionTag } from "./CoursesSectionTag";
|
|
||||||
import LookForMore from "./LookForMore";
|
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
interface GetTaxonomyProps {
|
|
||||||
categories: string[];
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
function useGetTaxonomy({ type }): GetTaxonomyProps {
|
|
||||||
const { data, isLoading }: { data: TermDto[]; isLoading: boolean } =
|
|
||||||
api.term.findMany.useQuery({
|
|
||||||
where: {
|
|
||||||
taxonomy: {
|
|
||||||
slug: type,
|
|
||||||
},
|
|
||||||
parentId: null,
|
|
||||||
},
|
|
||||||
take: 11, // 只取前10个
|
|
||||||
});
|
|
||||||
const categories = useMemo(() => {
|
|
||||||
const allCategories = isLoading
|
|
||||||
? []
|
|
||||||
: data?.filter(c=>!c.deletedAt)?.map((course) => course.name);
|
|
||||||
return [...Array.from(new Set(allCategories))];
|
|
||||||
}, [data]);
|
|
||||||
return { categories, isLoading };
|
|
||||||
}
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
interface CoursesSectionProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
initialVisibleCoursesCount?: number;
|
|
||||||
postType:string;
|
|
||||||
render?:(post)=>ReactNode;
|
|
||||||
to:string
|
|
||||||
}
|
|
||||||
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
initialVisibleCoursesCount = 8,
|
|
||||||
postType,
|
|
||||||
render,
|
|
||||||
to
|
|
||||||
}) => {
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>("全部");
|
|
||||||
const gateGory: GetTaxonomyProps = useGetTaxonomy({
|
|
||||||
type: TaxonomySlug.CATEGORY,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<section className="relative py-16 overflow-hidden ">
|
|
||||||
<div className="max-w-screen-2xl mx-auto px-4 relative">
|
|
||||||
<div className="flex justify-between items-end mb-12 ">
|
|
||||||
<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}
|
|
||||||
</Title>
|
|
||||||
<Text
|
|
||||||
type="secondary"
|
|
||||||
className="text-xl font-light text-gray-600">
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-12 flex flex-wrap gap-4">
|
|
||||||
{gateGory.isLoading ? (
|
|
||||||
<Skeleton paragraph={{ rows: 2 }}></Skeleton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{["全部", ...gateGory.categories].map(
|
|
||||||
(category, idx) => (
|
|
||||||
<CoursesSectionTag
|
|
||||||
key={idx}
|
|
||||||
category={category}
|
|
||||||
selectedCategory={selectedCategory}
|
|
||||||
setSelectedCategory={
|
|
||||||
setSelectedCategory
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => render(post)}
|
|
||||||
params={{
|
|
||||||
page: 1,
|
|
||||||
pageSize: initialVisibleCoursesCount,
|
|
||||||
where: {
|
|
||||||
terms: !(selectedCategory === "全部")
|
|
||||||
? {
|
|
||||||
some: {
|
|
||||||
name: selectedCategory,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
type: postType
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
showPagination={false}
|
|
||||||
cols={4}></PostList>
|
|
||||||
<LookForMore to={to}></LookForMore>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default CoursesSection;
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Tag } from "antd";
|
|
||||||
|
|
||||||
export function CoursesSectionTag({category, selectedCategory, setSelectedCategory}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tag
|
|
||||||
key={category}
|
|
||||||
color={
|
|
||||||
selectedCategory === category
|
|
||||||
? "blue"
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCategory(category);
|
|
||||||
}}
|
|
||||||
className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${selectedCategory === category
|
|
||||||
? "bg-blue-600 text-white shadow-lg"
|
|
||||||
: "bg-white text-gray-600 hover:bg-gray-100"
|
|
||||||
}`}>
|
|
||||||
{category}
|
|
||||||
</Tag>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import { Typography, Tag, Carousel } from 'antd';
|
|
||||||
import { StarFilled, UserOutlined, ReadOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
|
|
||||||
interface Teacher {
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
avatar: string;
|
|
||||||
courses: number;
|
|
||||||
students: number;
|
|
||||||
rating: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const featuredTeachers: Teacher[] = [
|
|
||||||
{
|
|
||||||
name: '张教授',
|
|
||||||
title: '资深前端开发专家',
|
|
||||||
avatar: '/images/teacher1.jpg',
|
|
||||||
courses: 12,
|
|
||||||
students: 25000,
|
|
||||||
rating: 4.9,
|
|
||||||
description: '前 BAT 高级工程师,10年+开发经验'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '李教授',
|
|
||||||
title: '算法与数据结构专家',
|
|
||||||
avatar: '/images/teacher2.jpg',
|
|
||||||
courses: 8,
|
|
||||||
students: 18000,
|
|
||||||
rating: 4.8,
|
|
||||||
description: '计算机博士,专注算法教育8年'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '王博士',
|
|
||||||
title: '人工智能研究员',
|
|
||||||
avatar: '/images/teacher3.jpg',
|
|
||||||
courses: 15,
|
|
||||||
students: 30000,
|
|
||||||
rating: 4.95,
|
|
||||||
description: '人工智能领域专家,曾主导多个大型AI项目'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '陈教授',
|
|
||||||
title: '云计算架构师',
|
|
||||||
avatar: '/images/teacher4.jpg',
|
|
||||||
courses: 10,
|
|
||||||
students: 22000,
|
|
||||||
rating: 4.85,
|
|
||||||
description: '知名云服务提供商技术总监,丰富的实战经验'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '郑老师',
|
|
||||||
title: '移动开发专家',
|
|
||||||
avatar: '/images/teacher5.jpg',
|
|
||||||
courses: 14,
|
|
||||||
students: 28000,
|
|
||||||
rating: 4.88,
|
|
||||||
description: '资深移动端开发者,著名互联网公司技术专家'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const generateGradientColors = (name: string) => {
|
|
||||||
// 优化的哈希函数
|
|
||||||
const hash = name.split('').reduce((acc, char, index) => {
|
|
||||||
return char.charCodeAt(0) + ((acc << 5) - acc) + index;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// 定义蓝色色相范围(210-240)
|
|
||||||
const blueHueStart = 210;
|
|
||||||
const blueHueRange = 30;
|
|
||||||
|
|
||||||
// 基础蓝色色相 - 将哈希值映射到蓝色范围内
|
|
||||||
const baseHue = blueHueStart + Math.abs(hash % blueHueRange);
|
|
||||||
|
|
||||||
// 生成第二个蓝色色相,保持在蓝色范围内
|
|
||||||
let secondHue = baseHue + 15; // 在基础色相的基础上略微偏移
|
|
||||||
if (secondHue > blueHueStart + blueHueRange) {
|
|
||||||
secondHue -= blueHueRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基于输入字符串的特征调整饱和度和亮度
|
|
||||||
const nameLength = name.length;
|
|
||||||
const saturation = Math.max(65, Math.min(85, 75 + (nameLength % 10))); // 65-85%范围
|
|
||||||
const lightness = Math.max(45, Math.min(65, 55 + (hash % 10))); // 45-65%范围
|
|
||||||
|
|
||||||
// 为第二个颜色稍微调整饱和度和亮度,创造层次感
|
|
||||||
const saturation2 = Math.max(60, saturation - 5);
|
|
||||||
const lightness2 = Math.min(70, lightness + 5);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: `hsl(${Math.round(baseHue)}, ${Math.round(saturation)}%, ${Math.round(lightness)}%)`,
|
|
||||||
to: `hsl(${Math.round(secondHue)}, ${Math.round(saturation2)}%, ${Math.round(lightness2)}%)`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeaturedTeachersSection: React.FC = () => {
|
|
||||||
const carouselRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
dots: true,
|
|
||||||
infinite: true,
|
|
||||||
speed: 500,
|
|
||||||
slidesToShow: 4,
|
|
||||||
slidesToScroll: 1,
|
|
||||||
autoplay: true,
|
|
||||||
autoplaySpeed: 5000,
|
|
||||||
responsive: [
|
|
||||||
{
|
|
||||||
breakpoint: 1024,
|
|
||||||
settings: {
|
|
||||||
slidesToShow: 2,
|
|
||||||
slidesToScroll: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
breakpoint: 640,
|
|
||||||
settings: {
|
|
||||||
slidesToShow: 1,
|
|
||||||
slidesToScroll: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const TeacherCard = ({ teacher }: { teacher: Teacher }) => {
|
|
||||||
const gradientColors = generateGradientColors(teacher.name);
|
|
||||||
return (
|
|
||||||
<div className="p-8">
|
|
||||||
<div className="bg-white rounded-2xl shadow-[0_4px_20px_-2px_rgba(0,0,0,0.1)] overflow-hidden transform transition-all duration-300 hover:shadow-[0_8px_30px_-4px_rgba(0,0,0,0.15)] hover:-translate-y-1">
|
|
||||||
<div className="relative h-48" style={{
|
|
||||||
background: `linear-gradient(to right, ${gradientColors.from}, ${gradientColors.to})`
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src={teacher.avatar}
|
|
||||||
alt={teacher.name}
|
|
||||||
className="absolute left-1/2 bottom-0 transform -translate-x-1/2 translate-y-1/2 w-24 h-24 rounded-full border-4 border-white object-cover shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="pt-16 p-6 min-h-[280px] flex flex-col">
|
|
||||||
<div className="text-center mb-4">
|
|
||||||
<Title level={4} className="mb-1">
|
|
||||||
{teacher.name}
|
|
||||||
</Title>
|
|
||||||
<Tag color="blue" className="text-sm">
|
|
||||||
{teacher.title}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text type="secondary" className="block text-center mb-6 line-clamp-2 min-h-[3rem]">
|
|
||||||
{teacher.description}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4 border-t pt-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center gap-1 text-blue-600">
|
|
||||||
<ReadOutlined className="text-lg" />
|
|
||||||
<span className="font-bold">{teacher.courses}</span>
|
|
||||||
</div>
|
|
||||||
<Text type="secondary" className="text-sm">课程</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-center border-x">
|
|
||||||
<div className="flex items-center justify-center gap-1 text-blue-600">
|
|
||||||
<UserOutlined className="text-lg" />
|
|
||||||
<span className="font-bold">{(teacher.students / 1000).toFixed(1)}k</span>
|
|
||||||
</div>
|
|
||||||
<Text type="secondary" className="text-sm">学员</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-center justify-center gap-1 text-blue-600">
|
|
||||||
<StarFilled className="text-lg" />
|
|
||||||
<span className="font-bold">{teacher.rating}</span>
|
|
||||||
</div>
|
|
||||||
<Text type="secondary" className="text-sm">评分</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="py-20 px-4 bg-gradient-to-b from-gray-50/50 to-transparent">
|
|
||||||
<div className="max-w-screen-2xl mx-auto">
|
|
||||||
<div className="relative z-10 text-center mb-16">
|
|
||||||
<Title level={2} className="font-bold text-4xl mb-4">
|
|
||||||
优秀讲师
|
|
||||||
</Title>
|
|
||||||
<Text type="secondary" className="text-lg">
|
|
||||||
业界专家实战分享,传授独家经验
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative group">
|
|
||||||
<Carousel ref={carouselRef} {...settings}>
|
|
||||||
{featuredTeachers.map((teacher, index) => (
|
|
||||||
<TeacherCard key={index} teacher={teacher} />
|
|
||||||
))}
|
|
||||||
</Carousel>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => carouselRef.current?.prev()}
|
|
||||||
className="absolute left-0 top-1/2 -translate-y-1/2 -ml-4 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-colors opacity-0 group-hover:opacity-100"
|
|
||||||
>
|
|
||||||
<LeftOutlined />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => carouselRef.current?.next()}
|
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 -mr-4 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-colors opacity-0 group-hover:opacity-100"
|
|
||||||
>
|
|
||||||
<RightOutlined />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeaturedTeachersSection;
|
|
|
@ -1,164 +0,0 @@
|
||||||
import React, {
|
|
||||||
useRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Carousel, Typography } from "antd";
|
|
||||||
import {
|
|
||||||
TeamOutlined,
|
|
||||||
BookOutlined,
|
|
||||||
StarOutlined,
|
|
||||||
LeftOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import type { CarouselRef } from "antd/es/carousel";
|
|
||||||
import { api, useAppConfig } from "@nice/client";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
interface PlatformStat {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeroSection = () => {
|
|
||||||
const carouselRef = useRef<CarouselRef>(null);
|
|
||||||
const { statistics, slides, slideLinks = [] } = useAppConfig();
|
|
||||||
const [countStatistics, setCountStatistics] = useState<number>(4);
|
|
||||||
const navigator = useNavigate()
|
|
||||||
const platformStats: PlatformStat[] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
icon: <TeamOutlined />,
|
|
||||||
value: statistics.staffs,
|
|
||||||
label: "注册学员",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <StarOutlined />,
|
|
||||||
value: statistics.courses,
|
|
||||||
label: "精品课程",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <BookOutlined />,
|
|
||||||
value: statistics.lectures,
|
|
||||||
label: "课程章节",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <EyeOutlined />,
|
|
||||||
value: statistics.reads,
|
|
||||||
label: "播放次数",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [statistics]);
|
|
||||||
const handlePrev = useCallback(() => {
|
|
||||||
carouselRef.current?.prev();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
|
||||||
carouselRef.current?.next();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const countNonZeroValues = (statistics: Record<string, number>): number => {
|
|
||||||
return Object.values(statistics).filter((value) => value !== 0).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const count = countNonZeroValues(statistics);
|
|
||||||
console.log(count);
|
|
||||||
setCountStatistics(count);
|
|
||||||
}, [statistics]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="relative ">
|
|
||||||
<div className="group">
|
|
||||||
<Carousel
|
|
||||||
ref={carouselRef}
|
|
||||||
autoplay
|
|
||||||
effect="fade"
|
|
||||||
className="h-[600px] mb-24"
|
|
||||||
dots={{
|
|
||||||
className: "carousel-dots !bottom-32 !z-20",
|
|
||||||
}}>
|
|
||||||
{Array.isArray(slides) ? (
|
|
||||||
slides.map((item, index) => (
|
|
||||||
<div key={index} className="relative h-[600px] cursor-pointer"
|
|
||||||
onClick={()=>{
|
|
||||||
if(slideLinks?.[index])window.open(slideLinks?.[index],"_blank")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center transform transition-[transform,filter] duration-[2000ms] group-hover:scale-105 group-hover:brightness-110 will-change-[transform,filter]"
|
|
||||||
style={{
|
|
||||||
//backgroundImage: `url(https://s.cn.bing.net/th?id=OHR.GiantCuttlefish_ZH-CN0670915878_1920x1080.webp&qlt=50)`,
|
|
||||||
backgroundImage: `url(${item})`,
|
|
||||||
backfaceVisibility: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* <div
|
|
||||||
className={`absolute inset-0 bg-gradient-to-r ${item.color} to-transparent opacity-90 mix-blend-overlay transition-opacity duration-500`}
|
|
||||||
/> */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
|
||||||
|
|
||||||
{/* Content Container */}
|
|
||||||
<div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div></div>
|
|
||||||
)}
|
|
||||||
</Carousel>
|
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
|
||||||
<button
|
|
||||||
onClick={handlePrev}
|
|
||||||
className="absolute left-4 md:left-8 top-1/2 -translate-y-1/2 z-10 opacity-0 group-hover:opacity-100 transition-all duration-300 bg-black/20 hover:bg-black/30 w-12 h-12 flex items-center justify-center rounded-full transform hover:scale-110 hover:shadow-lg"
|
|
||||||
aria-label="Previous slide">
|
|
||||||
<LeftOutlined className="text-white text-xl" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleNext}
|
|
||||||
className="absolute right-4 md:right-8 top-1/2 -translate-y-1/2 z-10 opacity-0 group-hover:opacity-100 transition-all duration-300 bg-black/20 hover:bg-black/30 w-12 h-12 flex items-center justify-center rounded-full transform hover:scale-110 hover:shadow-lg"
|
|
||||||
aria-label="Next slide">
|
|
||||||
<RightOutlined className="text-white text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Container */}
|
|
||||||
{countStatistics > 1 && (
|
|
||||||
<div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-3/5 max-w-6xl px-4">
|
|
||||||
<div
|
|
||||||
className={`rounded-2xl grid grid-cols-${countStatistics} lg:grid-cols-${countStatistics} md:grid-cols-${countStatistics} gap-4 md:gap-8 p-6 md:p-8 bg-white border shadow-xl hover:shadow-2xl transition-shadow duration-500 will-change-[transform,box-shadow]`}>
|
|
||||||
{platformStats.map((stat, index) => {
|
|
||||||
return stat.value ? (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="text-center transform hover:-translate-y-1 hover:scale-105 transition-transform duration-300 ease-out">
|
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 mb-4 rounded-full bg-primary-50 text-primary-600 text-3xl transition-colors duration-300 group-hover:text-primary-700">
|
|
||||||
{stat.icon}
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent mb-1.5">
|
|
||||||
{stat.value}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-600 font-medium">
|
|
||||||
{stat.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HeroSection;
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { ArrowRightOutlined } from "@ant-design/icons";
|
|
||||||
import { Button } from "antd";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function LookForMore({to}:{to:string}) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-4 justify-between mt-12">
|
|
||||||
<div className="h-[1px] flex-grow bg-gradient-to-r from-transparent via-gray-300 to-transparent"></div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => {
|
|
||||||
navigate(to)
|
|
||||||
window.scrollTo({top: 0,behavior: "smooth"});
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-blue-600 font-medium transition-colors duration-300">
|
|
||||||
查看更多
|
|
||||||
<ArrowRightOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,34 +1,9 @@
|
||||||
import HeroSection from "./components/HeroSection";
|
import React from "react"
|
||||||
import CategorySection from "./components/CategorySection";
|
|
||||||
import CoursesSection from "./components/CoursesSection";
|
|
||||||
import { PostType } from "@nice/common";
|
|
||||||
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
|
||||||
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
const HomePage = () => {
|
return (
|
||||||
|
<div >
|
||||||
return (
|
首页
|
||||||
<div className="min-h-screen">
|
</div>
|
||||||
<HeroSection />
|
)
|
||||||
<CoursesSection
|
}
|
||||||
title="最受欢迎的思维导图"
|
|
||||||
description="深受追捧的思维导图,点亮你的智慧人生"
|
|
||||||
postType={PostType.PATH}
|
|
||||||
render={(post)=><PathCard post={post}></PathCard>}
|
|
||||||
to={"path"}
|
|
||||||
/>
|
|
||||||
<CoursesSection
|
|
||||||
title="推荐课程"
|
|
||||||
description="最受欢迎的精品课程,助你快速成长"
|
|
||||||
postType={PostType.COURSE}
|
|
||||||
render={(post)=> <CourseCard post={post}></CourseCard>}
|
|
||||||
to={"/courses"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CategorySection />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HomePage;
|
|
|
@ -1,29 +0,0 @@
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<div className=" flex">
|
|
||||||
<div className="w-1/6">
|
|
||||||
<FilterSection></FilterSection>
|
|
||||||
</div>
|
|
||||||
<div className="w-5/6 p-4 py-8">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default BasePostLayout;
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { Divider } from "antd";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
import { useMainContext } from "../MainProvider";
|
|
||||||
import TermParentSelector from "@web/src/components/models/term/term-parent-selector";
|
|
||||||
import SearchModeRadio from "./SearchModeRadio";
|
|
||||||
export default function FilterSection() {
|
|
||||||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({});
|
|
||||||
const { selectedTerms, setSelectedTerms, showSearchMode } =
|
|
||||||
useMainContext();
|
|
||||||
const handleTermChange = (slug: string, selected: string[]) => {
|
|
||||||
setSelectedTerms({
|
|
||||||
...selectedTerms,
|
|
||||||
[slug]: selected, // 更新对应 slug 的选择
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className=" flex z-0 p-6 flex-col mt-4 space-y-6 overscroll-contain overflow-x-hidden">
|
|
||||||
{showSearchMode && <SearchModeRadio></SearchModeRadio>}
|
|
||||||
{taxonomies?.map((tax, index) => {
|
|
||||||
const items = Object.entries(selectedTerms).find(
|
|
||||||
([key, items]) => key === tax.slug
|
|
||||||
)?.[1];
|
|
||||||
return (
|
|
||||||
<div key={index}>
|
|
||||||
<h3 className="text-lg font-medium mb-4">
|
|
||||||
{tax?.name}
|
|
||||||
{/* {JSON.stringify(items)} */}
|
|
||||||
</h3>
|
|
||||||
<TermParentSelector
|
|
||||||
value={items}
|
|
||||||
// slug={tax?.slug}
|
|
||||||
className="w-70 max-h-[400px] overscroll-contain overflow-x-hidden"
|
|
||||||
onChange={(selected) =>
|
|
||||||
handleTermChange(
|
|
||||||
tax?.slug,
|
|
||||||
selected as string[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
taxonomyId={tax?.id}></TermParentSelector>
|
|
||||||
<Divider></Divider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
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 (
|
|
||||||
<Space direction="vertical" align="start" className="mb-2">
|
|
||||||
<h3 className="text-lg font-medium mb-4">只搜索</h3>
|
|
||||||
<Radio.Group
|
|
||||||
value={searchMode}
|
|
||||||
onChange={handleModeChange}
|
|
||||||
buttonStyle="solid">
|
|
||||||
<Radio.Button value={PostType.COURSE}>视频课程</Radio.Button>
|
|
||||||
<Radio.Button value={PostType.PATH}>思维导图</Radio.Button>
|
|
||||||
<Radio.Button value="both">所有资源</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import {
|
|
||||||
CloudOutlined,
|
|
||||||
FileSearchOutlined,
|
|
||||||
HomeOutlined,
|
|
||||||
MailOutlined,
|
|
||||||
PhoneOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
|
|
||||||
export function MainFooter() {
|
|
||||||
return (
|
|
||||||
<footer className="bg-gradient-to-b from-slate-800 to-slate-900 relative z-10 text-secondary-200">
|
|
||||||
<div className="container mx-auto px-4 py-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
{/* 开发组织信息 */}
|
|
||||||
<div className="text-center md:text-left space-y-2">
|
|
||||||
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
|
||||||
软件与数据小组
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-400 text-xs italic">
|
|
||||||
提供技术支持
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 联系方式 */}
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<PhoneOutlined className="text-gray-400" />
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
628532
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<MailOutlined className="text-gray-400" />
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
ruanjian1@tx3l.nb.kj
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 系统链接 */}
|
|
||||||
<div className="text-center md:text-right space-y-2">
|
|
||||||
<div className="flex items-center justify-center md:justify-end space-x-4">
|
|
||||||
<a
|
|
||||||
href="https://27.57.72.21"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
title="访问门户网站">
|
|
||||||
<HomeOutlined className="text-lg" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://27.57.72.14"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
title="访问烽火青云">
|
|
||||||
<CloudOutlined className="text-lg" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="http://27.57.72.38"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
|
||||||
title="访问烽火律询">
|
|
||||||
<FileSearchOutlined className="text-lg" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 版权信息 */}
|
|
||||||
<div className="border-t border-gray-700/50 mt-4 pt-4 text-center">
|
|
||||||
<p className="text-gray-400 text-xs">
|
|
||||||
© {new Date().getFullYear()} 南天烽火. All rights
|
|
||||||
reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,107 +1,46 @@
|
||||||
import { Input, Button } from "antd";
|
import { Layout, Menu, Avatar, Button } from 'antd';
|
||||||
import { PlusOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
import { UserOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import NavigationMenu from './NavigationMenu';
|
||||||
import { UserMenu } from "./UserMenu/UserMenu";
|
|
||||||
import { NavigationMenu } from "./NavigationMenu";
|
|
||||||
import { useMainContext } from "./MainProvider";
|
|
||||||
import { env } from "@web/src/env";
|
|
||||||
export function MainHeader() {
|
|
||||||
const { isAuthenticated, user } = useAuth();
|
|
||||||
const { id } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { searchValue, setSearchValue } = useMainContext();
|
|
||||||
|
|
||||||
return (
|
const { Sider, Content } = Layout;
|
||||||
<div className="select-none w-full flex items-center justify-between bg-white shadow-md border-b border-gray-100 fixed z-30 py-2 px-4 md:px-6">
|
|
||||||
{/* 左侧区域 - 设置为不收缩 */}
|
|
||||||
<div className="flex items-center justify-start space-x-4 flex-shrink-0">
|
|
||||||
<img src="/logo.svg" className="h-12 w-12" />
|
|
||||||
<div
|
|
||||||
onClick={() => 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 whitespace-nowrap">
|
|
||||||
{env.APP_NAME}
|
|
||||||
</div>
|
|
||||||
<NavigationMenu />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧区域 - 可以灵活收缩 */}
|
export default function MainHeader() {
|
||||||
<div className="flex justify-end gap-2 md:gap-4 flex-shrink">
|
const navigate = useNavigate();
|
||||||
<div className="flex items-center gap-2 md:gap-4">
|
|
||||||
<Input
|
|
||||||
size="large"
|
|
||||||
prefix={
|
return (
|
||||||
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
<Layout className="h-screen">
|
||||||
}
|
{/* 左侧导航栏 */}
|
||||||
placeholder="搜索课程"
|
<Sider
|
||||||
className="w-full md:w-96 rounded-full"
|
width={280}
|
||||||
value={searchValue}
|
className=" flex flex-col"
|
||||||
onClick={(e) => {
|
>
|
||||||
if (
|
{/* 用户头像区域 */}
|
||||||
!window.location.pathname.startsWith("/search")
|
<div className="p-6 border-b">
|
||||||
) {
|
<div className="relative group flex justify-center">
|
||||||
navigate(`/search`);
|
<Avatar
|
||||||
window.scrollTo({
|
size={120}
|
||||||
top: 0,
|
icon={<UserOutlined />}
|
||||||
behavior: "smooth",
|
className=" transition-all duration-300"
|
||||||
});
|
/>
|
||||||
}
|
<Button
|
||||||
}}
|
type="text"
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
icon={<SettingOutlined />}
|
||||||
onPressEnter={(e) => {
|
className="!absolute bottom-0 right-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
if (
|
onClick={() => navigate('/profile')}
|
||||||
!window.location.pathname.startsWith("/search")
|
/>
|
||||||
) {
|
</div>
|
||||||
navigate(`/search`);
|
</div>
|
||||||
window.scrollTo({
|
<NavigationMenu></NavigationMenu>
|
||||||
top: 0,
|
</Sider>
|
||||||
behavior: "smooth",
|
{/* 新增可滚动内容区域 */}
|
||||||
});
|
<Layout className="flex-1">
|
||||||
}
|
<Content className="overflow-auto">
|
||||||
}}
|
<Outlet />
|
||||||
/>
|
</Content>
|
||||||
{isAuthenticated && (
|
</Layout>
|
||||||
<>
|
</Layout>
|
||||||
<Button
|
);
|
||||||
size="large"
|
|
||||||
shape="round"
|
|
||||||
icon={<PlusOutlined></PlusOutlined>}
|
|
||||||
onClick={() => {
|
|
||||||
const url = "/course/editor";
|
|
||||||
navigate(url);
|
|
||||||
}}
|
|
||||||
type="primary">
|
|
||||||
{"创建课程"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isAuthenticated && (
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
shape="round"
|
|
||||||
onClick={() => {
|
|
||||||
window.location.href = "/path/editor";
|
|
||||||
}}
|
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined></PlusOutlined>}>
|
|
||||||
创建思维导图
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isAuthenticated ? (
|
|
||||||
<UserMenu />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
shape="round"
|
|
||||||
onClick={() => navigate("/login")}
|
|
||||||
icon={<UserOutlined />}>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
|
@ -1,21 +1,11 @@
|
||||||
import { Layout } from "antd";
|
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { MainHeader } from "./MainHeader";
|
import MainHeader from "./MainHeader";
|
||||||
import { MainFooter } from "./MainFooter";
|
import { Content } from "antd/es/layout/layout";
|
||||||
import { MainProvider } from "./MainProvider";
|
export default function MainLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainHeader />
|
||||||
|
</>
|
||||||
|
|
||||||
const { Content } = Layout;
|
)
|
||||||
|
|
||||||
export function MainLayout() {
|
|
||||||
return (
|
|
||||||
<MainProvider>
|
|
||||||
<div className=" min-h-screen bg-gray-100">
|
|
||||||
<MainHeader />
|
|
||||||
<Content className=" flex-grow pt-16 bg-gray-50 ">
|
|
||||||
<Outlet />
|
|
||||||
</Content>
|
|
||||||
<MainFooter />
|
|
||||||
</div>
|
|
||||||
</MainProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
|
@ -1,109 +0,0 @@
|
||||||
import { PostType, Prisma } from "@nice/common";
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
ReactNode,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
interface SelectedTerms {
|
|
||||||
[key: string]: string[]; // 每个 slug 对应一个 string 数组
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainContextType {
|
|
||||||
searchValue?: string;
|
|
||||||
selectedTerms?: SelectedTerms;
|
|
||||||
setSearchValue?: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setSelectedTerms?: React.Dispatch<React.SetStateAction<SelectedTerms>>;
|
|
||||||
searchCondition?: Prisma.PostWhereInput;
|
|
||||||
termsCondition?: Prisma.PostWhereInput;
|
|
||||||
searchMode?: PostType.COURSE | PostType.PATH | "both";
|
|
||||||
setSearchMode?: React.Dispatch<
|
|
||||||
React.SetStateAction<PostType.COURSE | PostType.PATH | "both">
|
|
||||||
>;
|
|
||||||
showSearchMode?: boolean;
|
|
||||||
setShowSearchMode?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MainContext = createContext<MainContextType | null>(null);
|
|
||||||
interface MainProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MainProvider({ children }: MainProviderProps) {
|
|
||||||
const [searchMode, setSearchMode] = useState<
|
|
||||||
PostType.COURSE | PostType.PATH | "both"
|
|
||||||
>("both");
|
|
||||||
const [showSearchMode, setShowSearchMode] = useState<boolean>(false);
|
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
|
||||||
const [debouncedValue] = useDebounce<string>(searchValue, 500);
|
|
||||||
const [selectedTerms, setSelectedTerms] = useState<SelectedTerms>({}); // 初始化状态
|
|
||||||
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: debouncedValue,
|
|
||||||
mode: "insensitive" as Prisma.QueryMode, // 使用类型断言
|
|
||||||
};
|
|
||||||
return debouncedValue
|
|
||||||
? {
|
|
||||||
OR: [
|
|
||||||
{ title: containTextCondition },
|
|
||||||
{ subTitle: containTextCondition },
|
|
||||||
{ content: containTextCondition },
|
|
||||||
{
|
|
||||||
terms: {
|
|
||||||
some: {
|
|
||||||
name: containTextCondition,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
}, [debouncedValue]);
|
|
||||||
return (
|
|
||||||
<MainContext.Provider
|
|
||||||
value={{
|
|
||||||
searchValue,
|
|
||||||
setSearchValue,
|
|
||||||
selectedTerms,
|
|
||||||
setSelectedTerms,
|
|
||||||
searchCondition,
|
|
||||||
termsCondition,
|
|
||||||
searchMode,
|
|
||||||
setSearchMode,
|
|
||||||
showSearchMode,
|
|
||||||
setShowSearchMode,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</MainContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export const useMainContext = () => {
|
|
||||||
const context = useContext(MainContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useMainContext must be used within MainProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
|
@ -1,59 +1,37 @@
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
import { Menu } from "antd";
|
import { Menu } from "antd";
|
||||||
import { useMemo } from "react";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
export const NavigationMenu = () => {
|
export default function NavigationMenu() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated } = useAuth();
|
// 导航菜单项配置
|
||||||
const { pathname } = useLocation();
|
const menuItems = [
|
||||||
|
{ key: 'home', label: '首页', path: '/' },
|
||||||
const menuItems = useMemo(() => {
|
{ key: 'staff', label: '人员总览', path: '/staff' },
|
||||||
const baseItems = [
|
{ key: 'day', label: '日统计', path: '/day' },
|
||||||
{ key: "home", path: "/", label: "首页" },
|
{ key: 'month', label: '月统计', path: '/month' },
|
||||||
{ key: "path", path: "/path", label: "全部思维导图" },
|
{ key: 'year', label: '年度统计', path: '/year' }
|
||||||
{ key: "courses", path: "/courses", label: "所有课程" },
|
];
|
||||||
];
|
return (
|
||||||
|
<>
|
||||||
if (!isAuthenticated) {
|
{/* 导航菜单 */}
|
||||||
return baseItems;
|
<Menu
|
||||||
} else {
|
theme="dark"
|
||||||
return [
|
mode="inline"
|
||||||
...baseItems,
|
className="!bg-transparent !border-0 pt-4 [&_.ant-menu-item]:!mt-2"
|
||||||
{ key: "my-duty", path: "/my-duty", label: "我创建的课程" },
|
defaultSelectedKeys={['home']}
|
||||||
{ key: "my-learning", path: "/my-learning", label: "我学习的课程" },
|
>
|
||||||
{ key: "my-duty-path", path: "/my-duty-path", label: "我创建的思维导图" },
|
{menuItems.map((item) => (
|
||||||
{ key: "my-path", path: "/my-path", label: "我学习的思维导图" },
|
<Menu.Item
|
||||||
];
|
key={item.key}
|
||||||
}
|
className="!h-14 !flex !items-center !text-gray-300 hover:!text-white group !rounded-lg mx-4"
|
||||||
}, [isAuthenticated]);
|
onClick={() => navigate(item.path)}
|
||||||
|
>
|
||||||
const selectedKey = useMemo(() => {
|
<div className="flex items-center justify-center w-full px-4 transition-all duration-300 h-full rounded-lg">
|
||||||
const normalizePath = (path: string): string => path.replace(/\/$/, "");
|
<span className="text-xl font-medium">{item.label}</span>
|
||||||
return pathname === '/' ? "home" : menuItems.find((item) => normalizePath(pathname) === item.path)?.key || "";
|
</div>
|
||||||
}, [pathname]);
|
</Menu.Item>
|
||||||
|
))}
|
||||||
return (
|
</Menu>
|
||||||
<Menu
|
</>
|
||||||
mode="horizontal"
|
);
|
||||||
className="border-none font-medium"
|
}
|
||||||
disabledOverflow={true}
|
|
||||||
selectedKeys={[selectedKey]}
|
|
||||||
onClick={({ key }) => {
|
|
||||||
const selectedItem = menuItems.find((item) => item.key === key);
|
|
||||||
if (selectedItem) navigate(selectedItem.path);
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
{menuItems.map(({ key, label }) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={key}
|
|
||||||
className="text-gray-600 hover:text-blue-600">
|
|
||||||
{label}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { Button, Drawer, Modal } from "antd";
|
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { UserEditorContext } from "./UserMenu";
|
|
||||||
import UserForm from "./UserForm";
|
|
||||||
|
|
||||||
export default function UserEditModal() {
|
|
||||||
const { formLoading, modalOpen, setModalOpen, form } =
|
|
||||||
useContext(UserEditorContext);
|
|
||||||
const handleOk = () => {
|
|
||||||
form.submit();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
width={400}
|
|
||||||
onOk={handleOk}
|
|
||||||
centered
|
|
||||||
open={modalOpen}
|
|
||||||
confirmLoading={formLoading}
|
|
||||||
onCancel={() => {
|
|
||||||
setModalOpen(false);
|
|
||||||
}}
|
|
||||||
title={"编辑个人信息"}>
|
|
||||||
<UserForm />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
import { Button, Form, Input, Spin, Switch, message } from "antd";
|
|
||||||
import { useContext, useEffect } from "react";
|
|
||||||
import { useStaff } from "@nice/client";
|
|
||||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
|
||||||
import { StaffDto } from "@nice/common";
|
|
||||||
import { UserEditorContext } from "./UserMenu";
|
|
||||||
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 {
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
data: StaffDto;
|
|
||||||
isLoading: boolean;
|
|
||||||
} = api.staff.findFirst.useQuery(
|
|
||||||
{ where: { id: user?.id } },
|
|
||||||
{ enabled: !!user?.id }
|
|
||||||
);
|
|
||||||
const { isRoot } = useAuth();
|
|
||||||
async function handleFinish(values: any) {
|
|
||||||
const {
|
|
||||||
username,
|
|
||||||
showname,
|
|
||||||
deptId,
|
|
||||||
domainId,
|
|
||||||
password,
|
|
||||||
phoneNumber,
|
|
||||||
officerId,
|
|
||||||
enabled,
|
|
||||||
avatar,
|
|
||||||
photoUrl,
|
|
||||||
email,
|
|
||||||
rank,
|
|
||||||
office,
|
|
||||||
} = values;
|
|
||||||
setFormLoading(true);
|
|
||||||
try {
|
|
||||||
if (data && user?.id) {
|
|
||||||
await update.mutateAsync({
|
|
||||||
where: { id: data.id },
|
|
||||||
data: {
|
|
||||||
username,
|
|
||||||
deptId,
|
|
||||||
showname,
|
|
||||||
domainId,
|
|
||||||
password,
|
|
||||||
phoneNumber,
|
|
||||||
officerId,
|
|
||||||
enabled,
|
|
||||||
avatar,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toast.success("提交成功");
|
|
||||||
setModalOpen(false);
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.error(err.message);
|
|
||||||
} finally {
|
|
||||||
setFormLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
form.resetFields();
|
|
||||||
console.log('cc', data);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
form.setFieldValue("username", data.username);
|
|
||||||
form.setFieldValue("showname", data.showname);
|
|
||||||
form.setFieldValue("domainId", data.domainId);
|
|
||||||
form.setFieldValue("deptId", data.deptId);
|
|
||||||
form.setFieldValue("officerId", data.officerId);
|
|
||||||
form.setFieldValue("phoneNumber", data.phoneNumber);
|
|
||||||
form.setFieldValue("enabled", data.enabled);
|
|
||||||
form.setFieldValue("avatar", data.avatar);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!data && domainId) {
|
|
||||||
// form.setFieldValue("domainId", domainId);
|
|
||||||
// form.setFieldValue("deptId", domainId);
|
|
||||||
// }
|
|
||||||
// }, [domainId, data as any]);
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
{isLoading && (
|
|
||||||
<div className="absolute h-full inset-0 flex items-center justify-center bg-white bg-opacity-50 z-10">
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form
|
|
||||||
disabled={isLoading}
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
requiredMark="optional"
|
|
||||||
autoComplete="off"
|
|
||||||
onFinish={handleFinish}>
|
|
||||||
<div className=" flex items-center gap-4 mb-2">
|
|
||||||
<div>
|
|
||||||
<Form.Item name={"avatar"} label="头像" noStyle>
|
|
||||||
<AvatarUploader
|
|
||||||
placeholder="点击上传头像"
|
|
||||||
className="rounded-lg"
|
|
||||||
style={{
|
|
||||||
width: "120px",
|
|
||||||
height: "150px",
|
|
||||||
}}></AvatarUploader>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 flex-1">
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
name={"showname"}
|
|
||||||
label="名称">
|
|
||||||
<Input
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
allowClear
|
|
||||||
autoComplete="new-name" // 使用非标准的自动完成值
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={"domainId"}
|
|
||||||
label="所属域"
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true }]}>
|
|
||||||
<DepartmentSelect
|
|
||||||
placeholder="选择域"
|
|
||||||
onChange={(value) => {
|
|
||||||
setDomainId(value as string);
|
|
||||||
}}
|
|
||||||
domain={true}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
name={"deptId"}
|
|
||||||
label="所属单位"
|
|
||||||
rules={[{ required: true }]}>
|
|
||||||
<DepartmentSelect rootId={domainId} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item noStyle label="密码" name={"password"}>
|
|
||||||
<Input.Password
|
|
||||||
placeholder="修改密码"
|
|
||||||
spellCheck={false}
|
|
||||||
visibilityToggle
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,246 +0,0 @@
|
||||||
import { useClickOutside } from "@web/src/hooks/useClickOutside";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import React, {
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
createContext,
|
|
||||||
} from "react";
|
|
||||||
import { Avatar } from "@web/src/components/common/element/Avatar";
|
|
||||||
import {
|
|
||||||
UserOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { FormInstance, Spin } from "antd";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { MenuItemType } from "./types";
|
|
||||||
import { RolePerms } from "@nice/common";
|
|
||||||
import { useForm } from "antd/es/form/Form";
|
|
||||||
import UserEditModal from "./UserEditModal";
|
|
||||||
const menuVariants = {
|
|
||||||
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exit: {
|
|
||||||
opacity: 0,
|
|
||||||
scale: 0.95,
|
|
||||||
y: -10,
|
|
||||||
transition: {
|
|
||||||
duration: 0.2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserEditorContext = createContext<{
|
|
||||||
domainId: string;
|
|
||||||
setDomainId: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
modalOpen: boolean;
|
|
||||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
form: FormInstance<any>;
|
|
||||||
formLoading: boolean;
|
|
||||||
setFormLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}>({
|
|
||||||
modalOpen: false,
|
|
||||||
domainId: undefined,
|
|
||||||
setDomainId: undefined,
|
|
||||||
setModalOpen: undefined,
|
|
||||||
form: undefined,
|
|
||||||
formLoading: undefined,
|
|
||||||
setFormLoading: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function UserMenu() {
|
|
||||||
const [form] = useForm();
|
|
||||||
const [formLoading, setFormLoading] = useState<boolean>();
|
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { user, logout, isLoading, hasSomePermissions } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
|
||||||
const [domainId, setDomainId] = useState<string>();
|
|
||||||
const toggleMenu = useCallback(() => {
|
|
||||||
setShowMenu((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
const canManageAnyStaff = useMemo(() => {
|
|
||||||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF);
|
|
||||||
}, [user]);
|
|
||||||
const menuItems: MenuItemType[] = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
icon: <UserOutlined className="text-lg" />,
|
|
||||||
label: "个人信息",
|
|
||||||
action: () => {
|
|
||||||
setModalOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
canManageAnyStaff && {
|
|
||||||
icon: <SettingOutlined className="text-lg" />,
|
|
||||||
label: "设置",
|
|
||||||
action: () => {
|
|
||||||
navigate("/admin/staff");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
icon: <LogoutOutlined className="text-lg" />,
|
|
||||||
label: "注销",
|
|
||||||
action: () => logout(),
|
|
||||||
},
|
|
||||||
].filter(Boolean),
|
|
||||||
[logout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMenuItemClick = useCallback((action: () => void) => {
|
|
||||||
action();
|
|
||||||
setShowMenu(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center w-10 h-10">
|
|
||||||
<Spin size="small" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserEditorContext.Provider
|
|
||||||
value={{
|
|
||||||
formLoading,
|
|
||||||
setFormLoading,
|
|
||||||
form,
|
|
||||||
domainId,
|
|
||||||
modalOpen,
|
|
||||||
setDomainId,
|
|
||||||
setModalOpen,
|
|
||||||
}}>
|
|
||||||
<div ref={menuRef} className="relative">
|
|
||||||
<motion.button
|
|
||||||
aria-label="用户菜单"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded={showMenu}
|
|
||||||
aria-controls="user-menu"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
onClick={toggleMenu}
|
|
||||||
className="flex items-center rounded-full transition-all duration-200 ease-in-out">
|
|
||||||
{/* Avatar 容器,相对定位 */}
|
|
||||||
<div className="relative">
|
|
||||||
<Avatar
|
|
||||||
src={user?.avatar}
|
|
||||||
name={user?.showname || user?.username}
|
|
||||||
size={40}
|
|
||||||
className="ring-2 ring-white hover:ring-[#00538E]/90
|
|
||||||
transition-all duration-200 ease-in-out shadow-md
|
|
||||||
hover:shadow-lg focus:outline-none
|
|
||||||
focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2
|
|
||||||
focus:ring-offset-white "
|
|
||||||
/>
|
|
||||||
{/* 小绿点 */}
|
|
||||||
<span
|
|
||||||
className="absolute bottom-0 right-0 h-3 w-3
|
|
||||||
rounded-full bg-emerald-500 ring-2 ring-white
|
|
||||||
shadow-sm transition-transform duration-200
|
|
||||||
ease-in-out hover:scale-110"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showMenu && (
|
|
||||||
<motion.div
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit="exit"
|
|
||||||
variants={menuVariants}
|
|
||||||
role="menu"
|
|
||||||
id="user-menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="user-menu-button"
|
|
||||||
style={{ zIndex: 100 }}
|
|
||||||
className="absolute right-0 mt-3 w-64 origin-top-right
|
|
||||||
bg-white rounded-xl overflow-hidden shadow-lg
|
|
||||||
border border-[#E5EDF5]">
|
|
||||||
{/* User Profile Section */}
|
|
||||||
<div
|
|
||||||
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
|
|
||||||
border-b border-[#E5EDF5] ">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Avatar
|
|
||||||
src={user?.avatar}
|
|
||||||
name={user?.showname || user?.username}
|
|
||||||
size={40}
|
|
||||||
className="ring-2 ring-white shadow-sm"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col space-y-0.5">
|
|
||||||
<span className="text-sm font-semibold text-[#00538E]">
|
|
||||||
{user?.showname || user?.username}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-[#718096] flex items-center gap-1.5">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
||||||
在线
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Menu Items */}
|
|
||||||
<div className="p-2">
|
|
||||||
{menuItems.map((item, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
role="menuitem"
|
|
||||||
tabIndex={showMenu ? 0 : -1}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleMenuItemClick(item.action);
|
|
||||||
}}
|
|
||||||
className={`flex items-center gap-3 w-full px-4 py-3
|
|
||||||
text-sm font-medium rounded-lg transition-all
|
|
||||||
focus:outline-none
|
|
||||||
focus:ring-2 focus:ring-[#00538E]/20
|
|
||||||
group relative overflow-hidden
|
|
||||||
active:scale-[0.99]
|
|
||||||
${
|
|
||||||
item.label === "注销"
|
|
||||||
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
|
||||||
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
|
||||||
}`}>
|
|
||||||
<span
|
|
||||||
className={`w-5 h-5 flex items-center justify-center
|
|
||||||
transition-all duration-200 ease-in-out
|
|
||||||
group-hover:scale-110 group-hover:rotate-6
|
|
||||||
group-hover:translate-x-0.5 ${
|
|
||||||
item.label === "注销"
|
|
||||||
? "group-hover:text-red-600"
|
|
||||||
: "group-hover:text-[#003F6A]"
|
|
||||||
}`}>
|
|
||||||
{item.icon}
|
|
||||||
</span>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
<UserEditModal></UserEditModal>
|
|
||||||
</UserEditorContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
export interface MenuItemType {
|
|
||||||
icon: ReactNode;
|
|
||||||
label: string;
|
|
||||||
action: () => void;
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import { PostType } from "@nice/common";
|
|
||||||
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
|
||||||
|
|
||||||
export default function MyDutyPathContainer() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <PathCard post={post}></PathCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.PATH,
|
|
||||||
authorId: user?.id,
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import BasePostLayout from "../layout/BasePost/BasePostLayout";
|
|
||||||
import { useMainContext } from "../layout/MainProvider";
|
|
||||||
import { PostType } from "@nice/common";
|
|
||||||
import MyDutyPathContainer from "./components/MyDutyPathContainer";
|
|
||||||
|
|
||||||
export default function MyDutyPathPage() {
|
|
||||||
const { setSearchMode } = useMainContext();
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchMode(PostType.PATH);
|
|
||||||
}, [setSearchMode]);
|
|
||||||
return (
|
|
||||||
<BasePostLayout>
|
|
||||||
<MyDutyPathContainer></MyDutyPathContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
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 PostCard from "@web/src/components/models/post/PostCard";
|
|
||||||
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
|
||||||
|
|
||||||
export default function MyDutyListContainer() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
authorId: user.id,
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
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 { setSearchMode } = useMainContext();
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchMode(PostType.COURSE);
|
|
||||||
}, [setSearchMode]);
|
|
||||||
return (
|
|
||||||
<BasePostLayout>
|
|
||||||
<MyDutyListContainer></MyDutyListContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import { PostType } from "@nice/common";
|
|
||||||
import PostCard from "@web/src/components/models/post/PostCard";
|
|
||||||
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
|
||||||
|
|
||||||
export default function MyLearningListContainer() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
students: {
|
|
||||||
some: {
|
|
||||||
id: user?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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 { setSearchMode } = useMainContext();
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchMode(PostType.COURSE);
|
|
||||||
}, [setSearchMode]);
|
|
||||||
return (
|
|
||||||
<BasePostLayout>
|
|
||||||
<MyLearningListContainer></MyLearningListContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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 PostCard from "@web/src/components/models/post/PostCard";
|
|
||||||
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
|
||||||
|
|
||||||
export default function MyPathListContainer() {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <PathCard post={post}></PathCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.PATH,
|
|
||||||
students: {
|
|
||||||
some: {
|
|
||||||
id: user?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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 (
|
|
||||||
<BasePostLayout>
|
|
||||||
<MyPathListContainer></MyPathListContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { BookOutlined, EyeOutlined, TeamOutlined } from "@ant-design/icons";
|
|
||||||
import { Typography } from "antd";
|
|
||||||
import { PostDto } from "@nice/common";
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
|
||||||
const DeptInfo = ({ post }: { post: PostDto }) => {
|
|
||||||
return (
|
|
||||||
<div className="gap-1 flex items-center justify-between flex-grow">
|
|
||||||
<div className=" flex justify-start gap-1 items-center">
|
|
||||||
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
|
||||||
{post?.depts && post?.depts?.length > 0 ? (
|
|
||||||
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
|
||||||
{post?.depts?.length > 1
|
|
||||||
? `${post.depts[0].name}等`
|
|
||||||
: post?.depts?.[0]?.name}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
|
||||||
未设置单位
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{post && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
|
||||||
浏览量
|
|
||||||
<EyeOutlined />
|
|
||||||
{`${post?.views || 0}`}
|
|
||||||
</span>
|
|
||||||
{post?.studentIds && post?.studentIds?.length > 0 && (
|
|
||||||
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
|
||||||
<BookOutlined />
|
|
||||||
{`${post?.studentIds?.length || 0}`}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeptInfo;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import { PostType, Prisma } from "@nice/common";
|
|
||||||
import PostCard from "@web/src/components/models/post/PostCard";
|
|
||||||
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
|
||||||
|
|
||||||
export function PathListContainer() {
|
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => <PathCard post={post}></PathCard>}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: PostType.PATH,
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default PathListContainer;
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { Tag } from "antd";
|
|
||||||
import { PostDto, TaxonomySlug, TermDto } from "@nice/common";
|
|
||||||
|
|
||||||
const TermInfo = ({ terms = [] }: { terms?: TermDto[] }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{terms && terms?.length > 0 ? (
|
|
||||||
<div className="flex gap-2 mb-4">
|
|
||||||
{terms
|
|
||||||
?.sort((a, b) =>
|
|
||||||
String(a?.taxonomy?.id || "").localeCompare(
|
|
||||||
String(b?.taxonomy?.id || "")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
?.map((term: any) => {
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
key={term.id}
|
|
||||||
color={
|
|
||||||
term?.taxonomy?.slug ===
|
|
||||||
TaxonomySlug.CATEGORY
|
|
||||||
? "green"
|
|
||||||
: term?.taxonomy?.slug ===
|
|
||||||
TaxonomySlug.LEVEL
|
|
||||||
? "blue"
|
|
||||||
: "orange"
|
|
||||||
}
|
|
||||||
className="px-3 py-1 rounded-full border-0">
|
|
||||||
{term.name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2 mb-4">
|
|
||||||
<Tag
|
|
||||||
color={"orange"}
|
|
||||||
className="px-3 py-1 rounded-full border-0">
|
|
||||||
{"未设置分类"}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TermInfo;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import MindEditor from "@web/src/components/common/editor/MindEditor";
|
|
||||||
import { PostDetailProvider } from "@web/src/components/models/course/detail/PostDetailContext";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function PathEditorPage() {
|
|
||||||
const { id } = useParams();
|
|
||||||
return (
|
|
||||||
<PostDetailProvider editId={id}>
|
|
||||||
<MindEditor id={id}></MindEditor>;
|
|
||||||
</PostDetailProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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";
|
|
||||||
import { PostDetailProvider } from "@web/src/components/models/course/detail/PostDetailContext";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function PathPage() {
|
|
||||||
const { setSearchMode } = useMainContext();
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchMode(PostType.PATH);
|
|
||||||
}, [setSearchMode]);
|
|
||||||
const { id } = useParams();
|
|
||||||
return (
|
|
||||||
<BasePostLayout>
|
|
||||||
<PathListContainer></PathListContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import PostCard from "@web/src/components/models/post/PostCard";
|
|
||||||
import { PostType } from "@nice/common";
|
|
||||||
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
|
||||||
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
|
||||||
const POST_TYPE_COMPONENTS = {
|
|
||||||
[PostType.COURSE]: CourseCard,
|
|
||||||
[PostType.PATH]: PathCard,
|
|
||||||
};
|
|
||||||
export default function SearchListContainer() {
|
|
||||||
const { searchCondition, termsCondition, searchMode } = useMainContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PostList
|
|
||||||
renderItem={(post) => {
|
|
||||||
const Component =
|
|
||||||
POST_TYPE_COMPONENTS[post.type] || PostCard;
|
|
||||||
return <Component post={post} />;
|
|
||||||
}}
|
|
||||||
params={{
|
|
||||||
pageSize: 12,
|
|
||||||
where: {
|
|
||||||
type: searchMode === "both" ? undefined : searchMode,
|
|
||||||
...termsCondition,
|
|
||||||
...searchCondition,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
cols={4}></PostList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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 (
|
|
||||||
<BasePostLayout>
|
|
||||||
<SearchListContainer></SearchListContainer>
|
|
||||||
</BasePostLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export default function MyCoursePage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
My Course Page
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default function ProfilesPage() {
|
|
||||||
return <>Profiles</>
|
|
||||||
}
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { api, useStaff } from "@nice/client";
|
||||||
|
import { Button, Form, Input, Modal, Select, Table } from "antd";
|
||||||
|
import { StaffDto } from "@nice/common";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
export default function StaffMessage() {
|
||||||
|
const initialValues = {
|
||||||
|
username: "",
|
||||||
|
deptId: "",
|
||||||
|
absent: "是",
|
||||||
|
};
|
||||||
|
const { create, update } = useStaff();
|
||||||
|
const [searchName, setSearchName] = useState("");
|
||||||
|
const { data: staffs, isLoading } = api.staff.findMany.useQuery({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
contains: searchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(staffs);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [editingRecord, setEditingRecord] = useState<StaffDto | null>(null);
|
||||||
|
const colnums = [
|
||||||
|
{
|
||||||
|
title: "姓名",
|
||||||
|
dataIndex: "username",
|
||||||
|
key: "username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "部门",
|
||||||
|
dataIndex: "deptId",
|
||||||
|
key: "deptId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "在位",
|
||||||
|
dataIndex: "absent",
|
||||||
|
key: "absent",
|
||||||
|
render: (_, record) => (
|
||||||
|
<Select
|
||||||
|
// defaultValue={record.absent ? "是" : "否"}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onChange={async (value) => {
|
||||||
|
try {
|
||||||
|
await update.mutateAsync({
|
||||||
|
where: { id: record.id },
|
||||||
|
data: { absent: value }
|
||||||
|
});
|
||||||
|
toast.success("状态更新成功");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("状态更新失败");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value="是">是</Select.Option>
|
||||||
|
<Select.Option value="否">否</Select.Option>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key={record.id}
|
||||||
|
onClick={() => handleEdit(record)}>编辑</Button>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleNew = () => {
|
||||||
|
form.setFieldsValue(initialValues);
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingRecord) {
|
||||||
|
form.setFieldsValue(editingRecord);
|
||||||
|
console.log(editingRecord);
|
||||||
|
}
|
||||||
|
}, [editingRecord]);
|
||||||
|
|
||||||
|
const handleEdit = (record) => {
|
||||||
|
setEditingRecord(record);
|
||||||
|
form.setFieldsValue(editingRecord);
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
const values = await form.getFieldsValue();
|
||||||
|
const orderValue = values.order ? parseFloat(values.order) : null;
|
||||||
|
console.log(values.username);
|
||||||
|
try {
|
||||||
|
if (editingRecord && editingRecord.id) {
|
||||||
|
// console.log(editingRecord);
|
||||||
|
const result = await update.mutateAsync(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
id: editingRecord.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
username: values.username,
|
||||||
|
deptId: values.deptId,
|
||||||
|
order: orderValue,
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// console.log(result);
|
||||||
|
} else {
|
||||||
|
await create.mutateAsync(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
username: values.username,
|
||||||
|
deptId: values.deptId,
|
||||||
|
order: orderValue,
|
||||||
|
createdAt: new Date(),
|
||||||
|
showname: values.username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
toast.success("保存成功");
|
||||||
|
setVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("保存失败");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
setSearchName(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-2 min-h-screen bg-gradient-to-br">
|
||||||
|
<Form>
|
||||||
|
<div className="p-4 h-full flex flex-col"> {/* 修改为flex布局 */}
|
||||||
|
<div className="max-w-full mx-auto flex-1 flex flex-col"> {/* 添加flex容器 */}
|
||||||
|
{/* 头部区域保持不变... */}
|
||||||
|
<div className="flex justify-between mb-4 space-x-4 items-center">
|
||||||
|
<div className="text-2xl">XX 公司人员信息表</div>
|
||||||
|
<div className="relative w-1/3">
|
||||||
|
<Input
|
||||||
|
placeholder="输入姓名搜索"
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="pl-10 w-full border"
|
||||||
|
/>
|
||||||
|
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 " />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleNew}
|
||||||
|
className=" font-bold py-2 px-4 rounded"
|
||||||
|
>
|
||||||
|
新建人员
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* 表格容器增加flex布局 */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center mt-4">
|
||||||
|
<div className="animate-spin rounded-full h-10 w-10 border-4"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 min-h-[calc(100vh-200px)]">
|
||||||
|
<Table
|
||||||
|
key={"username"}
|
||||||
|
columns={colnums}
|
||||||
|
dataSource={staffs}
|
||||||
|
className="bg-gray-900/50 backdrop-blur-sm border-2
|
||||||
|
[&_.ant-table-tbody>tr>td]:!text-lg
|
||||||
|
[&_.ant-table-tbody>tr>td]:!py-5
|
||||||
|
[&_.ant-table-thead>tr>th]:!text-lg
|
||||||
|
[&_.ant-table-thead>tr>th]:!py-5
|
||||||
|
h-full"
|
||||||
|
tableLayout="fixed"
|
||||||
|
pagination={{
|
||||||
|
position: ["bottomCenter"],
|
||||||
|
pageSize: 15
|
||||||
|
}}
|
||||||
|
onRow={(record) => ({
|
||||||
|
className: "hover:bg-gray-800/50 transition-colors even:bg-gray-800/50 hover:shadow-lg hover:shadow-blue-500/20",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<thead className="">
|
||||||
|
<tr>
|
||||||
|
{colnums.map((column) => (
|
||||||
|
<th
|
||||||
|
key={column.key}
|
||||||
|
className="px-4 border-b-2" // 简化样式
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody> {/* 移除原有text-gray-300 */}
|
||||||
|
{staffs?.map((record) => (
|
||||||
|
<tr
|
||||||
|
key={record.id}
|
||||||
|
className="border-b border-blue-400/10"
|
||||||
|
>
|
||||||
|
{colnums.map((column) => (
|
||||||
|
<td
|
||||||
|
key={column.key}
|
||||||
|
className="px-4 border" // 简化样式
|
||||||
|
>
|
||||||
|
{column.render?.(record[column.dataIndex], record) || record[column.dataIndex]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 模态框样式更新 */}
|
||||||
|
<Modal
|
||||||
|
title="编辑员工信息"
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
className="[&_.ant-modal-content]:bg-gray-800 [&_.ant-modal-title]:text-gray-100"
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
initialValues={initialValues}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name={"username"}
|
||||||
|
label="姓名"
|
||||||
|
// labelClassName="text-gray-300"
|
||||||
|
>
|
||||||
|
<Input className="rounded-lg" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={"order"}
|
||||||
|
label="序号"
|
||||||
|
// labelClassName="text-gray-300"
|
||||||
|
>
|
||||||
|
<Input className=" rounded-lg" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -6,25 +6,10 @@ import {
|
||||||
useParams,
|
useParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import ErrorPage from "../app/error";
|
import ErrorPage from "../app/error";
|
||||||
import WithAuth from "../components/utils/with-auth";
|
|
||||||
import LoginPage from "../app/login";
|
import LoginPage from "../app/login";
|
||||||
import HomePage from "../app/main/home/page";
|
import HomePage from "../app/main/home/page";
|
||||||
import { CourseDetailPage } from "../app/main/course/detail/page";
|
import StaffMessage from "../app/main/staffpage/page";
|
||||||
import { CourseBasicForm } from "../components/models/course/editor/form/CourseBasicForm";
|
import MainLayout from "../app/main/layout/MainLayout";
|
||||||
import CourseContentForm from "../components/models/course/editor/form/CourseContentForm/CourseContentForm";
|
|
||||||
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
|
|
||||||
import { MainLayout } from "../app/main/layout/MainLayout";
|
|
||||||
import CoursesPage from "../app/main/courses/page";
|
|
||||||
import PathPage from "../app/main/path/page";
|
|
||||||
import { adminRoute } from "./admin-route";
|
|
||||||
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";
|
|
||||||
import MyDutyPathPage from "../app/main/my-duty-path/page";
|
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
breadcrumb?: string;
|
breadcrumb?: string;
|
||||||
|
@ -60,103 +45,15 @@ export const routes: CustomRouteObject[] = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <HomePage />,
|
element: <HomePage></HomePage>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "path",
|
path: "/staff",
|
||||||
children: [
|
element: <StaffMessage></StaffMessage>,
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <PathPage></PathPage>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "editor/:id?",
|
|
||||||
element: (
|
|
||||||
<PathEditorPage></PathEditorPage>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "courses",
|
|
||||||
element: <CoursesPage></CoursesPage>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "my-path",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<MyPathPage></MyPathPage>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "my-duty-path",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<MyDutyPathPage></MyDutyPathPage>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "my-duty",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<MyDutyPage></MyDutyPage>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "my-learning",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<MyLearningPage></MyLearningPage>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "search",
|
|
||||||
element: <SearchPage></SearchPage>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "course/:id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的
|
|
||||||
element: <CourseDetailPage />,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
path: "course",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ":id?/editor",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<CourseEditorLayout></CourseEditorLayout>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<CourseBasicForm></CourseBasicForm>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: "content",
|
|
||||||
element: (
|
|
||||||
<WithAuth>
|
|
||||||
<CourseContentForm></CourseContentForm>
|
|
||||||
</WithAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
adminRoute,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue