feat: 临时提交一些修改

This commit is contained in:
画上佳人纱
2025-08-13 16:38:52 +08:00
parent b22272378e
commit b76dacc755
24 changed files with 870 additions and 473 deletions
@@ -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);
+4 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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,
});
+2 -5
View File
@@ -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",
+11
View File
@@ -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"
}
}
+5 -1
View File
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.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();
-26
View File
@@ -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 -1
View File
@@ -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": {
+10
View File
@@ -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"
+218 -218
View File
@@ -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>
+432 -68
View File
@@ -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
View File
@@ -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;
}