Bladeren bron

!344 商城装修
Merge pull request !344 from 疯狂的世界/dev

芋道源码 1 jaar geleden
bovenliggende
commit
605f906056

+ 0 - 19
.env.base

@@ -1,19 +0,0 @@
-# 本地开发环境
-NODE_ENV=development
-
-VITE_DEV=true
-
-# 请求路径
-VITE_BASE_URL='http://127.0.0.1:48080'
-
-# 上传路径
-VITE_UPLOAD_URL='http://127.0.0.1:48080/admin-api/infra/file/upload'
-
-# 接口前缀
-VITE_API_BASEPATH=/dev-api
-
-# 接口地址
-VITE_API_URL=/admin-api
-
-# 打包路径
-VITE_BASE_PATH=/

+ 15 - 9
.env.dev

@@ -1,13 +1,13 @@
-# 开发环境
+# 开发环境:本地只启动前端项目,依赖开发环境(后端、APP)
 NODE_ENV=development
 
-VITE_DEV=false
+VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='http://localhost:48080'
+VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
 
 # 上传路径
-VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
+VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
 
 # 接口前缀
 VITE_API_BASEPATH=/dev-api
@@ -15,17 +15,23 @@ VITE_API_BASEPATH=/dev-api
 # 接口地址
 VITE_API_URL=/admin-api
 
-# 打包路径
-VITE_BASE_PATH=/
-
 # 是否删除debugger
-VITE_DROP_DEBUGGER=true
+VITE_DROP_DEBUGGER=false
 
 # 是否删除console.log
 VITE_DROP_CONSOLE=false
 
 # 是否sourcemap
-VITE_SOURCEMAP=false
+VITE_SOURCEMAP=true
+
+# 打包路径
+VITE_BASE_PATH=/
 
 # 输出路径
 VITE_OUT_DIR=dist
+
+# 商城H5会员端域名
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# 验证码的开关
+VITE_APP_CAPTCHA_ENABLE=false

+ 10 - 10
.env.front → .env.local-dev

@@ -1,13 +1,13 @@
-# 本地开发环境
+# 本地开发环境:本地启动所有项目(前端、后端、APP)时使用,不依赖外部环境
 NODE_ENV=development
 
 VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
+VITE_BASE_URL='http://localhost:48080'
 
 # 上传路径
-VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
+VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
 
 # 接口前缀
 VITE_API_BASEPATH=/dev-api
@@ -15,12 +15,6 @@ VITE_API_BASEPATH=/dev-api
 # 接口地址
 VITE_API_URL=/admin-api
 
-# 打包路径
-VITE_BASE_PATH=/
-
-# 项目本地运行端口号, 与.vscode/launch.json配合
-VITE_PORT=80
-
 # 是否删除debugger
 VITE_DROP_DEBUGGER=false
 
@@ -28,7 +22,13 @@ VITE_DROP_DEBUGGER=false
 VITE_DROP_CONSOLE=false
 
 # 是否sourcemap
-VITE_SOURCEMAP=true
+VITE_SOURCEMAP=false
+
+# 打包路径
+VITE_BASE_PATH=/
+
+# 商城H5会员端域名
+VITE_MALL_H5_DOMAIN='http://localhost:3000'
 
 # 验证码的开关
 VITE_APP_CAPTCHA_ENABLE=false

+ 5 - 2
.env.pro → .env.prod

@@ -1,4 +1,4 @@
-# 生产环境
+# 生产环境:只在打包时使用
 NODE_ENV=production
 
 VITE_DEV=false
@@ -28,4 +28,7 @@ VITE_SOURCEMAP=false
 VITE_BASE_PATH=/
 
 # 输出路径
-VITE_OUT_DIR=dist-pro
+VITE_OUT_DIR=dist-prod
+
+# 商城H5会员端域名
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

+ 4 - 1
.env.stage

@@ -1,4 +1,4 @@
-# 生产环境
+# 预发布环境:只在打包时使用
 NODE_ENV=production
 
 VITE_DEV=false
@@ -29,3 +29,6 @@ VITE_BASE_PATH='http://static-vue3.yudao.iocoder.cn/'
 
 # 输出路径
 VITE_OUT_DIR=dist-stage
+
+# 商城H5会员端域名
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

+ 5 - 2
.env.static → .env.test

@@ -1,4 +1,4 @@
-# 开发环境
+# 测试环境:只在打包时使用
 NODE_ENV=production
 
 VITE_DEV=false
@@ -28,4 +28,7 @@ VITE_SOURCEMAP=false
 VITE_BASE_PATH=/admin-ui-vue3/
 
 # 输出路径
-VITE_OUT_DIR=dist-dev
+VITE_OUT_DIR=dist-test
+
+# 商城H5会员端域名
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

+ 7 - 8
package.json

