概述

本文将实现一个全栈笔记应用

功能如下

  • 支持tiptap无头文本编辑, 支持图片和视频上传;
  • 支持文档AI续写和提问;
  • 整合drowio/excalidraw白板;
  • 支持插入latex数学公式
  • 整合emoji-react-picker
  • 协作编程和离线协作文档
  • 笔记导出PDF/Word/Markdown
  • 博客站点静态导出

视频教程

项目初始化

1
2
3
npx create-next-app@14 yuque-clone-nextjs
# or
pnpm dlx create-next-app@14 yuque-clone-nextjs

sidebar

目录结构

1
2
3
4
5
6
7
8
- src
- app
- (dashboard)
- _components/
- sidebar.tsx
- layout.tsx
- page.tsx
- layout.tsx

安装依赖

1
2
3
4
5
6
7
8
9
# 图标库
pnpm i react-icons
# 组件库
pnpm dlx shadcn@latest init
# Which style would you like to use? › New York
# Which color would you like to use as base color? › Zinc
# Do you want to use CSS variables for colors? › no / yes
# 添加所有组件
pnpm dlx shadcn@latest add --all

带摘要的样式:

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// src/app/(dashboard)/_components/sidebar.tsx
import React from 'react';
import Image from "next/image";
import {GoChevronDown} from "react-icons/go";
import {HiOutlineBellAlert} from "react-icons/hi2";
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command"
import {BsFillBoxFill, BsJournalBookmark} from "react-icons/bs";
import Link from "next/link";
import {Input} from "@/components/ui/input";
import SidebarSearchInput from "@/app/(dashboard)/_components/sidebar-search-input";
import SidebarRoutes from "@/app/(dashboard)/_components/sidebar-routes";
import SidebarKnowledgeLibraryItem from "@/app/(dashboard)/_components/sidebar-knowledge-library-item";
import SidebarKnowledgeGroupItem from "@/app/(dashboard)/_components/sidebar-knowledge-group-item";

