mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-30 16:56:07 +08:00
feat: 2.0 markdown block (#7613)
* feat: markdown& liquid * feat: markdown block * feat: markdown block * fix: bug * refactor: markdown vditor * refactor: update package.json * fix: bug * fix: bug * Merge branch 'develop' into task-6928 * fix: bug * fix: bug * fix: style improve * fix: style improve * fix: style improve * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug
This commit is contained in:
@@ -76,7 +76,9 @@
|
||||
"react-to-print": "^2.14.7",
|
||||
"sanitize-html": "2.13.0",
|
||||
"tabulator-tables": "^6.3.1",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"vditor": "^3.10.3",
|
||||
"liquidjs": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { Liquid } from 'liquidjs';
|
||||
|
||||
export class LiquidEngine extends Liquid {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
extname: '.liquid',
|
||||
cache: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
// 注册国际化过滤器
|
||||
this.registerFilter('t', (key, locale = 'en', dict = {}) => {
|
||||
if (!key) return '';
|
||||
if (!dict) return key;
|
||||
|
||||
// 优先当前语言,否则 fallback 到英文
|
||||
return dict[key]?.[locale] || dict[key]?.['en'] || key;
|
||||
});
|
||||
|
||||
// (可选)注册一个日志过滤器,方便调试
|
||||
this.registerFilter('log', (value) => {
|
||||
console.log('[Liquid log]', value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将路径数组转为 Liquid 模板上下文对象
|
||||
* @param {string[]} paths - 如 ['ctx.user.name', 'ctx.order.total']
|
||||
* @returns {object} 形如 { user: { name: '{{ctx.user.name}}' }, order: {...} }
|
||||
*/
|
||||
transformLiquidContext(paths = []) {
|
||||
const result = {};
|
||||
|
||||
for (const fullPath of paths) {
|
||||
const path = fullPath.replace(/^ctx\./, '');
|
||||
const keys = path.split('.');
|
||||
|
||||
let current = result;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const isLast = i === keys.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
current[key] = `{{${fullPath}}}`;
|
||||
} else {
|
||||
current[key] = current[key] || {};
|
||||
current = current[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板
|
||||
* @param {string} template - Liquid 模板字符串
|
||||
* @param {object} context - 模板上下文变量
|
||||
* @returns {Promise<string>} 渲染后的字符串
|
||||
*/
|
||||
async render(template, context = {}) {
|
||||
try {
|
||||
return await this.parseAndRender(template, context);
|
||||
} catch (err) {
|
||||
console.error('[Liquid] 模板解析失败:', err);
|
||||
return `<pre style="color:red;">Liquid 模板错误:${err.message}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并步骤:获取变量 -> 构建 context -> 解析 -> 渲染
|
||||
* @param {string} template Liquid 模板字符串
|
||||
* @param {context} ctx flowContext
|
||||
*/
|
||||
async renderWithFullContext(template, ctx) {
|
||||
try {
|
||||
// 1️⃣ 分析模板中的变量
|
||||
const vars = await this.fullVariables(template);
|
||||
|
||||
// 2️⃣ 构造 Liquid context
|
||||
const liquidContext = this.transformLiquidContext(vars);
|
||||
|
||||
// 3️⃣ 只解析变量
|
||||
const resolvedCtx = await ctx.resolveJsonTemplate(liquidContext);
|
||||
|
||||
// 4️⃣ 渲染模板
|
||||
return await this.render(template, { ctx: resolvedCtx });
|
||||
} catch (err) {
|
||||
console.error('[Liquid] renderWithFullContext 错误:', err);
|
||||
return `<pre style="color:red;">Liquid 渲染错误:${err.message}</pre>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Popover } from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Vditor from 'vditor';
|
||||
import { useCDN } from './useCDN';
|
||||
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>(null);
|
||||
const { wrapSSR, componentCls, hashId } = useStyle();
|
||||
const cdn = useCDN();
|
||||
|
||||
useEffect(() => {
|
||||
Vditor.preview(containerRef.current, props.value ?? '', {
|
||||
mode: 'light',
|
||||
cdn,
|
||||
});
|
||||
setTimeout(() => {
|
||||
containerRef.current?.querySelectorAll('img').forEach((img: HTMLImageElement) => {
|
||||
img.style.cursor = 'zoom-in';
|
||||
img.addEventListener('click', () => {
|
||||
openCustomPreview(img.src);
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}, [props.value]);
|
||||
|
||||
return wrapSSR(
|
||||
<span className={`${hashId} ${componentCls}`}>
|
||||
<span ref={containerRef} style={{ border: 'none', ...(props?.style ?? {}) }} />
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
function openCustomPreview(src: string) {
|
||||
if (document.getElementById('custom-image-preview')) return;
|
||||
|
||||
// 创建容器
|
||||
const overlay = document.createElement('span');
|
||||
overlay.id = 'custom-image-preview';
|
||||
Object.assign(overlay.style, {
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: '9999',
|
||||
cursor: 'zoom-out',
|
||||
});
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
Object.assign(img.style, {
|
||||
maxWidth: '90%',
|
||||
maxHeight: '90%',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
|
||||
transition: 'transform 0.2s',
|
||||
cursor: 'zoom-out',
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', () => {
|
||||
document.body.removeChild(overlay);
|
||||
});
|
||||
|
||||
overlay.appendChild(img);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
export const Display = (props) => {
|
||||
const { value, textOnly = true } = props;
|
||||
const cdn = useCDN();
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [ellipsis, setEllipsis] = useState(false);
|
||||
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const elRef = useRef<HTMLDivElement>();
|
||||
useEffect(() => {
|
||||
if (!props.value) return;
|
||||
if (textOnly) {
|
||||
Vditor.md2html(props.value, {
|
||||
mode: 'light',
|
||||
cdn,
|
||||
})
|
||||
.then((html) => {
|
||||
setText(convertToText(html));
|
||||
})
|
||||
.catch(() => setText(''));
|
||||
}
|
||||
}, [props.value, textOnly]);
|
||||
|
||||
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}
|
||||
getPopupContainer={() => document.getElementsByClassName('ant-drawer-content')?.[0] as HTMLElement}
|
||||
onOpenChange={(visible) => {
|
||||
setPopoverVisible(ellipsis && visible);
|
||||
}}
|
||||
overlayStyle={{ maxWidth: 400, maxHeight: 450, overflow: 'auto' }}
|
||||
content={<DisplayInner value={value} />}
|
||||
>
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{textOnly ? (
|
||||
text
|
||||
) : (
|
||||
<div
|
||||
className={css`
|
||||
.vditor-reset {
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<DisplayInner value={value} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
if (textOnly) {
|
||||
return text;
|
||||
}
|
||||
return <DisplayInner value={value} />;
|
||||
};
|
||||
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useAPIClient, useCompile, usePlugin, useZIndexContext, getZIndex } from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import { FlowContextSelector, useFlowContext, useFlowModel } from '@nocobase/flow-engine';
|
||||
import React, { useEffect, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
import { useCDN } from './useCDN';
|
||||
import useStyle from './style';
|
||||
|
||||
const defaultToolbar = [
|
||||
'headings',
|
||||
'bold',
|
||||
'italic',
|
||||
'strike',
|
||||
'link',
|
||||
'list',
|
||||
'ordered-list',
|
||||
'check',
|
||||
'quote',
|
||||
'line',
|
||||
'code',
|
||||
'inline-code',
|
||||
'upload',
|
||||
'fullscreen',
|
||||
];
|
||||
|
||||
const NAMESPACE = 'block-markdown';
|
||||
|
||||
const locales = ['en_US', 'fr_FR', 'pt_BR', 'ja_JP', 'ko_KR', 'ru_RU', 'sv_SE', 'zh_CN', 'zh_TW'];
|
||||
|
||||
const Edit = (props) => {
|
||||
const { disabled, onChange, value, fileCollection, toolbar, vditorRef } = props;
|
||||
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const vdRef = useRef<Vditor>();
|
||||
const vdFullscreen = useRef(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerParentRef = useRef<HTMLDivElement>(null);
|
||||
const apiClient = useAPIClient();
|
||||
const cdn = useCDN();
|
||||
const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
|
||||
const locale = apiClient.auth.locale || 'en-US';
|
||||
const fileManagerPlugin: any = usePlugin('@nocobase/plugin-file-manager');
|
||||
const compile = useCompile();
|
||||
const compileRef = useRef(compile);
|
||||
compileRef.current = compile;
|
||||
const { t } = useTranslation();
|
||||
const parentZIndex = useZIndexContext();
|
||||
|
||||
const zIndex = getZIndex('drawer', parentZIndex + 1000, 0);
|
||||
|
||||
const lang: any = useMemo(() => {
|
||||
const currentLang = locale.replace(/-/g, '_');
|
||||
if (locales.includes(currentLang)) {
|
||||
return currentLang;
|
||||
}
|
||||
return 'en_US';
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const toolbarConfig = toolbar ?? defaultToolbar;
|
||||
|
||||
const vditor = new Vditor(containerRef.current, {
|
||||
value: value ?? '',
|
||||
lang,
|
||||
cache: { enable: false },
|
||||
undoDelay: 0,
|
||||
preview: { math: { engine: 'KaTeX' } },
|
||||
toolbar: toolbarConfig,
|
||||
fullscreen: { index: 1200 },
|
||||
cdn,
|
||||
minHeight: 200,
|
||||
after: () => {
|
||||
vdRef.current = vditor;
|
||||
setEditorReady(true); // Notify that the editor is ready
|
||||
vditor.setValue(value ?? '');
|
||||
if (disabled) {
|
||||
vditor.disabled();
|
||||
} else {
|
||||
vditor.enable();
|
||||
}
|
||||
},
|
||||
input(value) {
|
||||
onChange(value);
|
||||
},
|
||||
upload: {
|
||||
multiple: false,
|
||||
fieldName: 'file',
|
||||
async handler(files: File[]) {
|
||||
const file = files[0];
|
||||
|
||||
// Need to ensure focus is in the current input box before uploading
|
||||
vditor.focus();
|
||||
|
||||
const { data: checkData } = await apiClient.resource('vditor').check({
|
||||
fileCollectionName: fileCollection,
|
||||
});
|
||||
|
||||
if (!checkData?.data?.isSupportToUploadFiles) {
|
||||
vditor.tip(
|
||||
t('vditor.uploadError.message', { ns: NAMESPACE, storageTitle: checkData.data.storage?.title }),
|
||||
0,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
vditor.tip(t('uploading'), 0);
|
||||
const { data, errorMessage } = await fileManagerPlugin.uploadFile({
|
||||
file,
|
||||
fileCollectionName: fileCollection,
|
||||
storageId: checkData?.data?.storage?.id,
|
||||
storageType: checkData?.data?.storage?.type,
|
||||
storageRules: checkData?.data?.storage?.rules,
|
||||
});
|
||||
|
||||
if (errorMessage) {
|
||||
vditor.tip(compileRef.current(errorMessage), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
vditor.tip(t('Response data is empty', { ns: NAMESPACE }), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = data.filename;
|
||||
const fileUrl = data.url;
|
||||
|
||||
// Check if the uploaded file is an image
|
||||
const isImage = file.type.startsWith('image/');
|
||||
|
||||
if (isImage) {
|
||||
// Insert as an image - will be displayed in the editor
|
||||
vditor.insertValue(``);
|
||||
} else {
|
||||
// For non-image files, insert as a download link
|
||||
vditor.insertValue(`[${fileName}](${fileUrl})`);
|
||||
}
|
||||
|
||||
// hide the tip
|
||||
vditor.tip(t(''), 10);
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
vditorRef.current = vditor;
|
||||
|
||||
return () => {
|
||||
vdRef.current?.destroy();
|
||||
vdRef.current = undefined;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [toolbar?.join(',')]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && vdRef.current) {
|
||||
const editor = vdRef.current;
|
||||
if (value !== editor.getValue()) {
|
||||
editor.setValue(value ?? '');
|
||||
// editor.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); // Move cursor to the end
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [value, editorReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && vdRef.current) {
|
||||
if (disabled) {
|
||||
vdRef.current.disabled();
|
||||
} else {
|
||||
vdRef.current.enable();
|
||||
}
|
||||
}
|
||||
}, [disabled, editorReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerParentRef.current) return;
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of Array.from(mutation.addedNodes)) {
|
||||
if (node instanceof HTMLElement && node.classList.contains('vditor-img')) {
|
||||
// 移动图片预览层到弹窗容器
|
||||
containerParentRef.current.appendChild(node);
|
||||
node.style.zIndex = String(zIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [zIndex]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
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 ref={containerRef}></div>
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
|
||||
export interface MarkdownWithContextSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (v: string) => void;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown 与变量选择器的组合,紧凑排版,边框无缝拼接。
|
||||
*/
|
||||
export const MarkdownWithContextSelector: React.FC<MarkdownWithContextSelectorProps> = ({
|
||||
value = '',
|
||||
onChange,
|
||||
placeholder,
|
||||
style,
|
||||
}) => {
|
||||
const flowCtx = useFlowContext();
|
||||
const [innerValue, setInnerValue] = useState<string>(value || '');
|
||||
const ref = useRef<TextAreaRef>(null);
|
||||
const isConfigMode = !!flowCtx.model.flowEngine?.flowSettings?.enabled;
|
||||
// 外部 value 变化时同步内部显示
|
||||
useEffect(() => {
|
||||
setInnerValue(value || '');
|
||||
}, [value]);
|
||||
|
||||
const handleTextChange = useCallback(
|
||||
(e) => {
|
||||
const next = e ?? '';
|
||||
setInnerValue(next);
|
||||
onChange?.(next);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
// 将指定文本插入到当前光标位置
|
||||
const insertAtCaret = useCallback(
|
||||
(toInsert: string) => {
|
||||
const editor = ref.current as any;
|
||||
if (!editor) {
|
||||
console.warn('Vditor 尚未初始化,无法插入文本');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔹 在当前光标位置插入文本
|
||||
editor.insertValue(toInsert);
|
||||
|
||||
// 🔹 同步外部状态
|
||||
const next = editor.getValue();
|
||||
setInnerValue(next);
|
||||
onChange?.(next);
|
||||
|
||||
// 🔹 保持聚焦
|
||||
requestAnimationFrame(() => {
|
||||
editor.focus();
|
||||
});
|
||||
},
|
||||
[innerValue, onChange],
|
||||
);
|
||||
|
||||
const handleVariableSelected = useCallback(
|
||||
(varValue: string) => {
|
||||
if (!varValue) return;
|
||||
insertAtCaret(varValue);
|
||||
},
|
||||
[insertAtCaret],
|
||||
);
|
||||
|
||||
// 使用函数形式提供变量树,保证与运行时上下文一致
|
||||
const metaTree = useMemo(() => () => flowCtx.getPropertyMetaTree?.(), [flowCtx]);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', ...style }}>
|
||||
<Edit
|
||||
vditorRef={ref}
|
||||
value={innerValue}
|
||||
onChange={handleTextChange}
|
||||
placeholder={placeholder}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{isConfigMode && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
lineHeight: 0,
|
||||
}}
|
||||
>
|
||||
{/* 参考 1.0:小号按钮 + 非 hover 去掉右/上边框,背景透明,贴合右上角 */}
|
||||
<FlowContextSelector metaTree={metaTree} onChange={(val) => handleVariableSelected(val)} onlyLeafSelectable>
|
||||
<Button
|
||||
type="default"
|
||||
className={css`
|
||||
font-style: italic;
|
||||
font-family: 'New York, Times New Roman, Times, serif';
|
||||
line-height: 1;
|
||||
&:not(:hover) {
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
`}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</FlowContextSelector>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Display } from './Display';
|
||||
import { MarkdownWithContextSelector as Edit } from './Edit';
|
||||
|
||||
export class Markdown {
|
||||
/**
|
||||
* 渲染 Markdown
|
||||
* @param {string} text - Markdown 文本
|
||||
* @param {object} props - 其他属性
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
render(text, props) {
|
||||
if (!text) return null;
|
||||
|
||||
try {
|
||||
return <Display value={text} {...props} />;
|
||||
} catch (err) {
|
||||
console.error('渲染失败:', err);
|
||||
return <pre style={{ color: 'red' }}>Markdown 渲染错误:{err.message}</pre>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染可编辑的 Markdown 组件
|
||||
* @param {object} props - 编辑器属性
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
edit(props) {
|
||||
try {
|
||||
return <Edit {...props} />;
|
||||
} catch (err) {
|
||||
return <pre style={{ color: 'red' }}>Markdown 编辑器加载错误:{err.message}</pre>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { genStyleHook } from '../../../schema-component/antd/__builtins__';
|
||||
|
||||
export default genStyleHook('nb-markdown-vditor', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset', padding: `10px !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' },
|
||||
'.vditor-ir pre.vditor-reset': {
|
||||
backgroundColor: `${token.colorBgContainer}!important`,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { usePlugin } from '@nocobase/client';
|
||||
import { useFlowEngine } from '@nocobase/flow-engine';
|
||||
|
||||
export const useCDN = () => {
|
||||
const flowEngine = useFlowEngine();
|
||||
const app = flowEngine.context.app;
|
||||
const plugin: any = usePlugin('@nocobase/plugin-block-markdown');
|
||||
if (!plugin.dependencyLoaded) {
|
||||
plugin.initVditorDependency(app);
|
||||
plugin.dependencyLoaded = true;
|
||||
}
|
||||
return plugin.getCDN(app);
|
||||
};
|
||||
@@ -17,6 +17,8 @@ import { FlowRoute } from './FlowPage';
|
||||
import * as models from './models';
|
||||
import * as filterFormActions from './models/blocks/filter-manager/flow-actions';
|
||||
import { DynamicFlowsIcon } from './components/DynamicFlowsIcon';
|
||||
import { Markdown } from './common/Markdown/Markdown';
|
||||
import { LiquidEngine } from './common/Liquid';
|
||||
|
||||
export class PluginFlowEngine extends Plugin {
|
||||
async load() {
|
||||
@@ -44,6 +46,19 @@ export class PluginFlowEngine extends Plugin {
|
||||
},
|
||||
sort: 0,
|
||||
});
|
||||
// 实例化一个全局 Markdown 解析器
|
||||
const markdownInstance = new Markdown();
|
||||
this.flowEngine.context.defineProperty('markdown', {
|
||||
get: () => markdownInstance,
|
||||
});
|
||||
|
||||
// 创建全局实例
|
||||
const liquidInstance = new LiquidEngine();
|
||||
|
||||
// 注册到全局上下文
|
||||
this.flowEngine.context.defineProperty('liquid', {
|
||||
get: () => liquidInstance,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,79 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useParseMarkdown, convertToText } from './util';
|
||||
import { useMarkdownStyles } from './style';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
export const DisplayMarkdown = (props) => {
|
||||
const { textOnly } = props;
|
||||
const { textOnly, overflowMode, style } = props;
|
||||
const markdownClass = useMarkdownStyles();
|
||||
const { html = '' } = useParseMarkdown(props.value);
|
||||
const text = convertToText(html);
|
||||
const value = (
|
||||
const text: any = convertToText(html);
|
||||
const isEllipsis = overflowMode === 'ellipsis';
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isOverflowed, setIsOverflowed] = useState(false);
|
||||
|
||||
// 检测内容是否溢出
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const el = contentRef.current;
|
||||
const overflowed = el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
|
||||
setIsOverflowed(overflowed);
|
||||
}
|
||||
}, [html, style, overflowMode]);
|
||||
|
||||
// 通用样式(用于 ellipsis 模式)
|
||||
const baseStyle: React.CSSProperties = {
|
||||
...(style || {}),
|
||||
...(isEllipsis
|
||||
? {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 1, // 可改为多行省略
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
cursor: 'default',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={` ${markdownClass} nb-markdown nb-markdown-default nb-markdown-table`}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={props.style}
|
||||
ref={contentRef}
|
||||
className={`${markdownClass} nb-markdown`}
|
||||
dangerouslySetInnerHTML={{ __html: textOnly ? text : html }}
|
||||
style={baseStyle}
|
||||
/>
|
||||
);
|
||||
|
||||
return <>{textOnly ? text : value}</>;
|
||||
return isEllipsis && isOverflowed ? (
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
maxHeight: 500,
|
||||
overflowY: 'auto',
|
||||
padding: 10,
|
||||
color: '#000',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
overlayInnerStyle={{
|
||||
background: '#fff',
|
||||
color: '#000',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
color="#fff"
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@ export class DisplayTitleFieldModel extends FieldModel {
|
||||
rootClassName: css`
|
||||
.ant-tooltip-inner {
|
||||
color: #000;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
`,
|
||||
color: '#fff',
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
||||
@@ -0,0 +1 @@
|
||||
# @nocobase/plugin-block-markdown
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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,21 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-block-markdown",
|
||||
"version": "2.0.0-alpha.21",
|
||||
"main": "dist/server/index.js",
|
||||
"displayName": "Block: Markdown",
|
||||
"displayName.zh-CN": "Markdown",
|
||||
"description": "Provide Markdown block",
|
||||
"description.zh-CN": "提供Markdown 区块",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://docs.nocobase.com/handbook/block-markdown",
|
||||
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-markdown",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "1.x",
|
||||
"@nocobase/server": "1.x",
|
||||
"@nocobase/test": "1.x"
|
||||
},
|
||||
"keywords": [
|
||||
"Blocks"
|
||||
]
|
||||
}
|
||||
@@ -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,249 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
// CSS modules
|
||||
type CSSModuleClasses = { readonly [key: string]: string };
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sass' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.less' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.styl' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.stylus' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.pcss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
|
||||
// CSS
|
||||
declare module '*.css' { }
|
||||
declare module '*.scss' { }
|
||||
declare module '*.sass' { }
|
||||
declare module '*.less' { }
|
||||
declare module '*.styl' { }
|
||||
declare module '*.stylus' { }
|
||||
declare module '*.pcss' { }
|
||||
declare module '*.sss' { }
|
||||
|
||||
// Built-in asset types
|
||||
// see `src/node/constants.ts`
|
||||
|
||||
// images
|
||||
declare module '*.apng' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jfif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ico' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// media
|
||||
declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webm' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ogg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.wav' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.flac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.aac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.opus' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mov' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.m4a' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.vtt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// fonts
|
||||
declare module '*.woff' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.woff2' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.eot' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ttf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.otf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// other
|
||||
declare module '*.webmanifest' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pdf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// wasm?init
|
||||
declare module '*.wasm?init' {
|
||||
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||
export default initWasm;
|
||||
}
|
||||
|
||||
// web worker
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&inline' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&inline' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?inline' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { default } from './plugin';
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { tExpr as _tExpr, useFlowEngine } from '@nocobase/flow-engine';
|
||||
// @ts-ignore
|
||||
import pkg from './../../package.json';
|
||||
|
||||
export function useT() {
|
||||
const engine = useFlowEngine();
|
||||
return (str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] });
|
||||
}
|
||||
|
||||
export function tExpr(key: string) {
|
||||
return _tExpr(key, { ns: [pkg.name, 'client'] });
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { BlockModel, css } from '@nocobase/client';
|
||||
import { escapeT } from '@nocobase/flow-engine';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export class MarkdownBlockModel extends BlockModel {
|
||||
render() {
|
||||
const { content } = this.props;
|
||||
return (
|
||||
<Card
|
||||
className={css`
|
||||
.ant-card-body {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownBlockModel.define({
|
||||
label: escapeT('Markdown'),
|
||||
});
|
||||
|
||||
MarkdownBlockModel.registerFlow({
|
||||
key: 'markdownBlockSettings',
|
||||
title: escapeT('Markdown block settings', { ns: 'block-markdown' }),
|
||||
steps: {
|
||||
editMarkdown: {
|
||||
title: escapeT('Edit markdown'),
|
||||
uiSchema(ctx) {
|
||||
const t = ctx.t;
|
||||
const descriptionContent = (
|
||||
<>
|
||||
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
|
||||
{t('Syntax references')}:
|
||||
</span>
|
||||
<a href={`https://shopify.github.io/liquid/basics/introduction/`} target="_blank" rel="noreferrer">
|
||||
Liquid
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
content: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': (props) => ctx.markdown.edit(props),
|
||||
description: descriptionContent,
|
||||
},
|
||||
};
|
||||
},
|
||||
useRawParams: true,
|
||||
defaultParams: {
|
||||
content: "{{ 'This is a demo text, **supports Markdown syntax**.' | t: locale, i18n }}",
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
const content = params.content;
|
||||
try {
|
||||
const result = await ctx.liquid.renderWithFullContext(content, ctx);
|
||||
// 解析 Markdown
|
||||
const mdContent = ctx.markdown.render(ctx.t(result), { textOnly: false });
|
||||
ctx.model.setProps({
|
||||
content: mdContent,
|
||||
});
|
||||
} catch (error) {
|
||||
ctx.model.setProps({
|
||||
content: <pre>{`渲染失败: ${error}`}</pre>,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { MarkdownBlockModel } from './MarkdownBlockModel';
|
||||
|
||||
export { MarkdownBlockModel };
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { MarkdownBlockModel } from './models';
|
||||
|
||||
export class PluginBlockMarkdownClient extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({
|
||||
MarkdownBlockModel,
|
||||
});
|
||||
}
|
||||
|
||||
getCDN() {
|
||||
return this.app.getPublicPath() + 'static/plugins/@nocobase/plugin-block-markdown/dist/client/vditor';
|
||||
}
|
||||
|
||||
initVditorDependency() {
|
||||
const cdn = this.getCDN();
|
||||
try {
|
||||
const vditorDepdencePrefix = 'plugin-block-markdown-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 PluginBlockMarkdownClient;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './server';
|
||||
export { default } from './server';
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{ "Markdown block settings": "Markdown 区块设置" }
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export { default } from './plugin';
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
|
||||
export class PluginBlockMarkdownServer extends Plugin {
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
async load() {}
|
||||
|
||||
async install() {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginBlockMarkdownServer;
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DisplayItemModel, escapeT } from '@nocobase/flow-engine';
|
||||
import { DisplayTitleFieldModel, tval } from '@nocobase/client';
|
||||
import { Display } from './components/Display';
|
||||
|
||||
export class DisplayVditorFieldModel extends DisplayTitleFieldModel {
|
||||
public renderComponent(value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return <Display value={value} ellipsis={this.props.textOnly} />;
|
||||
}
|
||||
}
|
||||
DisplayVditorFieldModel.define({
|
||||
label: escapeT('MarkdownVditor'),
|
||||
});
|
||||
|
||||
DisplayVditorFieldModel.registerFlow({
|
||||
key: 'markdownVditorSettings',
|
||||
title: tval('Content settings'),
|
||||
sort: 200,
|
||||
steps: {
|
||||
renderMode: {
|
||||
use: 'renderMode',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
DisplayItemModel.bindModelToInterface('DisplayVditorFieldModel', ['vditor'], { isDefault: true });
|
||||
@@ -9,10 +9,9 @@
|
||||
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import 'vditor/dist/index.css';
|
||||
// import { MarkdownVditor } from './components';
|
||||
import { lazy } from '@nocobase/client';
|
||||
import { VditorFieldModel } from './VditorFieldModel';
|
||||
import { DisplayVditorFieldModel } from './DisplayVditorFieldModel';
|
||||
import { VditorFieldModel } from './models/VditorFieldModel';
|
||||
import { DisplayVditorFieldModel } from './models/DisplayVditorFieldModel';
|
||||
const { MarkdownVditor } = lazy(() => import('./components'), 'MarkdownVditor');
|
||||
|
||||
import { MarkdownVditorFieldInterface } from './interfaces/markdown-vditor';
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { DisplayItemModel, escapeT } from '@nocobase/flow-engine';
|
||||
import { DisplayTitleFieldModel, tval } from '@nocobase/client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const Display = ({ value, markdown, liquid, t, textOnly, ctx, overflowMode }) => {
|
||||
const [content, setContent] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await liquid.renderWithFullContext(value, ctx);
|
||||
const html = markdown.render(t(result), { ellipsis: overflowMode === 'ellipsis', textOnly });
|
||||
setContent(html);
|
||||
} catch (err) {
|
||||
setContent(`<pre style="color:red;"> 渲染错误: ${err.message}</pre>`);
|
||||
}
|
||||
})();
|
||||
}, [value, textOnly, overflowMode]);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export class DisplayVditorFieldModel extends DisplayTitleFieldModel {
|
||||
public renderComponent(value) {
|
||||
if (!value) return null;
|
||||
const { markdown, liquid, t } = this.context;
|
||||
const { textOnly, overflowMode } = this.props;
|
||||
return (
|
||||
<Display
|
||||
value={value}
|
||||
markdown={markdown}
|
||||
liquid={liquid}
|
||||
t={t}
|
||||
textOnly={textOnly}
|
||||
ctx={this.context}
|
||||
overflowMode={overflowMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DisplayVditorFieldModel.define({
|
||||
label: escapeT('MarkdownVditor'),
|
||||
});
|
||||
|
||||
DisplayVditorFieldModel.registerFlow({
|
||||
key: 'markdownVditorSettings',
|
||||
title: tval('Content settings'),
|
||||
sort: 200,
|
||||
steps: {
|
||||
renderMode: {
|
||||
use: 'renderMode',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
DisplayItemModel.bindModelToInterface('DisplayVditorFieldModel', ['vditor'], { isDefault: true });
|
||||
+3
-3
@@ -7,13 +7,13 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { FieldModel } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { largeField, EditableItemModel } from '@nocobase/flow-engine';
|
||||
import { Edit } from './components/Edit';
|
||||
@largeField()
|
||||
export class VditorFieldModel extends FieldModel {
|
||||
render() {
|
||||
return <Edit {...this.props} />;
|
||||
const markdown = this.context.markdown;
|
||||
|
||||
return markdown.edit(this.props);
|
||||
}
|
||||
}
|
||||
EditableItemModel.bindModelToInterface('VditorFieldModel', ['vditor'], { isDefault: true });
|
||||
@@ -24,6 +24,7 @@
|
||||
"@nocobase/plugin-block-grid-card": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-iframe": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-list": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-markdown": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-reference": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-template": "2.0.0-alpha.21",
|
||||
"@nocobase/plugin-block-workbench": "2.0.0-alpha.21",
|
||||
@@ -113,6 +114,7 @@
|
||||
"@nocobase/plugin-block-workbench",
|
||||
"@nocobase/plugin-block-list",
|
||||
"@nocobase/plugin-block-grid-card",
|
||||
"@nocobase/plugin-block-markdown",
|
||||
"@nocobase/plugin-calendar",
|
||||
"@nocobase/plugin-client",
|
||||
"@nocobase/plugin-collection-sql",
|
||||
|
||||
Reference in New Issue
Block a user