fix:完善预览模式

This commit is contained in:
lhj
2024-11-17 14:47:36 +08:00
parent 8424202fc8
commit f601417913
13 changed files with 130 additions and 124 deletions

View File

@ -1,12 +1,7 @@
<script setup lang="ts">
import MainView from "../preview/views/MainView.vue";
</script>
<template>
<div>
<MainView></MainView>
</div>
<router-view></router-view>
</template>
<style scoped>

View File

@ -1,26 +1,26 @@
<template>
<div
style="width: fit-content; display: flex; flex-direction: column; position: relative;"
v-if="store.nowComponentsData!==null&&(componentVisible || store.designerMode)"
v-if="store.nowComponentsData !== null && (componentVisible || store.designerMode)"
:id="componentId"
:class="[
'dynamic-component',
{ 'hover-state': isHovered },
{ 'click-state': componentSelected }
{ 'hover-state': isHovered && store.designerMode },
{ 'click-state': componentSelected && store.designerMode }
]"
@click.stop="handleClick"
@mouseover="isHovered = true"
@mouseleave="isHovered = false"
>
<div v-if="isHovered" class="component-header">
<span>{{componentName}}</span>
<div v-if="isHovered && store.designerMode" class="component-header">
<span>{{ componentName }}</span>
</div>
<div v-if="componentSelected" class="component-header" :style="headerStyle">
<div style="background-color:#3457cc;color: #ffffff;padding: 5px ;margin-right: 2px">{{ componentName }}</div>
<div v-if="componentSelected" style="background-color:#3457cc;color:#ffffff;padding: 6px 5px 5px 5px;display: flex;width: fit-content;flex-wrap: nowrap">
<div v-if="componentSelected && store.designerMode" class="component-header" :style="headerStyle">
<div style="background-color:#3457cc;color: #ffffff;padding: 5px ;margin-right: 2px">{{ componentName }}</div>
<div style="background-color:#3457cc;color:#ffffff;padding: 6px 5px 5px 5px;display: flex;width: fit-content;flex-wrap: nowrap">
<icon-copy class="clickable" size="20" />
<icon-edit class="clickable" @click="handleEditFunc" size="20"/>
<icon-delete class="clickable" @click="handleDeleteFunc" size="20"/>
<icon-edit class="clickable" @click="handleEditFunc" size="20" />
<icon-delete class="clickable" @click="handleDeleteFunc" size="20" />
</div>
</div>
<div class="component-content">
@ -32,10 +32,10 @@
>
{{ componentText }}
<template v-for="child in componentChildren" :key="child.id">
<DynamicComponent :componentData="child"/>
<DynamicComponent :componentData="child" />
</template>
<template v-for="(slot, key, index) in componentSlots" :key="index" v-slot:[key]>
<DynamicComponent :component-data="slot"/>
<DynamicComponent :component-data="slot" />
</template>
</component>
</div>
@ -43,9 +43,9 @@
</template>
<script setup lang="ts">
import {defineProps, ref, computed, onMounted, watch, markRaw, nextTick} from 'vue';
import {componentMapping} from './componentMapping';
import {useSchemeStore} from '../stores/useSchemeStore';
import { defineProps, ref, computed, onMounted, watch, markRaw, nextTick } from 'vue';
import { componentMapping } from './componentMapping';
import { useSchemeStore } from '../stores/useSchemeStore';
const store = useSchemeStore();
const props = defineProps({
@ -82,16 +82,16 @@ const isHovered = ref(false);
const handleClick = () => {
const currentComponent = getCurrentSchemeObj();
if (currentComponent) {
store.updateNowScheme(currentComponent);
console.log(`Component with id ${currentComponent?.id} was clicked.`);
// 你可以在这里执行更多的逻辑,例如发出一个事件或调用一个方法
}
};
const handleEditFunc = () => {
// 编辑功能
};
const handleDeleteFunc = () => {
store.deleteScheme(componentId.value);
};
@ -109,7 +109,7 @@ const adjustHeaderPosition = () => {
let top = 0;
let left = 0;
let justification='';
let justification = '';
// 获取最外层组件渲染区域的边界
const containerEl = document.getElementById('renderArea');
@ -117,26 +117,25 @@ const adjustHeaderPosition = () => {
const containerRect = containerEl.getBoundingClientRect();
debugger
// 计算上下左右的可用空间
const topSpace = componentRect.top - containerRect.top;
const bottomSpace = containerRect.bottom - componentRect.bottom;
// 检查上方是否有足够的空间
if (topSpace >= headerRect.height) {
top = -headerRect.height+2;
top = -headerRect.height + 2;
} else if (bottomSpace >= headerRect.height) {
top = componentRect.height-2;
top = componentRect.height - 2;
}
if (headerRect.width>componentRect.width)
{
left=-2;
justification='flex-start';
}else
{
left=componentRect.width-headerRect.width-2;
justification='flex-end';
if (headerRect.width > componentRect.width) {
left = -2;
justification = 'flex-start';
} else {
left = componentRect.width - headerRect.width - 2;
justification = 'flex-end';
}
headerStyle.value = {
top: `${top}px`,
left: `${left}px`,
@ -145,8 +144,7 @@ const adjustHeaderPosition = () => {
};
onMounted(() => {
adjustHeaderPosition()
// console.log("组件挂载后",props.componentData);
adjustHeaderPosition();
});
watch(() => componentSelected.value, () => {
@ -158,11 +156,10 @@ watch(() => componentSelected.value, () => {
});
</script>
<style scoped>
.dynamic-component {
position: relative;
border: 1px solid transparent; /* 默认透明边框 */
transition: box-shadow 0.1s;
width: fit-content;
}

View File

@ -2,7 +2,7 @@ import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ArcoVue from '@arco-design/web-vue';
import router from "./router";
import router from "@/router/index.ts";
// 额外引入图标库
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import '@arco-design/web-vue/dist/arco.css';

View File

@ -1,19 +1,37 @@
{
"type": "AdaptivePage",
"name": "AdaptivePage",
"id": "a31c7fb13910000",
"version": "2.0",
"props": {},
"class": "",
"style": "",
"variables": {},
"dataSources": {},
"functions": {},
"orchestrations": {},
"events": {},
"slots": {},
"header": {},
"footer": {},
[
{
"id": "Card-b22544833910000",
"name": "card",
"type": "Card",
"props": {
"title": "Card"
},
"class": "arco-card arco-card-size-medium arco-card-bordered",
"designer": "",
"text": "ByteDance's core product, Toutiao (\"Headlines\"), is a content platform in China and around the world. Toutiao started out as a news recommendation engine and gradually evolved into a platform delivering content in various formats.",
"children": [],
"meta": {}
}
"style": "width:360px",
"visible": true,
"slots": {
"extra": {
"id": "9f8289a12910000",
"name": "avatar",
"type": "Avatar",
"props": {},
"class": "",
"designer": "",
"text": "",
"children": [],
"style": "",
"visible": "",
"slots": {},
"disable": "",
"events": {},
"loop": {}
}
},
"disable": true,
"events": {},
"loop": {}
}
]

View File

@ -1,15 +0,0 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
{
path: "/:pathMatch(.*)*",
component: () => import("../preview/views/MainView.vue"),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;

View File

@ -1,18 +1,23 @@
import { createRouter, createWebHashHistory } from "vue-router";
import { createRouter, createWebHistory } from 'vue-router';
import Designer from '../views/Designer.vue';
import Preview from '../views/Preview.vue';
const routes = [
// {
// path: "/lowcode",
// name: "活动生成",
// meta: {
// title: "活动生成",
// icon: "Calendar",
// },
// component: () => import("@/pages/lowCode/index.vue"),
// },
{
path: '/',
name: 'Designer',
component: Designer
},
{
path: '/preview',
name: 'Preview',
component: Preview
}
];
const router = createRouter({
history: createWebHashHistory(),
routes,
history: createWebHistory(import.meta.env.VITE_BASE_URL),
routes
});
export { router, routes };
export default router;

290
src/views/Designer.vue Normal file
View File

@ -0,0 +1,290 @@
<template>
<div class="low_box">
<div class="header">
<div class="title">可视化系统</div>
<div class="web">pc</div>
<div class="btn">
<a-space>
<a-button :disabled="!store.canUndo" @click="store.undo">
<template #icon>
<icon-left />
</template>
</a-button>
<a-button :disabled="!store.canRedo" @click="store.redo">
<template #icon>
<icon-right />
</template>
</a-button>
<a-button type="primary" @click="save">保存</a-button>
<a-button type="primary" @click="view">预览</a-button>
</a-space>
</div>
</div>
<div class="content">
<div class="left">
<div class="title">组件</div>
<VueDraggable
v-model="store.components"
:animation="150"
:group="{ name: 'designer', pull: 'clone', put: false }"
:sort="false"
:clone="clone"
@start="onStart"
@end="onEnd"
>
<div v-for="item in store.components" :key="item.id" class="tem_btn">
{{ item.name }}
</div>
</VueDraggable>
</div>
<div ref="targetContent" id="renderArea" class="center">
<VueDraggable
v-model="store.previewScheme"
:sort="true"
:animation="150"
group="designer"
ghost-class="ghost"
class="canvas"
@start="onPreviewStart"
@update="onPreviewUpdate"
@stop="onPreviewStop"
@add="onPreViewAdd"
>
<DynamicComponent v-for="component in store.previewScheme" :key="component.id" :componentData="component">
{{ component.id }}
</DynamicComponent>
</VueDraggable>
</div>
<div class="right">
<PropertyEditor v-if="store.nowComponentsData" :scheme="store.nowComponentsData"></PropertyEditor>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { RadioGroup, Radio } from '@arco-design/web-vue';
import { onMounted, ref, watch } from 'vue';
import { uuid } from 'lsp-uuid';
import { componentScheme } from '@/schemes/scheme.ts';
import { useSchemeStore } from '@/stores/useSchemeStore.ts';
import { IComponent } from '@/type/IComponent.ts';
import DynamicComponent from '@/components/DynamicComponent.vue';
import PropertyEditor from '@/components/PropertyEditor.vue';
import { DraggableEvent, VueDraggable } from 'vue-draggable-plus';
import axios from 'axios';
let componentsList = [];
const store = useSchemeStore();
watch(store, (n) => {
// console.log("store发生了变化", n);
});
const baseScheme = {
"type": "AdaptivePage",
"name": "AdaptivePage",
"id": uuid(),
"version": "2.0",
"props": {},
"class": "",
"style": "",
"variables": {},
"dataSources": {},
"functions": {},
"orchestrations": {},
"events": {},
"slots": {},
"header": {},
"footer": {},
"children": [],
"meta": {}
};
store.$onAction(
({
name, // action 名称
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
after((result) => {
// console.log(`store action-${name}回调后:` + JSON.stringify(result));
});
onError((error) => {
console.warn(`Failed "${name}" after\nError: ${error}.`);
});
}
);
// 初始化scheme
const initScheme = async () => {
// try {
// const response = await axios.get('http://localhost:3000/load');
// if (response.data && Array.isArray(response.data)) {
// store.initPreviewScheme(response.data);
// } else {
// store.initPreviewScheme([baseScheme]);
// }
// } catch (error) {
// console.error('Error loading data:', error);
// store.initPreviewScheme([baseScheme]);
// }
componentsList = Object.values(componentScheme);
store.initComponents(componentsList);
};
onMounted(() => {
initScheme();
});
function clone(element: Record<'name' | 'id' | 'type' | 'props' | 'class' | 'text' | 'style' | 'slots' | 'visible' | 'disable' | 'children', IComponent>) {
// console.log("clone", element);
return {
id: `${element.type}-${uuid()}`,
name: element.name,
type: element.type,
props: element.props,
class: element.class,
designer: '',
text: element.text,
children: element.children || [],
style: element.style,
visible: element.visible,
slots: element.slots,
disable: element.visible,
events: {},
loop: {},
};
}
const onPreViewAdd = (event: DraggableEvent) => {
console.log("onPreViewAdd", event);
store.updateNowScheme(event.clonedData);
};
const onEnd = (event: DraggableEvent) => {
// console.log("onEnd", event);
// store.updateNowScheme(event.clonedData);
};
const onStart = (event) => {
// console.log("onStart", event);
};
const onPreviewStart = (event:DraggableEvent) => {
// console.log("onPreviewStart", event);
};
const onPreviewUpdate = (event) => {
// console.log("onPreviewUpdate", event);
};
const onPreviewStop = (event:DraggableEvent) => {
// console.log(event);
};
const save = async () => {
try {
await axios.post('http://localhost:3000/save', store.previewScheme);
alert('保存成功!');
} catch (error) {
console.error('Error saving data:', error);
alert('保存失败!');
}
};
const view = () => {
localStorage.setItem("lowcode", JSON.stringify(store.previewScheme));
// 打开新窗口并导航到预览页面
const newWindow = window.open('/preview', '_blank');
if (newWindow) {
newWindow.focus();
} else {
alert('弹窗被阻止,请允许弹窗');
}
};
</script>
<style lang="scss" scoped>
.low_box {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f2f2f2;
.header {
height: 65px;
background-color: #fff;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.content {
display: flex;
flex-direction: row;
height: calc(100vh - 66px);
.left {
width: 300px;
height: 100%;
overflow: hidden;
background-color: #fff;
border-top: 1px solid #dddddd;
overflow-y: auto;
.title {
font-size: 16px;
color: #333;
line-height: 50px;
width: calc(100% - 40px);
margin-left: 20px;
border-bottom: 1px solid #f2f2f2;
clear: both;
}
.tem_btn {
padding: 0 10px;
height: 30px;
line-height: 30px;
text-align: center;
font-size: 14px;
color: #666;
background-color: #f2f2f2;
border-radius: 4px;
cursor: move;
user-select: none;
margin-top: 20px;
float: left;
margin-left: 20px;
}
}
.center {
flex: 1;
padding: 20px;
background-color: #f2f2f2;
.canvas {
background-color: #fff;
width: 100%;
height: 100%;
overflow-y: auto; /* 确保当内容超出时,出现垂直滚动条 */
}
.ghost {
background-color: #f2f3f5 !important;
}
}
.right {
width: 300px;
height: 100%;
overflow: hidden;
background-color: #fff;
border-top: 1px solid #dddddd;
}
}
}
</style>

27
src/views/Preview.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<DynamicComponent v-for="component in preView" :key="component.id" :componentData="component">
{{ component.id }}
</DynamicComponent>
</template>
<script setup lang="ts">
import {onBeforeMount, shallowRef, ref} from "vue";
import DynamicComponent from "@/components/DynamicComponent.vue";
import { useSchemeStore } from '../stores/useSchemeStore';
const store = useSchemeStore();
let preView=ref([])
onBeforeMount(() => {
store.designerMode=false
preView = JSON.parse(localStorage.getItem("lowcode") || "");
console.log(preView)
});
</script>
<style lang="scss" scoped>
.box {
width: 1000px;
height: 600px;
overflow: hidden;
}
</style>