const Sidebar = () => {

return (
<div className={`bg-gray-200/20 h-full border-r flex flex-col overflow-y-auto shadow-sm`}>
{/*header*/}
<div className={`p-4 items-center flex justify-between`}>
<div className={`flex items-center gap-x-2`}>
{/*logo*/}
<Image
className={`rounded-md cursor-pointer`}
src={`/logo.png`}
alt={`logo`}
width={32}
height={32}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={`flex items-center`}>
<span className={`cursor-pointer text-font`}>Malog</span>
<GoChevronDown className={`cursor-pointer size-5`}/>
</div>
</TooltipTrigger>
<TooltipContent className={`bg-white text-black`}>
<Command className="p-2 rounded-lg border shadow-md md:min-w-72">
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="个人">
<CommandItem>
<Link
href={``}
className={`gap-x-2 flex items-center`}>
<Avatar className={`cursor-pointer rounded-full size-8`}>
<AvatarImage src={'/logo.png'}/>
<AvatarFallback>
{'malred'.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className={`flex flex-col justify-center`}>
<span>malred</span>
<span className={`text-xs`}>我自己</span>
</div>
</Link>
</CommandItem>
</CommandGroup>
<CommandSeparator/>
<CommandGroup heading="空间">
<CommandItem>
<Link
href={``}
className={`gap-x-2 flex items-center`}>
<div className={`flex items-center justify-center
size-7 bg-sky-400 rounded-md`}>
<BsFillBoxFill className={`text-white cursor-pointer size-5`}/>
</div>
<div className={`flex flex-col justify-center`}>
<span>malred</span>
<span className={`text-xs`}>1 成员</span>
</div>
</Link>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className={`flex gap-x-2 items-center`}>
<HiOutlineBellAlert className={`cursor-pointer size-5`}/>
<Avatar className={`cursor-pointer rounded-full size-7`}>
<AvatarImage src={'/logo.png'}/>
<AvatarFallback>
{'malred'.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
</div>
</div>
<div>
{/*search-input*/}
<SidebarSearchInput/>
</div>
<div className={`flex flex-col w-full py-4`}>
<SidebarRoutes/>
</div>
<SidebarKnowledgeLibraryItem/>
<div className={'w-full h-6'}/>
<SidebarKnowledgeGroupItem/>
</div>
)
};

export default Sidebar;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// src/app/(dashboard)/_components/sidebar-search-input.tsx
import React, {useState} from 'react';
import {Input} from "@/components/ui/input";
import {GoPlus} from "react-icons/go";

import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command"

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {FiClock} from "react-icons/fi";
import {TiPen} from "react-icons/ti";
import {BsJournalBookmark} from "react-icons/bs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {FcDocument} from "react-icons/fc";
import {LuTableProperties} from "react-icons/lu";
import {LuClipboardPenLine} from "react-icons/lu";
import {GrDocumentText} from "react-icons/gr";
import {BsClipboardData} from "react-icons/bs";
import {LuBookMarked} from "react-icons/lu";
import {FcPuzzle} from "react-icons/fc";
import {FcViewDetails} from "react-icons/fc";
import {FcImport} from "react-icons/fc";
import {RiRobot2Line} from "react-icons/ri";

const SidebarSearchInput = () => {

return (
<div className={`flex items-center gap-x-2 mx-4`}>
<Dialog>
<DialogTrigger asChild>
<Input className={`cursor-pointer h-8 bg-gray-200/40`}/>
</DialogTrigger>
<DialogContent className={`p-0`}>
<Command className="rounded-lg border shadow-md md:min-w-[450px]">
<CommandInput placeholder="Type a command or search..."/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="页面">
<CommandItem>
<FiClock/>
<span>开始</span>
</CommandItem>
<CommandItem>
<TiPen/>
<span>小记</span>
</CommandItem>
</CommandGroup>
<CommandSeparator/>
<CommandGroup heading="知识库">
<CommandItem>
<BsJournalBookmark/>
<span>知识库1</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
<div
className={`h-8 w-10 cursor-pointer border rounded-md flex items-center justify-center bg-white`}
>
<DropdownMenu>
<DropdownMenuTrigger>
<GoPlus className={`size-6`}/>
</DropdownMenuTrigger>
<DropdownMenuContent className={`py-4 px-2`}>
<DropdownMenuItem>
<GrDocumentText/>
文档
</DropdownMenuItem>
<DropdownMenuItem>
<LuTableProperties/>
{/*<FcViewDetails/>*/}
表格
</DropdownMenuItem>
<DropdownMenuItem>
<LuClipboardPenLine/>
画板
</DropdownMenuItem>
<DropdownMenuItem>
<BsClipboardData/>
数据表
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<LuBookMarked/>
知识库
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<FcPuzzle/>
从模板新建
</DropdownMenuItem>
<DropdownMenuItem>
<RiRobot2Line/>
Ai帮你写
</DropdownMenuItem>
<DropdownMenuItem>
<FcImport/>
导入
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
};

export default SidebarSearchInput;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/app/(dashboard)/_components/sidebar-routes.tsx
'use client';
import React from 'react';
import {FiClock} from "react-icons/fi";
import {TiPen} from "react-icons/ti";
import {Clock, Flower, PenLine, Sparkles} from 'lucide-react'
import SidebarItem from "@/app/(dashboard)/_components/sidebar-item";

const routes = [
{
icon: FiClock,
label: "开始",
href: '/dashboard',
},
{
icon: TiPen,
label: "小记",
href: '/dashboard/notes'
},
{
icon: Sparkles,
label: "收藏",
href: '/dashboard/collections'
},
{
icon: Flower,
label: "逛逛",
href: '/dashboard/explore'
}
]

const SidebarRoutes = () => {
return (
<div className={`flex flex-col w-full`}>
{routes.map(r => (
<SidebarItem
key={r.href}
icon={r.icon}
href={r.href}
label={r.label}
/>
))}
</div>
)
;
};

export default SidebarRoutes;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// src/app/(dashboard)/_components/sidebar-knowledge-library-item.tsx
'use client';
import React from 'react';
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
import Link from "next/link";
import {Ellipsis, GripVertical} from "lucide-react";
import {BsJournalBookmark} from "react-icons/bs";
import {useRouter} from "next/navigation";

const SidebarKnowledgeLibraryItem = () => {
const router = useRouter()

return (
<div className={`px-2`}>
<Accordion
type="single" collapsible>
<AccordionItem
onDoubleClick={() => router.push(`/`)}
value="item-1">
<AccordionTrigger
className={`py-3 hover:no-underline rounded-md bg-gray-200/40 px-4`}
>
知识库
</AccordionTrigger>
<AccordionContent className={`py-2 px-2`}>
<div className={`flex flex-col`}>
<Link
href={``}
className={`cursor-pointer group items-center flex flex-row justify-between
hover:bg-slate-300/40 p-2 rounded-md`}>
<GripVertical className={`mr-1 size-4 group-hover:inline hidden`}/>
<div className={`mr-1 size-4 block group-hover:hidden`}/>
<div className={`flex-1 flex flex-row items-center gap-x-1`}>
<BsJournalBookmark className={`size-5`}/>
<span className={``}>
知识库1
</span>
</div>
<Ellipsis className={`group-hover:block hidden`}/>
</Link>
<Link
href={``}
className={`cursor-pointer group items-center flex flex-row justify-between
hover:bg-slate-300/40 p-2 rounded-md`}>
<GripVertical className={`mr-1 size-4 group-hover:inline hidden`}/>
<div className={`mr-1 size-4 block group-hover:hidden`}/>
<div className={`flex-1 flex flex-row items-center gap-x-1`}>
<BsJournalBookmark className={`size-5`}/>
<span className={``}>
知识库2
</span>
</div>
<Ellipsis className={`group-hover:block hidden`}/>
</Link>
<Link
href={``}
className={`cursor-pointer group items-center flex flex-row justify-between
hover:bg-slate-300/40 p-2 rounded-md`}>
<GripVertical className={`mr-1 size-4 group-hover:inline hidden`}/>
<div className={`mr-1 size-4 block group-hover:hidden`}/>
<div className={`flex-1 flex flex-row items-center gap-x-1`}>
<BsJournalBookmark className={`size-5`}/>
<span className={``}>
知识库3
</span>
</div>
<Ellipsis className={`group-hover:block hidden`}/>
</Link>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

export default SidebarKnowledgeLibraryItem;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// src/app/(dashboard)/_components/sidebar-knowledge-group-item.tsx
'use client';
import React from 'react';
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
import Link from "next/link";
import {Ellipsis, GripVertical} from "lucide-react";
import {BsJournalBookmark} from "react-icons/bs";
import {useRouter} from "next/navigation";

const SidebarKnowledgeGroupItem = () => {
const router = useRouter()

return (
<div className={`px-2`}>
<Accordion
type="single" collapsible>
<AccordionItem
onDoubleClick={() => router.push(`/`)}
value="item-1">
<AccordionTrigger
className={`py-3 hover:no-underline rounded-md bg-gray-200/40 px-4`}
>
知识小组
</AccordionTrigger>
<AccordionContent className={`py-2 px-2`}>
<div className={`flex flex-col`}>
<Link
href={``}
className={`cursor-pointer group items-center flex flex-row justify-between
hover:bg-slate-300/40 p-2 rounded-md`}>
<GripVertical className={`mr-1 size-4 group-hover:inline hidden`}/>
<div className={`mr-1 size-4 block group-hover:hidden`}/>
<div className={`flex-1 flex flex-row items-center gap-x-1`}>
<BsJournalBookmark className={`size-5`}/>
<span className={``}>
知识小组1
</span>
</div>
<Ellipsis className={`group-hover:block hidden`}/>
</Link>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

export default SidebarKnowledgeGroupItem;

dashboard 开始页面

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/app/(dashboard)/dashboard/page.tsx
import React from 'react';
import {usePathnameStore} from "@/store/use-pathname-store";
import StartCards from '@/app/(dashboard)/_components/start-cards';
import NotesList from '@/app/(dashboard)/_components/notes-list';

const Page = () => {

return (
<div>
<h2 className={`p-4 font-semibold`}>开始</h2>
<StartCards/>
<h2 className={`p-4 font-semibold`}>文档</h2>
<NotesList/>
</div>
);
};

export default Page;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// src/app/(dashboard)/_components/start-hover-cards.tsx
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import {HiOutlineDocumentPlus} from "react-icons/hi2";
import {HiOutlineChevronDown} from "react-icons/hi";
import {GrDocumentText} from "react-icons/gr";
import {LuBookMarked, LuClipboardPenLine, LuTableProperties} from "react-icons/lu";
import {BsClipboardData} from "react-icons/bs";
import {FcImport, FcPuzzle} from "react-icons/fc";
import {Separator} from "@/components/ui/separator";
import {LuBookPlus} from "react-icons/lu";
import StartCardNewKnowledgeLibrary from "@/app/(dashboard)/_components/start-card-new-knowledge-library";
import {
Dialog,
DialogContent,
DialogTrigger,
} from "@/components/ui/dialog"
import StartCardTemplateDialogContent from "@/app/(dashboard)/_components/start-card-template-dialog-content";


const StartCards = () => {
return (
<div className={`p-4 pt-2 flex items-center gap-x-4`}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className={`w-[16vw] border rounded-md p-3`}>
{/*新建文档*/}
<div className={`flex items-center gap-x-2`}>
<HiOutlineDocumentPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>新建文档</span>
<span className={`text-xs text-slate-400/80`}>
文档、表格、画板、数据表
</span>
</div>
<HiOutlineChevronDown className={`ml-2 size-4`}/>
</div>
</TooltipTrigger>
<TooltipContent className={`pl-8 bg-white p-1 text-black w-[16vw] border`}>
<div
className={`mt-2 rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<GrDocumentText className={`size-4`}/>
<span>新建文档</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<LuTableProperties className={`size-4`}/>
{/*<FcViewDetails/>*/}
<span>新建表格</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<LuClipboardPenLine className={`size-4`}/>
<span>新建画板</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<BsClipboardData className={`size-4`}/>
<span>新建数据表</span>
</div>
<Separator className={`m-2`}/>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<FcImport className={`size-4`}/>
<span>新建导入</span>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/*新建知识库*/}
<StartCardNewKnowledgeLibrary/>
{/*模板中心*/}
<Dialog>
<DialogTrigger>
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>模板中心</span>
<span className={`text-xs text-slate-400/80`}>
从模板中获取灵感
</span>
</div>
</div>
</div>
</DialogTrigger>
<DialogContent className={`w-screen`}>
<StartCardTemplateDialogContent/>
</DialogContent>
</Dialog>

{/*AI帮你写*/}
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>AI帮你写</span>
<span className={`text-xs text-slate-400/80`}>
AI 助手帮你一键生成文档
</span>
</div>
</div>
</div>
</div>
);
};

export default StartCards;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/app/(dashboard)/_components/notes-list.tsx
import React from 'react';
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import IEditedNotesTable from "@/app/(dashboard)/_components/IEditedNotesTable";

const NotesList = () => {

return (
<div className={`p-4 pt-0`}>
<Tabs defaultValue="edited" className="w-full">
<TabsList>
<TabsTrigger value="edited">编辑过</TabsTrigger>
<TabsTrigger value="viewed">浏览过</TabsTrigger>
<TabsTrigger value="liked">我点赞的</TabsTrigger>
<TabsTrigger value="commented">我评论过</TabsTrigger>
</TabsList>
<TabsContent value="edited">
<IEditedNotesTable/>
</TabsContent>
<TabsContent value="viewed">
<div>
1
</div>
</TabsContent>
<TabsContent value="liked">
<div>1</div>
</TabsContent>
<TabsContent value="commented">
<div>1</div>
</TabsContent>
</Tabs>
</div>
);
};

export default NotesList;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// src/app/(dashboard)/_components/start-card-new-knowledge-library.tsx
'use client';
import React, {useState} from 'react';
import {
Dialog,
DialogContent,
DialogDescription, DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
import {LuBookPlus} from "react-icons/lu";
import {Button} from "@/components/ui/button";
import {BsJournalBookmark} from "react-icons/bs";
import {Input} from "@/components/ui/input";
import {Textarea} from "@/components/ui/textarea";
import {useRouter} from "next/navigation";

const StartCardNewKnowledgeLibrary = () => {
const router = useRouter()

const [name, setName] = useState('')
const [description, setDescription] = useState('')

return (
<Dialog>
<DialogTrigger>
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>新建知识库</span>
<span className={`text-xs text-slate-400/80`}>
使用知识库整理知识
</span>
</div>
</div>
</div>
</DialogTrigger>
<DialogContent className={`w-96`}>
<DialogHeader>
<DialogTitle>新建知识库</DialogTitle>
</DialogHeader>
<div className={`flex flex-col justify-center gap-y-2`}>
<div className={`flex gap-x-2`}>
<div className={`flex items-center justify-center size-10 p-1 border rounded-md`}>
<BsJournalBookmark className={`size-5 text-blue-700`}/>
</div>
<Input className={`h-10`}
placeholder={`知识库名称`}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Textarea
placeholder={`知识库简介选填)`}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<DialogFooter className="sm:justify-start">
<Button
// todo: form
onClick={() => {
router.push(`/username/knowledgeId`)
}}
disabled={!name}
className={`text-white font-bold hover:bg-green-700 bg-green-500 w-full`}
type="button" variant="secondary">
创建
</Button>
{/*<DialogClose asChild>*/}
{/* <Button type="button" variant="secondary">*/}
{/* Close*/}
{/* </Button>*/}
{/*</DialogClose>*/}
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default StartCardNewKnowledgeLibrary;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// src/app/(dashboard)/_components/start-card-template-dialog-content.tsx
import React from 'react';
import {Separator} from "@/components/ui/separator";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {Button} from "@/components/ui/button";
import Link from "next/link";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import {ScrollArea} from "@/components/ui/scroll-area";


const StartCardTemplateDialogContent = () => {
const __html = `<div class="TemplateDocViewer-module_content_jE3Dj">
<div class="TemplateDocViewer-module_title_lmsM-"><h1 id="article-title" class="index-module_articleTitle_VJTLJ doc-article-title">会议记录</h1></div><div class="yuque-doc-content" data-df="lake" style="position: relative;"><div><div class="ne-viewer lakex-yuque-theme-light ne-typography-classic ne-paragraph-spacing-relax ne-viewer-layout-mode-fixed" data-viewer-mode="normal" id="ua2fe"><div class="ne-viewer-header"><button type="button" class="ne-ui-exit-max-view-btn" style="background-image:url(https://gw.alipayobjects.com/zos/bmw-prod/09ca6e30-fd03-49ff-b2fb-15a2fbd8042a.svg)">返回文档</button></div><div class="ne-viewer-body"><ne-alert-hole id="u7924bb34" data-lake-id="u7924bb34"><ne-alert ne-alert-type="tips"><ne-p id="u7740cfd4" data-lake-id="u7740cfd4"><ne-text id="u26837188">参会人:@</ne-text><ne-text id="u2079b669" ne-bg-color="#F5F5F5" style="color: rgb(140, 140, 140); background-color: rgb(245, 245, 245);">提及</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p><ne-p id="ufb42cf9d" data-lake-id="ufb42cf9d"><ne-text id="udbcc9bde">会议时间:2022-01-20</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p><ne-p id="uca0821be" data-lake-id="uca0821be"><ne-text id="ua06ee8d8">会议地点:6 号会议室</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-alert></ne-alert-hole><ne-h2 id="e2CMG" data-lake-id="e2CMG"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u32e4cc08">会前材料 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u51be019a" data-lake-id="u51be019a"><ne-p id="ue8814ccf" data-lake-id="ue8814ccf"><ne-text id="ua121896c">不开没有准备的会。基于材料提前异步沟通、可以给会议带来惊人的提效。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-hole id="u8da83fda" data-lake-id="u8da83fda" class="ne-spacing-all"><ne-card data-card-name="yuque" data-card-type="block" id="tGV8e" data-event-boundary="card" class=""><div class="ne-card-container" data-alias="card"><div class="ne-yuque-doc-card-view ne-yuque-doc-" data-testid="ne-yuque-doc-card-view"><img class="ne-yuque-doc-card-view-bg" src="https://cdn.nlark.com/yuque/0/2022/jpeg/519985/1646826377486-b65b60ff-5e23-4326-b494-aff18aff657f.jpeg?x-oss-process=image%2Fquality%2Cq_10"><a href="https://www.yuque.com/templates/ye52sh/fxrz8f" target="_blank"><div class="ne-yuque-doc-detail"><div class="ne-yuque-doc-content"><img class="ne-yuque-doc-icon" src="https://cdn.nlark.com/yuque/0/2022/jpeg/519985/1646826377486-b65b60ff-5e23-4326-b494-aff18aff657f.jpeg?x-oss-process=image%2Fquality%2Cq_10"><div class="ne-yuque-doc-body"><div class="ne-yuque-doc-title" data-testid="ne-yuque-doc-title">📑 产品需求文档</div><div class="ne-yuque-doc-desc" data-testid="ne-yuque-doc-desc">变更记录记录每次修订的内容,方便追溯。版本号作者修订内容发布日期1.1...去除需求 1.0,增加需求 3.02022-01-301.0...发布 prd 1.0 需求宣讲2021-12-311. 背景介绍1.1 业务背景对本次项目的背景以及目标进行描述,让产研团队了解本需求的价值和收益。1....</div><div class="ne-yuque-doc-belong" data-testid="ne-yuque-doc-belong">官方模板(新)</div></div></div></div></a></div></div></ne-card></ne-hole><ne-h2 id="e8uyE" data-lake-id="e8uyE"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u8c4c63dc">会议议题</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u9133906a" data-lake-id="u9133906a"><ne-p id="u21572368" data-lake-id="u21572368"><ne-text id="udfecd9cf">简要记录本次会议的主要议题讨论。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="ua8475aeb" data-lake-id="ua8475aeb"><ne-text id="u7afb27c9">议题1...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="uef2a37f1" data-lake-id="uef2a37f1"><ne-text id="u3c08edbb">议题2...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u694eeca8" data-lake-id="u694eeca8"><ne-text id="uc98be337">议题3...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-h2 id="SRCky" data-lake-id="SRCky"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u826e5faf">会议结论</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u75cc57d8" data-lake-id="u75cc57d8"><ne-p id="u92d9c98d" data-lake-id="u92d9c98d"><ne-text id="uf4d42bb8">不开没有结论的会。哪怕“方案取消”或“下次再议”,也是结论的一种。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u749ba5e0" data-lake-id="u749ba5e0"><ne-text id="u05c95133">结论1...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u593d4b11" data-lake-id="u593d4b11"><ne-text id="ub13147c8">结论2...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u2d59f398" data-lake-id="u2d59f398"><ne-text id="u51291081">结论3...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-h2 id="b993v" data-lake-id="b993v"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="ub8a75315">执行计划</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="ue156e5e8" data-lake-id="ue156e5e8"><ne-p id="u657a07c1" data-lake-id="u657a07c1"><ne-text id="u571e37bd">设置后续待办任务,可使用 @人分配执行人。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-tli index-type="0"><ne-tli-i class="ne-checkbox ne-checkbox-cursor-default"><span class="ne-checkbox-inner"></span></ne-tli-i><ne-tli-c class="ne-oli-content" id="u877e3580" data-lake-id="u877e3580"><ne-text id="u9e5f650c">待办任务1 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-tli-c></ne-tli><ne-tli index-type="0"><ne-tli-i class="ne-checkbox ne-checkbox-cursor-default"><span class="ne-checkbox-inner"></span></ne-tli-i><ne-tli-c class="ne-oli-content" id="u76c48bd1" data-lake-id="u76c48bd1"><ne-text id="u5f30fd4e">待办任务2 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-tli-c></ne-tli><ne-p id="ub1dc1cf9" data-lake-id="ub1dc1cf9"><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></div><div class="ne-inner-overlay-container"></div></div>
<div style="height: 0px; overflow: hidden;">​</div></div></div></div>`

return (
<div className={`flex max-h-[80vh]`}>
<div className={`prose max-h-[80vh] overflow-auto w-[70%]`}>
<div className={``} dangerouslySetInnerHTML={{__html}}/>
</div>
<div className={`w-[30%] items-center flex flex-col gap-y-1`}>
<span className={`w-full text-left px-2 pt-2`}>模板中心</span>
<Separator className={`m-2`}/>
<Link href={`/`} className={`mb-2 w-full px-2`}>
<Button
className={`text-white w-full font-bold hover:bg-green-700 bg-green-500`}>
使用此模板
</Button>
</Link>
<Tabs className={`mb-2 w-full px-2`} defaultValue="recommend">
<TabsList>
<TabsTrigger value="recommend">推荐</TabsTrigger>
<TabsTrigger value="me">我的</TabsTrigger>
</TabsList>
<TabsContent value="recommend">
<ScrollArea className="h-[480px] rounded-md border p-4">
<div>团队协作</div>
<div>个人管理</div>
<div>基础模板</div>
</ScrollArea>
</TabsContent>
<TabsContent value="me">
</TabsContent>
</Tabs>
</div>
</div>
)
;
};

export default StartCardTemplateDialogContent;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// src/app/(dashboard)/_components/IEditedNotesTable.tsx
import React from 'react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {GoPencil} from "react-icons/go";
import {FcFile} from "react-icons/fc";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {HiEllipsisHorizontal} from "react-icons/hi2";
import {FcRating} from "react-icons/fc";
import {PiBroom} from "react-icons/pi";
import {CiShare1} from "react-icons/ci";
import Link from "next/link";

const IEditedNotesTable = () => {
return (
<Table className={`w-full`}>
<TableCaption>查看更多</TableCaption>
<TableBody className={``}>
<TableRow className={`group`}>
{/*标题*/}
<TableCell className={`w-[34vw] p-6`}>
<div className={`h-6 gap-x-2 flex items-center`}>
<FcFile className={`size-6`}/>
<span>无标题</span>
<Link href={`/username/knowledgeId/noteId`}
className={`group-hover:block hidden`}>
<GoPencil className={`size-4`}/>
</Link>
</div>
</TableCell>
{/*对应知识库*/}
<TableCell className="w-[25vw] text-gray-600/50">
malguy/myNotes
</TableCell>
{/*上次编辑时间*/}
<TableCell>
昨天
</TableCell>
{/*other*/}
<TableCell className="">
<DropdownMenu>
<DropdownMenuTrigger
className={`hover:bg-slate-200 rounded-md group-hover:block hidden`}
>
<HiEllipsisHorizontal
className={`size-8 p-2`}/>
</DropdownMenuTrigger>
<DropdownMenuTrigger
className={`hover:bg-slate-200 rounded-md group-hover:hidden block`}
>
<div
className={`size-8 p-2`}/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<FcRating/>
收藏
</DropdownMenuItem>
<DropdownMenuItem>
<PiBroom/>
移除
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<CiShare1/>
浏览器打开
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
};

export default IEditedNotesTable;

prisma & dashboard 首页样式

1
2
3
pnpm dlx prisma@5 init
pnpm dlx prisma@5 generate
pnpm dlx prisma@5 db push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

// 知识库
model Library {
id String @id @default(cuid())
name String
description String

childrenLibrary Library[] @relation("ParentChildrenLibrary")
// 级联删除
parentLibrary Library? @relation("ParentChildrenLibrary", fields: [parentLibraryId], references: [id], onDelete: Cascade)
parentLibraryId String?

createdAt DateTime @default(now())
Note Note[]
}

// 知识库分组
model Group {
id String @id @default(cuid())
name String
Note Note[]

childrenGroup Group[] @relation("ParentChildrenGroup")
// 级联删除
parentGroup Group? @relation("ParentChildrenGroup", fields: [parentGroupId], references: [id], onDelete: Cascade)
parentGroupId String?

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

// 知识库笔记
model Note {
id String @id @default(cuid())
name String
// 内容(富文本) - 类型为LongText
text String @db.LongText

libraryId String
library Library @relation(fields: [libraryId], references: [id])

childrenNote Note[] @relation("ParentChildrenNote")
// 级联删除
parentNote Note? @relation("ParentChildrenNote", fields: [parentNoteId], references: [id], onDelete: Cascade)
parentNoteId String?

groupId String?
group Group? @relation(fields: [groupId], references: [id])

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
1
2
# .env
DATABASE_URL="mysql://[username]:[passowrd]@localhost:[port]/[dbname]"
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/app/(dashboard)/dashboard/page.tsx
import React from 'react';
import {usePathnameStore} from "@/store/use-pathname-store";
import StartCards from '@/app/(dashboard)/_components/start-cards';
import NotesList from '@/app/(dashboard)/_components/notes-list';

const Page = () => {

return (
<div>
<h2 className={`p-4 font-semibold`}>开始</h2>
<StartCards/>
<h2 className={`p-4 font-semibold`}>文档</h2>
<NotesList/>
</div>
);
};

export default Page;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/app/(dashboard)/layout.tsx
import React from 'react';
import Sidebar from '@/app/(dashboard)/_components/sidebar';

export default function Layout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div>
<div className={`hidden md:flex h-full w-64 flex-col fixed inset-y-0 z-50`}>
<Sidebar/>
</div>
{/*added some style*/}
<main className={`ml-64 p-4`}>
{children}
</main>
</div>
);
};
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// src/app/(dashboard)/_components/start-hover-cards.tsx
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"

import {HiOutlineDocumentPlus} from "react-icons/hi2";
import {HiOutlineChevronDown} from "react-icons/hi";
import {DropdownMenuItem, DropdownMenuSeparator} from "@/components/ui/dropdown-menu";
import {GrDocumentText} from "react-icons/gr";
import {LuBookMarked, LuClipboardPenLine, LuTableProperties} from "react-icons/lu";
import {BsClipboardData} from "react-icons/bs";
import {FcImport, FcPuzzle} from "react-icons/fc";
import {RiRobot2Line} from "react-icons/ri";
import {Separator} from "@/components/ui/separator";
import {LuBookPlus} from "react-icons/lu";
import {Button} from '@/components/ui/button'
import StartCardNewKnowledgeLibrary from "@/app/(dashboard)/_components/start-card-new-knowledge-library";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import StartCardTemplateDialogContent from "@/app/(dashboard)/_components/start-card-template-dialog-content";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"


const StartCards = () => {
return (
<div className={`p-4 pt-2 flex items-center gap-x-4`}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className={`w-[16vw] border rounded-md p-3`}>
{/*新建文档*/}
<div className={`flex items-center gap-x-2`}>
<HiOutlineDocumentPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>新建文档</span>
<span className={`text-xs text-slate-400/80`}>
文档、表格、画板、数据表
</span>
</div>
<HiOutlineChevronDown className={`ml-2 size-4`}/>
</div>
</TooltipTrigger>
<TooltipContent className={`pl-8 bg-white p-1 text-black w-[16vw] border`}>
<div
className={`mt-2 rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<GrDocumentText className={`size-4`}/>
<span>新建文档</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<LuTableProperties className={`size-4`}/>
{/*<FcViewDetails/>*/}
<span>新建表格</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<LuClipboardPenLine className={`size-4`}/>
<span>新建画板</span>
</div>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<BsClipboardData className={`size-4`}/>
<span>新建数据表</span>
</div>
<Separator className={`m-2`}/>
<div
className={`rounded-md mx-1 py-3 cursor-pointer px-4 hover:bg-slate-300/30 flex items-center gap-x-2`}>
<FcImport className={`size-4`}/>
<span>新建导入</span>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/*新建知识库*/}
<StartCardNewKnowledgeLibrary/>
{/*模板中心*/}
<Dialog>
<DialogTrigger>
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>模板中心</span>
<span className={`text-xs text-slate-400/80`}>
从模板中获取灵感
</span>
</div>
</div>
</div>
</DialogTrigger>
<DialogContent className={`w-screen`}>
<StartCardTemplateDialogContent/>
</DialogContent>
</Dialog>

{/*AI帮你写*/}
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>AI帮你写</span>
<span className={`text-xs text-slate-400/80`}>
AI 助手帮你一键生成文档
</span>
</div>
</div>
</div>
</div>
);
};

export default StartCards;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// src/app/(dashboard)/_components/start-card-template-dialog-content.tsx
import React from 'react';
import {Separator} from "@/components/ui/separator";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {Button} from "@/components/ui/button";
import Link from "next/link";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import {ScrollArea} from "@/components/ui/scroll-area";


const StartCardTemplateDialogContent = () => {
const __html = `<div class="TemplateDocViewer-module_content_jE3Dj">
<div class="TemplateDocViewer-module_title_lmsM-"><h1 id="article-title" class="index-module_articleTitle_VJTLJ doc-article-title">会议记录</h1></div><div class="yuque-doc-content" data-df="lake" style="position: relative;"><div><div class="ne-viewer lakex-yuque-theme-light ne-typography-classic ne-paragraph-spacing-relax ne-viewer-layout-mode-fixed" data-viewer-mode="normal" id="ua2fe"><div class="ne-viewer-header"><button type="button" class="ne-ui-exit-max-view-btn" style="background-image:url(https://gw.alipayobjects.com/zos/bmw-prod/09ca6e30-fd03-49ff-b2fb-15a2fbd8042a.svg)">返回文档</button></div><div class="ne-viewer-body"><ne-alert-hole id="u7924bb34" data-lake-id="u7924bb34"><ne-alert ne-alert-type="tips"><ne-p id="u7740cfd4" data-lake-id="u7740cfd4"><ne-text id="u26837188">参会人:@</ne-text><ne-text id="u2079b669" ne-bg-color="#F5F5F5" style="color: rgb(140, 140, 140); background-color: rgb(245, 245, 245);">提及</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p><ne-p id="ufb42cf9d" data-lake-id="ufb42cf9d"><ne-text id="udbcc9bde">会议时间:2022-01-20</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p><ne-p id="uca0821be" data-lake-id="uca0821be"><ne-text id="ua06ee8d8">会议地点:6 号会议室</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-alert></ne-alert-hole><ne-h2 id="e2CMG" data-lake-id="e2CMG"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u32e4cc08">会前材料 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u51be019a" data-lake-id="u51be019a"><ne-p id="ue8814ccf" data-lake-id="ue8814ccf"><ne-text id="ua121896c">不开没有准备的会。基于材料提前异步沟通、可以给会议带来惊人的提效。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-hole id="u8da83fda" data-lake-id="u8da83fda" class="ne-spacing-all"><ne-card data-card-name="yuque" data-card-type="block" id="tGV8e" data-event-boundary="card" class=""><div class="ne-card-container" data-alias="card"><div class="ne-yuque-doc-card-view ne-yuque-doc-" data-testid="ne-yuque-doc-card-view"><img class="ne-yuque-doc-card-view-bg" src="https://cdn.nlark.com/yuque/0/2022/jpeg/519985/1646826377486-b65b60ff-5e23-4326-b494-aff18aff657f.jpeg?x-oss-process=image%2Fquality%2Cq_10"><a href="https://www.yuque.com/templates/ye52sh/fxrz8f" target="_blank"><div class="ne-yuque-doc-detail"><div class="ne-yuque-doc-content"><img class="ne-yuque-doc-icon" src="https://cdn.nlark.com/yuque/0/2022/jpeg/519985/1646826377486-b65b60ff-5e23-4326-b494-aff18aff657f.jpeg?x-oss-process=image%2Fquality%2Cq_10"><div class="ne-yuque-doc-body"><div class="ne-yuque-doc-title" data-testid="ne-yuque-doc-title">📑 产品需求文档</div><div class="ne-yuque-doc-desc" data-testid="ne-yuque-doc-desc">变更记录记录每次修订的内容,方便追溯。版本号作者修订内容发布日期1.1...去除需求 1.0,增加需求 3.02022-01-301.0...发布 prd 1.0 需求宣讲2021-12-311. 背景介绍1.1 业务背景对本次项目的背景以及目标进行描述,让产研团队了解本需求的价值和收益。1....</div><div class="ne-yuque-doc-belong" data-testid="ne-yuque-doc-belong">官方模板(新)</div></div></div></div></a></div></div></ne-card></ne-hole><ne-h2 id="e8uyE" data-lake-id="e8uyE"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u8c4c63dc">会议议题</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u9133906a" data-lake-id="u9133906a"><ne-p id="u21572368" data-lake-id="u21572368"><ne-text id="udfecd9cf">简要记录本次会议的主要议题讨论。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="ua8475aeb" data-lake-id="ua8475aeb"><ne-text id="u7afb27c9">议题1...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="uef2a37f1" data-lake-id="uef2a37f1"><ne-text id="u3c08edbb">议题2...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u694eeca8" data-lake-id="u694eeca8"><ne-text id="uc98be337">议题3...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-h2 id="SRCky" data-lake-id="SRCky"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="u826e5faf">会议结论</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="u75cc57d8" data-lake-id="u75cc57d8"><ne-p id="u92d9c98d" data-lake-id="u92d9c98d"><ne-text id="uf4d42bb8">不开没有结论的会。哪怕“方案取消”或“下次再议”,也是结论的一种。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u749ba5e0" data-lake-id="u749ba5e0"><ne-text id="u05c95133">结论1...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u593d4b11" data-lake-id="u593d4b11"><ne-text id="ub13147c8">结论2...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-uli index-type="0"><ne-uli-i><span class="ne-list-symbol"><span>●</span></span></ne-uli-i><ne-uli-c class="ne-uli-content" id="u2d59f398" data-lake-id="u2d59f398"><ne-text id="u51291081">结论3...</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-uli-c></ne-uli><ne-h2 id="b993v" data-lake-id="b993v"><ne-heading-ext></ne-heading-ext><ne-heading-content><ne-text id="ub8a75315">执行计划</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-heading-content></ne-h2><ne-quote id="ue156e5e8" data-lake-id="ue156e5e8"><ne-p id="u657a07c1" data-lake-id="u657a07c1"><ne-text id="u571e37bd">设置后续待办任务,可使用 @人分配执行人。</ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></ne-quote><ne-tli index-type="0"><ne-tli-i class="ne-checkbox ne-checkbox-cursor-default"><span class="ne-checkbox-inner"></span></ne-tli-i><ne-tli-c class="ne-oli-content" id="u877e3580" data-lake-id="u877e3580"><ne-text id="u9e5f650c">待办任务1 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-tli-c></ne-tli><ne-tli index-type="0"><ne-tli-i class="ne-checkbox ne-checkbox-cursor-default"><span class="ne-checkbox-inner"></span></ne-tli-i><ne-tli-c class="ne-oli-content" id="u76c48bd1" data-lake-id="u76c48bd1"><ne-text id="u5f30fd4e">待办任务2 </ne-text><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-tli-c></ne-tli><ne-p id="ub1dc1cf9" data-lake-id="ub1dc1cf9"><span class="ne-viewer-b-filler" ne-filler="block"><br></span></ne-p></div><div class="ne-inner-overlay-container"></div></div>
<div style="height: 0px; overflow: hidden;">​</div></div></div></div>`

return (
<div className={`flex max-h-[80vh]`}>
<div className={`prose max-h-[80vh] overflow-auto w-[70%]`}>
<div className={``} dangerouslySetInnerHTML={{__html}}/>
</div>
<div className={`w-[30%] items-center flex flex-col gap-y-1`}>
<span className={`w-full text-left px-2 pt-2`}>模板中心</span>
<Separator className={`m-2`}/>
<Link href={`/`} className={`mb-2 w-full px-2`}>
<Button
className={`text-white w-full font-bold hover:bg-green-700 bg-green-500`}>
使用此模板
</Button>
</Link>
<Tabs className={`mb-2 w-full px-2`} defaultValue="recommend">
<TabsList>
<TabsTrigger value="recommend">推荐</TabsTrigger>
<TabsTrigger value="me">我的</TabsTrigger>
</TabsList>
<TabsContent value="recommend">
<ScrollArea className="h-[480px] rounded-md border p-4">
<div>团队协作</div>
<div>个人管理</div>
<div>基础模板</div>
</ScrollArea>
</TabsContent>
<TabsContent value="me">
</TabsContent>
</Tabs>
</div>
</div>
)
;
};

export default StartCardTemplateDialogContent;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// src/app/(dashboard)/_components/start-card-new-knowledge-library.tsx
'use client';
import React, {useState} from 'react';
import {
Dialog,
DialogContent,
DialogDescription, DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
import {LuBookPlus} from "react-icons/lu";
import {Button} from "@/components/ui/button";
import {BsJournalBookmark} from "react-icons/bs";
import {Input} from "@/components/ui/input";
import {Textarea} from "@/components/ui/textarea";
import {useRouter} from "next/navigation";

const StartCardNewKnowledgeLibrary = () => {
const router = useRouter()

const [name, setName] = useState('')
const [description, setDescription] = useState('')

return (
<Dialog>
<DialogTrigger>
<div className={`w-[16vw] border rounded-md p-3`}>
<div className={`flex items-center gap-x-2`}>
<LuBookPlus className={`m-2 size-5`}/>
<div className={`flex flex-col items-start`}>
<span className={`text-sm font-semibold`}>新建知识库</span>
<span className={`text-xs text-slate-400/80`}>
使用知识库整理知识
</span>
</div>
</div>
</div>
</DialogTrigger>
<DialogContent className={`w-96`}>
<DialogHeader>
<DialogTitle>新建知识库</DialogTitle>
</DialogHeader>
<div className={`flex flex-col justify-center gap-y-2`}>
<div className={`flex gap-x-2`}>
<div className={`flex items-center justify-center size-10 p-1 border rounded-md`}>
<BsJournalBookmark className={`size-5 text-blue-700`}/>
</div>
<Input className={`h-10`}
placeholder={`知识库名称`}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<Textarea
placeholder={`知识库简介选填)`}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<DialogFooter className="sm:justify-start">
<Button
// todo: form
onClick={() => {
router.push(`/username/knowledgeId`)
}}
disabled={!name}
className={`text-white font-bold hover:bg-green-700 bg-green-500 w-full`}
type="button" variant="secondary">
创建
</Button>
{/*<DialogClose asChild>*/}
{/* <Button type="button" variant="secondary">*/}
{/* Close*/}
{/* </Button>*/}
{/*</DialogClose>*/}
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default StartCardNewKnowledgeLibrary;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/app/(dashboard)/_components/notes-list.tsx
import React from 'react';
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import IEditedNotesTable from "@/app/(dashboard)/_components/IEditedNotesTable";

const NotesList = () => {

return (
<div className={`p-4 pt-0`}>
<Tabs defaultValue="edited" className="w-full">
<TabsList>
<TabsTrigger value="edited">编辑过</TabsTrigger>
<TabsTrigger value="viewed">浏览过</TabsTrigger>
<TabsTrigger value="liked">我点赞的</TabsTrigger>
<TabsTrigger value="commented">我评论过</TabsTrigger>
</TabsList>
<TabsContent value="edited">
<IEditedNotesTable/>
</TabsContent>
<TabsContent value="viewed">
<div>
1
</div>
</TabsContent>
<TabsContent value="liked">
<div>1</div>
</TabsContent>
<TabsContent value="commented">
<div>1</div>
</TabsContent>
</Tabs>
</div>
);
};

export default NotesList;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// src/app/(dashboard)/_components/IEditedNotesTable.tsx
import React from 'react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {GoPencil} from "react-icons/go";
import {FcFile} from "react-icons/fc";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {HiEllipsisHorizontal} from "react-icons/hi2";
import {FcRating} from "react-icons/fc";
import {PiBroom} from "react-icons/pi";
import {CiShare1} from "react-icons/ci";
import Link from "next/link";

const IEditedNotesTable = () => {
return (
<Table className={`w-full`}>
<TableCaption>查看更多</TableCaption>
<TableBody className={``}>
<TableRow className={`group`}>
{/*标题*/}
<TableCell className={`w-[34vw] p-6`}>
<div className={`h-6 gap-x-2 flex items-center`}>
<FcFile className={`size-6`}/>
<span>无标题</span>
<Link href={`/username/knowledgeId/noteId`}
className={`group-hover:block hidden`}>
<GoPencil className={`size-4`}/>
</Link>
</div>
</TableCell>
{/*对应知识库*/}
<TableCell className="w-[25vw] text-gray-600/50">
malguy/myNotes
</TableCell>
{/*上次编辑时间*/}
<TableCell>
昨天
</TableCell>
{/*other*/}
<TableCell className="">
<DropdownMenu>
<DropdownMenuTrigger
className={`hover:bg-slate-200 rounded-md group-hover:block hidden`}
>
<HiEllipsisHorizontal
className={`size-8 p-2`}/>
</DropdownMenuTrigger>
<DropdownMenuTrigger
className={`hover:bg-slate-200 rounded-md group-hover:hidden block`}
>
<div
className={`size-8 p-2`}/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<FcRating/>
收藏
</DropdownMenuItem>
<DropdownMenuItem>
<PiBroom/>
移除
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<CiShare1/>
浏览器打开
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
};

export default IEditedNotesTable;

知识库首页样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}

/* Code and preformatted text styles */
code {
border: 1px solid #cccccc;
background-color: rgba(154, 117, 229, 0.79);
color: black;
font-size: 0.85rem;
padding: 0.25em 0.3em;
}

.code-block {
select {
height: auto;
border-radius: 3px;
color: white;
border: 1px solid white;
}

select, option {
background: black;
}
}

pre {
border: 1px solid #cccccc;
background: black;
border-radius: 0.5rem;
color: white;
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;

code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}

mark {
background-color: #FAF594;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
}

/* Table-specific styling */
table {
border-collapse: collapse;
margin: 0;
overflow: hidden;
table-layout: fixed;
width: 100%;
max-width: 800px;

td,
th {
border: 1px solid black;
box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative;
vertical-align: top;

> * {
margin-bottom: 0;
}
}

th {
background-color: #c7c7c7;
font-weight: bold;
text-align: left;
}

.selectedCell:after {
background: #959596;
content: "";
left: 0;
right: 0;
top: 0;
bottom: 0;
pointer-events: none;
position: absolute;
z-index: 2;
}

.column-resize-handle {
background-color: var(--primary);
bottom: -2px;
pointer-events: none;
position: absolute;
right: -2px;
top: 0;
width: 4px;
}
}

.tableWrapper {
margin: 1.5rem 0;
overflow-x: auto;
}

&.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}

/* Code styling */
.hljs-comment,
.hljs-quote {
color: #616161;
}

.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}

.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}

.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}

.hljs-title,
.hljs-section {
color: #faf594;
}

.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}

.hljs-emphasis {
font-style: italic;
}

.hljs-strong {
font-weight: 700;
}

mark {
background-color: #FAF594;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
}

blockquote {
border-left: 3px solid lightgray;
margin: 1.5rem 0;
padding-left: 1rem;
}

hr {
border: none;
border-top: 1px solid rgba(220, 220, 220, 0.89);
margin: 2rem 0;
}

/* List styles */

ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
}

ul li {
list-style-type: disc;

p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}

ol li {
/*10进制*/
list-style-type: decimal;

p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}

/* Task list specific styles */

ul[data-type="taskList"] {
list-style: none;
margin-left: 0;
padding: 0;

li {
align-items: flex-start;
display: flex;

> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}

> div {
flex: 1 1 auto;
}
}

input[type="checkbox"] {
cursor: pointer;
}

ul[data-type="taskList"] {
margin: 0;
}
}

/* Table-specific styling */

table {
border-collapse: collapse;
margin: 0;
overflow: hidden;
table-layout: fixed;
width: 100%;

td,
th {
border: 1px solid black;
box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative;
vertical-align: top;

> * {
margin-bottom: 0;
}
}

th {
background-color: #c7c7c7;
font-weight: bold;
text-align: left;
}

.selectedCell:after {
background: #959596;
content: "";
left: 0;
right: 0;
top: 0;
bottom: 0;
pointer-events: none;
position: absolute;
z-index: 2;
}

.column-resize-handle {
background-color: var(--primary);
bottom: -2px;
pointer-events: none;
position: absolute;
right: -2px;
top: 0;
width: 4px;
}
}

.tableWrapper {
margin: 1.5rem 0;
overflow-x: auto;
}

&.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}

img {
display: block;
height: auto;
margin: 1.5rem 0;
max-width: 100%;

&.ProseMirror-selectednode {
/*outline: 3px solid var(--purple);*/
outline: 3px solid var(--primary);
}
}


/* Heading styles */

h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}

