mirror of
https://github.com/BidingCC/BuildingAI.git
synced 2026-09-01 15:48:39 +08:00
feat: 临时提交一些修改
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { WebDesigner } from "@fastbuildai/designer";
|
||||
// import { WebDesigner } from "@fastbuildai/designer";
|
||||
const WebDesigner = defineAsyncComponent(() => import("@fastbuildai/designer/web-designer"));
|
||||
|
||||
definePageMeta({ layout: "design" });
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 网页设计预览页面
|
||||
* @description 简单预览网页设计的最终效果
|
||||
*/
|
||||
import { WebPreview } from "@fastbuildai/designer";
|
||||
const WebPreview = defineAsyncComponent(() => import("@fastbuildai/designer/web-preview"));
|
||||
|
||||
// 设置页面元信息
|
||||
definePageMeta({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { WebDesigner } from "@fastbuildai/designer";
|
||||
// import { WebDesigner } from "@fastbuildai/designer";
|
||||
const WebDesigner = defineAsyncComponent(() => import("@fastbuildai/designer/web-designer"));
|
||||
|
||||
const { query: URLQueryParams } = useRoute();
|
||||
const designId = ref(URLQueryParams.id as string);
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
* 前台微页面预览页面
|
||||
* @description 展示微页面的最终效果
|
||||
*/
|
||||
import { WebPreview } from "@fastbuildai/designer";
|
||||
// import { WebPreview } from "@fastbuildai/designer";
|
||||
// import WebPreview from "@fastbuildai/designer/components/web-preview";
|
||||
|
||||
import { apiGetWebMicropageDetail } from "~/services/web/decorate";
|
||||
const WebPreview = defineAsyncComponent(() => import("@fastbuildai/designer/web-preview"));
|
||||
|
||||
// 设置页面元信息
|
||||
definePageMeta({
|
||||
@@ -19,7 +21,7 @@ const micropageId = computed(() => (URLQueryParams as Record<string, string>).id
|
||||
|
||||
// 获取微页面详情
|
||||
const { data: micropage, pending } = await useAsyncData(() =>
|
||||
apiGetWebMicropageDetail(micropageId.value),
|
||||
apiGetWebMicropageDetail(micropageId.value as string),
|
||||
);
|
||||
|
||||
// 如果页面不存在,抛出404错误
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useDesignStore } from "@fastbuildai/designer";
|
||||
import { useDesignStore } from "@fastbuildai/designer/stores";
|
||||
import { ProModal, useLockFn, useMessage, useModal } from "@fastbuildai/ui";
|
||||
|
||||
import Logo from "@/public/logo.svg";
|
||||
|
||||
+13
-15
@@ -21,8 +21,7 @@ export default defineNuxtConfig({
|
||||
"@nuxt/ui",
|
||||
"@nuxtjs/i18n",
|
||||
// "@vite-pwa/nuxt",
|
||||
// 生产构建禁用 ESLint 模块以减少打包耗时
|
||||
...(!isProd ? ["@nuxt/eslint"] : []),
|
||||
"@nuxt/eslint",
|
||||
"motion-v/nuxt",
|
||||
],
|
||||
|
||||
@@ -135,6 +134,8 @@ export default defineNuxtConfig({
|
||||
"/profile/**": { prerender: false, ssr: false, swr: true },
|
||||
},
|
||||
|
||||
sourcemap: false,
|
||||
|
||||
devServer: {
|
||||
port: 4091,
|
||||
host: "127.0.0.1, 0.0.0.0",
|
||||
@@ -185,17 +186,7 @@ export default defineNuxtConfig({
|
||||
|
||||
// // 配置 Vite
|
||||
vite: {
|
||||
plugins: [
|
||||
tailwindcss() as PluginOption,
|
||||
// import.meta.env.NUXT_BUILD_ENV === "production"
|
||||
// ? visualizer({
|
||||
// open: true, //注意这里要设置为true,否则无效
|
||||
// filename: "stats.html", //分析图生成的文件名
|
||||
// gzipSize: true, // 收集 gzip 大小并将其显示
|
||||
// brotliSize: true, // 收集 brotli 大小并将其显示
|
||||
// })
|
||||
// : null,
|
||||
],
|
||||
plugins: [tailwindcss() as PluginOption],
|
||||
optimizeDeps: {
|
||||
include:
|
||||
import.meta.env.NUXT_BUILD_ENV === "development"
|
||||
@@ -203,7 +194,8 @@ export default defineNuxtConfig({
|
||||
"@tanstack/table-core",
|
||||
"@internationalized/date",
|
||||
"reka-ui",
|
||||
"md-editor-v3",
|
||||
"@tiptap/vue-3",
|
||||
"@tiptap/starter-kit",
|
||||
"@tanstack/vue-table",
|
||||
"@iconify-json/lucide",
|
||||
"yup",
|
||||
@@ -227,6 +219,13 @@ export default defineNuxtConfig({
|
||||
"@nuxt/ui/runtime/locale/index.js",
|
||||
"clsx",
|
||||
"tailwind-merge",
|
||||
"@tiptap/extension-color",
|
||||
"@tiptap/extension-image",
|
||||
"@tiptap/extension-link",
|
||||
"@tiptap/extension-list-item",
|
||||
"@tiptap/extension-placeholder",
|
||||
"@tiptap/extension-text-style",
|
||||
"@tiptap/extension-underline",
|
||||
]
|
||||
: [],
|
||||
exclude: [],
|
||||
@@ -272,6 +271,5 @@ export default defineNuxtConfig({
|
||||
icon: {
|
||||
serverBundle: "remote",
|
||||
},
|
||||
|
||||
// // pwa,
|
||||
});
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.0.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "NUXT_BUILD_SSR=true NUXT_BUILD_ENV=production nuxi build --dotenv ../../.env.production.local && NUXT_BUILD_SSR=true node libs/release.mjs",
|
||||
"dev:pwa": "NUXT_BUILD_ENV=production VITE_PLUGIN_PWA=true nuxi dev --dotenv ../../.env.development.local",
|
||||
"dev": "NUXT_BUILD_SSR=true NUXT_BUILD_ENV=development nuxi dev --no-clear --dotenv ../../.env.development.local",
|
||||
"generate": "NUXT_BUILD_SSR=false NUXT_BUILD_ENV=production nuxi generate --dotenv ../../.env.production.local && NUXT_BUILD_SSR=false node libs/release.mjs",
|
||||
"dev": "NUXT_BUILD_SSR=false NUXT_BUILD_ENV=development nuxi dev --no-clear --dotenv ../../.env.development.local",
|
||||
"generate": "NODE_OPTIONS='--max-old-space-size=1200' NUXT_BUILD_SSR=false NUXT_BUILD_ENV=production nuxi generate --dotenv ../../.env.production.local && NUXT_BUILD_SSR=false node libs/release.mjs",
|
||||
"prepare": "NUXT_BUILD_SSR=true NUXT_BUILD_ENV=development nuxi prepare --dotenv ../../.env.development.local",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"start:generate": "npx serve .output/public",
|
||||
@@ -36,7 +35,6 @@
|
||||
"katex": "^0.16.22",
|
||||
"libphonenumber-js": "^1.12.8",
|
||||
"markdown-it-github-alerts": "^1.0.0",
|
||||
"md-editor-v3": "^5.5.0",
|
||||
"motion-v": "^1.0.0-beta.2",
|
||||
"nuxt": "^3.17.3",
|
||||
"nuxt-svgo": "^4.2.6",
|
||||
@@ -67,7 +65,6 @@
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"markdown-it-async": "^2.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "1.78.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.13",
|
||||
|
||||
@@ -51,5 +51,16 @@
|
||||
"@intlify/core-base": "^11.1.3",
|
||||
"jotai": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-color": "^2.26.1",
|
||||
"@tiptap/extension-image": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-list-item": "^2.26.1",
|
||||
"@tiptap/extension-placeholder": "^2.10.4",
|
||||
"@tiptap/extension-text-style": "^2.26.1",
|
||||
"@tiptap/extension-underline": "^2.10.4",
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@tiptap/vue-3": "^2.10.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
"module": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
"./stores": "./src/stores/design.ts",
|
||||
"./stores/design": "./src/stores/design.ts",
|
||||
"./web-preview": "./src/components/web-preview.vue",
|
||||
"./web-designer": "./src/components/web-designer.vue"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@fastbuildai/assets": "workspace:*",
|
||||
"@fastbuildai/config": "workspace:*",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { type Component, shallowRef } from "vue";
|
||||
import { type Component, defineAsyncComponent } from "vue";
|
||||
|
||||
import { useDesignStore } from "../../stores/design";
|
||||
import { registerComponents } from "../../utils/register-components";
|
||||
// 避免一次性注册全部组件,按需加载 content 组件
|
||||
const contentModules = import.meta.glob("../../components/widgets/**/content.vue", {
|
||||
eager: false,
|
||||
});
|
||||
const componentCache: Record<string, Component | null> = {};
|
||||
|
||||
const props = defineProps<{
|
||||
component: ComponentConfig;
|
||||
@@ -18,8 +22,19 @@ const emit = defineEmits<{
|
||||
|
||||
const designStore = useDesignStore();
|
||||
|
||||
// 动态注册内容组件
|
||||
const components = shallowRef<Record<string, Component>>(registerComponents("content"));
|
||||
function resolveComponent(type: string) {
|
||||
if (componentCache[type] !== undefined) return componentCache[type];
|
||||
const matcher = new RegExp(`/(?:web|mobile)/${type}/content\\.vue$`);
|
||||
const entry = Object.entries(contentModules).find(([path]) => matcher.test(path));
|
||||
if (!entry) {
|
||||
componentCache[type] = null;
|
||||
return null;
|
||||
}
|
||||
const loader = entry[1] as any;
|
||||
const comp = defineAsyncComponent(loader);
|
||||
componentCache[type] = comp as unknown as Component;
|
||||
return comp as unknown as Component;
|
||||
}
|
||||
|
||||
// 处理右键菜单命令
|
||||
function handleCommand(command: string) {
|
||||
@@ -95,7 +110,7 @@ function handleResizeStart(direction: string, event: MouseEvent) {
|
||||
<!-- 内容区域 -->
|
||||
<div class="component-content-wrapper">
|
||||
<component
|
||||
:is="components[component.type]"
|
||||
:is="resolveComponent(component.type)"
|
||||
v-bind="component.props"
|
||||
:size="component.size"
|
||||
class="pointer-events-none select-none"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 设计器页面骨架屏组件
|
||||
* 使用 Nuxt UI 的 USkeleton 组件
|
||||
* 设计器页面骨架屏组件(轻量版)
|
||||
* 不依赖 UI 库,避免引入重依赖
|
||||
*/
|
||||
</script>
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
<div class="bg-background border-muted flex w-[208px] flex-col rounded-md border p-2">
|
||||
<!-- 面板头部 -->
|
||||
<div class="flex h-10 items-center justify-between">
|
||||
<USkeleton class="h-6 w-20" />
|
||||
<USkeleton class="h-6 w-6" />
|
||||
<div class="skeleton h-6 w-20" />
|
||||
<div class="skeleton h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<!-- 组件分类 -->
|
||||
<div class="flex-1 space-y-6 p-2">
|
||||
<div v-for="category in 4" :key="category" class="space-y-3">
|
||||
<!-- 分类标题 -->
|
||||
<USkeleton class="h-5 w-24" />
|
||||
<div class="skeleton h-5 w-24" />
|
||||
|
||||
<!-- 组件项 -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<USkeleton v-for="item in 6" :key="item" class="h-12 rounded-lg" />
|
||||
<div v-for="item in 6" :key="item" class="skeleton h-12 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,13 +34,13 @@
|
||||
<!-- 工具栏 -->
|
||||
<div class="flex h-14 items-center justify-between px-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<USkeleton class="h-8 w-16" />
|
||||
<USkeleton class="h-8 w-16" />
|
||||
<USkeleton class="h-8 w-16" />
|
||||
<div class="skeleton h-8 w-16" />
|
||||
<div class="skeleton h-8 w-16" />
|
||||
<div class="skeleton h-8 w-16" />
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<USkeleton class="h-8 w-12" />
|
||||
<USkeleton class="h-8 w-20" />
|
||||
<div class="skeleton h-8 w-12" />
|
||||
<div class="skeleton h-8 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
<!-- 底部缩放控制器 -->
|
||||
<div class="absolute bottom-5 left-1/2 -translate-x-1/2">
|
||||
<div class="flex items-center space-x-2 rounded-lg border px-4 py-2 shadow-lg">
|
||||
<USkeleton class="h-6 w-8" />
|
||||
<USkeleton class="h-6 w-8" />
|
||||
<USkeleton class="h-6 w-12" />
|
||||
<USkeleton class="h-6 w-8" />
|
||||
<USkeleton class="h-6 w-8" />
|
||||
<div class="skeleton h-6 w-8" />
|
||||
<div class="skeleton h-6 w-8" />
|
||||
<div class="skeleton h-6 w-12" />
|
||||
<div class="skeleton h-6 w-8" />
|
||||
<div class="skeleton h-6 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,8 +63,8 @@
|
||||
<div class="bg-background border-muted flex w-[280px] flex-col rounded-md border">
|
||||
<!-- 面板头部 -->
|
||||
<div class="flex h-14 items-center justify-between px-4">
|
||||
<USkeleton class="h-6 w-16" />
|
||||
<USkeleton class="h-6 w-6" />
|
||||
<div class="skeleton h-6 w-16" />
|
||||
<div class="skeleton h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<!-- 属性内容 -->
|
||||
@@ -74,19 +74,19 @@
|
||||
<USkeleton class="h-5 w-20" />
|
||||
<div class="space-y-3">
|
||||
<div v-for="prop in 4" :key="prop" class="space-y-2">
|
||||
<USkeleton class="h-4 w-16" />
|
||||
<USkeleton class="h-10 w-full" />
|
||||
<div class="skeleton h-4 w-16" />
|
||||
<div class="skeleton h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式属性 -->
|
||||
<div class="space-y-4">
|
||||
<USkeleton class="h-5 w-16" />
|
||||
<div class="skeleton h-5 w-16" />
|
||||
<div class="space-y-3">
|
||||
<div v-for="style in 4" :key="style" class="space-y-2">
|
||||
<USkeleton class="h-4 w-20" />
|
||||
<USkeleton class="h-10 w-full" />
|
||||
<div class="skeleton h-4 w-20" />
|
||||
<div class="skeleton h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,3 +94,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.skeleton {
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(90deg, #f2f3f5 25%, #e9eaee 37%, #f2f3f5 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s ease infinite;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
* 动态属性编辑器容器
|
||||
* @description 自动注册 widgets 目录下的属性组件
|
||||
*/
|
||||
import type { Component } from "vue";
|
||||
import { computed, ref, shallowRef } from "vue";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { useDesignStore } from "../../stores/design";
|
||||
import { registerComponents } from "../../utils/register-components";
|
||||
import WidgetsBasePage from "../widgets/base/widgets-base-page.vue";
|
||||
import LayersPanel from "./layers-panel.vue";
|
||||
|
||||
@@ -16,13 +14,23 @@ const router = useRouter();
|
||||
const gridStore = useDesignStore();
|
||||
const isExpanded = ref(true);
|
||||
|
||||
// 动态注册属性编辑器组件
|
||||
const components = shallowRef<Record<string, Component>>(registerComponents("attribute"));
|
||||
// 按需解析属性编辑器组件,避免全量注册
|
||||
const attributeModules = import.meta.glob("../../components/widgets/**/attribute.vue", {
|
||||
eager: false,
|
||||
});
|
||||
function resolveAttributeComponent(type: string) {
|
||||
const entry = Object.entries(attributeModules).find(([path]) =>
|
||||
path.includes(`/${type}/attribute.vue`),
|
||||
);
|
||||
if (!entry) return null;
|
||||
const loader = entry[1] as any;
|
||||
return defineAsyncComponent(loader);
|
||||
}
|
||||
|
||||
// 当前激活的组件
|
||||
const currentComponent = computed(() => gridStore.activeComponent);
|
||||
const currentEditor = computed(
|
||||
() => (currentComponent.value?.type && components.value[currentComponent.value.type]) || null,
|
||||
const currentEditor = computed(() =>
|
||||
currentComponent.value?.type ? resolveAttributeComponent(currentComponent.value.type) : null,
|
||||
);
|
||||
|
||||
// 组件属性的计算属性
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { defineAsyncComponent, onMounted, ref } from "vue";
|
||||
|
||||
import { setDesignConfig } from "../config/design";
|
||||
import { useDesignStore } from "../stores/design";
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
* @description 专门用于预览设计结果,模拟真实网页显示效果
|
||||
*/
|
||||
import { useColorMode } from "@vueuse/core";
|
||||
import { type Component, computed, onMounted, onUnmounted, ref, shallowRef } from "vue";
|
||||
import { type Component, computed, defineAsyncComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { DESIGN_CONFIG } from "../config/design";
|
||||
import { useDesignStore } from "../stores/design";
|
||||
import { registerComponents } from "../utils/register-components";
|
||||
// 避免一次性注册所有组件,按需加载
|
||||
const contentModules = import.meta.glob("../components/widgets/**/content.vue", { eager: false });
|
||||
|
||||
const router = useRouter();
|
||||
const colorMode = useColorMode();
|
||||
@@ -33,8 +34,14 @@ const props = withDefaults(
|
||||
// 响应式窗口宽度
|
||||
const windowWidth = ref(typeof window !== "undefined" ? window.innerWidth : 1920);
|
||||
|
||||
// 动态注册内容组件
|
||||
const componentsMaps = shallowRef<Record<string, Component>>(registerComponents("content"));
|
||||
function resolveComponent(type: string) {
|
||||
const entry = Object.entries(contentModules).find(([path]) =>
|
||||
path.includes(`/${type}/content.vue`),
|
||||
);
|
||||
if (!entry) return null;
|
||||
const loader = entry[1] as any;
|
||||
return defineAsyncComponent(loader);
|
||||
}
|
||||
|
||||
// 使用配置中的设计尺寸
|
||||
const designWidth = computed(() => DESIGN_CONFIG.value.DEFAULT_WIDTH);
|
||||
@@ -202,17 +209,16 @@ onMounted(async () => {
|
||||
<!-- 顶部工具栏 -->
|
||||
<div
|
||||
v-if="showToolbar"
|
||||
class="bg-background/80 fixed top-4 left-4 z-50 rounded-lg border border-gray-200 px-3 py-2 shadow-lg backdrop-blur-sm"
|
||||
class="fixed top-4 left-4 z-50 rounded-lg border border-gray-200 bg-white/80 px-3 py-2 shadow-lg backdrop-blur-sm dark:bg-black/40"
|
||||
>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
icon="i-lucide-arrow-left"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="router.back()"
|
||||
>
|
||||
<span aria-hidden>←</span>
|
||||
返回编辑
|
||||
</UButton>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 预览画布 - 正常网页效果 -->
|
||||
@@ -224,7 +230,7 @@ onMounted(async () => {
|
||||
class="design-component"
|
||||
:style="getOutsideComponentStyle(component)"
|
||||
>
|
||||
<component :is="componentsMaps[component.type]" v-bind="component.props" />
|
||||
<component :is="resolveComponent(component.type)" v-bind="component.props" />
|
||||
</div>
|
||||
|
||||
<!-- 居中内容区 - 始终mx-auto -->
|
||||
@@ -236,7 +242,7 @@ onMounted(async () => {
|
||||
class="design-component"
|
||||
:style="getCenterComponentStyle(component)"
|
||||
>
|
||||
<component :is="componentsMaps[component.type]" v-bind="component.props" />
|
||||
<component :is="resolveComponent(component.type)" v-bind="component.props" />
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
* 段落组件
|
||||
* @description 富文本段落组件,支持HTML标签和样式混排,提供完整的文本格式化功能
|
||||
*/
|
||||
import "md-editor-v3/lib/style.css";
|
||||
|
||||
import { MdPreview } from "md-editor-v3";
|
||||
import { ProMarkdown } from "@fastbuildai/ui";
|
||||
import { computed, type CSSProperties } from "vue";
|
||||
|
||||
import WidgetsBaseContent from "../../base/widgets-base-content.vue";
|
||||
@@ -48,26 +46,6 @@ const paragraphStyle = computed<CSSProperties>(() => ({
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* 清理和安全化HTML内容
|
||||
*/
|
||||
const safeContent = computed(() => {
|
||||
if (!props.enableRichText) {
|
||||
// 如果不启用富文本,则将HTML标签转义
|
||||
return props.content
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
// 启用富文本时,保留安全的HTML标签
|
||||
// 这里可以根据需要添加更多的安全过滤逻辑
|
||||
return props.content || "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -77,12 +55,9 @@ const safeContent = computed(() => {
|
||||
custom-class="paragraph-content"
|
||||
>
|
||||
<template #default="{ style }">
|
||||
<!-- <div
|
||||
class="paragraph-wrapper prose dark:prose-invert prose-sm max-w-none"
|
||||
:style="paragraphStyle"
|
||||
v-dompurify-html="safeContent"
|
||||
/> -->
|
||||
<MdPreview id="preview-only" :modelValue="safeContent" />
|
||||
<div :style="paragraphStyle" class="paragraph-wrapper">
|
||||
<ProMarkdown :content="props.content" />
|
||||
</div>
|
||||
</template>
|
||||
</WidgetsBaseContent>
|
||||
</template>
|
||||
|
||||
@@ -31,10 +31,18 @@ export function useDesignInteraction(
|
||||
const design = designRef.value;
|
||||
if (!design) return null;
|
||||
|
||||
const zoomContainer = design.closest(".zoom-content") as HTMLElement;
|
||||
if (!zoomContainer) return null;
|
||||
// 优先从设计容器向上查找缩放容器
|
||||
let zoomContainer = design.closest(".zoom-content") as HTMLElement | null;
|
||||
// 兜底:如果未找到,尝试全局查询
|
||||
if (!zoomContainer) {
|
||||
zoomContainer = document.querySelector(".zoom-content") as HTMLElement | null;
|
||||
}
|
||||
// 再兜底:取父级作为缩放容器,按未缩放处理
|
||||
if (!zoomContainer) {
|
||||
return { design, zoomContainer: design.parentElement as HTMLElement } as const;
|
||||
}
|
||||
|
||||
return { design, zoomContainer };
|
||||
return { design, zoomContainer } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +56,11 @@ export function useDesignInteraction(
|
||||
|
||||
// 获取缩放容器的变换信息
|
||||
const style = window.getComputedStyle(zoomContainer);
|
||||
const transform = new DOMMatrix(style.transform);
|
||||
const scale = transform.a; // transform.a 是 x 轴的缩放值
|
||||
const transformStr = style.transform;
|
||||
// 处理 transform 为 'none' 的情况
|
||||
const matrix =
|
||||
transformStr && transformStr !== "none" ? new DOMMatrix(transformStr) : new DOMMatrix();
|
||||
const scale = matrix.a || 1; // transform.a 是 x 轴缩放值
|
||||
|
||||
// 计算鼠标相对于设计画布的实际位置
|
||||
const designRect = design.getBoundingClientRect();
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @fastbuildai/designer 包的主入口文件
|
||||
* 导出设计器的核心功能模块
|
||||
*/
|
||||
|
||||
// 导出 stores
|
||||
export { useDesignStore } from "./stores/design";
|
||||
|
||||
// 导出 composables
|
||||
export { useCanvasMetrics } from "./composables/useCanvasMetrics";
|
||||
export { useDesignGuides } from "./composables/useDesignGuides";
|
||||
export { useDesignInteraction } from "./composables/useDesignInteraction";
|
||||
export { useDesignState } from "./composables/useDesignState";
|
||||
export { useDesignSystem } from "./composables/useDesignSystem";
|
||||
export { usePanzoom } from "./composables/usePanzoom";
|
||||
|
||||
// 导出组件
|
||||
// export { DesignerSkeleton } from "./components/designer-skeleton.vue";
|
||||
// export { WebDesigner } from "./components/web-designer.vue";
|
||||
// export { WebPreview } from "./components/web-preview.vue";
|
||||
export const WebDesigner = defineAsyncComponent(() => import("./components/web-designer.vue"));
|
||||
export const WebPreview = defineAsyncComponent(() => import("./components/web-preview.vue"));
|
||||
|
||||
// 导出工具函数
|
||||
export * from "./utils/components-dynamic";
|
||||
export * from "./utils/register-components";
|
||||
@@ -22,20 +22,20 @@ const categoryConfigMap = reactive<CategoryConfigMap>({});
|
||||
* 动态导入组件配置
|
||||
* @param DEFAULT_DEVICE_TYPE - 设备类型(web/mobile)
|
||||
*/
|
||||
export function importComponentConfigs(DEFAULT_DEVICE_TYPE: DecorateScene) {
|
||||
export async function importComponentConfigs(DEFAULT_DEVICE_TYPE: DecorateScene) {
|
||||
// 重置组件配置数据
|
||||
Object.keys(componentConfigMap).forEach((key) => delete componentConfigMap[key]);
|
||||
Object.keys(categoryConfigMap).forEach((key) => delete categoryConfigMap[key]);
|
||||
|
||||
// 动态导入PC组件配置
|
||||
const webConfigs = import.meta.glob<ConfigModule>("../components/widgets/web/*/config.ts", {
|
||||
eager: true,
|
||||
eager: false,
|
||||
});
|
||||
|
||||
// 动态导入Mobile组件配置
|
||||
const mobileConfigs = import.meta.glob<ConfigModule>(
|
||||
"../components/widgets/mobile/*/config.ts",
|
||||
{ eager: true },
|
||||
{ eager: false },
|
||||
);
|
||||
|
||||
// 根据设备类型选择要处理的配置
|
||||
@@ -43,14 +43,14 @@ export function importComponentConfigs(DEFAULT_DEVICE_TYPE: DecorateScene) {
|
||||
|
||||
// 处理每个配置集合
|
||||
for (const configs of configsToProcess) {
|
||||
Object.entries(configs).forEach(([path, module]) => {
|
||||
for (const [path, loader] of Object.entries(configs)) {
|
||||
const match = path.match(/\/(web|mobile)\/([^/]+)\/config\.ts$/);
|
||||
if (!match) return;
|
||||
if (!match) continue;
|
||||
|
||||
const componentType = match[2];
|
||||
|
||||
const config = Object.values(module).find(
|
||||
(item) => item && typeof item === "object" && item.type === componentType,
|
||||
const mod = (await (loader as any)()) as ConfigModule;
|
||||
const config = Object.values(mod).find(
|
||||
(item) => item && typeof item === "object" && (item as any).type === componentType,
|
||||
) as ComponentMenuItem | undefined;
|
||||
|
||||
if (config) {
|
||||
@@ -59,7 +59,7 @@ export function importComponentConfigs(DEFAULT_DEVICE_TYPE: DecorateScene) {
|
||||
categoryConfigMap[config.category.id] = config.category;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { Component } from "vue";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
|
||||
import { DEFAULT_DEVICE_TYPE } from "../config/design";
|
||||
|
||||
type ComponentType = "content" | "attribute";
|
||||
|
||||
/**
|
||||
* 动态注册组件
|
||||
* @param type 组件类型
|
||||
* @returns 组件映射表 { componentName: Component }
|
||||
*/
|
||||
export function registerComponents(type: ComponentType = "content") {
|
||||
const components: Record<string, Component> = {};
|
||||
|
||||
// 动态导入PC组件
|
||||
const webWidgets = import.meta.glob("../components/widgets/web/**/*.vue", { eager: false });
|
||||
|
||||
// 动态导入Mobile组件
|
||||
const mobileWidgets = import.meta.glob("../components/widgets/mobile/**/*.vue", {
|
||||
eager: false,
|
||||
});
|
||||
|
||||
// 根据设备类型选择要处理的模块集合
|
||||
const modulesToProcess = DEFAULT_DEVICE_TYPE.value === "web" ? [webWidgets] : [mobileWidgets];
|
||||
|
||||
// 处理每个模块集合
|
||||
for (const modules of modulesToProcess) {
|
||||
// 根据类型过滤和注册组件
|
||||
Object.entries(modules).forEach(([filePath, component]) => {
|
||||
if (filePath.endsWith(`/${type}.vue`)) {
|
||||
const match = filePath.match(/\/(web|mobile)\/([^/]+)\//);
|
||||
if (match?.[2]) {
|
||||
components[match[2]] = defineAsyncComponent(component as any);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../../apps/web/.nuxt/types/imports.d.ts"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"check-types": "tsc --noEmit",
|
||||
"build": "echo \"No build required for UI package\""
|
||||
},
|
||||
"sideEffects": false,
|
||||
"devDependencies": {
|
||||
"@fastbuildai/assets": "workspace:*",
|
||||
"@fastbuildai/config": "workspace:*",
|
||||
@@ -36,6 +37,15 @@
|
||||
"dependencies": {
|
||||
"@fancyapps/ui": "^6.0.5",
|
||||
"@nuxt/ui": "^3.1.2",
|
||||
"@tiptap/extension-color": "^2.26.1",
|
||||
"@tiptap/extension-image": "^2.26.1",
|
||||
"@tiptap/extension-link": "^2.26.1",
|
||||
"@tiptap/extension-list-item": "^2.26.1",
|
||||
"@tiptap/extension-placeholder": "^2.26.1",
|
||||
"@tiptap/extension-text-style": "^2.26.1",
|
||||
"@tiptap/extension-underline": "^2.26.1",
|
||||
"@tiptap/starter-kit": "^2.26.1",
|
||||
"@tiptap/vue-3": "^2.26.1",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"reka-ui": "^2.2.0",
|
||||
"vue": "^3.5.13"
|
||||
|
||||
@@ -1,247 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useResizeObserver, useThrottleFn } from "@vueuse/core";
|
||||
import type { EChartsOption } from "echarts";
|
||||
// 引入常用图表,图表后缀都为 Chart
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
PieChart,
|
||||
RadarChart,
|
||||
ScatterChart,
|
||||
} from "echarts/charts";
|
||||
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
|
||||
import {
|
||||
AriaComponent,
|
||||
CalendarComponent,
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
ParallelComponent,
|
||||
PolarComponent,
|
||||
RadarComponent,
|
||||
TimelineComponent,
|
||||
TitleComponent,
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
} from "echarts/components";
|
||||
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
|
||||
import * as echarts from "echarts/core";
|
||||
// 标签自动布局,全局过渡动画等特性
|
||||
import { LabelLayout, UniversalTransition } from "echarts/features";
|
||||
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
|
||||
// import { useElementSize, useResizeObserver, useThrottleFn } from "@vueuse/core";
|
||||
// import type { EChartsOption } from "echarts";
|
||||
// // 引入常用图表,图表后缀都为 Chart
|
||||
// import {
|
||||
// BarChart,
|
||||
// LineChart,
|
||||
// MapChart,
|
||||
// PictorialBarChart,
|
||||
// PieChart,
|
||||
// RadarChart,
|
||||
// ScatterChart,
|
||||
// } from "echarts/charts";
|
||||
// // 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
|
||||
// import {
|
||||
// AriaComponent,
|
||||
// CalendarComponent,
|
||||
// DataZoomComponent,
|
||||
// GraphicComponent,
|
||||
// GridComponent,
|
||||
// LegendComponent,
|
||||
// ParallelComponent,
|
||||
// PolarComponent,
|
||||
// RadarComponent,
|
||||
// TimelineComponent,
|
||||
// TitleComponent,
|
||||
// ToolboxComponent,
|
||||
// TooltipComponent,
|
||||
// VisualMapComponent,
|
||||
// } from "echarts/components";
|
||||
// // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
|
||||
// import * as echarts from "echarts/core";
|
||||
// // 标签自动布局,全局过渡动画等特性
|
||||
// import { LabelLayout, UniversalTransition } from "echarts/features";
|
||||
// // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||
// import { CanvasRenderer } from "echarts/renderers";
|
||||
// import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
// 注册必须的组件
|
||||
echarts.use([
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
RadarChart,
|
||||
PictorialBarChart,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
ScatterChart,
|
||||
CanvasRenderer,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
]);
|
||||
// // 注册必须的组件
|
||||
// echarts.use([
|
||||
// LegendComponent,
|
||||
// TitleComponent,
|
||||
// TooltipComponent,
|
||||
// GridComponent,
|
||||
// PolarComponent,
|
||||
// AriaComponent,
|
||||
// ParallelComponent,
|
||||
// BarChart,
|
||||
// LineChart,
|
||||
// PieChart,
|
||||
// MapChart,
|
||||
// RadarChart,
|
||||
// PictorialBarChart,
|
||||
// RadarComponent,
|
||||
// ToolboxComponent,
|
||||
// DataZoomComponent,
|
||||
// VisualMapComponent,
|
||||
// TimelineComponent,
|
||||
// CalendarComponent,
|
||||
// GraphicComponent,
|
||||
// ScatterChart,
|
||||
// CanvasRenderer,
|
||||
// LabelLayout,
|
||||
// UniversalTransition,
|
||||
// ]);
|
||||
|
||||
/**
|
||||
* ProEcharts 组件属性定义
|
||||
*/
|
||||
interface ProEchartsProps {
|
||||
/** ECharts 配置选项 */
|
||||
options: EChartsOption;
|
||||
/** 主题,可选 */
|
||||
theme?: string;
|
||||
/** 图表高度,默认为 '100%' */
|
||||
height?: string;
|
||||
/** 是否自动调整大小,默认为 true */
|
||||
autoResize?: boolean;
|
||||
/** 是否启用动画,默认为 true */
|
||||
animation?: boolean;
|
||||
/** 渲染模式,可选 'canvas' 或 'svg',默认为 'canvas' */
|
||||
renderer?: "canvas" | "svg";
|
||||
/** 加载状态,默认为 false */
|
||||
loading?: boolean;
|
||||
/** 加载文本,默认为 '加载中...' */
|
||||
loadingText?: string;
|
||||
}
|
||||
// /**
|
||||
// * ProEcharts 组件属性定义
|
||||
// */
|
||||
// interface ProEchartsProps {
|
||||
// /** ECharts 配置选项 */
|
||||
// options: EChartsOption;
|
||||
// /** 主题,可选 */
|
||||
// theme?: string;
|
||||
// /** 图表高度,默认为 '100%' */
|
||||
// height?: string;
|
||||
// /** 是否自动调整大小,默认为 true */
|
||||
// autoResize?: boolean;
|
||||
// /** 是否启用动画,默认为 true */
|
||||
// animation?: boolean;
|
||||
// /** 渲染模式,可选 'canvas' 或 'svg',默认为 'canvas' */
|
||||
// renderer?: "canvas" | "svg";
|
||||
// /** 加载状态,默认为 false */
|
||||
// loading?: boolean;
|
||||
// /** 加载文本,默认为 '加载中...' */
|
||||
// loadingText?: string;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 组件属性定义
|
||||
*/
|
||||
const props = withDefaults(defineProps<ProEchartsProps>(), {
|
||||
theme: "",
|
||||
height: "100%",
|
||||
autoResize: true,
|
||||
animation: true,
|
||||
renderer: "canvas",
|
||||
loading: false,
|
||||
loadingText: "加载中...",
|
||||
});
|
||||
// /**
|
||||
// * 组件属性定义
|
||||
// */
|
||||
// const props = withDefaults(defineProps<ProEchartsProps>(), {
|
||||
// theme: "",
|
||||
// height: "100%",
|
||||
// autoResize: true,
|
||||
// animation: true,
|
||||
// renderer: "canvas",
|
||||
// loading: false,
|
||||
// loadingText: "加载中...",
|
||||
// });
|
||||
|
||||
// 图表容器引用
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
// ECharts 实例 (使用 shallowRef 避免不必要的深层响应式)
|
||||
const chartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
// 容器尺寸
|
||||
const { width, height } = useElementSize(chartRef);
|
||||
// // 图表容器引用
|
||||
// const chartRef = ref<HTMLElement | null>(null);
|
||||
// // ECharts 实例 (使用 shallowRef 避免不必要的深层响应式)
|
||||
// const chartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
// // 容器尺寸
|
||||
// const { width, height } = useElementSize(chartRef);
|
||||
|
||||
/**
|
||||
* 初始化图表
|
||||
*/
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
// /**
|
||||
// * 初始化图表
|
||||
// */
|
||||
// const initChart = () => {
|
||||
// if (!chartRef.value) return;
|
||||
|
||||
// 销毁旧实例
|
||||
disposeChart();
|
||||
// // 销毁旧实例
|
||||
// disposeChart();
|
||||
|
||||
// 创建新实例
|
||||
chartInstance.value = echarts.init(chartRef.value, props.theme, {
|
||||
renderer: props.renderer,
|
||||
});
|
||||
// // 创建新实例
|
||||
// chartInstance.value = echarts.init(chartRef.value, props.theme, {
|
||||
// renderer: props.renderer,
|
||||
// });
|
||||
|
||||
// 设置配置项
|
||||
updateChart();
|
||||
// // 设置配置项
|
||||
// updateChart();
|
||||
|
||||
// 设置加载状态
|
||||
if (props.loading) {
|
||||
chartInstance.value.showLoading("default", {
|
||||
text: props.loadingText,
|
||||
maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
textColor: "#999",
|
||||
fontSize: 14,
|
||||
});
|
||||
}
|
||||
};
|
||||
// // 设置加载状态
|
||||
// if (props.loading) {
|
||||
// chartInstance.value.showLoading("default", {
|
||||
// text: props.loadingText,
|
||||
// maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
// textColor: "#999",
|
||||
// fontSize: 14,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 更新图表配置
|
||||
*/
|
||||
const updateChart = () => {
|
||||
if (!chartInstance.value) return;
|
||||
// /**
|
||||
// * 更新图表配置
|
||||
// */
|
||||
// const updateChart = () => {
|
||||
// if (!chartInstance.value) return;
|
||||
|
||||
// 设置配置项
|
||||
chartInstance.value.setOption(props.options, true);
|
||||
// // 设置配置项
|
||||
// chartInstance.value.setOption(props.options, true);
|
||||
|
||||
// 更新加载状态
|
||||
if (props.loading) {
|
||||
chartInstance.value.showLoading("default", {
|
||||
text: props.loadingText,
|
||||
maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
textColor: "#999",
|
||||
fontSize: 14,
|
||||
});
|
||||
} else {
|
||||
chartInstance.value.hideLoading();
|
||||
}
|
||||
};
|
||||
// // 更新加载状态
|
||||
// if (props.loading) {
|
||||
// chartInstance.value.showLoading("default", {
|
||||
// text: props.loadingText,
|
||||
// maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
// textColor: "#999",
|
||||
// fontSize: 14,
|
||||
// });
|
||||
// } else {
|
||||
// chartInstance.value.hideLoading();
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 调整图表大小
|
||||
*/
|
||||
const resizeChart = useThrottleFn(() => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.resize();
|
||||
}
|
||||
}, 100);
|
||||
// /**
|
||||
// * 调整图表大小
|
||||
// */
|
||||
// const resizeChart = useThrottleFn(() => {
|
||||
// if (chartInstance.value) {
|
||||
// chartInstance.value.resize();
|
||||
// }
|
||||
// }, 100);
|
||||
|
||||
/**
|
||||
* 销毁图表实例
|
||||
*/
|
||||
const disposeChart = () => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose();
|
||||
chartInstance.value = null;
|
||||
}
|
||||
};
|
||||
// /**
|
||||
// * 销毁图表实例
|
||||
// */
|
||||
// const disposeChart = () => {
|
||||
// if (chartInstance.value) {
|
||||
// chartInstance.value.dispose();
|
||||
// chartInstance.value = null;
|
||||
// }
|
||||
// };
|
||||
|
||||
// 监听配置项变化
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (chartInstance.value) {
|
||||
updateChart();
|
||||
} else {
|
||||
initChart();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
// // 监听配置项变化
|
||||
// watch(
|
||||
// () => props.options,
|
||||
// () => {
|
||||
// nextTick(() => {
|
||||
// if (chartInstance.value) {
|
||||
// updateChart();
|
||||
// } else {
|
||||
// initChart();
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// { deep: true, immediate: true },
|
||||
// );
|
||||
|
||||
// 监听加载状态变化
|
||||
watch(
|
||||
() => props.loading,
|
||||
(val) => {
|
||||
if (!chartInstance.value) return;
|
||||
// // 监听加载状态变化
|
||||
// watch(
|
||||
// () => props.loading,
|
||||
// (val) => {
|
||||
// if (!chartInstance.value) return;
|
||||
|
||||
if (val) {
|
||||
chartInstance.value.showLoading("default", {
|
||||
text: props.loadingText,
|
||||
maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
textColor: "#999",
|
||||
fontSize: 14,
|
||||
});
|
||||
} else {
|
||||
chartInstance.value.hideLoading();
|
||||
}
|
||||
},
|
||||
);
|
||||
// if (val) {
|
||||
// chartInstance.value.showLoading("default", {
|
||||
// text: props.loadingText,
|
||||
// maskColor: "rgba(255, 255, 255, 0.8)",
|
||||
// textColor: "#999",
|
||||
// fontSize: 14,
|
||||
// });
|
||||
// } else {
|
||||
// chartInstance.value.hideLoading();
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
|
||||
// 监听容器尺寸变化
|
||||
if (props.autoResize) {
|
||||
watch([width, height], () => {
|
||||
resizeChart();
|
||||
});
|
||||
// // 监听容器尺寸变化
|
||||
// if (props.autoResize) {
|
||||
// watch([width, height], () => {
|
||||
// resizeChart();
|
||||
// });
|
||||
|
||||
// 使用 ResizeObserver 监听容器尺寸变化
|
||||
useResizeObserver(chartRef, () => {
|
||||
resizeChart();
|
||||
});
|
||||
}
|
||||
// // 使用 ResizeObserver 监听容器尺寸变化
|
||||
// useResizeObserver(chartRef, () => {
|
||||
// resizeChart();
|
||||
// });
|
||||
// }
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
});
|
||||
});
|
||||
// // 组件挂载时初始化图表
|
||||
// onMounted(() => {
|
||||
// nextTick(() => {
|
||||
// initChart();
|
||||
// });
|
||||
// });
|
||||
|
||||
// 组件卸载前销毁图表
|
||||
onBeforeUnmount(() => {
|
||||
disposeChart();
|
||||
});
|
||||
// // 组件卸载前销毁图表
|
||||
// onBeforeUnmount(() => {
|
||||
// disposeChart();
|
||||
// });
|
||||
|
||||
// 暴露给父组件的方法
|
||||
defineExpose({
|
||||
// 获取 ECharts 实例
|
||||
getChartInstance: () => chartInstance.value,
|
||||
});
|
||||
// // 暴露给父组件的方法
|
||||
// defineExpose({
|
||||
// // 获取 ECharts 实例
|
||||
// getChartInstance: () => chartInstance.value,
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chartRef" class="pro-echarts" :style="{ height: props.height }"></div>
|
||||
<!-- <div ref="chartRef" class="pro-echarts" :style="{ height: props.height }"></div> -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import "md-editor-v3/lib/style.css";
|
||||
|
||||
import { useColorMode, useVModel } from "@vueuse/core";
|
||||
import { MdEditor, type Themes, type ToolbarNames } from "md-editor-v3";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import ListItem from "@tiptap/extension-list-item";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Editor, EditorContent } from "@tiptap/vue-3";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
import { apiUploadFile } from "@/services/common";
|
||||
|
||||
@@ -12,80 +19,437 @@ const emits = defineEmits<{
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/**
|
||||
* 受控内容,HTML 字符串
|
||||
*/
|
||||
modelValue: string;
|
||||
preview?: boolean;
|
||||
/**
|
||||
* 自定义样式类
|
||||
*/
|
||||
customClass?: string;
|
||||
/**
|
||||
* 占位提示
|
||||
*/
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{},
|
||||
{
|
||||
customClass: "",
|
||||
placeholder: "请输入内容...",
|
||||
},
|
||||
);
|
||||
|
||||
const colorMode = useColorMode();
|
||||
const content = useVModel(props, "modelValue", emits);
|
||||
const toolbars: ToolbarNames[] = [
|
||||
"bold",
|
||||
"underline",
|
||||
"italic",
|
||||
"strikeThrough",
|
||||
"-",
|
||||
"title",
|
||||
"sub",
|
||||
"sup",
|
||||
"quote",
|
||||
"unorderedList",
|
||||
"orderedList",
|
||||
"task",
|
||||
"-",
|
||||
"codeRow",
|
||||
"code",
|
||||
"link",
|
||||
"image",
|
||||
"table",
|
||||
// "mermaid",
|
||||
// "katex",
|
||||
"-",
|
||||
"revoke",
|
||||
"next",
|
||||
"=",
|
||||
"prettier",
|
||||
"pageFullscreen",
|
||||
"fullscreen",
|
||||
"catalog",
|
||||
].filter(Boolean) as ToolbarNames[];
|
||||
|
||||
/**
|
||||
* 处理图片上传并返回上传后的图片 URL。
|
||||
* 并在所有图片上传成功后通过回调函数返回上传后的图片 URL 数组。
|
||||
*
|
||||
* @param {File[]} files - 要上传的图片文件数组。
|
||||
* @param {(url: string[]) => void} callback - 上传完成后的回调函数,接收一个包含上传图片 URL 的字符串数组作为参数。
|
||||
* 回调函数会在所有图片上传成功后被调用。
|
||||
*
|
||||
* @returns {Promise<void>} 一个 Promise,当所有文件上传并调用回调函数后解析。
|
||||
*/
|
||||
async function handleUploadImage(files: File[], callback: (url: string[]) => void) {
|
||||
const res: { url: string }[] = await Promise.all(
|
||||
files.map((file) => {
|
||||
return new Promise<{ url: string }>((rev, rej) => {
|
||||
apiUploadFile({ file, description: "editor" })
|
||||
.then((res) => rev(res))
|
||||
.catch((error) => rej(error));
|
||||
});
|
||||
}),
|
||||
);
|
||||
const editor = shallowRef<any | null>(null);
|
||||
const uiRefresh = ref(0);
|
||||
|
||||
callback(res.map((item) => item.url));
|
||||
onMounted(() => {
|
||||
editor.value = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Link.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),
|
||||
Image,
|
||||
Placeholder.configure({ placeholder: props.placeholder }),
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle,
|
||||
],
|
||||
content: content.value || "",
|
||||
autofocus: false,
|
||||
injectCSS: false,
|
||||
editorProps: {
|
||||
attributes: { class: "prose prose-sm focus:outline-none" },
|
||||
},
|
||||
onUpdate: ({ editor }: { editor: any }) => {
|
||||
content.value = editor.getHTML();
|
||||
},
|
||||
});
|
||||
|
||||
// 首次渲染后自动聚焦到末尾,优化编辑体验
|
||||
nextTick(() => {
|
||||
editor.value?.commands.focus("end");
|
||||
});
|
||||
|
||||
// 监听编辑器状态变化以刷新工具栏激活/禁用态
|
||||
const bump = () => (uiRefresh.value = (uiRefresh.value + 1) % 1_000_000);
|
||||
editor.value?.on("selectionUpdate", bump);
|
||||
editor.value?.on("transaction", bump);
|
||||
editor.value?.on("update", bump);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
editor.value = null;
|
||||
});
|
||||
|
||||
// 外部变更时同步到编辑器
|
||||
watch(
|
||||
() => content.value,
|
||||
(val) => {
|
||||
if (!editor.value) return;
|
||||
if (val !== editor.value.getHTML()) {
|
||||
editor.value.commands.setContent(val || "", false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function handlePickAndInsertImage() {
|
||||
if (!editor.value) return;
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".jpg,.jpeg,.png,.gif,.webp";
|
||||
input.multiple = true;
|
||||
input.onchange = async () => {
|
||||
const files = Array.from(input.files || []);
|
||||
if (!files.length) return;
|
||||
const uploaded = await Promise.all(
|
||||
files.map(
|
||||
(file) =>
|
||||
new Promise<{ url: string }>((resolve, reject) => {
|
||||
apiUploadFile({ file, description: "editor" })
|
||||
.then((res) => resolve(res))
|
||||
.catch((err) => reject(err));
|
||||
}),
|
||||
),
|
||||
);
|
||||
uploaded.forEach(({ url }) => {
|
||||
editor.value!.chain().focus().setImage({ src: url }).run();
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
function toggleMark(mark: "bold" | "italic" | "strike" | "underline") {
|
||||
if (!editor.value) return;
|
||||
const chain = editor.value.chain().focus();
|
||||
if (mark === "bold") chain.toggleBold().run();
|
||||
if (mark === "italic") chain.toggleItalic().run();
|
||||
if (mark === "strike") chain.toggleStrike().run();
|
||||
if (mark === "underline") chain.toggleUnderline().run();
|
||||
}
|
||||
|
||||
function setParagraph() {
|
||||
editor.value?.chain().focus().setParagraph().run();
|
||||
}
|
||||
function setHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
|
||||
editor.value?.chain().focus().toggleHeading({ level }).run();
|
||||
}
|
||||
function toggleBulletList() {
|
||||
editor.value?.chain().focus().toggleBulletList().run();
|
||||
}
|
||||
function toggleOrderedList() {
|
||||
editor.value?.chain().focus().toggleOrderedList().run();
|
||||
}
|
||||
function toggleBlockquote() {
|
||||
editor.value?.chain().focus().toggleBlockquote().run();
|
||||
}
|
||||
function toggleCodeBlock() {
|
||||
editor.value?.chain().focus().toggleCodeBlock().run();
|
||||
}
|
||||
function clearMarks() {
|
||||
editor.value?.chain().focus().unsetAllMarks().run();
|
||||
}
|
||||
function clearNodes() {
|
||||
editor.value?.chain().focus().clearNodes().run();
|
||||
}
|
||||
function insertHorizontalRule() {
|
||||
editor.value?.chain().focus().setHorizontalRule().run();
|
||||
}
|
||||
function insertHardBreak() {
|
||||
editor.value?.chain().focus().setHardBreak().run();
|
||||
}
|
||||
function toggleInlineCode() {
|
||||
editor.value?.chain().focus().toggleCode().run();
|
||||
}
|
||||
function setTextColor(color: string) {
|
||||
editor.value?.chain().focus().setColor(color).run();
|
||||
}
|
||||
function undo() {
|
||||
editor.value?.chain().focus().undo().run();
|
||||
}
|
||||
function redo() {
|
||||
editor.value?.chain().focus().redo().run();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MdEditor
|
||||
v-model="content"
|
||||
class="rounded-md"
|
||||
:class="customClass"
|
||||
:no-mermaid="true"
|
||||
:no-katex="true"
|
||||
:theme="colorMode as Themes"
|
||||
:toolbars="toolbars"
|
||||
@onUploadImg="handleUploadImage"
|
||||
/>
|
||||
<div class="rounded-md border border-gray-200/60 dark:border-gray-700/60" :class="customClass">
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-1 border-b border-gray-200/60 p-2 dark:border-gray-700/60"
|
||||
>
|
||||
<!-- 行内样式 -->
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleBold().run()"
|
||||
:color="editor?.isActive('bold') ? 'primary' : undefined"
|
||||
@click="toggleMark('bold')"
|
||||
>
|
||||
B
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleItalic().run()"
|
||||
:color="editor?.isActive('italic') ? 'primary' : undefined"
|
||||
@click="toggleMark('italic')"
|
||||
>
|
||||
<i>I</i>
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:color="editor?.isActive('underline') ? 'primary' : undefined"
|
||||
@click="toggleMark('underline')"
|
||||
>
|
||||
<u>U</u>
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleStrike().run()"
|
||||
:color="editor?.isActive('strike') ? 'primary' : undefined"
|
||||
@click="toggleMark('strike')"
|
||||
>
|
||||
<s>S</s>
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleCode().run()"
|
||||
:color="editor?.isActive('code') ? 'primary' : undefined"
|
||||
@click="toggleInlineCode"
|
||||
>
|
||||
{ }
|
||||
</UButton>
|
||||
<span class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700"></span>
|
||||
|
||||
<!-- 段落与标题 -->
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:color="editor?.isActive('paragraph') ? 'primary' : undefined"
|
||||
@click="setParagraph"
|
||||
>P</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 1 }) ? 'primary' : undefined"
|
||||
@click="setHeading(1)"
|
||||
>H1</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 2 }) ? 'primary' : undefined"
|
||||
@click="setHeading(2)"
|
||||
>H2</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 3 }) ? 'primary' : undefined"
|
||||
@click="setHeading(3)"
|
||||
>H3</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 4 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 4 }) ? 'primary' : undefined"
|
||||
@click="setHeading(4)"
|
||||
>H4</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 5 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 5 }) ? 'primary' : undefined"
|
||||
@click="setHeading(5)"
|
||||
>H5</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleHeading({ level: 6 }).run()"
|
||||
:color="editor?.isActive('heading', { level: 6 }) ? 'primary' : undefined"
|
||||
@click="setHeading(6)"
|
||||
>H6</UButton
|
||||
>
|
||||
<span class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700"></span>
|
||||
|
||||
<!-- 列表/引用/代码块 -->
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleBulletList().run()"
|
||||
:color="editor?.isActive('bulletList') ? 'primary' : undefined"
|
||||
@click="toggleBulletList"
|
||||
>• List</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleOrderedList().run()"
|
||||
:color="editor?.isActive('orderedList') ? 'primary' : undefined"
|
||||
@click="toggleOrderedList"
|
||||
>1. List</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleBlockquote().run()"
|
||||
:color="editor?.isActive('blockquote') ? 'primary' : undefined"
|
||||
@click="toggleBlockquote"
|
||||
>“”</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().toggleCodeBlock().run()"
|
||||
:color="editor?.isActive('codeBlock') ? 'primary' : undefined"
|
||||
@click="toggleCodeBlock"
|
||||
>{ }</UButton
|
||||
>
|
||||
<span class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700"></span>
|
||||
|
||||
<!-- 图片与清理 -->
|
||||
<UButton size="xs" variant="soft" @click="handlePickAndInsertImage">Img</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="clearMarks">Clear marks</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="clearNodes">Clear nodes</UButton>
|
||||
<span class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700"></span>
|
||||
|
||||
<!-- 撤销/重做 与 分隔 -->
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().undo().run()"
|
||||
@click="undo"
|
||||
>Undo</UButton
|
||||
>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="!editor?.can().chain().focus().redo().run()"
|
||||
@click="redo"
|
||||
>Redo</UButton
|
||||
>
|
||||
<span class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700"></span>
|
||||
|
||||
<!-- 水平线/硬回车/颜色示例 -->
|
||||
<UButton size="xs" variant="ghost" @click="insertHorizontalRule">HR</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="insertHardBreak">BR</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:color="editor?.isActive('textStyle', { color: '#958DF1' }) ? 'primary' : undefined"
|
||||
@click="setTextColor('#958DF1')"
|
||||
>Purple</UButton
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div class="min-h-48 p-3">
|
||||
<EditorContent v-if="editor" :editor="editor as any" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
declare module "@tiptap/vue-3" {
|
||||
export const Editor: any;
|
||||
export const EditorContent: any;
|
||||
}
|
||||
|
||||
declare module "@tiptap/starter-kit" {
|
||||
const StarterKit: any;
|
||||
export default StarterKit;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-underline" {
|
||||
const Underline: any;
|
||||
export default Underline;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-link" {
|
||||
const Link: any;
|
||||
export default Link;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-image" {
|
||||
const Image: any;
|
||||
export default Image;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-placeholder" {
|
||||
const Placeholder: any;
|
||||
export default Placeholder;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-color" {
|
||||
export const Color: any;
|
||||
export default Color;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-text-style" {
|
||||
const TextStyle: any;
|
||||
export default TextStyle;
|
||||
}
|
||||
|
||||
declare module "@tiptap/extension-list-item" {
|
||||
const ListItem: any;
|
||||
export default ListItem;
|
||||
}
|
||||
Reference in New Issue
Block a user