123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- <template>
- <el-container class="editor">
- <!-- 顶部:工具栏 -->
- <el-header class="editor-header">
- <!-- 左侧操作区 -->
- <slot name="toolBarLeft"></slot>
- <!-- 中心操作区 -->
- <div class="header-center flex flex-1 items-center justify-center">
- <span>{{ title }}</span>
- </div>
- <!-- 右侧操作区 -->
- <el-button-group class="header-right">
- <el-tooltip content="重置">
- <el-button @click="handleReset">
- <Icon icon="system-uicons:reset-alt" :size="24" />
- </el-button>
- </el-tooltip>
- <el-tooltip content="预览">
- <el-button @click="handlePreview">
- <Icon icon="ep:view" :size="24" />
- </el-button>
- </el-tooltip>
- <el-tooltip content="保存">
- <el-button @click="handleSave">
- <Icon icon="ep:check" :size="24" />
- </el-button>
- </el-tooltip>
- </el-button-group>
- </el-header>
- <!-- 中心区域 -->
- <el-container class="editor-container">
- <!-- 左侧:组件库 -->
- <ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
- <!-- 中心设计区域 -->
- <div class="editor-center page-prop-area" @click="handlePageSelected">
- <div class="editor-design">
- <!-- 手机顶部 -->
- <div class="editor-design-top">
- <!-- 手机顶部状态栏 -->
- <img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
- <!-- 手机顶部导航栏 -->
- <NavigationBar
- v-if="showNavigationBar"
- :property="navigationBarComponent.property"
- @click="handleNavigationBarSelected"
- :class="[
- 'component',
- 'cursor-pointer!',
- { active: selectedComponent?.id === navigationBarComponent.id }
- ]"
- />
- </div>
- <!-- 手机页面编辑区域 -->
- <el-scrollbar class="editor-design-center" height="100%" view-class="page-prop-area">
- <div
- class="phone-container"
- :style="{
- backgroundColor: pageConfigComponent.property.backgroundColor,
- backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
- }"
- >
- <draggable
- class="page-prop-area drag-area"
- v-model="pageComponents"
- item-key="index"
- :animation="200"
- filter=".component-toolbar"
- ghost-class="draggable-ghost"
- :force-fallback="true"
- group="component"
- @change="handleComponentChange"
- >
- <template #item="{ element, index }">
- <div class="component-container" @click="handleComponentSelected(element, index)">
- <!-- 左侧组件名 -->
- <div
- :class="['component-name', { active: selectedComponentIndex === index }]"
- v-if="element.name"
- >
- {{ element.name }}
- </div>
- <!-- 组件内容区 -->
- <div :class="['component', { active: selectedComponentIndex === index }]">
- <component
- :is="element.id"
- :property="element.property"
- :data-type="element.id"
- />
- </div>
- <!-- 左侧:组件操作工具栏 -->
- <div
- class="component-toolbar"
- v-if="element.name && selectedComponentIndex === index"
- >
- <el-button-group type="primary">
- <el-tooltip content="上移" placement="right">
- <el-button
- :disabled="index === 0"
- @click.stop="handleMoveComponent(index, -1)"
- >
- <Icon icon="ep:arrow-up" />
- </el-button>
- </el-tooltip>
- <el-tooltip content="下移" placement="right">
- <el-button
- :disabled="index === pageComponents.length - 1"
- @click.stop="handleMoveComponent(index, 1)"
- >
- <Icon icon="ep:arrow-down" />
- </el-button>
- </el-tooltip>
- <el-tooltip content="复制" placement="right">
- <el-button @click.stop="handleCopyComponent(index)">
- <Icon icon="ep:copy-document" />
- </el-button>
- </el-tooltip>
- <el-tooltip content="删除" placement="right">
- <el-button @click.stop="handleDeleteComponent(index)">
- <Icon icon="ep:delete" />
- </el-button>
- </el-tooltip>
- </el-button-group>
- </div>
- </div>
- </template>
- </draggable>
- </div>
- </el-scrollbar>
- <!-- 手机底部导航 -->
- <div
- v-if="showTabBar"
- :class="[
- 'editor-design-bottom',
- 'component',
- 'cursor-pointer!',
- { active: selectedComponent?.id === tabBarComponent.id }
- ]"
- >
- <TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" />
- </div>
- </div>
- </div>
- <!-- 右侧属性面板 -->
- <el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
- <el-card
- shadow="never"
- body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
- class="h-full"
- >
- <!-- 组件名称 -->
- <template #header>
- <div class="flex items-center gap-8px">
- <Icon :icon="selectedComponent.icon" color="gray" />
- <span>{{ selectedComponent.name }}</span>
- </div>
- </template>
- <el-scrollbar
- class="m-[calc(0px-var(--el-card-padding))]"
- view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
- >
- <component
- :is="selectedComponent.id + 'Property'"
- v-model="selectedComponent.property"
- />
- </el-scrollbar>
- </el-card>
- </el-aside>
- </el-container>
- </el-container>
- </template>
- <script lang="ts">
- // 注册所有的组件
- import { components } from './components/mobile/index'
- export default {
- components: { ...components }
- }
- </script>
- <script lang="ts" setup>
- import draggable from 'vuedraggable'
- import ComponentLibrary from './components/ComponentLibrary.vue'
- import NavigationBar from './components/mobile/NavigationBar/index.vue'
- import TabBar from './components/mobile/TabBar/index.vue'
- import { cloneDeep, includes } from 'lodash-es'
- import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
- import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
- import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
- import { isString } from '@/utils/is'
- import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
- import { componentConfigs } from '@/components/DiyEditor/components/mobile'
- /** 页面装修详情页 */
- defineOptions({ name: 'DiyPageDetail' })
- // 消息弹窗
- const message = useMessage()
- // 左侧组件库
- const componentLibrary = ref()
- // 页面设置组件
- const pageConfigComponent = ref<DiyComponent<any>>(cloneDeep(PAGE_CONFIG_COMPONENT))
- // 顶部导航栏
- const navigationBarComponent = ref<DiyComponent<any>>(cloneDeep(NAVIGATION_BAR_COMPONENT))
- // 底部导航菜单
- const tabBarComponent = ref<DiyComponent<any>>(cloneDeep(TAB_BAR_COMPONENT))
- // 选中的组件,默认选中顶部导航栏
- const selectedComponent = ref<DiyComponent<any>>()
- // 选中的组件索引
- const selectedComponentIndex = ref<number>(-1)
- // 组件列表
- const pageComponents = ref<DiyComponent<any>[]>([])
- // 定义属性
- const props = defineProps<{
- // 页面配置,支持Json字符串
- modelValue: string | PageConfig
- // 标题
- title: string
- // 组件库
- libs: DiyComponentLibrary[]
- // 是否显示顶部导航栏
- showNavigationBar: boolean
- // 是否显示底部导航菜单
- showTabBar: boolean
- // 是否显示页面配置
- showPageConfig: boolean
- }>()
- // 监听传入的页面配置
- watch(
- () => props.modelValue,
- () => {
- const modelValue = isString(props.modelValue)
- ? (JSON.parse(props.modelValue) as PageConfig)
- : props.modelValue
- pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
- navigationBarComponent.value.property =
- modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
- tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
- // 查找对应的页面组件
- pageComponents.value = (modelValue?.components || []).map((item) => {
- const component = componentConfigs[item.id]
- return { ...component, property: item.property }
- })
- },
- {
- immediate: true
- }
- )
- // 保存
- const handleSave = () => {
- const pageConfig = {
- page: pageConfigComponent.value.property,
- navigationBar: navigationBarComponent.value.property,
- tabBar: tabBarComponent.value.property,
- components: pageComponents.value.map((component) => {
- // 只保留APP有用的字段
- return { id: component.id, property: component.property }
- })
- } as PageConfig
- // 发送数据更新通知
- const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
- emits('update:modelValue', modelValue)
- // 发送保存通知
- emits('save', pageConfig)
- }
- // 处理页面选中:显示属性表单
- const handlePageSelected = (event: any) => {
- if (!props.showPageConfig) return
- // 配置了样式 page-prop-area 的元素,才显示页面设置
- if (includes(event?.target?.classList, 'page-prop-area')) {
- handleComponentSelected(unref(pageConfigComponent))
- }
- }
- /**
- * 选中组件
- *
- * @param component 组件
- * @param index 组件的索引
- */
- const handleComponentSelected = (component: DiyComponent<any>, index: number = -1) => {
- selectedComponent.value = component
- selectedComponentIndex.value = index
- }
- // 选中顶部导航栏
- const handleNavigationBarSelected = () => {
- handleComponentSelected(unref(navigationBarComponent))
- }
- // 选中底部导航菜单
- const handleTabBarSelected = () => {
- handleComponentSelected(unref(tabBarComponent))
- }
- // 组件变动
- const handleComponentChange = (dragEvent: any) => {
- // 新增,即从组件库拖拽添加组件
- if (dragEvent.added) {
- const { element, newIndex } = dragEvent.added
- handleComponentSelected(element, newIndex)
- } else if (dragEvent.moved) {
- // 拖拽排序
- const { newIndex } = dragEvent.moved
- // 保持选中
- selectedComponentIndex.value = newIndex
- }
- }
- // 交换组件
- const swapComponent = (oldIndex: number, newIndex: number) => {
- ;[pageComponents.value[oldIndex], pageComponents.value[newIndex]] = [
- pageComponents.value[newIndex],
- pageComponents.value[oldIndex]
- ]
- // 保持选中
- selectedComponentIndex.value = newIndex
- }
- /** 移动组件 */
- const handleMoveComponent = (index: number, direction: number) => {
- const newIndex = index + direction
- if (newIndex < 0 || newIndex >= pageComponents.value.length) return
- swapComponent(index, newIndex)
- }
- /** 复制组件 */
- const handleCopyComponent = (index: number) => {
- const component = cloneDeep(pageComponents.value[index])
- pageComponents.value.splice(index + 1, 0, component)
- }
- /**
- * 删除组件
- * @param index 当前组件index
- */
- const handleDeleteComponent = (index: number) => {
- // 删除组件
- pageComponents.value.splice(index, 1)
- if (index < pageComponents.value.length) {
- // 1. 不是最后一个组件时,删除后选中下面的组件
- let bottomIndex = index
- handleComponentSelected(pageComponents.value[bottomIndex], bottomIndex)
- } else if (pageComponents.value.length > 0) {
- // 2. 不是第一个组件时,删除后选中上面的组件
- let topIndex = index - 1
- handleComponentSelected(pageComponents.value[topIndex], topIndex)
- } else {
- // 3. 组件全部删除之后,显示页面设置
- handleComponentSelected(unref(pageConfigComponent))
- }
- }
- // 工具栏操作
- const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
- // 重置
- const handleReset = () => {
- message.warning('开发中~')
- emits('reset')
- }
- // 预览
- const handlePreview = () => {
- message.warning('开发中~')
- emits('preview')
- }
- // 设置默认选中的组件
- const setDefaultSelectedComponent = () => {
- if (props.showPageConfig) {
- selectedComponent.value = unref(pageConfigComponent)
- } else if (props.showNavigationBar) {
- selectedComponent.value = unref(navigationBarComponent)
- } else if (props.showTabBar) {
- selectedComponent.value = unref(tabBarComponent)
- }
- }
- watch(
- () => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
- () => setDefaultSelectedComponent()
- )
- onMounted(() => setDefaultSelectedComponent())
- </script>
- <style lang="scss" scoped>
- /* 手机宽度 */
- $phone-width: 375px;
- /* 根节点样式 */
- .editor {
- height: 100%;
- margin: calc(0px - var(--app-content-padding));
- display: flex;
- flex-direction: column;
- /* 顶部:工具栏 */
- .editor-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: auto;
- padding: 0;
- border-bottom: solid 1px var(--el-border-color);
- background-color: var(--el-bg-color);
- /* 工具栏:右侧按钮 */
- .header-right {
- height: 100%;
- .el-button {
- height: 100%;
- }
- }
- /* 隐藏工具栏按钮的边框 */
- :deep(.el-radio-button__inner),
- :deep(.el-button) {
- border-top: none !important;
- border-bottom: none !important;
- border-radius: 0 !important;
- }
- }
- /* 中心操作区 */
- .editor-container {
- height: calc(
- 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 42px
- );
- /* 右侧属性面板 */
- .editor-right {
- flex-shrink: 0;
- box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12);
- /* 属性面板顶部:减少内边距 */
- :deep(.el-card__header) {
- padding: 8px 16px;
- }
- /* 属性面板分组 */
- .property-group {
- /* 属性分组 */
- :deep(.el-card__header) {
- border: none;
- background: var(--el-bg-color-page);
- }
- }
- }
- /* 中心区域 */
- .editor-center {
- flex: 1 1 0;
- padding: 16px 0;
- background-color: var(--app-content-bg-color);
- display: flex;
- justify-content: center;
- /* 中心设计区域 */
- .editor-design {
- position: relative;
- height: 100%;
- width: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- overflow: hidden;
- /* 组件 */
- .component {
- border: 1px solid #fff;
- width: $phone-width;
- cursor: move;
- /* 鼠标放到组件上时 */
- &:hover {
- border: 1px dashed var(--el-color-primary);
- }
- }
- /* 组件选中 */
- .component.active {
- border: 2px solid var(--el-color-primary);
- }
- /* 手机顶部 */
- .editor-design-top {
- width: $phone-width;
- /* 手机顶部状态栏 */
- .status-bar {
- height: 20px;
- width: $phone-width;
- background-color: #fff;
- }
- }
- /* 手机底部导航 */
- .editor-design-bottom {
- width: $phone-width;
- }
- /* 手机页面编辑区域 */
- .editor-design-center {
- width: 100%;
- flex: 1 1 0;
- :deep(.el-scrollbar__view) {
- height: 100%;
- }
- /* 主体内容 */
- .phone-container {
- height: 100%;
- box-sizing: border-box;
- position: relative;
- background-repeat: no-repeat;
- background-size: 100% 100%;
- width: $phone-width;
- margin: 0 auto;
- .drag-area {
- height: 100%;
- }
- /* 组件容器(左侧:组件名称,中间:组件,右侧:操作工具栏) */
- .component-container {
- width: 100%;
- position: relative;
- /* 左侧:组件名称 */
- .component-name {
- position: absolute;
- width: 80px;
- text-align: center;
- line-height: 25px;
- height: 25px;
- background: #fff;
- font-size: 12px;
- left: -85px;
- top: 0;
- box-shadow:
- 0 0 4px #00000014,
- 0 2px 6px #0000000f,
- 0 4px 8px 2px #0000000a;
- /* 右侧小三角 */
- &:after {
- position: absolute;
- top: 7.5px;
- right: -10px;
- content: ' ';
- height: 0;
- width: 0;
- border: 5px solid transparent;
- border-left-color: #fff;
- }
- }
- /* 组件选中按钮 */
- .component-name.active {
- background: var(--el-color-primary);
- color: #fff;
- &:after {
- border-left-color: var(--el-color-primary);
- }
- }
- /* 右侧:组件操作工具栏 */
- .component-toolbar {
- position: absolute;
- top: 0;
- right: -57px;
- /* 左侧小三角 */
- &:before {
- position: absolute;
- top: 10px;
- left: -10px;
- content: ' ';
- height: 0;
- width: 0;
- border: 5px solid transparent;
- border-right-color: #2d8cf0;
- }
- /* 重写 Element 按钮组的样式(官方只支持水平显示,增加垂直显示的样式) */
- .el-button-group {
- display: inline-flex;
- flex-direction: column;
- }
- .el-button-group > .el-button:first-child {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- border-top-right-radius: var(--el-border-radius-base);
- border-bottom-color: var(--el-button-divide-border-color);
- }
- .el-button-group > .el-button:last-child {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- border-bottom-left-radius: var(--el-border-radius-base);
- border-top-color: var(--el-button-divide-border-color);
- }
- .el-button-group .el-button--primary:not(:first-child):not(:last-child) {
- border-top-color: var(--el-button-divide-border-color);
- border-bottom-color: var(--el-button-divide-border-color);
- }
- .el-button-group > .el-button:not(:last-child) {
- margin-bottom: -1px;
- margin-right: 0;
- }
- }
- }
- }
- }
- }
- }
- }
- }
- </style>
|