h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}

h1 {
font-size: 1.4rem;
}

h2 {
font-size: 1.2rem;
}

h3 {
font-size: 1.1rem;
}

h4,
h5,
h6 {
font-size: 1rem;
}

/* Link styles */

a {
@apply text-blue-600;
cursor: pointer;

&:hover {
@apply underline;
}
}
}

.col-group {
display: flex;
flex-direction: row;

@media (max-width: 540px) {
flex-direction: column-reverse;
}
}

.main {
display: flex;
flex-direction: column;
width: 100%;
}

.sidebar {
border-left: 1px solid lightgray;
flex-grow: 0;
flex-shrink: 0;
padding: 1rem;
width: 15rem;
position: sticky;
height: 100vh;
top: 0;

@media (min-width: 800px) {
width: 20rem;
}

@media (max-width: 540px) {
border-bottom: 1px solid var(--gray-3);
border-left: unset;
width: 100%;
height: auto;
position: unset;
padding: 1.5rem;
}
}

.sidebar-options {
align-items: flex-start;
display: flex;
flex-direction: column;
height: 100%;
gap: 1rem;
position: sticky;
top: 1rem;
}

.table-of-contents {
display: flex;
flex-direction: column;
font-size: 0.875rem;
gap: 0.25rem;
overflow: auto;
text-decoration: none;

> div {
border-radius: 0.25rem;
padding-left: calc(0.875rem * (var(--level) - 1));
transition: all 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);

&:hover {
background-color: lightgray;
}
}

