no message

This commit is contained in:
KuroSago 2025-07-10 15:15:19 +08:00
parent c1138766c8
commit 63442cb50e
18 changed files with 2403 additions and 50 deletions

View File

@ -18,7 +18,7 @@
}, },
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true, "source.fixAll": "explicit",
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
} }
} }

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@mind-map/component": "workspace:*", "@mind-map/component": "workspace:*",
"@tiptap/component": "workspace:*",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"postcss": "^8.4.27", "postcss": "^8.4.27",

View File

@ -1,19 +1,29 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from "vue-router";
// 路由配置 // 路由配置
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: "/",
name: 'Home', name: "Home",
component: () => import('../views/Home.vue') component: () => import("../views/Home.vue"),
}, },
] {
path: "/mind-map",
name: "mindMap",
component: () => import("../views/mindMap/index.vue"),
},
{
path: "/tiptap",
name: "tiptap",
component: () => import("../views/tiptap/index.vue"),
},
];
// 创建路由实例 // 创建路由实例
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes routes,
}) });
export default router export default router;

View File

@ -1,43 +1,110 @@
<template> <template>
<div class="h-screen p-6 bg-gradient-to-br from-gray-50 to-gray-100"> <div class="home-container">
<MindMap @onDel="onDel" @onAddChild="onAddChild" @onEdit="onEdit" /> <div class="title">欢迎使用思维导图工具</div>
<div class="nav-buttons">
<button @click="goToMindMap" class="nav-btn mind-map-btn">
思维导图
</button>
<button @click="goToTiptap" class="nav-btn tiptap-btn">
富文本编辑器 (Tiptap)
</button>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { useRouter } from "vue-router";
import { MindMap, useMindMapStore } from "@mind-map/component";
import type { MindMapNode } from "@mind-map/component";
onMounted(() => { const router = useRouter();
console.log("Home view mounted");
});
const { removeNode, insertChildNode } = useMindMapStore(); //
function goToMindMap() {
function onDel(node: MindMapNode) { router.push({ name: "mindMap" });
removeNode({
beforeRemoveCallback: async () => {
return true;
},
nodeId: node.uid,
allowRemoveWithChildren: false,
});
} }
function onAddChild(node: MindMapNode) { // Tiptap
insertChildNode({ function goToTiptap() {
parentNodeId: node.uid, router.push({ name: "tiptap" });
beforeInsertCallback: async () => {
return {
uid: "node.uid",
text: "node.text",
};
},
});
}
function onEdit(node: MindMapNode) {
console.log("Node edited:", node);
} }
</script> </script>
<style scoped>
.home-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.title {
font-size: 2.5rem;
font-weight: bold;
color: white;
margin-bottom: 3rem;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.nav-buttons {
display: flex;
gap: 2rem;
flex-wrap: wrap;
justify-content: center;
}
.nav-btn {
padding: 1rem 2rem;
font-size: 1.2rem;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.mind-map-btn {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
color: #333;
}
.mind-map-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(255, 154, 158, 0.4);
}
.tiptap-btn {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
.tiptap-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(168, 237, 234, 0.4);
}
.nav-btn:active {
transform: translateY(-1px);
}
@media (max-width: 768px) {
.nav-buttons {
flex-direction: column;
align-items: center;
}
.title {
font-size: 2rem;
}
.nav-btn {
min-width: 250px;
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="h-screen p-6 bg-gradient-to-br from-gray-50 to-gray-100">
<MindMap @onDel="onDel" @onAddChild="onAddChild" @onEdit="onEdit" />
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { MindMap, useMindMapStore } from "@mind-map/component";
import type { MindMapNode } from "@mind-map/component";
onMounted(() => {
console.log("Home view mounted");
});
const { removeNode, insertChildNode } = useMindMapStore();
function onDel(node: MindMapNode) {
removeNode({
beforeRemoveCallback: async () => {
return true;
},
nodeId: node.uid,
allowRemoveWithChildren: false,
});
}
function onAddChild(node: MindMapNode) {
insertChildNode({
parentNodeId: node.uid,
beforeInsertCallback: async () => {
return {
uid: "node.uid",
text: "node.text",
};
},
});
}
function onEdit(node: MindMapNode) {
console.log("Node edited:", node);
}
</script>

View File

@ -0,0 +1,9 @@
<template>
<div>
<TiptapEditor />
</div>
</template>
<script lang="ts" setup>
import { TiptapEditor } from "@tiptap/component";
</script>

View File

@ -1,18 +1,16 @@
// import { App } from 'vue'; // import { App } from 'vue';
// import pinia from './store'; // import pinia from './store';
import MindMap from './views/index.vue'; import MindMap from "./views/index.vue";
import type MindMapNode from "simple-mind-map/types/src/core/render/node/MindMapNode"; import type MindMapNode from "simple-mind-map/types/src/core/render/node/MindMapNode";
export { MindMap }; export { MindMap };
export * from './store'; export * from "./store";
export type { MindMapNode }; export type { MindMapNode };
// export function installMindMap(app: App) { // export function installMindMap(app: App) {
// app.use(pinia); // app.use(pinia);
// } // }

View File

@ -0,0 +1,40 @@
{
"name": "@tiptap/component",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:no-check": "vite build",
"type-check": "vue-tsc",
"preview": "vite preview"
},
"dependencies": {
"@tiptap/core": "^2.25.0",
"@types/file-saver": "^2.0.7",
"docx": "^9.5.1",
"echo-editor": "^0.4.8",
"file-saver": "^2.0.5",
"openai": "^5.8.3",
"prosemirror-docx": "latest",
"vite-svg-loader": "latest",
"vue": "^3.3.4"
},
"main": "src/index.ts",
"module": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.14",
"ts-node": "^10.9.2",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vue-tsc": "^2.2.10"
}
}

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { ActionButton } from "echo-editor";
import type { Editor } from "echo-editor";
import { ButtonViewReturnComponentProps } from "echo-editor";
import iconUrl from "../../../icons/exportWord.svg";
const props = withDefaults(defineProps<Props>(), {
disabled: false,
isActive: undefined,
});
interface Props {
disabled?: boolean;
isActive?: ButtonViewReturnComponentProps["isActive"];
editor?: Editor;
}
function handleExport() {
props.editor?.commands.exportToWord();
}
</script>
<template>
<action-button
tooltip="ExportToWord"
:is-active="isActive"
:disabled="disabled"
:action="handleExport"
>
<template #icon>
<iconUrl style="width: 16px; height: 16px" />
</template>
</action-button>
</template>

View File

@ -0,0 +1,64 @@
import { Extension } from "@tiptap/core";
import { saveAs } from "file-saver";
import ActionButton from "./components/ActionButton.vue";
import { DocxSerializer, defaultNodes, defaultMarks } from "prosemirror-docx";
import { Packer } from "docx";
import type { GeneralOptions } from "echo-editor";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
exportWord: {
exportToWord: () => ReturnType;
};
}
}
export interface ExportWordOptions extends GeneralOptions<ExportWordOptions> {}
const nodeSerializer = {
...defaultNodes,
hardBreak: defaultNodes.hard_break,
codeBlock: defaultNodes.code_block,
orderedList: defaultNodes.ordered_list,
listItem: defaultNodes.list_item,
bulletList: defaultNodes.bullet_list,
horizontalRule: defaultNodes.horizontal_rule,
image(state: any, node: any) {
// No image
state.renderInline(node);
state.closeBlock(node);
},
};
const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);
export const ExportWord = Extension.create<ExportWordOptions>({
name: "exportWord",
addOptions() {
return {
...this.parent?.(),
button: ({}) => ({
component: ActionButton,
componentProps: {},
}),
};
},
addCommands() {
return {
exportToWord:
() =>
({ editor }) => {
const opts: any = {
getImageBuffer: async (src: string) => {
const response = await fetch(src);
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
},
};
const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
Packer.toBlob(wordDocument).then((blob) =>
saveAs(new Blob([blob]), "example.docx")
);
return true;
},
};
},
});