@@ -6,18 +6,17 @@
   "private": false,
   "scripts": {
     "i": "pnpm install",
-    "dev": "vite --mode base",
-    "front": "vite --mode front",
+    "local-dev": "vite --mode local-dev",
+    "dev": "vite --mode dev",
     "ts:check": "vue-tsc --noEmit",
-    "build:pro": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode pro",
+    "build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",
     "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
-    "build:base": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode base",
+    "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
     "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
-    "build:static": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode static",
-    "build:front": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode front",
-    "serve:pro": "vite preview --mode pro",
+    "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
     "serve:dev": "vite preview --mode dev",
-    "preview": "pnpm build:base && vite preview",
+    "serve:prod": "vite preview --mode prod",
+    "preview": "pnpm build:local-dev && vite preview",
     "clean": "npx rimraf node_modules",
     "clean:cache": "npx rimraf node_modules/.cache",
     "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",

+ 3 - 1
src/components/DiyEditor/components/ComponentLibrary.vue

@@ -82,7 +82,9 @@ watch(
 
 // 克隆组件
 const handleCloneComponent = (component: DiyComponent<any>) => {
-  return cloneDeep(component)
+  const instance = cloneDeep(component)
+  instance.uid = new Date().getTime()
+  return instance
 }
 </script>
 

+ 47 - 83
src/components/DiyEditor/components/mobile/Carousel/property.vue

@@ -39,87 +39,60 @@
         </el-form-item>
       </el-card>
       <el-card header="内容设置" class="property-group" shadow="never">
-        <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
-        <template v-if="formData.items[0]">
-          <draggable
-            :list="formData.items"
-            :force-fallback="true"
-            :animation="200"
-            handle=".drag-icon"
-            class="m-t-8px"
-            item-key="index"
-          >
-            <template #item="{ element, index }">
-              <div class="content mb-4px flex flex-col gap-4px rounded bg-gray-50 p-8px">
-                <div
-                  class="m--8px m-b-8px flex flex-row items-center justify-between bg-gray-100 p-8px"
-                >
-                  <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-                  <Icon
-                    icon="ep:delete"
-                    class="cursor-pointer text-red-5"
-                    @click="handleDeleteImage(index)"
-                    v-if="formData.items.length > 1"
-                  />
-                </div>
-                <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="50px">
-                  <el-radio-group v-model="element.type">
-                    <el-radio label="img">图片</el-radio>
-                    <el-radio label="video">视频</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-                <el-form-item
-                  label="图片"
-                  class="m-b-8px!"
-                  label-width="50px"
-                  v-if="element.type === 'img'"
-                >
-                  <UploadImg
-                    v-model="element.imgUrl"
-                    draggable="false"
-                    height="80px"
-                    width="100%"
-                    class="min-w-80px"
-                  />
-                </el-form-item>
-                <template v-else>
-                  <el-form-item label="封面" class="m-b-8px!" label-width="50px">
-                    <UploadImg
-                      v-model="element.imgUrl"
-                      draggable="false"
-                      height="80px"
-                      width="100%"
-                      class="min-w-80px"
-                    />
-                  </el-form-item>
-                  <el-form-item label="视频" class="m-b-8px!" label-width="50px">
-                    <UploadFile
-                      v-model="element.videoUrl"
-                      :file-type="['mp4']"
-                      :limit="1"
-                      :file-size="100"
-                      class="min-w-80px"
-                    />
-                  </el-form-item>
-                </template>
-                <el-form-item label="链接" class="m-b-8px!" label-width="50px">
-                  <AppLinkInput v-model="element.url" />
-                </el-form-item>
-              </div>
+        <Draggable v-model="formData.items" :empty-item="{ type: 'img' }">
+          <template #default="{ element }">
+            <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px">
+              <el-radio-group v-model="element.type">
+                <el-radio label="img">图片</el-radio>
+                <el-radio label="video">视频</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              label="图片"
+              class="m-b-8px!"
+              label-width="40px"
+              v-if="element.type === 'img'"
+            >
+              <UploadImg
+                v-model="element.imgUrl"
+                draggable="false"
+                height="80px"
+                width="100%"
+                class="min-w-80px"
+              />
+            </el-form-item>
+            <template v-else>
+              <el-form-item label="封面" class="m-b-8px!" label-width="40px">
+                <UploadImg
+                  v-model="element.imgUrl"
+                  draggable="false"
+                  height="80px"
+                  width="100%"
+                  class="min-w-80px"
+                />
+              </el-form-item>
+              <el-form-item label="视频" class="m-b-8px!" label-width="40px">
+                <UploadFile
+                  v-model="element.videoUrl"
+                  :file-type="['mp4']"
+                  :limit="1"
+                  :file-size="100"
+                  class="min-w-80px"
+                />
+              </el-form-item>
             </template>
-          </draggable>
-        </template>
-        <el-button @click="handleAddImage" type="primary" plain class="w-full">
-          添加图片
-        </el-button>
+            <el-form-item label="链接" class="m-b-8px!" label-width="40px">
+              <AppLinkInput v-model="element.url" />
+            </el-form-item>
+          </template>
+        </Draggable>
       </el-card>
     </el-form>
   </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import draggable from 'vuedraggable' //拖拽组件
-import { CarouselItemProperty, CarouselProperty } from './config'
+import { CarouselProperty } from './config'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 
 // 轮播图属性面板
@@ -128,15 +101,6 @@ defineOptions({ name: 'CarouselProperty' })
 const props = defineProps<{ modelValue: CarouselProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-// 添加图片
-const handleAddImage = () => {
-  formData.value.items.push({} as CarouselItemProperty)
-}
-// 删除图片
-const handleDeleteImage = (index: number) => {
-  formData.value.items.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 31 - 62
src/components/DiyEditor/components/mobile/MenuGrid/property.vue

@@ -9,72 +9,50 @@
         </el-radio-group>
       </el-form-item>
 
-      <el-text tag="p"> 菜单设置 </el-text>
-      <el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
-      <template v-if="formData.list.length">
-        <VueDraggable
-          class="m-t-8px"
-          :list="formData.list"
-          item-key="index"
-          handle=".drag-icon"
-          :forceFallback="true"
-          :animation="200"
-        >
-          <template #item="{ element, index }">
-            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
-              <div class="flex flex-row justify-between">
-                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
-              </div>
-              <el-form-item label="图标" prop="iconUrl">
-                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
-                  <template #tip> 建议尺寸:44 * 44 </template>
-                </UploadImg>
+      <el-card header="菜单设置" class="property-group" shadow="never">
+        <Draggable v-model="formData.list" :empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY">
+          <template #default="{ element }">
+            <el-form-item label="图标" prop="iconUrl">
+              <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+                <template #tip> 建议尺寸:44 * 44 </template>
+              </UploadImg>
+            </el-form-item>
+            <el-form-item label="标题" prop="title">
+              <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+            </el-form-item>
+            <el-form-item label="副标题" prop="subtitle">
+              <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+            </el-form-item>
+            <el-form-item label="链接" prop="url">
+              <AppLinkInput v-model="element.url" />
+            </el-form-item>
+            <el-form-item label="显示角标" prop="badge.show">
+              <el-switch v-model="element.badge.show" />
+            </el-form-item>
+            <template v-if="element.badge.show">
+              <el-form-item label="角标内容" prop="badge.text">
+                <InputWithColor
+                  v-model="element.badge.text"
+                  v-model:color="element.badge.textColor"
+                />
               </el-form-item>
-              <el-form-item label="标题" prop="title">
-                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+              <el-form-item label="背景颜色" prop="badge.bgColor">
+                <ColorInput v-model="element.badge.bgColor" />
               </el-form-item>
-              <el-form-item label="副标题" prop="subtitle">
-                <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
-              </el-form-item>
-              <el-form-item label="链接" prop="url">
-                <AppLinkInput v-model="element.url" />
-              </el-form-item>
-              <el-form-item label="显示角标" prop="badge.show">
-                <el-switch v-model="element.badge.show" />
-              </el-form-item>
-              <template v-if="element.badge.show">
-                <el-form-item label="角标内容" prop="badge.text">
-                  <InputWithColor
-                    v-model="element.badge.text"
-                    v-model:color="element.badge.textColor"
-                  />
-                </el-form-item>
-                <el-form-item label="背景颜色" prop="badge.bgColor">
-                  <ColorInput v-model="element.badge.bgColor" />
-                </el-form-item>
-              </template>
-            </div>
+            </template>
           </template>
-        </VueDraggable>
-      </template>
-      <el-form-item label-width="0">
-        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
-          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
-        </el-button>
-      </el-form-item>
+        </Draggable>
+      </el-card>
     </el-form>
   </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import VueDraggable from 'vuedraggable'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import {
   EMPTY_MENU_GRID_ITEM_PROPERTY,
   MenuGridProperty
 } from '@/components/DiyEditor/components/mobile/MenuGrid/config'
-import { cloneDeep } from 'lodash-es'
 
 /** 宫格导航属性面板 */
 defineOptions({ name: 'MenuGridProperty' })
@@ -82,15 +60,6 @@ defineOptions({ name: 'MenuGridProperty' })
 const props = defineProps<{ modelValue: MenuGridProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-/* 添加菜单 */
-const handleAddMenu = () => {
-  formData.value.list.push(cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY))
-}
-/* 删除菜单 */
-const handleDeleteMenu = (index: number) => {
-  formData.value.list.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 18 - 48
src/components/DiyEditor/components/mobile/MenuList/property.vue

@@ -5,55 +5,34 @@
 
     <!-- 表单 -->
     <el-form label-width="60px" :model="formData" class="m-t-8px">
-      <div v-if="formData.list.length">
-        <VueDraggable
-          :list="formData.list"
-          item-key="index"
-          handle=".drag-icon"
-          :forceFallback="true"
-          :animation="200"
-        >
-          <template #item="{ element, index }">
-            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
-              <div class="flex flex-row justify-between">
-                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
-              </div>
-              <el-form-item label="图标" prop="iconUrl">
-                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
-                  <template #tip> 建议尺寸:44 * 44 </template>
-                </UploadImg>
-              </el-form-item>
-              <el-form-item label="标题" prop="title">
-                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
-              </el-form-item>
-              <el-form-item label="副标题" prop="subtitle">
-                <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
-              </el-form-item>
-              <el-form-item label="链接" prop="url">
-                <AppLinkInput v-model="element.url" />
-              </el-form-item>
-            </div>
-          </template>
-        </VueDraggable>
-      </div>
-      <el-form-item label-width="0">
-        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
-          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
-        </el-button>
-      </el-form-item>
+      <Draggable v-model="formData.list" :empty-item="EMPTY_MENU_LIST_ITEM_PROPERTY">
+        <template #default="{ element }">
+          <el-form-item label="图标" prop="iconUrl">
+            <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+              <template #tip> 建议尺寸:44 * 44 </template>
+            </UploadImg>
+          </el-form-item>
+          <el-form-item label="标题" prop="title">
+            <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+          </el-form-item>
+          <el-form-item label="副标题" prop="subtitle">
+            <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+          </el-form-item>
+          <el-form-item label="链接" prop="url">
+            <AppLinkInput v-model="element.url" />
+          </el-form-item>
+        </template>
+      </Draggable>
     </el-form>
   </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import VueDraggable from 'vuedraggable'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import {
   EMPTY_MENU_LIST_ITEM_PROPERTY,
   MenuListProperty
 } from '@/components/DiyEditor/components/mobile/MenuList/config'
-import { cloneDeep } from 'lodash-es'
 
 /** 列表导航属性面板 */
 defineOptions({ name: 'MenuListProperty' })
@@ -61,15 +40,6 @@ defineOptions({ name: 'MenuListProperty' })
 const props = defineProps<{ modelValue: MenuListProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-/* 添加菜单 */
-const handleAddMenu = () => {
-  formData.value.list.push(cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY))
-}
-/* 删除菜单 */
-const handleDeleteMenu = (index: number) => {
-  formData.value.list.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 28 - 58
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue

@@ -22,63 +22,42 @@
         </el-radio-group>
       </el-form-item>
 
-      <el-text tag="p"> 菜单设置 </el-text>
-      <el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
-      <template v-if="formData.list.length">
-        <VueDraggable
-          class="m-t-8px"
-          :list="formData.list"
-          item-key="index"
-          handle=".drag-icon"
-          :forceFallback="true"
-          :animation="200"
-        >
-          <template #item="{ element, index }">
-            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
-              <div class="flex flex-row justify-between">
-                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
-              </div>
-              <el-form-item label="图标" prop="iconUrl">
-                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
-                  <template #tip> 建议尺寸:98 * 98 </template>
-                </UploadImg>
+      <el-card header="菜单设置" class="property-group" shadow="never">
+        <Draggable v-model="formData.list" :empty-item="cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY">
+          <template #default="{ element }">
+            <el-form-item label="图标" prop="iconUrl">
+              <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+                <template #tip> 建议尺寸:98 * 98 </template>
+              </UploadImg>
+            </el-form-item>
+            <el-form-item label="标题" prop="title">
+              <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+            </el-form-item>
+            <el-form-item label="链接" prop="url">
+              <AppLinkInput v-model="element.url" />
+            </el-form-item>
+            <el-form-item label="显示角标" prop="badge.show">
+              <el-switch v-model="element.badge.show" />
+            </el-form-item>
+            <template v-if="element.badge.show">
+              <el-form-item label="角标内容" prop="badge.text">
+                <InputWithColor
+                  v-model="element.badge.text"
+                  v-model:color="element.badge.textColor"
+                />
               </el-form-item>
-              <el-form-item label="标题" prop="title">
-                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+              <el-form-item label="背景颜色" prop="badge.bgColor">
+                <ColorInput v-model="element.badge.bgColor" />
               </el-form-item>
-              <el-form-item label="链接" prop="url">
-                <AppLinkInput v-model="element.url" />
-              </el-form-item>
-              <el-form-item label="显示角标" prop="badge.show">
-                <el-switch v-model="element.badge.show" />
-              </el-form-item>
-              <template v-if="element.badge.show">
-                <el-form-item label="角标内容" prop="badge.text">
-                  <InputWithColor
-                    v-model="element.badge.text"
-                    v-model:color="element.badge.textColor"
-                  />
-                </el-form-item>
-                <el-form-item label="背景颜色" prop="badge.bgColor">
-                  <ColorInput v-model="element.badge.bgColor" />
-                </el-form-item>
-              </template>
-            </div>
+            </template>
           </template>
-        </VueDraggable>
-      </template>
-      <el-form-item label-width="0">
-        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
-          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
-        </el-button>
-      </el-form-item>
+        </Draggable>
+      </el-card>
     </el-form>
   </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import VueDraggable from 'vuedraggable'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import {
   EMPTY_MENU_SWIPER_ITEM_PROPERTY,
@@ -92,15 +71,6 @@ defineOptions({ name: 'MenuSwiperProperty' })
 const props = defineProps<{ modelValue: MenuSwiperProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-/* 添加菜单 */
-const handleAddMenu = () => {
-  formData.value.list.push(cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY))
-}
-/* 删除菜单 */
-const handleDeleteMenu = (index: number) => {
-  formData.value.list.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 9 - 2
src/components/DiyEditor/components/mobile/NoticeBar/config.ts

@@ -1,4 +1,4 @@
-import { DiyComponent } from '@/components/DiyEditor/util'
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 
 /** 公告栏属性 */
 export interface NoticeBarProperty {
@@ -10,6 +10,8 @@ export interface NoticeBarProperty {
   backgroundColor: string
   // 文字颜色
   textColor: string
+  // 组件样式
+  style: ComponentStyle
 }
 
 /** 内容属性 */
@@ -34,6 +36,11 @@ export const component = {
       }
     ],
     backgroundColor: '#fff',
-    textColor: '#333'
+    textColor: '#333',
+    style: {
+      bgType: 'color',
+      bgColor: '#fff',
+      marginBottom: 8
+    } as ComponentStyle
   }
 } as DiyComponent<NoticeBarProperty>

+ 27 - 57
src/components/DiyEditor/components/mobile/NoticeBar/property.vue

@@ -1,58 +1,37 @@
 <template>
-  <el-form label-width="80px" :model="formData" :rules="rules">
-    <el-form-item label="公告图标" prop="iconUrl">
-      <UploadImg v-model="formData.iconUrl" height="48px">
-        <template #tip>建议尺寸:24 * 24</template>
-      </UploadImg>
-    </el-form-item>
-    <el-form-item label="背景颜色" prop="backgroundColor">
-      <ColorInput v-model="formData.backgroundColor" />
-    </el-form-item>
-    <el-form-item label="文字颜色" prop="文字颜色">
-      <ColorInput v-model="formData.textColor" />
-    </el-form-item>
-    <el-text tag="p"> 公告内容 </el-text>
-    <el-text type="info" size="small"> 拖动左上角的小圆点可以调整热词顺序 </el-text>
-    <template v-if="formData.contents.length">
-      <VueDraggable
-        :list="formData.contents"
-        item-key="index"
-        handle=".drag-icon"
-        :forceFallback="true"
-        :animation="200"
-        class="m-t-8px"
-      >
-        <template #item="{ element, index }">
-          <div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
-            <div class="flex flex-col items-start justify-between">
-              <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-              <Icon
-                icon="ep:delete"
-                class="cursor-pointer text-red-5"
-                @click="handleDeleteContent(index)"
-                v-if="formData.contents.length > 1"
-              />
-            </div>
-            <div class="w-full flex flex-col gap-8px">
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="80px" :model="formData" :rules="rules">
+      <el-form-item label="公告图标" prop="iconUrl">
+        <UploadImg v-model="formData.iconUrl" height="48px">
+          <template #tip>建议尺寸:24 * 24</template>
+        </UploadImg>
+      </el-form-item>
+      <el-form-item label="背景颜色" prop="backgroundColor">
+        <ColorInput v-model="formData.backgroundColor" />
+      </el-form-item>
+      <el-form-item label="文字颜色" prop="文字颜色">
+        <ColorInput v-model="formData.textColor" />
+      </el-form-item>
+
+      <el-card header="公告内容" class="property-group" shadow="never">
+        <Draggable v-model="formData.contents">
+          <template #default="{ element }">
+            <el-form-item label="公告" prop="text" label-width="40px">
               <el-input v-model="element.text" placeholder="请输入公告" />
+            </el-form-item>
+            <el-form-item label="链接" prop="url" label-width="40px">
               <AppLinkInput v-model="element.url" />
-            </div>
-          </div>
-        </template>
-      </VueDraggable>
-    </template>
-    <el-form-item label-width="0">
-      <el-button @click="handleAddContent" type="primary" plain class="m-t-8px w-full">
-        添加内容
-      </el-button>
-    </el-form-item>
-  </el-form>
+            </el-form-item>
+          </template>
+        </Draggable>
+      </el-card>
+    </el-form>
+  </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import { NoticeBarProperty, NoticeContentProperty } from './config'
+import { NoticeBarProperty } from './config'
 import { usePropertyForm } from '@/components/DiyEditor/util'
-import VueDraggable from 'vuedraggable'
 // 通知栏属性面板
 defineOptions({ name: 'NoticeBarProperty' })
 // 表单校验
@@ -63,15 +42,6 @@ const rules = {
 const props = defineProps<{ modelValue: NoticeBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-/* 添加公告 */
-const handleAddContent = () => {
-  formData.value.contents.push({} as NoticeContentProperty)
-}
-/* 删除公告 */
-const handleDeleteContent = (index: number) => {
-  formData.value.contents.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 51 - 77
src/components/DiyEditor/components/mobile/SearchBar/property.vue

@@ -1,81 +1,64 @@
 <template>
   <ComponentContainerProperty v-model="formData.style">
-    <el-text tag="p"> 搜索热词 </el-text>
-    <el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
-
     <!-- 表单 -->
     <el-form label-width="80px" :model="formData" class="m-t-8px">
-      <div v-if="formData.hotKeywords.length">
-        <VueDraggable
-          :list="formData.hotKeywords"
-          item-key="index"
-          handle=".drag-icon"
-          :forceFallback="true"
-          :animation="200"
-        >
-          <template #item="{ index }">
-            <div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px">
-              <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-              <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
-              <Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
-            </div>
+      <el-card header="搜索热词" class="property-group" shadow="never">
+        <Draggable v-model="formData.hotKeywords" :empty-item="''">
+          <template #default="{ index }">
+            <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
           </template>
-        </VueDraggable>
-      </div>
-      <el-form-item label-width="0">
-        <el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
-          添加热词
-        </el-button>
-      </el-form-item>
-      <el-form-item label="框体样式">
-        <el-radio-group v-model="formData!.borderRadius">
-          <el-tooltip content="方形" placement="top">
-            <el-radio-button :label="0">
-              <Icon icon="tabler:input-search" />
-            </el-radio-button>
-          </el-tooltip>
-          <el-tooltip content="圆形" placement="top">
-            <el-radio-button :label="10">
-              <Icon icon="iconoir:input-search" />
-            </el-radio-button>
-          </el-tooltip>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="提示文字" prop="placeholder">
-        <el-input v-model="formData.placeholder" />
-      </el-form-item>
-      <el-form-item label="文本位置" prop="placeholderPosition">
-        <el-radio-group v-model="formData!.placeholderPosition">
-          <el-tooltip content="居左" placement="top">
-            <el-radio-button label="left">
-              <Icon icon="ant-design:align-left-outlined" />
-            </el-radio-button>
-          </el-tooltip>
-          <el-tooltip content="居中" placement="top">
-            <el-radio-button label="center">
-              <Icon icon="ant-design:align-center-outlined" />
-            </el-radio-button>
-          </el-tooltip>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="扫一扫" prop="showScan">
-        <el-switch v-model="formData!.showScan" />
-      </el-form-item>
-      <el-form-item label="框体高度" prop="height">
-        <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
-      </el-form-item>
-      <el-form-item label="框体颜色" prop="backgroundColor">
-        <ColorInput v-model="formData.backgroundColor" />
-      </el-form-item>
-      <el-form-item class="lef" label="文本颜色" prop="textColor">
-        <ColorInput v-model="formData.textColor" />
-      </el-form-item>
+        </Draggable>
+      </el-card>
+      <el-card header="搜索样式" class="property-group" shadow="never">
+        <el-form-item label="框体样式">
+          <el-radio-group v-model="formData!.borderRadius">
+            <el-tooltip content="方形" placement="top">
+              <el-radio-button :label="0">
+                <Icon icon="tabler:input-search" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip content="圆形" placement="top">
+              <el-radio-button :label="10">
+                <Icon icon="iconoir:input-search" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="提示文字" prop="placeholder">
+          <el-input v-model="formData.placeholder" />
+        </el-form-item>
+        <el-form-item label="文本位置" prop="placeholderPosition">
+          <el-radio-group v-model="formData!.placeholderPosition">
+            <el-tooltip content="居左" placement="top">
+              <el-radio-button label="left">
+                <Icon icon="ant-design:align-left-outlined" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip content="居中" placement="top">
+              <el-radio-button label="center">
+                <Icon icon="ant-design:align-center-outlined" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="扫一扫" prop="showScan">
+          <el-switch v-model="formData!.showScan" />
+        </el-form-item>
+        <el-form-item label="框体高度" prop="height">
+          <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
+        </el-form-item>
+        <el-form-item label="框体颜色" prop="backgroundColor">
+          <ColorInput v-model="formData.backgroundColor" />
+        </el-form-item>
+        <el-form-item class="lef" label="文本颜色" prop="textColor">
+          <ColorInput v-model="formData.textColor" />
+        </el-form-item>
+      </el-card>
     </el-form>
   </ComponentContainerProperty>
 </template>
 
 <script setup lang="ts">
-import VueDraggable from 'vuedraggable'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
 
@@ -85,15 +68,6 @@ defineOptions({ name: 'SearchProperty' })
 const props = defineProps<{ modelValue: SearchProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
-
-/* 添加热词 */
-const handleAddHotWord = () => {
-  formData.value.hotKeywords.push('')
-}
-/* 删除热词 */
-const deleteHotWord = (index: number) => {
-  formData.value.hotKeywords.splice(index, 1)
-}
 </script>
 
 <style scoped lang="scss"></style>

+ 9 - 2
src/components/DiyEditor/components/mobile/TabBar/index.vue

@@ -12,7 +12,13 @@
       }"
     >
       <div v-for="(item, index) in property.items" :key="index" class="tab-bar-item">
-        <img :src="index === 0 ? item.activeIconUrl : item.iconUrl" alt="" />
+        <el-image :src="index === 0 ? item.activeIconUrl : item.iconUrl">
+          <template #error>
+            <div class="h-full w-full flex items-center justify-center">
+              <Icon icon="ep:picture" />
+            </div>
+          </template>
+        </el-image>
         <span :style="{ color: index === 0 ? property.style.activeColor : property.style.color }">
           {{ item.text }}
         </span>
@@ -48,7 +54,8 @@ defineProps<{ property: TabBarProperty }>()
       align-items: center;
       justify-content: center;
 
-      img {
+      :deep(img),
+      .el-icon {
         width: 26px;
         height: 26px;
         border-radius: 4px;

+ 28 - 73
src/components/DiyEditor/components/mobile/TabBar/property.vue

@@ -42,80 +42,44 @@
 
       <el-text tag="p">图标设置</el-text>
       <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 </el-text>
-      <draggable
-        :list="formData!.items"
-        item-key="index"
-        :forceFallback="true"
-        :animation="200"
-        handle=".drag-icon"
-        class="m-t-8px"
-      >
-        <template #item="{ element, index }">
-          <div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
-            <div class="flex flex-col items-start justify-between">
-              <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
-              <Icon
-                icon="ep:delete"
-                class="cursor-pointer text-red-5"
-                @click="handleDeleteItem(index)"
-                v-if="formData.items.length > 1"
+      <Draggable v-model="formData.items" :limit="5">
+        <template #default="{ element }">
+          <div class="m-b-8px flex items-center justify-around">
+            <div class="flex flex-col items-center justify-between">
+              <UploadImg
+                v-model="element.iconUrl"
+                width="40px"
+                height="40px"
+                :show-delete="false"
+                :show-btn-text="false"
               />
+              <el-text size="small">未选中</el-text>
             </div>
-            <div class="w-full flex flex-col">
-              <div class="m-b-8px flex items-center justify-around">
-                <div class="flex flex-col items-center justify-between">
-                  <UploadImg
-                    v-model="element.iconUrl"
-                    width="40px"
-                    height="40px"
-                    :show-delete="false"
-                    :show-btn-text="false"
-                  />
-                  <el-text size="small">默认图片</el-text>
-                </div>
-                <div>
-                  <UploadImg
-                    v-model="element.activeIconUrl"
-                    width="40px"
-                    height="40px"
-                    :show-delete="false"
-                    :show-btn-text="false"
-                  />
-                  <el-text>选中图片</el-text>
-                </div>
-              </div>
-              <el-form-item prop="text" label-width="0" class="m-b-8px!">
-                <el-input v-model="element.text" placeholder="请输入文字" />
-              </el-form-item>
-              <el-form-item prop="url" label-width="0" class="m-b-0!">
-                <AppLinkInput v-model="element.url" />
-              </el-form-item>
+            <div>
+              <UploadImg
+                v-model="element.activeIconUrl"
+                width="40px"
+                height="40px"
+                :show-delete="false"
+                :show-btn-text="false"
+              />
+              <el-text>已选中</el-text>
             </div>
           </div>
+          <el-form-item prop="text" label="文字" label-width="48px" class="m-b-8px!">
+            <el-input v-model="element.text" placeholder="请输入文字" />
+          </el-form-item>
+          <el-form-item prop="url" label="链接" label-width="48px" class="m-b-0!">
+            <AppLinkInput v-model="element.url" />
+          </el-form-item>
         </template>
-      </draggable>
-
-      <el-form-item label-width="0">
-        <!-- 添加导航按钮 -->
-        <el-tooltip content="最多添加5个">
-          <el-button
-            @click="handleAddItem"
-            class="m-b-16px w-full"
-            type="primary"
-            plain
-            :disabled="formData!.items.length >= 5"
-          >
-            添加导航
-          </el-button>
-        </el-tooltip>
-      </el-form-item>
+      </Draggable>
     </el-form>
   </div>
 </template>
 
 <script setup lang="ts">
-import draggable from 'vuedraggable' //拖拽组件
-import { TabBarItemProperty, TabBarProperty, THEME_LIST } from './config'
+import { TabBarProperty, THEME_LIST } from './config'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 // 底部导航栏
 defineOptions({ name: 'TabBarProperty' })
@@ -124,15 +88,6 @@ const props = defineProps<{ modelValue: TabBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
 const { formData } = usePropertyForm(props.modelValue, emit)
 
-/** 添加导航项 */
-const handleAddItem = () => {
-  formData?.value?.items?.push({} as TabBarItemProperty)
-}
-/** 删除导航项 */
-const handleDeleteItem = (index: number) => {
-  formData?.value?.items?.splice(index, 1)
-}
-
 // 要的主题
 const handleThemeChange = () => {
   const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)

+ 37 - 16
src/components/DiyEditor/index.vue

@@ -15,7 +15,7 @@
             <Icon icon="system-uicons:reset-alt" :size="24" />
           </el-button>
         </el-tooltip>
-        <el-tooltip content="预览">
+        <el-tooltip content="预览" v-if="previewUrl">
           <el-button @click="handlePreview">
             <Icon icon="ep:view" :size="24" />
           </el-button>
@@ -102,8 +102,8 @@
           <!-- 组件名称 -->
           <template #header>
             <div class="flex items-center gap-8px">
-              <Icon :icon="selectedComponent.icon" color="gray" />
-              <span>{{ selectedComponent.name }}</span>
+              <Icon :icon="selectedComponent?.icon" color="gray" />
+              <span>{{ selectedComponent?.name }}</span>
             </div>
           </template>
           <el-scrollbar
@@ -111,7 +111,8 @@
             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"
+              :is="selectedComponent?.id + 'Property'"
               v-model="selectedComponent.property"
             />
           </el-scrollbar>
@@ -119,6 +120,19 @@
       </el-aside>
     </el-container>
   </el-container>
+  <!-- 预览弹框 -->
+  <Dialog v-model="previewDialogVisible" title="预览" width="700">
+    <div class="flex justify-around">
+      <IFrame
+        class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!"
+        :src="previewUrl"
+      />
+      <div class="flex flex-col">
+        <el-text>手机扫码预览</el-text>
+        <Qrcode :text="previewUrl" logo="/logo.gif" />
+      </div>
+    </div>
+  </Dialog>
 </template>
 <script lang="ts">
 // 注册所有的组件
@@ -137,12 +151,12 @@ import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/confi
 import { 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 message = useMessage()
 // 左侧组件库
 const componentLibrary = ref()
 // 页面设置组件
@@ -159,20 +173,22 @@ const selectedComponentIndex = ref<number>(-1)
 // 组件列表
 const pageComponents = ref<DiyComponent<any>[]>([])
 // 定义属性
-const props = defineProps<{
+const props = defineProps({
   // 页面配置,支持Json字符串
-  modelValue: string | PageConfig
+  modelValue: oneOfType<string | PageConfig>([String, Object]).isRequired,
   // 标题
-  title: string
+  title: propTypes.string.def(''),
   // 组件库
-  libs: DiyComponentLibrary[]
+  libs: array<DiyComponentLibrary>(),
   // 是否显示顶部导航栏
-  showNavigationBar: boolean
+  showNavigationBar: propTypes.bool.def(true),
   // 是否显示底部导航菜单
-  showTabBar: boolean
+  showTabBar: propTypes.bool.def(false),
   // 是否显示页面配置
-  showPageConfig: boolean
-}>()
+  showPageConfig: propTypes.bool.def(true),
+  // 预览地址:提供了预览地址,才会显示预览按钮
+  previewUrl: propTypes.string.def('')
+})
 
 // 监听传入的页面配置
 watch(
@@ -281,6 +297,7 @@ const handleMoveComponent = (index: number, direction: number) => {
 /** 复制组件 */
 const handleCopyComponent = (index: number) => {
   const component = cloneDeep(pageComponents.value[index])
+  component.uid = new Date().getTime()
   pageComponents.value.splice(index + 1, 0, component)
 }
 /**
@@ -306,14 +323,18 @@ const handleDeleteComponent = (index: number) => {
 
 // 工具栏操作
 const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
+
+// 注入无感刷新页面函数
+const reload = inject<() => void>('reload')
 // 重置
 const handleReset = () => {
-  message.warning('开发中~')
+  if (reload) reload()
   emits('reset')
 }
 // 预览
+const previewDialogVisible = ref(false)
 const handlePreview = () => {
-  message.warning('开发中~')
+  previewDialogVisible.value = true
   emits('preview')
 }
 

+ 2 - 0
src/components/DiyEditor/util.ts

@@ -5,6 +5,8 @@ import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/
 
 // 页面装修组件
 export interface DiyComponent<T> {
+  // 用于区分同一种组件的不同实例
+  uid: number
   // 组件唯一标识
   id: string
   // 组件名称

+ 77 - 0
src/components/Draggable/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
+  <VueDraggable
+    :list="formData"
+    :force-fallback="true"
+    :animation="200"
+    handle=".drag-icon"
+    class="m-t-8px"
+    item-key="index"
+  >
+    <template #item="{ element, index }">
+      <div
+        class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px"
+      >
+        <!-- 操作按钮区 -->
+        <div class="m--8px m-b-4px flex flex-row items-center justify-between bg-gray-1 p-8px">
+          <el-tooltip content="拖动排序">
+            <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
+          </el-tooltip>
+          <el-tooltip content="删除">
+            <Icon
+              icon="ep:delete"
+              class="cursor-pointer text-red-5"
+              v-if="formData.length > 1"
+              @click="handleDelete(index)"
+            />
+          </el-tooltip>
+        </div>
+        <!-- 内容区 -->
+        <slot :element="element" :index="index"></slot>
+      </div>
+    </template>
+  </VueDraggable>
+  <el-tooltip :disabled="limit < 1" :content="`最多添加${limit}个`">
+    <el-button
+      type="primary"
+      plain
+      class="m-t-4px w-full"
+      :disabled="limit > 0 && formData.length >= limit"
+      @click="handleAdd"
+    >
+      <Icon icon="ep:plus" /><span>添加</span>
+    </el-button>
+  </el-tooltip>
+</template>
+
+<script setup lang="ts">
+// 拖拽组件
+import VueDraggable from 'vuedraggable'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import { any, array } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+import { cloneDeep } from 'lodash-es'
+
+// 拖拽组件封装
+defineOptions({ name: 'Draggable' })
+
+// 定义属性
+const props = defineProps({
+  // 绑定值
+  modelValue: array<any>().isRequired,
+  // 空的元素:点击添加按钮时,创建元素并添加到列表;默认为空对象
+  emptyItem: any<unknown>().def({}),
+  // 数量限制:默认为0,表示不限制
+  limit: propTypes.number.def(0)
+})
+// 定义事件
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+
+// 处理添加
+const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {}))
+// 处理删除
+const handleDelete = (index: number) => formData.value.splice(index, 1)
+</script>
+
+<style scoped lang="scss"></style>

+ 12 - 1
src/layout/components/AppView.vue

@@ -20,6 +20,17 @@ const getCaches = computed((): string[] => {
 })
 
 const tagsView = computed(() => appStore.getTagsView)
+
+//region 无感刷新
+const routerAlive = ref(true)
+// 无感刷新,防止出现页面闪烁白屏
+const reload = () => {
+  routerAlive.value = false
+  nextTick(() => (routerAlive.value = true))
+}
+// 为组件后代提供刷新方法
+provide('reload', reload)
+//endregion
 </script>
 
 <template>
@@ -49,7 +60,7 @@ const tagsView = computed(() => appStore.getTagsView)
       }
     ]"
   >
-    <router-view>
+    <router-view v-if="routerAlive">
       <template #default="{ Component, route }">
         <keep-alive :include="getCaches">
           <component :is="Component" :key="route.fullPath" />

+ 24 - 21
src/views/Home/Index.vue

@@ -2,10 +2,12 @@
   <div>
     <el-card shadow="never">
       <el-skeleton :loading="loading" animated>
-        <el-row :gutter="20" justify="space-between">
+        <el-row :gutter="16" justify="space-between">
           <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
             <div class="flex items-center">
-              <img :src="avatar" alt="" class="mr-20px h-70px w-70px rounded-[50%]" />
+              <el-avatar :src="avatar" :size="70" class="mr-16px">
+                <img src="@/assets/imgs/avatar.gif" alt="" />
+              </el-avatar>
               <div>
                 <div class="text-20px">
                   {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
@@ -19,7 +21,7 @@
           <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
             <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
               <div class="px-8px text-right">
-                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.project') }}</div>
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
                 <CountTo
                   class="text-20px"
                   :start-val="0"
@@ -29,7 +31,7 @@
               </div>
               <el-divider direction="vertical" />
               <div class="px-8px text-right">
-                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
                 <CountTo
                   class="text-20px"
                   :start-val="0"
@@ -39,7 +41,7 @@
               </div>
               <el-divider direction="vertical" border-style="dashed" />
               <div class="px-8px text-right">
-                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.access') }}</div>
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
                 <CountTo
                   class="text-20px"
                   :start-val="0"
@@ -54,8 +56,8 @@
     </el-card>
   </div>
 
-  <el-row class="mt-5px" :gutter="20" justify="space-between">
-    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-10px">
+  <el-row class="mt-8px" :gutter="8" justify="space-between">
+    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
       <el-card shadow="never">
         <template #header>
           <div class="h-3 flex justify-between">
@@ -76,11 +78,11 @@
             >
               <el-card shadow="hover">
                 <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-10px" />
+                  <Icon :icon="item.icon" :size="25" class="mr-8px" />
                   <span class="text-16px">{{ item.name }}</span>
                 </div>
-                <div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div>
-                <div class="mt-20px flex justify-between text-12px text-gray-400">
+                <div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
+                <div class="mt-16px flex justify-between text-12px text-gray-400">
                   <span>{{ item.personal }}</span>
                   <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
                 </div>
@@ -90,18 +92,18 @@
         </el-skeleton>
       </el-card>
 
-      <el-card shadow="never" class="mt-5px">
+      <el-card shadow="never" class="mt-8px">
         <el-skeleton :loading="loading" animated>
           <el-row :gutter="20" justify="space-between">
             <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-10px">
+              <el-card shadow="hover" class="mb-8px">
                 <el-skeleton :loading="loading" animated>
                   <Echart :options="pieOptionsData" :height="280" />
                 </el-skeleton>
               </el-card>
             </el-col>
             <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-10px">
+              <el-card shadow="hover" class="mb-8px">
                 <el-skeleton :loading="loading" animated>
                   <Echart :options="barOptionsData" :height="280" />
                 </el-skeleton>
@@ -111,7 +113,7 @@
         </el-skeleton>
       </el-card>
     </el-col>
-    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-10px">
+    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
       <el-card shadow="never">
         <template #header>
           <div class="h-3 flex justify-between">
@@ -120,9 +122,9 @@
         </template>
         <el-skeleton :loading="loading" animated>
           <el-row>
-            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-10px">
+            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
               <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-10px" />
+                <Icon :icon="item.icon" class="mr-8px" />
                 <el-link type="default" :underline="false" @click="setWatermark(item.name)">
                   {{ item.name }}
                 </el-link>
@@ -131,7 +133,7 @@
           </el-row>
         </el-skeleton>
       </el-card>
-      <el-card shadow="never" class="mt-10px">
+      <el-card shadow="never" class="mt-8px">
         <template #header>
           <div class="h-3 flex justify-between">
             <span>{{ t('workplace.notice') }}</span>
@@ -141,14 +143,16 @@
         <el-skeleton :loading="loading" animated>
           <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
             <div class="flex items-center">
-              <img :src="avatar" alt="" class="mr-20px h-35px w-35px rounded-[50%]" />
+              <el-avatar :src="avatar" :size="35" class="mr-16px">
+                <img src="@/assets/imgs/avatar.gif" alt="" />
+              </el-avatar>
               <div>
                 <div class="text-14px">
                   <Highlight :keys="item.keys.map((v) => t(v))">
                     {{ item.type }} : {{ item.title }}
                   </Highlight>
                 </div>
-                <div class="mt-15px text-12px text-gray-400">
+                <div class="mt-16px text-12px text-gray-400">
                   {{ formatTime(item.date, 'yyyy-MM-dd') }}
                 </div>
               </div>
@@ -167,7 +171,6 @@ import { formatTime } from '@/utils'
 
 import { useUserStore } from '@/store/modules/user'
 import { useWatermark } from '@/hooks/web/useWatermark'
-import avatarImg from '@/assets/imgs/avatar.gif'
 import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
 import { pieOptions, barOptions } from './echarts-data'
 
@@ -177,7 +180,7 @@ const { t } = useI18n()
 const userStore = useUserStore()
 const { setWatermark } = useWatermark()
 const loading = ref(true)
-const avatar = userStore.getUser.avatar ? userStore.getUser.avatar : avatarImg
+const avatar = userStore.getUser.avatar
 const username = userStore.getUser.nickname
 const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
 // 获取统计数

+ 4 - 1
src/views/mall/home/components/ShortcutCard.vue

@@ -10,7 +10,10 @@
         class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
         @click="handleMenuClick(menu.routerName)"
       >
-        <div :class="menu.bgColor" class="rounded p-3 text-white">
+        <div
+          :class="menu.bgColor"
+          class="h-48px w-48px flex items-center justify-center rounded text-white"
+        >
           <Icon :icon="menu.icon" class="text-7.5!" />
         </div>
         <span>{{ menu.name }}</span>

+ 0 - 3
src/views/mall/promotion/diy/page/decorate.vue

@@ -4,9 +4,6 @@
     v-model="formData.property"
     :title="formData.name"
     :libs="PAGE_LIBS"
-    :show-page-config="true"
-    :show-navigation-bar="true"
-    :show-tab-bar="false"
     @save="submitForm"
   />
 </template>

+ 39 - 4
src/views/mall/promotion/diy/template/decorate.vue

@@ -7,7 +7,9 @@
     :show-page-config="selectedTemplateItem !== 0"
     :show-tab-bar="selectedTemplateItem === 0"
     :show-navigation-bar="selectedTemplateItem !== 0"
+    :preview-url="previewUrl"
     @save="submitForm"
+    @reset="handleEditorReset"
   >
     <template #toolBarLeft>
       <el-radio-group
@@ -29,6 +31,7 @@ import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
+import { toNumber } from 'lodash-es'
 
 /** 装修模板表单 */
 defineOptions({ name: 'DiyTemplateDecorate' })
@@ -48,6 +51,8 @@ const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>()
 const formRef = ref() // 表单 Ref
 // 当前编辑的属性
 const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>()
+// 商城H5预览地址
+const previewUrl = ref('')
 
 // 获取详情
 const getPageDetail = async (id: any) => {
@@ -55,6 +60,10 @@ const getPageDetail = async (id: any) => {
   try {
     formData.value = await DiyTemplateApi.getDiyTemplateProperty(id)
     currentFormData.value = formData.value
+
+    // 拼接手机预览链接
+    const domain = import.meta.env.VITE_MALL_H5_DOMAIN
+    previewUrl.value = `${domain}/#/pages/index/index?templateId=${formData.value.id}`
   } finally {
     formLoading.value = false
   }
@@ -115,17 +124,43 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
+// 重置时记录当前编辑的页面
+const handleEditorReset = () => storePageIndex()
+
+//#region 无感刷新
+// 记录标识
+const DIY_PAGE_INDEX_KEY = 'diy_page_index'
+// 1. 记录
+const storePageIndex = () =>
+  sessionStorage.setItem(DIY_PAGE_INDEX_KEY, `${selectedTemplateItem.value}`)
+// 2. 恢复
+const recoverPageIndex = () => {
+  // 恢复重置前的页面,默认是第一个页面
+  const pageIndex = toNumber(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0
+  // 移除标记
+  sessionStorage.removeItem(DIY_PAGE_INDEX_KEY)
+  // 切换页面
+  if (pageIndex !== selectedTemplateItem.value) {
+    selectedTemplateItem.value = pageIndex
+    handleTemplateItemChange()
+  }
+}
+//#endregion
+
 /** 初始化 **/
 const { currentRoute } = useRouter() // 路由
 const { delView } = useTagsViewStore() // 视图操作
-const route = useRoute()
-onMounted(() => {
+onMounted(async () => {
   resetForm()
-  if (!route.params.id) {
+  if (!currentRoute.value.params.id) {
     message.warning('参数错误,页面编号不能为空!')
     delView(unref(currentRoute))
     return
   }
-  getPageDetail(route.params.id)
+
+  // 查询详情
+  await getPageDetail(currentRoute.value.params.id)
+  // 恢复重置前的页面
+  recoverPageIndex()
 })
 </script>