|
|
@@ -12,15 +12,17 @@
|
|
|
v-for="(grid, k) in col.grid_list"
|
|
|
:key="`grid-${i}-${j}-${k}`"
|
|
|
:style="{ gridArea: grid.grid_area, height: grid.height }"
|
|
|
- :class="[!noMoveComponent.includes(grid.type) ? 'grid' : 'no-grid']"
|
|
|
+ :class="[!noMoveComponent.includes(grid.type) ? 'grid' : 'no-grid', { active: curSelectId === grid.id }]"
|
|
|
+ @click="selectedComponent(grid.id)"
|
|
|
>
|
|
|
+ <!-- 拖拽调整线:绝对定位覆盖在组件四边,作为纯覆盖层不占用布局空间,使组件内容盒尺寸与 CoursewarePreview 完全一致 -->
|
|
|
<template v-for="{ type, cursor, lineClass } in moveLineList">
|
|
|
<span
|
|
|
v-if="!noMoveComponent.includes(grid.type)"
|
|
|
:key="`${type}-${i}-${j}-${k}`"
|
|
|
class="drag-line"
|
|
|
:class="[type, ...lineClass]"
|
|
|
- :style="{ gridArea: type, cursor }"
|
|
|
+ :style="{ cursor }"
|
|
|
:data-type="type"
|
|
|
@mousedown="dragStart($event, { cursor, type, i, j, k, id: grid.id })"
|
|
|
></span>
|
|
|
@@ -32,14 +34,8 @@
|
|
|
:key="`preview-${grid.id}`"
|
|
|
:courseware-id="coursewareId"
|
|
|
type="edit"
|
|
|
- :class="[grid.id, { active: curSelectId === grid.id }]"
|
|
|
+ :class="['component', grid.id]"
|
|
|
:data-id="grid.id"
|
|
|
- :style="{
|
|
|
- gridArea: 'preview',
|
|
|
- height: grid.height,
|
|
|
- overflow: 'auto',
|
|
|
- }"
|
|
|
- @click.native="selectedComponent(grid.id)"
|
|
|
@handleHeightChange="handleHeightChange"
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -148,6 +144,9 @@ export default {
|
|
|
background: {},
|
|
|
},
|
|
|
curSelectId: '', // 当前选中组件id
|
|
|
+ previewLoading: null, // 预览加载层实例
|
|
|
+ loadingTimer: null, // 预览加载轮询定时器
|
|
|
+ loadingTimeout: null, // 预览加载兜底超时定时器
|
|
|
};
|
|
|
},
|
|
|
created() {
|
|
|
@@ -167,27 +166,56 @@ export default {
|
|
|
beforeDestroy() {
|
|
|
document.removeEventListener('mousemove', this.dragMove);
|
|
|
document.removeEventListener('mouseup', this.dragEnd);
|
|
|
+ // 清理预览加载的轮询/超时定时器与 loading 层,避免组件销毁后定时器仍持有实例引用
|
|
|
+ this.finishPreviewLoading();
|
|
|
if (this.resizeObserver) {
|
|
|
this.resizeObserver.disconnect();
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
init() {
|
|
|
- const loading = this.$loading({
|
|
|
+ this.previewLoading = this.$loading({
|
|
|
lock: true,
|
|
|
text: '预览组件加载中...',
|
|
|
spinner: 'el-icon-loading',
|
|
|
});
|
|
|
|
|
|
- const timer = setInterval(() => {
|
|
|
- let isLoader = this.rowList.length === 0 || this.$refs?.preview?.every((item) => item.loader);
|
|
|
-
|
|
|
- if (isLoader) {
|
|
|
- loading.close();
|
|
|
- clearInterval(timer);
|
|
|
+ // 兜底超时:组件内容请求挂起时强制关闭加载层,避免永久停留
|
|
|
+ this.loadingTimeout = setTimeout(() => {
|
|
|
+ this.finishPreviewLoading();
|
|
|
+ }, 300000);
|
|
|
+
|
|
|
+ // 轮询检查所有预览组件是否加载完成。
|
|
|
+ // 之所以用轮询而非 computed/watch:$refs 在 Vue 2.6 中非响应式,无法直接监听子组件 loader 的变化;
|
|
|
+ // 该方式与编辑态 componentIsAllLoader 的轮询保持一致。
|
|
|
+ this.loadingTimer = setInterval(() => {
|
|
|
+ const previews = this.$refs?.preview;
|
|
|
+ // rowList 为空(空课件)时无需等待;previews 未就绪(尚未渲染)时继续等待
|
|
|
+ const isAllLoaded =
|
|
|
+ this.rowList.length === 0 || (Array.isArray(previews) ? previews.every((item) => item.loader) : false);
|
|
|
+
|
|
|
+ if (isAllLoaded) {
|
|
|
+ this.finishPreviewLoading();
|
|
|
}
|
|
|
}, 200);
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 结束预览加载:清理轮询定时器、兜底超时并关闭加载层(幂等)
|
|
|
+ */
|
|
|
+ finishPreviewLoading() {
|
|
|
+ if (this.loadingTimer) {
|
|
|
+ clearInterval(this.loadingTimer);
|
|
|
+ this.loadingTimer = null;
|
|
|
+ }
|
|
|
+ if (this.loadingTimeout) {
|
|
|
+ clearTimeout(this.loadingTimeout);
|
|
|
+ this.loadingTimeout = null;
|
|
|
+ }
|
|
|
+ if (this.previewLoading) {
|
|
|
+ this.previewLoading.close();
|
|
|
+ this.previewLoading = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
handleHeightChange(id, newHeight) {
|
|
|
this.$emit('handleHeightChange', id, newHeight);
|
|
|
},
|
|
|
@@ -277,15 +305,46 @@ export default {
|
|
|
});
|
|
|
|
|
|
// 计算 grid_template_rows
|
|
|
+ // - 单组件:auto 用 auto;px 用其值
|
|
|
+ // - 多组件:过滤 auto 后取最大 px,避免生成 max(auto, px) 非法 CSS
|
|
|
+ const toNumberHeight = (height) => {
|
|
|
+ const num = Number(String(height).replace('px', ''));
|
|
|
+ return Number.isFinite(num) ? num : NaN;
|
|
|
+ };
|
|
|
+
|
|
|
let gridTemplateRows = '';
|
|
|
sortedRows.forEach((row) => {
|
|
|
- const heights = (rowGroups.get(row) || []).map((item) => item.height);
|
|
|
- if (heights.length === 1) {
|
|
|
- gridTemplateRows += `${heights[0]} `;
|
|
|
- } else {
|
|
|
- const isAllAuto = heights.every((item) => item === 'auto'); // 是否全是 auto
|
|
|
- gridTemplateRows += isAllAuto ? 'auto ' : `max(${heights.join(', ')}) `;
|
|
|
+ const items = rowGroups.get(row) || [];
|
|
|
+ if (items.length === 1) {
|
|
|
+ const current = items[0];
|
|
|
+ if (current.height === 'auto') {
|
|
|
+ gridTemplateRows += 'auto ';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const baseHeight = toNumberHeight(current.height);
|
|
|
+ if (Number.isNaN(baseHeight)) {
|
|
|
+ gridTemplateRows += `${current.height} `;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ gridTemplateRows += `${baseHeight}px `;
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ const nonAutoItems = items.filter((item) => item.height !== 'auto');
|
|
|
+ if (nonAutoItems.length === 0) {
|
|
|
+ gridTemplateRows += 'auto ';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let maxHeight = 0;
|
|
|
+ nonAutoItems.forEach((item) => {
|
|
|
+ const current = toNumberHeight(item.height);
|
|
|
+ if (!Number.isNaN(current) && current > maxHeight) {
|
|
|
+ maxHeight = current;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ gridTemplateRows += `${maxHeight}px `;
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
@@ -309,6 +368,8 @@ export default {
|
|
|
const dragElement = this.findChildComponentByKey(`preview-${id}`);
|
|
|
if (!dragElement) return;
|
|
|
if (cursor === 'default') return; // 无需拖动
|
|
|
+ // 阻止默认行为,避免拖拽过程中选中文本 / 触发图片原生拖拽
|
|
|
+ event.preventDefault();
|
|
|
this.dragElement = dragElement;
|
|
|
|
|
|
const { clientX, clientY } = event;
|
|
|
@@ -356,8 +417,6 @@ export default {
|
|
|
|
|
|
this.drag.startX = clientX;
|
|
|
this.drag.startY = clientY;
|
|
|
-
|
|
|
- this.$forceUpdate();
|
|
|
},
|
|
|
/**
|
|
|
* 拖拽结束
|
|
|
@@ -494,28 +553,57 @@ export default {
|
|
|
.col {
|
|
|
display: grid;
|
|
|
gap: $component-spacing;
|
|
|
- align-items: flex-start;
|
|
|
|
|
|
- .active {
|
|
|
- box-shadow: 0 0 6px 1px $main-hover-color;
|
|
|
- }
|
|
|
+ // 防止列被行高拉伸时 auto 轨道膨胀(自动高度组件/分割线被推到底部)
|
|
|
+ align-content: start;
|
|
|
+ align-items: flex-start;
|
|
|
|
|
|
.grid {
|
|
|
- display: grid;
|
|
|
- grid-template:
|
|
|
- 'top top top' 3px
|
|
|
- 'left preview right' 1fr
|
|
|
- 'bottom bottom bottom' 3px / 3px 1fr 3px;
|
|
|
- }
|
|
|
+ position: relative;
|
|
|
+ display: block;
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 0;
|
|
|
+
|
|
|
+ // 选中态:基于网格单元格(而非组件内容盒)绘制选中框
|
|
|
+ &.active {
|
|
|
+ box-shadow: 0 0 6px 1px $main-hover-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ .component {
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拖拽调整线:绝对定位覆盖在组件四边,不占用布局空间,
|
|
|
+ // 保证组件内容盒尺寸与 CoursewarePreview 一致
|
|
|
+ .drag-line,
|
|
|
+ .drag-vertical-line {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
|
|
|
- .drag-line {
|
|
|
- z-index: 2;
|
|
|
- width: 100%;
|
|
|
- height: 6px;
|
|
|
- cursor: ns-resize;
|
|
|
- background: linear-gradient(to bottom, transparent, transparent 40%, #e5e6eb 40%, #e5e6eb 60%, transparent 60%);
|
|
|
+ .drag-line.top {
|
|
|
+ top: -3px;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 6px;
|
|
|
+ cursor: default;
|
|
|
+ background: linear-gradient(
|
|
|
+ to bottom,
|
|
|
+ transparent,
|
|
|
+ transparent 40%,
|
|
|
+ #e5e6eb 40%,
|
|
|
+ #e5e6eb 60%,
|
|
|
+ transparent 60%
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
- &.bottom {
|
|
|
+ .drag-line.bottom {
|
|
|
+ bottom: -3px;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 6px;
|
|
|
+ cursor: ns-resize;
|
|
|
background: linear-gradient(
|
|
|
to bottom,
|
|
|
transparent,
|
|
|
@@ -525,18 +613,31 @@ export default {
|
|
|
transparent 50%
|
|
|
);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- .drag-vertical-line {
|
|
|
- z-index: 2;
|
|
|
- width: 6px;
|
|
|
- height: 100%;
|
|
|
- cursor: ew-resize;
|
|
|
- background: linear-gradient(to right, transparent, transparent 40%, #e5e6eb 40%, #e5e6eb 60%, transparent 60%);
|
|
|
|
|
|
- &.left {
|
|
|
+ .drag-vertical-line.left {
|
|
|
+ top: 0;
|
|
|
+ left: -3px;
|
|
|
+ width: 6px;
|
|
|
+ height: 100%;
|
|
|
+ cursor: ew-resize;
|
|
|
background: linear-gradient(to left, transparent, transparent 60%, #e5e6eb 60%, #e5e6eb);
|
|
|
}
|
|
|
+
|
|
|
+ .drag-vertical-line.right {
|
|
|
+ top: 0;
|
|
|
+ right: -3px;
|
|
|
+ width: 6px;
|
|
|
+ height: 100%;
|
|
|
+ cursor: ew-resize;
|
|
|
+ background: linear-gradient(
|
|
|
+ to right,
|
|
|
+ transparent,
|
|
|
+ transparent 40%,
|
|
|
+ #e5e6eb 40%,
|
|
|
+ #e5e6eb 60%,
|
|
|
+ transparent 60%
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|