View File

@ -0,0 +1 @@
<svg t="1721031224139" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7445" width="200" height="200"><path d="M679.253333 402.363733l-60.484266 158.651734-60.347734-158.651734a30.037333 30.037333 0 0 0-30.446933-18.6368 29.764267 29.764267 0 0 0-30.446933 18.6368l-60.416 158.651734-60.416-158.651734a30.5152 30.5152 0 0 0-38.843734-17.271466 28.945067 28.945067 0 0 0-17.954133 37.546666l88.814933 233.2672c4.369067 11.4688 15.701333 19.114667 28.398934 19.114667a30.3104 30.3104 0 0 0 28.4672-19.114667l62.395733-163.908266 62.395733 163.84c4.437333 11.605333 15.701333 19.182933 28.398934 19.182933a30.3104 30.3104 0 0 0 28.4672-19.114667l88.746666-233.2672a28.945067 28.945067 0 0 0-17.885866-37.546666 30.446933 30.446933 0 0 0-38.912 17.271466zM898.730667 797.969067l-51.882667-29.218134c-28.672-16.1792-52.224-3.072-52.224 29.0816v0.273067H643.208533a29.832533 29.832533 0 0 0-30.3104 29.354667c0 16.1792 13.585067 29.218133 30.3104 29.218133h151.825067c1.092267 30.5152 24.029867 43.076267 52.224 27.648l51.063467-27.989333c29.013333-15.906133 29.149867-42.1888 0.4096-58.368z" fill="#333333" p-id="7446"></path><path d="M810.666667 913.134933l-0.477867 0.068267H201.796267c-19.8656 0-36.727467-11.6736-36.727467-25.6V269.243733h154.965333c51.268267 0 92.910933-39.389867 92.910934-87.8592V93.525333h397.243733c19.797333 0 36.522667 11.741867 36.522667 25.668267v620.066133h61.986133V119.261867c0-46.421333-44.168533-84.241067-98.5088-84.241067H328.362667l-225.28 194.56v658.090667c0 46.2848 44.2368 84.104533 98.7136 84.104533h608.392533c43.758933 0 80.554667-24.712533 93.320533-58.5728h-92.842666zM350.890667 94.890667v86.562133c0 16.110933-13.858133 29.2864-30.9248 29.2864H216.814933l134.144-115.848533z" fill="#333333" p-id="7447"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
import TiptapEditor from "./views/index.vue";
export { TiptapEditor };

