02262318
This commit is contained in:
parent
f98bc41f1c
commit
6da5a12ab9
|
@ -101,11 +101,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
},
|
},
|
||||||
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
||||||
) {
|
) {
|
||||||
// const await db.post.findMany({
|
|
||||||
// where: {
|
|
||||||
// type: PostType.COURSE,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
const { courseDetail } = args;
|
const { courseDetail } = args;
|
||||||
// If no transaction is provided, create a new one
|
// If no transaction is provided, create a new one
|
||||||
if (!params.tx) {
|
if (!params.tx) {
|
||||||
|
@ -131,6 +127,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
) {
|
) {
|
||||||
args.data.authorId = params?.staff?.id;
|
args.data.authorId = params?.staff?.id;
|
||||||
args.data.updatedAt = dayjs().toDate();
|
args.data.updatedAt = dayjs().toDate();
|
||||||
|
|
||||||
const result = await super.create(args);
|
const result = await super.create(args);
|
||||||
EventBus.emit('dataChanged', {
|
EventBus.emit('dataChanged', {
|
||||||
type: ObjectType.POST,
|
type: ObjectType.POST,
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"@nice/iconer": "workspace:^",
|
"@nice/iconer": "workspace:^",
|
||||||
"@nice/utils": "workspace:^",
|
"@nice/utils": "workspace:^",
|
||||||
"mind-elixir": "workspace:^",
|
"mind-elixir": "workspace:^",
|
||||||
|
"@mind-elixir/node-menu": "workspace:*",
|
||||||
"@nice/ui": "workspace:^",
|
"@nice/ui": "workspace:^",
|
||||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||||
"@tanstack/react-query": "^5.51.21",
|
"@tanstack/react-query": "^5.51.21",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import CourseList from "@web/src/components/models/course/list/CourseList";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
import { PostType, Prisma } from "@nice/common";
|
import { PostType, Prisma } from "@nice/common";
|
||||||
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import CourseCard from "./CourseCard";
|
||||||
|
|
||||||
export function CoursesContainer() {
|
export function CoursesContainer() {
|
||||||
const { searchValue, selectedTerms } = useMainContext();
|
const { searchValue, selectedTerms } = useMainContext();
|
||||||
|
@ -16,7 +17,8 @@ export function CoursesContainer() {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CourseList
|
<PostList
|
||||||
|
renderItem={(post) => <CourseCard course={post}></CourseCard>}
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
@ -44,7 +46,7 @@ export function CoursesContainer() {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
cols={4}></CourseList>
|
cols={4}></PostList>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,40 +3,40 @@ import { Typography, Skeleton } from "antd";
|
||||||
import { TaxonomySlug, TermDto } from "@nice/common";
|
import { TaxonomySlug, TermDto } from "@nice/common";
|
||||||
import { api } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import { CoursesSectionTag } from "./CoursesSectionTag";
|
import { CoursesSectionTag } from "./CoursesSectionTag";
|
||||||
import CourseList from "@web/src/components/models/course/list/CourseList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import LookForMore from "./LookForMore";
|
import LookForMore from "./LookForMore";
|
||||||
interface GetTaxonomyProps {
|
interface GetTaxonomyProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
function useGetTaxonomy({ type }): GetTaxonomyProps {
|
function useGetTaxonomy({ type }): GetTaxonomyProps {
|
||||||
const { data, isLoading }: { data: TermDto[]; isLoading: boolean } =
|
const { data, isLoading }: { data: TermDto[]; isLoading: boolean } =
|
||||||
api.term.findMany.useQuery({
|
api.term.findMany.useQuery({
|
||||||
where: {
|
where: {
|
||||||
taxonomy: {
|
taxonomy: {
|
||||||
slug: type,
|
slug: type,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
take: 10, // 只取前10个
|
take: 10, // 只取前10个
|
||||||
});
|
});
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
const allCategories = isLoading
|
const allCategories = isLoading
|
||||||
? []
|
? []
|
||||||
: data?.map((course) => course.name);
|
: data?.map((course) => course.name);
|
||||||
return [...Array.from(new Set(allCategories))];
|
return [...Array.from(new Set(allCategories))];
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return { categories, isLoading };
|
return { categories, isLoading };
|
||||||
}
|
}
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
interface CoursesSectionProps {
|
interface CoursesSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
initialVisibleCoursesCount?: number;
|
initialVisibleCoursesCount?: number;
|
||||||
}
|
}
|
||||||
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
initialVisibleCoursesCount = 8,
|
initialVisibleCoursesCount = 8,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>("全部");
|
const [selectedCategory, setSelectedCategory] = useState<string>("全部");
|
||||||
const gateGory: GetTaxonomyProps = useGetTaxonomy({
|
const gateGory: GetTaxonomyProps = useGetTaxonomy({
|
||||||
|
@ -52,7 +52,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
{title}
|
{title}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
type="secondary"
|
type="secondary"
|
||||||
className="text-xl font-light text-gray-600">
|
className="text-xl font-light text-gray-600">
|
||||||
|
@ -80,22 +80,22 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CourseList
|
<PostList
|
||||||
params={{
|
params={{
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: initialVisibleCoursesCount,
|
pageSize: initialVisibleCoursesCount,
|
||||||
where: {
|
where: {
|
||||||
terms: !(selectedCategory === "全部")
|
terms: !(selectedCategory === "全部")
|
||||||
? {
|
? {
|
||||||
some: {
|
some: {
|
||||||
name: selectedCategory,
|
name: selectedCategory,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
showPagination={false}
|
showPagination={false}
|
||||||
cols={4}></CourseList>
|
cols={4}></PostList>
|
||||||
<LookForMore to={"/courses"}></LookForMore>
|
<LookForMore to={"/courses"}></LookForMore>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
|
import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
|
||||||
import { Layout, Typography } from 'antd';
|
|
||||||
export function MainFooter() {
|
export function MainFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gradient-to-b from-slate-800 to-slate-900 text-secondary-200">
|
<footer className="bg-gradient-to-b from-slate-800 to-slate-900 text-secondary-200 ">
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{/* 开发组织信息 */}
|
{/* 开发组织信息 */}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
||||||
import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
import { EditFilled, PlusOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { UserMenu } from "./UserMenu/UserMenu";
|
import { UserMenu } from "./UserMenu/UserMenu";
|
||||||
import { NavigationMenu } from "./NavigationMenu";
|
import { NavigationMenu } from "./NavigationMenu";
|
||||||
import { useMainContext } from "./MainProvider";
|
import { useMainContext } from "./MainProvider";
|
||||||
const { Header } = Layout;
|
|
||||||
|
|
||||||
export function MainHeader() {
|
export function MainHeader() {
|
||||||
const { isAuthenticated, user } = useAuth();
|
const { isAuthenticated, user } = useAuth();
|
||||||
|
@ -14,69 +13,76 @@ export function MainHeader() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { searchValue, setSearchValue } = useMainContext();
|
const { searchValue, setSearchValue } = useMainContext();
|
||||||
return (
|
return (
|
||||||
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
<div className="select-none flex items-center justify-between p-4 bg-white shadow-md border-b border-gray-100 fixed w-full z-30 ">
|
||||||
<div className="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
|
<div className="flex items-center space-x-8">
|
||||||
<div className="flex items-center space-x-8">
|
<div
|
||||||
<div
|
onClick={() => navigate("/")}
|
||||||
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">
|
||||||
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">
|
烽火慕课
|
||||||
烽火慕课
|
|
||||||
</div>
|
|
||||||
<NavigationMenu />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-6">
|
|
||||||
<div className="group relative">
|
|
||||||
<Input
|
|
||||||
size="large"
|
|
||||||
prefix={
|
|
||||||
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
|
||||||
}
|
|
||||||
placeholder="搜索课程"
|
|
||||||
className="w-72 rounded-full"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
onPressEnter={(e) => {
|
|
||||||
if (
|
|
||||||
!window.location.pathname.startsWith(
|
|
||||||
"/courses/"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
navigate(`/courses/`);
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const url = id
|
|
||||||
? `/course/${id}/editor`
|
|
||||||
: "/course/editor";
|
|
||||||
navigate(url);
|
|
||||||
}}
|
|
||||||
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
|
||||||
icon={<EditFilled />}>
|
|
||||||
{id ? "编辑课程" : "创建课程"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isAuthenticated ? (
|
|
||||||
<UserMenu />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate("/login")}
|
|
||||||
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
|
||||||
icon={<UserOutlined />}>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<NavigationMenu />
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
<div className="flex items-center space-x-6">
|
||||||
|
<div className="group relative">
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
prefix={
|
||||||
|
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||||
|
}
|
||||||
|
placeholder="搜索课程"
|
||||||
|
className="w-72 rounded-full"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
|
onPressEnter={(e) => {
|
||||||
|
if (
|
||||||
|
!window.location.pathname.startsWith(
|
||||||
|
"/courses/"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigate(`/courses/`);
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const url = id
|
||||||
|
? `/course/${id}/editor`
|
||||||
|
: "/course/editor";
|
||||||
|
navigate(url);
|
||||||
|
}}
|
||||||
|
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
||||||
|
icon={<EditFilled />}>
|
||||||
|
{id ? "编辑课程" : "创建课程"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
isAuthenticated && <Button
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
window.location.href = "/path/editor";
|
||||||
|
}}
|
||||||
|
|
||||||
|
icon={<PlusOutlined></PlusOutlined>} >创建学习路径</Button>
|
||||||
|
}
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<UserMenu />
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
||||||
|
icon={<UserOutlined />}>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,13 @@ const { Content } = Layout;
|
||||||
export function MainLayout() {
|
export function MainLayout() {
|
||||||
return (
|
return (
|
||||||
<MainProvider>
|
<MainProvider>
|
||||||
<Layout className="min-h-screen bg-gray-100">
|
<div className=" min-h-screen bg-gray-100">
|
||||||
<MainHeader />
|
<MainHeader />
|
||||||
<Content className="mt-16 bg-gray-50 ">
|
<Content className=" pt-20 bg-gray-50 ">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Content>
|
</Content>
|
||||||
<MainFooter />
|
<MainFooter />
|
||||||
</Layout>
|
</div>
|
||||||
</MainProvider>
|
</MainProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useNavigate, useLocation } from "react-router-dom";
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: "home", path: "/", label: "首页" },
|
{ key: "home", path: "/", label: "首页" },
|
||||||
{ key: "courses", path: "/courses", label: "全部课程" },
|
{ key: "courses", path: "/courses", label: "全部课程" },
|
||||||
{ key: "paths", path: "/paths", label: "学习路径" },
|
{ key: "path", path: "/path", label: "学习路径" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NavigationMenu = () => {
|
export const NavigationMenu = () => {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { Card, Rate, Tag, Typography, Button } from "antd";
|
||||||
|
import {
|
||||||
|
PlayCircleOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { PostDto, TaxonomySlug } from "@nice/common";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
interface pathCardProps {
|
||||||
|
path: PostDto;
|
||||||
|
}
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
export default function PathCard({ path }: pathCardProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleClick = (path: PostDto) => {
|
||||||
|
navigate(`/path/editor/${path.id}`);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth", })
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
onClick={() => handleClick(path)}
|
||||||
|
key={path.id}
|
||||||
|
hoverable
|
||||||
|
className="group overflow-hidden rounded-xl border border-gray-200 bg-white
|
||||||
|
shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
|
||||||
|
cover={
|
||||||
|
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${path?.meta?.thumbnail})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" /> */}
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
{path?.terms?.map((term) => {
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
key={term.id}
|
||||||
|
// color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"}
|
||||||
|
color={
|
||||||
|
term?.taxonomy?.slug ===
|
||||||
|
TaxonomySlug.CATEGORY
|
||||||
|
? "blue"
|
||||||
|
: term?.taxonomy?.slug ===
|
||||||
|
TaxonomySlug.LEVEL
|
||||||
|
? "green"
|
||||||
|
: "orange"
|
||||||
|
}
|
||||||
|
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0">
|
||||||
|
{term.name}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Title
|
||||||
|
level={4}
|
||||||
|
className="mb-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left">
|
||||||
|
<button> {path.title}</button>
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-4 p-2 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
|
||||||
|
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
||||||
|
<div className="ml-2 flex items-center flex-grow">
|
||||||
|
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
||||||
|
{path?.depts?.length > 1
|
||||||
|
? `${path.depts[0].name}等`
|
||||||
|
: path?.depts?.[0]?.name}
|
||||||
|
{/* {path?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */}
|
||||||
|
{/* {path?.depts?.map((dept)=>{return dept.name})} */}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium text-gray-500">
|
||||||
|
{path?.meta?.views
|
||||||
|
? `观看次数 ${path?.meta?.views}`
|
||||||
|
: null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t border-gray-100 text-center">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)]
|
||||||
|
transform hover:translate-y-[-2px] transition-all duration-500 ease-out">
|
||||||
|
立即学习
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
|
import TermParentSelector from "@web/src/components/models/term/term-parent-selector";
|
||||||
|
|
||||||
|
export default function PathFilter() {
|
||||||
|
const { data: taxonomies } = api.taxonomy.getAll.useQuery({});
|
||||||
|
const { selectedTerms, setSelectedTerms } = useMainContext();
|
||||||
|
const handleTermChange = (slug: string, selected: string[]) => {
|
||||||
|
setSelectedTerms({
|
||||||
|
...selectedTerms,
|
||||||
|
[slug]: selected, // 更新对应 slug 的选择
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm space-y-6 h-full">
|
||||||
|
{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}
|
||||||
|
</h3>
|
||||||
|
<TermParentSelector
|
||||||
|
value={items}
|
||||||
|
slug = {tax?.slug}
|
||||||
|
className="w-70 max-h-[500px] overscroll-contain overflow-x-hidden"
|
||||||
|
onChange={(selected) =>
|
||||||
|
handleTermChange(
|
||||||
|
tax?.slug,
|
||||||
|
selected as string[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
taxonomyId={tax?.id}
|
||||||
|
></TermParentSelector>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
|
import { PostType, Prisma } from "@nice/common";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import PathCard from "./PathCard";
|
||||||
|
|
||||||
|
export function PathListContainer() {
|
||||||
|
const { searchValue, selectedTerms } = useMainContext();
|
||||||
|
const termFilters = useMemo(() => {
|
||||||
|
return Object.entries(selectedTerms)
|
||||||
|
.filter(([, terms]) => terms.length > 0)
|
||||||
|
.map(([, terms]) => terms);
|
||||||
|
}, [selectedTerms]);
|
||||||
|
const searchCondition: Prisma.StringNullableFilter = {
|
||||||
|
contains: searchValue,
|
||||||
|
mode: "insensitive" as Prisma.QueryMode, // 使用类型断言
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PostList
|
||||||
|
renderItem={(post) => <PathCard path={post}></PathCard>}
|
||||||
|
params={{
|
||||||
|
pageSize: 12,
|
||||||
|
where: {
|
||||||
|
type: PostType.PATH,
|
||||||
|
AND: termFilters.map((termFilter) => ({
|
||||||
|
terms: {
|
||||||
|
some: {
|
||||||
|
id: {
|
||||||
|
in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
OR: [
|
||||||
|
{ title: searchCondition },
|
||||||
|
{ subTitle: searchCondition },
|
||||||
|
{ content: searchCondition },
|
||||||
|
{
|
||||||
|
terms: {
|
||||||
|
some: {
|
||||||
|
name: searchCondition,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
cols={4}></PostList>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathListContainer;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import MindEditor from "@web/src/components/common/editor/MindEditor";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function PathEditorPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
return <div className="p-2">
|
||||||
|
<MindEditor id={id}></MindEditor>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import PathFilter from "../components/PathFilter";
|
||||||
|
import PathListContainer from "../components/PathListContainer";
|
||||||
|
|
||||||
|
export function PathListLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className=" flex">
|
||||||
|
<div className="w-1/6">
|
||||||
|
<PathFilter></PathFilter>
|
||||||
|
</div>
|
||||||
|
<div className="w-5/6 p-4">
|
||||||
|
<PathListContainer></PathListContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default PathListLayout;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import PathListLayout from "./layout/PathListLayout";
|
||||||
|
|
||||||
|
export default function PathPage() {
|
||||||
|
return <PathListLayout />
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
import MindEditor from "@web/src/components/common/editor/MindEditor";
|
|
||||||
|
|
||||||
export default function PathsPage() {
|
|
||||||
// return <MindEditor></MindEditor>;
|
|
||||||
return <>123</>
|
|
||||||
}
|
|
|
@ -1,27 +1,197 @@
|
||||||
import { MindElixirInstance } from "mind-elixir";
|
import { Button, Card, Empty, Form, Space, Spin, message, theme } from 'antd';
|
||||||
import { useRef, useEffect } from "react";
|
import NodeMenu from './NodeMenu';
|
||||||
import MindElixir from "mind-elixir";
|
import { useEntity, api, usePost } from '@nice/client';
|
||||||
|
import { ObjectType, postDetailSelect, PostDto, PostType, Prisma, Taxonomy } from '@nice/common';
|
||||||
|
import TermSelect from '../../models/term/term-select';
|
||||||
|
import DepartmentSelect from '../../models/department/department-select';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { MindElixirInstance } from 'mind-elixir';
|
||||||
|
import MindElixir from 'mind-elixir';
|
||||||
|
import { useTusUpload } from '@web/src/hooks/useTusUpload';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
const MIND_OPTIONS = {
|
||||||
|
direction: MindElixir.SIDE,
|
||||||
|
draggable: true,
|
||||||
|
contextMenu: true,
|
||||||
|
toolBar: true,
|
||||||
|
nodeMenu: true,
|
||||||
|
keypress: true,
|
||||||
|
locale: 'zh_CN' as const,
|
||||||
|
theme: {
|
||||||
|
name: 'Latte',
|
||||||
|
palette: [
|
||||||
|
'#dd7878',
|
||||||
|
'#ea76cb',
|
||||||
|
'#8839ef',
|
||||||
|
'#e64553',
|
||||||
|
'#fe640b',
|
||||||
|
'#df8e1d',
|
||||||
|
'#40a02b',
|
||||||
|
'#209fb5',
|
||||||
|
'#1e66f5',
|
||||||
|
'#7287fd',
|
||||||
|
],
|
||||||
|
cssVar: {
|
||||||
|
'--main-color': '#444446',
|
||||||
|
'--main-bgcolor': '#ffffff',
|
||||||
|
'--color': '#777777',
|
||||||
|
'--bgcolor': '#f6f6f6',
|
||||||
|
'--panel-color': '#444446',
|
||||||
|
'--panel-bgcolor': '#ffffff',
|
||||||
|
'--panel-border-color': '#eaeaea',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default function MindEditor({ id }: { id?: string }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
||||||
|
|
||||||
export default function MindEditor() {
|
const { data: post, isLoading }: { data: PostDto, isLoading: boolean } = api.post.findFirst.useQuery({
|
||||||
const me = useRef<MindElixirInstance>();
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
select: postDetailSelect
|
||||||
|
})
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { create, update } = usePost();
|
||||||
|
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||||||
|
type: ObjectType.COURSE,
|
||||||
|
});
|
||||||
|
const { handleFileUpload } = useTusUpload()
|
||||||
|
const [form] = Form.useForm()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const instance = new MindElixir({
|
if (post && form && instance && id) {
|
||||||
el: "#map",
|
console.log(post)
|
||||||
direction: MindElixir.SIDE,
|
instance.refresh((post as any).meta)
|
||||||
draggable: true, // default true
|
const deptIds = (post?.depts || [])?.map((dept) => dept.id);
|
||||||
contextMenu: true, // default true
|
const formData = {
|
||||||
toolBar: true, // default true
|
title: post.title,
|
||||||
nodeMenu: true, // default true
|
deptIds: deptIds,
|
||||||
keypress: true, // default true
|
};
|
||||||
locale: "zh_CN",
|
post.terms?.forEach((term) => {
|
||||||
|
formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name
|
||||||
|
});
|
||||||
|
form.setFieldsValue(formData);
|
||||||
|
}
|
||||||
|
}, [post, form, instance, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const mind = new MindElixir({
|
||||||
|
...MIND_OPTIONS,
|
||||||
|
el: containerRef.current,
|
||||||
});
|
});
|
||||||
// instance.install(NodeMenu);
|
mind.init(MindElixir.new('新学习路径'));
|
||||||
instance.init(MindElixir.new("新主题"));
|
containerRef.current.hidden = true;
|
||||||
me.current = instance;
|
setInstance(mind);
|
||||||
}, []);
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if ((!id || post) && instance) {
|
||||||
|
containerRef.current.hidden = false
|
||||||
|
instance.toCenter()
|
||||||
|
instance.refresh((post as any)?.meta)
|
||||||
|
}
|
||||||
|
}, [id, post, instance])
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!instance) return;
|
||||||
|
const values = form.getFieldsValue()
|
||||||
|
const imgBlob = await instance?.exportPng()
|
||||||
|
handleFileUpload(imgBlob, async (result) => {
|
||||||
|
const termIds = taxonomies.map((tax) => values[tax.id]).filter((id) => id);
|
||||||
|
const deptIds = (values?.deptIds || []) as string[];
|
||||||
|
const { theme, ...data } = instance.getData();
|
||||||
|
try {
|
||||||
|
if (post && id) {
|
||||||
|
const params: Prisma.PostUpdateArgs = {
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: data.nodeData.topic,
|
||||||
|
meta: { ...data, thumbnail: result.compressedUrl },
|
||||||
|
terms: {
|
||||||
|
set: termIds.map((id) => ({ id }))
|
||||||
|
},
|
||||||
|
depts: {
|
||||||
|
set: deptIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await update.mutateAsync(params);
|
||||||
|
toast.success('更新成功');
|
||||||
|
} else {
|
||||||
|
const params: Prisma.PostCreateInput = {
|
||||||
|
type: PostType.PATH,
|
||||||
|
title: data.nodeData.topic,
|
||||||
|
meta: { ...data, thumbnail: result.compressedUrl },
|
||||||
|
terms: {
|
||||||
|
connect: termIds.map((id) => ({ id }))
|
||||||
|
},
|
||||||
|
depts: {
|
||||||
|
connect: deptIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await create.mutateAsync({ data: params });
|
||||||
|
navigate(`/path/editor/${res.id}`, { replace: true })
|
||||||
|
toast.success('创建成功');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('保存失败');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.log(result)
|
||||||
|
}, (error) => { }, `mind-thumb-${new Date().toString()}`)
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className=' flex flex-col border rounded-lg overflow-hidden'>
|
||||||
<div id="map" style={{ width: "100%" }} />
|
{taxonomies && (
|
||||||
|
<Form onFinish={(values) => {
|
||||||
|
console.log(values)
|
||||||
|
}} form={form} className=' bg-white p-2 '>
|
||||||
|
<div className='flex items-center justify-between gap-4'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
{taxonomies.map((tax, index) => (
|
||||||
|
<Form.Item
|
||||||
|
key={tax.id}
|
||||||
|
name={tax.id}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<TermSelect
|
||||||
|
className=' w-48'
|
||||||
|
placeholder={`请选择${tax.name}`}
|
||||||
|
taxonomyId={tax.id}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
))}
|
||||||
|
<Form.Item name="deptIds" noStyle>
|
||||||
|
<DepartmentSelect className='w-96' placeholder='请选择制作单位' multiple />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Button ghost type='primary' onClick={handleSave} >{id ? '更新' : '保存'}</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className='mind-editor'
|
||||||
|
/>
|
||||||
|
{instance && (<NodeMenu mind={instance} />
|
||||||
|
)}
|
||||||
|
{isLoading && <div className='py-64 justify-center flex' style={{ height: "calc(100vh - 287px)" }}>
|
||||||
|
<Spin size='large'></Spin>
|
||||||
|
</div>}
|
||||||
|
{!post && id && !isLoading && <div className='py-64' style={{ height: "calc(100vh - 287px)" }}>
|
||||||
|
<Empty></Empty>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Input, Button, ColorPicker, Select } from 'antd';
|
||||||
|
import {
|
||||||
|
FontSizeOutlined,
|
||||||
|
BoldOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { MindElixirInstance, NodeObj } from 'mind-elixir';
|
||||||
|
|
||||||
|
const xmindColorPresets = [
|
||||||
|
// 经典16色
|
||||||
|
'#FFFFFF', '#F5F5F5', // 白色系
|
||||||
|
'#2196F3', '#1976D2', // 蓝色系
|
||||||
|
'#4CAF50', '#388E3C', // 绿色系
|
||||||
|
'#FF9800', '#F57C00', // 橙色系
|
||||||
|
'#F44336', '#D32F2F', // 红色系
|
||||||
|
'#9C27B0', '#7B1FA2', // 紫色系
|
||||||
|
'#424242', '#757575', // 灰色系
|
||||||
|
'#FFEB3B', '#FBC02D' // 黄色系
|
||||||
|
];
|
||||||
|
|
||||||
|
interface NodeMenuProps {
|
||||||
|
mind: MindElixirInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selectedFontColor, setSelectedFontColor] = useState<string>('');
|
||||||
|
const [selectedBgColor, setSelectedBgColor] = useState<string>('');
|
||||||
|
const [selectedSize, setSelectedSize] = useState<string>('');
|
||||||
|
const [isBold, setIsBold] = useState(false);
|
||||||
|
const [url, setUrl] = useState<string>('');
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSelectNode = (nodeObj: NodeObj) => {
|
||||||
|
setIsOpen(true);
|
||||||
|
|
||||||
|
const style = nodeObj.style || {};
|
||||||
|
setSelectedFontColor(style.color || '');
|
||||||
|
setSelectedBgColor(style.background || '');
|
||||||
|
|
||||||
|
setSelectedSize(style.fontSize || '24');
|
||||||
|
setIsBold(style.fontWeight === 'bold');
|
||||||
|
setUrl(nodeObj.hyperLink || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnselectNode = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
mind.bus.addListener('selectNode', handleSelectNode);
|
||||||
|
mind.bus.addListener('unselectNode', handleUnselectNode);
|
||||||
|
|
||||||
|
}, [mind]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current && mind.container) {
|
||||||
|
mind.container.appendChild(containerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [mind.container]);
|
||||||
|
|
||||||
|
const handleColorChange = (type: "font" | "background", color: string) => {
|
||||||
|
if (type === 'font') {
|
||||||
|
setSelectedFontColor(color);
|
||||||
|
} else {
|
||||||
|
setSelectedBgColor(color);
|
||||||
|
}
|
||||||
|
const patch = { style: {} as any };
|
||||||
|
if (type === 'font') {
|
||||||
|
patch.style.color = color;
|
||||||
|
} else {
|
||||||
|
patch.style.background = color;
|
||||||
|
}
|
||||||
|
mind.reshapeNode(mind.currentNode, patch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: string) => {
|
||||||
|
setSelectedSize(size);
|
||||||
|
mind.reshapeNode(mind.currentNode, { style: { fontSize: size } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBoldToggle = () => {
|
||||||
|
const fontWeight = isBold ? '' : 'bold';
|
||||||
|
setIsBold(!isBold);
|
||||||
|
mind.reshapeNode(mind.currentNode, { style: { fontWeight } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setUrl(value);
|
||||||
|
mind.reshapeNode(mind.currentNode, { hyperLink: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`node-menu-container absolute right-2 top-2 rounded-lg bg-slate-200 shadow-xl ring-2 ring-white transition-all duration-300 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<div className="p-5 space-y-6">
|
||||||
|
{/* Font Size Selector */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600">文字样式</h3>
|
||||||
|
<div className="flex gap-3 items-center justify-between">
|
||||||
|
<Select
|
||||||
|
value={selectedSize}
|
||||||
|
onChange={handleSizeChange}
|
||||||
|
prefix={<FontSizeOutlined className='mr-2' />}
|
||||||
|
className="w-1/2"
|
||||||
|
options={[
|
||||||
|
{ value: '12', label: '12' },
|
||||||
|
{ value: '14', label: '14' },
|
||||||
|
{ value: '16', label: '16' },
|
||||||
|
{ value: '18', label: '18' },
|
||||||
|
{ value: '20', label: '20' },
|
||||||
|
{ value: '24', label: '24' },
|
||||||
|
{ value: '28', label: '28' },
|
||||||
|
{ value: '32', label: '32' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type={isBold ? "primary" : "default"}
|
||||||
|
onClick={handleBoldToggle}
|
||||||
|
className='w-1/2'
|
||||||
|
icon={<BoldOutlined />}
|
||||||
|
>
|
||||||
|
|
||||||
|
加粗
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color Picker */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600">颜色设置</h3>
|
||||||
|
|
||||||
|
{/* Font Color Picker */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xs font-medium text-gray-500">文字颜色</h4>
|
||||||
|
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
|
||||||
|
{xmindColorPresets.map((color) => (
|
||||||
|
<div
|
||||||
|
key={`font-${color}`}
|
||||||
|
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${selectedFontColor === color ? 'outline-blue-500' : 'outline-transparent'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
handleColorChange('font', color);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Background Color Picker */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xs font-medium text-gray-500">背景颜色</h4>
|
||||||
|
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
|
||||||
|
{xmindColorPresets.map((color) => (
|
||||||
|
<div
|
||||||
|
key={`bg-${color}`}
|
||||||
|
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${selectedBgColor === color ? 'outline-blue-500' : 'outline-transparent'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={() => {
|
||||||
|
handleColorChange('background', color);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-sm font-medium text-gray-600">关联链接</h3>
|
||||||
|
{/* URL Input */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Input
|
||||||
|
placeholder="例如:https://example.com"
|
||||||
|
value={url}
|
||||||
|
onChange={handleUrlChange}
|
||||||
|
addonBefore={<LinkOutlined />}
|
||||||
|
/>
|
||||||
|
{url && !/^https?:\/\/\S+$/.test(url) && (
|
||||||
|
<p className="text-xs text-red-500">请输入有效的URL地址</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeMenu;
|
|
@ -0,0 +1,152 @@
|
||||||
|
interface I18n {
|
||||||
|
addChild: string
|
||||||
|
addParent: string
|
||||||
|
addSibling: string
|
||||||
|
removeNode: string
|
||||||
|
focus: string
|
||||||
|
cancelFocus: string
|
||||||
|
moveUp: string
|
||||||
|
moveDown: string
|
||||||
|
link: string
|
||||||
|
clickTips: string
|
||||||
|
font: string
|
||||||
|
background: string
|
||||||
|
tag: string
|
||||||
|
icon: string
|
||||||
|
tagsSeparate: string
|
||||||
|
iconsSeparate: string
|
||||||
|
url: string
|
||||||
|
memo?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const cn: I18n = {
|
||||||
|
addChild: '插入子节点',
|
||||||
|
addParent: '插入父节点',
|
||||||
|
addSibling: '插入同级节点',
|
||||||
|
removeNode: '删除节点',
|
||||||
|
focus: '专注',
|
||||||
|
cancelFocus: '取消专注',
|
||||||
|
moveUp: '上移',
|
||||||
|
moveDown: '下移',
|
||||||
|
link: '连接',
|
||||||
|
clickTips: '请点击目标节点',
|
||||||
|
font: '文字',
|
||||||
|
background: '背景',
|
||||||
|
tag: '标签',
|
||||||
|
icon: '图标',
|
||||||
|
tagsSeparate: '多个标签半角逗号分隔',
|
||||||
|
iconsSeparate: '多个图标半角逗号分隔',
|
||||||
|
url: 'URL',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface I18nCollection {
|
||||||
|
cn: I18n
|
||||||
|
zh_CN: I18n
|
||||||
|
zh_TW: I18n
|
||||||
|
en: I18n
|
||||||
|
ru: I18n
|
||||||
|
ja: I18n
|
||||||
|
pt: I18n
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n: I18nCollection = {
|
||||||
|
cn,
|
||||||
|
zh_CN: cn,
|
||||||
|
zh_TW: {
|
||||||
|
addChild: '插入子節點',
|
||||||
|
addParent: '插入父節點',
|
||||||
|
addSibling: '插入同級節點',
|
||||||
|
removeNode: '刪除節點',
|
||||||
|
focus: '專注',
|
||||||
|
cancelFocus: '取消專注',
|
||||||
|
moveUp: '上移',
|
||||||
|
moveDown: '下移',
|
||||||
|
link: '連接',
|
||||||
|
clickTips: '請點擊目標節點',
|
||||||
|
font: '文字',
|
||||||
|
background: '背景',
|
||||||
|
tag: '標簽',
|
||||||
|
icon: '圖標',
|
||||||
|
tagsSeparate: '多個標簽半角逗號分隔',
|
||||||
|
iconsSeparate: '多個圖標半角逗號分隔',
|
||||||
|
url: 'URL',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
addChild: 'Add child',
|
||||||
|
addParent: 'Add parent',
|
||||||
|
addSibling: 'Add sibling',
|
||||||
|
removeNode: 'Remove node',
|
||||||
|
focus: 'Focus Mode',
|
||||||
|
cancelFocus: 'Cancel Focus Mode',
|
||||||
|
moveUp: 'Move up',
|
||||||
|
moveDown: 'Move down',
|
||||||
|
link: 'Link',
|
||||||
|
clickTips: 'Please click the target node',
|
||||||
|
font: 'Font',
|
||||||
|
background: 'Background',
|
||||||
|
tag: 'Tag',
|
||||||
|
icon: 'Icon',
|
||||||
|
tagsSeparate: 'Separate tags by comma',
|
||||||
|
iconsSeparate: 'Separate icons by comma',
|
||||||
|
url: 'URL',
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
addChild: 'Добавить дочерний элемент',
|
||||||
|
addParent: 'Добавить родительский элемент',
|
||||||
|
addSibling: 'Добавить на этом уровне',
|
||||||
|
removeNode: 'Удалить узел',
|
||||||
|
focus: 'Режим фокусировки',
|
||||||
|
cancelFocus: 'Отменить режим фокусировки',
|
||||||
|
moveUp: 'Поднять выше',
|
||||||
|
moveDown: 'Опустить ниже',
|
||||||
|
link: 'Ссылка',
|
||||||
|
clickTips: 'Пожалуйста, нажмите на целевой узел',
|
||||||
|
font: 'Цвет шрифта',
|
||||||
|
background: 'Цвет фона',
|
||||||
|
tag: 'Тег',
|
||||||
|
icon: 'Иконка',
|
||||||
|
tagsSeparate: 'Разделяйте теги запятой',
|
||||||
|
iconsSeparate: 'Разделяйте иконки запятой',
|
||||||
|
url: 'URL',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
addChild: '子ノードを追加する',
|
||||||
|
addParent: '親ノードを追加します',
|
||||||
|
addSibling: '兄弟ノードを追加する',
|
||||||
|
removeNode: 'ノードを削除',
|
||||||
|
focus: '集中',
|
||||||
|
cancelFocus: '集中解除',
|
||||||
|
moveUp: '上へ移動',
|
||||||
|
moveDown: '下へ移動',
|
||||||
|
link: 'コネクト',
|
||||||
|
clickTips: 'ターゲットノードをクリックしてください',
|
||||||
|
font: 'フォント',
|
||||||
|
background: 'バックグラウンド',
|
||||||
|
tag: 'タグ',
|
||||||
|
icon: 'アイコン',
|
||||||
|
tagsSeparate: '複数タグはカンマ区切り',
|
||||||
|
iconsSeparate: '複数アイコンはカンマ区切り',
|
||||||
|
url: 'URL',
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
addChild: 'Adicionar item filho',
|
||||||
|
addParent: 'Adicionar item pai',
|
||||||
|
addSibling: 'Adicionar item irmao',
|
||||||
|
removeNode: 'Remover item',
|
||||||
|
focus: 'Modo Foco',
|
||||||
|
cancelFocus: 'Cancelar Modo Foco',
|
||||||
|
moveUp: 'Mover para cima',
|
||||||
|
moveDown: 'Mover para baixo',
|
||||||
|
link: 'Link',
|
||||||
|
clickTips: 'Favor clicar no item alvo',
|
||||||
|
font: 'Fonte',
|
||||||
|
background: 'Cor de fundo',
|
||||||
|
tag: 'Tag',
|
||||||
|
icon: 'Icone',
|
||||||
|
tagsSeparate: 'Separe tags por virgula',
|
||||||
|
iconsSeparate: 'Separe icones por virgula',
|
||||||
|
url: 'URL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default i18n
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { MindElixirInstance, MindElixirData } from 'mind-elixir';
|
||||||
|
import { PostType, ObjectType } from '@nice/common';
|
||||||
|
|
||||||
|
export interface MindEditorProps {
|
||||||
|
initialData?: MindElixirData;
|
||||||
|
onSave?: (data: MindElixirData) => Promise<void>;
|
||||||
|
taxonomyType?: ObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MindEditorState {
|
||||||
|
instance: MindElixirInstance | null;
|
||||||
|
isSaving: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
|
@ -40,7 +40,6 @@ export const TusUploader = ({
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
|
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
|
||||||
|
|
||||||
const handleRemoveFile = useCallback(
|
const handleRemoveFile = useCallback(
|
||||||
(fileId: string) => {
|
(fileId: string) => {
|
||||||
setCompletedFiles((prev) =>
|
setCompletedFiles((prev) =>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Pagination, Empty, Skeleton } from "antd";
|
import { Pagination, Empty, Skeleton } from "antd";
|
||||||
import CourseCard from "../../../../app/main/courses/components/CourseCard";
|
|
||||||
import { courseDetailSelect, CourseDto, Prisma } from "@nice/common";
|
import { courseDetailSelect, CourseDto, Prisma } from "@nice/common";
|
||||||
import { api } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
interface CourseListProps {
|
interface PostListProps {
|
||||||
params?: {
|
params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
@ -13,21 +12,24 @@ interface CourseListProps {
|
||||||
};
|
};
|
||||||
cols?: number;
|
cols?: number;
|
||||||
showPagination?: boolean;
|
showPagination?: boolean;
|
||||||
|
renderItem: (post: any) => React.ReactNode
|
||||||
}
|
}
|
||||||
interface CoursesPagnationProps {
|
interface PostPagnationProps {
|
||||||
data: {
|
data: {
|
||||||
items: CourseDto[];
|
items: CourseDto[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
export default function CourseList({
|
export default function PostList({
|
||||||
params,
|
params,
|
||||||
cols = 3,
|
cols = 3,
|
||||||
showPagination = true,
|
showPagination = true,
|
||||||
}: CourseListProps) {
|
renderItem
|
||||||
|
}: PostListProps) {
|
||||||
const [currentPage, setCurrentPage] = useState<number>(params?.page || 1);
|
const [currentPage, setCurrentPage] = useState<number>(params?.page || 1);
|
||||||
const { data, isLoading }: CoursesPagnationProps =
|
const { data, isLoading }: PostPagnationProps =
|
||||||
api.post.findManyWithPagination.useQuery({
|
api.post.findManyWithPagination.useQuery({
|
||||||
select: courseDetailSelect,
|
select: courseDetailSelect,
|
||||||
...params,
|
...params,
|
||||||
|
@ -40,7 +42,7 @@ export default function CourseList({
|
||||||
return 1;
|
return 1;
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading]);
|
||||||
|
|
||||||
const courses = useMemo(() => {
|
const posts = useMemo(() => {
|
||||||
if (data && !isLoading) {
|
if (data && !isLoading) {
|
||||||
return data?.items;
|
return data?.items;
|
||||||
}
|
}
|
||||||
|
@ -59,15 +61,15 @@ export default function CourseList({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{courses.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className={`grid lg:grid-cols-${cols} gap-6`}>
|
<div className={`grid lg:grid-cols-${cols} gap-6`}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton paragraph={{ rows: 5 }}></Skeleton>
|
<Skeleton paragraph={{ rows: 5 }}></Skeleton>
|
||||||
) : (
|
) : (
|
||||||
courses.map((course) => (
|
posts.map((post) => <div key={post.id}>
|
||||||
<CourseCard key={course.id} course={course} />
|
{renderItem(post)}
|
||||||
))
|
</div>)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showPagination && (
|
{showPagination && (
|
||||||
|
@ -83,7 +85,10 @@ export default function CourseList({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Empty description="暂无相关课程" />
|
<div className="py-64">
|
||||||
|
|
||||||
|
<Empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
|
@ -15,7 +15,7 @@ export function useTusUpload() {
|
||||||
>({});
|
>({});
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
|
||||||
const getFileId = (url: string) => {
|
const getFileId = (url: string) => {
|
||||||
const parts = url.split("/");
|
const parts = url.split("/");
|
||||||
const uploadIndex = parts.findIndex((part) => part === "upload");
|
const uploadIndex = parts.findIndex((part) => part === "upload");
|
||||||
|
@ -34,7 +34,7 @@ export function useTusUpload() {
|
||||||
return resUrl;
|
return resUrl;
|
||||||
};
|
};
|
||||||
const handleFileUpload = async (
|
const handleFileUpload = async (
|
||||||
file: File,
|
file: File | Blob,
|
||||||
onSuccess: (result: UploadResult) => void,
|
onSuccess: (result: UploadResult) => void,
|
||||||
onError: (error: Error) => void,
|
onError: (error: Error) => void,
|
||||||
fileKey: string // 添加文件唯一标识
|
fileKey: string // 添加文件唯一标识
|
||||||
|
@ -45,14 +45,24 @@ export function useTusUpload() {
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 如果是Blob,需要转换为File
|
||||||
|
let fileName = "uploaded-file";
|
||||||
|
if (file instanceof Blob && !(file instanceof File)) {
|
||||||
|
// 根据MIME类型设置文件扩展名
|
||||||
|
const extension = file.type.split('/')[1];
|
||||||
|
fileName = `uploaded-file.${extension}`;
|
||||||
|
}
|
||||||
|
const uploadFile = file instanceof Blob && !(file instanceof File)
|
||||||
|
? new File([file], fileName, { type: file.type })
|
||||||
|
: file as File;
|
||||||
console.log(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`);
|
console.log(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`);
|
||||||
const upload = new tus.Upload(file, {
|
const upload = new tus.Upload(uploadFile, {
|
||||||
endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`,
|
endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`,
|
||||||
retryDelays: [0, 1000, 3000, 5000],
|
retryDelays: [0, 1000, 3000, 5000],
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: file.name,
|
filename: uploadFile.name,
|
||||||
filetype: file.type,
|
filetype: uploadFile.type,
|
||||||
size: file.size as any,
|
size: uploadFile.size as any,
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
const progress = Number(
|
const progress = Number(
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
|
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
|
||||||
content: "标题 1";
|
content: "标题 1";
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
|
||||||
content: "标题 2";
|
content: "标题 2";
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
|
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
|
||||||
content: "标题 3";
|
content: "标题 3";
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
|
||||||
content: "标题 4";
|
content: "标题 4";
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
|
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
|
||||||
content: "标题 5";
|
content: "标题 5";
|
||||||
|
@ -41,11 +44,13 @@
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
|
||||||
content: "标题 6";
|
content: "标题 6";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 针对下拉菜单中的选项 */
|
/* 针对下拉菜单中的选项 */
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-item:not([data-value])::before,
|
.ql-snow .ql-picker.ql-header .ql-picker-item:not([data-value])::before,
|
||||||
.ql-snow .ql-picker.ql-header .ql-picker-label:not([data-value])::before {
|
.ql-snow .ql-picker.ql-header .ql-picker-label:not([data-value])::before {
|
||||||
content: "正文" !important;
|
content: "正文" !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-custom-dragging-class {
|
.ag-custom-dragging-class {
|
||||||
@apply border-b-2 border-blue-200;
|
@apply border-b-2 border-blue-200;
|
||||||
}
|
}
|
||||||
|
@ -76,11 +81,11 @@
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead>tr>th {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody>tr>td {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-bottom-color: transparent !important;
|
border-bottom-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
@ -117,9 +122,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
||||||
.ant-radio-button-wrapper-checked:not(
|
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before {
|
||||||
.ant-radio-button-wrapper-disabled
|
|
||||||
)::before {
|
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +135,7 @@
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap-header .ant-table-thead > tr > th {
|
.no-wrap-header .ant-table-thead>tr>th {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,17 +151,17 @@
|
||||||
/* 设置单元格边框 */
|
/* 设置单元格边框 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-table .ant-table-tbody > tr > td {
|
.custom-table .ant-table-tbody>tr>td {
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
/* 设置表格行底部边框 */
|
/* 设置表格行底部边框 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-table .ant-table-tbody > tr:last-child > td {
|
.custom-table .ant-table-tbody>tr:last-child>td {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
/* 去除最后一行的底部边框 */
|
/* 去除最后一行的底部边框 */
|
||||||
}
|
}
|
||||||
|
|
||||||
#map {
|
.mind-editor {
|
||||||
height: 600px;
|
height: calc(100vh - 285px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
|
@ -15,9 +15,10 @@ import CourseContentForm from "../components/models/course/editor/form/CourseCon
|
||||||
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
|
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
|
||||||
import { MainLayout } from "../app/main/layout/MainLayout";
|
import { MainLayout } from "../app/main/layout/MainLayout";
|
||||||
import CoursesPage from "../app/main/courses/page";
|
import CoursesPage from "../app/main/courses/page";
|
||||||
import PathsPage from "../app/main/paths/page";
|
import PathPage from "../app/main/path/page";
|
||||||
import { adminRoute } from "./admin-route";
|
import { adminRoute } from "./admin-route";
|
||||||
import { CoursePreview } from "../app/main/course/preview/page";
|
import PathEditorPage from "../app/main/path/editor/page";
|
||||||
|
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
breadcrumb?: string;
|
breadcrumb?: string;
|
||||||
|
@ -56,8 +57,17 @@ export const routes: CustomRouteObject[] = [
|
||||||
element: <HomePage />,
|
element: <HomePage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "paths",
|
path: "path",
|
||||||
element: <PathsPage></PathsPage>,
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <PathPage></PathPage>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "editor/:id?",
|
||||||
|
element: <PathEditorPage></PathEditorPage>
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "courses",
|
path: "courses",
|
||||||
|
@ -103,12 +113,7 @@ export const routes: CustomRouteObject[] = [
|
||||||
</WithAuth>
|
</WithAuth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: "setting",
|
|
||||||
// element: (
|
|
||||||
// <CourseSettingForm></CourseSettingForm>
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -100,7 +100,7 @@ server {
|
||||||
# 仅供内部使用
|
# 仅供内部使用
|
||||||
internal;
|
internal;
|
||||||
# 代理到认证服务
|
# 代理到认证服务
|
||||||
proxy_pass http://host.docker.internal:/auth/file;
|
proxy_pass http://host.docker.internal:3000/auth/file;
|
||||||
|
|
||||||
# 请求优化:不传递请求体
|
# 请求优化:不传递请求体
|
||||||
proxy_pass_request_body off;
|
proxy_pass_request_body off;
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "pnpm run --parallel dev",
|
"dev": "pnpm run --parallel dev",
|
||||||
"db:clear": "pnpm --filter common run db:clear"
|
"db:clear": "pnpm --filter common run db:clear",
|
||||||
|
"studio": "pnpm --filter common run studio"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "insiinc",
|
"author": "insiinc",
|
||||||
|
|
|
@ -9,4 +9,4 @@ export * from "./useTaxonomy"
|
||||||
export * from "./useVisitor"
|
export * from "./useVisitor"
|
||||||
export * from "./useMessage"
|
export * from "./useMessage"
|
||||||
export * from "./usePost"
|
export * from "./usePost"
|
||||||
// export * from "./useCourse"
|
export * from "./useEntity"
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getQueryKey } from "@trpc/react-query";
|
|
||||||
import { MutationResult, useEntity } from "./useEntity";
|
|
||||||
import { ObjectType } from "@nice/common";
|
|
||||||
import { api } from "../trpc";
|
|
||||||
import { CrudOperation, emitDataChange } from "../../event";
|
|
||||||
export function usePost() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const queryKey = getQueryKey(api.post);
|
|
||||||
|
|
||||||
|
import { MutationResult, useEntity } from "./useEntity";
|
||||||
|
export function usePost() {
|
||||||
return useEntity("post");
|
return useEntity("post");
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,6 @@ model Post {
|
||||||
order Float? @default(0) @map("order")
|
order Float? @default(0) @map("order")
|
||||||
duration Int?
|
duration Int?
|
||||||
rating Int? @default(0)
|
rating Int? @default(0)
|
||||||
|
|
||||||
depts Department[] @relation("post_dept")
|
depts Department[] @relation("post_dept")
|
||||||
// 索引
|
// 索引
|
||||||
// 日期时间类型字段
|
// 日期时间类型字段
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import { StaffDto } from "./staff";
|
import { StaffDto } from "./staff";
|
||||||
import { TermDto } from "./term";
|
import { TermDto } from "./term";
|
||||||
|
import { DepartmentDto } from "./department";
|
||||||
|
|
||||||
export type PostComment = {
|
export type PostComment = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -40,6 +41,8 @@ export type PostDto = Post & {
|
||||||
};
|
};
|
||||||
watchableDepts: Department[];
|
watchableDepts: Department[];
|
||||||
watchableStaffs: Staff[];
|
watchableStaffs: Staff[];
|
||||||
|
terms:TermDto[]
|
||||||
|
depts:DepartmentDto[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LectureMeta = {
|
export type LectureMeta = {
|
||||||
|
|
|
@ -9,6 +9,20 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
watchableDepts: true,
|
watchableDepts: true,
|
||||||
watchableStaffs: true,
|
watchableStaffs: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
|
terms: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
taxonomyId: true,
|
||||||
|
taxonomy: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
depts: true,
|
||||||
author: {
|
author: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -28,6 +42,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: true
|
||||||
};
|
};
|
||||||
export const postUnDetailSelect: Prisma.PostSelect = {
|
export const postUnDetailSelect: Prisma.PostSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
289
pnpm-lock.yaml
289
pnpm-lock.yaml
|
@ -284,6 +284,9 @@ importers:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^3.9.1
|
specifier: ^3.9.1
|
||||||
version: 3.10.0(react-hook-form@7.54.2(react@18.2.0))
|
version: 3.10.0(react-hook-form@7.54.2(react@18.2.0))
|
||||||
|
'@mind-elixir/node-menu':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/mind-node-menu
|
||||||
'@nice/client':
|
'@nice/client':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/client
|
version: link:../../packages/client
|
||||||
|
@ -706,6 +709,18 @@ importers:
|
||||||
specifier: ^3.5.1
|
specifier: ^3.5.1
|
||||||
version: 3.5.2(vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0))
|
version: 3.5.2(vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0))
|
||||||
|
|
||||||
|
packages/mind-node-menu:
|
||||||
|
devDependencies:
|
||||||
|
less:
|
||||||
|
specifier: ^4.1.3
|
||||||
|
version: 4.2.2
|
||||||
|
mind-elixir:
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../mind-elixir-core
|
||||||
|
vite:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.2.11(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0)
|
||||||
|
|
||||||
packages/template:
|
packages/template:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
|
@ -1460,6 +1475,12 @@ packages:
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.15.18':
|
||||||
|
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-arm@0.18.20':
|
'@esbuild/android-arm@0.18.20':
|
||||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -1622,6 +1643,12 @@ packages:
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.15.18':
|
||||||
|
resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.18.20':
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -4547,6 +4574,131 @@ packages:
|
||||||
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
esbuild-android-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
esbuild-android-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
esbuild-darwin-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
esbuild-darwin-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
esbuild-freebsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
esbuild-freebsd-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
esbuild-linux-32@0.15.18:
|
||||||
|
resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-arm@0.15.18:
|
||||||
|
resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-mips64le@0.15.18:
|
||||||
|
resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-ppc64le@0.15.18:
|
||||||
|
resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-riscv64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-linux-s390x@0.15.18:
|
||||||
|
resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
esbuild-netbsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
esbuild-openbsd-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
esbuild-sunos-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
esbuild-windows-32@0.15.18:
|
||||||
|
resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild-windows-64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild-windows-arm64@0.15.18:
|
||||||
|
resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
esbuild@0.15.18:
|
||||||
|
resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -6889,6 +7041,11 @@ packages:
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rollup@2.79.2:
|
||||||
|
resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
rollup@3.29.5:
|
rollup@3.29.5:
|
||||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
|
@ -7634,6 +7791,31 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: '>=2.6.0'
|
vite: '>=2.6.0'
|
||||||
|
|
||||||
|
vite@3.2.11:
|
||||||
|
resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '>= 14'
|
||||||
|
less: '*'
|
||||||
|
sass: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vite@4.5.9:
|
vite@4.5.9:
|
||||||
resolution: {integrity: sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==}
|
resolution: {integrity: sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
@ -8978,6 +9160,9 @@ snapshots:
|
||||||
'@esbuild/android-arm64@0.24.2':
|
'@esbuild/android-arm64@0.24.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.15.18':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm@0.18.20':
|
'@esbuild/android-arm@0.18.20':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
@ -9059,6 +9244,9 @@ snapshots:
|
||||||
'@esbuild/linux-ia32@0.24.2':
|
'@esbuild/linux-ia32@0.24.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.15.18':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.18.20':
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
@ -12330,6 +12518,91 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
esbuild-android-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-android-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-darwin-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-darwin-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-freebsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-freebsd-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-32@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-arm@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-mips64le@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-ppc64le@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-riscv64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-linux-s390x@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-netbsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-openbsd-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-sunos-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-32@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild-windows-arm64@0.15.18:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild@0.15.18:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.15.18
|
||||||
|
'@esbuild/linux-loong64': 0.15.18
|
||||||
|
esbuild-android-64: 0.15.18
|
||||||
|
esbuild-android-arm64: 0.15.18
|
||||||
|
esbuild-darwin-64: 0.15.18
|
||||||
|
esbuild-darwin-arm64: 0.15.18
|
||||||
|
esbuild-freebsd-64: 0.15.18
|
||||||
|
esbuild-freebsd-arm64: 0.15.18
|
||||||
|
esbuild-linux-32: 0.15.18
|
||||||
|
esbuild-linux-64: 0.15.18
|
||||||
|
esbuild-linux-arm: 0.15.18
|
||||||
|
esbuild-linux-arm64: 0.15.18
|
||||||
|
esbuild-linux-mips64le: 0.15.18
|
||||||
|
esbuild-linux-ppc64le: 0.15.18
|
||||||
|
esbuild-linux-riscv64: 0.15.18
|
||||||
|
esbuild-linux-s390x: 0.15.18
|
||||||
|
esbuild-netbsd-64: 0.15.18
|
||||||
|
esbuild-openbsd-64: 0.15.18
|
||||||
|
esbuild-sunos-64: 0.15.18
|
||||||
|
esbuild-windows-32: 0.15.18
|
||||||
|
esbuild-windows-64: 0.15.18
|
||||||
|
esbuild-windows-arm64: 0.15.18
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/android-arm': 0.18.20
|
'@esbuild/android-arm': 0.18.20
|
||||||
|
@ -15038,6 +15311,10 @@ snapshots:
|
||||||
glob: 11.0.0
|
glob: 11.0.0
|
||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
|
|
||||||
|
rollup@2.79.2:
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
rollup@3.29.5:
|
rollup@3.29.5:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
@ -15885,6 +16162,18 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
vite@3.2.11(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.15.18
|
||||||
|
postcss: 8.4.49
|
||||||
|
resolve: 1.22.10
|
||||||
|
rollup: 2.79.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 20.17.12
|
||||||
|
fsevents: 2.3.3
|
||||||
|
less: 4.2.2
|
||||||
|
terser: 5.37.0
|
||||||
|
|
||||||
vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0):
|
vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
|
|
Loading…
Reference in New Issue