From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目
---
src/components/DiyEditor/index.vue | 604 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 604 insertions(+), 0 deletions(-)
diff --git a/src/components/DiyEditor/index.vue b/src/components/DiyEditor/index.vue
new file mode 100644
index 0000000..fa23a4d
--- /dev/null
+++ b/src/components/DiyEditor/index.vue
@@ -0,0 +1,604 @@
+<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 :size="24" icon="system-uicons:reset-alt" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip v-if="previewUrl" content="棰勮">
+ <el-button @click="handlePreview">
+ <Icon :size="24" icon="ep:view" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="淇濆瓨">
+ <el-button @click="handleSave">
+ <Icon :size="24" icon="ep:check" />
+ </el-button>
+ </el-tooltip>
+ </el-button-group>
+ </el-header>
+
+ <!-- 涓績鍖哄煙 -->
+ <el-container class="editor-container">
+ <!-- 宸︿晶锛氱粍浠跺簱锛圕omponentLibrary锛� -->
+ <ComponentLibrary v-if="libs && libs.length > 0" ref="componentLibrary" :list="libs" />
+ <!-- 涓績锛氳璁″尯鍩燂紙ComponentContainer锛� -->
+ <div class="editor-center page-prop-area" @click="handlePageSelected">
+ <!-- 鎵嬫満椤堕儴 -->
+ <div class="editor-design-top">
+ <!-- 鎵嬫満椤堕儴鐘舵�佹爮 -->
+ <img alt="" class="status-bar" src="@/assets/imgs/diy/statusBar.png" />
+ <!-- 鎵嬫満椤堕儴瀵艰埅鏍� -->
+ <ComponentContainer
+ v-if="showNavigationBar"
+ :active="selectedComponent?.id === navigationBarComponent.id"
+ :component="navigationBarComponent"
+ :show-toolbar="false"
+ class="cursor-pointer!"
+ @click="handleNavigationBarSelected"
+ />
+ </div>
+ <!-- 缁濆瀹氫綅鐨勭粍浠讹細渚嬪 寮圭獥銆佹诞鍔ㄦ寜閽瓑 -->
+ <div
+ v-for="(component, index) in pageComponents"
+ :key="index"
+ @click="handleComponentSelected(component, index)"
+ >
+ <component
+ :is="component.id"
+ v-if="component.position === 'fixed' && selectedComponent?.uid === component.uid"
+ :property="component.property"
+ />
+ </div>
+ <!-- 鎵嬫満椤甸潰缂栬緫鍖哄煙 -->
+ <el-scrollbar
+ :view-style="{
+ backgroundColor: pageConfigComponent.property.backgroundColor,
+ backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
+ }"
+ height="100%"
+ view-class="phone-container"
+ wrap-class="editor-design-center page-prop-area"
+ >
+ <draggable
+ v-model="pageComponents"
+ :animation="200"
+ :force-fallback="false"
+ class="page-prop-area drag-area"
+ filter=".component-toolbar"
+ ghost-class="draggable-ghost"
+ group="component"
+ item-key="index"
+ @change="handleComponentChange"
+ >
+ <template #item="{ element, index }">
+ <ComponentContainer
+ v-if="!element.position || element.position === 'center'"
+ :active="selectedComponentIndex === index"
+ :can-move-down="index < pageComponents.length - 1"
+ :can-move-up="index > 0"
+ :component="element"
+ @click="handleComponentSelected(element, index)"
+ @copy="handleCopyComponent(index)"
+ @delete="handleDeleteComponent(index)"
+ @move="(direction) => handleMoveComponent(index, direction)"
+ />
+ </template>
+ </draggable>
+ </el-scrollbar>
+ <!-- 鎵嬫満搴曢儴瀵艰埅 -->
+ <div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
+ <ComponentContainer
+ :active="selectedComponent?.id === tabBarComponent.id"
+ :component="tabBarComponent"
+ :show-toolbar="false"
+ @click="handleTabBarSelected"
+ />
+ </div>
+ <!-- 鍥哄畾甯冨眬鐨勭粍浠� 鎿嶄綔鎸夐挳鍖� -->
+ <div class="fixed-component-action-group">
+ <el-tag
+ v-if="showPageConfig"
+ :effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'"
+ :type="selectedComponent?.uid === pageConfigComponent.uid ? 'primary' : 'info'"
+ size="large"
+ @click="handleComponentSelected(pageConfigComponent)"
+ >
+ <Icon :icon="pageConfigComponent.icon" :size="12" />
+ <span>{{ pageConfigComponent.name }}</span>
+ </el-tag>
+ <template v-for="(component, index) in pageComponents" :key="index">
+ <el-tag
+ v-if="component.position === 'fixed'"
+ :effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'"
+ :type="selectedComponent?.uid === component.uid ? 'primary' : 'info'"
+ closable
+ size="large"
+ @click="handleComponentSelected(component)"
+ @close="handleDeleteComponent(index)"
+ >
+ <Icon :icon="component.icon" :size="12" />
+ <span>{{ component.name }}</span>
+ </el-tag>
+ </template>
+ </div>
+ </div>
+ <!-- 鍙充晶锛氬睘鎬ч潰鏉匡紙ComponentContainerProperty锛� -->
+ <el-aside v-if="selectedComponent?.property" class="editor-right" width="350px">
+ <el-card
+ body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
+ class="h-full"
+ shadow="never"
+ >
+ <!-- 缁勪欢鍚嶇О -->
+ <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'"
+ :key="selectedComponent?.uid || selectedComponent?.id"
+ v-model="selectedComponent.property"
+ />
+ </el-scrollbar>
+ </el-card>
+ </el-aside>
+ </el-container>
+ </el-container>
+
+ <!-- 棰勮寮规 -->
+ <Dialog v-model="previewDialogVisible" title="棰勮" width="700">
+ <div class="flex justify-around">
+ <IFrame
+ :src="previewUrl"
+ class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!"
+ />
+ <div class="flex flex-col">
+ <el-text>鎵嬫満鎵爜棰勮</el-text>
+ <Qrcode :text="previewUrl" logo="/logo.gif" />
+ </div>
+ </div>
+ </Dialog>
+</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 { 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 { isEmpty, isString } from '@/utils/is'
+import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
+import { componentConfigs } from '@/components/DiyEditor/components/mobile'
+import { array, oneOfType } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+
+/** 椤甸潰瑁呬慨璇︽儏椤� */
+defineOptions({ name: 'DiyPageDetail' })
+
+// 宸︿晶缁勪欢搴�
+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({
+ // 椤甸潰閰嶇疆锛屾敮鎸丣son瀛楃涓�
+ modelValue: oneOfType<string | PageConfig>([String, Object]).isRequired,
+ // 鏍囬
+ title: propTypes.string.def(''),
+ // 缁勪欢搴�
+ libs: array<DiyComponentLibrary>(),
+ // 鏄惁鏄剧ず椤堕儴瀵艰埅鏍�
+ showNavigationBar: propTypes.bool.def(true),
+ // 鏄惁鏄剧ず搴曢儴瀵艰埅鑿滃崟
+ showTabBar: propTypes.bool.def(false),
+ // 鏄惁鏄剧ず椤甸潰閰嶇疆
+ showPageConfig: propTypes.bool.def(true),
+ // 棰勮鍦板潃锛氭彁渚涗簡棰勮鍦板潃锛屾墠浼氭樉绀洪瑙堟寜閽�
+ previewUrl: propTypes.string.def('')
+})
+
+// 鐩戝惉浼犲叆鐨勯〉闈㈤厤缃�
+// 瑙f瀽鍑� pageConfigComponent 椤甸潰鏁翠綋鐨勯厤缃紝navigationBarComponent銆乸ageComponents銆乼abBarComponent 椤甸潰涓娿�佷腑銆佷笅鐨勯厤缃�
+watch(
+ () => props.modelValue,
+ () => {
+ const modelValue =
+ isString(props.modelValue) && !isEmpty(props.modelValue)
+ ? (JSON.parse(props.modelValue) as PageConfig)
+ : props.modelValue
+ pageConfigComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.page) || PAGE_CONFIG_COMPONENT.property
+ navigationBarComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.navigationBar) ||
+ NAVIGATION_BAR_COMPONENT.property
+ tabBarComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.tabBar) || TAB_BAR_COMPONENT.property
+ // 鏌ユ壘瀵瑰簲鐨勯〉闈㈢粍浠�
+ pageComponents.value = ((typeof modelValue !== 'string' && modelValue?.components) || []).map(
+ (item) => {
+ const component = componentConfigs[item.id]
+ return { ...component, property: item.property }
+ }
+ )
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 閫夋嫨缁勪欢淇敼鍏跺睘鎬у悗鏇存柊瀹冪殑閰嶇疆 */
+watch(
+ selectedComponent,
+ (val: any) => {
+ if (!val || selectedComponentIndex.value === -1) {
+ return
+ }
+ // 濡傛灉鏄熀纭�璁剧疆椤碉紝榛樿閫変腑鐨勭储寮曟敼鎴� -1锛屼负浜嗛槻姝㈠垹闄ょ粍浠跺悗鍒囨崲鍒版椤靛鑷存姤閿�
+ // https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/792
+ if (props.showTabBar) {
+ selectedComponentIndex.value = -1
+ }
+ pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
+ },
+ { deep: true }
+)
+
+// 淇濆瓨
+const handleSave = () => {
+ // 鍙戦�佷繚瀛橀�氱煡
+ emits('save')
+}
+// 鐩戝惉閰嶇疆淇敼
+const pageConfigChange = () => {
+ const pageConfig = {
+ page: pageConfigComponent.value.property,
+ navigationBar: navigationBarComponent.value.property,
+ tabBar: tabBarComponent.value.property,
+ components: pageComponents.value.map((component) => {
+ // 鍙繚鐣橝PP鏈夌敤鐨勫瓧娈�
+ return { id: component.id, property: component.property }
+ })
+ } as PageConfig
+ if (!props.showTabBar) {
+ delete pageConfig.tabBar
+ }
+ // 鍙戦�佹暟鎹洿鏂伴�氱煡
+ const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
+ emits('update:modelValue', modelValue)
+}
+watch(
+ () => [
+ pageConfigComponent.value.property,
+ navigationBarComponent.value.property,
+ tabBarComponent.value.property,
+ pageComponents.value
+ ],
+ () => {
+ pageConfigChange()
+ },
+ { deep: true }
+)
+// 澶勭悊椤甸潰閫変腑锛氭樉绀哄睘鎬ц〃鍗�
+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])
+ component.uid = new Date().getTime()
+ 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 reload = inject<() => void>('reload')
+// 閲嶇疆
+const handleReset = () => {
+ if (reload) reload()
+ emits('reset')
+}
+
+// 棰勮
+const previewDialogVisible = ref(false)
+const handlePreview = () => {
+ previewDialogVisible.value = true
+ 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;
+$toolbar-height: 42px;
+
+/* 鏍硅妭鐐规牱寮� */
+.editor {
+ display: flex;
+ height: 100%;
+ margin: calc(0px - var(--app-content-padding));
+ flex-direction: column;
+
+ /* 椤堕儴锛氬伐鍏锋爮 */
+ .editor-header {
+ display: flex;
+ height: $toolbar-height;
+ padding: 0;
+ background-color: var(--el-bg-color);
+ border-bottom: solid 1px var(--el-border-color);
+ align-items: center;
+ justify-content: space-between;
+
+ /* 宸ュ叿鏍忥細鍙充晶鎸夐挳 */
+ .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) -
+ $toolbar-height
+ );
+
+ /* 鍙充晶灞炴�ч潰鏉� */
+ .editor-right {
+ overflow: hidden;
+ box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%);
+ flex-shrink: 0;
+
+ /* 灞炴�ч潰鏉块《閮細鍑忓皯鍐呰竟璺� */
+ :deep(.el-card__header) {
+ padding: 8px 16px;
+ }
+
+ /* 灞炴�ч潰鏉垮垎缁� */
+ :deep(.property-group) {
+ margin: 0 -20px;
+
+ &.el-card {
+ border: none;
+ }
+
+ /* 灞炴�у垎缁勫悕绉� */
+ .el-card__header {
+ padding: 8px 32px;
+ background: var(--el-bg-color-page);
+ border: none;
+ }
+
+ .el-card__body {
+ border: none;
+ }
+ }
+ }
+
+ /* 涓績鍖哄煙 */
+ .editor-center {
+ position: relative;
+ display: flex;
+ width: 100%;
+ margin: 16px 0 0;
+ overflow: hidden;
+ background-color: var(--app-content-bg-color);
+ flex: 1 1 0;
+ flex-direction: column;
+ justify-content: center;
+
+ /* 鎵嬫満椤堕儴 */
+ .editor-design-top {
+ display: flex;
+ width: $phone-width;
+ margin: 0 auto;
+ flex-direction: column;
+
+ /* 鎵嬫満椤堕儴鐘舵�佹爮 */
+ .status-bar {
+ width: $phone-width;
+ height: 20px;
+ background-color: #fff;
+ }
+ }
+
+ /* 鎵嬫満搴曢儴瀵艰埅 */
+ .editor-design-bottom {
+ width: $phone-width;
+ margin: 0 auto;
+ }
+
+ /* 鎵嬫満椤甸潰缂栬緫鍖哄煙 */
+ :deep(.editor-design-center) {
+ width: 100%;
+
+ /* 涓讳綋鍐呭 */
+ .phone-container {
+ position: relative;
+ width: $phone-width;
+ height: 100%;
+ margin: 0 auto;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+
+ .drag-area {
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+
+ /* 鍥哄畾甯冨眬鐨勭粍浠� 鎿嶄綔鎸夐挳鍖� */
+ .fixed-component-action-group {
+ position: absolute;
+ top: 0;
+ right: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ :deep(.el-tag) {
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
+ border: none;
+
+ .el-tag__content {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+
+ .el-icon {
+ margin-right: 4px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
--
Gitblit v1.8.0