View File

@ -0,0 +1,394 @@
<template>
<div class="min-h-screen bg-background">
<header
class="border-grid w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div class="container sticky flex items-center h-14">
<div class="hidden mr-4 md:mr-1 md:flex">
<a
href="/"
class="flex items-center mr-4 md:mr-2 lg:mr-6 lg:space-x1 xl:space-x-2"
>
<svg
viewBox="0 0 24 24"
width="24"
height="24"
class="mr-2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 7v10" />
<path d="M8 9v6" opacity="0.7" />
<path d="M4 11v2" opacity="0.4" />
<path d="M16 9v6" opacity="0.7" />
<path d="M20 11v2" opacity="0.4" />
<rect
x="10"
y="5"
width="4"
height="14"
fill="currentColor"
opacity="0.1"
/>
</svg>
<span class="font-bold"> Echo Editor </span>
</a>
<nav class="flex items-center gap-4 text-sm xl:gap-6"></nav>
</div>
<div
class="flex items-center justify-between flex-1 space-x-2 md:justify-end"
>
<div class="flex-1 w-full md:w-auto md:flex-none"></div>
<nav class="flex items-center gap-0.5">
<a
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground w-8 h-8"
href="https://github.com/Seedsa/echo-editor"
target="_blank"
><svg
viewBox="0 0 15 15"
width="1.2em"
height="1.2em"
class="w-4 h-4"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M7.5.25a7.25 7.25 0 0 0-2.292 14.13c.363.066.495-.158.495-.35c0-.172-.006-.628-.01-1.233c-2.016.438-2.442-.972-2.442-.972c-.33-.838-.805-1.06-.805-1.06c-.658-.45.05-.441.05-.441c.728.051 1.11.747 1.11.747c.647 1.108 1.697.788 2.11.602c.066-.468.254-.788.46-.969c-1.61-.183-3.302-.805-3.302-3.583a2.8 2.8 0 0 1 .747-1.945c-.075-.184-.324-.92.07-1.92c0 0 .61-.194 1.994.744A7 7 0 0 1 7.5 3.756A7 7 0 0 1 9.315 4c1.384-.938 1.992-.743 1.992-.743c.396.998.147 1.735.072 1.919c.465.507.745 1.153.745 1.945c0 2.785-1.695 3.398-3.31 3.577c.26.224.492.667.492 1.343c0 .97-.009 1.751-.009 1.989c0 .194.131.42.499.349A7.25 7.25 0 0 0 7.499.25"
clip-rule="evenodd"
></path></svg></a
><ThemeToggle />
</nav>
</div>
</div>
</header>
<div class="my-0 mx-auto max-w-[1024px] p-6">
<div class="mb-2">
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('zhHans')"
>
中文
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('en')"
>
English
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="toggleMinimal"
>
{{ minimal ? "Full" : "Minimal" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="hideToolbar = !hideToolbar"
>
{{ !hideToolbar ? "Hide Toolbar" : "Show Toolbar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="hideMenubar = !hideMenubar"
>
{{ !hideMenubar ? "Hide Menubar" : "Show Menubar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors rounded-md hover:bg-accent hover:text-accent-foreground"
@click="disabled = !disabled"
>
{{ disabled ? "Editable" : "Readonly" }}
</button>
</div>
<div class="border rounded-lg shadow-sm bg-card text-card-foreground">
<echo-editor
v-model="content"
:extensions="extensions"
:hideToolbar="hideToolbar"
:hideMenubar="hideMenubar || minimal"
:key="minimal ? 'minimal' : 'full'"
:disabled="disabled"
:maxHeight="512"
output="html"
:dark="theme === 'dark'"
>
</echo-editor>
</div>
<div class="p-4 mt-6 border rounded-lg bg-muted">
<h3 class="mb-2 text-sm font-medium">HTML Output</h3>
<div class="rounded bg-muted-foreground/5 max-h-[500px] overflow-auto">
<span>{{ content }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import {
Bold,
BulletList,
Italic,
BaseKit,
Underline,
Strike,
LineHeight,
Image,
History,
Heading,
CodeBlock,
FontSize,
Highlight,
Table,
Clear,
Blockquote,
Link,
Color,
Video,
OrderedList,
HorizontalRule,
Fullscreen,
TaskList,
MoreMark,
FormatPainter,
SlashCommand,
Indent,
locale,
ImportWord,
Columns,
TextAlign,
ImageUpload,
VideoUpload,
FontFamily,
FindAndReplace,
Code,
AI,
Preview,
Printer,
Iframe,
EchoEditor,
ThemeToggle,
SpecialCharacter,
SourceCode,
} from "echo-editor";
import { ExportWord } from "../components/ExportWord";
import OpenAI from "openai";
import { DEMO_CONTENT } from "./initContent";
import "./style.css";
import "echo-editor/style.css";
const content = ref(DEMO_CONTENT);
const theme = ref<string | null>(null);
const hideToolbar = ref<boolean>(false);
const hideMenubar = ref<boolean>(false);
const disabled = ref<boolean>(false);
const minimal = ref(false);
const extensions = computed(() =>
minimal.value ? minimalExtensions : fullExtensions
);
const minimalExtensions = [
BaseKit.configure({
characterCount: {
limit: 50000,
},
}),
Heading,
Bold.configure({ spacer: true }),
Italic,
Underline,
HorizontalRule,
TextAlign.configure({
types: ["heading", "paragraph", "image"],
spacer: true,
}),
Image,
Blockquote.configure({ spacer: true }),
Code,
Link,
Color,
TaskList.configure({ spacer: true }),
OrderedList,
BulletList,
];
const fullExtensions = [
BaseKit.configure({
placeholder: {
showOnlyCurrent: true,
},
characterCount: {
limit: 50000,
},
}),
History,
Columns,
FormatPainter,
Clear,
Heading.configure({ spacer: true }),
FontSize,
FontFamily,
Bold,
Italic,
Underline,
Strike,
MoreMark,
Color.configure({ spacer: true }),
Highlight,
BulletList,
OrderedList,
TextAlign.configure({
types: ["heading", "paragraph", "image"],
spacer: true,
}),
Indent,
LineHeight,
TaskList.configure({
spacer: true,
taskItem: {
nested: true,
},
}),
Link,
Image,
ImageUpload.configure({
upload: (files: File) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(URL.createObjectURL(files));
}, 3000);
});
},
}),
Video,
VideoUpload.configure({
upload: handleFileUpload,
}),
Blockquote,
SlashCommand,
HorizontalRule,
CodeBlock,
Table.configure({ spacer: true }),
Code,
ExportWord,
AI.configure({
completions: AICompletions,
shortcuts: [
// shortcuts
{
label: "Custom Actions",
children: [
{
label: "This is Custom Action",
prompt:
"Rewrite this content with no spelling mistakes, proper grammar, and with more descriptive language, using best writing practices without losing the original meaning.",
},
],
},
],
}),
ImportWord.configure({
upload: handleFileUpload,
}),
SpecialCharacter,
Fullscreen.configure({ spacer: true }),
SourceCode,
Preview,
FindAndReplace.configure({ spacer: true }),
Printer,
Iframe,
];
async function handleFileUpload(files: File[]) {
const f = files.map((file) => ({
src: URL.createObjectURL(file),
alt: file.name,
}));
return Promise.resolve(f);
}
function toggleMinimal() {
minimal.value = !minimal.value;
}
/**
* AI Completions handler function
* WARNING: This is just a demo implementation. In production:
* - DO NOT expose API keys in the frontend
* - DO implement this through your backend API
* - DO add proper error handling and rate limiting
*
* @param history - Chat history array containing messages with role and content
* @param signal - AbortSignal for cancelling requests
* @returns OpenAI chat completion stream
*/
async function AICompletions(
history: Array<{ role: string; content: string }> = [],
signal?: AbortSignal
) {
// groq.com For free llm api recommend deepseek r1 70b
// SECURITY WARNING: API keys should never be exposed in the frontend
// This is just for demo purposes
// In production, implement this through your backend API
const apiKey = "import.meta.env.VITE_OPENAI_API_KEY";
const baseURL = "import.meta.env.VITE_OPENAI_BASE_URL";
const model = "import.meta.env.VITE_OPENAI_MODEL";
if (!apiKey || !baseURL || !model) {
throw new Error(
"OpenAI configuration is missing. Please check your environment variables."
);
}
const openai = new OpenAI({
apiKey: apiKey,
dangerouslyAllowBrowser: true,
baseURL: baseURL,
});
const systemMsg = `You are a professional writing assistant. Please respond based on the user's context:
1. Maintain a professional, accurate, and objective tone
2. Ensure responses are clear, coherent, and well-structured
3. Responses must be in HTML format, preserving all HTML tags, links, and styles
4. Support the following writing enhancements:
- Grammar and spelling corrections
- Improved sentence structure and expression
- Optimized article formatting and layout
- Maintain the core meaning of the original text
5. If context includes code, maintain code formatting and provide optimization suggestions
6. Add appropriate HTML elements like headings, lists, quotes etc. to enhance readability as needed
Please respond only based on the provided context, do not add irrelevant information.`;
const systemPrompt = [{ role: "system", content: systemMsg }];
const finalMessages = [...systemPrompt];
if (history.length > 0) {
finalMessages.push(...history);
}
try {
const stream = await openai.chat.completions.create(
{
model,
messages: finalMessages,
temperature: 0.7,
stream: true,
reasoning_format: "parsed", // groq deepseek r1 need this
} as any,
{ signal }
);
return stream;
} catch (error) {
if (error instanceof Error) {
console.error("Error in AI Completions:", error.message);
}
throw error;
}
}
</script>

View File

@ -0,0 +1,258 @@
export const DEMO_CONTENT = {
type: 'doc',
content: [
{
type: 'heading',
attrs: { textAlign: 'center', indent: 0, lineHeight: null, level: 1 },
content: [{ type: 'text', text: 'Echo Editor' }],
},
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [
{ type: 'text', text: 'A modern WYSIWYG AI rich text editor based on ' },
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://github.com/scrumpy/tiptap',
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: 'link',
},
},
],
text: 'tiptap',
},
{ type: 'text', text: ' and ' },
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://www.shadcn-vue.com/',
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: 'link',
},
},
],
text: 'shadcn ui',
},
{ type: 'text', text: ' for Vue.js' },
],
},
{ type: 'paragraph', attrs: { textAlign: 'left', indent: 0, lineHeight: null } },
{ type: 'paragraph', attrs: { textAlign: 'left', indent: 0, lineHeight: null } },
{ type: 'paragraph', attrs: { textAlign: 'left', indent: 0, lineHeight: null } },
{
type: 'image',
attrs: {
textAlign: 'center',
src: 'https://picsum.photos/1920/1080.webp?t=1',
alt: null,
title: null,
width: 500,
},
},
{ type: 'paragraph', attrs: { textAlign: 'left', indent: 0, lineHeight: null } },
{ type: 'horizontalRule' },
{
type: 'heading',
attrs: { textAlign: 'left', indent: 0, lineHeight: null, level: 2 },
content: [{ type: 'text', text: 'Demo' }],
},
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [
{ type: 'text', text: '👉' },
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://echo-editor.jzcloud.site/',
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: 'link',
},
},
],
text: 'Demo',
},
],
},
{
type: 'heading',
attrs: { textAlign: 'left', indent: 0, lineHeight: null, level: 2 },
content: [{ type: 'text', text: 'Features' }],
},
{
type: 'bulletList',
attrs: { listStyleType: 'disc' },
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [
{ type: 'text', text: 'Use ' },
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://www.shadcn-vue.com/',
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: 'link',
},
},
],
text: 'shadcn ui',
},
{ type: 'text', text: ' components' },
],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Markdown support' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'TypeScript support' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'I18n support' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Vue 3.x support' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Slash Command' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Dark Mode' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Multi Column' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'AI Power' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'Embed' }],
},
],
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: { textAlign: 'left', indent: 0, lineHeight: null },
content: [{ type: 'text', text: 'TailwindCss' }],
},
],
},
],
},
{
type: 'heading',
attrs: { textAlign: 'left', indent: 0, lineHeight: null, level: 2 },
content: [{ type: 'text', text: 'Installation' }],
},
{
type: 'codeBlock',
attrs: { language: 'bash' },
content: [
{ type: 'text', text: 'pnpm add echo-editor\n// or\nor yarn add echo-editor\n// or\nor npm i echo-editor -S' },
],
},
{
type: 'heading',
attrs: { textAlign: 'left', indent: 0, lineHeight: null, level: 3 },
content: [{ type: 'text', text: 'install plugin' }],
},
{
type: 'codeBlock',
attrs: { language: "typescript", },
content: [
{
type: 'text',
text: "import { createApp } from 'vue'\nimport App from './App.vue'\nimport EchoEditor from 'echo-editor'\nimport 'echo-editor/style.css'\nconst app = createApp(App)\napp.use(EchoEditor)\napp.mount('#app')",
},
],
},
{ type: 'paragraph', attrs: { textAlign: 'left', indent: 0, lineHeight: null } },
],
}

View File

@ -0,0 +1,89 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@views/*": ["src/views/*"],
"@components/*": ["src/components/*"]
// "@store/*": ["src/store/*"],
// "@assets/*": ["src/assets/*"],
// "@utils/*": ["src/utils/*"],
// "@styles/*": ["src/styles/*"],
// "@helpers/*": ["src/store/helpers/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

1310
web3/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
packages: packages:
- 'packages/*' - 'packages/*'
- 'packages/mind-map/*' - 'packages/mind-map/*'
- 'packages/tiptap/*'
- 'apps/*' - 'apps/*'