.empty-state {
color: var(--gray-5);
user-select: none;
}

.is-active a {
color: var(--purple);
}

.is-scrolled-over a {
color: rgba(128, 128, 128, 0.51);
}

a {
display: flex;
gap: 0.25rem;

&::before {
content: attr(data-item-index) ".";
}
}


pre {
background: white;
border-radius: 0.5rem;
color: white;
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;

code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
}

pre {
border: 1px solid #cccccc;
}
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/app/(knowledge)/[username]/[libraryId]/layout.tsx
import React from 'react';
import Sidebar from "@/app/(knowledge)/[username]/[libraryId]/_components/sidebar";
import './style.scss'

const Layout = ({children, params}: {
children: React.ReactNode;
params: {
username: string
libraryId: string
}
}) => {

return (
<div>
<div className={`hidden md:flex h-full w-64 flex-col fixed inset-y-0 z-50`}>
<Sidebar libraryId={params.libraryId}/>
</div>
<main className={`h-full bg-gray-300/30 ml-64 p-4`}>
{children}
</main>
</div>
);
};

export default Layout;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// src/app/(dashboard)/dashboard/library/[username]/[id]/page.tsx
import React from 'react';
import {BsJournalBookmark} from "react-icons/bs";
import {HiEllipsisHorizontal} from "react-icons/hi2";
import {Group, Library, Note} from "@prisma/client";
import {db} from "@/lib/db";
import Link from 'next/link';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import HomepageRenameInput from "@/app/(knowledge)/[username]/[libraryId]/_components/homepage-rename-input";
import {TiStarOutline} from "react-icons/ti";
import EditHomepage from "@/app/(knowledge)/[username]/[libraryId]/_components/edit-homepage";

