diff --git a/.bk.env b/.bk.env index db873e974..4394e491c 100644 --- a/.bk.env +++ b/.bk.env @@ -6,3 +6,5 @@ BK_APP_PORT = 5000 // 接口路径地址 BK_AJAX_URL_PREFIX = '/api' +// default为社区版, ieod为内部版 +BK_ENGINE_REGION = default diff --git a/.eslintrc.js b/.eslintrc.js index 5c6eebb88..3b9407da9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,6 +42,7 @@ module.exports = { }, // add your custom rules hered rules: { + 'standard/no-callback-literal': 0, '@typescript-eslint/adjacent-overload-signatures': 'error', /** * 类的只读属性若是一个字面量,则必须使用只读属性而不是 getter diff --git a/bk.config.js b/bk.config.js index 7c5d28e46..21f6a769f 100644 --- a/bk.config.js +++ b/bk.config.js @@ -11,6 +11,7 @@ module.exports = { publicPath: process.env.BK_STATIC_URL, typescript: true, bundleAnalysis: false, + parseNodeModules: false, replaceStatic: true, parallel: 8, copy: { @@ -40,7 +41,8 @@ module.exports = { resolve: { alias: { '@': path.resolve(__dirname, './lib/client/src'), - 'shared': path.resolve(__dirname, './lib/shared') + 'shared': path.resolve(__dirname, './lib/shared'), + VueI18n: path.resolve(__dirname, './node_modules/vue-i18n') }, fallback: { buffer: require.resolve('buffer') @@ -73,21 +75,49 @@ module.exports = { toast: { name: 'toast-ui', test: /toast-ui/, - priority: 20, + priority: 1, chunks: 'all', reuseExistingChunk: true }, element: { name: 'element-ui', test: /element-ui/, - priority: 30, + priority: 1, chunks: 'all', reuseExistingChunk: true }, - third: { + echartsMavonVant: { name: 'echarts-mavon-vant', test: /(echarts)|(mavon-editor)|(vant)/, - priority: 40, + priority: 1, + chunks: 'all', + reuseExistingChunk: true + }, + render: { + name: 'bk-lesscode-render', + test: /bk-lesscode-render/, + priority: 1, + chunks: 'all', + reuseExistingChunk: true + }, + sqlParser: { + name: 'node-sql-parser', + test: /node-sql-parser/, + priority: 1, + chunks: 'all', + reuseExistingChunk: true + }, + blueking: { + name: 'blueking', + test: /@blueking/, + priority: 1, + chunks: 'all', + reuseExistingChunk: true + }, + xlsxTypeormMoment: { + name: 'xlsx-typeorm-moment', + test: /(xlsx)|(typeorm)|(moment)/, + priority: 1, chunks: 'all', reuseExistingChunk: true } diff --git a/lib/client/preview.html b/lib/client/preview.html index 58f32e95e..6d39c8ae3 100644 --- a/lib/client/preview.html +++ b/lib/client/preview.html @@ -12,8 +12,8 @@ - - + + 可视化开发平台 | 腾讯蓝鲸智云 @@ -96,14 +96,15 @@ } } .preview-tip { + display: none; position: fixed; - top: 7px; + top: 11px; right: -29px; transform: rotate(45deg); font-size: 12px; line-height: 13px; padding: 6px 0; - width: 110px; + width: 120px; text-align: center; background: rgba(239, 184, 61, 0.8); color: #000000; @@ -115,7 +116,7 @@
-
+
@@ -125,7 +126,7 @@
-
未发布
请勿分享
+
+ + diff --git a/lib/client/src/components/lc-form/item.vue b/lib/client/src/components/lc-form/item.vue index df4746835..dab197b25 100644 --- a/lib/client/src/components/lc-form/item.vue +++ b/lib/client/src/components/lc-form/item.vue @@ -14,6 +14,9 @@ methods: { clearValidator () { this.$refs.bkFormItem.clearValidator() + }, + validate () { + return this.$refs.bkFormItem.validate() } } } diff --git a/lib/client/src/components/methods/choose-function/index.vue b/lib/client/src/components/methods/choose-function/index.vue index f2eddfb03..505e1291f 100644 --- a/lib/client/src/components/methods/choose-function/index.vue +++ b/lib/client/src/components/methods/choose-function/index.vue @@ -68,7 +68,7 @@
- - + + @@ -67,7 +67,6 @@ - + {{ $t('新建路由') }}
-

+

{{ $t('修改一级路由的同时会变更页面模板,已绑定页面或跳转的路由不能再次选择') }}

