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