import katex from 'katex';
import 'katex/dist/katex.min.css';
import hljs from "highlight.js";

// import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import python from 'highlight.js/lib/languages/python';
import java from 'highlight.js/lib/languages/java';
import csharp from 'highlight.js/lib/languages/csharp';
import cpp from 'highlight.js/lib/languages/cpp';
import ruby from 'highlight.js/lib/languages/ruby';
import php from 'highlight.js/lib/languages/php';
import go from 'highlight.js/lib/languages/go';
import rust from 'highlight.js/lib/languages/rust';
import swift from 'highlight.js/lib/languages/swift';
import kotlin from 'highlight.js/lib/languages/kotlin';
import typescript from 'highlight.js/lib/languages/typescript';
import html from 'highlight.js/lib/languages/xml'; // HTML/XML
import css from 'highlight.js/lib/languages/css';
import markdown from 'highlight.js/lib/languages/markdown';
import json from 'highlight.js/lib/languages/json';
import bash from 'highlight.js/lib/languages/bash';
import sql from 'highlight.js/lib/languages/sql';
import yaml from 'highlight.js/lib/languages/yaml';
import shell from 'highlight.js/lib/languages/shell';
import r from 'highlight.js/lib/languages/r';
import perl from 'highlight.js/lib/languages/perl';

