This commit is contained in:
longdayi 2025-02-26 23:18:14 +08:00
parent f98bc41f1c
commit 6da5a12ab9
32 changed files with 1268 additions and 188 deletions

View File

@ -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) {
@ -131,6 +127,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,

View File

@ -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",

View File

@ -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 { searchValue, selectedTerms } = useMainContext();
@ -16,7 +17,8 @@ export function CoursesContainer() {
};
return (
<>
<CourseList
<PostList
renderItem={(post) => <CourseCard course={post}></CourseCard>}
params={{
pageSize: 12,
where: {
@ -44,7 +46,7 @@ export function CoursesContainer() {
],
},
}}
cols={4}></CourseList>
cols={4}></PostList>
</>
);
}

View File

@ -3,7 +3,7 @@ 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 PostList from "@web/src/components/models/course/list/PostList";
import LookForMore from "./LookForMore";
interface GetTaxonomyProps {
categories: string[];
@ -80,7 +80,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
</>
)}
</div>
<CourseList
<PostList
params={{
page: 1,
pageSize: initialVisibleCoursesCount,
@ -95,7 +95,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
},
}}
showPagination={false}
cols={4}></CourseList>
cols={4}></PostList>
<LookForMore to={"/courses"}></LookForMore>
</div>
</section>

View File

@ -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 text-secondary-200 ">

View File

@ -1,12 +1,11 @@
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";
const { Header } = Layout;
export function MainHeader() {
const { isAuthenticated, user } = useAuth();
@ -14,8 +13,7 @@ export function MainHeader() {
const navigate = useNavigate();
const { searchValue, setSearchValue } = useMainContext();
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="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
<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="flex items-center space-x-8">
<div
onClick={() => navigate("/")}
@ -65,6 +63,15 @@ export function MainHeader() {
</Button>
</>
)}
{
isAuthenticated && <Button
onClick={() => {
window.location.href = "/path/editor";
}}
icon={<PlusOutlined></PlusOutlined>} ></Button>
}
{isAuthenticated ? (
<UserMenu />
) : (
@ -77,6 +84,5 @@ export function MainHeader() {
)}
</div>
</div>
</Header>
);
}

View File

@ -9,13 +9,13 @@ const { Content } = Layout;
export function MainLayout() {
return (
<MainProvider>
<Layout className="min-h-screen bg-gray-100">
<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>
);
}

View File

@ -4,7 +4,7 @@ import { useNavigate, useLocation } from "react-router-dom";
const menuItems = [
{ key: "home", path: "/", label: "首页" },
{ key: "courses", path: "/courses", label: "全部课程" },
{ key: "paths", path: "/paths", label: "学习路径" },
{ key: "path", path: "/path", label: "学习路径" },
];
export const NavigationMenu = () => {

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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>
}

View File

@ -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;

View File

@ -0,0 +1,5 @@
import PathListLayout from "./layout/PathListLayout";
export default function PathPage() {
return <PathListLayout />
}

View File

@ -1,6 +0,0 @@
import MindEditor from "@web/src/components/common/editor/MindEditor";
export default function PathsPage() {
// return <MindEditor></MindEditor>;
return <>123</>
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -40,7 +40,6 @@ export const TusUploader = ({
})) || []
);
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
const handleRemoveFile = useCallback(
(fileId: string) => {
setCompletedFiles((prev) =>

View File

@ -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,21 +12,24 @@ interface CourseListProps {
};
cols?: number;
showPagination?: 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,
}: 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,
@ -40,7 +42,7 @@ export default function CourseList({
return 1;
}, [data, isLoading]);
const courses = useMemo(() => {
const posts = useMemo(() => {
if (data && !isLoading) {
return data?.items;
}
@ -59,15 +61,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 key={course.id} course={course} />
))
posts.map((post) => <div key={post.id}>
{renderItem(post)}
</div>)
)}
</div>
{showPagination && (
@ -83,7 +85,10 @@ export default function CourseList({
)}
</>
) : (
<Empty description="暂无相关课程" />
<div className="py-64">
<Empty description="暂无数据" />
</div>
)}
</div>
);

View File

@ -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(

View File

@ -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%;
}

View File

@ -15,9 +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 { CoursePreview } from "../app/main/course/preview/page";
import PathEditorPage from "../app/main/path/editor/page";
interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
@ -56,8 +57,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",
@ -103,12 +113,7 @@ export const routes: CustomRouteObject[] = [
</WithAuth>
),
},
// {
// path: "setting",
// element: (
// <CourseSettingForm></CourseSettingForm>
// ),
// },
],
},
{

View File

@ -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;

View File

@ -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",

View File

@ -9,4 +9,4 @@ export * from "./useTaxonomy"
export * from "./useVisitor"
export * from "./useMessage"
export * from "./usePost"
// export * from "./useCourse"
export * from "./useEntity"

View File

@ -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");
}

View File

@ -201,7 +201,6 @@ model Post {
order Float? @default(0) @map("order")
duration Int?
rating Int? @default(0)
depts Department[] @relation("post_dept")
// 索引
// 日期时间类型字段

View File

@ -8,6 +8,7 @@ import {
} from "@prisma/client";
import { StaffDto } from "./staff";
import { TermDto } from "./term";
import { DepartmentDto } from "./department";
export type PostComment = {
id: string;
@ -40,6 +41,8 @@ export type PostDto = Post & {
};
watchableDepts: Department[];
watchableStaffs: Staff[];
terms:TermDto[]
depts:DepartmentDto[]
};
export type LectureMeta = {

View File

@ -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,

View File

@ -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