From fda9853509c9c6671c37862963e4ae0884749053 Mon Sep 17 00:00:00 2001 From: sealday Date: Tue, 30 Apr 2024 02:24:09 +0800 Subject: [PATCH] feat: support vditor (#894) Co-authored-by: sealday Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/894 --- .../core/client/src/api-client/APIClient.ts | 27 ++++ .../client/src/application/Application.tsx | 13 ++ packages/core/sdk/src/APIClient.ts | 17 +++ .../plugin-field-markdown-vditor/.npmignore | 2 + .../plugin-field-markdown-vditor/README.md | 1 + .../build.config.ts | 15 ++ .../plugin-field-markdown-vditor/client.d.ts | 2 + .../plugin-field-markdown-vditor/client.js | 1 + .../plugin-field-markdown-vditor/package.json | 25 ++++ .../plugin-field-markdown-vditor/server.d.ts | 2 + .../plugin-field-markdown-vditor/server.js | 1 + .../src/client/components/Display.tsx | 120 ++++++++++++++++ .../src/client/components/Edit.tsx | 134 ++++++++++++++++++ .../src/client/components/const.ts | 7 + .../src/client/components/index.tsx | 8 ++ .../src/client/components/style.ts | 15 ++ .../src/client/index.tsx | 54 +++++++ .../src/client/interfaces/markdown-vditor.tsx | 106 ++++++++++++++ .../src/client/locale/index.ts | 7 + .../src/client/utils/index.ts | 0 .../plugin-field-markdown-vditor/src/index.ts | 2 + .../src/locale/en-US.ts | 33 +++++ .../src/locale/ko_KR.ts | 33 +++++ .../src/locale/zh-CN.ts | 33 +++++ .../src/server/collections/.gitkeep | 0 .../src/server/index.ts | 1 + .../src/server/markdown-vditor-field.ts | 7 + .../src/server/plugin.ts | 19 +++ pnpm-lock.yaml | 38 +++++ 29 files changed, 723 insertions(+) create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/.npmignore create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/README.md create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/build.config.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/client.js create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/package.json create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/server.js create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Display.tsx create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Edit.tsx create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/const.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/index.tsx create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/style.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/index.tsx create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/interfaces/markdown-vditor.tsx create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/locale/index.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/utils/index.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/index.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/en-US.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/ko_KR.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/zh-CN.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/collections/.gitkeep create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/index.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/markdown-vditor-field.ts create mode 100644 packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/plugin.ts diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 9c8ff70cd..8a1e80062 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -25,6 +25,22 @@ const handleErrorMessage = (error, notification) => { }; const errorCache = new Map(); + +function offsetToTimeZone(offset) { + const hours = Math.floor(Math.abs(offset)); + const minutes = Math.abs((offset % 1) * 60); + + const formattedHours = (hours < 10 ? '0' : '') + hours; + const formattedMinutes = (minutes < 10 ? '0' : '') + minutes; + + const sign = offset >= 0 ? '+' : '-'; + return sign + formattedHours + ':' + formattedMinutes; +} + +const getCurrentTimezone = () => { + const timezoneOffset = new Date().getTimezoneOffset() / -60; + return offsetToTimeZone(timezoneOffset); +}; export class APIClient extends APIClientSDK { services: Record> = {}; silence = false; @@ -36,6 +52,17 @@ export class APIClient extends APIClientSDK { return this.services[uid]; } + getHeaders() { + const headers = super.getHeaders(); + const appName = this.app.getName(); + if (appName) { + headers['X-App'] = appName; + } + headers['X-Timezone'] = getCurrentTimezone(); + headers['X-Hostname'] = window?.location?.hostname; + return headers; + } + interceptors() { this.axios.interceptors.request.use((config) => { config.headers['X-With-ACL-Meta'] = true; diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 12f98b1d0..dfc74ded2 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -167,6 +167,19 @@ export class Application { return this.options.publicPath || '/'; } + getName() { + return getSubAppName(this.getPublicPath()) || null; + } + + getApiUrl(pathname = '') { + let baseURL = this.apiClient.axios['defaults']['baseURL']; + if (!baseURL.startsWith('http://') && !baseURL.startsWith('https://')) { + const { protocol, host } = window.location; + baseURL = `${protocol}//${host}${baseURL}`; + } + return baseURL.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, ''); + } + getRouteUrl(pathname: string) { return this.options.publicPath.replace(/\/$/g, '') + pathname; } diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts index 297ed29d7..7d9e98ef6 100644 --- a/packages/core/sdk/src/APIClient.ts +++ b/packages/core/sdk/src/APIClient.ts @@ -258,6 +258,23 @@ export class APIClient { } } + getHeaders() { + const headers = {}; + if (this.auth.locale) { + headers['X-Locale'] = this.auth.locale; + } + if (this.auth.role) { + headers['X-Role'] = this.auth.role; + } + if (this.auth.authenticator) { + headers['X-Authenticator'] = this.auth.authenticator; + } + if (this.auth.token) { + headers['Authorization'] = `Bearer ${this.auth.token}`; + } + return headers; + } + interceptors() { this.axios.interceptors.request.use((config) => { config.paramsSerializer = (params) => { diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/.npmignore b/packages/plugins/@tachybase/plugin-field-markdown-vditor/.npmignore new file mode 100644 index 000000000..65f5e8779 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/README.md b/packages/plugins/@tachybase/plugin-field-markdown-vditor/README.md new file mode 100644 index 000000000..c186374ae --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/README.md @@ -0,0 +1 @@ +# @nocobase/plugin-field-markdown-vditor diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/build.config.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/build.config.ts new file mode 100644 index 000000000..696749bed --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/build.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@nocobase/build'; +import fs from 'fs/promises'; +import path from 'path'; + +const vditor = path.dirname(require.resolve('vditor')); + +export default defineConfig({ + afterBuild: async (log) => { + log('coping vditor dist'); + await fs.cp(vditor, path.resolve(__dirname, 'dist/client/vditor/dist'), { + recursive: true, + force: true, + }); + }, +}); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts new file mode 100644 index 000000000..6c459cbac --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.js b/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.js new file mode 100644 index 000000000..b6e3be70e --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/package.json b/packages/plugins/@tachybase/plugin-field-markdown-vditor/package.json new file mode 100644 index 000000000..c517f2b97 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/package.json @@ -0,0 +1,25 @@ +{ + "name": "@tachybase/plugin-field-markdown-vditor", + "displayName": "Collection field: Markdown(Vditor)", + "version": "0.0.1", + "description": "Used to store Markdown and render it using Vditor editor, supports common Markdown syntax such as list, code, quote, etc., and supports uploading images, recordings, etc.It also allows for instant rendering, where what you see is what you get.", + "keywords": [ + "Collection fields" + ], + "license": "Apache-2.0", + "main": "dist/server/index.js", + "devDependencies": { + "@ant-design/icons": "^5.3.6", + "@tachybase/schema": "workspace:*", + "antd": "^5.16.1", + "koa-send": "^5.0.1", + "vditor": "^3.10.3" + }, + "peerDependencies": { + "@nocobase/client": "workspace:*", + "@nocobase/server": "workspace:*", + "@nocobase/test": "workspace:*" + }, + "description.zh-CN": "用于存储 Markdown,并使用 Vditor 编辑器渲染,支持常见 Markdown 语法,如列表,代码,引用等,并支持上传图片,录音等。同时可以做到即时渲染,所见即所得。", + "displayName.zh-CN": "数据表字段:Markdown(Vditor)" +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts new file mode 100644 index 000000000..c41081ddc --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.js b/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.js new file mode 100644 index 000000000..972842039 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Display.tsx b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Display.tsx new file mode 100644 index 000000000..dd8b517ce --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Display.tsx @@ -0,0 +1,120 @@ +import { Field, useField } from '@tachybase/schema'; +import { withDynamicSchemaProps } from '@nocobase/client'; +import { Popover } from 'antd'; +import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; +import Vditor from 'vditor'; +import { useCDN } from './const'; +import useStyle from './style'; + +function convertToText(markdownText: string) { + const content = markdownText; + let temp = document.createElement('div'); + temp.innerHTML = content; + const text = temp.innerText; + temp = null; + return text?.replace(/[\n\r]/g, '') || ''; +} + +const getContentWidth = (element) => { + if (element) { + const range = document.createRange(); + range.selectNodeContents(element); + const contentWidth = range.getBoundingClientRect().width; + return contentWidth; + } +}; + +function DisplayInner(props: { value: string; style?: CSSProperties }) { + const containerRef = useRef(); + const { wrapSSR, componentCls, hashId } = useStyle(); + const cdn = useCDN(); + + useEffect(() => { + if (!props.value) return; + Vditor.preview(containerRef.current, props.value, { + mode: 'light', + cdn, + }); + }, [props.value]); + + return wrapSSR( +
+
+
, + ); +} + +export const Display = withDynamicSchemaProps((props) => { + const field = useField(); + const value = props.value ?? field.value; + const cdn = useCDN(); + + const containerRef = useRef(); + + const [popoverVisible, setPopoverVisible] = useState(false); + const [ellipsis, setEllipsis] = useState(false); + + const [text, setText] = useState(''); + + const elRef = useRef(); + + useEffect(() => { + if (!props.value || !field.value) return; + if (props.ellipsis) { + Vditor.md2html(props.value, { + mode: 'light', + cdn, + }) + .then((html) => { + setText(convertToText(html)); + }) + .catch(() => setText('')); + } else { + Vditor.preview(containerRef.current, props.value ?? field.value, { + mode: 'light', + cdn, + }); + } + }, [props.value, props.ellipsis, field.value]); + + const isOverflowTooltip = useCallback(() => { + if (!elRef.current) return false; + const contentWidth = getContentWidth(elRef.current); + const offsetWidth = elRef.current?.offsetWidth; + return contentWidth > offsetWidth; + }, [elRef]); + + if (props.ellipsis) { + return ( + { + setPopoverVisible(ellipsis && visible); + }} + content={} + > +
{ + const el = e.target as any; + const isShowTooltips = isOverflowTooltip(); + if (isShowTooltips) { + setEllipsis(el.scrollWidth >= el.clientWidth); + } + }} + > + {text} +
+
+ ); + } + + return ; +}); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Edit.tsx b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Edit.tsx new file mode 100644 index 000000000..7240f62f7 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/Edit.tsx @@ -0,0 +1,134 @@ +import { useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import Vditor from 'vditor'; +import { defaultToolbar } from '../interfaces/markdown-vditor'; +import { useCDN } from './const'; +import useStyle from './style'; + +export const Edit = withDynamicSchemaProps((props) => { + const { disabled, onChange, value, fileCollection, toolbar } = props; + + const vdRef = useRef(); + const vdFullscreen = useRef(false); + const containerRef = useRef(); + const containerParentRef = useRef(); + const app = useApp(); + const apiClient = useAPIClient(); + const cdn = useCDN(); + const { wrapSSR, hashId, componentCls: containerClassName } = useStyle(); + + useEffect(() => { + const uploadFileCollection = fileCollection || 'attachments'; + const toolbarConfig = toolbar ?? defaultToolbar; + const vditor = new Vditor(containerRef.current, { + value: value ?? '', + lang: apiClient.auth.locale.replaceAll('-', '_') as any, + cache: { + enable: false, + }, + undoDelay: 0, + preview: { + math: { + engine: 'KaTeX', + }, + }, + toolbar: toolbarConfig, + fullscreen: { + index: 1200, + }, + cdn, + minHeight: 200, + after: () => { + vdRef.current = vditor; + vditor.setValue(value ?? ''); + if (disabled) { + vditor.disabled(); + } else { + vditor.enable(); + } + }, + input(value) { + onChange(value); + }, + upload: { + url: app.getApiUrl(`${uploadFileCollection}:create`), + headers: apiClient.getHeaders(), + multiple: false, + fieldName: 'file', + format(files, responseText) { + const response = JSON.parse(responseText); + const formatResponse = { + msg: '', + code: 0, + data: { + errFiles: [], + succMap: { + [response.data.filename]: response.data.url, + }, + }, + }; + return JSON.stringify(formatResponse); + }, + }, + }); + return () => { + vdRef.current?.destroy(); + vdRef.current = undefined; + }; + }, [fileCollection, toolbar?.join(',')]); + + useEffect(() => { + if (value === vdRef?.current?.getValue()) { + return; + } + vdRef.current?.setValue(value); + vdRef.current?.focus(); + // 移动光标到末尾 + const preArea = containerRef.current.querySelector('div.vditor-content > div.vditor-ir > pre') as HTMLPreElement; + if (preArea) { + const range = document.createRange(); + const selection = window.getSelection(); + if (selection) { + range.selectNodeContents(preArea); + range.collapse(false); // 将光标移动到内容末尾 + selection.removeAllRanges(); + selection.addRange(range); + } + } + }, [value]); + + useEffect(() => { + if (disabled) { + vdRef.current?.disabled(); + } else { + vdRef.current?.enable(); + } + }, [disabled]); + + useLayoutEffect(() => { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const target = entry.target; + if (target.className.includes('vditor--fullscreen')) { + document.body.appendChild(target); + vdFullscreen.current = true; + } else if (vdFullscreen.current) { + containerParentRef.current?.appendChild(target); + vdFullscreen.current = false; + } + } + }); + + observer.observe(containerRef.current); + + return () => { + observer.unobserve(containerRef.current); + }; + }, []); + + return wrapSSR( +
+
+
, + ); +}); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/const.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/const.ts new file mode 100644 index 000000000..0d02033e8 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/const.ts @@ -0,0 +1,7 @@ +import { usePlugin } from '@nocobase/client'; +import { PluginFieldMarkdownVditorClient } from '../'; + +export const useCDN = () => { + const plugin = usePlugin(PluginFieldMarkdownVditorClient); + return plugin.getCDN(); +}; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/index.tsx b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/index.tsx new file mode 100644 index 000000000..3816fb0ff --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/index.tsx @@ -0,0 +1,8 @@ +import { connect, mapReadPretty } from '@tachybase/schema'; +import { withDynamicSchemaProps } from '@nocobase/client'; +import { Display } from './Display'; +import { Edit } from './Edit'; + +export const MarkdownVditor = withDynamicSchemaProps(connect(Edit, mapReadPretty(Display))); + +export default MarkdownVditor; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/style.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/style.ts new file mode 100644 index 000000000..6362855af --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/components/style.ts @@ -0,0 +1,15 @@ +import { genStyleHook } from '@nocobase/client'; + +export default genStyleHook('nb-field-markdown-vditor', (token) => { + const { componentCls } = token; + + return { + [componentCls]: { + '.vditor-reset': { fontSize: `${token.fontSize}px !important` }, + '.vditor': { borderRadius: 8 }, + '.vditor .vditor-content': { borderRadius: '0 0 8px 8px', overflow: 'hidden' }, + '.vditor .vditor-toolbar': { paddingLeft: ' 16px !important', borderRadius: '8px 8px 0 0' }, + '.vditor .vditor-content .vditor-ir .vditor-reset': { paddingLeft: ' 16px !important' }, + }, + }; +}); diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/index.tsx b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/index.tsx new file mode 100644 index 000000000..974cc0ce8 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/index.tsx @@ -0,0 +1,54 @@ +import { Plugin } from '@nocobase/client'; +import 'vditor/dist/index.css'; +import { MarkdownVditor } from './components'; +import { MarkdownVditorFieldInterface } from './interfaces/markdown-vditor'; +export class PluginFieldMarkdownVditorClient extends Plugin { + async afterAdd() {} + + async beforeLoad() {} + + async load() { + this.app.addComponents({ MarkdownVditor }); + this.initVditorDependency(); + this.app.dataSourceManager.addFieldInterfaces([MarkdownVditorFieldInterface]); + } + + getCDN() { + if (process.env.NODE_ENV !== 'production') { + // 开发模式下使用远程 cdn + return 'https://unpkg.com/vditor@3.10.4'; + } + // 生产环境,使用本地链接,支持内网 + // 需要支持子目录,比如应用部署在 /xxx/ 目录下 + return this.app.getPublicPath() + 'static/plugins/@nocobase/plugin-field-markdown-vditor/dist/client/vditor'; + } + + initVditorDependency() { + const cdn = this.getCDN(); + try { + const vditorDepdencePrefix = 'plugin-field-markdown-vditor-dep'; + const vditorDepdence = { + [`${vditorDepdencePrefix}.katex`]: `${cdn}/dist/js/katex/katex.min.js?v=0.16.9`, + [`${vditorDepdencePrefix}.ABCJS`]: `${cdn}/dist/js/abcjs/abcjs_basic.min`, + [`${vditorDepdencePrefix}.plantumlEncoder`]: `${cdn}/dist/js/plantuml/plantuml-encoder.min`, + [`${vditorDepdencePrefix}.echarts`]: `${cdn}/dist/js/echarts/echarts.min`, + [`${vditorDepdencePrefix}.flowchart`]: `${cdn}/dist/js/flowchart.js/flowchart.min`, + [`${vditorDepdencePrefix}.Viz`]: `${cdn}/dist/js/graphviz/viz`, + [`${vditorDepdencePrefix}.mermaid`]: `${cdn}/dist/js/mermaid/mermaid.min`, + }; + this.app.requirejs.require.config({ + waitSeconds: 120, + paths: vditorDepdence, + }); + Object.keys(vditorDepdence).forEach((key) => { + this.app.requirejs.require([key], (m) => { + window[key.split('.')[1]] = m; + }); + }); + } catch (e) { + console.log('initVditorDependency failed', e); + } + } +} + +export default PluginFieldMarkdownVditorClient; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/interfaces/markdown-vditor.tsx b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/interfaces/markdown-vditor.tsx new file mode 100644 index 000000000..0b37858ef --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/interfaces/markdown-vditor.tsx @@ -0,0 +1,106 @@ +import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client'; +import { generateNTemplate } from '../locale'; +import { ISchema } from '@tachybase/schema'; + +const { defaultProps, operators } = interfacesProperties; + +export const defaultToolbar = [ + 'headings', + 'bold', + 'italic', + 'strike', + 'link', + 'list', + 'ordered-list', + 'check', + 'quote', + 'line', + 'code', + 'inline-code', + 'upload', + 'fullscreen', +]; + +export class MarkdownVditorFieldInterface extends CollectionFieldInterface { + name = 'vditor'; + type = 'object'; + group = 'media'; + order = 1; + title = generateNTemplate('Vditor'); + sortable = true; + default = { + type: 'text', + length: 'long', + uiSchema: { + type: 'string', + 'x-component': 'MarkdownVditor', + }, + }; + properties = { + ...defaultProps, + 'uiSchema.x-component-props.fileCollection': { + type: 'string', + title: generateNTemplate('File collection'), + 'x-component': 'CollectionSelect', + 'x-component-props': { filter: (collection) => collection?.options?.template === 'file' }, + 'x-decorator': 'FormItem', + default: '', + 'x-reactions': { + fulfill: { + schema: { + description: generateNTemplate('Used to store files uploaded in the Markdown editor'), + }, + }, + }, + }, + 'uiSchema.x-component-props.toolbar': { + type: 'array', + title: generateNTemplate('Toolbar'), + 'x-component': 'Select', + 'x-component-props': { + mode: 'multiple', + }, + 'x-decorator': 'FormItem', + default: defaultToolbar, + enum: [ + { value: 'emoji', label: generateNTemplate('Emoji') }, + { value: 'headings', label: generateNTemplate('Headings') }, + { value: 'bold', label: generateNTemplate('Bold') }, + { value: 'italic', label: generateNTemplate('Italic') }, + { value: 'strike', label: generateNTemplate('Strike') }, + { value: 'line', label: generateNTemplate('Line') }, + { value: 'quote', label: generateNTemplate('Quote') }, + { value: 'list', label: generateNTemplate('List') }, + { value: 'ordered-list', label: generateNTemplate('OrderedList') }, + { value: 'check', label: generateNTemplate('Check') }, + { value: 'outdent', label: generateNTemplate('Outdent') }, + { value: 'indent', label: generateNTemplate('Indent') }, + { value: 'code', label: generateNTemplate('Code') }, + { value: 'inline-code', label: generateNTemplate('InlineCode') }, + { value: 'insert-after', label: generateNTemplate('InsertAfter') }, + { value: 'insert-before', label: generateNTemplate('InsertBefore') }, + { value: 'undo', label: generateNTemplate('Undo') }, + { value: 'redo', label: generateNTemplate('Redo') }, + { value: 'upload', label: generateNTemplate('Upload') }, + { value: 'link', label: generateNTemplate('Link') }, + { value: 'record', label: generateNTemplate('Record') }, + { value: 'table', label: generateNTemplate('Table') }, + { value: 'edit-mode', label: generateNTemplate('EditMode') }, + { value: 'both', label: generateNTemplate('Both') }, + { value: 'preview', label: generateNTemplate('Preview') }, + { value: 'fullscreen', label: generateNTemplate('Fullscreen') }, + { value: 'outline', label: generateNTemplate('Outline') }, + ], + }, + }; + schemaInitialize(schema: ISchema, { block }) { + if (['Table', 'Kanban'].includes(block)) { + schema['x-component-props'] = schema['x-component-props'] || {}; + schema['x-component-props']['ellipsis'] = true; + } + } + filterable = { + operators: operators.number, + }; + titleUsable = true; +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/locale/index.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/locale/index.ts new file mode 100644 index 000000000..3527fecf3 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/locale/index.ts @@ -0,0 +1,7 @@ +import { tval } from '@nocobase/client'; + +const NAMESPACE = 'field-markdown-vditor'; + +export function generateNTemplate(key: string) { + return tval(key, { ns: NAMESPACE }) +} \ No newline at end of file diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/utils/index.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/client/utils/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/index.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/index.ts new file mode 100644 index 000000000..7e74612df --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/index.ts @@ -0,0 +1,2 @@ +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/en-US.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/en-US.ts new file mode 100644 index 000000000..d58eac370 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/en-US.ts @@ -0,0 +1,33 @@ +export default { + "Vditor": "Markdown(Vditor)", + "File collection": "File collection", + "Used to store files uploaded in the Markdown editor": "Used to store files uploaded in the Markdown editor", + "Toolbar": "Editor toolbar configuration", + "Emoji": "Emoji", + "Headings": "Headings", + "Bold": "Bold", + "Italic": "Italic", + "Strike": "Strike", + "Record": "Start Record/End Record", + "Line": "Line", + "Quote": "Quote", + "List": "List", + "OrderedList": "Order List", + "Check": "Task List", + "Outdent": "Outdent", + "Indent": "Indent", + "Code": "Code Block", + "InlineCode": "Inline Code", + "InsertAfter": "Insert Line After", + "InsertBefore": "Insert Line Before", + "Undo": "Undo", + "Redo": "Redo", + "Upload": "Upload image or file", + "Link": "Link", + "Table": "Table", + "EditMode": "Edit Mode", + "Both": "Editor & Preview", + "Preview": "Preview", + "Fullscreen": "Toggle Fullscreen", + "Outline": "Outline" +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/ko_KR.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/ko_KR.ts new file mode 100644 index 000000000..3794bb939 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/ko_KR.ts @@ -0,0 +1,33 @@ +export default { + "Vditor": "Markdown(Vditor)", + "File collection": "파일 데이터 테이블", + "Used to store files uploaded in the Markdown editor": "Markdown 편집기에 업로드된 파일을 저장하는 데 사용됩니다", + "Toolbar": "편집기 도구 모음 구성", + "Emoji": "이모지", + "Headings": "제목크기", + "Bold": "굵게", + "Italic": "기울임꼴", + "Strike": "취소선", + "Record": "녹음시작/녹음종료", + "Line": "문단나눔", + "Quote": "인용단락", + "List": "순서없는 목록", + "OrderedList": "순서있는 목록", + "Check": "체크박스", + "Outdent": "내어쓰기", + "Indent": "들여쓰기", + "Code": "코드블럭삽입", + "InlineCode": "인라인코드", + "InsertAfter": "블락 뒤로 입력", + "InsertBefore": "블락 앞으로 입력", + "Undo": "취소하기", + "Redo": "되돌리기", + "Upload": "이미지 업로드하기", + "Link": "링크", + "Table": "표삽입", + "EditMode": "편집모드", + "Both": "에디터 & 미리보기", + "Preview": "미리보기", + "Fullscreen": "전체화면", + "Outline": "개요" +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/zh-CN.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/zh-CN.ts new file mode 100644 index 000000000..4786181db --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/locale/zh-CN.ts @@ -0,0 +1,33 @@ +export default { + "Vditor": "Markdown(Vditor)", + "File collection": "文件数据表", + "Used to store files uploaded in the Markdown editor": "用于存储在Markdown编辑器中上传的文件", + "Toolbar": "编辑器工具栏配置", + "Emoji": "表情", + "Headings": "标题", + "Bold": "粗体", + "Italic": "斜体", + "Strike": "删除线", + "Record": "开始录音/结束录音", + "Line": "分割线", + "Quote": "引用", + "List": "无序列表", + "OrderedList": "有序列表", + "Check": "任务列表", + "Outdent": "列表反向缩进", + "Indent": "列表缩进", + "Code": "代码块", + "InlineCode": "行内代码", + "InsertAfter": "末尾插入行", + "InsertBefore": "起始插入行", + "Undo": "撤销", + "Redo": "重做", + "Upload": "上传图片或文件", + "Link": "链接", + "Table": "表格", + "EditMode": "切换编辑模式", + "Both": "编辑 & 预览", + "Preview": "预览", + "Fullscreen": "全屏切换", + "Outline": "大纲" +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/collections/.gitkeep b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/index.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/markdown-vditor-field.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/markdown-vditor-field.ts new file mode 100644 index 000000000..c53ca9241 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/markdown-vditor-field.ts @@ -0,0 +1,7 @@ +import { DataTypes, Field } from '@nocobase/database'; + +export class MarkdownVditorField extends Field { + get dataType() { + return DataTypes.TEXT; + } +} diff --git a/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/plugin.ts b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/plugin.ts new file mode 100644 index 000000000..d6c05326c --- /dev/null +++ b/packages/plugins/@tachybase/plugin-field-markdown-vditor/src/server/plugin.ts @@ -0,0 +1,19 @@ +import { Plugin } from '@nocobase/server'; + +export class PluginFieldMarkdownVditorServer extends Plugin { + async afterAdd() {} + + async beforeLoad() {} + + async load() {} + + async install() {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginFieldMarkdownVditorServer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0ce9bd07..1c71ff490 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3801,6 +3801,34 @@ importers: specifier: ^4.0.525 version: 4.0.759 + packages/plugins/@tachybase/plugin-field-markdown-vditor: + dependencies: + '@nocobase/client': + specifier: workspace:* + version: link:../../../core/client + '@nocobase/server': + specifier: workspace:* + version: link:../../../core/server + '@nocobase/test': + specifier: workspace:* + version: link:../../../core/test + devDependencies: + '@ant-design/icons': + specifier: ^5.3.6 + version: 5.3.6(react-dom@18.2.0)(react@18.2.0) + '@tachybase/schema': + specifier: workspace:* + version: link:../../../core/schema + antd: + specifier: ^5.16.1 + version: 5.16.1(react-dom@18.2.0)(react@18.2.0) + koa-send: + specifier: ^5.0.1 + version: 5.0.1 + vditor: + specifier: ^3.10.3 + version: 3.10.4 + packages/plugins/@tachybase/plugin-mobile-client: dependencies: '@nocobase/client': @@ -16944,6 +16972,10 @@ packages: readable-stream: 1.1.14 streamsearch: 0.1.2 + /diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + dev: true + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -29784,6 +29816,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + /vditor@3.10.4: + resolution: {integrity: sha512-NWaMom0buUvRjOCaK/jKeJEVfZNmfTgblK4+pxBoeTdiCYn5yWokcGYMh9GzHIvt5gy6FiQFc1VQvytIwyeIwA==} + dependencies: + diff-match-patch: 1.0.5 + dev: true + /verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0}