// 注册语言
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('python', python);
hljs.registerLanguage('java', java);
hljs.registerLanguage('csharp', csharp);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('ruby', ruby);
hljs.registerLanguage('php', php);
hljs.registerLanguage('go', go);
hljs.registerLanguage('rust', rust);
hljs.registerLanguage('swift', swift);
hljs.registerLanguage('kotlin', kotlin);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('html', html);
hljs.registerLanguage('css', css);
hljs.registerLanguage('markdown', markdown);
hljs.registerLanguage('json', json);
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('shell', shell);
hljs.registerLanguage('r', r);
hljs.registerLanguage('perl', perl);

import Highlight from '@tiptap/extension-highlight'
import {common, createLowlight, all} from 'lowlight'

import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
// const lowlight = createLowlight(common)
const lowlight = createLowlight(all)

lowlight.highlight('html', '"use strict";')
lowlight.highlight('css', '"use strict";')
lowlight.highlight('js', '"use strict";')
lowlight.highlight('ts', '"use strict";')
// you can also register individual languages
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'

lowlight.register('html', html)
lowlight.register('css', css)
lowlight.register('js', js)
lowlight.register('ts', ts)

import {JSDOM} from 'jsdom'
import './style.scss'

const Page = async ({params, searchParams}: {
params: {
username: string
libraryId: string
}
searchParams: {
type: 'rename' | 'edit'
}
}) => {
// 服务端渲染时给html富文本中的code添加高亮
function renderRichTextWithHighlightServerside(richText: string) {
const dom = new JSDOM(richText);
const doc = dom.window.document;

const codeBlocks = doc.querySelectorAll('pre code');

// @ts-ignore
codeBlocks.forEach(block => {
const language = block.className.split('-')[1] || 'plaintext';
const code = block.textContent;
const highlighted = hljs.highlight(code!, {language}).value;
block.innerHTML = highlighted;
});

return doc.body.innerHTML;
}

// @ts-ignore
const library: Library & {
Note: Note[]
Group: Group[]
} = await db.library.findUnique({
where: {id: params.libraryId},
include: {
Note: true,
Group: true
}
})

return (
<div className={`bg-white mx-24 my-16 min-h-[70vh] rounded-md`}>
{searchParams.type !== 'edit' && (
<>
<div className={`flex p-8 justify-between items-center`}>
<div className={`flex gap-x-2 items-center`}>
<BsJournalBookmark className={`text-blue-500 size-8`}/>
{searchParams.type !== 'rename' ?
<text className={`font-bold text-2xl`}>{library?.name}</text> :
<HomepageRenameInput library={library}/>
}
</div>
<div className={`flex items-center gap-x-2`}>
<>
<div className={`p-2 border flex items-center rounded-md`}>
<TiStarOutline className={``}/>
<span className={`text-sm`}>收藏</span>
</div>
<div className={`p-2 border flex items-center rounded-md`}>
<span className={`text-sm`}>分享</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<HiEllipsisHorizontal className={`cursor-pointer size-6 m-1`}/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href={`/malred/${library.id}?type=rename`}>
重命名
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href={`/malred/${library.id}?type=edit`}>
编辑首页
</Link>
</DropdownMenuItem>
<DropdownMenuItem>更多设置</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
</div>
</div>
<div className={`h-full p-4 w-full`}>
<div className={`w-full prose-base md:prose-lg px-4 rounded-md`}>
<div
dangerouslySetInnerHTML={{
__html:
renderRichTextWithHighlightServerside(library.text)
}}
/>
</div>
</div>
</>)}
{searchParams.type === 'edit' && (
<EditHomepage library={library}/>
)}
</div>
)
;
};

