diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b65f97fee9e8ca6af7123fe90ea997e9557897..0532beba0f67eca99f006b1c1ca9c0fd7b7c3eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - 搜索表单识别按钮样式和按钮位置 - 图表单序列时不出图例 - 搜索栏、导航栏快速搜索的占位符取实体搜索属性组成 +- 支持devtool调试工具 ### Fixed diff --git a/src/devtool/devtool-action.scss b/src/devtool/devtool-action.scss new file mode 100644 index 0000000000000000000000000000000000000000..0597168ada54119e54588ccb983c81f37305ba18 --- /dev/null +++ b/src/devtool/devtool-action.scss @@ -0,0 +1,124 @@ +@include b(devtool-action) { + position: absolute; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + height: 100%; + font-size: 13px; + color: #fff; + background: rgb(96 98 102 / 80%); + transition: all .5s; +} + +@include b(devtool-action-collapse) { + flex: 0 0 auto; + width: 320px; + padding: 0 12px; + margin-top: 44px; + margin-bottom: 12px; + overflow: auto; + border-right: 1px solid #999; +} + +@include b(devtool-action-collapse-item) { + display: flex; + align-items: center; + padding-bottom: 4px; + + @include e(key) { + flex: 0 0 auto; + width: 40%; + overflow: hidden; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + + &:hover { + color: #dbe6ff; + } + } + + @include e(value) { + flex: 0 0 auto; + width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + + &:hover { + color: #dbe6ff; + } + } +} + +@include b(devtool-action-container) { + display: flex; + flex: 0 0 auto; + flex-direction: column; + width: 400px; + padding: 12px; +} + +@include b(devtool-action-header) { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: flex-end; + height: 20px; + margin-bottom: 12px; + + ion-icon { + margin-right: 12px; + font-size: 20px; + cursor: pointer; + + &:last-child { + margin-right: 0; + } + } +} + +@include b(devtool-action-content) { + display: flex; + flex: 0 0 auto; + flex-direction: column; + height: calc(100% - 32px); + overflow: auto; +} + +@include b(devtool-action-item) { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; + + @include e(text) { + display: flex; + flex: 1 1 0; + justify-content: flex-start; + height: auto; + padding: 4px 6px; + margin-right: 16px; + overflow: hidden; + line-height: normal; + cursor: pointer; + + >.el-tag__content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + @include e(button) { + flex: 0 0 auto; + cursor: pointer; + + &:hover { + color: #409EFF; + } + } +} \ No newline at end of file diff --git a/src/devtool/devtool-action.tsx b/src/devtool/devtool-action.tsx new file mode 100644 index 0000000000000000000000000000000000000000..934df0cbeb7ca2bf97a801158d386a77e1f910b4 --- /dev/null +++ b/src/devtool/devtool-action.tsx @@ -0,0 +1,248 @@ +import { useNamespace } from '@ibiz-template/vue3-util'; +import { + Ref, + computed, + defineComponent, + getCurrentInstance, + inject, + onMounted, + onUnmounted, + ref, + unref, +} from 'vue'; +import './devtool-action.scss'; +import { QXEvent } from 'qx-util'; +import { useRouter } from 'vue-router'; +import { DevtoolEvent } from './interface/devtool-event'; +import { DevtoolView } from './interface/devtool-view'; + +export const DevtoolAction = defineComponent({ + name: 'IBizDevtoolAction', + setup() { + const ns = useNamespace('devtool-action'); + + const evt: QXEvent = inject('devtool-evt')!; + const items: Ref = ref([]); + const activeItem: Ref = ref(null); + const isShow = ref(false); + const height = ref(''); + const activeCollapse = ref(['context', 'params']); + + const router = useRouter(); + const instance = getCurrentInstance()?.proxy; + + router.afterEach(() => { + if (instance) { + activeItem.value = null; + instance.$forceUpdate(); + } + }); + + const getObjEntriesArray = (obj: Record) => { + if (!obj) { + return []; + } + return Object.entries(obj).map(([key, value]) => { + return { + key, + value: + Object.prototype.toString.call(value) === '[object Object]' + ? JSON.stringify(value) + : `${value}`, + }; + }); + }; + + const contextArray = computed(() => { + if (!activeItem.value) { + return []; + } + return getObjEntriesArray(activeItem.value.controller.context); + }); + + const paramsArray = computed(() => { + if (!activeItem.value) { + return []; + } + return getObjEntriesArray(activeItem.value.controller.params); + }); + + const onDataChange = (data: Map) => { + items.value = [...data.values()]; + }; + + const keydownHandle = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.code === 'F12') { + isShow.value = !isShow.value; + } + }; + + const onItemClick = (item: DevtoolView) => { + if (item.key === activeItem.value?.key) { + activeItem.value = null; + } else { + activeItem.value = item; + } + }; + + const copy = async (value: string) => { + if (!value) { + return; + } + try { + await ibiz.util.text.copy(value); + ibiz.message.success('拷贝成功!'); + } catch (error) { + ibiz.message.error('拷贝失败!'); + } + }; + + const minimize = () => { + height.value = '44px'; + }; + + const expand = () => { + height.value = '100%'; + }; + + const close = () => { + isShow.value = false; + }; + + const onMouseEnter = (item: DevtoolView) => { + const el = unref(item.dom); + if (el) { + el.style.outline = '3px solid #409EFF'; + } + }; + + const onMouseLeave = (item: DevtoolView) => { + const el = unref(item.dom); + if (el) { + el.style.outline = ''; + } + }; + + onMounted(() => { + evt.on('change', onDataChange); + document.addEventListener('keydown', keydownHandle); + }); + + onUnmounted(() => { + evt.off('change', onDataChange); + document.removeEventListener('keydown', keydownHandle); + }); + + return { + ns, + items, + activeItem, + height, + isShow, + activeCollapse, + contextArray, + paramsArray, + onItemClick, + copy, + minimize, + expand, + close, + onMouseEnter, + onMouseLeave, + }; + }, + render() { + if (!this.isShow) { + return; + } + + return ( +
+ {this.activeItem ? ( +
+ + + {this.contextArray.map(item => ( +
+
this.copy(item.key)} + > + {item.key} +
+
this.copy(item.value)} + > + {`:${item.value}`} +
+
+ ))} +
+ + {this.paramsArray.map(item => ( +
+
this.copy(item.key)} + > + {item.key} +
+
this.copy(item.value)} + > + {`:${item.value}`} +
+
+ ))} +
+
+
+ ) : null} +
+
+ + + +
+
+ {this.items.map(item => { + if (!item.controller.isActive) { + return null; + } + const { caption, name } = item.controller.model; + const text = `${caption || ''}(${name || ''})`; + const type = this.activeItem?.key === item.key ? 'success' : ''; + return ( +
+ this.onItemClick(item)} + onMouseenter={() => this.onMouseEnter(item)} + onMouseleave={() => this.onMouseLeave(item)} + > + {text} + +
this.copy(name!)} + > + 拷贝 +
+
+ ); + })} +
+
+
+ ); + }, +}); diff --git a/src/devtool/index.ts b/src/devtool/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..656629eb0099f88269a0f66e79017320516dcb5d --- /dev/null +++ b/src/devtool/index.ts @@ -0,0 +1,85 @@ +import { Ref, createApp } from 'vue'; +import { ViewController } from '@ibiz-template/runtime'; +import { QXEvent } from 'qx-util'; +import ElementPlus from 'element-plus'; +import zhCn from 'element-plus/dist/locale/zh-cn.mjs'; +import { Router } from 'vue-router'; +import { DevtoolAction } from './devtool-action'; +import { DevtoolEvent } from './interface/devtool-event'; +import { DevtoolView } from './interface/devtool-view'; + +export class Devtool { + /** + * 视图map + * + * @author zhanghengfeng + * @date 2023-08-28 18:08:16 + */ + viewMap = new Map(); + + /** + * 事件中心 + * + * @author zhanghengfeng + * @date 2023-08-28 18:08:43 + */ + evt = new QXEvent(); + + /** + * 初始化devtool工具 + * + * @author zhanghengfeng + * @date 2023-08-28 18:08:24 + * @param {Router} router + */ + init(router: Router): void { + const devtool = document.createElement('div'); + devtool.classList.add('devtool'); + document.body.append(devtool); + const app = createApp(DevtoolAction); + app.use(router); + app.use(ElementPlus, { + locale: zhCn, + }); + app.provide('devtool-evt', this.evt); + app.mount(devtool); + } + + /** + * 注册视图 + * + * @author zhanghengfeng + * @date 2023-08-28 18:08:25 + * @param {string} key + * @param {ViewController} controller + * @param {(Ref)} dom + */ + registerView( + key: string, + controller: ViewController, + dom: Ref, + ): void { + this.viewMap.set(key, { + key, + controller, + dom, + }); + this.evt.emit('change', this.viewMap); + } + + /** + * 注销视图 + * + * @author zhanghengfeng + * @date 2023-08-28 18:08:39 + * @param {string} key + */ + unRegisterView(key: string): void { + this.viewMap.delete(key); + this.evt.emit('change', this.viewMap); + } +} + +const devtool = new Devtool(); + +export { devtool }; diff --git a/src/devtool/interface/devtool-event.ts b/src/devtool/interface/devtool-event.ts new file mode 100644 index 0000000000000000000000000000000000000000..47b0b492b13a76d5e213e134afad83640eafe32f --- /dev/null +++ b/src/devtool/interface/devtool-event.ts @@ -0,0 +1,13 @@ +import { DevtoolView } from './devtool-view'; + +/** + * devtool事件 + * + * @author zhanghengfeng + * @date 2023-08-28 19:08:42 + * @export + * @interface DevtoolEvent + */ +export interface DevtoolEvent { + change: (data: Map) => void; +} diff --git a/src/devtool/interface/devtool-view.ts b/src/devtool/interface/devtool-view.ts new file mode 100644 index 0000000000000000000000000000000000000000..dadb2343abe2fea2f66c34dce07f99c2b952fd8f --- /dev/null +++ b/src/devtool/interface/devtool-view.ts @@ -0,0 +1,16 @@ +import { ViewController } from '@ibiz-template/runtime'; +import { Ref } from 'vue'; + +/** + * devtool视图 + * + * @author zhanghengfeng + * @date 2023-08-28 19:08:07 + * @export + * @interface DevtoolView + */ +export interface DevtoolView { + key: string; + controller: ViewController; + dom: Ref; +} diff --git a/src/index.ts b/src/index.ts index 9cf3151db8aa9469c2413c04764f8ef52bda5804..99c2305b58abee57ca97ebbcc5974e65c2cabae5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ export * from './editor'; export * from './view'; export * from './view-engine'; export * from './util'; +export { devtool } from './devtool'; export { i18n } from './locale'; export default { diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts index 1e4998eff6cfa3538a7c8dfdcc25ac88472bf543..021589d072498dfe696c240b2561f604b5b2352d 100644 --- a/src/shims-vue.d.ts +++ b/src/shims-vue.d.ts @@ -4,3 +4,5 @@ declare module '*.vue' { const componentOptions: ComponentOptions; export default componentOptions; } + +declare module 'element-plus/dist/locale/zh-cn.mjs'; diff --git a/src/view/common/view.tsx b/src/view/common/view.tsx index 9509dc87ba7c69201ba140c9fcb4a756e36edc74..92adc9cd99d82c6e54b13b87bcba918de9799eac 100644 --- a/src/view/common/view.tsx +++ b/src/view/common/view.tsx @@ -14,8 +14,13 @@ import { onActivated, onDeactivated, VNode, + onMounted, + onUnmounted, + ref, } from 'vue'; import './view.scss'; +import { createUUID } from 'qx-util'; +import { devtool } from '../../devtool'; export const View = defineComponent({ name: 'IBizView', @@ -47,11 +52,22 @@ export const View = defineComponent({ c.setLayoutPanel(controller as IViewLayoutPanelController); }; + const container = ref(null); + const key = createUUID(); + + onMounted(() => { + devtool.registerView(key, c, container); + }); + + onUnmounted(() => { + devtool.unRegisterView(key); + }); + onActivated(() => c.onActivated()); onDeactivated(() => c.onDeactivated()); - return { c, controls, viewClassNames, onLayoutPanelCreated }; + return { c, controls, viewClassNames, container, onLayoutPanelCreated }; }, render() { let layoutPanel = null; @@ -115,7 +131,11 @@ export const View = defineComponent({ } return ( -
+
{layoutPanel}
);