diff --git a/packages/li-analysis-assets/package.json b/packages/li-analysis-assets/package.json index 54113cbb..3610671d 100644 --- a/packages/li-analysis-assets/package.json +++ b/packages/li-analysis-assets/package.json @@ -50,7 +50,8 @@ "classnames": "^2.3.1", "dayjs": "^1.11.7", "geotiff": "^2.1.0", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "quill": "^1.3.7" }, "devDependencies": { "@ant-design/icons": "^5.0.1", diff --git a/packages/li-analysis-assets/src/widgets/AppIntroductionControl/Component.tsx b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/Component.tsx new file mode 100644 index 00000000..29835426 --- /dev/null +++ b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/Component.tsx @@ -0,0 +1,57 @@ +import { CustomControl } from '@antv/larkmap'; +import type { ImplementWidgetProps } from '@antv/li-sdk'; +import classNames from 'classnames'; +import { isEmpty } from 'lodash-es'; +import Quill from 'quill'; +import 'quill/dist/quill.snow.css'; +import React, { useEffect, useMemo, useRef } from 'react'; +import type { Properties } from './registerForm'; +import useStyle from './style'; + +const CLS_PREFIX = 'li-analysis-app-introduction'; + +export interface AppIntroductionControlProps extends Properties, ImplementWidgetProps {} + +const AppIntroductionControl: React.FC = (props) => { + const { position, width, content } = props; + const styles = useStyle(); + const informationRef = useRef(null); + const quillRef = useRef(); + + const isEffectiveContent = useMemo(() => { + if (content && content.ops && Array.isArray(content.ops)) { + const isExist = content.ops.find((item: { insert?: any }) => item.insert !== '\n'); + return isExist; + } + + return false; + }, [content]); + + useEffect(() => { + const quill = new Quill(informationRef.current!, { + modules: {}, + theme: 'bubble', + }); + quill.enable(false); + quillRef.current = quill; + }, []); + + useEffect(() => { + if (quillRef.current && isEffectiveContent) { + // @ts-ignore + quillRef.current.setContents(content); + } + }, [isEffectiveContent]); + + return ( + +
+ + ); +}; + +export default AppIntroductionControl; diff --git a/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.md b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.md new file mode 100644 index 00000000..8a521154 --- /dev/null +++ b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.md @@ -0,0 +1 @@ +## AppIntroductionControl diff --git a/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.tsx b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.tsx new file mode 100644 index 00000000..eb4cce03 --- /dev/null +++ b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/index.tsx @@ -0,0 +1,20 @@ +import { implementWidget } from '@antv/li-sdk'; +import component from './Component'; +import registerForm from './registerForm'; + +export default implementWidget({ + version: 'v0.1', + metadata: { + name: 'AppIntroductionControl', + displayName: '应用描述控件', + description: '利用富文本编辑器,添加应用描述信息', + type: 'Auto', + category: 'MapControl', + }, + defaultProperties: { + position: 'topright' as const, + width: 300, + }, + component, + registerForm, +}); diff --git a/packages/li-analysis-assets/src/widgets/AppIntroductionControl/registerForm.ts b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/registerForm.ts new file mode 100644 index 00000000..605ad074 --- /dev/null +++ b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/registerForm.ts @@ -0,0 +1,44 @@ +import type { PositionName } from '@antv/l7'; +import type { WidgetRegisterForm } from '@antv/li-sdk'; +import type { RichTextEditingType } from '@antv/li-p2'; + +/** + * 属性面板生产的数据类型定义 + */ +export type Properties = { + position?: PositionName; + width?: number; + content?: RichTextEditingType; +}; + +export default (): WidgetRegisterForm => { + // 属性面板表单的 Schema 定义,来自表单库 formily 的 Schema + const schema = { + position: { + title: '放置方位', + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'ControlPositionSelect', + default: 'topright', + }, + width: { + title: '宽度', + type: 'string', + default: 300, + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + 'x-component-props': { + addonAfter: 'px', + min: 0, + precision: 0, + }, + }, + content: { + type: 'any', + 'x-decorator': 'FormItem', + 'x-component': 'RichTextEditing', + 'x-decorator-props': {}, + }, + }; + return { schema }; +}; diff --git a/packages/li-analysis-assets/src/widgets/AppIntroductionControl/style.ts b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/style.ts new file mode 100644 index 00000000..c8e20b0a --- /dev/null +++ b/packages/li-analysis-assets/src/widgets/AppIntroductionControl/style.ts @@ -0,0 +1,24 @@ +import { css } from '@emotion/css'; +import { theme } from 'antd'; + +function useStyle() { + const { useToken } = theme; + const { token } = useToken(); + const { colorBgContainer } = token; + + return { + appIntroduction: css` + background-color: ${colorBgContainer}; + + .ql-editor { + overflow: hidden; + } + + .ql-tooltip { + display: none; + } + `, + }; +} + +export default useStyle; diff --git a/packages/li-analysis-assets/src/widgets/index.ts b/packages/li-analysis-assets/src/widgets/index.ts index 493c4758..b6dec033 100644 --- a/packages/li-analysis-assets/src/widgets/index.ts +++ b/packages/li-analysis-assets/src/widgets/index.ts @@ -1,6 +1,5 @@ // organize-imports-ignore export { default as AnalysisLayout } from './AnalysisLayout'; - export { default as FilterWidget } from './FilterWidget'; export { default as LegendWidget } from './LegendWidget'; export { default as VectorTilesLoaderControl } from './VectorTilesLoaderControl'; @@ -13,5 +12,6 @@ export { default as MeasureControl } from './MeasureControl'; export { default as TimeLine } from './TimeLine'; export { default as SpreadSheetTable } from './SpreadSheetTable'; export { default as AdministrativeSelectControl } from './AdministrativeSelectControl'; +export { default as AppIntroductionControl } from './AppIntroductionControl'; export { default as SwipeControl } from './SwipeControl'; export { default as FilterControl } from './FilterControl'; diff --git a/packages/li-editor/src/widgets/WidgetsPanel/WidgetAttribute/WidgetForm/SchemaField.tsx b/packages/li-editor/src/widgets/WidgetsPanel/WidgetAttribute/WidgetForm/SchemaField.tsx index d351d62e..29188c28 100644 --- a/packages/li-editor/src/widgets/WidgetsPanel/WidgetAttribute/WidgetForm/SchemaField.tsx +++ b/packages/li-editor/src/widgets/WidgetsPanel/WidgetAttribute/WidgetForm/SchemaField.tsx @@ -1,4 +1,5 @@ import { + RichTextEditing, ControlPositionSelect, FieldSelect, FormCollapse, @@ -38,6 +39,7 @@ const SchemaField = createSchemaField({ Space, FormGrid, ControlPositionSelect, + RichTextEditing, TimeGranularitySelect, FilterConfiguration, }, diff --git a/packages/li-p2/package.json b/packages/li-p2/package.json index 34c28047..7c0d891b 100644 --- a/packages/li-p2/package.json +++ b/packages/li-p2/package.json @@ -48,6 +48,7 @@ "colorbrewer": "^1.5.3", "dayjs": "^1.11.7", "lodash-es": "^4.17.21", + "quill": "^1.3.7", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1" }, @@ -56,11 +57,14 @@ "@antv/l7": "^2.17.2", "@antv/larkmap": "^1.4.11", "@types/lodash-es": "^4.17.6", + "@types/quill": "^2.0.14", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "antd": "^5.5.0", "dumi": "^1.1.46", "father": "^4.0.7", + "quill-image-resize-module": "^3.0.0", + "quill-image-upload": "^0.1.3", "rimraf": "^3.0.2", "typescript": "^4.7.4" }, diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.css b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.css new file mode 100644 index 00000000..1214f657 --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.css @@ -0,0 +1,99 @@ +/* 字号样式 */ +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft12']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft12']::before { + content: '12px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft14']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft14']::before { + content: '14px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft16']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft16']::before { + content: '16px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft18']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft18']::before { + content: '18px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft32']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft32']::before { + content: '32px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='ft48']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='ft48']::before { + content: '48px'; +} +.ql-size-ft12 { + font-size: 10px; +} + +.ql-size-ft14 { + font-size: 14px; +} + +.ql-size-ft16 { + font-size: 16px; +} + +.ql-size-ft18 { + font-size: 18px; +} + +.ql-size-ft32 { + font-size: 32px; +} + +.ql-size-ft48 { + font-size: 48px; +} + +/* 标题样式 */ +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { + content: 'H1'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { + content: 'H2'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { + content: 'H3'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { + content: 'H4'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { + content: 'H5'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { + content: 'H6'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label::before, +.ql-snow .ql-picker.ql-header .ql-picker-item::before { + content: '常规'; +} + +/* 链接 */ +.ql-snow .ql-tooltip[data-mode='link']::before { + content: 'url:'; +} + +.ql-snow .ql-tooltip.ql-editing a.ql-action::after { + content: '保存'; +} + +.ql-snow .ql-tooltip::before { + content: 'url:'; +} + +.ql-snow .ql-tooltip a.ql-action::after { + content: '修改'; +} + +.ql-snow .ql-tooltip a.ql-remove::before { + content: '删除'; +} diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.tsx b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.tsx new file mode 100644 index 00000000..aef69e13 --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/index.tsx @@ -0,0 +1,150 @@ +import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__'; +import { Button, Form, Input, Popover } from 'antd'; +import cls from 'classnames'; +import Quill from 'quill'; +import type { RichTextEditingType } from '../type'; +import 'quill/dist/quill.snow.css'; +import React, { useEffect, useRef, useState } from 'react'; +import './index.css'; +import useStyle from './style'; + +type EditorProps = { + value?: RichTextEditingType; + onChange: (val: RichTextEditingType) => void; +}; + +// 字体大小 +const FontAttributor = Quill.import('attributors/class/size'); +FontAttributor.whitelist = ['ft12', 'ft14', 'ft16', 'ft18', 'ft32', 'ft48']; +Quill.register(FontAttributor, true); + +const Editor: React.FC = (props) => { + const { onChange, value } = props; + const prefixCls = usePrefixCls('formily-rich-editor'); + const [wrapSSR, hashId] = useStyle(prefixCls); + const editorContainerRef = useRef(null); + const quillRef = useRef(); + const [open, setOpen] = useState(false); + const [form] = Form.useForm(); + + useEffect(() => { + const quill = new Quill(editorContainerRef.current!, { + modules: { + toolbar: { + container: [ + [ + { size: ['ft12', 'ft14', 'ft16', 'ft18', 'ft32', 'ft48'] }, //字体大小 + { header: [false, 1, 2, 3, 4, 5, 6] }, // 几级标题 + ], + [ + 'bold', // 加粗 + 'italic', // 斜体 + 'underline', //下划线, + ], + [ + { color: [] }, // 字体颜色 + { background: [] }, // 字体背景颜色 + ], + [ + { align: [] }, // 对齐方式 + { list: 'ordered' }, + { list: 'bullet' }, // 列表 + ], + [ + 'image', // 上传图片 + 'link', // 链接 + ], + ], + handlers: { + image: () => { + setOpen(true); + }, + }, + }, + }, + placeholder: '请输入...', + theme: 'snow', + }); + + quillRef.current = quill; + if (value) { + // @ts-ignore + quillRef.current.setContents(value); + } + + quill.on('editor-change', function () { + const delta = quill.getContents(); + onChange(delta); + }); + + return () => { + quill.off('editor-change', function () { + const delta = quill.getContents(); + onChange(delta); + }); + }; + }, []); + + const onFormClose = () => { + setOpen(false); + form.resetFields(); + }; + + const content = ( +
+
+ + + + + + + + +
+
+ ); + + return wrapSSR( +
+
+ setOpen(open)} + getPopupContainer={() => editorContainerRef.current!} + /> +
, + ); +}; + +export default Editor; diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/Editor/style.ts b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/style.ts new file mode 100644 index 00000000..ef1ffd4f --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/Editor/style.ts @@ -0,0 +1,148 @@ +import { genStyleHook } from '@formily/antd-v5/esm/__builtins__'; + +export default genStyleHook('rich-editor', (token) => { + const { + componentCls, + colorText, + colorBgContainer, + colorBgElevated, + colorPrimaryHover, + colorPrimaryActive, + colorBorder, + boxShadow, + } = token; + + return { + [componentCls]: { + width: '100%', + minHeight: '300px', + background: colorBgContainer, + + '.ql-editor': { + minHeight: '300px', + }, + + '.ql-snow .ql-picker-options': { + backgroundColor: colorBgElevated, + }, + + // 颜色 + '.ql-snow .ql-picker': { + color: colorText, + }, + + '.ql-snow .ql-stroke': { + stroke: colorText, + }, + + '.ql-snow .ql-fill': { + fill: colorText, + }, + + // 鼠标经过 + '.ql-snow.ql-toolbar .ql-picker-label:hover': { + color: colorPrimaryHover, + + '.ql-stroke': { + stroke: colorPrimaryHover, + }, + + '.ql-fill': { + fill: colorPrimaryHover, + }, + }, + + '.ql-snow.ql-toolbar button:hover': { + color: colorPrimaryHover, + '.ql-stroke': { + stroke: colorPrimaryHover, + }, + + '.ql-fill': { + fill: colorPrimaryHover, + }, + }, + + '.ql-snow.ql-toolbar .ql-picker-item:hover': { + color: colorPrimaryHover, + + '.ql-stroke': { + stroke: colorPrimaryHover, + }, + }, + + // 选中颜色 + '.ql-snow.ql-toolbar button.ql-active': { + color: colorPrimaryActive, + '.ql-stroke': { + stroke: colorPrimaryActive, + }, + + '.ql-fill': { + fill: colorPrimaryActive, + }, + }, + + '.ql-snow.ql-toolbar .ql-picker-label.ql-active': { + color: colorPrimaryActive, + '.ql-stroke': { + stroke: colorPrimaryActive, + }, + + '.ql-fill': { + fill: colorPrimaryActive, + }, + }, + + // 边框颜色 + '.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options': { + borderColor: colorBgElevated, + }, + + '.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label': { + borderColor: colorBorder, + }, + + '.ql-toolbar.ql-snow , .ql-container.ql-snow': { + borderColor: colorBorder, + }, + + // 链接 + '.ql-snow .ql-tooltip': { + background: colorBgElevated, + boxShadow: boxShadow, + borderColor: colorBorder, + }, + + '.ql-snow .ql-tooltip::before': { + color: colorText, + }, + + '.ql-snow .ql-tooltip input[type=text]': { + background: colorBgElevated, + borderColor: colorBorder, + color: colorText, + }, + + ".ql-snow .ql-tooltip[data-mode='link']::before": { + color: colorText, + }, + + '.ql-snow .ql-tooltip a': { + color: colorPrimaryActive, + + '&:hover': { + color: colorPrimaryHover, + }, + }, + }, + + [`${componentCls}__popover__content`]: { + background: colorBgElevated, + + '&-btn': { + textAlign: 'right', + }, + }, + }; +}); diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/demos/default.tsx b/packages/li-p2/src/components/Formily/RichTextEditing/demos/default.tsx new file mode 100644 index 00000000..e0358f60 --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/demos/default.tsx @@ -0,0 +1,51 @@ +import { RichTextEditing } from '@antv/li-p2'; +import { Form, FormItem } from '@formily/antd-v5'; +import type { Form as FormInstance } from '@formily/core'; +import { createForm, onFormValuesChange } from '@formily/core'; +import { createSchemaField, FormConsumer } from '@formily/react'; +import React from 'react'; + +const form = createForm({ + initialValues: {}, + effects() { + onFormValuesChange((formIns: FormInstance) => { + console.log('formIns.values: ', formIns.values); + }); + }, +}); + +const SchemaField = createSchemaField({ + components: { + FormItem, + RichTextEditing, + }, +}); + +const schema = { + type: 'object', + properties: { + slider: { + type: 'any', + title: '编辑器', + 'x-decorator': 'FormItem', + 'x-component': 'RichTextEditing', + 'x-decorator-props': {}, + }, + }, +}; + +export default () => { + return ( +
+ + + + {() => ( + +
{JSON.stringify(form.values, null, 2)}
+
+ )} +
+ + ); +}; diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/index.md b/packages/li-p2/src/components/Formily/RichTextEditing/index.md new file mode 100644 index 00000000..d5fcd457 --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/index.md @@ -0,0 +1,24 @@ +--- +toc: content +order: 9 +group: + title: formily 组件 + order: 1 +nav: + title: 组件 + path: /components +--- + +# 富文本编辑器 - RichTextEditing + +## 介绍 + +富文本编辑器 + +## 代码演示 + +### 默认示例 + + + + diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/index.tsx b/packages/li-p2/src/components/Formily/RichTextEditing/index.tsx new file mode 100644 index 00000000..3ede115d --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/index.tsx @@ -0,0 +1,60 @@ +import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__'; +import { connect } from '@formily/react'; +import { Button, Modal } from 'antd'; +import cls from 'classnames'; +import React, { useState } from 'react'; +import useStyle from './style'; +import 'quill/dist/quill.snow.css'; +import Editor from './Editor'; +import type { RichTextEditingType } from './type'; + +type InternalRichTextEditingProps = { + value?: RichTextEditingType; + onChange: (val?: RichTextEditingType) => void; +}; + +const InternalRichTextEditing: React.FC = (props) => { + const { onChange, value } = props; + const prefixCls = usePrefixCls('formily-rich-text-editing'); + const [wrapSSR, hashId] = useStyle(prefixCls); + const [isModalOpen, setIsModalOpen] = useState(false); + const [content, setContent] = useState(); + + const handleOk = () => { + onChange(content); + setIsModalOpen(false); + }; + + const onSubmit = (val: RichTextEditingType) => { + setContent(val); + }; + + const handleCancel = () => { + setIsModalOpen(false); + }; + + return wrapSSR( +
+ + + + +
, + ); +}; + +const RichTextEditing = connect(InternalRichTextEditing); + +export default RichTextEditing; diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/style.ts b/packages/li-p2/src/components/Formily/RichTextEditing/style.ts new file mode 100644 index 00000000..87ea895e --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/style.ts @@ -0,0 +1,15 @@ +import { genStyleHook } from '@formily/antd-v5/esm/__builtins__'; + +export default genStyleHook('rich-text-editing', (token) => { + const { componentCls } = token; + + return { + [componentCls]: { + width: '100%', + + [`${componentCls}__btn`]: { + width: '100%', + }, + }, + }; +}); diff --git a/packages/li-p2/src/components/Formily/RichTextEditing/type.ts b/packages/li-p2/src/components/Formily/RichTextEditing/type.ts new file mode 100644 index 00000000..85e3fa55 --- /dev/null +++ b/packages/li-p2/src/components/Formily/RichTextEditing/type.ts @@ -0,0 +1,8 @@ +type Op = { + insert?: string | Record; + attributes?: string | Record; +}; + +export type RichTextEditingType = { + ops?: Op[] | { ops: Op[] }; +}; diff --git a/packages/li-p2/src/components/Formily/index.ts b/packages/li-p2/src/components/Formily/index.ts index 448fe9d3..7fbcf50d 100644 --- a/packages/li-p2/src/components/Formily/index.ts +++ b/packages/li-p2/src/components/Formily/index.ts @@ -20,3 +20,5 @@ export { default as RibbonSelect } from './RibbonSelect'; export { default as Slider } from './Slider'; export { default as SliderRange } from './SliderRange'; export { default as TimeGranularitySelect } from './TimeGranularitySelect'; +export { default as RichTextEditing } from './RichTextEditing'; +export type { RichTextEditingType } from './RichTextEditing/type';