@@ -72,8 +72,8 @@ routeOptionList () { const routeList = this.routeGroup.map(({ children }) => children) .reduce((pre, cur) => pre.concat(cur), []) - .map(({ id, layoutPath, path, pageId, redirect }) => ({ - id, + .map(({ id, routeId, layoutPath, path, pageId, redirect }) => ({ + id: routeId || id, name: `${layoutPath}${layoutPath.endsWith('/') ? '' : '/'}${path}`, pageId, redirect, @@ -87,6 +87,8 @@ 'dialog.visible' (val) { if (val) { this.setSelectedRoute() + } else { + this.$emit('closeDialog') } }, currentRoute () { diff --git a/lib/client/src/components/project/layout-thumb-list.vue b/lib/client/src/components/project/layout-thumb-list.vue index 18464b7c1..323108ce6 100644 --- a/lib/client/src/components/project/layout-thumb-list.vue +++ b/lib/client/src/components/project/layout-thumb-list.vue @@ -13,19 +13,21 @@
- {{ layout.defaultName }} + {{ $t(layout.defaultName) }}
{{ $t('预览') }}
-
- {{ layout.defaultName }} +
+ {{ $t(layout.defaultName) }}
diff --git a/lib/client/src/components/project/select-project.vue b/lib/client/src/components/project/select-project.vue new file mode 100644 index 000000000..ffa2ecf88 --- /dev/null +++ b/lib/client/src/components/project/select-project.vue @@ -0,0 +1,100 @@ + + + diff --git a/lib/client/src/components/project/sort-select.vue b/lib/client/src/components/project/sort-select.vue index 6470b9098..4e542ed42 100644 --- a/lib/client/src/components/project/sort-select.vue +++ b/lib/client/src/components/project/sort-select.vue @@ -79,6 +79,7 @@ diff --git a/lib/client/src/components/render-nocode/form/components/form-content/index.vue b/lib/client/src/components/render-nocode/form/components/form-content/index.vue index b9c82e511..ce9293b0b 100644 --- a/lib/client/src/components/render-nocode/form/components/form-content/index.vue +++ b/lib/client/src/components/render-nocode/form/components/form-content/index.vue @@ -224,7 +224,7 @@ @import "@/css/mixins/scroller"; .form-panel { - height: calc(100vh - 200px); + height: calc(100vh - 180px); } .fields-container { diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue index 16a540632..9d131215e 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue @@ -181,7 +181,7 @@ if (msg) { this.$bkMessage({ theme: 'error', - message: `【${window.i18n.t('规则')}${index + 1}】${window.i18n.t('的')}${msg}` + message: `【${window.i18n.t('规则')}${index + 1}】${msg}` }) return true diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/components/serialEdit.vue b/lib/client/src/components/render-nocode/form/components/form-edit/components/serialEdit.vue index 03f1f5571..37d984a3e 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/components/serialEdit.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/components/serialEdit.vue @@ -34,67 +34,64 @@ @confirm="handlerConfirm" @cancel="handlerCancel">
- - -
-
- {{ item.name }} - -
-
- - - - - -
- + +
+
+ {{ item.name }} + +
+
+ + + + +
- + +
@@ -355,19 +352,18 @@ diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/custumData.vue b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/custom-data.vue similarity index 100% rename from lib/client/src/components/render-nocode/form/components/form-edit/data-source/custumData.vue rename to lib/client/src/components/render-nocode/form/components/form-edit/data-source/custom-data.vue diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/function-data.vue b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/function-data.vue new file mode 100644 index 000000000..305dadf5d --- /dev/null +++ b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/function-data.vue @@ -0,0 +1,71 @@ + + diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/getRequestParams.vue b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/getRequestParams.vue deleted file mode 100644 index 5e9e44b7f..000000000 --- a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/getRequestParams.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/index.vue b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/index.vue index a5650891b..0abe05785 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/index.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/index.vue @@ -10,22 +10,6 @@ :local-val-is-display-tag="localValIsDisplayTag" @update="$emit('change', $event)"> - - - - diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/data-source/worksheetData.vue b/lib/client/src/components/render-nocode/form/components/form-edit/data-source/worksheet-data.vue similarity index 100% rename from lib/client/src/components/render-nocode/form/components/form-edit/data-source/worksheetData.vue rename to lib/client/src/components/render-nocode/form/components/form-edit/data-source/worksheet-data.vue diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/dataSourceDialog.vue b/lib/client/src/components/render-nocode/form/components/form-edit/dataSourceDialog.vue index 8279ad37c..ac7a71f64 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/dataSourceDialog.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/dataSourceDialog.vue @@ -4,7 +4,7 @@ ext-cls="data-source-dialog" :mask-close="false" :auto-close="false" - :width="sourceType === 'API' ? 960 : 780" + :width="780" :show-type-select="false" :value="show" @confirm="onConfirm" diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/fieldEdit.vue b/lib/client/src/components/render-nocode/form/components/form-edit/fieldEdit.vue index d7b51b834..5a6161a67 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/fieldEdit.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/fieldEdit.vue @@ -65,32 +65,48 @@ - - - --> + + + {{ item.name }} + + + + + + {{ $t('配置数据源') }} + +
@@ -285,7 +301,7 @@
{{ $t('添加额外填写说明') }} - {{ $t('效果预览') }} + {{ $t('效果预览') }}
({}) @@ -410,10 +425,6 @@ fieldsShowDefaultValue: FIELDS_SHOW_DEFAULT_VALUE, fieldsDataSource: DATA_SOURCE_FIELD }, - systemList: [], - systemListLoading: false, - apiDetail: {}, - resArrayTreeData: [], alignList: [{ id: 'left', name: this.$t('居左') }, { id: 'right', name: this.$t('居右') }, { id: 'center', name: this.$t('居中') }], dataSourceDialogShow: false, readerOnlyShow: false, @@ -448,19 +459,13 @@ } return FIELDS_SOURCE_TYPE }, - isConfigDataSourceDisabled () { - return this.fieldData.source_type === 'API' && !this.fieldData.api_info.remote_system_id - }, sourceData () { - const { source_type: sourceType, choice, meta, api_info: apiInfo, kv_relation: kvRelation } = this.fieldData + const { source_type: sourceType, choice, meta } = this.fieldData let data = {} switch (sourceType) { case 'CUSTOM': data = choice break - case 'API': - data = { apiInfo, kvRelation } - break case 'WORKSHEET': data = meta.data_config break @@ -477,17 +482,9 @@ this.basicIsFolded = false this.handleIsFolded = false } - if (this.fieldProps.fieldsDataSource.includes(val.type) && val.id !== oldVal.id) { - this.getSystems() - } this.fieldData = cloneDeep(val) } }, - created () { - if (this.fieldProps.fieldsDataSource.includes(this.fieldData.type)) { - this.getSystems() - } - }, methods: { getRegexList (val) { @@ -546,9 +543,6 @@ this.fieldData.choice = val.localVal this.fieldData.default = val.localVal.find(item => item.isDefaultVal)?.key || '' this.fieldData.isDisplayTag = !!val?.localValIsDisplayTag - } else if (sourceType === 'API') { - this.fieldData.api_info = val.api_info - this.fieldData.kv_relation = val.kv_relation } else if (sourceType === 'WORKSHEET') { this.fieldData.meta.data_config = val.localVal } @@ -558,59 +552,48 @@ handleSourceTypeChange (val) { this.fieldData.source_type = val if (val === 'CUSTOM') { - this.fieldData.choice = [ - { key: 'XUANXIANG1', name: this.$t('选项1'), color: '#3a84ff', isDefaultVal: true }, - { key: 'XUANXIANG2', name: this.$t('选项2'), color: '#2dcb56', isDefaultVal: false } - ] - delete this.fieldData.meta.data_config - this.fieldData.kv_relation = {} - } else if (val === 'API') { - this.fieldData.choice = [] - this.fieldData.api_info = { - remote_api_id: '', - remote_system_id: '', - req_body: {}, - req_params: {}, - rsp_data: '' + if (!this.fieldData.choice || this.fieldData.choice.length === 0) { + this.fieldData.choice = [ + { key: 'XUANXIANG1', name: this.$t('选项1'), color: '#3a84ff', isDefaultVal: true }, + { key: 'XUANXIANG2', name: this.$t('选项2'), color: '#2dcb56', isDefaultVal: false } + ] } - this.fieldData.kv_relation = { key: '', name: '' } } else if (val === 'WORKSHEET') { - this.fieldData.choice = [] - this.fieldData.kv_relation = {} - this.fieldData.meta.data_config = { - formId: '', - tableName: '', - field: '', - conditions: { - type: 'and', - expressions: [] + if (!this.fieldData.meta.data_config) { + this.fieldData.meta.data_config = { + formId: '', + tableName: '', + field: '', + conditions: { + type: 'and', + expressions: [] + } + } + } + } else if (val === 'FUNCTION') { + if (!this.fieldData.meta.function_data_source_config) { + this.fieldData.meta.function_data_source_config = { + payload: { + methodData: { + methodCode: '', + params: [] + } + }, + returnedValue: [], + keys: {} } } } this.change() }, - // 获取系统列表 - async getSystems () { - try { - this.systemListLoading = true - const params = { - projectId: this.projectId, - versionId: this.versionId - } - this.systemList = await this.$store.dispatch('nocode/formSetting/getRemoteSystem', params) - } catch (e) { - console.error(e) - } finally { - this.systemListLoading = false + // 函数数据源配置变更 + handleFunctionDataSourceChange (val) { + // 防止接口返回速度慢的情况下,切换数据源类型之后再更新数据 + if (this.fieldData.source_type === 'FUNCTION') { + this.fieldData.meta.function_data_source_config = val + this.change() } }, - async handleSelectSystem (val) { - this.setFormData(val) - }, - setFormData (apiId) { - this.apiDetail = this.systemList.find(item => item.id === apiId) - this.resArrayTreeData = transSchemeToArrayTypeTree(this.apiDetail.response) - }, // 默认值修改 handleDefaultValChange (val) { this.fieldData.default = ['MULTISELECT', 'CHECKBOX', 'MEMBER', 'MEMBERS'].includes(this.fieldData.type) diff --git a/lib/client/src/components/render-nocode/form/components/left-panel/index.vue b/lib/client/src/components/render-nocode/form/components/left-panel/index.vue index b33489e88..e453c9d1b 100644 --- a/lib/client/src/components/render-nocode/form/components/left-panel/index.vue +++ b/lib/client/src/components/render-nocode/form/components/left-panel/index.vue @@ -4,6 +4,7 @@
{{ group.name }}
+ + diff --git a/lib/client/src/components/render-nocode/markdown/index.vue b/lib/client/src/components/render-nocode/markdown/index.vue index 6e9fdac2d..aacc97869 100644 --- a/lib/client/src/components/render-nocode/markdown/index.vue +++ b/lib/client/src/components/render-nocode/markdown/index.vue @@ -1,18 +1,21 @@ diff --git a/lib/client/src/components/render/mobile/area.scss b/lib/client/src/components/render/mobile/area.postcss similarity index 88% rename from lib/client/src/components/render/mobile/area.scss rename to lib/client/src/components/render/mobile/area.postcss index 12f2c1779..7c5a53b3c 100644 --- a/lib/client/src/components/render/mobile/area.scss +++ b/lib/client/src/components/render/mobile/area.postcss @@ -13,6 +13,9 @@ .edit-button { display: flex; } + :global(.mobile-edit-area-dropdown .bk-dropdown-content) { + z-index: 100000001 + } .dropdown-text { cursor: pointer; span { @@ -35,10 +38,6 @@ &:hover { background: #f5f7fa; } - &.active { - background: rgba(225,236,255,0.60); - color: #3a84ff; - } .list-item-text { display: inline-block; width: 100%; @@ -55,4 +54,9 @@ box-shadow: 0px 2px 6px 0px rgba(0,0,0,0.10); transform: translate(0, 0); } - } \ No newline at end of file +} + +.active { + background: rgba(225,236,255,0.60); + color: #3a84ff; +} diff --git a/lib/client/src/components/render/mobile/common/mobile-header-height.js b/lib/client/src/components/render/mobile/common/mobile-header-height.js index 0efe59d2a..4798f5a12 100644 --- a/lib/client/src/components/render/mobile/common/mobile-header-height.js +++ b/lib/client/src/components/render/mobile/common/mobile-header-height.js @@ -1,4 +1,4 @@ -import { ref, computed } from '@vue/composition-api' +import { ref, computed } from 'bk-lesscode-render' import moment from 'moment' export default function () { const height = ref(30) diff --git a/lib/client/src/components/render/mobile/common/mobile-header.vue b/lib/client/src/components/render/mobile/common/mobile-header.vue deleted file mode 100644 index f5e2217a5..000000000 --- a/lib/client/src/components/render/mobile/common/mobile-header.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - diff --git a/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.js b/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.js new file mode 100644 index 000000000..0dd280f45 --- /dev/null +++ b/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.js @@ -0,0 +1,87 @@ +import cssModule from './mobile-header.postcss?module' +import { + h +} from 'bk-lesscode-render' +import headerHeight from '../mobile-header-height' + +export default { + setup () { + const { height, time } = headerHeight() + return { + height, + time + } + }, + render (render) { + h.init(render) + + const self = this + + const renderHeaderLeft = () => { + return h({ + component: 'div', + class: cssModule['header-left'], + children: [ + h({ + component: 'span', + class: 'bk-drag-cellular-connection bk-drag-icon bk-drag mr5' + }), + h({ + component: 'span', + class: 'mr5', + children: [ + 'iPhone' + ] + }), + h({ + component: 'span', + class: 'mr5', + children: [ + '5G' + ] + }) + ] + }) + } + + const renderHeaderMid = () => { + return h({ + component: 'div', + class: cssModule['header-mid'], + children: [ + self.time + ] + }) + } + + const renderHeaderRight = () => { + return h({ + component: 'div', + class: cssModule['header-right'], + children: [ + h({ + component: 'span', + children: ['100%'] + }), + h({ + component: 'span', + class: 'bk-drag-battery bk-drag-icon bk-drag ml5' + }) + ] + }) + } + + return h({ + component: 'div', + class: cssModule['phone-header'], + style: { + height: self.height + 'px' + }, + children: [ + renderHeaderLeft(), + renderHeaderMid(), + renderHeaderRight() + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.postcss b/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.postcss new file mode 100644 index 000000000..d0e0c7158 --- /dev/null +++ b/lib/client/src/components/render/mobile/common/mobile-header/mobile-header.postcss @@ -0,0 +1,23 @@ +.phone-header { + display: flex; + background: #fff; + padding: 8px 18px; + font-size: 12px; + color: #000; + transform: translateX(-5px); + div { + flex: 1; + } + .header-left { + flex: 4; + } + .header-mid { + flex: 2; + text-align: center; + } + .header-right { + flex: 4; + text-align: right; + align-items: center; + } +} diff --git a/lib/client/src/components/render/mobile/common/model.js b/lib/client/src/components/render/mobile/common/model.js index 61c31c0ab..3b28ef598 100644 --- a/lib/client/src/components/render/mobile/common/model.js +++ b/lib/client/src/components/render/mobile/common/model.js @@ -1,13 +1,15 @@ -import { ref, reactive, computed } from '@vue/composition-api' +import { ref, reactive, watch, computed } from 'bk-lesscode-render' import { list } from './model-list' import { useStore } from '@/store' import LC from '@/element-materials/core' +import emitter from 'tiny-emitter/instance' export default function () { const store = useStore() const model = ref('iPhone 11 Pro') const modelList = reactive(list) + const canvasSize = ref({}) // 当前机型的信息 const activeModelInfo = computed(() => { @@ -31,20 +33,26 @@ export default function () { } // 画布尺寸 { width: xx, height:xx } - const canvasSize = computed(() => { - const size = JSON.parse(JSON.stringify(store.getters['page/pageSize'])) - if (activeModelInfo && modelList.length) { - size.width = parseInt(activeModelInfo.value[1].split('x')[0]) - size.height = parseInt(activeModelInfo.value[1].split('x')[1]) - - // 满屏幕为20rem,以此设置root的字体大小,移动端响应式 - document.documentElement.style.fontSize = `${size.width / 20}px` + watch( + () => model.value, + () => { + const size = JSON.parse(JSON.stringify(store.getters['page/pageSize'])) + if (activeModelInfo && modelList.length) { + size.width = parseInt(activeModelInfo.value[1].split('x')[0]) + size.height = parseInt(activeModelInfo.value[1].split('x')[1]) + + // 满屏幕为20rem,以此设置root的字体大小,移动端响应式 + document.documentElement.style.fontSize = `${size.width / 20}px` + } + store.commit('page/setPageSize', size) + syncH5PageHeight(`${size.height}px`) + emitter.emit('update-canvas-size', size) + canvasSize.value = size + }, + { + immediate: true } - store.commit('page/setPageSize', size) - syncH5PageHeight(`${size.height}px`) - - return size - }) + ) return { model, diff --git a/lib/client/src/components/render/mobile/common/simulator-mobile.vue b/lib/client/src/components/render/mobile/common/simulator-mobile.vue deleted file mode 100644 index 1772ff94d..000000000 --- a/lib/client/src/components/render/mobile/common/simulator-mobile.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.js b/lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.js new file mode 100644 index 000000000..aa27c5df0 --- /dev/null +++ b/lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.js @@ -0,0 +1,69 @@ +import cssModule from './simulator-mobile.postcss?module' +import { + h +} from 'bk-lesscode-render' +import mobileHeader from '../mobile-header/mobile-header' +import '@vant/touch-emulator' // PC端模拟移动端事件 用于预览 + +export default { + props: { + pageSize: { + type: Object, + default: () => ({ height: 0, width: 0 }) + }, + source: { + type: String, + default: '' + } + }, + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + class: cssModule['simulator-wrapper'], + style: { + width: self.pageSize.width + 'px', + height: self.pageSize.height + 'px' + }, + children: [ + h({ + component: 'div', + class: cssModule['device-phone-frame'], + children: [ + h({ + component: 'div', + class: cssModule['device-phone'] + }) + ] + }), + h({ + component: 'div', + class: cssModule['simulator-preview'], + style: { + width: self.pageSize.width + 'px', + height: self.pageSize.height + 'px' + }, + children: [ + h({ + component: mobileHeader + }), + h({ + component: 'iframe', + attrs: { + width: '100%', + height: '100%', + src: self.source + }, + style: { + border: 'none' + } + }) + ] + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/common/simulator-mobile.scss b/lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.postcss similarity index 92% rename from lib/client/src/components/render/mobile/common/simulator-mobile.scss rename to lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.postcss index ae1b9e8ff..ebb06ae8c 100644 --- a/lib/client/src/components/render/mobile/common/simulator-mobile.scss +++ b/lib/client/src/components/render/mobile/common/simulator-mobile/simulator-mobile.postcss @@ -19,7 +19,7 @@ bottom: 0; left: 0; right: 0; - background-image: url(../../../../images/phone.png); + background-image: url(../../../../../images/phone.png); background-size: 100% 100%; } .simulator-preview { @@ -36,4 +36,4 @@ display: none; } } -} \ No newline at end of file +} diff --git a/lib/client/src/components/render/mobile/edit-area.vue b/lib/client/src/components/render/mobile/edit-area.vue deleted file mode 100644 index f1f521da1..000000000 --- a/lib/client/src/components/render/mobile/edit-area.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - - diff --git a/lib/client/src/components/render/mobile/edit-area/edit-area.js b/lib/client/src/components/render/mobile/edit-area/edit-area.js new file mode 100644 index 000000000..6a82c7ffb --- /dev/null +++ b/lib/client/src/components/render/mobile/edit-area/edit-area.js @@ -0,0 +1,213 @@ +import cssModule from '../area.postcss?module' +import { + ref, + framework, + h +} from 'bk-lesscode-render' +import layout from '../widget/layout/index/index' +import render from '../../pc/index' +import getModelInfo from '../common/model' +import previewSwitch from '../preview-switch/preview-switch' +export default { + setup (props) { + const { canvasSize, model, modelList } = getModelInfo() + + const { preview } = ref(props) + const isShowModeList = ref(false) + + return { + canvasSize, + model, + modelList, + preview, + isShowModeList + } + }, + render (renderMethod) { + h.init(renderMethod) + + const self = this + + const renderDropDownTrigger = () => { + return h({ + component: 'div', + slot: 'dropdown-trigger', + class: cssModule['dropdown-text'], + children: [ + h({ + component: 'span', + on: { + click () { + self.isShowModeList = true + } + }, + children: [ + self.model, + h({ + component: 'i', + class: 'bk-icon icon-down-shape' + }) + ] + }) + ] + }) + } + + const renderDropDownV3 = () => { + return h({ + component: 'bk-dropdown', + props: { + trigger: 'click', + isShow: self.isShowModeList + }, + slots: { + default () { + return renderDropDownTrigger() + }, + content () { + return h({ + component: 'bk-dropdown-menu', + children: self.modelList.map((item) => { + return h({ + component: 'bk-dropdown-item', + key: item.key, + class: { + [cssModule.active]: item.key === self.model + }, + on: { + click () { + self.isShowModeList = false + self.model = item.key + } + }, + children: [ + item.text + ] + }) + }) + }) + } + } + }) + } + + const renderDropDownV2 = () => { + return h({ + component: 'bk-dropdown-menu', + props: { + trigger: 'click' + }, + class: 'mobile-edit-area-dropdown', + slots: { + 'dropdown-trigger' () { + return renderDropDownTrigger() + }, + 'dropdown-content' () { + return h({ + component: 'ul', + slot: 'dropdown-content', + class: cssModule['bk-dropdown-list'], + children: self.modelList.map((item) => { + return h({ + component: 'li', + key: item.key, + class: { + [cssModule.active]: item.key === self.model + }, + on: { + click () { + self.model = item.key + } + }, + children: [ + h({ + component: 'span', + class: cssModule['list-item-text'], + children: [ + item.text + ] + }) + ] + }) + }) + }) + } + } + }) + } + + const renderPreviewSwitch = () => { + return h({ + component: previewSwitch, + props: { + value: self.preview + }, + on: { + change (val) { + self.preview = val + } + } + }) + } + + const renderTool = () => { + return h({ + component: 'div', + class: cssModule['title'], + children: [ + h({ + component: 'span', + class: cssModule['title-text'], + children: [ + self.$t('编辑区') + ] + }), + h({ + component: 'div', + class: cssModule['edit-button'], + children: [ + framework === 'vue2' ? renderDropDownV2() : renderDropDownV3(), + renderPreviewSwitch() + ] + }) + ] + }) + } + + const renderDraw = () => { + return h({ + component: 'div', + attrs: { + id: 'lesscodeMobileDraw' + }, + class: cssModule['edit-area'], + style: { + width: self.canvasSize.width + 'px', + height: self.canvasSize.height + 'px' + }, + children: [ + h({ + component: layout, + children: [ + h({ + component: render + }) + ] + }) + ] + }) + } + + return h({ + component: 'div', + class: cssModule['area-wrapper'], + attrs: { + id: 'mobileDrawContent' + }, + children: [ + renderTool(), + renderDraw() + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/index.vue b/lib/client/src/components/render/mobile/index.vue deleted file mode 100644 index e7b86678d..000000000 --- a/lib/client/src/components/render/mobile/index.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - diff --git a/lib/client/src/components/render/mobile/index/index.js b/lib/client/src/components/render/mobile/index/index.js new file mode 100644 index 000000000..a195043ec --- /dev/null +++ b/lib/client/src/components/render/mobile/index/index.js @@ -0,0 +1,80 @@ +import cssModule from './index.postcss?module' +import { + h, + ref, + onBeforeMount, + onBeforeUnmount +} from 'bk-lesscode-render' +import editArea from '../edit-area/edit-area' +import simulator from '../simulator-area/simulator-area' +import LC from '@/element-materials/core' +import { + uuid +} from '@/common/util' +import useComponentAction from '../../hooks/component-action-use' + +export default { + setup () { + const { + unselectComponent + } = useComponentAction() + + const showPreview = ref(true) + const previewKey = ref(uuid()) + + const mobileSwitchCallback = (val) => { + showPreview.value = val + } + + const updatePreview = () => { + previewKey.value = uuid() + } + + onBeforeMount(() => { + LC.addEventListener('mobilePreviewSwitch', mobileSwitchCallback) + LC.addEventListener('refreshPreview', updatePreview) + }) + + onBeforeUnmount(() => { + LC.removeEventListener('mobilePreviewSwitch', mobileSwitchCallback) + LC.removeEventListener('refreshPreview', updatePreview) + }) + + return { + showPreview, + previewKey, + unselectComponent + } + }, + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + class: cssModule['mobile-canvas-wrapper'], + on: { + click: self.unselectComponent + }, + children: [ + h({ + component: editArea, + props: { + preview: self.showPreview + } + }), + h({ + component: simulator, + key: self.previewKey, + class: { + [cssModule.hidden]: !self.showPreview + }, + style: { + transform: 'translateX(-40px)' + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/index/index.postcss b/lib/client/src/components/render/mobile/index/index.postcss new file mode 100644 index 000000000..d127c7d4f --- /dev/null +++ b/lib/client/src/components/render/mobile/index/index.postcss @@ -0,0 +1,18 @@ +.mobile-canvas-wrapper { + display: flex; + justify-content: space-around; + min-width: 1000px; + padding: 40px 0 80px; + background: #fff; + overflow-y: auto; + height: 100%; + ::v-deep .lesscode-layout-empty { + min-height: 0; + min-width: 0; + padding: 0; + overflow: hidden; + } +} +.hidden { + display: none; +} diff --git a/lib/client/src/components/render/mobile/preview-switch.vue b/lib/client/src/components/render/mobile/preview-switch.vue deleted file mode 100644 index 87516342e..000000000 --- a/lib/client/src/components/render/mobile/preview-switch.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/lib/client/src/components/render/mobile/preview-switch/preview-switch.js b/lib/client/src/components/render/mobile/preview-switch/preview-switch.js new file mode 100644 index 000000000..3654becd3 --- /dev/null +++ b/lib/client/src/components/render/mobile/preview-switch/preview-switch.js @@ -0,0 +1,48 @@ +import cssModule from './preview-switch.postcss?module' +import { + h +} from 'bk-lesscode-render' +import LC from '@/element-materials/core' + +export default { + props: { + value: { + type: Boolean, + default: true + } + }, + emits: ['change'], + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + class: cssModule['mobile-preview-switcher'], + children: [ + h({ + component: 'bk-switcher', + props: { + size: 'small', + theme: 'primary', + value: self.value + }, + on: { + change (val) { + self.$emit('change', val) + const activeNode = LC.getActiveNode() + if (activeNode) { + activeNode.activeClear() + } + LC.triggerEventListener('componentMouserleave', { + type: 'componentMouserleave' + }) + LC.triggerEventListener('mobilePreviewSwitch', val) + } + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/preview-switch/preview-switch.postcss b/lib/client/src/components/render/mobile/preview-switch/preview-switch.postcss new file mode 100644 index 000000000..db4d10484 --- /dev/null +++ b/lib/client/src/components/render/mobile/preview-switch/preview-switch.postcss @@ -0,0 +1,9 @@ +.mobile-preview-switcher { + display: flex; + align-items: center; + justify-self: flex-start; + margin-left: 20px; + span { + margin-right: 8px; + } +} diff --git a/lib/client/src/components/render/mobile/simulator-area.vue b/lib/client/src/components/render/mobile/simulator-area.vue deleted file mode 100644 index 38bad3216..000000000 --- a/lib/client/src/components/render/mobile/simulator-area.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/lib/client/src/components/render/mobile/simulator-area/simulator-area.js b/lib/client/src/components/render/mobile/simulator-area/simulator-area.js new file mode 100644 index 000000000..6c29dc528 --- /dev/null +++ b/lib/client/src/components/render/mobile/simulator-area/simulator-area.js @@ -0,0 +1,76 @@ +import cssModule from '../area.postcss?module' +import { h, ref, onBeforeUnmount } from 'bk-lesscode-render' +import { useStore } from '@/store' +import { useRoute } from '@/router' +import getHeaderHeight from '../common/mobile-header-height' +import simulatorMobile from '../common/simulator-mobile/simulator-mobile' +import emitter from 'tiny-emitter/instance' + +export default { + setup () { + const store = useStore() + const route = useRoute() + const pageSize = ref({}) + + const { height: headerHeight } = getHeaderHeight() + + const projectId = route.params.projectId + const pagePath = `${store.getters['page/pageRoute'].layoutPath}${store.getters['page/pageRoute'].layoutPath.endsWith('/') ? '' : '/'}${store.getters['page/pageRoute'].path}` + const versionId = store.getters['projectVersion/currentVersionId'] + + let pathStr = `${versionId ? `/version/${versionId}` : ''}` + pathStr += '/platform/MOBILE' + + const calcPageSize = () => { + const { height, width } = store.getters['page/pageSize'] + pageSize.value = { + width, + height: parseInt(height + headerHeight.value) + } + } + + emitter.on('update-canvas-size', calcPageSize) + onBeforeUnmount(() => { + emitter.off('update-canvas-size', calcPageSize) + }) + + calcPageSize() + + return { + pageSize, + source: `${location.origin}/preview/project/${projectId}${pathStr}${pagePath}` + } + }, + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + class: cssModule['area-wrapper'], + children: [ + h({ + component: 'div', + class: cssModule['title'], + children: [ + h({ + component: 'span', + class: cssModule['title-text'], + children: [ + '效果预览' + ] + }) + ] + }), + h({ + component: simulatorMobile, + props: { + pageSize: self.pageSize, + source: self.source + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/widget/layout/empty-layout.vue b/lib/client/src/components/render/mobile/widget/layout/empty-layout.vue deleted file mode 100644 index ad53f0bc7..000000000 --- a/lib/client/src/components/render/mobile/widget/layout/empty-layout.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.js b/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.js new file mode 100644 index 000000000..51a2ddc5f --- /dev/null +++ b/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.js @@ -0,0 +1,22 @@ +import cssModule from './empty-layout.postcss?module' +import { h } from 'bk-lesscode-render' +import useComponentAction from '../../../../hooks/component-action-use' +export default { + setup () { + const layoutName = 'mobileEmptyLayout' + const pageSetting = useComponentAction(false, layoutName, 'MOBILE') + return { + pageSetting + } + }, + render (render) { + h.init(render) + + return h({ + component: 'div', + class: [cssModule['mobile-empty-layout-wrapper'], cssModule['mobile-layout-wrapper'], 'bk-lesscode-mobile-layout-wrapper'], + ref: 'layout', + children: this.$slots.default + }) + } +} diff --git a/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.postcss b/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.postcss new file mode 100644 index 000000000..a357b49ed --- /dev/null +++ b/lib/client/src/components/render/mobile/widget/layout/empty-layout/empty-layout.postcss @@ -0,0 +1,13 @@ +.mobile-empty-layout-wrapper { + height: 100%; + :global(.lesscode-editor-layout) { + min-height: auto; + } +} + +.mobile-layout-wrapper { + overflow-y: scroll; + &::-webkit-scrollbar { + display: none; + } +} diff --git a/lib/client/src/components/render/mobile/widget/layout/index.vue b/lib/client/src/components/render/mobile/widget/layout/index.vue deleted file mode 100644 index 56685ce86..000000000 --- a/lib/client/src/components/render/mobile/widget/layout/index.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/lib/client/src/components/render/mobile/widget/layout/index/index.js b/lib/client/src/components/render/mobile/widget/layout/index/index.js new file mode 100644 index 000000000..a963f5348 --- /dev/null +++ b/lib/client/src/components/render/mobile/widget/layout/index/index.js @@ -0,0 +1,33 @@ +import { h } from 'bk-lesscode-render' +import tabBar from '../tab-bar/tab-bar' +import emptyLayout from '../empty-layout/empty-layout' +import useComponentAction from '../../../../hooks/component-action-use' + +const componentMap = { + 'mobile-bottom': tabBar, + 'mobile-empty': emptyLayout +} +export default { + components: { + tabBar + }, + setup () { + const { layout } = useComponentAction(false, '', 'MOBILE') + return { + layout + } + }, + computed: { + component () { + return componentMap[this.layout] || emptyLayout + } + }, + render (render) { + h.init(render) + + return h({ + component: this.component, + children: this.$slots.default + }) + } +} diff --git a/lib/client/src/components/render/mobile/widget/layout/tab-bar.vue b/lib/client/src/components/render/mobile/widget/layout/tab-bar.vue deleted file mode 100644 index ef1fa1ba2..000000000 --- a/lib/client/src/components/render/mobile/widget/layout/tab-bar.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - diff --git a/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.js b/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.js new file mode 100644 index 000000000..da9cccaa1 --- /dev/null +++ b/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.js @@ -0,0 +1,58 @@ +import cssModule from './tab-bar.postcss?module' +import { h } from 'bk-lesscode-render' +import useComponentAction from '../../../../hooks/component-action-use' + +export default { + setup () { + const layoutName = 'mobileBottomMenu' + const { isSelectedRef, componentClickHandler, curTemplateData } = useComponentAction(false, layoutName, 'MOBILE') + return { + isTabbarSelected: isSelectedRef, + componentClickHandler, + curTemplateData + } + }, + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + class: [cssModule['mobile-tabbar-layout-wrapper'], cssModule['mobile-layout-wrapper'], 'bk-lesscode-mobile-layout-wrapper'], + ref: 'layout', + children: [ + typeof this.$slots.default === 'function' ? this.$slots.default() : this.$slots.default, + h({ + component: 'van-tabbar', + props: { + route: true + }, + children: [ + h({ + component: 'div', + class: cssModule['tabbar-item-wrapper'], + on: { + click: self.componentClickHandler + }, + children: self.curTemplateData?.menuList?.map((menu) => { + return h({ + component: 'van-tabbar-item', + key: menu.id, + props: { + replace: true, + to: menu.fullPath, + icon: menu.icon + }, + children: [ + menu.name + ] + }) + }) + }) + ] + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.postcss b/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.postcss new file mode 100644 index 000000000..d01077670 --- /dev/null +++ b/lib/client/src/components/render/mobile/widget/layout/tab-bar/tab-bar.postcss @@ -0,0 +1,33 @@ +.mobile-tabbar-layout-wrapper { + height: 100%; + :global(.lesscode-editor-layout) { + min-height: auto; + } + .van-tabbar { + position: absolute; + } + .tabbar-item-wrapper { + width: 100%; + display: flex; + box-sizing: content-box; + border: 1px solid transparent; + z-index: 2; + cursor: pointer; + &:hover { + border: 1px dashed #3a84ff; + } + &.selected { + border: 1px solid #3a84ff; + } + .van-tabbar-item { + pointer-events: none; + } + } +} + +.mobile-layout-wrapper { + overflow-y: scroll; + &::-webkit-scrollbar { + display: none; + } +} diff --git a/lib/client/src/components/render/pc/components/draggable.vue b/lib/client/src/components/render/pc/components/draggable.vue deleted file mode 100644 index 78411c936..000000000 --- a/lib/client/src/components/render/pc/components/draggable.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - diff --git a/lib/client/src/components/render/pc/components/draggable/draggable.js b/lib/client/src/components/render/pc/components/draggable/draggable.js new file mode 100644 index 000000000..27265c51e --- /dev/null +++ b/lib/client/src/components/render/pc/components/draggable/draggable.js @@ -0,0 +1,233 @@ +import draggableCssModule from './draggable.postcss?module' +import { h } from 'bk-lesscode-render' +import LC from '@/element-materials/core' +import { setMousedown } from '../../resolve-component/resolve-component' +import sortable from '@/components/sortable' +import getRoot from '@/element-materials/core/static/get-root' +import clearCanvas from '@/element-materials/core/static/reset' +import store from '@/store' + +export default { + name: 'render-draggable', + props: { + list: { + type: Array, + required: false, + default: null + }, + componentData: { + type: Object, + required: true + }, + ghostClass: { + type: String + }, + group: { + type: Object, + required: true + }, + forceFallback: { + type: Boolean + } + }, + inject: { + attachToInteractiveComponent: { + value: 'attachToInteractiveComponent', + default: false + } + }, + emits: ['choose', 'unchoose', 'start', 'end', 'add', 'sort', 'change'], + data () { + return { + dragGroup: this.group, + styles: {} + } + }, + created () { + LC.addEventListener('removeChild', this.removeChildCallback) + LC.addEventListener('toggleInteractive', this.dragableCheck) + }, + mounted () { + // setTimeout(() => { + // this.$refs.draggable.computeIndexes?.() + // }) + }, + beforeDestroy () { + LC.removeEventListener('removeChild', this.removeChildCallback) + LC.removeEventListener('toggleInteractive', this.dragableCheck) + }, + methods: { + /** + * 交互式组件状态更新 + * @description 当交互式组件激活时,不属于交互式组件的drag area不可拖动 + * 只有关闭后,才可以继续拖拽 + */ + dragableCheck (event) { + if (event.interactiveShow + && !this.attachToInteractiveComponent) { + this.dragGroup = Object.freeze({ + pull: false, + put: false + }) + } else { + this.dragGroup = this.group + } + }, + removeChildCallback (event) { + if (event.child.interactiveShow) { + this.dragGroup = this.group + } + }, + /** + * @desc 拖动选中 + * @param { Object } dragEvent + */ + handleChoose (event) { + const { + height + } = this.$refs.draggable.$el.getBoundingClientRect() + this.styles = { + height: `${height}px` + } + this.$emit('choose', event) + }, + handleUnchoose (event) { + this.styles = {} + this.$emit('unchoose', event) + }, + /** + * @desc 开始拖拽 + * @param { Object } dragEvent + */ + handleStart (event) { + this.$emit('start', event) + LC.triggerEventListener('componentDragStart', { + type: 'componentDragStart' + }) + // LC.triggerEventListener('componentMouserleave') + // const activeNode = LC.getActiveNode() + // if (activeNode) { + // activeNode.activeClear() + // } + }, + /** + * @desc 结束拖拽 + * @param { Object } dragEvent + */ + handleEnd (event) { + this.styles = {} + setMousedown(false) + LC.triggerEventListener('componentDragEnd', { + type: 'componentDragEnd' + }) + this.$emit('end', event) + }, + /** + * @desc 添加组件 + * @param { Object } dragEvent + */ + handleAdd (event) { + // fix: vue-draggable 内部索引不更新的问题 + // this.$refs.draggable.computeIndexes() + setMousedown(false) + this.$emit('add', event) + }, + handleSort (event) { + this.$emit('sort', event) + }, + /** + * @desc 拖动更新 + * @param { Object } dragEvent + */ + handleChange (event) { + let operationNode = null + const triggerEvent = { + target: this.componentData, + type: '', + child: null + } + if (event.added) { + operationNode = event.added.element + triggerEvent.type = 'appendChild' + // 拖动组件需要重置会影响排版的样式 + operationNode.setStyle({ + position: '', + top: '', + right: '', + bottom: '', + left: '', + zIndex: '', + marginTop: '', + marginRight: '', + marginBottom: '', + marginLeft: '' + }) + setTimeout(() => { + // 新加的组件默认选中 + operationNode.active() + }, 100) + } else if (event.removed) { + operationNode = event.removed.element + triggerEvent.type = 'removeChild' + } else if (event.moved) { + operationNode = event.moved.element + triggerEvent.type = 'moveChild' + } + + triggerEvent.child = operationNode + LC.triggerEventListener(triggerEvent.type, triggerEvent) + LC.triggerEventListener('update', triggerEvent) + // fix: vue-draggable 内部索引不更新的问题 + // this.$refs.draggable.computeIndexes() + this.$emit('change', event) + + /** 优化H5使用体验,当H5插入到非root节点时,进行相关提示 */ + if (triggerEvent.child.type === 'h5-container' && triggerEvent.target.type !== 'root') { + const root = getRoot() + const updateH5Layout = () => { + root.appendChild(triggerEvent.child) + } + clearCanvas( + window.i18n.t('H5容器应在根节点上'), + window.i18n.t('H5容器默认占满全屏,与其他容器不兼容,是否自动清空其他容器,仅保留H5容器'), + updateH5Layout) + } + } + }, + render (render) { + h.init(render) + + const self = this + + return h({ + component: sortable, + ref: 'draggable', + class: draggableCssModule['drag'], + style: self.styles, + attrs: { + role: 'draggable' + }, + props: { + list: self.list, + options: { + group: self.dragGroup, + chosenClass: draggableCssModule['chosen'], + ghostClass: self.ghostClass || (store.state.Language === 'en' ? draggableCssModule['en-ghost'] : draggableCssModule['ghost']), + filter: '[data-render-drag="disabled"]', + forceFallback: self.forceFallback + } + }, + on: { + choose: self.handleChoose, + unchoose: self.handleUnchoose, + start: self.handleStart, + end: self.handleEnd, + add: self.handleAdd, + sort: self.handleSort, + change: self.handleChange, + ...self.$listeners + }, + children: self.$slots.default + }) + } +} diff --git a/lib/client/src/components/render/pc/components/draggable/draggable.postcss b/lib/client/src/components/render/pc/components/draggable/draggable.postcss new file mode 100644 index 000000000..31818ed45 --- /dev/null +++ b/lib/client/src/components/render/pc/components/draggable/draggable.postcss @@ -0,0 +1,84 @@ +.drag{ + position: relative; + width: 100%; + height: 100% !important; + pointer-events: auto !important; + &:empty{ + min-height: 34px; + } +} +.chosen{ + opacity: .9; +} +.ghost{ + margin-bottom: 5px; + &:after { + content: "放在这里"; + display: block; + height: 24px; + padding: 0 5px; + font-size: 12px; + color: #fff; + text-align: center; + line-height: 24px; + background-color: #C2D7F9; + } + &:global(.inline), + &:global(.inline-block) { + display: inline-block; + vertical-align: sub; + &:after { + width: 60px; + display: inline-block; + position: relative; + } + } + &:global(.block) { + display: block; + &:after { + top: 0; + display: inline-block; + width: 100%; + position: relative; + } + } + & > * { + display: none !important; + } +} +.en-ghost{ + margin-bottom: 5px; + &:after { + content: "Set it here"; + display: block; + height: 24px; + padding: 0 5px; + font-size: 12px; + color: #fff; + text-align: center; + line-height: 24px; + background-color: #C2D7F9; + } + &:global(.inline), + &:global(.inline-block) { + display: inline-block; + vertical-align: sub; + &:after { + width: 60px; + display: inline-block; + position: relative; + } + } + &:global(.block) { + display: block; + &:after { + top: 0; + display: inline-block; + width: 100%; + position: relative; + } + } + & > * { + display: none !important; + } +} diff --git a/lib/client/src/components/render/pc/components/edit-object/edit-object.js b/lib/client/src/components/render/pc/components/edit-object/edit-object.js new file mode 100644 index 000000000..719f8bd5b --- /dev/null +++ b/lib/client/src/components/render/pc/components/edit-object/edit-object.js @@ -0,0 +1,69 @@ +import cssModule from './edit-object.postcss?module' +import { + h +} from 'bk-lesscode-render' + +export default { + props: { + value: Object + }, + + emits: ['change'], + + data () { + return { + displayJSON: '', + copyValue: '' + } + }, + + created () { + this.getDisplayJson(JSON.stringify(this.value)) + }, + + methods: { + getDisplayJson (val) { + try { + this.displayJSON = JSON.stringify(JSON.parse(val), null, 2) + this.copyValue = JSON.stringify(JSON.parse(val), null, 2) + this.$emit('change', JSON.parse(val)) + } catch (e) { + this.displayJSON = this.$t('请输入正确格式的数据') + } + } + }, + + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'section', + class: cssModule['edit-object'], + style: self.styleVar, + children: [ + h({ + component: 'bk-input', + props: { + type: 'textarea', + value: self.copyValue, + modelValue: self.copyValue + }, + on: { + input: self.getDisplayJson + } + }), + h({ + component: 'bk-input', + props: { + type: 'textarea', + readonly: true, + value: self.displayJSON, + modelValue: self.displayJSON + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/components/edit-object/edit-object.postcss b/lib/client/src/components/render/pc/components/edit-object/edit-object.postcss new file mode 100644 index 000000000..9c36688fc --- /dev/null +++ b/lib/client/src/components/render/pc/components/edit-object/edit-object.postcss @@ -0,0 +1,5 @@ +.edit-object { + display: flex; + flex-direction: row; + align-items: stretch; +} diff --git a/lib/client/src/components/render/pc/components/navigation/menu-item.js b/lib/client/src/components/render/pc/components/navigation/menu-item.js new file mode 100644 index 000000000..880e38f5a --- /dev/null +++ b/lib/client/src/components/render/pc/components/navigation/menu-item.js @@ -0,0 +1,92 @@ +import { + h, + framework +} from 'bk-lesscode-render' + +export default { + props: { + children: Array, + icon: String, + id: String, + title: String + }, + + methods: { + handleOpen () { + this.$refs.item?.handleOpen?.() + }, + + renderVue2 () { + return h({ + component: 'bk-navigation-menu-item', + ref: 'item', + props: { + hasChild: this.children?.length > 0, + icon: this.icon, + id: this.id + }, + children: [ + this.title, + ...this.children?.map?.(childrenItem => h({ + component: 'bk-navigation-menu-item', + slot: 'child', + key: childrenItem.id, + props: { + id: childrenItem.pageCode + }, + children: [ + childrenItem.name + ] + })) + ] + }) + }, + + renderVue3 () { + const renderIconSlot = (icon) => { + if (!icon) return '' + return h({ + component: 'i', + class: icon + }) + } + const renderMenuItem = (key, title, icon) => { + return h({ + component: 'bk-menu-item', + props: { + key + }, + slots: { + icon: () => renderIconSlot(icon), + default: () => title + } + }) + } + // 多个子节点 + if (this.children?.length > 0) { + return h({ + component: 'bk-submenu', + props: { + key: this.id, + title: this.title + }, + slots: { + icon: renderIconSlot(this.icon), + default: () => this.children?.map?.(childrenItem => renderMenuItem(childrenItem.pageCode, childrenItem.name)) + } + }) + } + // 单个 menu-item + return renderMenuItem(this.id, this.title, this.icon) + } + }, + + render (render) { + h.init(render) + if (framework === 'vue2') { + return this.renderVue2() + } else { + return this.renderVue3() + } + } +} diff --git a/lib/client/src/components/render/pc/components/navigation/menu.js b/lib/client/src/components/render/pc/components/navigation/menu.js new file mode 100644 index 000000000..5db9c620c --- /dev/null +++ b/lib/client/src/components/render/pc/components/navigation/menu.js @@ -0,0 +1,38 @@ +import { + h, + framework +} from 'bk-lesscode-render' + +export default { + props: { + defaultActive: String, + themeColorProps: Object, + openedKeys: Array + }, + + render (render) { + h.init(render) + + if (framework === 'vue2') { + return h({ + component: 'bk-navigation-menu', + props: { + uniqueOpened: false, + toggleActive: true, + defaultActive: this.defaultActive, + ...this.themeColorProps + }, + children: this.$slots.default + }) + } + return h({ + component: 'bk-menu', + props: { + uniqueOpen: false, + activeKey: this.defaultActive, + openedKeys: this.openedKeys + }, + children: this.$slots.default + }) + } +} diff --git a/lib/client/src/components/render/pc/index.vue b/lib/client/src/components/render/pc/index.vue deleted file mode 100644 index dd9ad127b..000000000 --- a/lib/client/src/components/render/pc/index.vue +++ /dev/null @@ -1,327 +0,0 @@ - - - - - diff --git a/lib/client/src/components/render/pc/index/index.js b/lib/client/src/components/render/pc/index/index.js new file mode 100644 index 000000000..d1e42b018 --- /dev/null +++ b/lib/client/src/components/render/pc/index/index.js @@ -0,0 +1,321 @@ + +// Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available. +// Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. All rights reserved. +// Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://opensource.org/licenses/MIT +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +import indexCssModule from './index.postcss?module' +import { h } from 'bk-lesscode-render' +import LC from '@/element-materials/core' +import Draggable from '../components/draggable/draggable' +import Layout from '../widget/layout' +import ResolveComponent, { setMousedown } from '../resolve-component/resolve-component' +import ResolveInteractiveComponent from '../resolve-interactive-component/resolve-interactive-component' +import LesscodeFocus from '../tools/lesscode-focus' +import LesscodeTools from '../tools/lesscode-tool' +import LesscodeResize from '../tools/lesscode-resize' +import LesscodeMargin from '../tools/lesscode-margin' + +export default { + name: 'render', + provide () { + return { + attachToInteractiveComponent: false + } + }, + data () { + return { + isReady: LC.__isReady, + showNotVisibleMask: false, + invisibleComponent: '' + } + }, + watch: { + showNotVisibleMask (val) { + if (val) { + const activeNode = LC.getActiveNode() + this.invisibleComponent = activeNode.componentId + } + } + }, + created () { + this.componentData = LC.getRoot() + LC.addEventListener('ready', this.readyCallback) + LC.addEventListener('update', this.updateCallback) + LC.addEventListener('update', this.updateLogCallback) + LC.addEventListener('active', this.interactiveCallback) + LC.addEventListener('active', this.activeLogCallback) + LC.addEventListener('toggleInteractive', this.interactiveCallback) + LC.addEventListener('appendChild', this.nodeCallback) + LC.addEventListener('moveChild', this.nodeCallback) + LC.addEventListener('insertAfter', this.nodeCallback) + LC.addEventListener('removeChild', this.interactiveCallback) + LC.addEventListener('rollback', this.rollbackCallback) + }, + mounted () { + this.componentData.mounted(document.querySelector('#drawTarget')) + LC._mounted() + + document.body.addEventListener('mousedown', this.mousedownCallback) + document.body.addEventListener('mouseup', this.mouseupCallback) + document.body.addEventListener('click', this.resetCallback) + }, + beforeDestroy () { + // remove created listener + LC.removeEventListener('ready', this.readyCallback) + LC.removeEventListener('update', this.updateCallback) + LC.removeEventListener('update', this.updateLogCallback) + LC.removeEventListener('active', this.interactiveCallback) + LC.removeEventListener('active', this.activeLogCallback) + LC.removeEventListener('toggleInteractive', this.interactiveCallback) + LC.removeEventListener('appendChild', this.nodeCallback) + LC.removeEventListener('moveChild', this.nodeCallback) + LC.removeEventListener('insertAfter', this.nodeCallback) + LC.removeEventListener('removeChild', this.interactiveCallback) + LC.removeEventListener('rollback', this.rollbackCallback) + // remove mounted listener + document.body.removeEventListener('mousedown', this.mousedownCallback) + document.body.removeEventListener('mouseup', this.mouseupCallback) + document.body.removeEventListener('click', this.resetCallback) + }, + methods: { + readyCallback () { + this.isReady = true + }, + updateLogCallback (event) { + console.log('\n') + console.log(`%c>> ${new Date().toString().slice(0, 25)}`, + 'background-color: #3A84FF; color: #fff; padding: 2px 5px; border-radius: 3px; font-weight: bold;') + console.log(`%c组件更新%c${event.target.componentId}`, + 'padding: 2px 5px; background: #ea3636; color: #fff; border-radius: 3px 0 0 3px;', + 'padding: 2px 5px; background: #42c02e; color: #fff; border-radius: 0 3px 3px 0; font-weight: bold;', + event) + }, + activeLogCallback (event) { + console.log('\n') + console.log(`%c>> ${new Date().toString().slice(0, 25)}`, + 'background-color: #3A84FF; color: #fff; padding: 2px 5px; border-radius: 3px; font-weight: bold;') + console.log(`%c组件选中%c${event.target.componentId}`, + 'padding: 2px 5px; background: #ff9c01; color: #fff; border-radius: 3px 0 0 3px;', + 'padding: 2px 5px; background: #42c02e; color: #fff; border-radius: 0 3px 3px 0; font-weight: bold;', + event) + }, + updateCallback (event) { + if (event.target.componentId === this.componentData.componentId) { + this.$forceUpdate() + } + }, + /** + * @name interactiveCallback + * @description 当交互式组件的状态改变,每次更新需要监测是否显示“打开交互式组件”的提示 + */ + interactiveCallback () { + const activeNode = LC.getActiveNode() + if (activeNode) { + this.showNotVisibleMask = activeNode.isInteractiveComponent && !activeNode.interactiveShow + return + } + this.showNotVisibleMask = false + }, + rollbackCallback () { + this.componentData.mounted(document.querySelector('#drawTarget')) + }, + nodeCallback (event) { + if (event.target.componentId === this.componentData.componentId) { + this.$forceUpdate() + setTimeout(() => { + this.autoType(event.child) + }, 20) + } + }, + mousedownCallback () { + setMousedown(true) + }, + mouseupCallback () { + setMousedown(false) + }, + resetCallback () { + LC.clearMenu() + }, + /** + * @desc 自动排版子组件 + */ + autoType (childNode) { + if (this._isDestroyed || !childNode || childNode.isInteractiveComponent) { + return + } + const { + top: boxTop, + left: boxLeft + } = document.querySelector('.target-drag-area').getBoundingClientRect() + const $childEl = childNode.$elm + const { + top: componentTop, + left: componentLeft + } = $childEl.getBoundingClientRect() + + const styles = {} + // 如果被复制的组件已经有marginLeft或marginTop, 则不覆盖 + const { marginTop: childMarginTop, marginLeft: childMarginLeft } = childNode.renderStyles || {} + if (componentTop > boxTop + 3 && !childMarginTop) { + styles['marginTop'] = '8px' + } + if (componentLeft > boxLeft + 3 && !childMarginLeft) { + styles['marginLeft'] = '8px' + } + if (Object.keys(styles).length > 0) { + childNode.setStyle(styles) + } + }, + maskDbCLickHandler () { + this.showNotVisibleMask = false + }, + /** + * @desc 鼠标右键操作面板 + */ + handleShowContextmenu (event) { + event.stopPropagation() + const activeNode = LC.getActiveNode() + if (activeNode) { + activeNode.activeClear() + } + LC.showMenu(event) + }, + /** + * @desc 鼠标离开时清除组件 hover 效果 + * @param { Boolean } name + * @returns { Boolean } + */ + handleMouseleave () { + LC.triggerEventListener('componentMouserleave', { + type: 'componentMouserleave' + }) + }, + /** + * @desc 画布编辑区空白区域被点击取消组件的选中状态 + * @param { Object } event + */ + handleCanvasClick (event) { + if (event.target.classList.contains(indexCssModule['drag-area'])) { + const activeNode = LC.getActiveNode() + if (activeNode) { + activeNode.activeClear() + } + LC.triggerEventListener('componentMouserleave', { + type: 'componentMouserleave' + }) + } + } + }, + render (render) { + h.init(render) + + const self = this + + const renderDraggable = () => { + const nodes = [h({ + component: Draggable, + class: `target-drag-area ${indexCssModule['drag-area']}`, + props: { + componentData: self.componentData, + list: self.componentData.slot.default, + sort: true, + group: { + name: 'layout', + pull: true, + put: true + } + }, + children: self.componentData?.slot?.default?.map?.((componentNode) => { + // root 的子组件只会是布局组件和交互式组件 + return h({ + component: componentNode.isInteractiveComponent ? ResolveInteractiveComponent : ResolveComponent, + props: { + componentData: componentNode + }, + key: componentNode.renderKey + }) + }) + })] + + if (self.componentData.children.length < 1) { + nodes.unshift(h({ + component: 'div', + class: indexCssModule['empty'], + children: [ + self.$t('请拖入组件') + ] + })) + } + return nodes + } + + const renderTools = () => { + return [ + h({ component: LesscodeFocus }), + h({ component: LesscodeTools }), + h({ component: LesscodeResize }), + h({ component: LesscodeMargin }) + ] + } + + const renderDrawTarget = () => { + return h({ + component: 'div', + attrs: { + id: 'drawTarget' + }, + class: { + [indexCssModule['canvas']]: true + }, + on: { + click: self.handleCanvasClick, + mouseleave: self.handleMouseleave, + contextmenu: self.handleShowContextmenu + }, + children: [ + renderDraggable(), + ...renderTools() + ] + }) + } + + const renderNotVisibleMask = () => { + return h({ + component: 'div', + class: indexCssModule['not-visible-mask'], + on: { + dblclick: self.maskDbCLickHandler + }, + children: [ + h({ + component: 'p', + children: [ + `该组件(${self.invisibleComponent})处于隐藏状态,请先打开` + ] + }), + h({ + component: 'p', + props: { + class: 'mt20' + }, + children: [ + self.$t('双击继续操作页面其他组件') + ] + }) + ] + }) + } + + return h({ + component: Layout, + children: [ + renderDrawTarget(), + self.showNotVisibleMask ? renderNotVisibleMask() : '' + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/index/index.postcss b/lib/client/src/components/render/pc/index/index.postcss new file mode 100644 index 000000000..d02c8ceb6 --- /dev/null +++ b/lib/client/src/components/render/pc/index/index.postcss @@ -0,0 +1,63 @@ +@import '../widget/patch.css'; + +.canvas{ + position: relative; + z-index: 99999999 !important; + min-height: calc(100% - 20px) !important; + .empty{ + min-height: 34px !important; + background: #FAFBFD; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + color: #C4C6CC; + pointer-events: all; + } +} +.drag-area{ + padding-bottom: 300px; + * { + /* 规避一些组件内部因为设置了 pointer-events 导致鼠标事件非法触发 */ + pointer-events: none; + } +} +.not-visible-mask{ + position: fixed; + z-index: 99999999; + display: flex; + flex-direction: column; + align-items: center; + background: rgba(0,0,0,0.8); + top: 0; + right: 0; + bottom: 0; + left: 0; + padding-top: 100px; + color: #fff; +} + +:global(.lesscode-bk-color-picker-icon .icon-angle-down){ + &::before { + content: '' !important; + } +} + +:global(.lesscode-bk-cascader-node .icon-angle-right){ + &::before { + content: '' !important; + } +} + +:global(.lesscode-bk-table-body-content .cell.none) { + display: block !important; +} + +:global([data-bk-mask-uid]) { + pointer-events: none; +} diff --git a/lib/client/src/components/render/pc/render-component.js b/lib/client/src/components/render/pc/render-component.js index 1e4fcbe38..61e25c29b 100644 --- a/lib/client/src/components/render/pc/render-component.js +++ b/lib/client/src/components/render/pc/render-component.js @@ -1,76 +1,136 @@ import LC from '@/element-materials/core' +import { + h +} from 'bk-lesscode-render' +import RenderSlot from './render-slot' +import ResolveComponent from './resolve-component/resolve-component' +import ResolveInteractiveComponent from './resolve-interactive-component/resolve-interactive-component' +import FreeLayout from './widget/free-layout/free-layout' +import RenderGrid from './widget/grid/grid' +import h5Container from './widget/h5-container/h5-container' +import RenderColumn from './widget/column/column' +import RenderBlock from './widget/block/block' +import WidgetTab from './widget/tab/tab' +import WidgetForm from './widget/form/form' +import WidgetFormItem from './widget/form/form-item' +import WidgetBkTable from './widget/table/table' + +import LesscodeLoading from './tools/lesscode-loading' export default { name: 'render-component', - functional: true, - render (h, context) { - const { - componentData - } = context.props - - // fix: 修正部分样式在编辑时应用到实际的组件会产生偏差和重叠 - const baseComponentStyleReset = { - // 修正组件会影响位置的样式 - 'margin': '', - 'margin-top': '', - 'margin-right': '', - 'margin-bottom': '', - 'margin-left': '', - 'transform': '', - // 修正会产生叠加效果的样式 - 'box-shadow': '', - 'z-index': '' - } - // fix: 基础组件的根元素可能会有定位样式(relative, absolute)当top、right、bottom、left 生效时会导致偏移 - // - 组件在 freelayout 里面时进行位置修正 - // - 非交互式组件对定位样式进行修正 - if (context.parent.attachToFreelayout - || !componentData.isInteractiveComponent) { - Object.assign(baseComponentStyleReset, { - position: '', - top: '', - right: '', - bottom: '', - left: '' - }) + + components: { + RenderSlot, + ResolveComponent, + ResolveInteractiveComponent, + FreeLayout, + RenderGrid, + h5Container, + h5Page: FreeLayout, + RenderColumn, + RenderBlock, + WidgetTab, + WidgetForm, + WidgetFormItem, + WidgetBkTable + }, + + props: { + componentData: Object + }, + + computed: { + baseComponentStyleReset () { + // fix: 修正部分样式在编辑时应用到实际的组件会产生偏差和重叠 + const baseComponentStyleReset = { + // 修正组件会影响位置的样式 + 'margin': '', + 'margin-top': '', + 'margin-right': '', + 'margin-bottom': '', + 'margin-left': '', + 'transform': '', + // 修正会产生叠加效果的样式 + 'box-shadow': '', + 'z-index': '' + } + // fix: 基础组件的根元素可能会有定位样式(relative, absolute)当top、right、bottom、left 生效时会导致偏移 + // - 组件在 freelayout 里面时进行位置修正 + // - 非交互式组件对定位样式进行修正 + if (this.$parent.attachToFreelayout + || !this.componentData.isInteractiveComponent) { + Object.assign(baseComponentStyleReset, { + position: '', + top: '', + right: '', + bottom: '', + left: '' + }) + } + + // fix: 样式导致基础组件的交互问题 + if (!this.$parent.isShadowComponent) { + Object.assign(baseComponentStyleReset, { + // 基础组件的层级最低(基础组件可能本身有 border 样式,保证组件选中和 hover 时的边框效果能显示出来) + 'z-index': 0, + // 隔绝基础组件的鼠标事件响应 + 'pointer-events': 'none' + }) + } + + return baseComponentStyleReset } + }, - // fix: 样式导致基础组件的交互问题 - if (!context.parent.isShadowComponent) { - Object.assign(baseComponentStyleReset, { - // 基础组件的层级最低(基础组件可能本身有 border 样式,保证组件选中和 hover 时的边框效果能显示出来) - 'z-index': 0, - // 隔绝基础组件的鼠标事件响应 - 'pointer-events': 'none' - }) + created () { + LC.addEventListener('update', this.updateCallback) + }, + + beforeDestroy () { + LC.removeEventListener('update', this.updateCallback) + }, + + methods: { + updateCallback (event) { + if (event.target.componentId === this.componentData.componentId) { + this.$forceUpdate() + } } + }, + + render (render) { + h.init(render) + + const parent = this.$parent // 如果是画布区域的 shadow 组件需要透传 componentData - const props = Object.assign({}, componentData.prop, { - 'component-data': componentData, + const props = Object.assign({}, this.componentData.prop, { + 'component-data': this.componentData, 'show-mask': false }) const events = {} // 交互式组件需要处理隐藏显示逻辑 - if (componentData.isInteractiveComponent) { - props.value = componentData.interactiveShow + if (this.componentData.isInteractiveComponent) { + props.value = this.componentData.interactiveShow + props.isShow = this.componentData.interactiveShow events.input = value => { - componentData.setProperty('interactiveShow', value) + this.componentData.setProperty('interactiveShow', value) } } // table 组件逻辑 - if (['widget-bk-table', 'widget-el-table'].includes(componentData.type)) { + if (['widget-bk-table', 'widget-el-table'].includes(this.componentData.type)) { // 处理操作逻辑 - if (componentData.renderProps.data.valueType === 'table-data-source') { - props.tableName = componentData.renderProps.data?.payload?.sourceData?.tableName - props.bkDataSourceType = componentData.renderProps.data?.payload?.sourceData?.dataSourceType - props.showOperationColumn = componentData.renderProps.data?.payload?.sourceData?.showOperationColumn + if (this.componentData.renderProps.data.valueType === 'table-data-source') { + props.tableName = this.componentData.renderProps.data?.payload?.sourceData?.tableName + props.bkDataSourceType = this.componentData.renderProps.data?.payload?.sourceData?.dataSourceType + props.showOperationColumn = this.componentData.renderProps.data?.payload?.sourceData?.showOperationColumn } // 处理分页逻辑 - if (componentData.renderProps.pagination) { - const paginationPayload = componentData.renderProps.pagination?.payload || {} + if (this.componentData.renderProps.pagination) { + const paginationPayload = this.componentData.renderProps.pagination?.payload || {} if (['local', 'remote'].includes(paginationPayload.type)) { props.pagination = Object.keys(paginationPayload.val || {}).reduce((acc, cur) => { acc[cur] = paginationPayload.val[cur].val @@ -81,15 +141,15 @@ export default { delete props.pagination } } - props.dataValueType = componentData.renderProps.data.valueType + props.dataValueType = this.componentData.renderProps.data.valueType } const attrs = { - role: componentData.type + role: this.componentData.type } - Object.keys(context.parent.material.props || {}).forEach(propName => { - const propConfig = context.parent.material.props[propName] + Object.keys(parent.material.props || {}).forEach(propName => { + const propConfig = parent.material.props[propName] // feature: prop 被标记为 staticValue,在画布编辑时不动态改变 // 永远使用默认值 if (Object.prototype.hasOwnProperty.call(propConfig, 'staticValue')) { @@ -98,24 +158,26 @@ export default { // fix:vue 特性 // class、style属性默认会被子组件继承 if (['class', 'style'].includes(propName)) { - attrs[propName] = componentData.prop[propName] + attrs[propName] = this.componentData.prop[propName] } }) // 为基础组件打上标记 - if (!context.parent.isShadowComponent) { + if (!parent.isShadowComponent) { attrs['lesscode-base-component'] = '' } - const renderSlotMap = Object.keys(componentData.slot).reduce((result, slotName) => { - const slotList = Array.isArray(componentData.slot[slotName]) - ? componentData.slot[slotName] - : [componentData.slot[slotName]] + const renderSlotMap = Object.keys(this.componentData.slot).reduce((result, slotName) => { + const slotList = Array.isArray(this.componentData.slot[slotName]) + ? this.componentData.slot[slotName] + : [this.componentData.slot[slotName]] result[slotName] = () => slotList.map(slot => { // 如果是组件渲染组件 if (LC.isNode(slot)) { - return h('resolve-component', { + return h({ + component: ResolveComponent, + slot: slotName, props: { componentData: slot }, @@ -123,22 +185,43 @@ export default { }) } // 渲染组件slot配置 - return h('render-slot', { + return h({ + component: RenderSlot, + slot: slotName, props: { slotData: slot, - key: componentData.renderSlotKey + slot: slotName, + renderKey: this.componentData.renderSlotKey } }) }) return result }, {}) - return h(componentData.type, { + const renderProps = { + component: this.componentData.type, + key: this.componentData.renderKey, props, attrs, - on: events, - scopedSlots: renderSlotMap, - style: Object.assign({}, componentData.style, baseComponentStyleReset) - }, renderSlotMap.default && renderSlotMap.default()) + style: Object.assign({}, this.componentData.style, this.baseComponentStyleReset), + on: events + } + + // 原生 html 元素只有 children + if (parent.isHtmlElement) { + renderProps.children = renderSlotMap.default?.() + } else { + renderProps.slots = renderSlotMap + } + + return h({ + component: LesscodeLoading, + props: { + componentData: this.componentData + }, + children: [ + h(renderProps) + ] + }) } } diff --git a/lib/client/src/components/render/pc/render-slot.js b/lib/client/src/components/render/pc/render-slot.js index 9c6010853..0077c3d5c 100644 --- a/lib/client/src/components/render/pc/render-slot.js +++ b/lib/client/src/components/render/pc/render-slot.js @@ -9,23 +9,23 @@ * specific language governing permissions and limitations under the License. */ -import { transformHtmlToVnode } from '@/common/util' -import slotRenderConfig from '@/element-materials/modifier/component/slots/render-config' +import { + getRenderMap +} from '@/element-materials/modifier/component/slots/render-config' +import { + transformHtmlToVnode, + framework +} from 'bk-lesscode-render' export default { name: 'render-slot', functional: true, - props: { - key: [Number, String], - slotData: { - type: [Number, String, Boolean, Object, Array], - default: '' - } - }, - render (h, ctx) { - const { key, slotData } = ctx.props + render (vue3Context, vue2Context) { + const props = framework === 'vue3' ? vue3Context._.attrs : vue2Context.props + const { renderKey, slotData } = props const { component } = slotData + const slotRenderConfig = getRenderMap(framework) const render = slotRenderConfig[component] || (() => {}) const slotRenderParams = [] let curSlot = slotData @@ -42,11 +42,22 @@ export default { curSlot = curSlot.renderSlots } while (curSlot && Object.keys(curSlot).length > 0) - const html = `${render(...slotRenderParams, key)}` - const slotChildrenInstance = transformHtmlToVnode(html).children || [] - slotChildrenInstance?.forEach((vnode) => { - vnode.key = vnode.key + key + // 转换 vnode + let children + if (framework === 'vue2') { + // vue2 需要有一个父容器包裹 + const html = `${render(...slotRenderParams, renderKey)}` + children = transformHtmlToVnode(html).children || [] + } else { + const html = render(...slotRenderParams, renderKey) + children = transformHtmlToVnode(html) || [] + children = Array.isArray(children) ? children : [children] + } + children.forEach?.((vnode) => { + if (vnode.key) { + vnode.key = vnode.key + renderKey + } }) - return slotChildrenInstance + return children } } diff --git a/lib/client/src/components/render/pc/resolve-component.vue b/lib/client/src/components/render/pc/resolve-component.vue deleted file mode 100644 index 39b70df69..000000000 --- a/lib/client/src/components/render/pc/resolve-component.vue +++ /dev/null @@ -1,625 +0,0 @@ - - - - - diff --git a/lib/client/src/components/render/pc/resolve-component/resolve-component.js b/lib/client/src/components/render/pc/resolve-component/resolve-component.js new file mode 100644 index 000000000..1d34a1798 --- /dev/null +++ b/lib/client/src/components/render/pc/resolve-component/resolve-component.js @@ -0,0 +1,553 @@ +import cssModule from './resolve-component.postcss?module' +import _ from 'lodash' +import LC from '@/element-materials/core' +import RenderComponent from '../render-component' +import RenderSlot from '../render-slot' +import { h, framework } from 'bk-lesscode-render' + +const getRenderStyleDisplayValue = display => { + switch (display) { + case 'flex': + case 'grid': + case 'block': + return 'block' + case 'inline-flex': + case 'inline-grid': + case 'inline-block': + return 'inline-block' + case 'inline': + return 'inline' + // 默认按块级元素处理 + default: + return 'block' + } +} + +const isNumberValue = value => { + return /^[-]?\d/.test(value) +} + +// 记录 mousedown 状态 +// mousedown时不响应mousemove +let isMousedown = false + +export const setMousedown = value => { + isMousedown = value +} + +const safeStyles = { + // fix: 影响子元素排版 + display: 'block', + 'padding': '', + 'padding-top': '', + 'padding-right': '', + 'padding-bottom': '', + 'padding-left': '', + 'line-height': '', + 'letter-spacing': '', + 'word-spacing': '', + 'text-align': '', + 'text-decoration': '', + 'text-indent': '', + 'text-overflow': '', + 'text-rendering': '', + 'text-size-adjust': '', + 'text-shadow': '', + 'text-transform': '', + 'word-break': '', + 'word-wrap': '', + 'white-space': '', + // fix: 父子元素效果叠加 + 'background': '', + 'background-attachment': '', + 'background-color': '', + 'background-image': '', + 'background-position': '', + 'background-repeat': '', + 'background-size': '', + 'border': '', + 'border-image': '', + 'border-collapse': '', + 'border-color': '', + 'border-top': '', + 'border-right': '', + 'border-bottom': '', + 'border-left': '', + 'border-top-color': '', + 'border-right-color': '', + 'border-bottom-color': '', + 'border-left-color': '', + 'border-spacing': '', + 'border-style': '', + 'border-top-style': '', + 'border-right-style': '', + 'border-bottom-style': '', + 'border-left-style': '', + 'border-width': '', + 'border-top-width': '', + 'border-right-width': '', + 'border-bottom-width': '', + 'border-left-width': '', + 'border-radius': '', + 'border-top-right-radius': '', + 'border-bottom-right-radius': '', + 'border-bottom-left-radius': '', + 'border-top-left-radius': '', + 'border-radius-topright': '', + 'border-radius-bottomright': '', + 'border-radius-bottomleft': '', + 'border-radius-topleft': '', + opacity: '' +} + +export default { + name: 'resolve-component', + components: { + RenderComponent, + RenderSlot + }, + emits: ['component-update', 'component-mounted', 'component-mousedown'], + props: { + componentData: { + type: Object, + required: true + }, + attachToFreelayout: { + type: Boolean, + default: false + } + }, + data () { + return { + bindAttrsAlign: {}, + // 默认会继承组件的 style 配置,如果直接继承有些样式会造成排版问题需要重置 + safeStyles: Object.assign({}, safeStyles), + // 百分比宽度时需要修正相对父级的值 + fixPercentStyleWidth: false, + // 百分比高度时需要修正相对父级的值 + fixPercentStyleHeight: false + } + }, + computed: { + /** + * @desc 为了达到编辑区渲染排版效果而创建的一些影子组件 + * @returns { Boolean } + */ + isShadowComponent () { + const shadowComMap = { + 'free-layout': true, + 'render-block': true, + 'render-grid': true, + 'h5-container': true, + 'h5-page': true, + 'render-column': true, + 'widget-form': true, + 'widget-form-item': true, + 'widget-tab': true, + 'resolve-component': true + } + return shadowComMap[this.componentData.type] || false + }, + + isHtmlElement () { + const htmlElementMap = { + 'p': true, + 'span': true, + 'i': true, + 'img': true, + 'pre': true + } + return htmlElementMap[this.componentData.type] || false + } + }, + created () { + // 优先获取组件的 material Config 缓存起来, + // 后续需要使用直接使用这个不在从 componentData.material 获取 + this.material = this.componentData.material + LC.addEventListener('update', this.updateCallback) + LC.addEventListener('rollback', this.rollbackCallback) + }, + mounted () { + this.safeStylesOfDisplay() + this.safeStyleOfWidth() + this.safeStyleOfHeight() + this.safeStyleOfLineHeight() + this.setDefaultStyleWithAttachToFreelayout() + this.updateAlign() + this.componentData.mounted(this.$refs.componentRoot) + this.$emit('component-mounted') + }, + beforeDestroy () { + LC.removeEventListener('update', this.updateCallback) + LC.removeEventListener('rollback', this.rollbackCallback) + setMousedown(false) + // 销毁时如果组件被激活,取消激活状态 + if (this.componentData.isActived) { + this.componentData.activeClear() + } + }, + methods: { + // 编辑更新 + updateCallback (event) { + if (event.target.componentId === this.componentData.componentId) { + this.safeStylesOfDisplay() + this.safeStyleOfWidth() + this.safeStyleOfHeight() + this.safeStyleOfLineHeight() + this.updateAlign() + this.$forceUpdate() + this.$emit('component-update') + } + }, + rollbackCallback () { + this.componentData.mounted(this.$refs.componentRoot) + }, + /** + * @desc 保证组件的 display 配置和渲染正确 + */ + safeStylesOfDisplay () { + if (this.isShadowComponent) { + return + } + + // 优先使用自定义配置的 display + const customDisplay = this.componentData.style.display + if (customDisplay) { + this.safeStyles = Object.assign({}, this.safeStyles, { + display: getRenderStyleDisplayValue(customDisplay) + }) + return + } + + // 兼容异步组件 bk-custom-icon + if (['bk-custom-icon'].includes(this.componentData.type)) { + this.safeStyles = Object.assign({}, this.safeStyles, { + display: 'inline-block' + }) + return + } + + this.$nextTick(() => { + // 因为异步任务执行的时机问题,此时可能组件已经被销毁 + if (!this.$refs.componentRoot) { + return + } + + // 继承组件渲染结果的 display + const $baseComponentEl = this.$refs.componentRoot.querySelector('[lesscode-base-component]') + if ($baseComponentEl) { + const { + display + } = window.getComputedStyle($baseComponentEl) + this.safeStyles = Object.assign({}, this.safeStyles, { + display: getRenderStyleDisplayValue(display) + }) + } + }) + }, + /** + * @desc 保证组件的 width 渲染正确 + * + * 某些组件可能是通过 prop 配置 width 而不是直接配置 css 的 width + */ + safeStyleOfWidth () { + if (this.isShadowComponent) { + return + } + const componentDataStyle = this.componentData.style + // 绝对定位并且同时设置了left、right + if ( + componentDataStyle.position === 'absolute' + && isNumberValue(componentDataStyle.left) + && isNumberValue(componentDataStyle.right)) { + this.safeStyles = Object.assign({}, this.safeStyles, { + width: componentDataStyle.width + }) + this.fixPercentStyleWidth = true + return + } + // 优先使用自定义配置的 width + if (_.has(componentDataStyle, 'width')) { + this.safeStyles = Object.assign({}, this.safeStyles, { + width: componentDataStyle.width + }) + this.fixPercentStyleWidth = /%$/.test(componentDataStyle.width) + return + } + + this.$nextTick(() => { + // 因为异步任务执行的时机问题,此时可能组件已经被销毁 + if (!this.$refs.componentRoot) { + return + } + const $baseComponentEl = this.$refs.componentRoot + .querySelector('[lesscode-base-component]') + if ($baseComponentEl) { + const styleWidth = $baseComponentEl.style.width + if (styleWidth) { + this.safeStyles = Object.assign({}, this.safeStyles, { + width: styleWidth + }) + } + this.fixPercentStyleWidth = /%$/.test(styleWidth) + } + }) + }, + /** + * @desc 保证组件的 height 渲染正确 + * + * 某些组件可能是通过 prop 配置 height 而不是直接配置 css 的 height + */ + safeStyleOfHeight () { + if (this.isShadowComponent) { + return + } + const componentDataStyle = this.componentData.style + // 绝对定位并且同时设置了top、bottom + if ( + componentDataStyle.position === 'absolute' + && isNumberValue(componentDataStyle.top) + && isNumberValue(componentDataStyle.bottom)) { + this.safeStyles = Object.assign({}, this.safeStyles, { + height: componentDataStyle.height + }) + this.fixPercentStyleHeight = true + return + } + // 优先使用自定义配置的 height + if (_.has(this.componentData.style, 'height')) { + this.safeStyles = Object.assign({}, this.safeStyles, { + height: componentDataStyle.height + }) + this.fixPercentStyleHeight = /%$/.test(componentDataStyle.height) + return + } + + this.$nextTick(() => { + // 因为异步任务执行的时机问题,此时可能组件已经被销毁 + if (!this.$refs.componentRoot) { + return + } + const $baseComponentEl = this.$refs.componentRoot + .querySelector('[lesscode-base-component]') + if ($baseComponentEl) { + const styleHeight = $baseComponentEl.style.height + if (styleHeight) { + this.safeStyles = Object.assign({}, this.safeStyles, { + height: styleHeight + }) + } + this.fixPercentStyleHeight = /%$/.test(styleHeight) + } + }) + }, + /** + * @desc 保证组件的 line-height 渲染正确 + * + * 渲染实际组件时会包裹一层 div 导致 line-height 与预览页面效果不一致 + */ + safeStyleOfLineHeight () { + if (this.isShadowComponent) { + return + } + const componentDataStyle = this.componentData.style + + // 优先使用自定义配置的 line-height + if (_.has(componentDataStyle, 'line-height') + && componentDataStyle['line-height'] !== '') { + this.safeStyles = Object.assign({}, this.safeStyles, { + 'line-height': componentDataStyle['line-height'] + }) + return + } + + this.$nextTick(() => { + // 因为异步任务执行的时机问题,此时可能组件已经被销毁 + if (!this.$refs.componentRoot) { + return + } + const $baseComponentEl = this.$refs.componentRoot + .querySelector('[lesscode-base-component]') + if ($baseComponentEl) { + const styleLineHeight = document.defaultView.getComputedStyle($baseComponentEl).lineHeight + if (styleLineHeight) { + this.safeStyles = Object.assign({}, this.safeStyles, { + 'line-height': styleLineHeight + }) + } + } + }) + }, + /** + * @desc 当组件在 freelayout 布局中时需要设置一些默认样式 + */ + setDefaultStyleWithAttachToFreelayout () { + if (this.componentData._isMounted + || !this.attachToFreelayout) { + return + } + this.componentData.setStyle('position', 'absolute') + let maxZIndex = 0 + this.componentData.parentNode.children.forEach(childrenNode => { + maxZIndex = Math.max(maxZIndex, ~~childrenNode.style['z-index']) + }) + this.componentData.setStyle('z-index', maxZIndex) + const defaultStyle = { + width: { + 'bk-tag-input': '200px', + 'bk-slider': '200px', + 'bk-select': '200px', + 'bk-member-selector': '400px', + 'bk-cascade': '200px', + 'bk-process': '600px', + 'bk-steps': '500px', + 'bk-divider': '500px', + 'van-slider': '50%', + 'van-cell': '100%', + 'van-datetime-picker': '100%', + 'van-picker': '100%', + 'van-nav-bar': '100%', + 'van-steps': '100%', + 'van-tabs': '100%', + 'van-skeleton': '100%', + 'van-progress': '100%', + 'van-notice-bar': '100%', + 'van-divider': '100%' + + }, + pointerEvents: { + 'bk-badge': 'none' + }, + left: { + 'van-cell': '0px', + 'van-nav-bar': '0px', + 'van-datetime-picker': '0px', + 'van-picker': '0px', + 'van-tabs': '0px', + 'van-steps': '0px', + 'van-skeleton': '0px', + 'van-progress': '0px', + 'van-notice-bar': '0px', + 'van-divider': '0px' + } + } + Object.keys(defaultStyle).forEach(styleName => { + if (defaultStyle[styleName].hasOwnProperty(this.componentData.type)) { + this.componentData.setStyle(styleName, defaultStyle[styleName][this.componentData.type]) + } + }) + }, + updateAlign () { + this.bindAttrsAlign = {} + if (this.componentData.align.horizontal) { + this.bindAttrsAlign[this.componentData.align.horizontal] = '' + } + if (this.componentData.align.vertical) { + this.bindAttrsAlign[this.componentData.align.vertical] = '' + } + }, + /** + * @desc 组件点击事件回调 + */ + handleClick (event) { + event.stopPropagation() + LC.clearMenu() + if (!this.componentData.isActived) { + this.componentData.active() + } + }, + handleDBClick (event) { + event.stopPropagation() + LC.triggerEventListener('componentDbclick', { + type: 'componentDbclick', + target: this.componentData + }) + }, + /** + * @desc 记录鼠标按下状态,抛出 component-mousedown 事件 + * @param {Object} event 事件对象 + */ + handleMousedown (event) { + event.stopPropagation() + setMousedown(true) + this.$emit('component-mousedown', event) + }, + /** + * @desc 切换鼠标按下状态 + */ + handleMouseup () { + setMousedown(false) + }, + /** + * @desc 组件 wrapper mousemove 事件回调 + * @param { Object } event + * + * 如果鼠标是按下状态不执行 hover 的逻辑 + */ + handleMousemove (event) { + // fix: 在自由布局中同样监听 mouseover 事件 + // 鼠标 hover 效果和自由布局拖动效果有点冲突 + if (isMousedown) { + return + } + event.stopImmediatePropagation() + event.stopPropagation() + event.preventDefault() + LC.triggerEventListener('componentHover', { + type: 'componentHover', + target: this.componentData + }) + }, + /** + * @desc 鼠标右键——选中组件、弹出菜单 + * @param { Object } event + */ + handleShowContextmenu (event) { + event.stopPropagation() + this.componentData.active() + LC.showMenu(event) + } + }, + render (render) { + h.init(render) + const self = this + return h({ + component: 'div', + ref: 'componentRoot', + class: { + [cssModule['component']]: true, + [cssModule['component-root']]: true, + [cssModule['precent-width']]: self.fixPercentStyleWidth, + [cssModule['precent-height']]: self.fixPercentStyleHeight, + [cssModule[framework]]: true, + 'bk-layout-custom-component-wrapper': self.componentData.isCustomComponent + }, + style: Object.assign({}, self.componentData.style, self.safeStyles), + props: { + list: self.list + }, + on: { + mousedown: self.handleMousedown, + mousemove: self.handleMousemove, + mouseup: self.handleMouseup, + click: self.handleClick, + dblclick: self.handleDBClick, + contextmenu: self.handleShowContextmenu + }, + attrs: { + role: 'component-root', + [self.componentData.componentId]: '', + ...self.bindAttrsAlign + }, + children: [ + h({ + component: RenderComponent, + ref: self.componentData.componentId, + props: { + componentData: self.componentData + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/resolve-component/resolve-component.postcss b/lib/client/src/components/render/pc/resolve-component/resolve-component.postcss new file mode 100644 index 000000000..ca00d9b82 --- /dev/null +++ b/lib/client/src/components/render/pc/resolve-component/resolve-component.postcss @@ -0,0 +1,145 @@ +.component { + position: relative; + min-height: 10px; + pointer-events: auto !important; + cursor: pointer; + &.component-root :global(.bk-loading-body) { + border: 1px solid transparent; + } + &.precent-width :global(.bk-loading-body) { + & > * { + width: 100% !important; + } + } + &.precent-height{ + & > * { + height: 100% !important; + } + } + &.vue2 { + &[align-horizontal-left], + &[align-horizontal-center], + &[align-horizontal-right], + &[align-horizontal-space-between]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + display: flex !important; + align-items: flex-start; + flex-wrap: wrap; + & > * { + flex-shrink: 0; + } + } + + } + &[align-horizontal-left]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + justify-content: flex-start; + } + } + &[align-horizontal-center]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + justify-content: center; + } + } + &[align-horizontal-right]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + justify-content: flex-end; + } + } + &[align-horizontal-space-between]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + justify-content: space-between; + } + } + &[align-vertical-top], + &[align-vertical-center], + &[align-vertical-bottom]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + display: flex !important; + flex-wrap: wrap; + & > * { + flex-shrink: 0; + } + } + + } + &[align-vertical-top]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + align-items: flex-start; + } + } + &[align-vertical-center]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + align-items: center; + } + } + &[align-vertical-bottom]{ + > div:first-child > div:first-child > div:first-child[role=draggable] { + align-items: flex-end; + } + } + } + &.vue3 { + &[align-horizontal-left], + &[align-horizontal-center], + &[align-horizontal-right], + &[align-horizontal-space-between]{ + > div:first-child > div:first-child[role=draggable] { + display: flex !important; + align-items: flex-start; + flex-wrap: wrap; + & > * { + flex-shrink: 0; + } + } + + } + &[align-horizontal-left]{ + > div:first-child > div:first-child[role=draggable] { + justify-content: flex-start; + } + } + &[align-horizontal-center]{ + > div:first-child > div:first-child[role=draggable] { + justify-content: center; + } + } + &[align-horizontal-right]{ + > div:first-child > div:first-child[role=draggable] { + justify-content: flex-end; + } + } + &[align-horizontal-space-between]{ + > div:first-child > div:first-child[role=draggable] { + justify-content: space-between; + } + } + &[align-vertical-top], + &[align-vertical-center], + &[align-vertical-bottom]{ + > div:first-child > div:first-child[role=draggable] { + display: flex !important; + flex-wrap: wrap; + & > * { + flex-shrink: 0; + } + } + + } + &[align-vertical-top]{ + > div:first-child > div:first-child[role=draggable] { + align-items: flex-start; + } + } + &[align-vertical-center]{ + > div:first-child > div:first-child[role=draggable] { + align-items: center; + } + } + &[align-vertical-bottom]{ + > div:first-child > div:first-child[role=draggable] { + align-items: flex-end; + } + } + } +} diff --git a/lib/client/src/components/render/pc/resolve-interactive-component.vue b/lib/client/src/components/render/pc/resolve-interactive-component.vue deleted file mode 100644 index c0282649d..000000000 --- a/lib/client/src/components/render/pc/resolve-interactive-component.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.js b/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.js new file mode 100644 index 000000000..60bc88a1f --- /dev/null +++ b/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.js @@ -0,0 +1,71 @@ +import cssModule from './resolve-interactive-component.postcss?module' +import { h } from 'bk-lesscode-render' +import LC from '@/element-materials/core' +import ResolveComponent from '../resolve-component/resolve-component' + +export default { + name: 'resolve-interactive-component', + provide () { + return { + attachToInteractiveComponent: true + } + }, + props: { + componentData: { + type: Object, + required: true + } + }, + created () { + LC.addEventListener('update', this.updateCallback) + LC.addEventListener('toggleInteractive', this.updateCallback) + + // 新拖入的交互式组件默认要显示出来 + if (!this.componentData._isMounted) { + setTimeout(() => { + this.componentData.toggleInteractive(true) + }) + } + }, + mounted () { + if (!this.componentData._isMounted) { + this.componentData.active() + } + }, + beforeDestroy () { + LC.removeEventListener('update', this.updateCallback) + LC.removeEventListener('toggleInteractive', this.updateCallback) + }, + methods: { + updateCallback (event) { + if (event.target.componentId === this.componentData.componentId) { + this.$forceUpdate() + } + } + }, + render (render) { + h.init(render) + const self = this + + return h({ + component: 'div', + class: { + [cssModule['interactive']]: true, + [cssModule['hidden']]: !self.componentData.interactiveShow + }, + attrs: { + role: 'interactive-root', + 'data-render-drag': 'disabled' + }, + children: [ + h({ + component: ResolveComponent, + key: self.componentData.renderKey, + props: { + componentData: self.componentData + } + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.postcss b/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.postcss new file mode 100644 index 000000000..69bca1d52 --- /dev/null +++ b/lib/client/src/components/render/pc/resolve-interactive-component/resolve-interactive-component.postcss @@ -0,0 +1,20 @@ +.interactive { + position: fixed; + z-index: 99999999; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: auto !important; + :global(.lesscode-bk-sideslider-wrapper) { + right: 0; + } + > div { + width: auto !important; + height: auto !important; + } +} + +.hidden { + display: none; +} diff --git a/lib/client/src/components/render/pc/tools/hooks/use-component-active.js b/lib/client/src/components/render/pc/tools/hooks/use-component-active.js index 0c0fd03e6..a7243340c 100644 --- a/lib/client/src/components/render/pc/tools/hooks/use-component-active.js +++ b/lib/client/src/components/render/pc/tools/hooks/use-component-active.js @@ -2,7 +2,7 @@ import { shallowRef, onMounted, onBeforeUnmount -} from '@vue/composition-api' +} from 'bk-lesscode-render' import _ from 'lodash' import LC from '@/element-materials/core' diff --git a/lib/client/src/components/render/pc/tools/hooks/use-component-hover.js b/lib/client/src/components/render/pc/tools/hooks/use-component-hover.js index 7c8f83e63..ac40053d1 100644 --- a/lib/client/src/components/render/pc/tools/hooks/use-component-hover.js +++ b/lib/client/src/components/render/pc/tools/hooks/use-component-hover.js @@ -2,7 +2,7 @@ import { shallowRef, onMounted, onBeforeUnmount -} from '@vue/composition-api' +} from 'bk-lesscode-render' import _ from 'lodash' import LC from '@/element-materials/core' @@ -64,12 +64,12 @@ export default function (callbak) { LC.addEventListener('reset', resetCallback) LC.addEventListener('componentDragStart', componentDragStartCallbak) LC.addEventListener('componentDragEnd', componentDragEndCallbak) - + onMounted(() => { $drawTarget = document.body.querySelector('#drawTarget') activeResizeObserver.observe($drawTarget) }) - + onBeforeUnmount(() => { activeResizeObserver.unobserve($drawTarget) LC.removeEventListener('componentHover', componentHoverCallback) diff --git a/lib/client/src/components/render/pc/tools/hooks/use-show-menu.js b/lib/client/src/components/render/pc/tools/hooks/use-show-menu.js index 75fec89d6..ccc73ad67 100644 --- a/lib/client/src/components/render/pc/tools/hooks/use-show-menu.js +++ b/lib/client/src/components/render/pc/tools/hooks/use-show-menu.js @@ -2,6 +2,7 @@ import LC from '@/element-materials/core' export default function () { return (event) => { + event.stopPropagation() LC.showMenu(event) } } diff --git a/lib/client/src/components/render/pc/tools/lesscode-focus/index.js b/lib/client/src/components/render/pc/tools/lesscode-focus/index.js new file mode 100644 index 000000000..2b7382350 --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-focus/index.js @@ -0,0 +1,203 @@ +import { + reactive, + toRefs, + h +} from 'bk-lesscode-render' +import useComponentActive from '../hooks/use-component-active' +import useComponentHover from '../hooks/use-component-hover' + +const baseStyles = { + position: 'absolute', + zIndex: 100000001, + borderWidth: '0px', + borderColor: '#3a84ff', + pointer: 'none' +} + +const hideStyles = { + display: 'none' +} + +export default { + setup () { + const state = reactive({ + hoverTopStyles: {}, + hoverRightStyles: {}, + hoverBottomStyles: {}, + hoverLeftStyles: {}, + activeTopStyles: {}, + activeRightStyles: {}, + activeBottomStyles: {}, + activeLeftStyles: {}, + resizeStyles: {} + }) + + /** + * @desc 鼠标hover状态 + * @param { Node } componentData + */ + const showHover = (componentData = {}) => { + if (!componentData.componentId + || componentData === activeComponentData.componentId) { + state.hoverTopStyles = hideStyles + state.hoverRightStyles = hideStyles + state.hoverBottomStyles = hideStyles + state.hoverLeftStyles = hideStyles + return + } + const { + top: containerTop, + left: containerLeft + } = document.body.querySelector('#drawTarget').getBoundingClientRect() + + const { + top, + left, + width, + height + } = componentData.$elm.getBoundingClientRect() + + const hoverBaseStyle = Object.assign({}, baseStyles, { + borderStyle: 'dashed' + }) + + state.hoverTopStyles = Object.assign({}, hoverBaseStyle, { + top: `${top - containerTop}px`, + left: `${left - containerLeft}px`, + width: `${width}px`, + borderBottomWidth: '1px' + }) + state.hoverRightStyles = Object.assign({}, hoverBaseStyle, { + top: `${top - containerTop}px`, + left: `${left + width - 1 - containerLeft}px`, + height: `${height}px`, + borderLeftWidth: '1px', + borderColor: '#3a84ff' + }) + state.hoverBottomStyles = Object.assign({}, hoverBaseStyle, { + top: `${top + height - 1 - containerTop}px`, + left: `${left - containerLeft}px`, + width: `${width}px`, + borderTopWidth: '1px', + borderColor: '#3a84ff' + }) + state.hoverLeftStyles = Object.assign({}, hoverBaseStyle, { + top: `${top - containerTop}px`, + left: `${left - containerLeft}px`, + height: `${height}px`, + borderRightWidth: '1px', + borderColor: '#3a84ff' + }) + } + /** + * @desc 选中状态 + * @param { Node } componentData + */ + const showActive = (componentData = {}) => { + if (!componentData.componentId) { + state.activeTopStyles = hideStyles + state.activeRightStyles = hideStyles + state.activeBottomStyles = hideStyles + state.activeLeftStyles = hideStyles + return + } + if (componentData.componentId === hoverComponentData.value.componentId) { + state.hoverTopStyles = hideStyles + state.hoverRightStyles = hideStyles + state.hoverBottomStyles = hideStyles + state.hoverLeftStyles = hideStyles + } + const { + top: containerTop, + left: containerLeft + } = document.body.querySelector('#drawTarget').getBoundingClientRect() + const { + top, + left, + width, + height + } = componentData.$elm.getBoundingClientRect() + + const activeBaseStyle = Object.assign({}, baseStyles, { + borderStyle: 'solid' + }) + + state.activeTopStyles = Object.assign({}, activeBaseStyle, { + top: `${top - containerTop}px`, + left: `${left - containerLeft}px`, + width: `${width}px`, + borderBottomWidth: '1px' + }) + state.activeRightStyles = Object.assign({}, activeBaseStyle, { + top: `${top - containerTop}px`, + left: `${left + width - 1 - containerLeft}px`, + height: `${height}px`, + borderLeftWidth: '1px', + borderColor: '#3a84ff' + }) + state.activeBottomStyles = Object.assign({}, activeBaseStyle, { + top: `${top + height - 1 - containerTop}px`, + left: `${left - containerLeft}px`, + width: `${width}px`, + borderTopWidth: '1px', + borderColor: '#3a84ff' + }) + state.activeLeftStyles = Object.assign({}, activeBaseStyle, { + top: `${top - containerTop}px`, + left: `${left - containerLeft}px`, + height: `${height}px`, + borderRightWidth: '1px', + borderColor: '#3a84ff' + }) + } + + const { hoverComponentData } = useComponentHover(showHover) + const { activeComponentData } = useComponentActive(showActive) + + return { + ...toRefs(state) + } + }, + render (render) { + h.init(render) + + const renderStyles = () => { + return [ + { + role: 'hover', + styles: [ + this.hoverTopStyles, + this.hoverRightStyles, + this.hoverBottomStyles, + this.hoverLeftStyles + ] + }, + { + role: 'active', + styles: [ + this.activeTopStyles, + this.activeRightStyles, + this.activeBottomStyles, + this.activeLeftStyles + ] + } + ].map((item) => { + return h({ + component: 'div', + attrs: { + role: item.role + }, + children: item.styles.map(style => h({ + component: 'div', + style + })) + }) + }) + } + + return h({ + component: 'div', + children: renderStyles() + }) + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-focus/index.vue b/lib/client/src/components/render/pc/tools/lesscode-focus/index.vue deleted file mode 100644 index fab8ba21e..000000000 --- a/lib/client/src/components/render/pc/tools/lesscode-focus/index.vue +++ /dev/null @@ -1,178 +0,0 @@ - - diff --git a/lib/client/src/components/render/pc/tools/lesscode-loading/index.js b/lib/client/src/components/render/pc/tools/lesscode-loading/index.js new file mode 100644 index 000000000..96b1d08cf --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-loading/index.js @@ -0,0 +1,295 @@ +import './index.postcss' +import { + h, + ref, + onBeforeMount, + onBeforeUnmount +} from 'bk-lesscode-render' +import LC from '@/element-materials/core' +import { + addEventListener, + removeEventListener +} from '@/common/watcher' +import { + getVariableValue +} from 'shared/variable' +import { + evalWithSandBox +} from 'shared/function' +import store from '@/store' +import http from '@/api/pureAxios' +import _ from 'lodash' +import useDatasource from '@/hooks/use-datasource' + +export default { + props: { + componentData: Object + }, + setup (props) { + const { + getTableDatas + } = useDatasource() + + const isLoading = ref(false) + const watchMap = {} + + const getResourceData = async (type, code, payload, staticValue) => { + if (staticValue !== undefined) return staticValue + + const variableList = store.getters['variable/variableList'] + const functionList = store.getters['functions/functionList'] + const apiList = store.getters['api/apiList'] + let data + switch (type) { + case 'variable': + data = getVariableValue( + variableList.find(variable => variable.variableCode === code) + ) + break + case 'function': + data = await evalWithSandBox( + code, + payload, + functionList, + variableList, + apiList, + { + $store: store, + $http: http + } + ) + break + case 'datasource': + ({ list: data } = await getTableDatas(payload.tableName, payload.bkDataSourceType)) + break + } + return data + } + + const triggerUpdate = (update) => { + return () => { + isLoading.value = true + update().finally(() => { + LC.triggerEventListener('update', { + target: props.componentData, + type: 'resourceChange' + }) + isLoading.value = false + }) + } + } + + // 监听变量的变化 + const watchVariable = (data, key, staticValue) => { + const { + code, + format + } = data + const watchKey = `watchVariable-${key}` + const watchedCode = watchMap[watchKey] + const watchCode = code + const update = triggerUpdate(async () => { + data.renderValue = await getResourceData('variable', watchCode, null, staticValue) + }) + if (watchedCode && (format !== 'variable' || watchedCode !== watchCode)) { + delete watchMap[watchKey] + removeEventListener(watchedCode, update) + } + if ((!watchedCode || watchedCode !== watchCode) && format === 'variable' && code) { + watchMap[watchKey] = watchCode + addEventListener(watchCode, update, { immediate: true }) + } + } + + // 监听远程函数变化 + const watchRemote = (data, key, staticValue) => { + // 兼容 methodCode 平铺的老数据 + let methodData = data?.payload?.methodData + if (data?.payload?.methodCode) { + methodData = { + methodCode: data?.payload?.methodCode, + params: data?.payload?.params + } + } + // 远程函数由函数加变量组成 + const watchKey = `watchRemote-${key}` + const watchCode = methodData?.params?.reduce?.((acc, cur) => { + if (cur.format === 'variable' && cur.code) { + acc += `&${cur.code}` + } + return acc + }, `${methodData?.methodCode}`) + const watchedCode = watchMap[watchKey] + const update = triggerUpdate(async () => { + const val = await getResourceData('function', methodData.methodCode, methodData.params, staticValue) + if (typeof val === typeof data.renderValue || typeof val === typeof data.code) { + data.renderValue = val + data.code = val + } + }) + // 更新event + if (watchedCode && (!['select-remote', 'remote'].includes(data.valueType) || watchedCode !== watchCode)) { + delete watchMap[watchKey] + watchedCode.split('&').forEach((code) => { + removeEventListener(code, update) + }) + } + if ((!watchedCode || watchedCode !== watchCode) && ['select-remote', 'remote'].includes(data.valueType) && watchCode) { + watchMap[watchKey] = watchCode + watchCode.split('&').forEach((code) => { + addEventListener(code, update, { immediate: true }) + }) + } + } + + // 更新数据源数据 + const updateDataSource = (data, key) => { + const isDataSourceType = [ + 'data-source', + 'select-data-source', + 'table-data-source' + ].includes(data.valueType) + const tableName = data?.payload?.sourceData?.tableName + const dataSourceType = data?.payload?.sourceData?.dataSourceType + const watchKey = `updateDataSource-${key}` + const watchCode = `datasource-${tableName}` + const watchedCode = watchMap[watchKey] + if (watchedCode && (!isDataSourceType || watchedCode !== watchCode)) { + delete watchMap[watchKey] + } + if (isDataSourceType && tableName && !watchMap[watchKey]) { + watchMap[watchKey] = watchCode + triggerUpdate(() => { + return getResourceData( + 'datasource', + tableName, + { + tableName, + bkDataSourceType: dataSourceType + } + ).then((val) => { + if (!_.isEqual(data.renderValue, val)) { + data.renderValue = val + } + }) + })() + } + } + + // 监听 payload 中使用到的变量 + const watchPayload = (data, key) => { + const payload = data.payload || {} + // 检查 table pagination + if (key === 'prop-pagination' && data.format === 'value' && ['local', 'remote'].includes(payload.type)) { + const paginationVal = payload.val || {} + Object + .keys(paginationVal) + .forEach((key) => { + const { + format, + code + } = paginationVal[key] + const watchKey = `watchPayload-${key}` + const watchedCode = watchMap[watchKey] + const watchCode = code + const update = triggerUpdate(async () => { + paginationVal[key].val = await getResourceData('variable', watchCode) + }) + if (watchedCode && (format !== 'variable' || watchedCode !== watchCode)) { + delete watchMap[watchKey] + removeEventListener(watchedCode, update) + } + if ((!watchedCode || watchedCode !== watchCode) && format === 'variable' && watchCode) { + watchMap[watchKey] = watchCode + addEventListener(watchCode, update, { immediate: true }) + } + }) + } + } + + const watchResource = () => { + const node = props.componentData + const propMaterial = node.material?.props + + Object.keys(node.renderProps).forEach(propKey => { + const material = propMaterial[propKey] + const prop = node.renderProps[propKey] + const key = `prop-${propKey}` + watchVariable(prop, key, material?.staticValue) + watchRemote(prop, key, material?.staticValue) + watchPayload(prop, key) + updateDataSource(prop, key) + }) + + Object.keys(node.renderSlots).forEach(slotKey => { + const slot = node.renderSlots[slotKey] + const key = `slot-${slotKey}` + if (!LC.isNode(slot)) { + watchVariable(slot, key) + watchRemote(slot, key) + updateDataSource(slot, key) + } + }) + } + + const handleUpdate = (event) => { + if (event.target.componentId === props.componentData.componentId) { + watchResource() + } + } + + onBeforeMount(() => { + watchResource() + LC.addEventListener('update', handleUpdate) + }) + + onBeforeUnmount(() => { + LC.removeEventListener('update', handleUpdate) + }) + + return { + isLoading + } + }, + render (render) { + h.init(render) + + // loading 组件样式 + const style = Object.assign( + {}, + this.componentData.style, + this.$parent.baseComponentStyleReset, + { + position: 'relative', + height: '100%', + width: '100%' + } + ) + + // 交互式组件需要父级fixed + if (this.componentData.isInteractiveComponent) { + Object.assign(style, { + position: 'fixed', + background: 'rgba(0,0,0,0.5)', + zIndex: 9999, + top: 0, + right: 0, + bottom: 0, + left: 0, + pointerEvents: 'auto !important' + }) + } + + return h({ + component: 'bk-loading', + key: this.isLoading, + style, + props: { + extCls: 'component-loading', + loading: this.isLoading, + isLoading: this.isLoading + }, + children: this.$slots.default + }) + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-loading/index.postcss b/lib/client/src/components/render/pc/tools/lesscode-loading/index.postcss new file mode 100644 index 000000000..2d85b25eb --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-loading/index.postcss @@ -0,0 +1,13 @@ +.component-loading { + background-color: transparent; + .bk-loading-wrapper { + z-index: 101; + position: absolute; + left: 0; + } + .bk-loading-body { + display: inherit; + height: 100%; + width: 100%; + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-margin/hooks/use-margin.js b/lib/client/src/components/render/pc/tools/lesscode-margin/hooks/use-margin.js index e1f26a5a5..62fbd7d96 100644 --- a/lib/client/src/components/render/pc/tools/lesscode-margin/hooks/use-margin.js +++ b/lib/client/src/components/render/pc/tools/lesscode-margin/hooks/use-margin.js @@ -4,7 +4,7 @@ import { getCurrentInstance, onMounted, onBeforeUnmount -} from '@vue/composition-api' +} from 'bk-lesscode-render' import _ from 'lodash' import DragLine from '../../../../common/drag-line' import { autoStyle, autoPxTransform } from '@/common/util.js' diff --git a/lib/client/src/components/render/pc/tools/lesscode-margin/index.js b/lib/client/src/components/render/pc/tools/lesscode-margin/index.js new file mode 100644 index 000000000..ef3609fe5 --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-margin/index.js @@ -0,0 +1,140 @@ +import cssModule from './index.postcss?module' +import { + reactive, + toRefs, + h +} from 'bk-lesscode-render' +import useComponentActive from '../hooks/use-component-active' +import useMargin from './hooks/use-margin' +import { isFreeLayoutProperty } from '@/element-materials/core/helper/utils.js' + +const baseStyles = { + position: 'absolute', + zIndex: 100000002 +} + +const hideStyles = { + display: 'none' +} + +const halfBtnSize = 8 + +export default { + setup () { + const state = reactive({ + btnTopStyles: hideStyles, + btnLeftStyles: hideStyles + }) + + const { activeComponentData } = useComponentActive((componentData = {}) => { + state.btnTopStyles = hideStyles + state.btnLeftStyles = hideStyles + if (!componentData.componentId + || isFreeLayoutProperty(componentData.parentNode.type)) { + return + } + const { + top: containerTop, + left: containerLeft + } = document.body.querySelector('#drawTarget').getBoundingClientRect() + const { + top, + left, + width, + height + } = componentData.$elm.getBoundingClientRect() + + const btnBaseStyle = Object.assign({}, baseStyles) + + state.btnTopStyles = Object.assign({}, btnBaseStyle, { + top: `${top - containerTop - halfBtnSize}px`, + left: `${left - containerLeft + width / 2 - halfBtnSize}px`, + cursor: 'ns-resize' + }) + state.btnLeftStyles = Object.assign({}, btnBaseStyle, { + top: `${top - containerTop + height / 2 - halfBtnSize}px`, + left: `${left - containerLeft - halfBtnSize}px`, + cursor: 'ew-resize' + }) + }) + + const { + margin, + tipTopStyles, + tipLeftStyles, + handleMarginTop, + handleMarginLeft + } = useMargin() + + return { + ...toRefs(state), + margin, + activeComponentData, + tipTopStyles, + tipLeftStyles, + handleMarginTop, + handleMarginLeft + } + }, + + render (render) { + h.init(render) + + const self = this + + return h({ + component: 'div', + on: { + click (event) { + event.stopPropagation() + } + }, + children: [ + h({ + component: 'div', + class: cssModule['achor-top'], + style: self.btnTopStyles, + on: { + mousedown: self.handleMarginTop + }, + children: [ + h({ + component: 'i', + class: 'bk-drag-icon bk-drag-zuoyouchengkai' + }), + h({ + component: 'div', + class: cssModule['tip-top'], + style: self.tipTopStyles, + children: [ + self.margin.top + ] + }) + ] + }), + h({ + component: 'div', + class: cssModule['achor-left'], + style: self.btnLeftStyles, + on: { + mousedown: self.handleMarginLeft + }, + children: [ + h({ + component: 'i', + class: 'bk-drag-icon bk-drag-zuoyouchengkai' + }), + h({ + component: 'div', + class: cssModule['tip-left'], + style: self.tipLeftStyles, + children: [ + self.margin.left + ] + }) + ] + }) + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-margin/index.postcss b/lib/client/src/components/render/pc/tools/lesscode-margin/index.postcss new file mode 100644 index 000000000..1386a20f1 --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-margin/index.postcss @@ -0,0 +1,33 @@ +.achor-top, +.achor-left{ + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 15px; + width: 15px; + cursor: pointer; +} +.achor-top{ + :global(.bk-drag-icon){ + transform: rotateZ(90deg); + } +} +.tip-top, +.tip-left{ + position: absolute; + display: flex; + justify-content: center; + align-items: center; + color: #ff9700; +} +.tip-top{ + left: 7px; + bottom: 7px; + border-left: 1px solid #ff9700; +} +.tip-left{ + top: -10px; + right: 7px; + border-bottom: 1px solid #ff9700; +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-margin/index.vue b/lib/client/src/components/render/pc/tools/lesscode-margin/index.vue deleted file mode 100644 index bcf6a0db8..000000000 --- a/lib/client/src/components/render/pc/tools/lesscode-margin/index.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - diff --git a/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-full-width.js b/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-full-width.js index d0ba40bfe..6462ee92a 100644 --- a/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-full-width.js +++ b/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-full-width.js @@ -8,7 +8,7 @@ export default function () { activeNode.setStyle({ right: '0', left: '0', - width: '' + width: '100%' }) } else { activeNode.setStyle({ @@ -17,5 +17,7 @@ export default function () { marginRight: 0 }) } + // vue3 样式变更,没有重新渲染,通过 key 来控制 + activeNode.rerender() } } diff --git a/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-resize.js b/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-resize.js index dbc50caba..facfe1272 100644 --- a/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-resize.js +++ b/lib/client/src/components/render/pc/tools/lesscode-resize/hooks/use-resize.js @@ -6,10 +6,10 @@ import { onMounted, onBeforeUnmount, nextTick -} from '@vue/composition-api' +} from 'bk-lesscode-render' import _ from 'lodash' import DragLine from '../../../../common/drag-line' -import { autoStyle } from '@/common/util.js' +import { autoStyle, setCompProp } from '@/common/util.js' // const halfDotSize = 8 @@ -43,7 +43,8 @@ export default function () { const { clientX } = event if (isWidthResizeable) { newWidth = Math.max(parseInt(clientX - startScreenX + moveStartWidth, 10), 0) - autoStyle(proxy.activeComponentData, 'width', newWidth) + // autoStyle(proxy.activeComponentData, 'width', newWidth) + setCompSize(proxy.activeComponentData, 'width', newWidth) // 指定了width,right不生效 if (componentDataParentNode.value.type === 'free-layout') { proxy.activeComponentData.setStyle('right', '') @@ -52,7 +53,8 @@ export default function () { const { clientY } = event if (isHeightResizeable) { newHeight = Math.max(parseInt(clientY - startScreenY + moveStartHeight, 10), 0) - autoStyle(proxy.activeComponentData, 'height', newHeight) + // autoStyle(proxy.activeComponentData, 'height', newHeight) + setCompSize(proxy.activeComponentData, 'height', newHeight) // 指定了height, bottom不生效 if (componentDataParentNode.value.type === 'free-layout') { proxy.activeComponentData.setStyle('bottom', '') @@ -106,6 +108,17 @@ export default function () { }) } + const setCompSize = (componentData, key, val) => { + if (componentData?.type === 'chart' || componentData?.type === 'bk-charts') { + if (key === 'width' && val) { + val = val + 'px' + } + setCompProp(componentData, key, val) + } else { + autoStyle(componentData, key, val) + } + } + /** * @desc 设置宽度 * @param { Event } event diff --git a/lib/client/src/components/render/pc/tools/lesscode-resize/index.js b/lib/client/src/components/render/pc/tools/lesscode-resize/index.js new file mode 100644 index 000000000..b1bf733de --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-resize/index.js @@ -0,0 +1,304 @@ +import cssModule from './index.postcss?module' +import { + ref, + reactive, + toRefs, + h, + toolTips +} from 'bk-lesscode-render' +import useComponentActive from '../hooks/use-component-active' +import useShowMenu from '../hooks/use-show-menu' +import useResize from './hooks/use-resize' +import useAutoHeight from './hooks/use-auto-height' +import useFullWidth from './hooks/use-full-width' +import useAutoWidth from './hooks/use-auto-width' +import { isFreeLayoutProperty } from '@/element-materials/core/helper/utils.js' + +const baseStyles = { + position: 'absolute', + zIndex: 100000002 +} + +const hideStyles = { + display: 'none' +} + +const halfDotSize = 8 +const actionBtnSize = 24 +const actionBtnOffset = 8 + +export default { + setup () { + const state = reactive({ + dotWidthStyles: hideStyles, + dotHeightStyles: hideStyles, + dotBothStyles: hideStyles, + fullWidthStyles: hideStyles, + autoHeightStyles: hideStyles + }) + + const resizeRef = ref() + const tipRef = ref() + + /** + * @desc 选中状态 + * @param { Node } componentData + */ + const showActive = (componentData = {}) => { + state.dotWidthStyles = hideStyles + state.dotHeightStyles = hideStyles + state.dotBothStyles = hideStyles + state.fullWidthStyles = hideStyles + state.autoHeightStyles = hideStyles + if (!componentData.componentId) { + return + } + + // 解析styles配置,是否支持width、height配置 + const styleConfig = componentData.material.styles || [] + // 如果是图表类型,也需要支持拖拽大小 + const isChartType = componentData?.type === 'chart' || componentData?.type === 'bk-charts' + let resizeWidthEnabel = false + let resizeHeightEnable = false + if (isChartType) { + resizeWidthEnabel = true + resizeHeightEnable = true + } else { + styleConfig.forEach(styleItem => { + if (styleItem === 'size') { + resizeWidthEnabel = true + resizeHeightEnable = true + return + } + if (styleItem.name === 'size') { + if (styleItem.include) { + resizeWidthEnabel = styleItem.include.includes('width') + resizeHeightEnable = styleItem.include.includes('height') + } + if (styleItem.exclude) { + resizeWidthEnabel = !styleItem.exclude.includes('width') + resizeHeightEnable = !styleItem.exclude.includes('height') + } + } + }) + } + + const { + top: containerTop, + left: containerLeft, + right: containerRight + } = document.body.querySelector('#drawTarget').getBoundingClientRect() + const { + top, + left, + right, + width, + height + } = componentData.$elm.getBoundingClientRect() + + const dotBaseStyle = Object.assign({}, baseStyles) + + if (resizeWidthEnabel) { + state.dotWidthStyles = Object.assign({}, dotBaseStyle, { + top: `${top - containerTop + height / 2 - halfDotSize}px`, + left: `${right - halfDotSize - containerLeft}px`, + cursor: 'col-resize' + }) + // 图表类型不需要显示 + if (!isChartType) { + // 100% 宽度按钮的位置 + let fullWidthBtnLeft = right - containerLeft + actionBtnOffset + if (right + 20 >= containerRight) { + fullWidthBtnLeft = right - containerLeft - (actionBtnSize + actionBtnOffset) + } + state.fullWidthStyles = Object.assign({}, dotBaseStyle, { + top: `${top - containerTop + height / 2 - actionBtnSize / 2}px`, + left: `${fullWidthBtnLeft}px` + }) + } + } + if (resizeHeightEnable) { + state.dotHeightStyles = Object.assign({}, dotBaseStyle, { + top: `${top + height - halfDotSize - containerTop}px`, + left: `${left - containerLeft + width / 2 - halfDotSize}px`, + cursor: 'row-resize' + }) + // 高度自适应的按钮 + // free-layout 不支持该功能,必须给定 height、 图表类型也不需要 + if (!isFreeLayoutProperty(componentData.type) && !isChartType) { + state.autoHeightStyles = Object.assign({}, dotBaseStyle, { + top: `${top + height - containerTop + actionBtnOffset}px`, + left: `${left - containerLeft + width / 2 - actionBtnSize / 2}px` + }) + } + } + + if (resizeWidthEnabel && resizeHeightEnable) { + state.dotBothStyles = Object.assign({}, dotBaseStyle, { + top: `${top - containerTop + height - halfDotSize}px`, + left: `${left - containerLeft + width - halfDotSize}px`, + cursor: 'nwse-resize' + }) + } + } + + const { activeComponentData } = useComponentActive(showActive) + + const { + size, + tipStyles, + handleResizeWidth, + handleResizeHeight, + handleResizeBoth + } = useResize() + + // 显示快捷面板 + const handleShowMenu = useShowMenu() + + const handleAutoHeight = useAutoHeight() + + const handleFullWidth = useFullWidth() + + const handleAutoWidth = useAutoWidth() + + return { + ...toRefs(state), + size, + tipStyles, + activeComponentData, + resizeRef, + tipRef, + handleResizeWidth, + handleResizeHeight, + handleResizeBoth, + handleShowMenu, + handleAutoHeight, + handleFullWidth, + handleAutoWidth + } + }, + render (render) { + h.init(render) + + const self = this + + const renderAchors = () => { + return [ + { style: self.dotWidthStyles, handler: self.handleResizeWidth }, + { style: self.dotHeightStyles, handler: self.handleResizeHeight }, + { style: self.dotBothStyles, handler: self.handleResizeBoth } + ].map(item => { + return h({ + component: 'div', + class: cssModule['achor'], + style: item.style, + on: { + mousedown: item.handler + } + }) + }) + } + + const renedrTipRef = () => { + return h({ + component: 'div', + ref: 'tipRef', + style: self.tipStyles, + children: [ + `${self.size.width} x ${self.size.height}` + ] + }) + } + + const renderModifyWidth = () => { + return h({ + component: 'div', + class: cssModule['width-actions'], + style: self.fullWidthStyles, + children: [ + h({ + component: 'div', + class: [cssModule['btn'], cssModule['auto-width']], + directives: [{ + name: toolTips, + value: { + content: '宽度随内容自适应', + placement: 'right' + } + }], + on: { + click: self.handleAutoWidth + }, + children: [ + h({ + component: 'i', + class: 'bk-drag-icon bk-drag-xiangxiazishiying' + }) + ] + }), + h({ + component: 'div', + class: cssModule['btn'], + directives: [{ + name: toolTips, + value: { + content: '宽度撑满', + placement: 'right' + } + }], + on: { + click: self.handleFullWidth + }, + children: [ + h({ + component: 'i', + class: 'bk-drag-icon bk-drag-zuoyouchengkai' + }) + ] + }) + ] + }) + } + + const renderModifyHeight = () => { + return h({ + component: 'div', + class: cssModule['btn'], + style: self.autoHeightStyles, + directives: [{ + name: toolTips, + value: { + content: '高度随内容自适应', + placement: 'bottom' + } + }], + on: { + click: self.handleAutoHeight + }, + children: [ + h({ + component: 'i', + class: 'bk-drag-icon bk-drag-xiangxiazishiying' + }) + ] + }) + } + + return h({ + component: 'div', + ref: 'resizeRef', + on: { + click (event) { + event.stopPropagation() + }, + contextmenu: self.handleShowMenu + }, + children: [ + ...renderAchors(), + renedrTipRef(), + renderModifyWidth(), + renderModifyHeight() + ] + }) + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-resize/index.postcss b/lib/client/src/components/render/pc/tools/lesscode-resize/index.postcss new file mode 100644 index 000000000..fb2a3c834 --- /dev/null +++ b/lib/client/src/components/render/pc/tools/lesscode-resize/index.postcss @@ -0,0 +1,62 @@ +.achor{ + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 15px; + width: 15px; + + &:after{ + content: ''; + position: absolute; + width: 5px; + height: 5px; + border-radius: 50%; + border: 1px solid #3a84ff; + background: #fff; + } +} +.width-actions{ + position: relative; + &:hover{ + .auto-width{ + transform: translateY(calc(-100% - 6px)); + opacity: 1; + &:after{ + content: ''; + position: absolute; + height: 10px; + right: 0; + bottom: -10px; + left: 0; + } + } + } +} +.btn{ + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + font-size: 10px; + color: #979BA5; + border-radius: 50%; + border: 1px solid #DCDEE5; + background: #fff; + cursor: pointer; + &:hover{ + color: #63656E; + } +} +.auto-width{ + position: absolute; + top: 0; + left: 0; + opacity: 0; + z-index: -1; + transition: all .15s; + :global(.bk-drag-icon){ + transform: rotateZ(-90deg); + } +} diff --git a/lib/client/src/components/render/pc/tools/lesscode-resize/index.vue b/lib/client/src/components/render/pc/tools/lesscode-resize/index.vue deleted file mode 100644 index ff2c9fd07..000000000 --- a/lib/client/src/components/render/pc/tools/lesscode-resize/index.vue +++ /dev/null @@ -1,280 +0,0 @@ - - - diff --git a/lib/client/src/components/render/pc/tools/lesscode-tool/hooks/use-slot.js b/lib/client/src/components/render/pc/tools/lesscode-tool/hooks/use-slot.js index 26f126374..c98fe6403 100644 --- a/lib/client/src/components/render/pc/tools/lesscode-tool/hooks/use-slot.js +++ b/lib/client/src/components/render/pc/tools/lesscode-tool/hooks/use-slot.js @@ -3,7 +3,7 @@ import { ref, getCurrentInstance, onBeforeUnmount -} from '@vue/composition-api' +} from 'bk-lesscode-render' import _ from 'lodash' import LC from '@/element-materials/core' import { messageWarn } from '@/common/bkmagic' @@ -60,7 +60,7 @@ const getSlotInhertStyles = (text, $el) => { }) return } - + elNode.children.forEach(childrenNode => search(childrenNode)) } search($el) @@ -95,21 +95,21 @@ export default function () { && slotConfig.default && slotConfig.default.type) { state.show = slotConfig.default.type.includes('text') || slotConfig.default.type.includes('textarea') - state.tips = this.$t('编辑文字') + state.tips = proxy.$t('编辑文字') // slot default format 配置成 variable 或者 expression 不支持编辑 const slotDefault = proxy.activeComponentData.slot.default state.disabled = ['variable', 'expression'].includes(slotDefault.format) if (slotDefault.format === 'variable') { - state.tips = this.$t('文字来源于变量不支持编辑') + state.tips = proxy.$t('文字来源于变量不支持编辑') } else if (slotDefault.format === 'expression') { - state.tips = this.$t('文字来源于表达式不支持编辑') + state.tips = proxy.$t('文字来源于表达式不支持编辑') } } - + return state } - + const handleEdit = () => { const disabledEditComponents = ['divider'] const activeComponent = LC.getActiveNode() @@ -146,7 +146,7 @@ export default function () { width, height } = proxy.activeComponentData.$elm.getBoundingClientRect() - + Object.assign(wrapperStyles, baseStyles, { display: 'block', top: `${top - containerTop}px`, @@ -163,7 +163,7 @@ export default function () { borderRadius: '2px' }, getSlotInhertStyles(vm.value, proxy.activeComponentData.$elm)) - + vm && vm.$forceUpdate() }) } @@ -171,7 +171,7 @@ export default function () { const isTextarea = slotConfig.default.type.includes('textarea') calcPosition() - + vm = new Vue({ data () { return { @@ -217,7 +217,7 @@ export default function () { if (vm.$el.parentNode === $container) { vm.$el.parentNode.removeChild(vm.$el) } - + vm.$destroy() } }, @@ -226,6 +226,7 @@ export default function () { if (isTextarea) { return ( + {{$t('编辑区')}} +
+ +
- + {{$t('预览区')}} +
+ +
@@ -127,6 +133,13 @@ this.initJsonStr = circleJSON(defaultValue, null, 4) }, immediate: true + }, + isShow (val) { + if (val) { + setTimeout(() => { + this.$refs.jsonInput.focus() + }, 500) + } } }, methods: { @@ -193,9 +206,6 @@ .json-setting-dialog { /deep/ .bk-dialog { position: initial; - /* &.ease-enter-active.ease-enter-to { - animation: none!important; - } */ .bk-dialog-content { top: calc(50vh - 324px)!important; } @@ -209,16 +219,20 @@ display:flex; overflow: hidden; margin: 0 auto; - border: solid 1px #E5EBEE; - > div { - border-right: solid 1px #E5EBEE; + .area-container { + height: 430px; + border: 1px solid #E5EBEE; + } + .area-scroll { overflow: auto; @mixin scroller; } + .area-name { + font-size: 12px; + } .init-json { flex: 1; overflow: hidden; - margin-top: 10px; .json-input { border: 0px; width: 100%; diff --git a/lib/client/src/element-materials/modifier/component/props/components/strategy/number.vue b/lib/client/src/element-materials/modifier/component/props/components/strategy/number.vue index e749974e8..46d97cfce 100644 --- a/lib/client/src/element-materials/modifier/component/props/components/strategy/number.vue +++ b/lib/client/src/element-materials/modifier/component/props/components/strategy/number.vue @@ -69,7 +69,9 @@ // 190, // . 38, 40, 37, 39, // up down left right 46, // del - 9 // tab + 9, // tab + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, // 小键盘0-9 + 109, 110 // 小键盘 - . ], renderValue: 0 } diff --git a/lib/client/src/element-materials/modifier/component/props/components/strategy/pagination.vue b/lib/client/src/element-materials/modifier/component/props/components/strategy/pagination.vue index 30b47d981..119e7f0a7 100644 --- a/lib/client/src/element-materials/modifier/component/props/components/strategy/pagination.vue +++ b/lib/client/src/element-materials/modifier/component/props/components/strategy/pagination.vue @@ -42,7 +42,8 @@ v-bk-tooltips="{ content: config.tips, placements: ['left-start'], - boundary: 'window' + boundary: 'window', + maxWidth: 400 }" > count @@ -79,7 +80,8 @@ v-bk-tooltips="{ content: config.tips, placements: ['left-start'], - boundary: 'window' + boundary: 'window', + maxWidth: 400 }" > {{ config.name }} diff --git a/lib/client/src/element-materials/modifier/component/props/components/strategy/remote.vue b/lib/client/src/element-materials/modifier/component/props/components/strategy/remote.vue index cadcad3df..5b9e6bc55 100644 --- a/lib/client/src/element-materials/modifier/component/props/components/strategy/remote.vue +++ b/lib/client/src/element-materials/modifier/component/props/components/strategy/remote.vue @@ -123,9 +123,6 @@ }, created () { this.remoteData = Object.assign({}, this.remoteData, this.payload) - if (this.autoGetData && this.remoteData.methodCode) { - this.getApiData() - } }, methods: { changeFunc (val) { diff --git a/lib/client/src/element-materials/modifier/component/props/components/strategy/select.vue b/lib/client/src/element-materials/modifier/component/props/components/strategy/select.vue index c3b018782..a24fff3cb 100644 --- a/lib/client/src/element-materials/modifier/component/props/components/strategy/select.vue +++ b/lib/client/src/element-materials/modifier/component/props/components/strategy/select.vue @@ -20,7 +20,13 @@ --> + + diff --git a/lib/client/src/element-materials/modifier/component/styles/layout/index.vue b/lib/client/src/element-materials/modifier/component/styles/layout/index.vue index 6e6163088..2d34cd8d0 100644 --- a/lib/client/src/element-materials/modifier/component/styles/layout/index.vue +++ b/lib/client/src/element-materials/modifier/component/styles/layout/index.vue @@ -10,19 +10,37 @@ --> @@ -37,9 +55,21 @@ iconShow: { type: Boolean, default: false + }, + tips: { + type: String, + default: '' + } + }, + data () { + return { + isFolded: this.folded } }, methods: { + handleToggle () { + this.isFolded = !this.isFolded + }, handleClick () { this.$emit('reset') } @@ -48,12 +78,13 @@ diff --git a/lib/client/src/element-materials/modifier/component/styles/layout/item.vue b/lib/client/src/element-materials/modifier/component/styles/layout/item.vue index dec12eda0..cf475bf6c 100644 --- a/lib/client/src/element-materials/modifier/component/styles/layout/item.vue +++ b/lib/client/src/element-materials/modifier/component/styles/layout/item.vue @@ -10,14 +10,15 @@ -->