export default Page;

修改数据库的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/app/(knowledge)/[username]/[libraryId]/actions/update-library.ts
'use server';
import {db} from "@/lib/db"

interface Props {
name?: string
text?: string
description?: string
showDir?: boolean
id: string
}

export const updateLibrary = async (values: Props) => {
const {id, ...value} = values

return db.library.update({
where: {id},
data: value
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/app/(knowledge)/[username]/[libraryId]/actions/create-note.ts
'use server';
import {db} from "@/lib/db";

export const createNote = async ({libraryId}: { libraryId: string }) => {
return db.note.create({
data: {
libraryId: libraryId,
name: `无标题文档`,
text: ``
}
})
}
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar.tsx
import {BsJournalBookmark} from "react-icons/bs";
import React from 'react';
import {db} from "@/lib/db";
import {HiEllipsisHorizontal} from "react-icons/hi2";
import {Separator} from "@/components/ui/separator";
import SidebarSearchInput from "@/app/(knowledge)/[username]/[libraryId]/_components/sidebar-search-input";
import SidebarDirList from "@/app/(knowledge)/[username]/[libraryId]/_components/sidebar-dir-list";
import {Note, Group, Library} from '@prisma/client';
import SidebarHomeItem from "@/app/(knowledge)/[username]/[libraryId]/_components/sidebar-home-item";

const Sidebar = async ({libraryId}: { libraryId: string }) => {
// @ts-ignore
const library: Library & {
Note: Note[]
Group: Group[]
} = await db.library.findUnique({
where: {id: libraryId},
include: {
Note: true,
Group: true
}
})

return (
<div className={`bg-gray-200/20 h-full border-r flex flex-col overflow-y-auto shadow-sm`}>
{/*header: icon - library name*/}
<div className={`flex p-4 justify-between items-center`}>
<div className={`flex gap-x-2 items-center`}>
<BsJournalBookmark className={`text-blue-500 size-5`}/>
<text className={`font-bold`}>{library?.name}</text>
</div>
<HiEllipsisHorizontal className={`cursor-pointer size-5`}/>
</div>
<Separator/>
<div className={`pt-4`}>
<SidebarSearchInput/>
</div>
<div className={`flex flex-col py-4 items-center gap-y-2`}>
<SidebarHomeItem libraryId={libraryId}/>
</div>
<SidebarDirList
libraryId={libraryId}
notes={library?.Note}
groups={library?.Group}
/>
</div>
);
};

export default Sidebar;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-dir-list.tsx
'use client';
import React, {useState} from 'react';
import {CiFolderOn} from "react-icons/ci";
import {IoIosArrowDown} from "react-icons/io";
import {ScrollArea} from "@/components/ui/scroll-area"
import {Note, Group} from '@prisma/client';
import {cn} from "@/lib/utils";
import {createNote} from "@/app/(knowledge)/[username]/[libraryId]/actions/create-note";
import {useRouter} from "next/navigation";

const SidebarDirList = ({libraryId, notes, groups}: {
libraryId: string
notes: Note[]
groups: Group[]
}) => {
const router = useRouter()

const [open, setOpen] = useState(false)

const onClick = async () => {
const note = await createNote({libraryId})
router.push(`/malred/${libraryId}/${note.id}`)
}

return (
<div className={`px-5`}>
<div className={`flex justify-between`}>
<div className={`flex gap-x-2`}>
<CiFolderOn className={`size-5`}/>
<span className={`text-sm`}>目录</span>
</div>
<IoIosArrowDown
onClick={() => setOpen(!open)}
className={cn(
`transition-all duration-200 size-5`,
!open && `rotate-90`
)}/>
</div>
<ScrollArea className="my-2 h-[72vh] w-full">
{open && notes.length === 0 && groups.length === 0 && (
<div className={`transition-all duration-200 w-full h-[71vh] flex justify-center items-center`}>
<div>知识库为空,你可以<span
className={`text-blue-500 underline`}
onClick={onClick}
>新建文档</span>
</div>
</div>
)}
{open && (notes.length === 0 || groups.length === 0) && (
<div>a</div>
)}
</ScrollArea>
</div>
);
};

export default SidebarDirList;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-home-item.tsx
'use client';
import React from 'react';
import {cn} from "@/lib/utils";
import {RiHome4Line} from "react-icons/ri";
import {usePathname, useRouter} from "next/navigation";

const SidebarHomeItem = ({libraryId}: { libraryId: string }) => {
const pathname = usePathname()
const isCurrent = pathname === `/malred/${libraryId}`
const router = useRouter()

return (<div
className={cn(
`w-[90%] rounded-md items-center px-2 flex hover:bg-slate-300/30 h-8 gap-x-2`,
isCurrent && `bg-slate-300/30`
)}
onClick={() => router.push(`/malred/${libraryId}`)}
>
<RiHome4Line className={`size-5`}/>
<span className={`text-sm`}>首页</span>
</div>
);
};

export default SidebarHomeItem;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-search-input.tsx
import React from 'react';
import {Dialog, DialogContent, DialogTrigger} from "@/components/ui/dialog";
import {Input} from "@/components/ui/input";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator
} from "@/components/ui/command";
import {FiClock} from "react-icons/fi";
import {TiPen} from "react-icons/ti";
import {BsClipboardData, BsJournalBookmark} from "react-icons/bs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {GoPlus} from "react-icons/go";
import {GrDocumentText} from "react-icons/gr";
import {LuBookMarked, LuClipboardPenLine, LuTableProperties} from "react-icons/lu";
import {FcImport, FcPuzzle} from "react-icons/fc";
import {RiRobot2Line} from "react-icons/ri";

const SidebarSearchInput = () => {
return (
<div className={`flex items-center gap-x-2 mx-4`}>
<Dialog>
<DialogTrigger asChild>
<Input className={`cursor-pointer h-8 bg-gray-200/40`}/>
</DialogTrigger>
<DialogContent className={`p-0`}>
<Command className="rounded-lg border shadow-md md:min-w-[450px]">
<CommandInput placeholder="Type a command or search..."/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="页面">
{/*todo: library children*/}

</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
<div
className={`h-8 w-10 cursor-pointer border rounded-md flex items-center justify-center bg-white`}
>
<DropdownMenu>
<DropdownMenuTrigger>
<GoPlus className={`size-6`}/>
</DropdownMenuTrigger>
<DropdownMenuContent className={`py-4 px-2`}>
<DropdownMenuItem>
<GrDocumentText/>
文档
</DropdownMenuItem>
<DropdownMenuItem>
<LuTableProperties/>
{/*<FcViewDetails/>*/}
表格
</DropdownMenuItem>
<DropdownMenuItem>
<LuClipboardPenLine/>
画板
</DropdownMenuItem>
<DropdownMenuItem>
<BsClipboardData/>
数据表
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<LuBookMarked/>
知识库
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem>
<FcPuzzle/>
从模板新建
</DropdownMenuItem>
<DropdownMenuItem>
<RiRobot2Line/>
Ai帮你写
</DropdownMenuItem>
<DropdownMenuItem>
<FcImport/>
导入
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
};

export default SidebarSearchInput;

homepage相关

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/app/(knowledge)/[username]/[libraryId]/_components/edit-homepage.tsx
'use client';
import React, {useState} from 'react';
import {BsJournalBookmark} from "react-icons/bs";
import HomepageRenameInput from "@/app/(knowledge)/[username]/[libraryId]/_components/homepage-rename-input";
import HomepageUpdateButtons from "@/app/(knowledge)/[username]/[libraryId]/_components/homepage-update-buttons";
import HomepageEditText from "@/app/(knowledge)/[username]/[libraryId]/_components/homepage-edit-text";
import {Library} from "@prisma/client";

const EditHomepage = ({library}: { library: Library }) => {
const [showDir, setShowDir] = useState(library.showDir)
const [text, setText] = useState(library.text)

return (
<>
<div className={`flex p-8 justify-between items-center`}>
<div className={`flex gap-x-2 items-center`}>
<BsJournalBookmark className={`text-blue-500 size-8`}/>
<text className={`font-bold text-2xl`}>{library?.name}</text>
</div>
<div className={`flex items-center gap-x-2`}>
<HomepageUpdateButtons
id={library.id}
text={text}
showDir={showDir}
setShowDir={setShowDir}
/>
</div>
</div>
<div className={`h-full p-4 w-full`}>
<HomepageEditText
text={text}
setText={setText}
/>
</div>
</>
);
};

export default EditHomepage;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-edit-text.tsx
'use client';
import React from 'react';
import {Library} from "@prisma/client";
import {RichTextEditor, Link} from '@mantine/tiptap';
import {BubbleMenu, EditorContent, FloatingMenu, useEditor} from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import {FaBold, FaItalic} from "react-icons/fa";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import '@mantine/core/styles.css';
import {TiptapExtensions} from "@/lib/constants";

const HomepageEditText = ({text, setText}: {
text: string
setText: Function
}) => {

const editor = useEditor({
// @ts-ignore
extensions: [
...TiptapExtensions
],
content: text,
onUpdate({editor}) {
setText(editor.getHTML())
},
});

if (!editor) {
return null
}

return (
<div className={`mx-4 h-full prose-lg rounded-md p-2 border`}>
<RichTextEditor className={`h-full`} editor={editor}>
{editor && (
<BubbleMenu editor={editor}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold/>
<RichTextEditor.Italic/>
<RichTextEditor.Link/>
</RichTextEditor.ControlsGroup>
</BubbleMenu>
)}
{editor && (
<FloatingMenu editor={editor}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1/>
<RichTextEditor.H2/>
<RichTextEditor.H3/>
<RichTextEditor.H4/>
<RichTextEditor.BulletList/>
<RichTextEditor.OrderedList/>
</RichTextEditor.ControlsGroup>
</FloatingMenu>
)}

<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup className={`flex items-center`}>
<Select>
<SelectTrigger className="h-8 border-none shadow-none">
{editor.isActive('heading', {level: 1}) && 'H1'}
{editor.isActive('heading', {level: 2}) && 'H2'}
{editor.isActive('heading', {level: 3}) && 'H3'}
{editor.isActive('heading', {level: 4}) && 'H4'}
{!editor.isActive('heading') && `正文`}
</SelectTrigger>
<SelectContent>
<RichTextEditor.H1/>
<RichTextEditor.H2/>
<RichTextEditor.H3/>
<RichTextEditor.H4/>
</SelectContent>
</Select>
<RichTextEditor.Bold/>
<RichTextEditor.Italic/>
<RichTextEditor.Underline/>
<RichTextEditor.Strikethrough/>
<RichTextEditor.ClearFormatting/>
<RichTextEditor.Highlight/>
<RichTextEditor.Code/>
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>

<RichTextEditor.Content/>
</RichTextEditor>
</div>
);
};

export default HomepageEditText;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-rename-input.tsx
'use client';
import React, {useState} from 'react';
import {Library} from "@prisma/client";
import {Input} from "@/components/ui/input";
import {updateLibrary} from "@/app/(knowledge)/[username]/[libraryId]/actions/update-library";
import {useRouter} from "next/navigation";

const HomepageRenameInput = ({library}: { library: Library }) => {
const router = useRouter()
const [v, setV] = useState(library.name)

return (
<Input
onBlur={async () => {
await updateLibrary({id: library.id, name: v})
router.push(`/malred/${library.id}`)
router.refresh()
}}
value={v}
className={`font-bold text-2xl`}
onChange={(e) => setV(e.target.value)}
/>
);
};

export default HomepageRenameInput;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// src/app/(knowledge)/[username]/[libraryId]/_components/sidebar-update-buttons.tsx
'use client';
import React, {useState} from 'react';
import {TiStarOutline} from "react-icons/ti";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {BsSliders} from "react-icons/bs";
import {Switch} from "@/components/ui/switch"
import {updateLibrary} from "@/app/(knowledge)/[username]/[libraryId]/actions/update-library";
import {useRouter} from "next/navigation";

const HomepageUpdateButtons = ({id, showDir, setShowDir, text}: {
id: string
text: string
showDir: boolean;
setShowDir: Function
}) => {
const router = useRouter()

return (
<>
<DropdownMenu>
<DropdownMenuTrigger>
<div className={`p-2 border flex items-center rounded-md`}>
<BsSliders className={`size-5`}/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>
<div className={`flex flex-col`}>
<span>页面设置</span>
<span className={`text-xs text-gray-600/40`}>
选择是否显示以下模块,打开即为显示
</span>
</div>
</DropdownMenuLabel>
{/*不是item就不会点击后马上让dropdown关闭*/}
<div className={`p-2 flex gap-x-1 items-center`}>
<span>目录模块</span>
<Switch
checked={showDir}
onCheckedChange={() => {
setShowDir(!showDir)
}}
/>
</div>
<DropdownMenuItem>
<span>自定义模块</span>
<Switch/>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className={`cursor-pointer p-2 border flex items-center rounded-md`}>
<span
onClick={async () => {
await updateLibrary({id, text, showDir})
router.push(`/malred/${id}`)
router.refresh()
}}
className={`text-sm`}>更新</span>
</div>
</>
);
};

export default HomepageUpdateButtons;

笔记首页

最后, 静态导出!


本站总访问量