feat: support vditor (#894)
Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#894
This commit is contained in:
		
							parent
							
								
									8a789c70f1
								
							
						
					
					
						commit
						fda9853509
					
				@ -25,6 +25,22 @@ const handleErrorMessage = (error, notification) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const errorCache = new Map();
 | 
					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 {
 | 
					export class APIClient extends APIClientSDK {
 | 
				
			||||||
  services: Record<string, Result<any, any>> = {};
 | 
					  services: Record<string, Result<any, any>> = {};
 | 
				
			||||||
  silence = false;
 | 
					  silence = false;
 | 
				
			||||||
@ -36,6 +52,17 @@ export class APIClient extends APIClientSDK {
 | 
				
			|||||||
    return this.services[uid];
 | 
					    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() {
 | 
					  interceptors() {
 | 
				
			||||||
    this.axios.interceptors.request.use((config) => {
 | 
					    this.axios.interceptors.request.use((config) => {
 | 
				
			||||||
      config.headers['X-With-ACL-Meta'] = true;
 | 
					      config.headers['X-With-ACL-Meta'] = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -167,6 +167,19 @@ export class Application {
 | 
				
			|||||||
    return this.options.publicPath || '/';
 | 
					    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) {
 | 
					  getRouteUrl(pathname: string) {
 | 
				
			||||||
    return this.options.publicPath.replace(/\/$/g, '') + pathname;
 | 
					    return this.options.publicPath.replace(/\/$/g, '') + pathname;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -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() {
 | 
					  interceptors() {
 | 
				
			||||||
    this.axios.interceptors.request.use((config) => {
 | 
					    this.axios.interceptors.request.use((config) => {
 | 
				
			||||||
      config.paramsSerializer = (params) => {
 | 
					      config.paramsSerializer = (params) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					/node_modules
 | 
				
			||||||
 | 
					/src
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					# @nocobase/plugin-field-markdown-vditor
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-field-markdown-vditor/client.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from './dist/client';
 | 
				
			||||||
 | 
					export { default } from './dist/client';
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					module.exports = require('./dist/client/index.js');
 | 
				
			||||||
@ -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)"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@tachybase/plugin-field-markdown-vditor/server.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from './dist/server';
 | 
				
			||||||
 | 
					export { default } from './dist/server';
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					module.exports = require('./dist/server/index.js');
 | 
				
			||||||
@ -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<HTMLDivElement>();
 | 
				
			||||||
 | 
					  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(
 | 
				
			||||||
 | 
					    <div className={`${hashId} ${componentCls}`}>
 | 
				
			||||||
 | 
					      <div ref={containerRef} style={{ border: 'none', ...(props?.style ?? {}) }} />
 | 
				
			||||||
 | 
					    </div>,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Display = withDynamicSchemaProps((props) => {
 | 
				
			||||||
 | 
					  const field = useField<Field>();
 | 
				
			||||||
 | 
					  const value = props.value ?? field.value;
 | 
				
			||||||
 | 
					  const cdn = useCDN();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const containerRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [popoverVisible, setPopoverVisible] = useState(false);
 | 
				
			||||||
 | 
					  const [ellipsis, setEllipsis] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [text, setText] = useState('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const elRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 (
 | 
				
			||||||
 | 
					      <Popover
 | 
				
			||||||
 | 
					        open={popoverVisible}
 | 
				
			||||||
 | 
					        onOpenChange={(visible) => {
 | 
				
			||||||
 | 
					          setPopoverVisible(ellipsis && visible);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        content={<DisplayInner value={value} style={{ maxWidth: 500, maxHeight: 400, overflowY: 'auto' }} />}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          ref={elRef}
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            overflow: 'hidden',
 | 
				
			||||||
 | 
					            overflowWrap: 'break-word',
 | 
				
			||||||
 | 
					            textOverflow: 'ellipsis',
 | 
				
			||||||
 | 
					            whiteSpace: 'nowrap',
 | 
				
			||||||
 | 
					            wordBreak: 'break-all',
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          onMouseEnter={(e) => {
 | 
				
			||||||
 | 
					            const el = e.target as any;
 | 
				
			||||||
 | 
					            const isShowTooltips = isOverflowTooltip();
 | 
				
			||||||
 | 
					            if (isShowTooltips) {
 | 
				
			||||||
 | 
					              setEllipsis(el.scrollWidth >= el.clientWidth);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {text}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Popover>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <DisplayInner value={value} />;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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<Vditor>();
 | 
				
			||||||
 | 
					  const vdFullscreen = useRef(false);
 | 
				
			||||||
 | 
					  const containerRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					  const containerParentRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					  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(
 | 
				
			||||||
 | 
					    <div ref={containerParentRef} className={`${hashId} ${containerClassName}`}>
 | 
				
			||||||
 | 
					      <div id={hashId} ref={containerRef}></div>
 | 
				
			||||||
 | 
					    </div>,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { usePlugin } from '@nocobase/client';
 | 
				
			||||||
 | 
					import { PluginFieldMarkdownVditorClient } from '../';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useCDN = () => {
 | 
				
			||||||
 | 
					  const plugin = usePlugin(PluginFieldMarkdownVditorClient);
 | 
				
			||||||
 | 
					  return plugin.getCDN();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -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;
 | 
				
			||||||
@ -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' },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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;
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { tval } from '@nocobase/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NAMESPACE = 'field-markdown-vditor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function generateNTemplate(key: string) {
 | 
				
			||||||
 | 
					  return tval(key, { ns: NAMESPACE })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from './server';
 | 
				
			||||||
 | 
					export { default } from './server';
 | 
				
			||||||
@ -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"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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": "개요"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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": "大纲"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './plugin';
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { DataTypes, Field } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MarkdownVditorField extends Field {
 | 
				
			||||||
 | 
					  get dataType() {
 | 
				
			||||||
 | 
					    return DataTypes.TEXT;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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;
 | 
				
			||||||
@ -3801,6 +3801,34 @@ importers:
 | 
				
			|||||||
        specifier: ^4.0.525
 | 
					        specifier: ^4.0.525
 | 
				
			||||||
        version: 4.0.759
 | 
					        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:
 | 
					  packages/plugins/@tachybase/plugin-mobile-client:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@nocobase/client':
 | 
					      '@nocobase/client':
 | 
				
			||||||
@ -16944,6 +16972,10 @@ packages:
 | 
				
			|||||||
      readable-stream: 1.1.14
 | 
					      readable-stream: 1.1.14
 | 
				
			||||||
      streamsearch: 0.1.2
 | 
					      streamsearch: 0.1.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /diff-match-patch@1.0.5:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
 | 
				
			||||||
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /diff-sequences@29.6.3:
 | 
					  /diff-sequences@29.6.3:
 | 
				
			||||||
    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
 | 
					    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
 | 
				
			||||||
    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 | 
					    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==}
 | 
					    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
 | 
				
			||||||
    engines: {node: '>= 0.8'}
 | 
					    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:
 | 
					  /verror@1.10.0:
 | 
				
			||||||
    resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
 | 
					    resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
 | 
				
			||||||
    engines: {'0': node >=0.6.0}
 | 
					    engines: {'0': node >=0.6.0}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user