diff --git a/package.json b/package.json index 90091132aec758effed12aa21d1e5b563d3c666a..ba2e92456559a13b28206e578135d8a1f71a23b2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,12 @@ "dayjs": "^1.11.7", "element-plus": "^2.3.5", "lodash-es": "^4.17.21", + "monaco-editor": "^0.36.1", + "@monaco-editor/loader": "^1.3.2", + "@wangeditor/core": "^1.1.19", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.11", + "@wangeditor/plugin-ctrl-enter": "^1.1.2", "pinia": "^2.1.3", "qs": "^6.11.2", "qx-util": "^0.4.8", @@ -95,4 +101,4 @@ "*.ts": "eslint --fix", "*.scss": "stylelint --custom-syntax=postcss-scss" } -} +} \ No newline at end of file diff --git a/src/control/index.ts b/src/control/index.ts index 55c0faec19fe4883e985138fc77008a905971c17..75aac7b7d15b8f96a1fe859a6bde495765256aae 100644 --- a/src/control/index.ts +++ b/src/control/index.ts @@ -10,3 +10,4 @@ export * from './tree'; export * from './pickup-view-panel'; export * from './tab-exp-panel'; export * from './exp-bar'; +export * from './search-bar'; diff --git a/src/control/search-bar/index.ts b/src/control/search-bar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..90f67e644c5f838d9ddbb1c7f286835a61bddfa6 --- /dev/null +++ b/src/control/search-bar/index.ts @@ -0,0 +1,21 @@ +import { ControlType, registerControlProvider } from '@ibiz-template/runtime'; +import { App } from 'vue'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { SearchBarControl } from './search-bar'; +import { SearchBarProvider } from './search-bar.provider'; + +export * from './search-bar.provider'; +export * from './search-bar.controller'; + +export const IBizSearchBarControl = withInstall( + SearchBarControl, + function (v: App) { + v.component(SearchBarControl.name, SearchBarControl); + registerControlProvider( + ControlType.SEARCHBAR, + () => new SearchBarProvider(), + ); + }, +); + +export default IBizSearchBarControl; diff --git a/src/control/search-bar/search-bar.controller.ts b/src/control/search-bar/search-bar.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..2bd1a4e52022a8ecd4d4fa57f32c1b6edbe5b8df --- /dev/null +++ b/src/control/search-bar/search-bar.controller.ts @@ -0,0 +1,51 @@ +import { + ControlController, + ISearchBarState, + ISearchBarEvent, + ISearchBarController, +} from '@ibiz-template/runtime'; +import { ISearchBar } from '@ibiz/model-core'; + +/** + * 搜索栏控制器 + * + * @author chitanda + * @date 2022-07-24 15:07:07 + * @export + * @class SearchBarController + * @extends {ControlController} + */ +export class SearchBarController + extends ControlController + implements ISearchBarController +{ + protected initState(): void { + super.initState(); + this.state.query = ''; + } + + protected async doCreated(): Promise { + await super.doCreated(); + } + + /** + * 处理输入 + * @param {string} val + * @return {*} + * @author: zhujiamin + * @Date: 2023-06-01 18:08:07 + */ + handleInput(val: string) { + this.state.query = val; + } + + /** + * 处理搜索 + * @return {*} + * @author: zhujiamin + * @Date: 2023-06-01 18:08:18 + */ + handleSearch() { + this.evt.emit('onSearch', undefined); + } +} diff --git a/src/control/search-bar/search-bar.provider.ts b/src/control/search-bar/search-bar.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..adaa06c00573a261d2afb5ee0a2c3916238ddc52 --- /dev/null +++ b/src/control/search-bar/search-bar.provider.ts @@ -0,0 +1,14 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +/** + * 搜索栏适配器 + * + * @author lxm + * @date 2022-10-25 18:10:57 + * @export + * @class SearchBarProvider + * @implements {IControlProvider} + */ +export class SearchBarProvider implements IControlProvider { + component: string = 'IBizSearchBarControl'; +} diff --git a/src/control/search-bar/search-bar.scss b/src/control/search-bar/search-bar.scss new file mode 100644 index 0000000000000000000000000000000000000000..a8d5e0c80e156cc2b8b25e588474fe06ec32e10d --- /dev/null +++ b/src/control/search-bar/search-bar.scss @@ -0,0 +1,6 @@ +@include b(control-searchbar) { + @include set-component-css-var('control-searchbar', $control-searchbar); + @include b(control-searchbar-quick-search) { + width: getCssVar('control-searchbar', 'quick-search-width'); + } +} diff --git a/src/control/search-bar/search-bar.tsx b/src/control/search-bar/search-bar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e51b2ff4604ee5e34bc5e95dd83afca4bba3344c --- /dev/null +++ b/src/control/search-bar/search-bar.tsx @@ -0,0 +1,67 @@ +import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; +import { computed, defineComponent, PropType } from 'vue'; +import { ISearchBar } from '@ibiz/model-core'; +import { debounce } from 'lodash-es'; +import { SearchBarController } from './search-bar.controller'; +import './search-bar.scss'; + +export const SearchBarControl = defineComponent({ + name: 'IBizSearchBarControl', + props: { + modelData: { + type: Object as PropType, + required: true, + }, + context: { type: Object as PropType, required: true }, + params: { type: Object as PropType, default: () => ({}) }, + }, + setup() { + const c = useControlController( + (...args) => new SearchBarController(...args), + ); + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + + const onSearch = () => { + c.handleSearch(); + }; + + const debounceSearch = debounce(() => { + if (onSearch) { + onSearch(); + } + }, 500); + + const onInput = (value: string) => { + c.handleInput(value); + debounceSearch(); + }; + + const cssVars = computed(() => { + if (c.model.quickSearchWidth) { + return ns.cssVarBlock({ + 'quick-search-width': `${c.model.quickSearchWidth}px`, + }); + } + return {}; + }); + + return { c, ns, onInput, onSearch, cssVars }; + }, + render() { + return ( + + {this.c.model.enableQuickSearch && ( + + )} + + ); + }, +}); diff --git a/src/control/toolbar/toolbar.tsx b/src/control/toolbar/toolbar.tsx index b1684fd01e26ad058534998e9d8fbbbac41b12f9..b8cd59ec4afc043300c77ac5703d73c60b621213 100644 --- a/src/control/toolbar/toolbar.tsx +++ b/src/control/toolbar/toolbar.tsx @@ -97,23 +97,35 @@ export const ToolbarControl = defineComponent({ class={[this.ns.e('item'), this.ns.e('item-items')]} > - - {btnContent(item, state.viewMode)} - - - {(item as IDETBGroupItem).detoolbarItems?.map(item2 => { + {{ + default: () => { return ( - - this.handleClick(item2, e) - } - > - {btnContent(item2, state.viewMode)} - + + {btnContent(item, state.viewMode)} + ); - })} - + }, + dropdown: () => { + return ( + + {(item as IDETBGroupItem).detoolbarItems?.map( + item2 => { + return ( + + this.handleClick(item2, e) + } + > + {btnContent(item2, state.viewMode)} + + ); + }, + )} + + ); + }, + }} ); diff --git a/src/editor/code/code-editor.controller.ts b/src/editor/code/code-editor.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9c10513ec60171469d9154676702e8318e6113b --- /dev/null +++ b/src/editor/code/code-editor.controller.ts @@ -0,0 +1,11 @@ +import { EditorController } from '@ibiz-template/runtime'; +import { ICode } from '@ibiz/model-core'; + +/** + * 代码框编辑器控制器 + * + * @export + * @class CodeEditorController + * @extends {EditorController} + */ +export class CodeEditorController extends EditorController {} diff --git a/src/editor/code/code-editor.provider.ts b/src/editor/code/code-editor.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef87832c466c783a66876790e3944e779ca466c5 --- /dev/null +++ b/src/editor/code/code-editor.provider.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { ICode } from '@ibiz/model-core'; +import { CodeEditorController } from './code-editor.controller'; + +/** + * 代码框编辑器适配器 + * + * @author lxm + * @date 2022-09-19 22:09:03 + * @export + * @class CodeEditorProvider + * @implements {EditorProvider} + */ +export class CodeEditorProvider implements IEditorProvider { + formEditor: string = 'IBizCode'; + + gridEditor: string = 'IBizCode'; + + async createController( + editorModel: ICode, + parentController: IEditorContainerController, + ): Promise { + const c = new CodeEditorController(editorModel, parentController); + await c.init(); + return c; + } +} diff --git a/src/editor/code/index.ts b/src/editor/code/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a157ec554a9691559bd880855d86ceccc2ca8d14 --- /dev/null +++ b/src/editor/code/index.ts @@ -0,0 +1,3 @@ +export { IBizCode } from './monaco-editor/monaco-editor'; +export * from './code-editor.controller'; +export * from './code-editor.provider'; diff --git a/src/editor/code/monaco-editor/monaco-editor.scss b/src/editor/code/monaco-editor/monaco-editor.scss new file mode 100644 index 0000000000000000000000000000000000000000..c246aac256bf709878e0b171f794ba7c9af0e663 --- /dev/null +++ b/src/editor/code/monaco-editor/monaco-editor.scss @@ -0,0 +1,6 @@ +@include b(code) { + display: inline-block; + width: 100%; + height: 100%; + min-height: 200px; +} diff --git a/src/editor/code/monaco-editor/monaco-editor.tsx b/src/editor/code/monaco-editor/monaco-editor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2c4354a0cbb76410ba3db93a2f6f7210a9a5a465 --- /dev/null +++ b/src/editor/code/monaco-editor/monaco-editor.tsx @@ -0,0 +1,99 @@ +import { + defineComponent, + nextTick, + onMounted, + onUnmounted, + ref, + watch, +} from 'vue'; +import './monaco-editor.scss'; +import { + getCodeProps, + getEditorEmits, + useNamespace, +} from '@ibiz-template/vue3-util'; +import * as monaco from 'monaco-editor'; +import loader from '@monaco-editor/loader'; +import { CodeEditorController } from '../code-editor.controller'; + +export const IBizCode = defineComponent({ + name: 'IBizCode', + props: getCodeProps(), + emits: getEditorEmits(), + setup(props, { emit }) { + const ns = useNamespace('code'); + + const currentVal = ref(''); + + watch( + () => props.value, + (newVal, oldVal) => { + if (newVal !== oldVal) { + if (!newVal) { + currentVal.value = ''; + } else { + currentVal.value = newVal; + } + } + }, + { immediate: true }, + ); + + const codeEditBox = ref(); + + let editor: monaco.editor.IStandaloneCodeEditor; + + const editorInit = () => { + nextTick(() => { + loader.init().then(loaderMonaco => { + // 初始化编辑器 + if (!editor) { + editor = loaderMonaco.editor.create(codeEditBox.value, { + value: currentVal.value, // 编辑器初始显示文字 + language: props.language, // 语言支持自行查阅demo + theme: props.theme, // 官方自带三种主题vs, hc-black, or vs-dark + foldingStrategy: 'indentation', + renderLineHighlight: 'all', // 行亮 + selectOnLineNumbers: true, // 显示行号 + minimap: { + enabled: true, + }, + readOnly: false, // 只读 + fontSize: 16, // 字体大小 + scrollBeyondLastLine: false, // 取消代码后面一大段空白 + overviewRulerBorder: false, // 不要滚动条的边框 + }); + setTimeout(() => { + editor.layout(); + }); + } else { + editor.setValue(''); + } + + // 监听值的变化 + editor.onDidChangeModelContent(() => { + currentVal.value = editor.getValue(); + emit('change', currentVal.value); + }); + + window.addEventListener('resize', () => { + editor.layout(); + }); + }); + }); + }; + + onMounted(() => { + editorInit(); + }); + + onUnmounted(() => { + editor?.dispose(); + }); + + return { ns, currentVal, codeEditBox }; + }, + render() { + return
; + }, +}); diff --git a/src/editor/html/html-editor.controller.ts b/src/editor/html/html-editor.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..734c873139556f6bcc208a1f166828ff254f5d42 --- /dev/null +++ b/src/editor/html/html-editor.controller.ts @@ -0,0 +1,124 @@ +import { + downloadFileFromBlob, + RuntimeError, + RuntimeModelError, +} from '@ibiz-template/core'; +import { convertNavData, EditorController } from '@ibiz-template/runtime'; +import { IHtml } from '@ibiz/model-core'; +import qs from 'qs'; + +/** + * html框编辑器控制器 + * + * @export + * @class HtmlEditorController + * @extends {EditorController} + */ +export class HtmlEditorController extends EditorController { + /** + * 上传参数 + */ + public uploadParams?: IParams; + + /** + * 下载参数 + */ + public exportParams?: IParams; + + protected async onInit(): Promise { + await super.onInit(); + + if (this.editorParams) { + const { uploadparams, exportparams } = this.editorParams; + + if (uploadparams) { + try { + this.uploadParams = JSON.parse(uploadparams); + } catch (error) { + throw new RuntimeModelError( + uploadparams, + `配置uploadparams没有按标准JSON格式`, + ); + } + } + if (exportparams) { + try { + this.exportParams = JSON.parse(exportparams); + } catch (error) { + throw new RuntimeModelError( + exportparams, + `配置exportparams没有按标准JSON格式`, + ); + } + } + } + } + + /** + * 计算文件的上传路径和下载路径 + * 下载路径文件id用%fileId%占位,替换即可 + * 配置编辑器参数uploadParams和exportParams时,会像导航参数一样动态添加对应的参数到url上 + * + * @author lxm + * @date 2022-11-17 13:11:43 + * @param {IData} data + * @returns {*} {{ uploadUrl: string; downloadUrl: string }} + */ + calcBaseUrl(data: IData): { uploadUrl: string; downloadUrl: string } { + let uploadUrl = `${ibiz.env.baseUrl}/${ibiz.env.appId}${ibiz.env.uploadFileUrl}`; + let downloadUrl = `${ibiz.env.baseUrl}/${ibiz.env.appId}${ibiz.env.downloadFileUrl}/%fileId%`; + let uploadParams: IParams = {}; + let exportParams: IParams = {}; + if (this.uploadParams) { + uploadParams = convertNavData( + this.uploadParams, + this.context, + this.params, + data, + ); + } + if (this.exportParams) { + exportParams = convertNavData( + this.exportParams, + this.context, + this.params, + data, + ); + } + uploadUrl += qs.stringify(uploadParams, { addQueryPrefix: true }); + downloadUrl += qs.stringify(exportParams, { addQueryPrefix: true }); + + return { uploadUrl, downloadUrl }; + } + + /** + * 请求url获取文件流,并用JS触发文件下载 + * + * @author lxm + * @date 2022-11-17 14:11:09 + * @param {string} url + * @param {IData} file + */ + fileDownload(file: { url: string; name: string }): void { + // 发送get请求 + ibiz.net + .request(file.url, { + method: 'get', + responseType: 'blob', + baseURL: '', // 已经有baseURL了,这里无需再写 + }) + .then((response: IData) => { + if (response.status !== 200) { + throw new RuntimeError('下载文件失败'); + } + // 请求成功,后台返回的是一个文件流 + if (!response.data) { + throw new RuntimeError('文件流数据不存在'); + } else { + // 获取文件名 + const fileName = file.name; + downloadFileFromBlob(response.data, fileName); + } + }); + } +} diff --git a/src/editor/html/html-editor.provider.ts b/src/editor/html/html-editor.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6c1be38d66ce84d8945c48a626c9df83d04e653 --- /dev/null +++ b/src/editor/html/html-editor.provider.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { IHtml } from '@ibiz/model-core'; +import { HtmlEditorController } from './html-editor.controller'; + +/** + * html框编辑器适配器 + * + * @author lxm + * @date 2022-09-19 22:09:03 + * @export + * @class HtmlEditorProvider + * @implements {EditorProvider} + */ +export class HtmlEditorProvider implements IEditorProvider { + formEditor: string = 'IBizHtml'; + + gridEditor: string = 'IBizHtml'; + + async createController( + editorModel: IHtml, + parentController: IEditorContainerController, + ): Promise { + const c = new HtmlEditorController(editorModel, parentController); + await c.init(); + return c; + } +} diff --git a/src/editor/html/index.ts b/src/editor/html/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5169b035aa5eec85d9544748468d7db48fc2d63c --- /dev/null +++ b/src/editor/html/index.ts @@ -0,0 +1,3 @@ +export { IBizHtml } from './wang-editor/wang-editor'; +export * from './html-editor.controller'; +export * from './html-editor.provider'; diff --git a/src/editor/html/wang-editor/wang-editor.scss b/src/editor/html/wang-editor/wang-editor.scss new file mode 100644 index 0000000000000000000000000000000000000000..67c823a67853ca8a188aade074e5ec398c2bf971 --- /dev/null +++ b/src/editor/html/wang-editor/wang-editor.scss @@ -0,0 +1,8 @@ +@include b(html) { + display: inline-block; + width: 100%; + + .w-e-full-screen-container { + z-index: 9999; + } +} diff --git a/src/editor/html/wang-editor/wang-editor.tsx b/src/editor/html/wang-editor/wang-editor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ef1f4c04d72633f90a8ba85cb94a7dc319440b34 --- /dev/null +++ b/src/editor/html/wang-editor/wang-editor.tsx @@ -0,0 +1,343 @@ +import { + onBeforeUnmount, + ref, + shallowRef, + onMounted, + watch, + Ref, + defineComponent, +} from 'vue'; +import { Editor, Toolbar } from '@wangeditor/editor-for-vue'; +import { IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; +import type { IDomEditor } from '@wangeditor/editor'; +import { getCookie } from 'qx-util'; +import { + getEditorEmits, + getHtmlProps, + useNamespace, +} from '@ibiz-template/vue3-util'; +import { HtmlEditorController } from '../html-editor.controller'; +import './wang-editor.scss'; +import '@wangeditor/editor/dist/css/style.css'; + +type InsertFnType = (_url: string, _alt: string, _href: string) => void; + +export const IBizHtml = defineComponent({ + name: 'IBizHtml', + props: getHtmlProps(), + emits: getEditorEmits(), + setup(props, { emit }) { + const ns = useNamespace('html'); + + const c = props.controller!; + + // 编辑器实例,必须用 shallowRef,重要! + const editorRef = shallowRef(); + + // 内容 HTML + const valueHtml = ref(''); + + // 请求头 + const headers: Ref = ref({ + Authorization: `Bearer ${getCookie('access_token')}`, + }); + + // 上传文件路径 + const uploadUrl: Ref = ref(''); + + // 下载文件路径 + const downloadUrl: Ref = ref(''); + + // data响应式变更基础路径 + watch( + () => props.data, + newVal => { + if (newVal) { + const urls = c.calcBaseUrl(newVal); + uploadUrl.value = urls.uploadUrl; + downloadUrl.value = urls.downloadUrl; + } + }, + { immediate: true, deep: true }, + ); + + // 自定义校验链接 + const customCheckLinkFn = ( + text: string, + url: string, + ): string | boolean | undefined => { + if (!url) { + return; + } + // if (url.indexOf('http') !== 0) { + // return '链接必须以 http/https 开头'; + // } + return true; + + // 返回值有三种选择: + // 1. 返回 true ,说明检查通过,编辑器将正常插入链接 + // 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串) + // 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入。但不会提示任何信息 + }; + + // 自定义转换链接 url + const customParseLinkUrl = (url: string): string => { + // if (url.indexOf('http') !== 0) { + // return `http://${url}`; + // } + return url; + }; + + // 工具栏配置 + const toolbarConfig: Partial = { + excludeKeys: ['group-video'], + }; + + // 编辑器配置 + const editorConfig: Partial = { + placeholder: '请输入内容...', + readOnly: props.readonly, + MENU_CONF: { + // 图片上传 + uploadImage: { + // 上传地址 + server: uploadUrl.value, + + // form-data fieldName ,默认值 'wangeditor-uploaded-image' + fieldName: 'file', + + // 单个文件的最大体积限制,默认为 2M + maxFileSize: 10 * 1024 * 1024, // 10M + + // 最多可上传几个文件,默认为 100 + maxNumberOfFiles: 10, + + // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] + allowedFileTypes: [], + + // 自定义增加 http header + headers: headers.value, + + // 跨域是否传递 cookie ,默认为 false + withCredentials: true, + + // 上传之前触发 + onBeforeUpload(file: File) { + // TS 语法 + // onBeforeUpload(file) { // JS 语法 + // file 选中的文件,格式如 { key: file } + return file; + + // 可以 return + // 1. return file 或者 new 一个 file ,接下来将上传 + // 2. return false ,不上传这个 file + }, + + // 上传进度的回调函数 + onProgress(progress: number) { + console.log('progress', progress); + }, + + // 单个文件上传成功之后 + onSuccess(file: File, res: IData) { + console.log(`${file.name} 上传成功`, res); + }, + + // 单个文件上传失败 + onFailed(file: File, res: IData) { + console.log(`${file.name} 上传失败`, res); + }, + + // 上传错误,或者触发 timeout 超时 + onError(file: File, err: IData, res: IData) { + console.log(`${file.name} 上传出错`, err, res); + }, + + // 自定义插入图片 + async customInsert(res: IData, insertFn: InsertFnType) { + const url = downloadUrl.value.replace('%fileId%', res.id); + const alt = res.filename; + // 从 res 中找到 url alt href ,然后插入图片 + insertFn(url, alt, ''); + }, + }, + // 插入链接 + insertLink: { + checkLink: customCheckLinkFn, // 也支持 async 函数 + parseLinkUrl: customParseLinkUrl, // 也支持 async 函数 + }, + // 更新链接 + editLink: { + checkLink: customCheckLinkFn, // 也支持 async 函数 + parseLinkUrl: customParseLinkUrl, // 也支持 async 函数 + }, + }, + }; + + // 组件销毁时,也及时销毁编辑器,重要! + onBeforeUnmount(() => { + const editor = editorRef.value; + if (editor == null) return; + + editor.destroy(); + }); + + // 编辑器回调函数 + // 编辑器创建完毕时的回调函数 + const handleCreated = (editor: IDomEditor) => { + editorRef.value = editor; // 记录 editor 实例,重要! + // 配置菜单 + // setTimeout(() => { + // const toolbar = DomEditor.getToolbar(editor); + // const curToolbarConfig = toolbar?.getConfig(); + // console.log(curToolbarConfig?.toolbarKeys); // 当前菜单排序和分组 + // }, 3000); + }; + // 编辑器内容、选区变化时的回调函数 + const handleChange = (editor: IDomEditor) => { + // console.log('change:', editor.getHtml()); + const html = editor.getHtml(); + // wangEditor初始值抛空字符串给后台 + const emitValue = html === '


' ? '' : html; + emit('change', emitValue); + }; + // 编辑器销毁时的回调函数。调用 editor.destroy() 即可销毁编辑器 + const handleDestroyed = (_editor: IDomEditor) => { + // console.log('destroyed', _editor); + }; + // 编辑器 focus 时的回调函数 + const handleFocus = (_editor: IDomEditor) => { + // console.log('focus', _editor); + }; + // 编辑器 blur 时的回调函数。 + const handleBlur = (_editor: IDomEditor) => { + // console.log('blur', _editor); + }; + // 自定义编辑器 alert + const customAlert = (info: string, type: string) => { + // eslint-disable-next-line no-alert + alert(`【自定义提示】${type} - ${info}`); + }; + // 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑 + const customPaste = ( + editor: IDomEditor, + event: ClipboardEvent, + callback: Function, + ) => { + // 返回值(注意,vue 事件的返回值,不能用 return) + // callback(false); // 返回 false ,阻止默认粘贴行为 + callback(true); // 返回 true ,继续默认的粘贴行为 + }; + + // 插入文本 + const insertText = (str: string) => { + const editor = editorRef.value; + if (editor == null) return; + + editor.insertText(str); + }; + + // 获取非格式化的 html + const printHtml = () => { + const editor = editorRef.value; + if (editor == null) return; + console.log(editor.getHtml()); + }; + + // 禁用编辑器 + const disable = () => { + const editor = editorRef.value; + if (editor == null) return; + editor.disable(); + }; + + // 取消禁用编辑器 + const enable = () => { + const editor = editorRef.value; + if (editor == null) return; + editor.enable(); + }; + + onMounted(() => { + // 监听值变化赋值 + watch( + () => props.value, + (newVal, oldVal) => { + if ( + newVal !== oldVal && + (typeof props.value === 'string' || newVal === null) + ) { + if (newVal === null) { + valueHtml.value = ''; + } else { + valueHtml.value = newVal as string; + } + } + }, + { immediate: true }, + ); + + // 监听disabled禁用 + watch( + () => props.disabled, + (newVal, oldVal) => { + if (newVal !== oldVal) { + if (newVal === true) { + disable(); + } else { + enable(); + } + } + }, + { immediate: true }, + ); + }); + + return { + ns, + editorRef, + mode: 'default', + valueHtml, + toolbarConfig, + editorConfig, + handleCreated, + handleChange, + handleDestroyed, + handleFocus, + handleBlur, + customAlert, + customPaste, + insertText, + printHtml, + disable, + enable, + }; + }, + render() { + return ( +
+
+ + +
+
+ ); + }, +}); diff --git a/src/editor/html/wang-editor/wang-edtor.d.ts b/src/editor/html/wang-editor/wang-edtor.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fbe3e549dd3d93e5b4374d881916376bde75382 --- /dev/null +++ b/src/editor/html/wang-editor/wang-edtor.d.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// 声明合并,扩展 Editor 的类型定义 +declare module '@wangeditor/editor-for-vue' { + const Editor = import('vue').DefineComponent< + { + /** 编辑器模式 */ + mode: { + type: StringConstructor; + default: string; + }; + /** 编辑器默认内容 */ + defaultContent: { + type: PropType; + default: never[]; + }; + defaultHtml: { + type: StringConstructor; + default: string; + }; + /** 编辑器默认配置 */ + defaultConfig: { + type: PropType>; + default: {}; + }; + modelValue: { + type: StringConstructor; + default: string; + }; + }, + { + box: import('vue').Ref; + }, + unknown, + {}, + {}, + import('vue').ComponentOptionsMixin, + import('vue').ComponentOptionsMixin, + Record, + string, + import('vue').VNodeProps & + import('vue').AllowedComponentProps & + import('vue').ComponentCustomProps, + Readonly< + { + mode?: unknown; + defaultContent?: unknown; + defaultHtml?: unknown; + defaultConfig?: unknown; + modelValue?: unknown; + } & { + mode: string; + defaultContent: SlateDescendant[]; + defaultHtml: string; + defaultConfig: Partial; + modelValue: string; + } & {} + >, + { + mode: string; + defaultContent: SlateDescendant[]; + defaultHtml: string; + defaultConfig: Partial; + modelValue: string; + } + >; + + const Toolbar = import('vue').DefineComponent< + { + editor: { + type: PropType; + }; + /** 编辑器模式 */ + mode: { + type: StringConstructor; + default: string; + }; + /** 编辑器默认配置 */ + defaultConfig: { + type: PropType>; + default: {}; + }; + }, + { + selector: import('vue').Ref; + }, + unknown, + {}, + {}, + import('vue').ComponentOptionsMixin, + import('vue').ComponentOptionsMixin, + Record, + string, + import('vue').VNodeProps & + import('vue').AllowedComponentProps & + import('vue').ComponentCustomProps, + Readonly< + { + editor?: unknown; + mode?: unknown; + defaultConfig?: unknown; + } & { + mode: string; + defaultConfig: Partial; + } & { + editor?: IDomEditor | undefined; + } + >, + { + mode: string; + defaultConfig: Partial; + } + >; +} diff --git a/src/editor/index.ts b/src/editor/index.ts index 5f885397ba186eb8873c8975b53136d6e49487f0..77e9c7714fc25178e4ade5996a3b72d8663936ff 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -40,6 +40,8 @@ import { NumberRangeEditorProvider, } from './number-range'; import { IBizDateRangePicker, DateRangeEditorProvider } from './date-range'; +import { CodeEditorProvider, IBizCode } from './code'; +import { HtmlEditorProvider, IBizHtml } from './html'; export const IBizEditor = { install: (v: App) => { @@ -73,6 +75,8 @@ export const IBizEditor = { v.component(IBizPickerSelectView.name, IBizPickerSelectView); v.component(IBizNumberRangePicker.name, IBizNumberRangePicker); v.component(IBizDateRangePicker.name, IBizDateRangePicker); + v.component(IBizCode.name, IBizCode); + v.component(IBizHtml.name, IBizHtml); // 标签 registerEditorProvider('SPAN', () => new SpanEditorProvider()); @@ -231,6 +235,10 @@ export const IBizEditor = { 'DATERANGE_NOTIME', () => new DateRangeEditorProvider(), ); + // 代码编辑框 + registerEditorProvider('CODE', () => new CodeEditorProvider()); + // 富文本HTML编辑框 + registerEditorProvider('HTMLEDITOR', () => new HtmlEditorProvider()); }, }; diff --git a/src/index.ts b/src/index.ts index d4e9ee919a4febe68e6402e0bce724d8f5d44e28..c9f610e4c71e435ead1079a9c4691d3c4dd796bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { IBizGridExpBarControl, IBizDataViewExpBarControl, IBizTreeExpBarControl, + IBizSearchBarControl, } from './control'; import IBizEditor from './editor'; import { IBizView } from './view'; @@ -54,6 +55,7 @@ export default { v.use(IBizCaptionBarControl); v.use(IBizTabExpPanelControl); v.use(IBizTreeExpBarControl); + v.use(IBizSearchBarControl); // 编辑器 v.use(IBizEditor); },