Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
d299da29c8
|
@ -101,11 +101,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
|||
},
|
||||
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
||||
) {
|
||||
// const await db.post.findMany({
|
||||
// where: {
|
||||
// type: PostType.COURSE,
|
||||
// },
|
||||
// });
|
||||
|
||||
const { courseDetail } = args;
|
||||
// If no transaction is provided, create a new one
|
||||
if (!params.tx) {
|
||||
|
@ -128,6 +124,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
|||
) {
|
||||
args.data.authorId = params?.staff?.id;
|
||||
args.data.updatedAt = dayjs().toDate();
|
||||
|
||||
const result = await super.create(args);
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.POST,
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"@nice/iconer": "workspace:^",
|
||||
"@nice/utils": "workspace:^",
|
||||
"mind-elixir": "workspace:^",
|
||||
"@mind-elixir/node-menu": "workspace:*",
|
||||
"@nice/ui": "workspace:^",
|
||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||
"@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 { PostType, Prisma } from "@nice/common";
|
||||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import { useMemo } from "react";
|
||||
import CourseCard from "./CourseCard";
|
||||
|
||||
export function CoursesContainer() {
|
||||
const { selectedTerms, searchCondition } = useMainContext();
|
||||
|
@ -13,7 +14,8 @@ export function CoursesContainer() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<CourseList
|
||||
<PostList
|
||||
renderItem={(post) => <CourseCard course={post} edit={false}></CourseCard>}
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
|
@ -30,7 +32,7 @@ export function CoursesContainer() {
|
|||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></CourseList>
|
||||
cols={4}></PostList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ import { Typography, Skeleton } from "antd";
|
|||
import { TaxonomySlug, TermDto } from "@nice/common";
|
||||
import { api } from "@nice/client";
|
||||
import { CoursesSectionTag } from "./CoursesSectionTag";
|
||||
import CourseList from "@web/src/components/models/course/list/CourseList";
|
||||
import LookForMore from "./LookForMore";
|
||||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import CourseCard from "../../courses/components/CourseCard";
|
||||
interface GetTaxonomyProps {
|
||||
categories: string[];
|
||||
isLoading: boolean;
|
||||
|
@ -80,7 +81,8 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<CourseList
|
||||
<PostList
|
||||
renderItem={(post) => <CourseCard course={post} edit={false}></CourseCard>}
|
||||
params={{
|
||||
page: 1,
|
||||
pageSize: initialVisibleCoursesCount,
|
||||
|
@ -95,7 +97,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
|||
},
|
||||
}}
|
||||
showPagination={false}
|
||||
cols={4}></CourseList>
|
||||
cols={4}></PostList>
|
||||
<LookForMore to={"/courses"}></LookForMore>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
|
||||
import { Layout, Typography } from 'antd';
|
||||
|
||||
export function MainFooter() {
|
||||
return (
|
||||
<footer className="bg-gradient-to-b from-slate-800 to-slate-900 z-20 text-secondary-200">
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import { useContext, useState } from "react";
|
||||
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 { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { UserMenu } from "./UserMenu/UserMenu";
|
||||
import { NavigationMenu } from "./NavigationMenu";
|
||||
import { useMainContext } from "./MainProvider";
|
||||
import { useStaff } from "@nice/client";
|
||||
const { Header } = Layout;
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
|
||||
export function MainHeader() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { searchValue, setSearchValue } = useMainContext();
|
||||
const { update } = useStaff();
|
||||
|
||||
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-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||
<div className="w-full max-w-screen-3xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
|
||||
<div className="flex items-center space-x-8">
|
||||
<div
|
||||
|
@ -80,7 +82,63 @@ export function MainHeader() {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<NavigationMenu />
|
||||
</div>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={
|
||||
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
}
|
||||
placeholder="搜索课程"
|
||||
className="w-96 rounded-full"
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onPressEnter={(e) => {
|
||||
if (!window.location.pathname.startsWith("/courses/")) {
|
||||
navigate(`/courses/`);
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
{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>
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@ const { Content } = Layout;
|
|||
export function MainLayout() {
|
||||
return (
|
||||
<MainProvider>
|
||||
<Layout className="min-h-screen">
|
||||
<div className=" min-h-screen bg-gray-100">
|
||||
<MainHeader />
|
||||
<Content className="mt-16 bg-gray-50">
|
||||
<Content className=" pt-20 bg-gray-50 ">
|
||||
<Outlet />
|
||||
</Content>
|
||||
<MainFooter />
|
||||
</Layout>
|
||||
</div>
|
||||
</MainProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ export const NavigationMenu = () => {
|
|||
const baseItems = [
|
||||
{ key: "home", path: "/", label: "首页" },
|
||||
{ key: "courses", path: "/courses", label: "全部课程" },
|
||||
{ key: "paths", path: "/paths", label: "学习路径" },
|
||||
{ key: "path", path: "/path", label: "学习路径" },
|
||||
];
|
||||
if (!isAuthenticated) {
|
||||
return baseItems;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import CourseList from "@web/src/components/models/course/list/CourseList";
|
||||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
import CourseCard from "../courses/components/CourseCard";
|
||||
|
||||
export default function MyDutyPage() {
|
||||
const { user } = useAuth();
|
||||
|
@ -8,8 +9,10 @@ export default function MyDutyPage() {
|
|||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<CourseList
|
||||
edit
|
||||
<PostList
|
||||
renderItem={(post) => (
|
||||
<CourseCard edit course={post}></CourseCard>
|
||||
)}
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
|
@ -17,7 +20,7 @@ export default function MyDutyPage() {
|
|||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></CourseList>
|
||||
cols={4}></PostList>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import CourseList from "@web/src/components/models/course/list/CourseList";
|
||||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
import CourseCard from "../courses/components/CourseCard";
|
||||
|
||||
export default function MyLearningPage() {
|
||||
const { user } = useAuth();
|
||||
|
@ -8,7 +9,10 @@ export default function MyLearningPage() {
|
|||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<CourseList
|
||||
<PostList
|
||||
renderItem={(post) => (
|
||||
<CourseCard edit={false} course={post}></CourseCard>
|
||||
)}
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
|
@ -20,7 +24,7 @@ export default function MyLearningPage() {
|
|||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></CourseList>
|
||||
cols={4}></PostList>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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 { useRef, useEffect } from "react";
|
||||
import MindElixir from "mind-elixir";
|
||||
|
||||
export default function MindEditor() {
|
||||
const me = useRef<MindElixirInstance>();
|
||||
useEffect(() => {
|
||||
const instance = new MindElixir({
|
||||
el: "#map",
|
||||
import { Button, Card, Empty, Form, Space, Spin, message, theme } from 'antd';
|
||||
import NodeMenu from './NodeMenu';
|
||||
import { useEntity, api, usePost } from '@nice/client';
|
||||
import { ObjectType, postDetailSelect, PostDto, PostType, Prisma, Taxonomy } from '@nice/common';
|
||||
import TermSelect from '../../models/term/term-select';
|
||||
import DepartmentSelect from '../../models/department/department-select';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MindElixirInstance } from 'mind-elixir';
|
||||
import MindElixir from 'mind-elixir';
|
||||
import { useTusUpload } from '@web/src/hooks/useTusUpload';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
const MIND_OPTIONS = {
|
||||
direction: MindElixir.SIDE,
|
||||
draggable: true, // default true
|
||||
contextMenu: true, // default true
|
||||
toolBar: true, // default true
|
||||
nodeMenu: true, // default true
|
||||
keypress: true, // default true
|
||||
locale: "zh_CN",
|
||||
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);
|
||||
|
||||
const { data: post, isLoading }: { data: PostDto, isLoading: boolean } = api.post.findFirst.useQuery({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: postDetailSelect
|
||||
})
|
||||
const navigate = useNavigate()
|
||||
const { create, update } = usePost();
|
||||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||||
type: ObjectType.COURSE,
|
||||
});
|
||||
// instance.install(NodeMenu);
|
||||
instance.init(MindElixir.new("新主题"));
|
||||
me.current = instance;
|
||||
const { handleFileUpload } = useTusUpload()
|
||||
const [form] = Form.useForm()
|
||||
useEffect(() => {
|
||||
if (post && form && instance && id) {
|
||||
console.log(post)
|
||||
instance.refresh((post as any).meta)
|
||||
const deptIds = (post?.depts || [])?.map((dept) => dept.id);
|
||||
const formData = {
|
||||
title: post.title,
|
||||
deptIds: deptIds,
|
||||
};
|
||||
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,
|
||||
});
|
||||
mind.init(MindElixir.new('新学习路径'));
|
||||
containerRef.current.hidden = true;
|
||||
setInstance(mind);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if ((!id || post) && instance) {
|
||||
containerRef.current.hidden = false
|
||||
instance.toCenter()
|
||||
instance.refresh((post as any)?.meta)
|
||||
}
|
||||
}, [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 (
|
||||
<div>
|
||||
<div id="map" style={{ width: "100%" }} />
|
||||
<div className=' flex flex-col border rounded-lg overflow-hidden'>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 handleRemoveFile = useCallback(
|
||||
(fileId: string) => {
|
||||
setCompletedFiles((prev) =>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Pagination, Empty, Skeleton } from "antd";
|
||||
import CourseCard from "../../../../app/main/courses/components/CourseCard";
|
||||
import { courseDetailSelect, CourseDto, Prisma } from "@nice/common";
|
||||
import { api } from "@nice/client";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
interface CourseListProps {
|
||||
interface PostListProps {
|
||||
params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
@ -13,23 +12,25 @@ interface CourseListProps {
|
|||
};
|
||||
cols?: number;
|
||||
showPagination?: boolean;
|
||||
edit?: boolean;
|
||||
renderItem: (post: any) => React.ReactNode
|
||||
|
||||
}
|
||||
interface CoursesPagnationProps {
|
||||
interface PostPagnationProps {
|
||||
data: {
|
||||
items: CourseDto[];
|
||||
totalPages: number;
|
||||
};
|
||||
isLoading: boolean;
|
||||
|
||||
}
|
||||
export default function CourseList({
|
||||
export default function PostList({
|
||||
params,
|
||||
cols = 3,
|
||||
showPagination = true,
|
||||
edit = false,
|
||||
}: CourseListProps) {
|
||||
renderItem
|
||||
}: PostListProps) {
|
||||
const [currentPage, setCurrentPage] = useState<number>(params?.page || 1);
|
||||
const { data, isLoading }: CoursesPagnationProps =
|
||||
const { data, isLoading }: PostPagnationProps =
|
||||
api.post.findManyWithPagination.useQuery({
|
||||
select: courseDetailSelect,
|
||||
...params,
|
||||
|
@ -42,7 +43,7 @@ export default function CourseList({
|
|||
return 1;
|
||||
}, [data, isLoading]);
|
||||
|
||||
const courses = useMemo(() => {
|
||||
const posts = useMemo(() => {
|
||||
if (data && !isLoading) {
|
||||
return data?.items;
|
||||
}
|
||||
|
@ -65,19 +66,15 @@ export default function CourseList({
|
|||
}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{courses.length > 0 ? (
|
||||
{posts.length > 0 ? (
|
||||
<>
|
||||
<div className={`grid lg:grid-cols-${cols} gap-6`}>
|
||||
{isLoading ? (
|
||||
<Skeleton paragraph={{ rows: 5 }}></Skeleton>
|
||||
) : (
|
||||
courses.map((course) => (
|
||||
<CourseCard
|
||||
edit={edit}
|
||||
key={course.id}
|
||||
course={course}
|
||||
/>
|
||||
))
|
||||
posts.map((post) => <div key={post.id}>
|
||||
{renderItem(post)}
|
||||
</div>)
|
||||
)}
|
||||
</div>
|
||||
{showPagination && (
|
||||
|
@ -93,7 +90,10 @@ export default function CourseList({
|
|||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无相关课程" />
|
||||
<div className="py-64">
|
||||
|
||||
<Empty description="暂无数据" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
|
@ -34,7 +34,7 @@ export function useTusUpload() {
|
|||
return resUrl;
|
||||
};
|
||||
const handleFileUpload = async (
|
||||
file: File,
|
||||
file: File | Blob,
|
||||
onSuccess: (result: UploadResult) => void,
|
||||
onError: (error: Error) => void,
|
||||
fileKey: string // 添加文件唯一标识
|
||||
|
@ -45,14 +45,24 @@ export function useTusUpload() {
|
|||
setUploadError(null);
|
||||
|
||||
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`);
|
||||
const upload = new tus.Upload(file, {
|
||||
const upload = new tus.Upload(uploadFile, {
|
||||
endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`,
|
||||
retryDelays: [0, 1000, 3000, 5000],
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
size: file.size as any,
|
||||
filename: uploadFile.name,
|
||||
filetype: uploadFile.type,
|
||||
size: uploadFile.size as any,
|
||||
},
|
||||
onProgress: (bytesUploaded, bytesTotal) => {
|
||||
const progress = Number(
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
border-bottom-right-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.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 {
|
||||
content: "标题 1";
|
||||
|
@ -23,6 +24,7 @@
|
|||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
|
||||
content: "标题 2";
|
||||
}
|
||||
|
||||
.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 {
|
||||
content: "标题 3";
|
||||
|
@ -32,6 +34,7 @@
|
|||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
|
||||
content: "标题 4";
|
||||
}
|
||||
|
||||
.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 {
|
||||
content: "标题 5";
|
||||
|
@ -41,11 +44,13 @@
|
|||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
|
||||
content: "标题 6";
|
||||
}
|
||||
|
||||
/* 针对下拉菜单中的选项 */
|
||||
.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 {
|
||||
content: "正文" !important;
|
||||
}
|
||||
|
||||
.ag-custom-dragging-class {
|
||||
@apply border-b-2 border-blue-200;
|
||||
}
|
||||
|
@ -117,9 +122,7 @@
|
|||
}
|
||||
|
||||
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
||||
.ant-radio-button-wrapper-checked:not(
|
||||
.ant-radio-button-wrapper-disabled
|
||||
)::before {
|
||||
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
|
@ -158,7 +161,7 @@
|
|||
/* 去除最后一行的底部边框 */
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 600px;
|
||||
.mind-editor {
|
||||
height: calc(100vh - 285px);
|
||||
width: 100%;
|
||||
}
|
|
@ -15,8 +15,10 @@ import CourseContentForm from "../components/models/course/editor/form/CourseCon
|
|||
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
|
||||
import { MainLayout } from "../app/main/layout/MainLayout";
|
||||
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 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";
|
||||
|
@ -58,8 +60,17 @@ export const routes: CustomRouteObject[] = [
|
|||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
path: "paths",
|
||||
element: <PathsPage></PathsPage>,
|
||||
path: "path",
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PathPage></PathPage>,
|
||||
},
|
||||
{
|
||||
path: "editor/:id?",
|
||||
element: <PathEditorPage></PathEditorPage>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "courses",
|
||||
|
|
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://host.docker.internal:/auth/file;
|
||||
proxy_pass http://host.docker.internal:3000/auth/file;
|
||||
|
||||
# 请求优化:不传递请求体
|
||||
proxy_pass_request_body off;
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"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": [],
|
||||
"author": "insiinc",
|
||||
|
|
|
@ -9,4 +9,4 @@ export * from "./useTaxonomy"
|
|||
export * from "./useVisitor"
|
||||
export * from "./useMessage"
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { StaffDto } from "./staff";
|
||||
import { TermDto } from "./term";
|
||||
import { ResourceDto } from "./resource";
|
||||
import { DepartmentDto } from "./department";
|
||||
|
||||
export type PostComment = {
|
||||
id: string;
|
||||
|
@ -41,6 +42,12 @@ export type PostDto = Post & {
|
|||
};
|
||||
watchableDepts: Department[];
|
||||
watchableStaffs: Staff[];
|
||||
terms: TermDto[]
|
||||
depts: DepartmentDto[]
|
||||
meta?: {
|
||||
thumbnail?: string
|
||||
views?: number
|
||||
}
|
||||
};
|
||||
|
||||
export type LectureMeta = {
|
||||
|
|
|
@ -9,6 +9,20 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
// watchableDepts: true,
|
||||
// watchableStaffs: true,
|
||||
updatedAt: true,
|
||||
terms: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
taxonomyId: true,
|
||||
taxonomy: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
depts: true,
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -28,6 +42,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
},
|
||||
},
|
||||
},
|
||||
meta: true
|
||||
};
|
||||
export const postUnDetailSelect: Prisma.PostSelect = {
|
||||
id: true,
|
||||
|
|
289
pnpm-lock.yaml
289
pnpm-lock.yaml
|
@ -284,6 +284,9 @@ importers:
|
|||
'@hookform/resolvers':
|
||||
specifier: ^3.9.1
|
||||
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':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/client
|
||||
|
@ -706,6 +709,18 @@ importers:
|
|||
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))
|
||||
|
||||
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:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
|
@ -1460,6 +1475,12 @@ packages:
|
|||
cpu: [arm64]
|
||||
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':
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1622,6 +1643,12 @@ packages:
|
|||
cpu: [ia32]
|
||||
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':
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -4547,6 +4574,131 @@ packages:
|
|||
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -6889,6 +7041,11 @@ packages:
|
|||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
rollup@2.79.2:
|
||||
resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
rollup@3.29.5:
|
||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
|
@ -7634,6 +7791,31 @@ packages:
|
|||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
@ -8978,6 +9160,9 @@ snapshots:
|
|||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.15.18':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
|
@ -9059,6 +9244,9 @@ snapshots:
|
|||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.15.18':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
optional: true
|
||||
|
||||
|
@ -12330,6 +12518,91 @@ snapshots:
|
|||
dependencies:
|
||||
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:
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
|
@ -15038,6 +15311,10 @@ snapshots:
|
|||
glob: 11.0.0
|
||||
package-json-from-dist: 1.0.1
|
||||
|
||||
rollup@2.79.2:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
rollup@3.29.5:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
@ -15885,6 +16162,18 @@ snapshots:
|
|||
- supports-color
|
||||
- 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):
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
|
|
Loading…
Reference in New Issue