feat: (web-ele)新增颜色输入框组件并优化图片上传组件
- 新增 ColorInput 组件用于颜色选择 - 重构 ImageUpload 组件,增加编辑和删除功能 - 更新 DIY 编辑器相关组件,优化用户体验 - 添加商城 H5 预览地址配置 - 优化导航栏单元格属性配置pull/194/head
							parent
							
								
									1f155fa7c5
								
							
						
					
					
						commit
						e7fc44715b
					
				|  | @ -21,7 +21,8 @@ | ||||||
|     // CSS 变量提示 |     // CSS 变量提示 | ||||||
|     "vunguyentuan.vscode-css-variables", |     "vunguyentuan.vscode-css-variables", | ||||||
|     // 在 package.json 中显示 PNPM catalog 的版本 |     // 在 package.json 中显示 PNPM catalog 的版本 | ||||||
|     "antfu.pnpm-catalog-lens" |     "antfu.pnpm-catalog-lens", | ||||||
|  |     "augment.vscode-augment" | ||||||
|   ], |   ], | ||||||
|   "unwantedRecommendations": [ |   "unwantedRecommendations": [ | ||||||
|     // 和 volar 冲突 |     // 和 volar 冲突 | ||||||
|  |  | ||||||
|  | @ -19,3 +19,5 @@ VITE_INJECT_APP_LOADING=true | ||||||
| VITE_APP_DEFAULT_USERNAME=admin | VITE_APP_DEFAULT_USERNAME=admin | ||||||
| # 默认登录密码 | # 默认登录密码 | ||||||
| VITE_APP_DEFAULT_PASSWORD=admin123 | VITE_APP_DEFAULT_PASSWORD=admin123 | ||||||
|  | 
 | ||||||
|  | VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' | ||||||
|  |  | ||||||
|  | @ -21,3 +21,5 @@ VITE_INJECT_APP_LOADING=true | ||||||
| 
 | 
 | ||||||
| # 打包后是否生成dist.zip | # 打包后是否生成dist.zip | ||||||
| VITE_ARCHIVER=true | VITE_ARCHIVER=true | ||||||
|  | 
 | ||||||
|  | VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.5 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.7 KiB | 
|  | @ -1,4 +1,5 @@ | ||||||
| import { createApp, watchEffect } from 'vue'; | import { createApp, watchEffect } from 'vue'; | ||||||
|  | import VueDOMPurifyHTML from 'vue-dompurify-html'; | ||||||
| 
 | 
 | ||||||
| import { registerAccessDirective } from '@vben/access'; | import { registerAccessDirective } from '@vben/access'; | ||||||
| import { registerLoadingDirective } from '@vben/common-ui'; | import { registerLoadingDirective } from '@vben/common-ui'; | ||||||
|  | @ -34,7 +35,7 @@ async function bootstrap(namespace: string) { | ||||||
|   //   zIndex: 2000,
 |   //   zIndex: 2000,
 | ||||||
|   // });
 |   // });
 | ||||||
|   const app = createApp(App); |   const app = createApp(App); | ||||||
| 
 |   app.use(VueDOMPurifyHTML); | ||||||
|   // 注册Element Plus提供的v-loading指令
 |   // 注册Element Plus提供的v-loading指令
 | ||||||
|   app.directive('loading', ElLoading.directive); |   app.directive('loading', ElLoading.directive); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ import { nextTick, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { getUrlNumberValue } from '@vben/utils'; | import { getUrlNumberValue } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElScrollbar } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import ProductCategorySelect from '#/views/mall/product/category/components/product-category-select.vue'; | import ProductCategorySelect from '#/views/mall/product/category/components/product-category-select.vue'; | ||||||
| 
 | 
 | ||||||
| import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'; | import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'; | ||||||
|  | @ -28,7 +30,6 @@ const dialogVisible = ref(false); | ||||||
| const open = (link: string) => { | const open = (link: string) => { | ||||||
|   activeAppLink.value.path = link; |   activeAppLink.value.path = link; | ||||||
|   dialogVisible.value = true; |   dialogVisible.value = true; | ||||||
| 
 |  | ||||||
|   // 滚动到当前的链接 |   // 滚动到当前的链接 | ||||||
|   const group = APP_LINK_GROUP_LIST.find((group) => |   const group = APP_LINK_GROUP_LIST.find((group) => | ||||||
|     group.links.some((linkItem) => { |     group.links.some((linkItem) => { | ||||||
|  | @ -127,7 +128,7 @@ const scrollToGroupBtn = (group: string) => { | ||||||
| 
 | 
 | ||||||
| // 是否为相同的链接(不比较参数,只比较链接) | // 是否为相同的链接(不比较参数,只比较链接) | ||||||
| const isSameLink = (link1: string, link2: string) => { | const isSameLink = (link1: string, link2: string) => { | ||||||
|   return link1.split('?')[0] === link2.split('?')[0]; |   return link2 ? link1.split('?')[0] === link2.split('?')[0] : false; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // 详情选择对话框 | // 详情选择对话框 | ||||||
|  | @ -154,10 +155,10 @@ const handleProductCategorySelected = (id: number) => { | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <Dialog v-model="dialogVisible" title="选择链接" width="65%"> |   <el-dialog v-model="dialogVisible" title="选择链接" width="65%"> | ||||||
|     <div class="h-500px gap-8px flex"> |     <div class="flex h-[500px] gap-2"> | ||||||
|       <!-- 左侧分组列表 --> |       <!-- 左侧分组列表 --> | ||||||
|       <el-scrollbar |       <ElScrollbar | ||||||
|         wrap-class="h-full" |         wrap-class="h-full" | ||||||
|         ref="groupScrollbar" |         ref="groupScrollbar" | ||||||
|         view-class="flex flex-col" |         view-class="flex flex-col" | ||||||
|  | @ -165,7 +166,7 @@ const handleProductCategorySelected = (id: number) => { | ||||||
|         <el-button |         <el-button | ||||||
|           v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" |           v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" | ||||||
|           :key="groupIndex" |           :key="groupIndex" | ||||||
|           class="m-r-16px m-l-0px! justify-start! w-90px" |           class="ml-0 mr-4 w-[90px] justify-start" | ||||||
|           :class="[{ active: activeGroup === group.name }]" |           :class="[{ active: activeGroup === group.name }]" | ||||||
|           ref="groupBtnRefs" |           ref="groupBtnRefs" | ||||||
|           :text="activeGroup !== group.name" |           :text="activeGroup !== group.name" | ||||||
|  | @ -174,9 +175,9 @@ const handleProductCategorySelected = (id: number) => { | ||||||
|         > |         > | ||||||
|           {{ group.name }} |           {{ group.name }} | ||||||
|         </el-button> |         </el-button> | ||||||
|       </el-scrollbar> |       </ElScrollbar> | ||||||
|       <!-- 右侧链接列表 --> |       <!-- 右侧链接列表 --> | ||||||
|       <el-scrollbar |       <ElScrollbar | ||||||
|         class="h-full flex-1" |         class="h-full flex-1" | ||||||
|         @scroll="handleScroll" |         @scroll="handleScroll" | ||||||
|         ref="linkScrollbar" |         ref="linkScrollbar" | ||||||
|  | @ -196,7 +197,7 @@ const handleProductCategorySelected = (id: number) => { | ||||||
|             :show-after="300" |             :show-after="300" | ||||||
|           > |           > | ||||||
|             <el-button |             <el-button | ||||||
|               class="m-b-8px m-r-8px m-l-0px!" |               class="mb-2 ml-0 mr-2" | ||||||
|               :type=" |               :type=" | ||||||
|                 isSameLink(appLink.path, activeAppLink.path) |                 isSameLink(appLink.path, activeAppLink.path) | ||||||
|                   ? 'primary' |                   ? 'primary' | ||||||
|  | @ -208,16 +209,16 @@ const handleProductCategorySelected = (id: number) => { | ||||||
|             </el-button> |             </el-button> | ||||||
|           </el-tooltip> |           </el-tooltip> | ||||||
|         </div> |         </div> | ||||||
|       </el-scrollbar> |       </ElScrollbar> | ||||||
|     </div> |     </div> | ||||||
|     <!-- 底部对话框操作按钮 --> |     <!-- 底部对话框操作按钮 --> | ||||||
|     <template #footer> |     <template #footer> | ||||||
|       <el-button type="primary" @click="handleSubmit">确 定</el-button> |       <el-button type="primary" @click="handleSubmit">确 定</el-button> | ||||||
|       <el-button @click="dialogVisible = false">取 消</el-button> |       <el-button @click="dialogVisible = false">取 消</el-button> | ||||||
|     </template> |     </template> | ||||||
|   </Dialog> |   </el-dialog> | ||||||
|   <Dialog v-model="detailSelectDialog.visible" title="" width="50%"> |   <el-dialog v-model="detailSelectDialog.visible" title="" width="50%"> | ||||||
|     <el-form class="min-h-200px"> |     <el-form class="min-h-[200px]"> | ||||||
|       <el-form-item |       <el-form-item | ||||||
|         label="选择分类" |         label="选择分类" | ||||||
|         v-if=" |         v-if=" | ||||||
|  | @ -231,6 +232,10 @@ const handleProductCategorySelected = (id: number) => { | ||||||
|         /> |         /> | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
|     </el-form> |     </el-form> | ||||||
|   </Dialog> |   </el-dialog> | ||||||
| </template> | </template> | ||||||
| <style lang="scss" scoped></style> | <style lang="scss" scoped> | ||||||
|  | :deep(.el-button + .el-button) { | ||||||
|  |   margin-left: 0 !important; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -1,12 +1,17 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { propTypes } from '@/utils/propTypes'; | import { ref, watch } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import AppLinkSelectDialog from './app-link-select-dialog.vue'; | ||||||
| 
 | 
 | ||||||
| // APP 链接输入框 | // APP 链接输入框 | ||||||
| defineOptions({ name: 'AppLinkInput' }); | defineOptions({ name: 'AppLinkInput' }); | ||||||
| // 定义属性 | // 定义属性 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   // 当前选中的链接 |   // 当前选中的链接 | ||||||
|   modelValue: propTypes.string.def(''), |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
| // setter | // setter | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
|  |  | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { PREDEFINE_COLORS } from '#/utils/constants'; | ||||||
|  | 
 | ||||||
|  | // 颜色输入框 | ||||||
|  | defineOptions({ name: 'ColorInput' }); | ||||||
|  | 
 | ||||||
|  | const props = defineProps({ | ||||||
|  |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | const emit = defineEmits(['update:modelValue']); | ||||||
|  | const color = computed({ | ||||||
|  |   get: () => { | ||||||
|  |     return props.modelValue; | ||||||
|  |   }, | ||||||
|  |   set: (val: string) => { | ||||||
|  |     emit('update:modelValue', val); | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <el-input v-model="color"> | ||||||
|  |     <template #prepend> | ||||||
|  |       <el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" /> | ||||||
|  |     </template> | ||||||
|  |   </el-input> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | :deep(.el-input-group__prepend) { | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -6,11 +6,16 @@ import type { | ||||||
| 
 | 
 | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { ElButton, ElTooltip } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import { components } from '#/components/diy-editor/components/mobile'; | ||||||
|  | import VerticalButtonGroup from '#/components/vertical-button-group/index.vue'; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * 组件容器:目前在中间部分 |  * 组件容器:目前在中间部分 | ||||||
|  * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式 |  * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式 | ||||||
|  */ |  */ | ||||||
| defineOptions({ name: 'ComponentContainer' }); | defineOptions({ name: 'ComponentContainer', components }); | ||||||
| 
 | 
 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   component: { |   component: { | ||||||
|  | @ -29,6 +34,10 @@ const props = defineProps({ | ||||||
|     type: Boolean, |     type: Boolean, | ||||||
|     default: false, |     default: false, | ||||||
|   }, |   }, | ||||||
|  |   showToolbar: { | ||||||
|  |     type: Boolean, | ||||||
|  |     default: false, | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const emits = defineEmits<{ | const emits = defineEmits<{ | ||||||
|  | @ -106,34 +115,37 @@ const handleDeleteComponent = () => { | ||||||
|         {{ component.name }} |         {{ component.name }} | ||||||
|       </div> |       </div> | ||||||
|       <!-- 右侧:组件操作工具栏 --> |       <!-- 右侧:组件操作工具栏 --> | ||||||
|       <div class="component-toolbar" v-if="component.name && active"> |       <div | ||||||
|  |         class="component-toolbar" | ||||||
|  |         v-if="showToolbar && component.name && active" | ||||||
|  |       > | ||||||
|         <VerticalButtonGroup type="primary"> |         <VerticalButtonGroup type="primary"> | ||||||
|           <el-tooltip content="上移" placement="right"> |           <ElTooltip content="上移" placement="right"> | ||||||
|             <el-button |             <ElButton | ||||||
|               :disabled="!canMoveUp" |               :disabled="!canMoveUp" | ||||||
|               @click.stop="handleMoveComponent(-1)" |               @click.stop="handleMoveComponent(-1)" | ||||||
|             > |             > | ||||||
|               <Icon icon="ep:arrow-up" /> |               <Icon icon="ep:arrow-up" /> | ||||||
|             </el-button> |             </ElButton> | ||||||
|           </el-tooltip> |           </ElTooltip> | ||||||
|           <el-tooltip content="下移" placement="right"> |           <ElTooltip content="下移" placement="right"> | ||||||
|             <el-button |             <ElButton | ||||||
|               :disabled="!canMoveDown" |               :disabled="!canMoveDown" | ||||||
|               @click.stop="handleMoveComponent(1)" |               @click.stop="handleMoveComponent(1)" | ||||||
|             > |             > | ||||||
|               <Icon icon="ep:arrow-down" /> |               <Icon icon="ep:arrow-down" /> | ||||||
|             </el-button> |             </ElButton> | ||||||
|           </el-tooltip> |           </ElTooltip> | ||||||
|           <el-tooltip content="复制" placement="right"> |           <ElTooltip content="复制" placement="right"> | ||||||
|             <el-button @click.stop="handleCopyComponent()"> |             <ElButton @click.stop="handleCopyComponent()"> | ||||||
|               <Icon icon="ep:copy-document" /> |               <Icon icon="ep:copy-document" /> | ||||||
|             </el-button> |             </ElButton> | ||||||
|           </el-tooltip> |           </ElTooltip> | ||||||
|           <el-tooltip content="删除" placement="right"> |           <ElTooltip content="删除" placement="right"> | ||||||
|             <el-button @click.stop="handleDeleteComponent()"> |             <ElButton @click.stop="handleDeleteComponent()"> | ||||||
|               <Icon icon="ep:delete" /> |               <Icon icon="ep:delete" /> | ||||||
|             </el-button> |             </ElButton> | ||||||
|           </el-tooltip> |           </ElTooltip> | ||||||
|         </VerticalButtonGroup> |         </VerticalButtonGroup> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,20 @@ | ||||||
| import type { ComponentStyle } from '#/components/diy-editor/util'; | import type { ComponentStyle } from '#/components/diy-editor/util'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadio, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElTabPane, | ||||||
|  |   ElTabs, | ||||||
|  |   ElTree, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 组件容器属性:目前右边部分 |  * 组件容器属性:目前右边部分 | ||||||
|  | @ -110,48 +124,54 @@ const handleSliderChange = (prop: string) => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <el-tabs stretch> |   <ElTabs stretch> | ||||||
|     <!-- 每个组件的自定义内容 --> |     <!-- 每个组件的自定义内容 --> | ||||||
|     <el-tab-pane label="内容" v-if="$slots.default"> |     <ElTabPane label="内容" v-if="$slots.default"> | ||||||
|       <slot></slot> |       <slot></slot> | ||||||
|     </el-tab-pane> |     </ElTabPane> | ||||||
| 
 | 
 | ||||||
|     <!-- 每个组件的通用内容 --> |     <!-- 每个组件的通用内容 --> | ||||||
|     <el-tab-pane label="样式" lazy> |     <ElTabPane label="样式" lazy> | ||||||
|       <el-card header="组件样式" class="property-group"> |       <ElCard header="组件样式" class="property-group"> | ||||||
|         <el-form :model="formData" label-width="80px"> |         <ElForm :model="formData" label-width="80px"> | ||||||
|           <el-form-item label="组件背景" prop="bgType"> |           <ElFormItem label="组件背景" prop="bgType"> | ||||||
|             <el-radio-group v-model="formData.bgType"> |             <ElRadioGroup v-model="formData.bgType"> | ||||||
|               <el-radio value="color">纯色</el-radio> |               <ElRadio value="color">纯色</ElRadio> | ||||||
|               <el-radio value="img">图片</el-radio> |               <ElRadio value="img">图片</ElRadio> | ||||||
|             </el-radio-group> |             </ElRadioGroup> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item |           <ElFormItem | ||||||
|             label="选择颜色" |             label="选择颜色" | ||||||
|             prop="bgColor" |             prop="bgColor" | ||||||
|             v-if="formData.bgType === 'color'" |             v-if="formData.bgType === 'color'" | ||||||
|           > |           > | ||||||
|             <ColorInput v-model="formData.bgColor" /> |             <ColorInput v-model="formData.bgColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="上传图片" prop="bgImg" v-else> |           <ElFormItem label="上传图片" prop="bgImg" v-else> | ||||||
|             <UploadImg v-model="formData.bgImg" :limit="1"> |             <UploadImg | ||||||
|  |               v-model="formData.bgImg" | ||||||
|  |               :limit="1" | ||||||
|  |               :show-description="false" | ||||||
|  |             > | ||||||
|               <template #tip>建议宽度 750px</template> |               <template #tip>建议宽度 750px</template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-tree |           <ElTree | ||||||
|             :data="treeData" |             :data="treeData" | ||||||
|             :expand-on-click-node="false" |             :expand-on-click-node="false" | ||||||
|             default-expand-all |             default-expand-all | ||||||
|           > |           > | ||||||
|             <template #default="{ node, data }"> |             <template #default="{ node, data }"> | ||||||
|               <el-form-item |               <ElFormItem | ||||||
|                 :label="data.label" |                 :label="data.label" | ||||||
|                 :prop="data.prop" |                 :prop="data.prop" | ||||||
|                 :label-width="node.level === 1 ? '80px' : '62px'" |                 :label-width="node.level === 1 ? '80px' : '62px'" | ||||||
|                 class="m-b-0! w-full" |                 class="m-b-0! w-full" | ||||||
|               > |               > | ||||||
|                 <el-slider |                 <ElSlider | ||||||
|                   v-model="formData[data.prop as keyof ComponentStyle]" |                   v-model=" | ||||||
|  |                     formData[data.prop as keyof ComponentStyle] as number | ||||||
|  |                   " | ||||||
|                   :max="100" |                   :max="100" | ||||||
|                   :min="0" |                   :min="0" | ||||||
|                   show-input |                   show-input | ||||||
|  | @ -159,14 +179,14 @@ const handleSliderChange = (prop: string) => { | ||||||
|                   :show-input-controls="false" |                   :show-input-controls="false" | ||||||
|                   @input="handleSliderChange(data.prop)" |                   @input="handleSliderChange(data.prop)" | ||||||
|                 /> |                 /> | ||||||
|               </el-form-item> |               </ElFormItem> | ||||||
|             </template> |             </template> | ||||||
|           </el-tree> |           </ElTree> | ||||||
|           <slot name="style" :style="formData"></slot> |           <slot name="style" :style="formData"></slot> | ||||||
|         </el-form> |         </ElForm> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-tab-pane> |     </ElTabPane> | ||||||
|   </el-tabs> |   </ElTabs> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
|  |  | ||||||
|  | @ -6,8 +6,10 @@ import type { | ||||||
| 
 | 
 | ||||||
| import { reactive, watch } from 'vue'; | import { reactive, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
| import { cloneDeep } from '@vben/utils'; | import { cloneDeep } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElAside, ElCollapse, ElCollapseItem, ElScrollbar } from 'element-plus'; | ||||||
| import draggable from 'vuedraggable'; | import draggable from 'vuedraggable'; | ||||||
| 
 | 
 | ||||||
| import { componentConfigs } from '../components/mobile/index'; | import { componentConfigs } from '../components/mobile/index'; | ||||||
|  | @ -63,10 +65,10 @@ const handleCloneComponent = (component: DiyComponent<any>) => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <el-aside class="editor-left" width="261px"> |   <ElAside class="editor-left" width="261px"> | ||||||
|     <el-scrollbar> |     <ElScrollbar> | ||||||
|       <el-collapse v-model="extendGroups"> |       <ElCollapse v-model="extendGroups"> | ||||||
|         <el-collapse-item |         <ElCollapseItem | ||||||
|           v-for="group in groups" |           v-for="group in groups" | ||||||
|           :key="group.name" |           :key="group.name" | ||||||
|           :name="group.name" |           :name="group.name" | ||||||
|  | @ -87,16 +89,16 @@ const handleCloneComponent = (component: DiyComponent<any>) => { | ||||||
|               <div> |               <div> | ||||||
|                 <div class="drag-placement">组件放置区域</div> |                 <div class="drag-placement">组件放置区域</div> | ||||||
|                 <div class="component"> |                 <div class="component"> | ||||||
|                   <Icon :icon="element.icon" :size="32" /> |                   <IconifyIcon :icon="element.icon" :size="32" /> | ||||||
|                   <span class="mt-4px text-12px">{{ element.name }}</span> |                   <span class="mt-4px text-12px">{{ element.name }}</span> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             </template> |             </template> | ||||||
|           </draggable> |           </draggable> | ||||||
|         </el-collapse-item> |         </ElCollapseItem> | ||||||
|       </el-collapse> |       </ElCollapse> | ||||||
|     </el-scrollbar> |     </ElScrollbar> | ||||||
|   </el-aside> |   </ElAside> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
|  |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| import { components } from '../components/mobile/index'; |  | ||||||
| 
 |  | ||||||
| export default { |  | ||||||
|   components: { ...components }, |  | ||||||
| }; |  | ||||||
|  | @ -3,6 +3,10 @@ import type { CarouselProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElCarousel, ElCarouselItem, ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| /** 轮播图 */ | /** 轮播图 */ | ||||||
| defineOptions({ name: 'Carousel' }); | defineOptions({ name: 'Carousel' }); | ||||||
| 
 | 
 | ||||||
|  | @ -16,13 +20,13 @@ const handleIndexChange = (index: number) => { | ||||||
| <template> | <template> | ||||||
|   <!-- 无图片 --> |   <!-- 无图片 --> | ||||||
|   <div |   <div | ||||||
|     class="h-250px bg-gray-3 flex items-center justify-center" |     class="flex h-[250px] items-center justify-center bg-gray-300" | ||||||
|     v-if="property.items.length === 0" |     v-if="property.items.length === 0" | ||||||
|   > |   > | ||||||
|     <Icon icon="tdesign:image" class="text-gray-8 text-120px!" /> |     <IconifyIcon icon="tdesign:image" class="text-[120px] text-gray-800" /> | ||||||
|   </div> |   </div> | ||||||
|   <div v-else class="relative"> |   <div v-else class="relative"> | ||||||
|     <el-carousel |     <ElCarousel | ||||||
|       height="174px" |       height="174px" | ||||||
|       :type="property.type === 'card' ? 'card' : ''" |       :type="property.type === 'card' ? 'card' : ''" | ||||||
|       :autoplay="property.autoplay" |       :autoplay="property.autoplay" | ||||||
|  | @ -30,13 +34,13 @@ const handleIndexChange = (index: number) => { | ||||||
|       :indicator-position="property.indicator === 'number' ? 'none' : undefined" |       :indicator-position="property.indicator === 'number' ? 'none' : undefined" | ||||||
|       @change="handleIndexChange" |       @change="handleIndexChange" | ||||||
|     > |     > | ||||||
|       <el-carousel-item v-for="(item, index) in property.items" :key="index"> |       <ElCarouselItem v-for="(item, index) in property.items" :key="index"> | ||||||
|         <el-image class="h-full w-full" :src="item.imgUrl" /> |         <ElImage class="h-full w-full" :src="item.imgUrl" /> | ||||||
|       </el-carousel-item> |       </ElCarouselItem> | ||||||
|     </el-carousel> |     </ElCarousel> | ||||||
|     <div |     <div | ||||||
|       v-if="property.indicator === 'number'" |       v-if="property.indicator === 'number'" | ||||||
|       class="bottom-10px right-10px p-x-8px p-y-2px text-10px absolute rounded-xl bg-black text-white opacity-40" |       class="absolute bottom-[10px] right-[10px] rounded-xl bg-black px-[8px] py-[2px] text-[10px] text-white opacity-40" | ||||||
|     > |     > | ||||||
|       {{ currentIndex }} / {{ property.items.length }} |       {{ currentIndex }} / {{ property.items.length }} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,26 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { CarouselProperty } from './config'; | import type { CarouselProperty } from './config'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  |   ElText, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import AppLinkInput from '#/components/app-link-input/index.vue'; | ||||||
|  | import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue'; | ||||||
|  | import Draggable from '#/components/draggable/index.vue'; | ||||||
|  | import UploadFile from '#/components/upload/file-upload.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| // 轮播图属性面板 | // 轮播图属性面板 | ||||||
| defineOptions({ name: 'CarouselProperty' }); | defineOptions({ name: 'CarouselProperty' }); | ||||||
|  | @ -13,33 +32,33 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData"> |     <ElForm label-width="80px" :model="formData"> | ||||||
|       <el-card header="样式设置" class="property-group" shadow="never"> |       <ElCard header="样式设置" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="样式" prop="type"> |         <ElFormItem label="样式" prop="type"> | ||||||
|           <el-radio-group v-model="formData.type"> |           <ElRadioGroup v-model="formData.type"> | ||||||
|             <el-tooltip class="item" content="默认" placement="bottom"> |             <ElTooltip class="item" content="默认" placement="bottom"> | ||||||
|               <el-radio-button value="default"> |               <ElRadioButton value="default"> | ||||||
|                 <Icon icon="system-uicons:carousel" /> |                 <IconifyIcon icon="system-uicons:carousel" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="卡片" placement="bottom"> |             <ElTooltip class="item" content="卡片" placement="bottom"> | ||||||
|               <el-radio-button value="card"> |               <ElRadioButton value="card"> | ||||||
|                 <Icon icon="ic:round-view-carousel" /> |                 <IconifyIcon icon="ic:round-view-carousel" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="指示器" prop="indicator"> |         <ElFormItem label="指示器" prop="indicator"> | ||||||
|           <el-radio-group v-model="formData.indicator"> |           <ElRadioGroup v-model="formData.indicator"> | ||||||
|             <el-radio value="dot">小圆点</el-radio> |             <ElRadio value="dot">小圆点</ElRadio> | ||||||
|             <el-radio value="number">数字</el-radio> |             <ElRadio value="number">数字</ElRadio> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="是否轮播" prop="autoplay"> |         <ElFormItem label="是否轮播" prop="autoplay"> | ||||||
|           <el-switch v-model="formData.autoplay" /> |           <ElSwitch v-model="formData.autoplay" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay"> |         <ElFormItem label="播放间隔" prop="interval" v-if="formData.autoplay"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.interval" |             v-model="formData.interval" | ||||||
|             :max="10" |             :max="10" | ||||||
|             :min="0.5" |             :min="0.5" | ||||||
|  | @ -48,24 +67,24 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|           <el-text type="info">单位:秒</el-text> |           <ElText type="info">单位:秒</ElText> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="内容设置" class="property-group" shadow="never"> |       <ElCard header="内容设置" class="property-group" shadow="never"> | ||||||
|         <Draggable v-model="formData.items" :empty-item="{ type: 'img' }"> |         <Draggable v-model="formData.items" :empty-item="{ type: 'img' }"> | ||||||
|           <template #default="{ element }"> |           <template #default="{ element }"> | ||||||
|             <el-form-item |             <ElFormItem | ||||||
|               label="类型" |               label="类型" | ||||||
|               prop="type" |               prop="type" | ||||||
|               class="m-b-8px!" |               class="m-b-8px!" | ||||||
|               label-width="40px" |               label-width="40px" | ||||||
|             > |             > | ||||||
|               <el-radio-group v-model="element.type"> |               <ElRadioGroup v-model="element.type"> | ||||||
|                 <el-radio value="img">图片</el-radio> |                 <ElRadio value="img">图片</ElRadio> | ||||||
|                 <el-radio value="video">视频</el-radio> |                 <ElRadio value="video">视频</ElRadio> | ||||||
|               </el-radio-group> |               </ElRadioGroup> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <el-form-item |             <ElFormItem | ||||||
|               label="图片" |               label="图片" | ||||||
|               class="m-b-8px!" |               class="m-b-8px!" | ||||||
|               label-width="40px" |               label-width="40px" | ||||||
|  | @ -77,19 +96,21 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|                 height="80px" |                 height="80px" | ||||||
|                 width="100%" |                 width="100%" | ||||||
|                 class="min-w-80px" |                 class="min-w-80px" | ||||||
|  |                 :show-description="false" | ||||||
|               /> |               /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <template v-else> |             <template v-else> | ||||||
|               <el-form-item label="封面" class="m-b-8px!" label-width="40px"> |               <ElFormItem label="封面" class="m-b-8px!" label-width="40px"> | ||||||
|                 <UploadImg |                 <UploadImg | ||||||
|                   v-model="element.imgUrl" |                   v-model="element.imgUrl" | ||||||
|                   draggable="false" |                   draggable="false" | ||||||
|  |                   :show-description="false" | ||||||
|                   height="80px" |                   height="80px" | ||||||
|                   width="100%" |                   width="100%" | ||||||
|                   class="min-w-80px" |                   class="min-w-80px" | ||||||
|                 /> |                 /> | ||||||
|               </el-form-item> |               </ElFormItem> | ||||||
|               <el-form-item label="视频" class="m-b-8px!" label-width="40px"> |               <ElFormItem label="视频" class="m-b-8px!" label-width="40px"> | ||||||
|                 <UploadFile |                 <UploadFile | ||||||
|                   v-model="element.videoUrl" |                   v-model="element.videoUrl" | ||||||
|                   :file-type="['mp4']" |                   :file-type="['mp4']" | ||||||
|  | @ -97,15 +118,15 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|                   :file-size="100" |                   :file-size="100" | ||||||
|                   class="min-w-80px" |                   class="min-w-80px" | ||||||
|                 /> |                 /> | ||||||
|               </el-form-item> |               </ElFormItem> | ||||||
|             </template> |             </template> | ||||||
|             <el-form-item label="链接" class="m-b-8px!" label-width="40px"> |             <ElFormItem label="链接" class="m-b-8px!" label-width="40px"> | ||||||
|               <AppLinkInput v-model="element.url" /> |               <AppLinkInput v-model="element.url" /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|           </template> |           </template> | ||||||
|         </Draggable> |         </Draggable> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTe | ||||||
| 
 | 
 | ||||||
| import { onMounted, ref, watch } from 'vue'; | import { onMounted, ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { ElScrollbar } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate'; | import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|  | @ -64,9 +66,9 @@ onMounted(() => { | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> |   <ElScrollbar class="z-10 min-h-[30px]" wrap-class="w-full" ref="containerRef"> | ||||||
|     <div |     <div | ||||||
|       class="text-12px flex flex-row" |       class="flex flex-row text-xs" | ||||||
|       :style="{ |       :style="{ | ||||||
|         gap: `${property.space}px`, |         gap: `${property.space}px`, | ||||||
|         width: scrollbarWidth, |         width: scrollbarWidth, | ||||||
|  | @ -87,9 +89,9 @@ onMounted(() => { | ||||||
|         <!-- 布局1:1列--> |         <!-- 布局1:1列--> | ||||||
|         <div |         <div | ||||||
|           v-if="property.columns === 1" |           v-if="property.columns === 1" | ||||||
|           class="m-l-16px p-8px flex flex-row justify-between" |           class="ml-4 flex flex-row justify-between p-2" | ||||||
|         > |         > | ||||||
|           <div class="gap-4px flex flex-col justify-evenly"> |           <div class="flex flex-col justify-evenly gap-1"> | ||||||
|             <!-- 优惠值 --> |             <!-- 优惠值 --> | ||||||
|             <CouponDiscount :coupon="coupon" /> |             <CouponDiscount :coupon="coupon" /> | ||||||
|             <!-- 优惠描述 --> |             <!-- 优惠描述 --> | ||||||
|  | @ -99,7 +101,7 @@ onMounted(() => { | ||||||
|           </div> |           </div> | ||||||
|           <div class="flex flex-col justify-evenly"> |           <div class="flex flex-col justify-evenly"> | ||||||
|             <div |             <div | ||||||
|               class="rounded-20px p-x-8px p-y-2px" |               class="rounded-full px-2 py-0.5" | ||||||
|               :style="{ |               :style="{ | ||||||
|                 color: property.button.color, |                 color: property.button.color, | ||||||
|                 background: property.button.bgColor, |                 background: property.button.bgColor, | ||||||
|  | @ -112,9 +114,9 @@ onMounted(() => { | ||||||
|         <!-- 布局2:2列--> |         <!-- 布局2:2列--> | ||||||
|         <div |         <div | ||||||
|           v-else-if="property.columns === 2" |           v-else-if="property.columns === 2" | ||||||
|           class="m-l-16px p-8px flex flex-row justify-between" |           class="ml-4 flex flex-row justify-between p-2" | ||||||
|         > |         > | ||||||
|           <div class="gap-4px flex flex-col justify-evenly"> |           <div class="flex flex-col justify-evenly gap-1"> | ||||||
|             <!-- 优惠值 --> |             <!-- 优惠值 --> | ||||||
|             <CouponDiscount :coupon="coupon" /> |             <CouponDiscount :coupon="coupon" /> | ||||||
|             <!-- 优惠描述 --> |             <!-- 优惠描述 --> | ||||||
|  | @ -127,7 +129,7 @@ onMounted(() => { | ||||||
|           </div> |           </div> | ||||||
|           <div class="flex flex-col"> |           <div class="flex flex-col"> | ||||||
|             <div |             <div | ||||||
|               class="w-20px rounded-20px p-x-2px p-y-8px h-full text-center" |               class="h-full w-5 rounded-full px-0.5 py-2 text-center" | ||||||
|               :style="{ |               :style="{ | ||||||
|                 color: property.button.color, |                 color: property.button.color, | ||||||
|                 background: property.button.bgColor, |                 background: property.button.bgColor, | ||||||
|  | @ -138,16 +140,13 @@ onMounted(() => { | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <!-- 布局3:3列--> |         <!-- 布局3:3列--> | ||||||
|         <div |         <div v-else class="flex flex-col items-center justify-around gap-1 p-1"> | ||||||
|           v-else |  | ||||||
|           class="gap-4px p-4px flex flex-col items-center justify-around" |  | ||||||
|         > |  | ||||||
|           <!-- 优惠值 --> |           <!-- 优惠值 --> | ||||||
|           <CouponDiscount :coupon="coupon" /> |           <CouponDiscount :coupon="coupon" /> | ||||||
|           <!-- 优惠描述 --> |           <!-- 优惠描述 --> | ||||||
|           <CouponDiscountDesc :coupon="coupon" /> |           <CouponDiscountDesc :coupon="coupon" /> | ||||||
|           <div |           <div | ||||||
|             class="rounded-20px p-x-8px p-y-2px" |             class="rounded-full px-2 py-0.5" | ||||||
|             :style="{ |             :style="{ | ||||||
|               color: property.button.color, |               color: property.button.color, | ||||||
|               background: property.button.bgColor, |               background: property.button.bgColor, | ||||||
|  | @ -158,6 +157,6 @@ onMounted(() => { | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </el-scrollbar> |   </ElScrollbar> | ||||||
| </template> | </template> | ||||||
| <style scoped lang="scss"></style> | <style scoped lang="scss"></style> | ||||||
|  |  | ||||||
|  | @ -5,11 +5,23 @@ import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTe | ||||||
| 
 | 
 | ||||||
| import { ref, watch } from 'vue'; | import { ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
| import { floatToFixed2 } from '@vben/utils'; | import { floatToFixed2 } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate'; | import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate'; | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| import { | import { | ||||||
|   CouponTemplateTakeTypeEnum, |   CouponTemplateTakeTypeEnum, | ||||||
|   PromotionDiscountTypeEnum, |   PromotionDiscountTypeEnum, | ||||||
|  | @ -52,15 +64,15 @@ watch( | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData"> |     <ElForm label-width="80px" :model="formData"> | ||||||
|       <el-card header="优惠券列表" class="property-group" shadow="never"> |       <ElCard header="优惠券列表" class="property-group" shadow="never"> | ||||||
|         <div |         <div | ||||||
|           v-for="(coupon, index) in couponList" |           v-for="(coupon, index) in couponList" | ||||||
|           :key="index" |           :key="index" | ||||||
|           class="flex items-center justify-between" |           class="flex items-center justify-between" | ||||||
|         > |         > | ||||||
|           <el-text size="large" truncated>{{ coupon.name }}</el-text> |           <ElText size="large" truncated>{{ coupon.name }}</ElText> | ||||||
|           <el-text type="info" truncated> |           <ElText type="info" truncated> | ||||||
|             <span v-if="coupon.usePrice > 0"> |             <span v-if="coupon.usePrice > 0"> | ||||||
|               满{{ floatToFixed2(coupon.usePrice) }}元, |               满{{ floatToFixed2(coupon.usePrice) }}元, | ||||||
|             </span> |             </span> | ||||||
|  | @ -72,58 +84,59 @@ watch( | ||||||
|               减{{ floatToFixed2(coupon.discountPrice) }}元 |               减{{ floatToFixed2(coupon.discountPrice) }}元 | ||||||
|             </span> |             </span> | ||||||
|             <span v-else> 打{{ coupon.discountPercent }}折 </span> |             <span v-else> 打{{ coupon.discountPercent }}折 </span> | ||||||
|           </el-text> |           </ElText> | ||||||
|         </div> |         </div> | ||||||
|         <el-form-item label-width="0"> |         <ElFormItem label-width="0"> | ||||||
|           <el-button |           <ElButton | ||||||
|             @click="handleAddCoupon" |             @click="handleAddCoupon" | ||||||
|             type="primary" |             type="primary" | ||||||
|             plain |             plain | ||||||
|             class="m-t-8px w-full" |             class="m-t-8px w-full" | ||||||
|           > |           > | ||||||
|             <Icon icon="ep:plus" class="mr-5px" /> 添加 |             <IconifyIcon icon="ep:plus" class="mr-5px" /> 添加 | ||||||
|           </el-button> |           </ElButton> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="优惠券样式" class="property-group" shadow="never"> |       <ElCard header="优惠券样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="列数" prop="type"> |         <ElFormItem label="列数" prop="type"> | ||||||
|           <el-radio-group v-model="formData.columns"> |           <ElRadioGroup v-model="formData.columns"> | ||||||
|             <el-tooltip class="item" content="一列" placement="bottom"> |             <ElTooltip class="item" content="一列" placement="bottom"> | ||||||
|               <el-radio-button :value="1"> |               <ElRadioButton :value="1"> | ||||||
|                 <Icon icon="fluent:text-column-one-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-one-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="二列" placement="bottom"> |             <ElTooltip class="item" content="二列" placement="bottom"> | ||||||
|               <el-radio-button :value="2"> |               <ElRadioButton :value="2"> | ||||||
|                 <Icon icon="fluent:text-column-two-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="三列" placement="bottom"> |             <ElTooltip class="item" content="三列" placement="bottom"> | ||||||
|               <el-radio-button :value="3"> |               <ElRadioButton :value="3"> | ||||||
|                 <Icon icon="fluent:text-column-three-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-three-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="背景图片" prop="bgImg"> |         <ElFormItem label="背景图片" prop="bgImg"> | ||||||
|           <UploadImg |           <UploadImg | ||||||
|             v-model="formData.bgImg" |             v-model="formData.bgImg" | ||||||
|             height="80px" |             height="80px" | ||||||
|             width="100%" |             width="100%" | ||||||
|             class="min-w-160px" |             class="min-w-160px" | ||||||
|  |             :show-description="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="文字颜色" prop="textColor"> |         <ElFormItem label="文字颜色" prop="textColor"> | ||||||
|           <ColorInput v-model="formData.textColor" /> |           <ColorInput v-model="formData.textColor" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="按钮背景" prop="button.bgColor"> |         <ElFormItem label="按钮背景" prop="button.bgColor"> | ||||||
|           <ColorInput v-model="formData.button.bgColor" /> |           <ColorInput v-model="formData.button.bgColor" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="按钮文字" prop="button.color"> |         <ElFormItem label="按钮文字" prop="button.color"> | ||||||
|           <ColorInput v-model="formData.button.color" /> |           <ColorInput v-model="formData.button.color" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="间隔" prop="space"> |         <ElFormItem label="间隔" prop="space"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.space" |             v-model="formData.space" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -131,9 +144,9 @@ watch( | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
|   <!-- 优惠券选择 --> |   <!-- 优惠券选择 --> | ||||||
|   <CouponSelect |   <CouponSelect | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ import type { FloatingActionButtonProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { ElMessage } from 'element-plus'; | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElImage, ElMessage } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| /** 悬浮按钮 */ | /** 悬浮按钮 */ | ||||||
| defineOptions({ name: 'FloatingActionButton' }); | defineOptions({ name: 'FloatingActionButton' }); | ||||||
|  | @ -23,7 +25,7 @@ const handleActive = (index: number) => { | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="bottom-32px z-12 gap-12px absolute right-[calc(50%-375px/2+32px)] flex items-center" |     class="absolute bottom-8 right-[calc(50%-375px/2+32px)] z-20 flex items-center gap-3" | ||||||
|     :class="[ |     :class="[ | ||||||
|       { |       { | ||||||
|         'flex-row': property.direction === 'horizontal', |         'flex-row': property.direction === 'horizontal', | ||||||
|  | @ -38,16 +40,16 @@ const handleActive = (index: number) => { | ||||||
|         class="flex flex-col items-center" |         class="flex flex-col items-center" | ||||||
|         @click="handleActive(index)" |         @click="handleActive(index)" | ||||||
|       > |       > | ||||||
|         <el-image :src="item.imgUrl" fit="contain" class="h-27px w-27px"> |         <ElImage :src="item.imgUrl" fit="contain" class="h-7 w-7"> | ||||||
|           <template #error> |           <template #error> | ||||||
|             <div class="flex h-full w-full items-center justify-center"> |             <div class="flex h-full w-full items-center justify-center"> | ||||||
|               <Icon icon="ep:picture" :color="item.textColor" /> |               <IconifyIcon icon="ep:picture" :color="item.textColor" /> | ||||||
|             </div> |             </div> | ||||||
|           </template> |           </template> | ||||||
|         </el-image> |         </ElImage> | ||||||
|         <span |         <span | ||||||
|           v-if="property.showText" |           v-if="property.showText" | ||||||
|           class="mt-4px text-12px" |           class="mt-1 text-xs" | ||||||
|           :style="{ color: item.textColor }" |           :style="{ color: item.textColor }" | ||||||
|         > |         > | ||||||
|           {{ item.text }} |           {{ item.text }} | ||||||
|  | @ -56,7 +58,11 @@ const handleActive = (index: number) => { | ||||||
|     </template> |     </template> | ||||||
|     <!-- todo: @owen 使用APP主题色 --> |     <!-- todo: @owen 使用APP主题色 --> | ||||||
|     <el-button type="primary" size="large" circle @click="handleToggleFab"> |     <el-button type="primary" size="large" circle @click="handleToggleFab"> | ||||||
|       <Icon icon="ep:plus" class="fab-icon" :class="[{ active: expanded }]" /> |       <IconifyIcon | ||||||
|  |         icon="ep:plus" | ||||||
|  |         class="fab-icon" | ||||||
|  |         :class="[{ active: expanded }]" | ||||||
|  |       /> | ||||||
|     </el-button> |     </el-button> | ||||||
|   </div> |   </div> | ||||||
|   <!-- 模态背景:展开时显示,点击后折叠 --> |   <!-- 模态背景:展开时显示,点击后折叠 --> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,19 @@ | ||||||
| import type { FloatingActionButtonProperty } from './config'; | import type { FloatingActionButtonProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadio, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSwitch, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import AppLinkInput from '#/components/app-link-input/index.vue'; | ||||||
|  | import Draggable from '#/components/draggable/index.vue'; | ||||||
|  | import InputWithColor from '#/components/input-with-color/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| // 悬浮按钮属性面板 | // 悬浮按钮属性面板 | ||||||
| defineOptions({ name: 'FloatingActionButtonProperty' }); | defineOptions({ name: 'FloatingActionButtonProperty' }); | ||||||
|  | @ -12,37 +25,42 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <el-form label-width="80px" :model="formData"> |   <ElForm label-width="80px" :model="formData"> | ||||||
|     <el-card header="按钮配置" class="property-group" shadow="never"> |     <ElCard header="按钮配置" class="property-group" shadow="never"> | ||||||
|       <el-form-item label="展开方向" prop="direction"> |       <ElFormItem label="展开方向" prop="direction"> | ||||||
|         <el-radio-group v-model="formData.direction"> |         <ElRadioGroup v-model="formData.direction"> | ||||||
|           <el-radio value="vertical">垂直</el-radio> |           <ElRadio value="vertical">垂直</ElRadio> | ||||||
|           <el-radio value="horizontal">水平</el-radio> |           <ElRadio value="horizontal">水平</ElRadio> | ||||||
|         </el-radio-group> |         </ElRadioGroup> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="显示文字" prop="showText"> |       <ElFormItem label="显示文字" prop="showText"> | ||||||
|         <el-switch v-model="formData.showText" /> |         <ElSwitch v-model="formData.showText" /> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|     </el-card> |     </ElCard> | ||||||
|     <el-card header="按钮列表" class="property-group" shadow="never"> |     <ElCard header="按钮列表" class="property-group" shadow="never"> | ||||||
|       <Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }"> |       <Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }"> | ||||||
|         <template #default="{ element, index }"> |         <template #default="{ element, index }"> | ||||||
|           <el-form-item label="图标" :prop="`list[${index}].imgUrl`"> |           <ElFormItem label="图标" :prop="`list[${index}].imgUrl`"> | ||||||
|             <UploadImg v-model="element.imgUrl" height="56px" width="56px" /> |             <UploadImg | ||||||
|           </el-form-item> |               v-model="element.imgUrl" | ||||||
|           <el-form-item label="文字" :prop="`list[${index}].text`"> |               height="56px" | ||||||
|  |               width="56px" | ||||||
|  |               :show-description="false" | ||||||
|  |             /> | ||||||
|  |           </ElFormItem> | ||||||
|  |           <ElFormItem label="文字" :prop="`list[${index}].text`"> | ||||||
|             <InputWithColor |             <InputWithColor | ||||||
|               v-model="element.text" |               v-model="element.text" | ||||||
|               v-model:color="element.textColor" |               v-model:color="element.textColor" | ||||||
|             /> |             /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="跳转链接" :prop="`list[${index}].url`"> |           <ElFormItem label="跳转链接" :prop="`list[${index}].url`"> | ||||||
|             <AppLinkInput v-model="element.url" /> |             <AppLinkInput v-model="element.url" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </Draggable> |       </Draggable> | ||||||
|     </el-card> |     </ElCard> | ||||||
|   </el-form> |   </ElForm> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="scss"></style> | <style scoped lang="scss"></style> | ||||||
|  |  | ||||||
|  | @ -6,6 +6,10 @@ import type { HotZoneItemProperty } from '#/components/diy-editor/components/mob | ||||||
| 
 | 
 | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElButton, ElDialog, ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import { | import { | ||||||
|   CONTROL_DOT_LIST, |   CONTROL_DOT_LIST, | ||||||
|   CONTROL_TYPE_ENUM, |   CONTROL_TYPE_ENUM, | ||||||
|  | @ -172,14 +176,14 @@ const handleAppLinkChange = (appLink: AppLink) => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <Dialog |   <ElDialog | ||||||
|     v-model="dialogVisible" |     v-model="dialogVisible" | ||||||
|     title="设置热区" |     title="设置热区" | ||||||
|     width="780" |     width="780" | ||||||
|     @close="handleClose" |     @close="handleClose" | ||||||
|   > |   > | ||||||
|     <div ref="container" class="w-750px relative h-full"> |     <div ref="container" class="w-750px relative h-full"> | ||||||
|       <el-image |       <ElImage | ||||||
|         :src="imgUrl" |         :src="imgUrl" | ||||||
|         class="w-750px pointer-events-none h-full select-none" |         class="w-750px pointer-events-none h-full select-none" | ||||||
|       /> |       /> | ||||||
|  | @ -199,7 +203,7 @@ const handleAppLinkChange = (appLink: AppLink) => { | ||||||
|         <span class="pointer-events-none select-none">{{ |         <span class="pointer-events-none select-none">{{ | ||||||
|           item.name || '双击选择链接' |           item.name || '双击选择链接' | ||||||
|         }}</span> |         }}</span> | ||||||
|         <Icon |         <IconifyIcon | ||||||
|           icon="ep:close" |           icon="ep:close" | ||||||
|           class="delete" |           class="delete" | ||||||
|           :size="14" |           :size="14" | ||||||
|  | @ -217,16 +221,16 @@ const handleAppLinkChange = (appLink: AppLink) => { | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <template #footer> |     <template #footer> | ||||||
|       <el-button @click="handleAdd" type="primary" plain> |       <ElButton @click="handleAdd" type="primary" plain> | ||||||
|         <Icon icon="ep:plus" class="mr-5px" /> |         <IconifyIcon icon="ep:plus" class="mr-5px" /> | ||||||
|         添加热区 |         添加热区 | ||||||
|       </el-button> |       </ElButton> | ||||||
|       <el-button @click="handleSubmit" type="primary" plain> |       <ElButton @click="handleSubmit" type="primary" plain> | ||||||
|         <Icon icon="ep:check" class="mr-5px" /> |         <IconifyIcon icon="ep:check" class="mr-5px" /> | ||||||
|         确定 |         确定 | ||||||
|       </el-button> |       </ElButton> | ||||||
|     </template> |     </template> | ||||||
|   </Dialog> |   </ElDialog> | ||||||
|   <AppLinkSelectDialog |   <AppLinkSelectDialog | ||||||
|     ref="appLinkDialogRef" |     ref="appLinkDialogRef" | ||||||
|     @app-link-change="handleAppLinkChange" |     @app-link-change="handleAppLinkChange" | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ const handleOpenEditDialog = () => { | ||||||
|           height="50px" |           height="50px" | ||||||
|           width="auto" |           width="auto" | ||||||
|           class="min-w-80px" |           class="min-w-80px" | ||||||
|  |           :show-description="false" | ||||||
|         > |         > | ||||||
|           <template #tip> |           <template #tip> | ||||||
|             <el-text type="info" size="small"> 推荐宽度 750</el-text> |             <el-text type="info" size="small"> 推荐宽度 750</el-text> | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|           height="80px" |           height="80px" | ||||||
|           width="100%" |           width="100%" | ||||||
|           class="min-w-80px" |           class="min-w-80px" | ||||||
|  |           :show-description="false" | ||||||
|         > |         > | ||||||
|           <template #tip> 建议宽度750 </template> |           <template #tip> 建议宽度750 </template> | ||||||
|         </UploadImg> |         </UploadImg> | ||||||
|  |  | ||||||
|  | @ -35,7 +35,12 @@ const handleHotAreaSelected = (_: any, index: number) => { | ||||||
|       <template v-for="(hotArea, index) in formData.list" :key="index"> |       <template v-for="(hotArea, index) in formData.list" :key="index"> | ||||||
|         <template v-if="selectedHotAreaIndex === index"> |         <template v-if="selectedHotAreaIndex === index"> | ||||||
|           <el-form-item label="上传图片" :prop="`list[${index}].imgUrl`"> |           <el-form-item label="上传图片" :prop="`list[${index}].imgUrl`"> | ||||||
|             <UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" /> |             <UploadImg | ||||||
|  |               v-model="hotArea.imgUrl" | ||||||
|  |               height="80px" | ||||||
|  |               width="80px" | ||||||
|  |               :show-description="false" | ||||||
|  |             /> | ||||||
|           </el-form-item> |           </el-form-item> | ||||||
|           <el-form-item label="链接" :prop="`list[${index}].url`"> |           <el-form-item label="链接" :prop="`list[${index}].url`"> | ||||||
|             <AppLinkInput v-model="hotArea.url" /> |             <AppLinkInput v-model="hotArea.url" /> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { MenuGridProperty } from './config'; | import type { MenuGridProperty } from './config'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| /** 宫格导航 */ | /** 宫格导航 */ | ||||||
| defineOptions({ name: 'MenuGrid' }); | defineOptions({ name: 'MenuGrid' }); | ||||||
| defineProps<{ property: MenuGridProperty }>(); | defineProps<{ property: MenuGridProperty }>(); | ||||||
|  | @ -11,13 +13,13 @@ defineProps<{ property: MenuGridProperty }>(); | ||||||
|     <div |     <div | ||||||
|       v-for="(item, index) in property.list" |       v-for="(item, index) in property.list" | ||||||
|       :key="index" |       :key="index" | ||||||
|       class="p-b-14px p-t-20px relative flex flex-col items-center" |       class="relative flex flex-col items-center pb-3.5 pt-5" | ||||||
|       :style="{ width: `${100 * (1 / property.column)}%` }" |       :style="{ width: `${100 * (1 / property.column)}%` }" | ||||||
|     > |     > | ||||||
|       <!-- 右上角角标 --> |       <!-- 右上角角标 --> | ||||||
|       <span |       <span | ||||||
|         v-if="item.badge?.show" |         v-if="item.badge?.show" | ||||||
|         class="left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-12px leading-20px absolute text-center" |         class="absolute left-1/2 top-2.5 z-10 h-5 rounded-full px-1.5 text-center text-xs leading-5" | ||||||
|         :style="{ |         :style="{ | ||||||
|           color: item.badge.textColor, |           color: item.badge.textColor, | ||||||
|           backgroundColor: item.badge.bgColor, |           backgroundColor: item.badge.bgColor, | ||||||
|  | @ -25,15 +27,15 @@ defineProps<{ property: MenuGridProperty }>(); | ||||||
|       > |       > | ||||||
|         {{ item.badge.text }} |         {{ item.badge.text }} | ||||||
|       </span> |       </span> | ||||||
|       <el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" /> |       <ElImage v-if="item.iconUrl" class="h-7 w-7" :src="item.iconUrl" /> | ||||||
|       <span |       <span | ||||||
|         class="m-t-8px h-16px text-12px leading-16px" |         class="mt-2 h-4 text-xs leading-4" | ||||||
|         :style="{ color: item.titleColor }" |         :style="{ color: item.titleColor }" | ||||||
|       > |       > | ||||||
|         {{ item.title }} |         {{ item.title }} | ||||||
|       </span> |       </span> | ||||||
|       <span |       <span | ||||||
|         class="m-t-6px h-12px text-10px leading-12px" |         class="mt-1.5 h-3 text-xs leading-3" | ||||||
|         :style="{ color: item.subtitleColor }" |         :style="{ color: item.subtitleColor }" | ||||||
|       > |       > | ||||||
|         {{ item.subtitle }} |         {{ item.subtitle }} | ||||||
|  |  | ||||||
|  | @ -2,6 +2,19 @@ | ||||||
| import type { MenuGridProperty } from './config'; | import type { MenuGridProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadio, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSwitch, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import AppLinkInput from '#/components/app-link-input/index.vue'; | ||||||
|  | import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue'; | ||||||
|  | import Draggable from '#/components/draggable/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config'; | import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config'; | ||||||
| 
 | 
 | ||||||
|  | @ -16,58 +29,63 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <!-- 表单 --> |     <!-- 表单 --> | ||||||
|     <el-form label-width="80px" :model="formData" class="m-t-8px"> |     <ElForm label-width="80px" :model="formData" class="m-t-8px"> | ||||||
|       <el-form-item label="每行数量" prop="column"> |       <ElFormItem label="每行数量" prop="column"> | ||||||
|         <el-radio-group v-model="formData.column"> |         <ElRadioGroup v-model="formData.column"> | ||||||
|           <el-radio :value="3">3个</el-radio> |           <ElRadio :value="3">3个</ElRadio> | ||||||
|           <el-radio :value="4">4个</el-radio> |           <ElRadio :value="4">4个</ElRadio> | ||||||
|         </el-radio-group> |         </ElRadioGroup> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
| 
 | 
 | ||||||
|       <el-card header="菜单设置" class="property-group" shadow="never"> |       <ElCard header="菜单设置" class="property-group" shadow="never"> | ||||||
|         <Draggable |         <Draggable | ||||||
|           v-model="formData.list" |           v-model="formData.list" | ||||||
|           :empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY" |           :empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY" | ||||||
|         > |         > | ||||||
|           <template #default="{ element }"> |           <template #default="{ element }"> | ||||||
|             <el-form-item label="图标" prop="iconUrl"> |             <ElFormItem label="图标" prop="iconUrl"> | ||||||
|               <UploadImg v-model="element.iconUrl" height="80px" width="80px"> |               <UploadImg | ||||||
|  |                 v-model="element.iconUrl" | ||||||
|  |                 height="80px" | ||||||
|  |                 width="80px" | ||||||
|  |                 :show-description="false" | ||||||
|  |               > | ||||||
|                 <template #tip> 建议尺寸:44 * 44 </template> |                 <template #tip> 建议尺寸:44 * 44 </template> | ||||||
|               </UploadImg> |               </UploadImg> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <el-form-item label="标题" prop="title"> |             <ElFormItem label="标题" prop="title"> | ||||||
|               <InputWithColor |               <InputWithColor | ||||||
|                 v-model="element.title" |                 v-model="element.title" | ||||||
|                 v-model:color="element.titleColor" |                 v-model:color="element.titleColor" | ||||||
|               /> |               /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <el-form-item label="副标题" prop="subtitle"> |             <ElFormItem label="副标题" prop="subtitle"> | ||||||
|               <InputWithColor |               <InputWithColor | ||||||
|                 v-model="element.subtitle" |                 v-model="element.subtitle" | ||||||
|                 v-model:color="element.subtitleColor" |                 v-model:color="element.subtitleColor" | ||||||
|               /> |               /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <el-form-item label="链接" prop="url"> |             <ElFormItem label="链接" prop="url"> | ||||||
|               <AppLinkInput v-model="element.url" /> |               <AppLinkInput v-model="element.url" /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <el-form-item label="显示角标" prop="badge.show"> |             <ElFormItem label="显示角标" prop="badge.show"> | ||||||
|               <el-switch v-model="element.badge.show" /> |               <ElSwitch v-model="element.badge.show" /> | ||||||
|             </el-form-item> |             </ElFormItem> | ||||||
|             <template v-if="element.badge.show"> |             <template v-if="element.badge.show"> | ||||||
|               <el-form-item label="角标内容" prop="badge.text"> |               <ElFormItem label="角标内容" prop="badge.text"> | ||||||
|                 <InputWithColor |                 <InputWithColor | ||||||
|                   v-model="element.badge.text" |                   v-model="element.badge.text" | ||||||
|                   v-model:color="element.badge.textColor" |                   v-model:color="element.badge.textColor" | ||||||
|                 /> |                 /> | ||||||
|               </el-form-item> |               </ElFormItem> | ||||||
|               <el-form-item label="背景颜色" prop="badge.bgColor"> |               <ElFormItem label="背景颜色" prop="badge.bgColor"> | ||||||
|                 <ColorInput v-model="element.badge.bgColor" /> |                 <ColorInput v-model="element.badge.bgColor" /> | ||||||
|               </el-form-item> |               </ElFormItem> | ||||||
|             </template> |             </template> | ||||||
|           </template> |           </template> | ||||||
|         </Draggable> |         </Draggable> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,7 +26,12 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|       > |       > | ||||||
|         <template #default="{ element }"> |         <template #default="{ element }"> | ||||||
|           <el-form-item label="图标" prop="iconUrl"> |           <el-form-item label="图标" prop="iconUrl"> | ||||||
|             <UploadImg v-model="element.iconUrl" height="80px" width="80px"> |             <UploadImg | ||||||
|  |               v-model="element.iconUrl" | ||||||
|  |               height="80px" | ||||||
|  |               width="80px" | ||||||
|  |               :show-description="false" | ||||||
|  |             > | ||||||
|               <template #tip> 建议尺寸:44 * 44 </template> |               <template #tip> 建议尺寸:44 * 44 </template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </el-form-item> | ||||||
|  |  | ||||||
|  | @ -46,7 +46,12 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|         > |         > | ||||||
|           <template #default="{ element }"> |           <template #default="{ element }"> | ||||||
|             <el-form-item label="图标" prop="iconUrl"> |             <el-form-item label="图标" prop="iconUrl"> | ||||||
|               <UploadImg v-model="element.iconUrl" height="80px" width="80px"> |               <UploadImg | ||||||
|  |                 v-model="element.iconUrl" | ||||||
|  |                 height="80px" | ||||||
|  |                 width="80px" | ||||||
|  |                 :show-description="false" | ||||||
|  |               > | ||||||
|                 <template #tip> 建议尺寸:98 * 98 </template> |                 <template #tip> 建议尺寸:98 * 98 </template> | ||||||
|               </UploadImg> |               </UploadImg> | ||||||
|             </el-form-item> |             </el-form-item> | ||||||
|  |  | ||||||
|  | @ -1,9 +1,24 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { NavigationBarCellProperty } from '../config'; | import type { NavigationBarCellProperty } from '../config'; | ||||||
| 
 | 
 | ||||||
|  | import type { Rect } from '#/components/magic-cube-editor/util'; | ||||||
|  | 
 | ||||||
| import { computed, ref } from 'vue'; | import { computed, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElFormItem, | ||||||
|  |   ElInput, | ||||||
|  |   ElRadio, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import appNavBarMp from '#/assets/imgs/diy/app-nav-bar-mp.png'; | ||||||
|  | import AppLinkInput from '#/components/app-link-input/index.vue'; | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import MagicCubeEditor from '#/components/magic-cube-editor/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| // 导航栏属性面板 | // 导航栏属性面板 | ||||||
| defineOptions({ name: 'NavigationBarCellProperty' }); | defineOptions({ name: 'NavigationBarCellProperty' }); | ||||||
|  | @ -24,6 +39,18 @@ const cellList = useVModel(props, 'modelValue', emit); | ||||||
| // 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个 | // 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个 | ||||||
| const cellCount = computed(() => (props.isMp ? 6 : 8)); | const cellCount = computed(() => (props.isMp ? 6 : 8)); | ||||||
| 
 | 
 | ||||||
|  | // 转换为Rect格式的数据 | ||||||
|  | const rectList = computed<Rect[]>(() => { | ||||||
|  |   return cellList.value.map((cell) => ({ | ||||||
|  |     left: cell.left, | ||||||
|  |     top: cell.top, | ||||||
|  |     width: cell.width, | ||||||
|  |     height: cell.height, | ||||||
|  |     right: cell.left + cell.width, | ||||||
|  |     bottom: cell.top + cell.height, | ||||||
|  |   })); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| // 选中的热区 | // 选中的热区 | ||||||
| const selectedHotAreaIndex = ref(0); | const selectedHotAreaIndex = ref(0); | ||||||
| const handleHotAreaSelected = ( | const handleHotAreaSelected = ( | ||||||
|  | @ -41,7 +68,7 @@ const handleHotAreaSelected = ( | ||||||
| <template> | <template> | ||||||
|   <div class="h-40px flex items-center justify-center"> |   <div class="h-40px flex items-center justify-center"> | ||||||
|     <MagicCubeEditor |     <MagicCubeEditor | ||||||
|       v-model="cellList" |       v-model="rectList" | ||||||
|       :cols="cellCount" |       :cols="cellCount" | ||||||
|       :cube-size="38" |       :cube-size="38" | ||||||
|       :rows="1" |       :rows="1" | ||||||
|  | @ -51,54 +78,55 @@ const handleHotAreaSelected = ( | ||||||
|     <img |     <img | ||||||
|       v-if="isMp" |       v-if="isMp" | ||||||
|       alt="" |       alt="" | ||||||
|       class="h-30px w-76px" |       style="width: 76px; height: 30px" | ||||||
|       src="@/assets/imgs/diy/app-nav-bar-mp.png" |       :src="appNavBarMp" | ||||||
|     /> |     /> | ||||||
|   </div> |   </div> | ||||||
|   <template v-for="(cell, cellIndex) in cellList" :key="cellIndex"> |   <template v-for="(cell, cellIndex) in cellList" :key="cellIndex"> | ||||||
|     <template v-if="selectedHotAreaIndex === Number(cellIndex)"> |     <template v-if="selectedHotAreaIndex === Number(cellIndex)"> | ||||||
|       <el-form-item :prop="`cell[${cellIndex}].type`" label="类型"> |       <ElFormItem :prop="`cell[${cellIndex}].type`" label="类型"> | ||||||
|         <el-radio-group v-model="cell.type"> |         <ElRadioGroup v-model="cell.type"> | ||||||
|           <el-radio value="text">文字</el-radio> |           <ElRadio value="text">文字</ElRadio> | ||||||
|           <el-radio value="image">图片</el-radio> |           <ElRadio value="image">图片</ElRadio> | ||||||
|           <el-radio value="search">搜索框</el-radio> |           <ElRadio value="search">搜索框</ElRadio> | ||||||
|         </el-radio-group> |         </ElRadioGroup> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <!-- 1. 文字 --> |       <!-- 1. 文字 --> | ||||||
|       <template v-if="cell.type === 'text'"> |       <template v-if="cell.type === 'text'"> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].text`" label="内容"> |         <ElFormItem :prop="`cell[${cellIndex}].text`" label="内容"> | ||||||
|           <el-input v-model="cell!.text" maxlength="10" show-word-limit /> |           <ElInput v-model="cell!.text" maxlength="10" show-word-limit /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].text`" label="颜色"> |         <ElFormItem :prop="`cell[${cellIndex}].text`" label="颜色"> | ||||||
|           <ColorInput v-model="cell!.textColor" /> |           <ColorInput v-model="cell!.textColor" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].url`" label="链接"> |         <ElFormItem :prop="`cell[${cellIndex}].url`" label="链接"> | ||||||
|           <AppLinkInput v-model="cell.url" /> |           <AppLinkInput v-model="cell.url" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </template> |       </template> | ||||||
|       <!-- 2. 图片 --> |       <!-- 2. 图片 --> | ||||||
|       <template v-else-if="cell.type === 'image'"> |       <template v-else-if="cell.type === 'image'"> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].imgUrl`" label="图片"> |         <ElFormItem :prop="`cell[${cellIndex}].imgUrl`" label="图片"> | ||||||
|           <UploadImg |           <UploadImg | ||||||
|             v-model="cell.imgUrl" |             v-model="cell.imgUrl" | ||||||
|             :limit="1" |             :limit="1" | ||||||
|             height="56px" |             height="56px" | ||||||
|             width="56px" |             width="56px" | ||||||
|  |             :show-description="false" | ||||||
|           > |           > | ||||||
|             <template #tip>建议尺寸 56*56</template> |             <template #tip>建议尺寸 56*56</template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].url`" label="链接"> |         <ElFormItem :prop="`cell[${cellIndex}].url`" label="链接"> | ||||||
|           <AppLinkInput v-model="cell.url" /> |           <AppLinkInput v-model="cell.url" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </template> |       </template> | ||||||
|       <!-- 3. 搜索框 --> |       <!-- 3. 搜索框 --> | ||||||
|       <template v-else> |       <template v-else> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字"> |         <ElFormItem :prop="`cell[${cellIndex}].placeholder`" label="提示文字"> | ||||||
|           <el-input v-model="cell.placeholder" maxlength="10" show-word-limit /> |           <ElInput v-model="cell.placeholder" maxlength="10" show-word-limit /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角"> |         <ElFormItem :prop="`cell[${cellIndex}].borderRadius`" label="圆角"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="cell.borderRadius" |             v-model="cell.borderRadius" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -106,7 +134,7 @@ const handleHotAreaSelected = ( | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             show-input |             show-input | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </template> |       </template> | ||||||
|     </template> |     </template> | ||||||
|   </template> |   </template> | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import type { SearchProperty } from '#/components/diy-editor/components/mobile/S | ||||||
| 
 | 
 | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import appNavbarMp from '#/assets/imgs/diy/app-nav-bar-mp.png'; | ||||||
| import SearchBar from '#/components/diy-editor/components/mobile/SearchBar/index.vue'; | import SearchBar from '#/components/diy-editor/components/mobile/SearchBar/index.vue'; | ||||||
| 
 | 
 | ||||||
| /** 页面顶部导航栏 */ | /** 页面顶部导航栏 */ | ||||||
|  | @ -75,9 +76,9 @@ const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => { | ||||||
|     </div> |     </div> | ||||||
|     <img |     <img | ||||||
|       v-if="property._local?.previewMp" |       v-if="property._local?.previewMp" | ||||||
|       src="@/assets/imgs/diy/app-nav-bar-mp.png" |       :src="appNavbarMp" | ||||||
|       alt="" |       alt="" | ||||||
|       class="h-30px w-86px" |       style="width: 86px; height: 30px" | ||||||
|     /> |     /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -70,6 +70,7 @@ if (!formData.value._local) { | ||||||
|           :limit="1" |           :limit="1" | ||||||
|           width="56px" |           width="56px" | ||||||
|           height="56px" |           height="56px" | ||||||
|  |           :show-description="false" | ||||||
|         /> |         /> | ||||||
|         <span class="mb-2 ml-2 text-xs text-gray-400">建议宽度:750</span> |         <span class="mb-2 ml-2 text-xs text-gray-400">建议宽度:750</span> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -21,7 +21,11 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData" :rules="rules"> |     <el-form label-width="80px" :model="formData" :rules="rules"> | ||||||
|       <el-form-item label="公告图标" prop="iconUrl"> |       <el-form-item label="公告图标" prop="iconUrl"> | ||||||
|         <UploadImg v-model="formData.iconUrl" height="48px"> |         <UploadImg | ||||||
|  |           v-model="formData.iconUrl" | ||||||
|  |           height="48px" | ||||||
|  |           :show-description="false" | ||||||
|  |         > | ||||||
|           <template #tip>建议尺寸:24 * 24</template> |           <template #tip>建议尺寸:24 * 24</template> | ||||||
|         </UploadImg> |         </UploadImg> | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
|  |  | ||||||
|  | @ -28,7 +28,11 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|       <ColorInput v-model="formData!.backgroundColor" /> |       <ColorInput v-model="formData!.backgroundColor" /> | ||||||
|     </el-form-item> |     </el-form-item> | ||||||
|     <el-form-item label="背景图片" prop="backgroundImage"> |     <el-form-item label="背景图片" prop="backgroundImage"> | ||||||
|       <UploadImg v-model="formData!.backgroundImage" :limit="1"> |       <UploadImg | ||||||
|  |         v-model="formData!.backgroundImage" | ||||||
|  |         :limit="1" | ||||||
|  |         :show-description="false" | ||||||
|  |       > | ||||||
|         <template #tip>建议宽度 750px</template> |         <template #tip>建议宽度 750px</template> | ||||||
|       </UploadImg> |       </UploadImg> | ||||||
|     </el-form-item> |     </el-form-item> | ||||||
|  |  | ||||||
|  | @ -16,7 +16,12 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|     <Draggable v-model="formData.list" :empty-item="{ showType: 'once' }"> |     <Draggable v-model="formData.list" :empty-item="{ showType: 'once' }"> | ||||||
|       <template #default="{ element, index }"> |       <template #default="{ element, index }"> | ||||||
|         <el-form-item label="图片" :prop="`list[${index}].imgUrl`"> |         <el-form-item label="图片" :prop="`list[${index}].imgUrl`"> | ||||||
|           <UploadImg v-model="element.imgUrl" height="56px" width="56px" /> |           <UploadImg | ||||||
|  |             v-model="element.imgUrl" | ||||||
|  |             height="56px" | ||||||
|  |             width="56px" | ||||||
|  |             :show-description="false" | ||||||
|  |           /> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <el-form-item label="跳转链接" :prop="`list[${index}].url`"> |         <el-form-item label="跳转链接" :prop="`list[${index}].url`"> | ||||||
|           <AppLinkInput v-model="element.url" /> |           <AppLinkInput v-model="element.url" /> | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ import { ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { fenToYuan } from '@vben/utils'; | import { fenToYuan } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
| 
 | 
 | ||||||
| /** 商品卡片 */ | /** 商品卡片 */ | ||||||
|  | @ -55,7 +57,7 @@ const calculateWidth = () => { | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="min-h-30px box-content flex w-full flex-row flex-wrap" |     class="box-content flex min-h-[30px] w-full flex-row flex-wrap" | ||||||
|     ref="containerRef" |     ref="containerRef" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|  | @ -74,28 +76,28 @@ const calculateWidth = () => { | ||||||
|       <!-- 角标 --> |       <!-- 角标 --> | ||||||
|       <div |       <div | ||||||
|         v-if="property.badge.show && property.badge.imgUrl" |         v-if="property.badge.show && property.badge.imgUrl" | ||||||
|         class="z-1 absolute left-0 top-0 items-center justify-center" |         class="absolute left-0 top-0 z-[1] items-center justify-center" | ||||||
|       > |       > | ||||||
|         <el-image |         <ElImage | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.badge.imgUrl" |           :src="property.badge.imgUrl" | ||||||
|           class="h-26px w-38px" |           class="h-[26px] w-[38px]" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 商品封面图 --> |       <!-- 商品封面图 --> | ||||||
|       <div |       <div | ||||||
|         class="h-140px" |         class="h-[140px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|             'w-140px': property.layoutType === 'oneColSmallImg', |             'w-[140px]': property.layoutType === 'oneColSmallImg', | ||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       > |       > | ||||||
|         <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |         <ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" /> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         class="gap-8px p-8px box-border flex flex-col" |         class="box-border flex flex-col gap-[8px] p-[8px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -107,7 +109,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品名称 --> |         <!-- 商品名称 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.name.show" |           v-if="property.fields.name.show" | ||||||
|           class="text-14px" |           class="text-[14px]" | ||||||
|           :class="[ |           :class="[ | ||||||
|             { |             { | ||||||
|               truncate: property.layoutType !== 'oneColSmallImg', |               truncate: property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -122,7 +124,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品简介 --> |         <!-- 商品简介 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.introduction.show" |           v-if="property.fields.introduction.show" | ||||||
|           class="text-12px truncate" |           class="truncate text-[12px]" | ||||||
|           :style="{ color: property.fields.introduction.color }" |           :style="{ color: property.fields.introduction.color }" | ||||||
|         > |         > | ||||||
|           {{ spu.introduction }} |           {{ spu.introduction }} | ||||||
|  | @ -131,7 +133,7 @@ const calculateWidth = () => { | ||||||
|           <!-- 价格 --> |           <!-- 价格 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.price.show" |             v-if="property.fields.price.show" | ||||||
|             class="text-16px" |             class="text-[16px]" | ||||||
|             :style="{ color: property.fields.price.color }" |             :style="{ color: property.fields.price.color }" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.price as any) }} |             ¥{{ fenToYuan(spu.price as any) }} | ||||||
|  | @ -139,12 +141,12 @@ const calculateWidth = () => { | ||||||
|           <!-- 市场价 --> |           <!-- 市场价 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.marketPrice.show && spu.marketPrice" |             v-if="property.fields.marketPrice.show && spu.marketPrice" | ||||||
|             class="ml-4px text-10px line-through" |             class="ml-[4px] text-[10px] line-through" | ||||||
|             :style="{ color: property.fields.marketPrice.color }" |             :style="{ color: property.fields.marketPrice.color }" | ||||||
|             >¥{{ fenToYuan(spu.marketPrice) }} |             >¥{{ fenToYuan(spu.marketPrice) }} | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="text-12px"> |         <div class="text-[12px]"> | ||||||
|           <!-- 销量 --> |           <!-- 销量 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.salesCount.show" |             v-if="property.fields.salesCount.show" | ||||||
|  | @ -162,11 +164,11 @@ const calculateWidth = () => { | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 购买按钮 --> |       <!-- 购买按钮 --> | ||||||
|       <div class="bottom-8px right-8px absolute"> |       <div class="absolute bottom-[8px] right-[8px]"> | ||||||
|         <!-- 文字按钮 --> |         <!-- 文字按钮 --> | ||||||
|         <span |         <span | ||||||
|           v-if="property.btnBuy.type === 'text'" |           v-if="property.btnBuy.type === 'text'" | ||||||
|           class="p-x-12px p-y-4px text-12px rounded-full text-white" |           class="rounded-full px-[12px] py-[4px] text-[12px] text-white" | ||||||
|           :style="{ |           :style="{ | ||||||
|             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, |             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, | ||||||
|           }" |           }" | ||||||
|  | @ -174,9 +176,9 @@ const calculateWidth = () => { | ||||||
|           {{ property.btnBuy.text }} |           {{ property.btnBuy.text }} | ||||||
|         </span> |         </span> | ||||||
|         <!-- 图片按钮 --> |         <!-- 图片按钮 --> | ||||||
|         <el-image |         <ElImage | ||||||
|           v-else |           v-else | ||||||
|           class="h-28px w-28px rounded-full" |           class="h-[28px] w-[28px] rounded-full" | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.btnBuy.imgUrl" |           :src="property.btnBuy.imgUrl" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,24 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { ProductCardProperty } from './config'; | import type { ProductCardProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { IconifyIcon } from '@vben/icons'; | ||||||
| 
 | 
 | ||||||
|  | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElCheckbox, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElInput, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue'; | import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue'; | ||||||
| 
 | 
 | ||||||
| // 商品卡片属性面板 | // 商品卡片属性面板 | ||||||
|  | @ -15,114 +31,116 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData"> |     <ElForm label-width="80px" :model="formData"> | ||||||
|       <el-card header="商品列表" class="property-group" shadow="never"> |       <ElCard header="商品列表" class="property-group" shadow="never"> | ||||||
|         <SpuShowcase v-model="formData.spuIds" /> |         <SpuShowcase v-model="formData.spuIds" /> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="布局" prop="type"> |         <ElFormItem label="布局" prop="type"> | ||||||
|           <el-radio-group v-model="formData.layoutType"> |           <ElRadioGroup v-model="formData.layoutType"> | ||||||
|             <el-tooltip class="item" content="单列大图" placement="bottom"> |             <ElTooltip class="item" content="单列大图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColBigImg"> |               <ElRadioButton value="oneColBigImg"> | ||||||
|                 <Icon icon="fluent:text-column-one-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-one-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="单列小图" placement="bottom"> |             <ElTooltip class="item" content="单列小图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColSmallImg"> |               <ElRadioButton value="oneColSmallImg"> | ||||||
|                 <Icon icon="fluent:text-column-two-left-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-left-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="双列" placement="bottom"> |             <ElTooltip class="item" content="双列" placement="bottom"> | ||||||
|               <el-radio-button value="twoCol"> |               <ElRadioButton value="twoCol"> | ||||||
|                 <Icon icon="fluent:text-column-two-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品名称" prop="fields.name.show"> |         <ElFormItem label="商品名称" prop="fields.name.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.name.color" /> |             <ColorInput v-model="formData.fields.name.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.name.show" /> |             <ElCheckbox v-model="formData.fields.name.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品简介" prop="fields.introduction.show"> |         <ElFormItem label="商品简介" prop="fields.introduction.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.introduction.color" /> |             <ColorInput v-model="formData.fields.introduction.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.introduction.show" /> |             <ElCheckbox v-model="formData.fields.introduction.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品价格" prop="fields.price.show"> |         <ElFormItem label="商品价格" prop="fields.price.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.price.color" /> |             <ColorInput v-model="formData.fields.price.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.price.show" /> |             <ElCheckbox v-model="formData.fields.price.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="市场价" prop="fields.marketPrice.show"> |         <ElFormItem label="市场价" prop="fields.marketPrice.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.marketPrice.color" /> |             <ColorInput v-model="formData.fields.marketPrice.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.marketPrice.show" /> |             <ElCheckbox v-model="formData.fields.marketPrice.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品销量" prop="fields.salesCount.show"> |         <ElFormItem label="商品销量" prop="fields.salesCount.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.salesCount.color" /> |             <ColorInput v-model="formData.fields.salesCount.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.salesCount.show" /> |             <ElCheckbox v-model="formData.fields.salesCount.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品库存" prop="fields.stock.show"> |         <ElFormItem label="商品库存" prop="fields.stock.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.stock.color" /> |             <ColorInput v-model="formData.fields.stock.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.stock.show" /> |             <ElCheckbox v-model="formData.fields.stock.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="角标" class="property-group" shadow="never"> |       <ElCard header="角标" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="角标" prop="badge.show"> |         <ElFormItem label="角标" prop="badge.show"> | ||||||
|           <el-switch v-model="formData.badge.show" /> |           <ElSwitch v-model="formData.badge.show" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item |         <ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> | ||||||
|           label="角标" |           <UploadImg | ||||||
|           prop="badge.imgUrl" |             v-model="formData.badge.imgUrl" | ||||||
|           v-if="formData.badge.show" |             height="44px" | ||||||
|         > |             width="72px" | ||||||
|           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |             :show-description="false" | ||||||
|  |           > | ||||||
|             <template #tip> 建议尺寸:36 * 22 </template> |             <template #tip> 建议尺寸:36 * 22 </template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="按钮" class="property-group" shadow="never"> |       <ElCard header="按钮" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="按钮类型" prop="btnBuy.type"> |         <ElFormItem label="按钮类型" prop="btnBuy.type"> | ||||||
|           <el-radio-group v-model="formData.btnBuy.type"> |           <ElRadioGroup v-model="formData.btnBuy.type"> | ||||||
|             <el-radio-button value="text">文字</el-radio-button> |             <ElRadioButton value="text">文字</ElRadioButton> | ||||||
|             <el-radio-button value="img">图片</el-radio-button> |             <ElRadioButton value="img">图片</ElRadioButton> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <template v-if="formData.btnBuy.type === 'text'"> |         <template v-if="formData.btnBuy.type === 'text'"> | ||||||
|           <el-form-item label="按钮文字" prop="btnBuy.text"> |           <ElFormItem label="按钮文字" prop="btnBuy.text"> | ||||||
|             <el-input v-model="formData.btnBuy.text" /> |             <ElInput v-model="formData.btnBuy.text" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |           <ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |           <ElFormItem label="右侧背景" prop="btnBuy.bgEndColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgEndColor" /> |             <ColorInput v-model="formData.btnBuy.bgEndColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|         <template v-else> |         <template v-else> | ||||||
|           <el-form-item label="图片" prop="btnBuy.imgUrl"> |           <ElFormItem label="图片" prop="btnBuy.imgUrl"> | ||||||
|             <UploadImg |             <UploadImg | ||||||
|               v-model="formData.btnBuy.imgUrl" |               v-model="formData.btnBuy.imgUrl" | ||||||
|               height="56px" |               height="56px" | ||||||
|               width="56px" |               width="56px" | ||||||
|  |               :show-description="false" | ||||||
|             > |             > | ||||||
|               <template #tip> 建议尺寸:56 * 56 </template> |               <template #tip> 建议尺寸:56 * 56 </template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="上圆角" prop="borderRadiusTop"> |         <ElFormItem label="上圆角" prop="borderRadiusTop"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusTop" |             v-model="formData.borderRadiusTop" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -130,9 +148,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="下圆角" prop="borderRadiusBottom"> |         <ElFormItem label="下圆角" prop="borderRadiusBottom"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusBottom" |             v-model="formData.borderRadiusBottom" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -140,9 +158,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="间隔" prop="space"> |         <ElFormItem label="间隔" prop="space"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.space" |             v-model="formData.space" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -150,9 +168,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -61,7 +61,12 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|           prop="badge.imgUrl" |           prop="badge.imgUrl" | ||||||
|           v-if="formData.badge.show" |           v-if="formData.badge.show" | ||||||
|         > |         > | ||||||
|           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |           <UploadImg | ||||||
|  |             v-model="formData.badge.imgUrl" | ||||||
|  |             height="44px" | ||||||
|  |             width="72px" | ||||||
|  |             :show-description="false" | ||||||
|  |           > | ||||||
|             <template #tip> 建议尺寸:36 * 22 </template> |             <template #tip> 建议尺寸:36 * 22 </template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|  |  | ||||||
|  | @ -8,6 +8,8 @@ import { ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { fenToYuan } from '@vben/utils'; | import { fenToYuan } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
| import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; | import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; | ||||||
| 
 | 
 | ||||||
|  | @ -97,7 +99,7 @@ const calculateWidth = () => { | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="min-h-30px box-content flex w-full flex-row flex-wrap" |     class="box-content flex min-h-[30px] w-full flex-row flex-wrap" | ||||||
|     ref="containerRef" |     ref="containerRef" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|  | @ -116,28 +118,28 @@ const calculateWidth = () => { | ||||||
|       <!-- 角标 --> |       <!-- 角标 --> | ||||||
|       <div |       <div | ||||||
|         v-if="property.badge.show" |         v-if="property.badge.show" | ||||||
|         class="z-1 absolute left-0 top-0 items-center justify-center" |         class="absolute left-0 top-0 z-[1] items-center justify-center" | ||||||
|       > |       > | ||||||
|         <el-image |         <ElImage | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.badge.imgUrl" |           :src="property.badge.imgUrl" | ||||||
|           class="h-26px w-38px" |           class="h-[26px] w-[38px]" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 商品封面图 --> |       <!-- 商品封面图 --> | ||||||
|       <div |       <div | ||||||
|         class="h-140px" |         class="h-[140px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|             'w-140px': property.layoutType === 'oneColSmallImg', |             'w-[140px]': property.layoutType === 'oneColSmallImg', | ||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       > |       > | ||||||
|         <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |         <ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" /> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         class="gap-8px p-8px box-border flex flex-col" |         class="box-border flex flex-col gap-[8px] p-[8px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -149,7 +151,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品名称 --> |         <!-- 商品名称 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.name.show" |           v-if="property.fields.name.show" | ||||||
|           class="text-14px" |           class="text-[14px]" | ||||||
|           :class="[ |           :class="[ | ||||||
|             { |             { | ||||||
|               truncate: property.layoutType !== 'oneColSmallImg', |               truncate: property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -164,7 +166,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品简介 --> |         <!-- 商品简介 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.introduction.show" |           v-if="property.fields.introduction.show" | ||||||
|           class="text-12px truncate" |           class="truncate text-[12px]" | ||||||
|           :style="{ color: property.fields.introduction.color }" |           :style="{ color: property.fields.introduction.color }" | ||||||
|         > |         > | ||||||
|           {{ spu.introduction }} |           {{ spu.introduction }} | ||||||
|  | @ -173,7 +175,7 @@ const calculateWidth = () => { | ||||||
|           <!-- 价格 --> |           <!-- 价格 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.price.show" |             v-if="property.fields.price.show" | ||||||
|             class="text-16px" |             class="text-[16px]" | ||||||
|             :style="{ color: property.fields.price.color }" |             :style="{ color: property.fields.price.color }" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.price || Infinity) }} |             ¥{{ fenToYuan(spu.price || Infinity) }} | ||||||
|  | @ -181,13 +183,13 @@ const calculateWidth = () => { | ||||||
|           <!-- 市场价 --> |           <!-- 市场价 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.marketPrice.show && spu.marketPrice" |             v-if="property.fields.marketPrice.show && spu.marketPrice" | ||||||
|             class="ml-4px text-10px line-through" |             class="ml-[4px] text-[10px] line-through" | ||||||
|             :style="{ color: property.fields.marketPrice.color }" |             :style="{ color: property.fields.marketPrice.color }" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.marketPrice) }} |             ¥{{ fenToYuan(spu.marketPrice) }} | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="text-12px"> |         <div class="text-[12px]"> | ||||||
|           <!-- 销量 --> |           <!-- 销量 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.salesCount.show" |             v-if="property.fields.salesCount.show" | ||||||
|  | @ -205,11 +207,11 @@ const calculateWidth = () => { | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 购买按钮 --> |       <!-- 购买按钮 --> | ||||||
|       <div class="bottom-8px right-8px absolute"> |       <div class="absolute bottom-[8px] right-[8px]"> | ||||||
|         <!-- 文字按钮 --> |         <!-- 文字按钮 --> | ||||||
|         <span |         <span | ||||||
|           v-if="property.btnBuy.type === 'text'" |           v-if="property.btnBuy.type === 'text'" | ||||||
|           class="p-x-12px p-y-4px text-12px rounded-full text-white" |           class="rounded-full px-[12px] py-[4px] text-[12px] text-white" | ||||||
|           :style="{ |           :style="{ | ||||||
|             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, |             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, | ||||||
|           }" |           }" | ||||||
|  | @ -217,9 +219,9 @@ const calculateWidth = () => { | ||||||
|           {{ property.btnBuy.text }} |           {{ property.btnBuy.text }} | ||||||
|         </span> |         </span> | ||||||
|         <!-- 图片按钮 --> |         <!-- 图片按钮 --> | ||||||
|         <el-image |         <ElImage | ||||||
|           v-else |           v-else | ||||||
|           class="h-28px w-28px rounded-full" |           class="h-[28px] w-[28px] rounded-full" | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.btnBuy.imgUrl" |           :src="property.btnBuy.imgUrl" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  | @ -5,9 +5,22 @@ import type { MallCombinationActivityApi } from '#/api/mall/promotion/combinatio | ||||||
| 
 | 
 | ||||||
| import { onMounted, ref } from 'vue'; | import { onMounted, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; | import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity'; | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| import { CommonStatusEnum } from '#/utils'; | import { CommonStatusEnum } from '#/utils'; | ||||||
| import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue'; | import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue'; | ||||||
| 
 | 
 | ||||||
|  | @ -31,119 +44,116 @@ onMounted(async () => { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData"> |     <ElForm label-width="80px" :model="formData"> | ||||||
|       <el-card header="拼团活动" class="property-group" shadow="never"> |       <ElCard header="拼团活动" class="property-group" shadow="never"> | ||||||
|         <CombinationShowcase v-model="formData.activityIds" /> |         <CombinationShowcase v-model="formData.activityIds" /> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="布局" prop="type"> |         <ElFormItem label="布局" prop="type"> | ||||||
|           <el-radio-group v-model="formData.layoutType"> |           <ElRadioGroup v-model="formData.layoutType"> | ||||||
|             <el-tooltip class="item" content="单列大图" placement="bottom"> |             <ElTooltip class="item" content="单列大图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColBigImg"> |               <ElRadioButton value="oneColBigImg"> | ||||||
|                 <Icon icon="fluent:text-column-one-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-one-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="单列小图" placement="bottom"> |             <ElTooltip class="item" content="单列小图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColSmallImg"> |               <ElRadioButton value="oneColSmallImg"> | ||||||
|                 <Icon icon="fluent:text-column-two-left-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-left-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="双列" placement="bottom"> |             <ElTooltip class="item" content="双列" placement="bottom"> | ||||||
|               <el-radio-button value="twoCol"> |               <ElRadioButton value="twoCol"> | ||||||
|                 <Icon icon="fluent:text-column-two-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <!--<el-tooltip class="item" content="三列" placement="bottom"> |             <!--<el-tooltip class="item" content="三列" placement="bottom"> | ||||||
|               <el-radio-button value="threeCol"> |               <el-radio-button value="threeCol"> | ||||||
|                 <Icon icon="fluent:text-column-three-24-filled" /> |                 <Icon icon="fluent:text-column-three-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip>--> |             </ElTooltip>--> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品名称" prop="fields.name.show"> |         <ElFormItem label="商品名称" prop="fields.name.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.name.color" /> |             <ColorInput v-model="formData.fields.name.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.name.show" /> |             <ElCheckbox v-model="formData.fields.name.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品简介" prop="fields.introduction.show"> |         <ElFormItem label="商品简介" prop="fields.introduction.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.introduction.color" /> |             <ColorInput v-model="formData.fields.introduction.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.introduction.show" /> |             <ElCheckbox v-model="formData.fields.introduction.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品价格" prop="fields.price.show"> |         <ElFormItem label="商品价格" prop="fields.price.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.price.color" /> |             <ColorInput v-model="formData.fields.price.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.price.show" /> |             <ElCheckbox v-model="formData.fields.price.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="市场价" prop="fields.marketPrice.show"> |         <ElFormItem label="市场价" prop="fields.marketPrice.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.marketPrice.color" /> |             <ColorInput v-model="formData.fields.marketPrice.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.marketPrice.show" /> |             <ElCheckbox v-model="formData.fields.marketPrice.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品销量" prop="fields.salesCount.show"> |         <ElFormItem label="商品销量" prop="fields.salesCount.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.salesCount.color" /> |             <ColorInput v-model="formData.fields.salesCount.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.salesCount.show" /> |             <ElCheckbox v-model="formData.fields.salesCount.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品库存" prop="fields.stock.show"> |         <ElFormItem label="商品库存" prop="fields.stock.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.stock.color" /> |             <ColorInput v-model="formData.fields.stock.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.stock.show" /> |             <ElCheckbox v-model="formData.fields.stock.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="角标" class="property-group" shadow="never"> |       <ElCard header="角标" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="角标" prop="badge.show"> |         <ElFormItem label="角标" prop="badge.show"> | ||||||
|           <el-switch v-model="formData.badge.show" /> |           <ElSwitch v-model="formData.badge.show" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item |         <ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> | ||||||
|           label="角标" |  | ||||||
|           prop="badge.imgUrl" |  | ||||||
|           v-if="formData.badge.show" |  | ||||||
|         > |  | ||||||
|           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> | ||||||
|             <template #tip> 建议尺寸:36 * 22</template> |             <template #tip> 建议尺寸:36 * 22</template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="按钮" class="property-group" shadow="never"> |       <ElCard header="按钮" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="按钮类型" prop="btnBuy.type"> |         <ElFormItem label="按钮类型" prop="btnBuy.type"> | ||||||
|           <el-radio-group v-model="formData.btnBuy.type"> |           <ElRadioGroup v-model="formData.btnBuy.type"> | ||||||
|             <el-radio-button value="text">文字</el-radio-button> |             <ElRadioButton value="text">文字</ElRadioButton> | ||||||
|             <el-radio-button value="img">图片</el-radio-button> |             <ElRadioButton value="img">图片</ElRadioButton> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <template v-if="formData.btnBuy.type === 'text'"> |         <template v-if="formData.btnBuy.type === 'text'"> | ||||||
|           <el-form-item label="按钮文字" prop="btnBuy.text"> |           <ElFormItem label="按钮文字" prop="btnBuy.text"> | ||||||
|             <el-input v-model="formData.btnBuy.text" /> |             <ElInput v-model="formData.btnBuy.text" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |           <ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |           <ElFormItem label="右侧背景" prop="btnBuy.bgEndColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgEndColor" /> |             <ColorInput v-model="formData.btnBuy.bgEndColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|         <template v-else> |         <template v-else> | ||||||
|           <el-form-item label="图片" prop="btnBuy.imgUrl"> |           <ElFormItem label="图片" prop="btnBuy.imgUrl"> | ||||||
|             <UploadImg |             <UploadImg | ||||||
|               v-model="formData.btnBuy.imgUrl" |               v-model="formData.btnBuy.imgUrl" | ||||||
|               height="56px" |               height="56px" | ||||||
|               width="56px" |               width="56px" | ||||||
|  |               :show-description="false" | ||||||
|             > |             > | ||||||
|               <template #tip> 建议尺寸:56 * 56</template> |               <template #tip> 建议尺寸:56 * 56</template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="上圆角" prop="borderRadiusTop"> |         <ElFormItem label="上圆角" prop="borderRadiusTop"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusTop" |             v-model="formData.borderRadiusTop" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -151,9 +161,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="下圆角" prop="borderRadiusBottom"> |         <ElFormItem label="下圆角" prop="borderRadiusBottom"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusBottom" |             v-model="formData.borderRadiusBottom" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -161,9 +171,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="间隔" prop="space"> |         <ElFormItem label="间隔" prop="space"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.space" |             v-model="formData.space" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -171,9 +181,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ import { ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { fenToYuan } from '@vben/utils'; | import { fenToYuan } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
| import * as PointActivityApi from '#/api/mall/promotion/point'; | import * as PointActivityApi from '#/api/mall/promotion/point'; | ||||||
| 
 | 
 | ||||||
|  | @ -94,7 +96,7 @@ const calculateWidth = () => { | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     ref="containerRef" |     ref="containerRef" | ||||||
|     class="min-h-30px box-content flex w-full flex-row flex-wrap" |     class="box-content flex min-h-[30px] w-full flex-row flex-wrap" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|       v-for="(spu, index) in spuList" |       v-for="(spu, index) in spuList" | ||||||
|  | @ -112,28 +114,28 @@ const calculateWidth = () => { | ||||||
|       <!-- 角标 --> |       <!-- 角标 --> | ||||||
|       <div |       <div | ||||||
|         v-if="property.badge.show" |         v-if="property.badge.show" | ||||||
|         class="z-1 absolute left-0 top-0 items-center justify-center" |         class="absolute left-0 top-0 z-[1] items-center justify-center" | ||||||
|       > |       > | ||||||
|         <el-image |         <ElImage | ||||||
|           :src="property.badge.imgUrl" |           :src="property.badge.imgUrl" | ||||||
|           class="h-26px w-38px" |           class="h-[26px] w-[38px]" | ||||||
|           fit="cover" |           fit="cover" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 商品封面图 --> |       <!-- 商品封面图 --> | ||||||
|       <div |       <div | ||||||
|         class="h-140px" |         class="h-[140px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|             'w-140px': property.layoutType === 'oneColSmallImg', |             'w-[140px]': property.layoutType === 'oneColSmallImg', | ||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       > |       > | ||||||
|         <el-image :src="spu.picUrl" class="h-full w-full" fit="cover" /> |         <ElImage :src="spu.picUrl" class="h-full w-full" fit="cover" /> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         class="gap-8px p-8px box-border flex flex-col" |         class="box-border flex flex-col gap-[8px] p-[8px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -145,7 +147,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品名称 --> |         <!-- 商品名称 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.name.show" |           v-if="property.fields.name.show" | ||||||
|           class="text-14px" |           class="text-[14px]" | ||||||
|           :class="[ |           :class="[ | ||||||
|             { |             { | ||||||
|               truncate: property.layoutType !== 'oneColSmallImg', |               truncate: property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -161,7 +163,7 @@ const calculateWidth = () => { | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.introduction.show" |           v-if="property.fields.introduction.show" | ||||||
|           :style="{ color: property.fields.introduction.color }" |           :style="{ color: property.fields.introduction.color }" | ||||||
|           class="text-12px truncate" |           class="truncate text-[12px]" | ||||||
|         > |         > | ||||||
|           {{ spu.introduction }} |           {{ spu.introduction }} | ||||||
|         </div> |         </div> | ||||||
|  | @ -170,7 +172,7 @@ const calculateWidth = () => { | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.price.show" |             v-if="property.fields.price.show" | ||||||
|             :style="{ color: property.fields.price.color }" |             :style="{ color: property.fields.price.color }" | ||||||
|             class="text-16px" |             class="text-[16px]" | ||||||
|           > |           > | ||||||
|             {{ spu.point }}积分 |             {{ spu.point }}积分 | ||||||
|             {{ |             {{ | ||||||
|  | @ -183,12 +185,12 @@ const calculateWidth = () => { | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.marketPrice.show && spu.marketPrice" |             v-if="property.fields.marketPrice.show && spu.marketPrice" | ||||||
|             :style="{ color: property.fields.marketPrice.color }" |             :style="{ color: property.fields.marketPrice.color }" | ||||||
|             class="ml-4px text-10px line-through" |             class="ml-[4px] text-[10px] line-through" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.marketPrice) }} |             ¥{{ fenToYuan(spu.marketPrice) }} | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="text-12px"> |         <div class="text-[12px]"> | ||||||
|           <!-- 销量 --> |           <!-- 销量 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.salesCount.show" |             v-if="property.fields.salesCount.show" | ||||||
|  | @ -206,22 +208,22 @@ const calculateWidth = () => { | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 购买按钮 --> |       <!-- 购买按钮 --> | ||||||
|       <div class="bottom-8px right-8px absolute"> |       <div class="absolute bottom-[8px] right-[8px]"> | ||||||
|         <!-- 文字按钮 --> |         <!-- 文字按钮 --> | ||||||
|         <span |         <span | ||||||
|           v-if="property.btnBuy.type === 'text'" |           v-if="property.btnBuy.type === 'text'" | ||||||
|           :style="{ |           :style="{ | ||||||
|             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, |             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, | ||||||
|           }" |           }" | ||||||
|           class="p-x-12px p-y-4px text-12px rounded-full text-white" |           class="rounded-full px-[12px] py-[4px] text-[12px] text-white" | ||||||
|         > |         > | ||||||
|           {{ property.btnBuy.text }} |           {{ property.btnBuy.text }} | ||||||
|         </span> |         </span> | ||||||
|         <!-- 图片按钮 --> |         <!-- 图片按钮 --> | ||||||
|         <el-image |         <ElImage | ||||||
|           v-else |           v-else | ||||||
|           :src="property.btnBuy.imgUrl" |           :src="property.btnBuy.imgUrl" | ||||||
|           class="h-28px w-28px rounded-full" |           class="h-[28px] w-[28px] rounded-full" | ||||||
|           fit="cover" |           fit="cover" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,22 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { PromotionPointProperty } from './config'; | import type { PromotionPointProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { IconifyIcon } from '@vben/icons'; | ||||||
| 
 | 
 | ||||||
|  | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElInput, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
| import PointShowcase from '#/views/mall/promotion/point/components/point-showcase.vue'; | import PointShowcase from '#/views/mall/promotion/point/components/point-showcase.vue'; | ||||||
| 
 | 
 | ||||||
| // 秒杀属性面板 | // 秒杀属性面板 | ||||||
|  | @ -15,119 +29,121 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form :model="formData" label-width="80px"> |     <ElForm :model="formData" label-width="80px"> | ||||||
|       <el-card class="property-group" header="积分商城活动" shadow="never"> |       <ElCard class="property-group" header="积分商城活动" shadow="never"> | ||||||
|         <PointShowcase v-model="formData.activityIds" /> |         <PointShowcase v-model="formData.activityIds" /> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card class="property-group" header="商品样式" shadow="never"> |       <ElCard class="property-group" header="商品样式" shadow="never"> | ||||||
|         <el-form-item label="布局" prop="type"> |         <ElFormItem label="布局" prop="type"> | ||||||
|           <el-radio-group v-model="formData.layoutType"> |           <ElRadioGroup v-model="formData.layoutType"> | ||||||
|             <el-tooltip class="item" content="单列大图" placement="bottom"> |             <ElTooltip class="item" content="单列大图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColBigImg"> |               <ElRadioButton value="oneColBigImg"> | ||||||
|                 <Icon icon="fluent:text-column-one-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-one-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="单列小图" placement="bottom"> |             <ElTooltip class="item" content="单列小图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColSmallImg"> |               <ElRadioButton value="oneColSmallImg"> | ||||||
|                 <Icon icon="fluent:text-column-two-left-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-left-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="双列" placement="bottom"> |             <ElTooltip class="item" content="双列" placement="bottom"> | ||||||
|               <el-radio-button value="twoCol"> |               <ElRadioButton value="twoCol"> | ||||||
|                 <Icon icon="fluent:text-column-two-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <!--<el-tooltip class="item" content="三列" placement="bottom"> |             <!--<el-tooltip class="item" content="三列" placement="bottom"> | ||||||
|               <el-radio-button value="threeCol"> |               <el-radio-button value="threeCol"> | ||||||
|                 <Icon icon="fluent:text-column-three-24-filled" /> |                 <Icon icon="fluent:text-column-three-24-filled" /> | ||||||
|               </el-radio-button> |               </el-radio-button> | ||||||
|             </el-tooltip>--> |             </el-tooltip>--> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品名称" prop="fields.name.show"> |         <ElFormItem label="商品名称" prop="fields.name.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.name.color" /> |             <ColorInput v-model="formData.fields.name.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.name.show" /> |             <ElCheckbox v-model="formData.fields.name.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品简介" prop="fields.introduction.show"> |         <ElFormItem label="商品简介" prop="fields.introduction.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.introduction.color" /> |             <ColorInput v-model="formData.fields.introduction.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.introduction.show" /> |             <ElCheckbox v-model="formData.fields.introduction.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品价格" prop="fields.price.show"> |         <ElFormItem label="商品价格" prop="fields.price.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.price.color" /> |             <ColorInput v-model="formData.fields.price.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.price.show" /> |             <ElCheckbox v-model="formData.fields.price.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="市场价" prop="fields.marketPrice.show"> |         <ElFormItem label="市场价" prop="fields.marketPrice.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.marketPrice.color" /> |             <ColorInput v-model="formData.fields.marketPrice.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.marketPrice.show" /> |             <ElCheckbox v-model="formData.fields.marketPrice.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品销量" prop="fields.salesCount.show"> |         <ElFormItem label="商品销量" prop="fields.salesCount.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.salesCount.color" /> |             <ColorInput v-model="formData.fields.salesCount.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.salesCount.show" /> |             <ElCheckbox v-model="formData.fields.salesCount.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品库存" prop="fields.stock.show"> |         <ElFormItem label="商品库存" prop="fields.stock.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.stock.color" /> |             <ColorInput v-model="formData.fields.stock.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.stock.show" /> |             <ElCheckbox v-model="formData.fields.stock.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card class="property-group" header="角标" shadow="never"> |       <ElCard class="property-group" header="角标" shadow="never"> | ||||||
|         <el-form-item label="角标" prop="badge.show"> |         <ElFormItem label="角标" prop="badge.show"> | ||||||
|           <el-switch v-model="formData.badge.show" /> |           <ElSwitch v-model="formData.badge.show" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item |         <ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl"> | ||||||
|           v-if="formData.badge.show" |           <UploadImg | ||||||
|           label="角标" |             v-model="formData.badge.imgUrl" | ||||||
|           prop="badge.imgUrl" |             height="44px" | ||||||
|         > |             width="72px" | ||||||
|           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |             :show-description="false" | ||||||
|  |           > | ||||||
|             <template #tip> 建议尺寸:36 * 22</template> |             <template #tip> 建议尺寸:36 * 22</template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card class="property-group" header="按钮" shadow="never"> |       <ElCard class="property-group" header="按钮" shadow="never"> | ||||||
|         <el-form-item label="按钮类型" prop="btnBuy.type"> |         <ElFormItem label="按钮类型" prop="btnBuy.type"> | ||||||
|           <el-radio-group v-model="formData.btnBuy.type"> |           <ElRadioGroup v-model="formData.btnBuy.type"> | ||||||
|             <el-radio-button value="text">文字</el-radio-button> |             <ElRadioButton value="text">文字</ElRadioButton> | ||||||
|             <el-radio-button value="img">图片</el-radio-button> |             <ElRadioButton value="img">图片</ElRadioButton> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <template v-if="formData.btnBuy.type === 'text'"> |         <template v-if="formData.btnBuy.type === 'text'"> | ||||||
|           <el-form-item label="按钮文字" prop="btnBuy.text"> |           <ElFormItem label="按钮文字" prop="btnBuy.text"> | ||||||
|             <el-input v-model="formData.btnBuy.text" /> |             <ElInput v-model="formData.btnBuy.text" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |           <ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |           <ElFormItem label="右侧背景" prop="btnBuy.bgEndColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgEndColor" /> |             <ColorInput v-model="formData.btnBuy.bgEndColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|         <template v-else> |         <template v-else> | ||||||
|           <el-form-item label="图片" prop="btnBuy.imgUrl"> |           <ElFormItem label="图片" prop="btnBuy.imgUrl"> | ||||||
|             <UploadImg |             <UploadImg | ||||||
|               v-model="formData.btnBuy.imgUrl" |               v-model="formData.btnBuy.imgUrl" | ||||||
|               height="56px" |               height="56px" | ||||||
|               width="56px" |               width="56px" | ||||||
|  |               :show-description="false" | ||||||
|             > |             > | ||||||
|               <template #tip> 建议尺寸:56 * 56</template> |               <template #tip> 建议尺寸:56 * 56</template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card class="property-group" header="商品样式" shadow="never"> |       <ElCard class="property-group" header="商品样式" shadow="never"> | ||||||
|         <el-form-item label="上圆角" prop="borderRadiusTop"> |         <ElFormItem label="上圆角" prop="borderRadiusTop"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusTop" |             v-model="formData.borderRadiusTop" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -135,9 +151,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             show-input |             show-input | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="下圆角" prop="borderRadiusBottom"> |         <ElFormItem label="下圆角" prop="borderRadiusBottom"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusBottom" |             v-model="formData.borderRadiusBottom" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -145,9 +161,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             show-input |             show-input | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="间隔" prop="space"> |         <ElFormItem label="间隔" prop="space"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.space" |             v-model="formData.space" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -155,9 +171,9 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             show-input |             show-input | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,8 @@ import { ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { fenToYuan } from '@vben/utils'; | import { fenToYuan } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
| import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | ||||||
| 
 | 
 | ||||||
|  | @ -93,7 +95,7 @@ const calculateWidth = () => { | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="min-h-30px box-content flex w-full flex-row flex-wrap" |     class="box-content flex min-h-[30px] w-full flex-row flex-wrap" | ||||||
|     ref="containerRef" |     ref="containerRef" | ||||||
|   > |   > | ||||||
|     <div |     <div | ||||||
|  | @ -112,28 +114,28 @@ const calculateWidth = () => { | ||||||
|       <!-- 角标 --> |       <!-- 角标 --> | ||||||
|       <div |       <div | ||||||
|         v-if="property.badge.show" |         v-if="property.badge.show" | ||||||
|         class="z-1 absolute left-0 top-0 items-center justify-center" |         class="absolute left-0 top-0 z-[1] items-center justify-center" | ||||||
|       > |       > | ||||||
|         <el-image |         <ElImage | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.badge.imgUrl" |           :src="property.badge.imgUrl" | ||||||
|           class="h-26px w-38px" |           class="h-[26px] w-[38px]" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 商品封面图 --> |       <!-- 商品封面图 --> | ||||||
|       <div |       <div | ||||||
|         class="h-140px" |         class="h-[140px]" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|             'w-140px': property.layoutType === 'oneColSmallImg', |             'w-[140px]': property.layoutType === 'oneColSmallImg', | ||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       > |       > | ||||||
|         <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |         <ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" /> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         class="gap-8px p-8px box-border flex flex-col" |         class="box-border flex flex-col gap-2 p-2" | ||||||
|         :class="[ |         :class="[ | ||||||
|           { |           { | ||||||
|             'w-full': property.layoutType !== 'oneColSmallImg', |             'w-full': property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -145,7 +147,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品名称 --> |         <!-- 商品名称 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.name.show" |           v-if="property.fields.name.show" | ||||||
|           class="text-14px" |           class="text-sm" | ||||||
|           :class="[ |           :class="[ | ||||||
|             { |             { | ||||||
|               truncate: property.layoutType !== 'oneColSmallImg', |               truncate: property.layoutType !== 'oneColSmallImg', | ||||||
|  | @ -160,7 +162,7 @@ const calculateWidth = () => { | ||||||
|         <!-- 商品简介 --> |         <!-- 商品简介 --> | ||||||
|         <div |         <div | ||||||
|           v-if="property.fields.introduction.show" |           v-if="property.fields.introduction.show" | ||||||
|           class="text-12px truncate" |           class="truncate text-xs" | ||||||
|           :style="{ color: property.fields.introduction.color }" |           :style="{ color: property.fields.introduction.color }" | ||||||
|         > |         > | ||||||
|           {{ spu.introduction }} |           {{ spu.introduction }} | ||||||
|  | @ -169,7 +171,7 @@ const calculateWidth = () => { | ||||||
|           <!-- 价格 --> |           <!-- 价格 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.price.show" |             v-if="property.fields.price.show" | ||||||
|             class="text-16px" |             class="text-base" | ||||||
|             :style="{ color: property.fields.price.color }" |             :style="{ color: property.fields.price.color }" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.price || Infinity) }} |             ¥{{ fenToYuan(spu.price || Infinity) }} | ||||||
|  | @ -177,13 +179,13 @@ const calculateWidth = () => { | ||||||
|           <!-- 市场价 --> |           <!-- 市场价 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.marketPrice.show && spu.marketPrice" |             v-if="property.fields.marketPrice.show && spu.marketPrice" | ||||||
|             class="ml-4px text-10px line-through" |             class="ml-1 text-[10px] line-through" | ||||||
|             :style="{ color: property.fields.marketPrice.color }" |             :style="{ color: property.fields.marketPrice.color }" | ||||||
|           > |           > | ||||||
|             ¥{{ fenToYuan(spu.marketPrice) }} |             ¥{{ fenToYuan(spu.marketPrice) }} | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="text-12px"> |         <div class="text-xs"> | ||||||
|           <!-- 销量 --> |           <!-- 销量 --> | ||||||
|           <span |           <span | ||||||
|             v-if="property.fields.salesCount.show" |             v-if="property.fields.salesCount.show" | ||||||
|  | @ -201,11 +203,11 @@ const calculateWidth = () => { | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <!-- 购买按钮 --> |       <!-- 购买按钮 --> | ||||||
|       <div class="bottom-8px right-8px absolute"> |       <div class="absolute bottom-2 right-2"> | ||||||
|         <!-- 文字按钮 --> |         <!-- 文字按钮 --> | ||||||
|         <span |         <span | ||||||
|           v-if="property.btnBuy.type === 'text'" |           v-if="property.btnBuy.type === 'text'" | ||||||
|           class="p-x-12px p-y-4px text-12px rounded-full text-white" |           class="rounded-full px-3 py-1 text-xs text-white" | ||||||
|           :style="{ |           :style="{ | ||||||
|             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, |             background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`, | ||||||
|           }" |           }" | ||||||
|  | @ -213,9 +215,9 @@ const calculateWidth = () => { | ||||||
|           {{ property.btnBuy.text }} |           {{ property.btnBuy.text }} | ||||||
|         </span> |         </span> | ||||||
|         <!-- 图片按钮 --> |         <!-- 图片按钮 --> | ||||||
|         <el-image |         <ElImage | ||||||
|           v-else |           v-else | ||||||
|           class="h-28px w-28px rounded-full" |           class="h-7 w-7 rounded-full" | ||||||
|           fit="cover" |           fit="cover" | ||||||
|           :src="property.btnBuy.imgUrl" |           :src="property.btnBuy.imgUrl" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  | @ -5,9 +5,24 @@ import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckil | ||||||
| 
 | 
 | ||||||
| import { onMounted, ref } from 'vue'; | import { onMounted, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElCheckbox, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElInput, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  | } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| import { CommonStatusEnum } from '#/utils/constants'; | import { CommonStatusEnum } from '#/utils/constants'; | ||||||
| import SeckillShowcase from '#/views/mall/promotion/seckill/components/seckill-showcase.vue'; | import SeckillShowcase from '#/views/mall/promotion/seckill/components/seckill-showcase.vue'; | ||||||
| 
 | 
 | ||||||
|  | @ -31,119 +46,121 @@ onMounted(async () => { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <el-form label-width="80px" :model="formData"> |     <ElForm label-width="80px" :model="formData"> | ||||||
|       <el-card header="秒杀活动" class="property-group" shadow="never"> |       <ElCard header="秒杀活动" class="property-group" shadow="never"> | ||||||
|         <SeckillShowcase v-model="formData.activityIds" /> |         <SeckillShowcase v-model="formData.activityIds" /> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="布局" prop="type"> |         <ElFormItem label="布局" prop="type"> | ||||||
|           <el-radio-group v-model="formData.layoutType"> |           <ElRadioGroup v-model="formData.layoutType"> | ||||||
|             <el-tooltip class="item" content="单列大图" placement="bottom"> |             <ElTooltip class="item" content="单列大图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColBigImg"> |               <ElRadioButton value="oneColBigImg"> | ||||||
|                 <Icon icon="fluent:text-column-one-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-one-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="单列小图" placement="bottom"> |             <ElTooltip class="item" content="单列小图" placement="bottom"> | ||||||
|               <el-radio-button value="oneColSmallImg"> |               <ElRadioButton value="oneColSmallImg"> | ||||||
|                 <Icon icon="fluent:text-column-two-left-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-left-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip class="item" content="双列" placement="bottom"> |             <ElTooltip class="item" content="双列" placement="bottom"> | ||||||
|               <el-radio-button value="twoCol"> |               <ElRadioButton value="twoCol"> | ||||||
|                 <Icon icon="fluent:text-column-two-24-filled" /> |                 <IconifyIcon icon="fluent:text-column-two-24-filled" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <!--<el-tooltip class="item" content="三列" placement="bottom"> |             <!--<el-tooltip class="item" content="三列" placement="bottom"> | ||||||
|               <el-radio-button value="threeCol"> |               <el-radio-button value="threeCol"> | ||||||
|                 <Icon icon="fluent:text-column-three-24-filled" /> |                 <Icon icon="fluent:text-column-three-24-filled" /> | ||||||
|               </el-radio-button> |               </el-radio-button> | ||||||
|             </el-tooltip>--> |             </ElTooltip>--> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品名称" prop="fields.name.show"> |         <ElFormItem label="商品名称" prop="fields.name.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.name.color" /> |             <ColorInput v-model="formData.fields.name.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.name.show" /> |             <ElCheckbox v-model="formData.fields.name.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品简介" prop="fields.introduction.show"> |         <ElFormItem label="商品简介" prop="fields.introduction.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.introduction.color" /> |             <ColorInput v-model="formData.fields.introduction.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.introduction.show" /> |             <ElCheckbox v-model="formData.fields.introduction.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品价格" prop="fields.price.show"> |         <ElFormItem label="商品价格" prop="fields.price.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.price.color" /> |             <ColorInput v-model="formData.fields.price.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.price.show" /> |             <ElCheckbox v-model="formData.fields.price.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="市场价" prop="fields.marketPrice.show"> |         <ElFormItem label="市场价" prop="fields.marketPrice.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.marketPrice.color" /> |             <ColorInput v-model="formData.fields.marketPrice.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.marketPrice.show" /> |             <ElCheckbox v-model="formData.fields.marketPrice.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品销量" prop="fields.salesCount.show"> |         <ElFormItem label="商品销量" prop="fields.salesCount.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.salesCount.color" /> |             <ColorInput v-model="formData.fields.salesCount.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.salesCount.show" /> |             <ElCheckbox v-model="formData.fields.salesCount.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品库存" prop="fields.stock.show"> |         <ElFormItem label="商品库存" prop="fields.stock.show"> | ||||||
|           <div class="gap-8px flex"> |           <div class="gap-8px flex"> | ||||||
|             <ColorInput v-model="formData.fields.stock.color" /> |             <ColorInput v-model="formData.fields.stock.color" /> | ||||||
|             <el-checkbox v-model="formData.fields.stock.show" /> |             <ElCheckbox v-model="formData.fields.stock.show" /> | ||||||
|           </div> |           </div> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="角标" class="property-group" shadow="never"> |       <ElCard header="角标" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="角标" prop="badge.show"> |         <ElFormItem label="角标" prop="badge.show"> | ||||||
|           <el-switch v-model="formData.badge.show" /> |           <ElSwitch v-model="formData.badge.show" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item |         <ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl"> | ||||||
|           label="角标" |           <UploadImg | ||||||
|           prop="badge.imgUrl" |             v-model="formData.badge.imgUrl" | ||||||
|           v-if="formData.badge.show" |             height="44px" | ||||||
|         > |             width="72px" | ||||||
|           <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |             :show-description="false" | ||||||
|  |           > | ||||||
|             <template #tip> 建议尺寸:36 * 22</template> |             <template #tip> 建议尺寸:36 * 22</template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="按钮" class="property-group" shadow="never"> |       <ElCard header="按钮" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="按钮类型" prop="btnBuy.type"> |         <ElFormItem label="按钮类型" prop="btnBuy.type"> | ||||||
|           <el-radio-group v-model="formData.btnBuy.type"> |           <ElRadioGroup v-model="formData.btnBuy.type"> | ||||||
|             <el-radio-button value="text">文字</el-radio-button> |             <ElRadioButton value="text">文字</ElRadioButton> | ||||||
|             <el-radio-button value="img">图片</el-radio-button> |             <ElRadioButton value="img">图片</ElRadioButton> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <template v-if="formData.btnBuy.type === 'text'"> |         <template v-if="formData.btnBuy.type === 'text'"> | ||||||
|           <el-form-item label="按钮文字" prop="btnBuy.text"> |           <ElFormItem label="按钮文字" prop="btnBuy.text"> | ||||||
|             <el-input v-model="formData.btnBuy.text" /> |             <ElInput v-model="formData.btnBuy.text" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |           <ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |             <ColorInput v-model="formData.btnBuy.bgBeginColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|           <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |           <ElFormItem label="右侧背景" prop="btnBuy.bgEndColor"> | ||||||
|             <ColorInput v-model="formData.btnBuy.bgEndColor" /> |             <ColorInput v-model="formData.btnBuy.bgEndColor" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|         <template v-else> |         <template v-else> | ||||||
|           <el-form-item label="图片" prop="btnBuy.imgUrl"> |           <ElFormItem label="图片" prop="btnBuy.imgUrl"> | ||||||
|             <UploadImg |             <UploadImg | ||||||
|               v-model="formData.btnBuy.imgUrl" |               v-model="formData.btnBuy.imgUrl" | ||||||
|               height="56px" |               height="56px" | ||||||
|               width="56px" |               width="56px" | ||||||
|  |               :show-description="false" | ||||||
|             > |             > | ||||||
|               <template #tip> 建议尺寸:56 * 56</template> |               <template #tip> 建议尺寸:56 * 56</template> | ||||||
|             </UploadImg> |             </UploadImg> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="商品样式" class="property-group" shadow="never"> |       <ElCard header="商品样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="上圆角" prop="borderRadiusTop"> |         <ElFormItem label="上圆角" prop="borderRadiusTop"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusTop" |             v-model="formData.borderRadiusTop" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -151,9 +168,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="下圆角" prop="borderRadiusBottom"> |         <ElFormItem label="下圆角" prop="borderRadiusBottom"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.borderRadiusBottom" |             v-model="formData.borderRadiusBottom" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -161,9 +178,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="间隔" prop="space"> |         <ElFormItem label="间隔" prop="space"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData.space" |             v-model="formData.space" | ||||||
|             :max="100" |             :max="100" | ||||||
|             :min="0" |             :min="0" | ||||||
|  | @ -171,9 +188,9 @@ onMounted(async () => { | ||||||
|             input-size="small" |             input-size="small" | ||||||
|             :show-input-controls="false" |             :show-input-controls="false" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { SearchProperty } from './config'; | import type { SearchProperty } from './config'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
| /** 搜索框 */ | /** 搜索框 */ | ||||||
| defineOptions({ name: 'SearchBar' }); | defineOptions({ name: 'SearchBar' }); | ||||||
| defineProps<{ property: SearchProperty }>(); | defineProps<{ property: SearchProperty }>(); | ||||||
|  | @ -28,7 +30,7 @@ defineProps<{ property: SearchProperty }>(); | ||||||
|           justifyContent: property.placeholderPosition, |           justifyContent: property.placeholderPosition, | ||||||
|         }" |         }" | ||||||
|       > |       > | ||||||
|         <Icon icon="ep:search" /> |         <IconifyIcon icon="ep:search" /> | ||||||
|         <span>{{ property.placeholder || '搜索商品' }}</span> |         <span>{{ property.placeholder || '搜索商品' }}</span> | ||||||
|       </div> |       </div> | ||||||
|       <div class="right"> |       <div class="right"> | ||||||
|  | @ -37,7 +39,10 @@ defineProps<{ property: SearchProperty }>(); | ||||||
|           keyword |           keyword | ||||||
|         }}</span> |         }}</span> | ||||||
|         <!-- 扫一扫 --> |         <!-- 扫一扫 --> | ||||||
|         <Icon icon="ant-design:scan-outlined" v-show="property.showScan" /> |         <IconifyIcon | ||||||
|  |           icon="ant-design:scan-outlined" | ||||||
|  |           v-show="property.showScan" | ||||||
|  |         /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|  | @ -3,9 +3,23 @@ import type { SearchProperty } from './config'; | ||||||
| 
 | 
 | ||||||
| import { watch } from 'vue'; | import { watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
| import { isString } from '@vben/utils'; | import { isString } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElCard, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSlider, | ||||||
|  |   ElSwitch, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue'; | ||||||
|  | import Draggable from '#/components/draggable/index.vue'; | ||||||
| 
 | 
 | ||||||
| /** 搜索框属性面板 */ | /** 搜索框属性面板 */ | ||||||
| defineOptions({ name: 'SearchProperty' }); | defineOptions({ name: 'SearchProperty' }); | ||||||
|  | @ -31,9 +45,15 @@ watch( | ||||||
| <template> | <template> | ||||||
|   <ComponentContainerProperty v-model="formData.style"> |   <ComponentContainerProperty v-model="formData.style"> | ||||||
|     <!-- 表单 --> |     <!-- 表单 --> | ||||||
|     <el-form label-width="80px" :model="formData" class="m-t-8px"> |     <ElForm label-width="80px" :model="formData" class="m-t-8px"> | ||||||
|       <el-card header="搜索热词" class="property-group" shadow="never"> |       <ElCard header="搜索热词" class="property-group" shadow="never"> | ||||||
|         <Draggable v-model="formData.hotKeywords" empty-item="" :min="0"> |         <Draggable | ||||||
|  |           v-model="formData.hotKeywords" | ||||||
|  |           :empty-item="{ | ||||||
|  |             type: 'input', | ||||||
|  |             placeholder: '请输入热词', | ||||||
|  |           }" | ||||||
|  |         > | ||||||
|           <template #default="{ index }"> |           <template #default="{ index }"> | ||||||
|             <el-input |             <el-input | ||||||
|               v-model="formData.hotKeywords[index]" |               v-model="formData.hotKeywords[index]" | ||||||
|  | @ -41,59 +61,59 @@ watch( | ||||||
|             /> |             /> | ||||||
|           </template> |           </template> | ||||||
|         </Draggable> |         </Draggable> | ||||||
|       </el-card> |       </ElCard> | ||||||
|       <el-card header="搜索样式" class="property-group" shadow="never"> |       <ElCard header="搜索样式" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="框体样式"> |         <ElFormItem label="框体样式"> | ||||||
|           <el-radio-group v-model="formData!.borderRadius"> |           <ElRadioGroup v-model="formData!.borderRadius"> | ||||||
|             <el-tooltip content="方形" placement="top"> |             <ElTooltip content="方形" placement="top"> | ||||||
|               <el-radio-button :value="0"> |               <ElRadioButton :value="0"> | ||||||
|                 <Icon icon="tabler:input-search" /> |                 <IconifyIcon icon="tabler:input-search" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip content="圆形" placement="top"> |             <ElTooltip content="圆形" placement="top"> | ||||||
|               <el-radio-button :value="10"> |               <ElRadioButton :value="10"> | ||||||
|                 <Icon icon="iconoir:input-search" /> |                 <IconifyIcon icon="iconoir:input-search" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="提示文字" prop="placeholder"> |         <ElFormItem label="提示文字" prop="placeholder"> | ||||||
|           <el-input v-model="formData.placeholder" /> |           <el-input v-model="formData.placeholder" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="文本位置" prop="placeholderPosition"> |         <ElFormItem label="文本位置" prop="placeholderPosition"> | ||||||
|           <el-radio-group v-model="formData!.placeholderPosition"> |           <ElRadioGroup v-model="formData!.placeholderPosition"> | ||||||
|             <el-tooltip content="居左" placement="top"> |             <ElTooltip content="居左" placement="top"> | ||||||
|               <el-radio-button value="left"> |               <ElRadioButton value="left"> | ||||||
|                 <Icon icon="ant-design:align-left-outlined" /> |                 <IconifyIcon icon="ant-design:align-left-outlined" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|             <el-tooltip content="居中" placement="top"> |             <ElTooltip content="居中" placement="top"> | ||||||
|               <el-radio-button value="center"> |               <ElRadioButton value="center"> | ||||||
|                 <Icon icon="ant-design:align-center-outlined" /> |                 <IconifyIcon icon="ant-design:align-center-outlined" /> | ||||||
|               </el-radio-button> |               </ElRadioButton> | ||||||
|             </el-tooltip> |             </ElTooltip> | ||||||
|           </el-radio-group> |           </ElRadioGroup> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="扫一扫" prop="showScan"> |         <ElFormItem label="扫一扫" prop="showScan"> | ||||||
|           <el-switch v-model="formData!.showScan" /> |           <ElSwitch v-model="formData!.showScan" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="框体高度" prop="height"> |         <ElFormItem label="框体高度" prop="height"> | ||||||
|           <el-slider |           <ElSlider | ||||||
|             v-model="formData!.height" |             v-model="formData!.height" | ||||||
|             :max="50" |             :max="50" | ||||||
|             :min="28" |             :min="28" | ||||||
|             show-input |             show-input | ||||||
|             input-size="small" |             input-size="small" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="框体颜色" prop="backgroundColor"> |         <ElFormItem label="框体颜色" prop="backgroundColor"> | ||||||
|           <ColorInput v-model="formData.backgroundColor" /> |           <ColorInput v-model="formData.backgroundColor" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item class="lef" label="文本颜色" prop="textColor"> |         <ElFormItem class="lef" label="文本颜色" prop="textColor"> | ||||||
|           <ColorInput v-model="formData.textColor" /> |           <ColorInput v-model="formData.textColor" /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-card> |       </ElCard> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </ComponentContainerProperty> |   </ComponentContainerProperty> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { TabBarProperty } from './config'; | import type { TabBarProperty } from './config'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElImage } from 'element-plus'; | ||||||
|  | 
 | ||||||
| /** 页面底部导航栏 */ | /** 页面底部导航栏 */ | ||||||
| defineOptions({ name: 'TabBar' }); | defineOptions({ name: 'TabBar' }); | ||||||
| 
 | 
 | ||||||
|  | @ -24,13 +28,13 @@ defineProps<{ property: TabBarProperty }>(); | ||||||
|         :key="index" |         :key="index" | ||||||
|         class="tab-bar-item" |         class="tab-bar-item" | ||||||
|       > |       > | ||||||
|         <el-image :src="index === 0 ? item.activeIconUrl : item.iconUrl"> |         <ElImage :src="index === 0 ? item.activeIconUrl : item.iconUrl"> | ||||||
|           <template #error> |           <template #error> | ||||||
|             <div class="flex h-full w-full items-center justify-center"> |             <div class="flex h-full w-full items-center justify-center"> | ||||||
|               <Icon icon="ep:picture" /> |               <IconifyIcon icon="ep:picture" /> | ||||||
|             </div> |             </div> | ||||||
|           </template> |           </template> | ||||||
|         </el-image> |         </ElImage> | ||||||
|         <span |         <span | ||||||
|           :style="{ |           :style="{ | ||||||
|             color: |             color: | ||||||
|  |  | ||||||
|  | @ -1,7 +1,24 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { TabBarProperty } from './config'; | import type { TabBarProperty } from './config'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
| import { useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElInput, | ||||||
|  |   ElOption, | ||||||
|  |   ElRadioButton, | ||||||
|  |   ElRadioGroup, | ||||||
|  |   ElSelect, | ||||||
|  |   ElText, | ||||||
|  | } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import AppLinkInput from '#/components/app-link-input/index.vue'; | ||||||
|  | import ColorInput from '#/components/color-input/index.vue'; | ||||||
|  | import Draggable from '#/components/draggable/index.vue'; | ||||||
|  | import UploadImg from '#/components/upload/image-upload.vue'; | ||||||
| 
 | 
 | ||||||
| import { component, THEME_LIST } from './config'; | import { component, THEME_LIST } from './config'; | ||||||
| // 底部导航栏 | // 底部导航栏 | ||||||
|  | @ -26,10 +43,10 @@ const handleThemeChange = () => { | ||||||
| <template> | <template> | ||||||
|   <div class="tab-bar"> |   <div class="tab-bar"> | ||||||
|     <!-- 表单 --> |     <!-- 表单 --> | ||||||
|     <el-form :model="formData" label-width="80px"> |     <ElForm :model="formData" label-width="80px"> | ||||||
|       <el-form-item label="主题" prop="theme"> |       <ElFormItem label="主题" prop="theme"> | ||||||
|         <el-select v-model="formData!.theme" @change="handleThemeChange"> |         <ElSelect v-model="formData!.theme" @change="handleThemeChange"> | ||||||
|           <el-option |           <ElOption | ||||||
|             v-for="(theme, index) in THEME_LIST" |             v-for="(theme, index) in THEME_LIST" | ||||||
|             :key="index" |             :key="index" | ||||||
|             :label="theme.name" |             :label="theme.name" | ||||||
|  | @ -37,55 +54,56 @@ const handleThemeChange = () => { | ||||||
|           > |           > | ||||||
|             <template #default> |             <template #default> | ||||||
|               <div class="flex items-center justify-between"> |               <div class="flex items-center justify-between"> | ||||||
|                 <Icon :icon="theme.icon" :color="theme.color" /> |                 <IconifyIcon :icon="theme.icon" :color="theme.color" /> | ||||||
|                 <span>{{ theme.name }}</span> |                 <span>{{ theme.name }}</span> | ||||||
|               </div> |               </div> | ||||||
|             </template> |             </template> | ||||||
|           </el-option> |           </ElOption> | ||||||
|         </el-select> |         </ElSelect> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="默认颜色"> |       <ElFormItem label="默认颜色"> | ||||||
|         <ColorInput v-model="formData!.style.color" /> |         <ColorInput v-model="formData!.style.color" /> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="选中颜色"> |       <ElFormItem label="选中颜色"> | ||||||
|         <ColorInput v-model="formData!.style.activeColor" /> |         <ColorInput v-model="formData!.style.activeColor" /> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="导航背景"> |       <ElFormItem label="导航背景"> | ||||||
|         <el-radio-group v-model="formData!.style.bgType"> |         <ElRadioGroup v-model="formData!.style.bgType"> | ||||||
|           <el-radio-button value="color">纯色</el-radio-button> |           <ElRadioButton value="color">纯色</ElRadioButton> | ||||||
|           <el-radio-button value="img">图片</el-radio-button> |           <ElRadioButton value="img">图片</ElRadioButton> | ||||||
|         </el-radio-group> |         </ElRadioGroup> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'"> |       <ElFormItem label="选择颜色" v-if="formData!.style.bgType === 'color'"> | ||||||
|         <ColorInput v-model="formData!.style.bgColor" /> |         <ColorInput v-model="formData!.style.bgColor" /> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
|       <el-form-item label="选择图片" v-if="formData!.style.bgType === 'img'"> |       <ElFormItem label="选择图片" v-if="formData!.style.bgType === 'img'"> | ||||||
|         <UploadImg |         <UploadImg | ||||||
|           v-model="formData!.style.bgImg" |           v-model="formData!.style.bgImg" | ||||||
|           width="100%" |           width="100%" | ||||||
|           height="50px" |           height="50px" | ||||||
|           class="min-w-200px" |           class="min-w-[200px]" | ||||||
|  |           :show-description="false" | ||||||
|         > |         > | ||||||
|           <template #tip> 建议尺寸 375 * 50 </template> |           <template #tip> 建议尺寸 375 * 50 </template> | ||||||
|         </UploadImg> |         </UploadImg> | ||||||
|       </el-form-item> |       </ElFormItem> | ||||||
| 
 | 
 | ||||||
|       <el-text tag="p">图标设置</el-text> |       <ElText tag="p">图标设置</ElText> | ||||||
|       <el-text type="info" size="small"> |       <ElText type="info" size="small"> | ||||||
|         拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 |         拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 | ||||||
|       </el-text> |       </ElText> | ||||||
|       <Draggable v-model="formData.items" :limit="5"> |       <Draggable v-model="formData.items" :limit="5"> | ||||||
|         <template #default="{ element }"> |         <template #default="{ element }"> | ||||||
|           <div class="m-b-8px flex items-center justify-around"> |           <div class="mb-2 flex items-center justify-around"> | ||||||
|             <div class="flex flex-col items-center justify-between"> |             <div class="flex flex-col items-center justify-between"> | ||||||
|               <UploadImg |               <UploadImg | ||||||
|                 v-model="element.iconUrl" |                 v-model="element.iconUrl" | ||||||
|                 width="40px" |                 width="40px" | ||||||
|                 height="40px" |                 height="40px" | ||||||
|                 :show-delete="false" |                 :show-delete="false" | ||||||
|                 :show-btn-text="false" |                 :show-description="false" | ||||||
|               /> |               /> | ||||||
|               <el-text size="small">未选中</el-text> |               <ElText size="small">未选中</ElText> | ||||||
|             </div> |             </div> | ||||||
|             <div> |             <div> | ||||||
|               <UploadImg |               <UploadImg | ||||||
|  | @ -93,30 +111,20 @@ const handleThemeChange = () => { | ||||||
|                 width="40px" |                 width="40px" | ||||||
|                 height="40px" |                 height="40px" | ||||||
|                 :show-delete="false" |                 :show-delete="false" | ||||||
|                 :show-btn-text="false" |                 :show-description="false" | ||||||
|               /> |               /> | ||||||
|               <el-text>已选中</el-text> |               <ElText>已选中</ElText> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|           <el-form-item |           <ElFormItem prop="text" label="文字" label-width="48px" class="mb-2"> | ||||||
|             prop="text" |             <ElInput v-model="element.text" placeholder="请输入文字" /> | ||||||
|             label="文字" |           </ElFormItem> | ||||||
|             label-width="48px" |           <ElFormItem prop="url" label="链接" label-width="48px" class="mb-0"> | ||||||
|             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" /> |             <AppLinkInput v-model="element.url" /> | ||||||
|           </el-form-item> |           </ElFormItem> | ||||||
|         </template> |         </template> | ||||||
|       </Draggable> |       </Draggable> | ||||||
|     </el-form> |     </ElForm> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,7 +18,12 @@ const rules = {}; | ||||||
|     <el-form label-width="85px" :model="formData" :rules="rules"> |     <el-form label-width="85px" :model="formData" :rules="rules"> | ||||||
|       <el-card header="风格" class="property-group" shadow="never"> |       <el-card header="风格" class="property-group" shadow="never"> | ||||||
|         <el-form-item label="背景图片" prop="bgImgUrl"> |         <el-form-item label="背景图片" prop="bgImgUrl"> | ||||||
|           <UploadImg v-model="formData.bgImgUrl" width="100%" height="40px"> |           <UploadImg | ||||||
|  |             v-model="formData.bgImgUrl" | ||||||
|  |             width="100%" | ||||||
|  |             height="40px" | ||||||
|  |             :show-description="false" | ||||||
|  |           > | ||||||
|             <template #tip>建议尺寸 750*80</template> |             <template #tip>建议尺寸 750*80</template> | ||||||
|           </UploadImg> |           </UploadImg> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|  |  | ||||||
|  | @ -42,6 +42,7 @@ const formData = useVModel(props, 'modelValue', emit); | ||||||
|           height="80px" |           height="80px" | ||||||
|           width="100%" |           width="100%" | ||||||
|           class="min-w-80px" |           class="min-w-80px" | ||||||
|  |           :show-description="false" | ||||||
|         > |         > | ||||||
|           <template #tip> 建议宽度750 </template> |           <template #tip> 建议宽度750 </template> | ||||||
|         </UploadImg> |         </UploadImg> | ||||||
|  |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| import { components } from './components/mobile/index'; |  | ||||||
| 
 |  | ||||||
| export default { |  | ||||||
|   components: { ...components }, |  | ||||||
| }; |  | ||||||
|  | @ -3,20 +3,40 @@ import type { DiyComponent, DiyComponentLibrary, PageConfig } from './util'; | ||||||
| 
 | 
 | ||||||
| import { inject, onMounted, ref, unref, watch } from 'vue'; | import { inject, onMounted, ref, unref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
| import { cloneDeep, isEmpty, isString } from '@vben/utils'; | import { cloneDeep, isEmpty, isString } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { | ||||||
|  |   ElAside, | ||||||
|  |   ElButtonGroup, | ||||||
|  |   ElCard, | ||||||
|  |   ElContainer, | ||||||
|  |   ElDialog, | ||||||
|  |   ElHeader, | ||||||
|  |   ElScrollbar, | ||||||
|  |   ElTag, | ||||||
|  |   ElText, | ||||||
|  |   ElTooltip, | ||||||
|  | } from 'element-plus'; | ||||||
| import draggable from 'vuedraggable'; | import draggable from 'vuedraggable'; | ||||||
| 
 | 
 | ||||||
| import { componentConfigs } from '#/components/diy-editor/components/mobile'; | import statusBarImg from '#/assets/imgs/diy/statusBar.png'; | ||||||
|  | import { | ||||||
|  |   componentConfigs, | ||||||
|  |   components, | ||||||
|  | } from '#/components/diy-editor/components/mobile'; | ||||||
| import { component as PAGE_CONFIG_COMPONENT } from '#/components/diy-editor/components/mobile/PageConfig/config'; | import { component as PAGE_CONFIG_COMPONENT } from '#/components/diy-editor/components/mobile/PageConfig/config'; | ||||||
| 
 | 
 | ||||||
|  | import ComponentContainer from './components/ComponentContainer.vue'; | ||||||
| import ComponentLibrary from './components/ComponentLibrary.vue'; | import ComponentLibrary from './components/ComponentLibrary.vue'; | ||||||
| import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'; | import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'; | ||||||
| import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'; | import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'; | ||||||
| 
 | 
 | ||||||
| /** 页面装修详情页 */ | /** 页面装修详情页 */ | ||||||
| defineOptions({ name: 'DiyPageDetail' }); | defineOptions({ | ||||||
| 
 |   name: 'DiyPageDetail', | ||||||
|  |   components, | ||||||
|  | }); | ||||||
| // 定义属性 | // 定义属性 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   // 页面配置,支持Json字符串 |   // 页面配置,支持Json字符串 | ||||||
|  | @ -34,7 +54,6 @@ const props = defineProps({ | ||||||
|   // 预览地址:提供了预览地址,才会显示预览按钮 |   // 预览地址:提供了预览地址,才会显示预览按钮 | ||||||
|   previewUrl: { type: String, default: '' }, |   previewUrl: { type: String, default: '' }, | ||||||
| }); | }); | ||||||
| 
 |  | ||||||
| // 工具栏操作 | // 工具栏操作 | ||||||
| const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue']); | const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue']); | ||||||
| 
 | 
 | ||||||
|  | @ -271,218 +290,217 @@ watch( | ||||||
|   () => [props.showPageConfig, props.showNavigationBar, props.showTabBar], |   () => [props.showPageConfig, props.showNavigationBar, props.showTabBar], | ||||||
|   () => setDefaultSelectedComponent(), |   () => setDefaultSelectedComponent(), | ||||||
| ); | ); | ||||||
| 
 | onMounted(() => { | ||||||
| onMounted(() => setDefaultSelectedComponent()); |   setDefaultSelectedComponent(); | ||||||
|  | }); | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <el-container class="editor"> |   <div> | ||||||
|     <!-- 顶部:工具栏 --> |     <ElContainer class="editor"> | ||||||
|     <el-header class="editor-header"> |       <!-- 顶部:工具栏 --> | ||||||
|       <!-- 左侧操作区 --> |       <ElHeader class="editor-header"> | ||||||
|       <slot name="toolBarLeft"></slot> |         <!-- 左侧操作区 --> | ||||||
|       <!-- 中心操作区 --> |         <slot name="toolBarLeft"></slot> | ||||||
|       <div class="header-center flex flex-1 items-center justify-center"> |         <!-- 中心操作区 --> | ||||||
|         <span>{{ title }}</span> |         <div class="header-center flex flex-1 items-center justify-center"> | ||||||
|       </div> |           <span>{{ title }}</span> | ||||||
|       <!-- 右侧操作区 --> |         </div> | ||||||
|       <el-button-group class="header-right"> |         <!-- 右侧操作区 --> | ||||||
|         <el-tooltip content="重置"> |         <ElButtonGroup class="header-right"> | ||||||
|           <el-button @click="handleReset"> |           <ElTooltip content="重置"> | ||||||
|             <Icon :size="24" icon="system-uicons:reset-alt" /> |             <ElButton @click="handleReset"> | ||||||
|           </el-button> |               <IconifyIcon :size="24" icon="system-uicons:reset-alt" /> | ||||||
|         </el-tooltip> |             </ElButton> | ||||||
|         <el-tooltip v-if="previewUrl" content="预览"> |           </ElTooltip> | ||||||
|           <el-button @click="handlePreview"> |           <ElTooltip v-if="previewUrl" content="预览"> | ||||||
|             <Icon :size="24" icon="ep:view" /> |             <ElButton @click="handlePreview"> | ||||||
|           </el-button> |               <IconifyIcon :size="24" icon="ep:view" /> | ||||||
|         </el-tooltip> |             </ElButton> | ||||||
|         <el-tooltip content="保存"> |           </ElTooltip> | ||||||
|           <el-button @click="handleSave"> |           <ElTooltip content="保存"> | ||||||
|             <Icon :size="24" icon="ep:check" /> |             <ElButton @click="handleSave"> | ||||||
|           </el-button> |               <IconifyIcon :size="24" icon="ep:check" /> | ||||||
|         </el-tooltip> |             </ElButton> | ||||||
|       </el-button-group> |           </ElTooltip> | ||||||
|     </el-header> |         </ElButtonGroup> | ||||||
|  |       </ElHeader> | ||||||
| 
 | 
 | ||||||
|     <!-- 中心区域 --> |       <!-- 中心区域 --> | ||||||
|     <el-container class="editor-container"> |       <ElContainer class="editor-container"> | ||||||
|       <!-- 左侧:组件库(ComponentLibrary) --> |         <!-- 左侧:组件库(ComponentLibrary) --> | ||||||
|       <ComponentLibrary |         <ComponentLibrary | ||||||
|         v-if="libs && libs.length > 0" |           v-if="libs && libs.length > 0" | ||||||
|         ref="componentLibrary" |           ref="componentLibrary" | ||||||
|         :list="libs" |           :list="libs" | ||||||
|       /> |         /> | ||||||
|       <!-- 中心:设计区域(ComponentContainer) --> |         <!-- 中心:设计区域(ComponentContainer) --> | ||||||
|       <div class="editor-center page-prop-area" @click="handlePageSelected"> |         <div class="editor-center page-prop-area" @click="handlePageSelected"> | ||||||
|         <!-- 手机顶部 --> |           <!-- 手机顶部 --> | ||||||
|         <div class="editor-design-top"> |           <div class="editor-design-top"> | ||||||
|           <!-- 手机顶部状态栏 --> |             <!-- 手机顶部状态栏 --> | ||||||
|           <img |             <img alt="" class="status-bar" :src="statusBarImg" /> | ||||||
|             alt="" |             <!-- 手机顶部导航栏 --> | ||||||
|             class="status-bar" |             <ComponentContainer | ||||||
|             src="@/assets/imgs/diy/statusBar.png" |               v-if="showNavigationBar" | ||||||
|           /> |               :active="selectedComponent?.id === navigationBarComponent.id" | ||||||
|           <!-- 手机顶部导航栏 --> |               :component="navigationBarComponent" | ||||||
|           <ComponentContainer |               :show-toolbar="false" | ||||||
|             v-if="showNavigationBar" |               class="cursor-pointer" | ||||||
|             :active="selectedComponent?.id === navigationBarComponent.id" |               @click="handleNavigationBarSelected" | ||||||
|             :component="navigationBarComponent" |             /> | ||||||
|             :show-toolbar="false" |           </div> | ||||||
|             class="cursor-pointer!" |           <!-- 绝对定位的组件:例如 弹窗、浮动按钮等 --> | ||||||
|             @click="handleNavigationBarSelected" |           <div | ||||||
|           /> |             v-for="(component, index) in pageComponents" | ||||||
|         </div> |             :key="index" | ||||||
|         <!-- 绝对定位的组件:例如 弹窗、浮动按钮等 --> |             @click="handleComponentSelected(component, index)" | ||||||
|         <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="true" |  | ||||||
|             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: number) => 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="gap-8px flex items-center"> |  | ||||||
|               <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 |             <component | ||||||
|               :is="`${selectedComponent?.id}Property`" |               :is="component.id" | ||||||
|               :key="selectedComponent?.uid || selectedComponent?.id" |               v-if=" | ||||||
|               v-model="selectedComponent.property" |                 component.position === 'fixed' && | ||||||
|  |                 selectedComponent?.uid === component.uid | ||||||
|  |               " | ||||||
|  |               :property="component.property" | ||||||
|             /> |             /> | ||||||
|           </el-scrollbar> |           </div> | ||||||
|         </el-card> |           <!-- 手机页面编辑区域 --> | ||||||
|       </el-aside> |           <ElScrollbar | ||||||
|     </el-container> |             :view-style="{ | ||||||
|   </el-container> |               backgroundColor: pageConfigComponent.property.backgroundColor, | ||||||
|  |               backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`, | ||||||
|  |             }" | ||||||
|  |             view-class="phone-container" | ||||||
|  |             wrap-class="editor-design-center page-prop-area" | ||||||
|  |             style="height: calc(100vh - 135px - 120px)" | ||||||
|  |           > | ||||||
|  |             <draggable | ||||||
|  |               v-model="pageComponents" | ||||||
|  |               :animation="200" | ||||||
|  |               :force-fallback="true" | ||||||
|  |               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: number) => handleMoveComponent(index, direction) | ||||||
|  |                   " | ||||||
|  |                 /> | ||||||
|  |               </template> | ||||||
|  |             </draggable> | ||||||
|  |           </ElScrollbar> | ||||||
|  |           <!-- 手机底部导航 --> | ||||||
|  |           <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 gap-2"> | ||||||
|  |             <ElTag | ||||||
|  |               v-if="showPageConfig" | ||||||
|  |               :effect=" | ||||||
|  |                 selectedComponent?.uid === pageConfigComponent.uid | ||||||
|  |                   ? 'dark' | ||||||
|  |                   : 'plain' | ||||||
|  |               " | ||||||
|  |               :type=" | ||||||
|  |                 selectedComponent?.uid === pageConfigComponent.uid | ||||||
|  |                   ? 'primary' | ||||||
|  |                   : 'info' | ||||||
|  |               " | ||||||
|  |               size="large" | ||||||
|  |               @click="handleComponentSelected(pageConfigComponent)" | ||||||
|  |             > | ||||||
|  |               <IconifyIcon :icon="pageConfigComponent.icon" :size="12" /> | ||||||
|  |               <span>{{ pageConfigComponent.name }}</span> | ||||||
|  |             </ElTag> | ||||||
|  |             <template v-for="(component, index) in pageComponents" :key="index"> | ||||||
|  |               <ElTag | ||||||
|  |                 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)" | ||||||
|  |               > | ||||||
|  |                 <IconifyIcon :icon="component.icon" :size="12" /> | ||||||
|  |                 <span>{{ component.name }}</span> | ||||||
|  |               </ElTag> | ||||||
|  |             </template> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <!-- 右侧:属性面板(ComponentContainerProperty) --> | ||||||
|  |         <ElAside | ||||||
|  |           v-if="selectedComponent?.property" | ||||||
|  |           class="editor-right" | ||||||
|  |           width="350px" | ||||||
|  |         > | ||||||
|  |           <ElCard | ||||||
|  |             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-2"> | ||||||
|  |                 <IconifyIcon :icon="selectedComponent?.icon" color="gray" /> | ||||||
|  |                 <span>{{ selectedComponent?.name }}</span> | ||||||
|  |               </div> | ||||||
|  |             </template> | ||||||
|  |             <ElScrollbar | ||||||
|  |               class="m-[calc(0px-var(--el-card-padding))]" | ||||||
|  |               view-class="p-[var(--el-card-padding)] pb-[calc(var(--el-card-padding)+var(--el-card-padding))] property" | ||||||
|  |             > | ||||||
|  |               <component | ||||||
|  |                 :is="`${selectedComponent?.id}Property`" | ||||||
|  |                 :key="selectedComponent?.uid || selectedComponent?.id" | ||||||
|  |                 v-model="selectedComponent.property" | ||||||
|  |               /> | ||||||
|  |             </ElScrollbar> | ||||||
|  |           </ElCard> | ||||||
|  |         </ElAside> | ||||||
|  |       </ElContainer> | ||||||
|  |     </ElContainer> | ||||||
| 
 | 
 | ||||||
|   <!-- 预览弹框 --> |     <!-- 预览弹框 --> | ||||||
|   <Dialog v-model="previewDialogVisible" title="预览" width="700"> |     <ElDialog v-model="previewDialogVisible" title="预览" width="700"> | ||||||
|     <div class="flex justify-around"> |       <div class="flex justify-around"> | ||||||
|       <IFrame |         <IFrame | ||||||
|         :src="previewUrl" |           :src="previewUrl" | ||||||
|         class="w-375px border-4px border-rounded-8px p-2px h-667px! border-solid" |           class="h-[667px] w-[375px] rounded-lg border-4 border-solid p-0.5" | ||||||
|       /> |         /> | ||||||
|       <div class="flex flex-col"> |         <div class="flex flex-col"> | ||||||
|         <el-text>手机扫码预览</el-text> |           <ElText>手机扫码预览</ElText> | ||||||
|         <Qrcode :text="previewUrl" logo="/logo.gif" /> |           <Qrcode :text="previewUrl" logo="/logo.gif" /> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </ElDialog> | ||||||
|   </Dialog> |   </div> | ||||||
| </template> | </template> | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| /* 手机宽度 */ | /* 手机宽度 */ | ||||||
|  | @ -493,7 +511,6 @@ $phone-width: 375px; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   margin: calc(0px - var(--app-content-padding)); |  | ||||||
| 
 | 
 | ||||||
|   /* 顶部:工具栏 */ |   /* 顶部:工具栏 */ | ||||||
|   .editor-header { |   .editor-header { | ||||||
|  | @ -525,15 +542,10 @@ $phone-width: 375px; | ||||||
| 
 | 
 | ||||||
|   /* 中心操作区 */ |   /* 中心操作区 */ | ||||||
|   .editor-container { |   .editor-container { | ||||||
|     height: calc( |     height: calc(100vh - 135px); | ||||||
|       100vh - var(--top-tool-height) - var(--tags-view-height) - var( |  | ||||||
|           --app-footer-height |  | ||||||
|         ) - |  | ||||||
|         42px |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     /* 右侧属性面板 */ |     /* 右侧属性面板 */ | ||||||
|     .editor-right { |     :deep(.editor-right) { | ||||||
|       flex-shrink: 0; |       flex-shrink: 0; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|       box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%); |       box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%); | ||||||
|  | @ -624,7 +636,6 @@ $phone-width: 375px; | ||||||
|         right: 16px; |         right: 16px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: 8px; |  | ||||||
| 
 | 
 | ||||||
|         :deep(.el-tag) { |         :deep(.el-tag) { | ||||||
|           border: none; |           border: none; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,96 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | import { cloneDeep } from '@vben/utils'; | ||||||
|  | 
 | ||||||
|  | import { useVModel } from '@vueuse/core'; | ||||||
|  | // 拖拽组件 | ||||||
|  | import VueDraggable from 'vuedraggable'; | ||||||
|  | 
 | ||||||
|  | // 拖拽组件封装 | ||||||
|  | defineOptions({ name: 'Draggable' }); | ||||||
|  | 
 | ||||||
|  | // 定义属性 | ||||||
|  | const props = defineProps({ | ||||||
|  |   // 绑定值 | ||||||
|  |   modelValue: { | ||||||
|  |     type: Array, | ||||||
|  |     default: () => [], | ||||||
|  |   }, | ||||||
|  |   // 空的元素:点击添加按钮时,创建元素并添加到列表;默认为空对象 | ||||||
|  |   emptyItem: { | ||||||
|  |     type: Object, | ||||||
|  |     default: () => ({}), | ||||||
|  |   }, | ||||||
|  |   // 数量限制:默认为0,表示不限制 | ||||||
|  |   limit: { | ||||||
|  |     type: Number, | ||||||
|  |     default: Number.MAX_VALUE, | ||||||
|  |   }, | ||||||
|  |   // 最小数量:默认为1 | ||||||
|  |   min: { | ||||||
|  |     type: Number, | ||||||
|  |     default: 1, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | // 定义事件 | ||||||
|  | const emit = defineEmits(['update:modelValue']); | ||||||
|  | const formData = useVModel(props, 'modelValue', emit); | ||||||
|  | 
 | ||||||
|  | // 处理添加 | ||||||
|  | const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {})); | ||||||
|  | // 处理删除 | ||||||
|  | const handleDelete = (index: number) => formData.value.splice(index, 1); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text> | ||||||
|  |   <VueDraggable | ||||||
|  |     :list="formData" | ||||||
|  |     :force-fallback="true" | ||||||
|  |     :animation="200" | ||||||
|  |     handle=".drag-icon" | ||||||
|  |     class="mt-2" | ||||||
|  |     item-key="index" | ||||||
|  |   > | ||||||
|  |     <template #item="{ element, index }"> | ||||||
|  |       <div class="mb-1 flex flex-col gap-1 rounded border border-gray-200 p-2"> | ||||||
|  |         <!-- 操作按钮区 --> | ||||||
|  |         <div | ||||||
|  |           class="-m-2 mb-1 flex flex-row items-center justify-between p-2" | ||||||
|  |           style="background-color: var(--app-content-bg-color)" | ||||||
|  |         > | ||||||
|  |           <el-tooltip content="拖动排序"> | ||||||
|  |             <IconifyIcon | ||||||
|  |               icon="ic:round-drag-indicator" | ||||||
|  |               class="drag-icon cursor-move" | ||||||
|  |               style="color: #8a909c" | ||||||
|  |             /> | ||||||
|  |           </el-tooltip> | ||||||
|  |           <el-tooltip content="删除"> | ||||||
|  |             <IconifyIcon | ||||||
|  |               icon="ep:delete" | ||||||
|  |               class="cursor-pointer text-red-500" | ||||||
|  |               v-if="formData.length > min" | ||||||
|  |               @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="mt-1 w-full" | ||||||
|  |       :disabled="limit > 0 && formData.length >= limit" | ||||||
|  |       @click="handleAdd" | ||||||
|  |     > | ||||||
|  |       <IconifyIcon icon="ep:plus" /><span>添加</span> | ||||||
|  |     </el-button> | ||||||
|  |   </el-tooltip> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="scss"></style> | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { useVModels } from '@vueuse/core'; | ||||||
|  | import { ElColorPicker, ElInput } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import { PREDEFINE_COLORS } from '#/utils/constants'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 带颜色选择器输入框 | ||||||
|  |  */ | ||||||
|  | defineOptions({ name: 'InputWithColor' }); | ||||||
|  | 
 | ||||||
|  | const props = defineProps({ | ||||||
|  |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
|  |   color: { | ||||||
|  |     type: String, | ||||||
|  |     default: '', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | const emit = defineEmits(['update:modelValue', 'update:color']); | ||||||
|  | const { modelValue, color } = useVModels(props, emit); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <ElInput v-model="modelValue" v-bind="$attrs"> | ||||||
|  |     <template #append> | ||||||
|  |       <ElColorPicker v-model="color" :predefine="PREDEFINE_COLORS" /> | ||||||
|  |     </template> | ||||||
|  |   </ElInput> | ||||||
|  | </template> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | :deep(.el-input-group__append) { | ||||||
|  |   padding: 0; | ||||||
|  | 
 | ||||||
|  |   .el-color-picker__trigger { | ||||||
|  |     padding: 0; | ||||||
|  |     border-left: none; | ||||||
|  |     border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius) | ||||||
|  |       0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,300 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { Point, Rect } from './util'; | ||||||
|  | 
 | ||||||
|  | import { ref, watch } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { createRect, isContains, isOverlap } from './util'; | ||||||
|  | 
 | ||||||
|  | // 魔方编辑器 | ||||||
|  | // 有两部分组成: | ||||||
|  | // 1. 魔方矩阵:位于底层,由方块组件的二维表格,用于创建热区 | ||||||
|  | //    操作方法: | ||||||
|  | //    1.1 点击其中一个方块就会进入热区选择模式 | ||||||
|  | //    1.2 再次点击另外一个方块时,结束热区选择模式 | ||||||
|  | //    1.3 在两个方块中间的区域创建热区 | ||||||
|  | //    如果两次点击的都是同一方块,就只创建一个格子的热区 | ||||||
|  | // 2. 热区:位于顶层,采用绝对定位,覆盖在魔方矩阵上面。 | ||||||
|  | defineOptions({ name: 'MagicCubeEditor' }); | ||||||
|  | 
 | ||||||
|  | // 定义属性 | ||||||
|  | const props = defineProps({ | ||||||
|  |   // 热区列表 | ||||||
|  |   modelValue: { | ||||||
|  |     type: Array as () => Rect[], | ||||||
|  |     default: () => [], | ||||||
|  |   }, | ||||||
|  |   // 行数,默认 4 行 | ||||||
|  |   rows: { | ||||||
|  |     type: Number, | ||||||
|  |     default: 4, | ||||||
|  |   }, | ||||||
|  |   // 列数,默认 4 列 | ||||||
|  |   cols: { | ||||||
|  |     type: Number, | ||||||
|  |     default: 4, | ||||||
|  |   }, | ||||||
|  |   // 方块大小,单位px,默认75px | ||||||
|  |   cubeSize: { | ||||||
|  |     type: Number, | ||||||
|  |     default: 75, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // 发送模型更新 | ||||||
|  | const emit = defineEmits(['update:modelValue', 'hotAreaSelected']); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 方块 | ||||||
|  |  * @property active 是否激活 | ||||||
|  |  */ | ||||||
|  | type Cube = Point & { active: boolean }; | ||||||
|  | 
 | ||||||
|  | // 魔方矩阵:所有的方块 | ||||||
|  | const cubes = ref<Cube[][]>([]); | ||||||
|  | // 监听行数、列数变化 | ||||||
|  | watch( | ||||||
|  |   () => [props.rows, props.cols], | ||||||
|  |   () => { | ||||||
|  |     // 清空魔方 | ||||||
|  |     cubes.value = []; | ||||||
|  |     if (!props.rows || !props.cols) return; | ||||||
|  | 
 | ||||||
|  |     // 初始化魔方 | ||||||
|  |     for (let row = 0; row < props.rows; row++) { | ||||||
|  |       cubes.value[row] = []; | ||||||
|  |       for (let col = 0; col < props.cols; col++) { | ||||||
|  |         cubes.value[row]!.push({ x: col, y: row, active: false }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { immediate: true }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | // 热区列表 | ||||||
|  | const hotAreas = ref<Rect[]>([]); | ||||||
|  | // 初始化热区 | ||||||
|  | watch( | ||||||
|  |   () => props.modelValue, | ||||||
|  |   () => (hotAreas.value = props.modelValue || []), | ||||||
|  |   { immediate: true }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | // 热区起始方块 | ||||||
|  | const hotAreaBeginCube = ref<Cube>(); | ||||||
|  | // 是否开启了热区选择模式 | ||||||
|  | const isHotAreaSelectMode = () => !!hotAreaBeginCube.value; | ||||||
|  | /** | ||||||
|  |  * 处理鼠标点击方块 | ||||||
|  |  * | ||||||
|  |  * @param currentRow 当前行号 | ||||||
|  |  * @param currentCol 当前列号 | ||||||
|  |  */ | ||||||
|  | const handleCubeClick = (currentRow: number, currentCol: number) => { | ||||||
|  |   const currentCube = cubes.value[currentRow]?.[currentCol]; | ||||||
|  |   if (!currentCube) return; | ||||||
|  | 
 | ||||||
|  |   // 情况1:进入热区选择模式 | ||||||
|  |   if (!isHotAreaSelectMode()) { | ||||||
|  |     hotAreaBeginCube.value = currentCube; | ||||||
|  |     hotAreaBeginCube.value!.active = true; | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 情况2:结束热区选择模式 | ||||||
|  |   hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube)); | ||||||
|  |   // 结束热区选择模式 | ||||||
|  |   exitHotAreaSelectMode(); | ||||||
|  |   // 创建后就选中热区 | ||||||
|  |   const hotAreaIndex = hotAreas.value.length - 1; | ||||||
|  |   const hotArea = hotAreas.value[hotAreaIndex]; | ||||||
|  |   if (hotArea) { | ||||||
|  |     handleHotAreaSelected(hotArea, hotAreaIndex); | ||||||
|  |   } | ||||||
|  |   // 发送热区变动通知 | ||||||
|  |   emitUpdateModelValue(); | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * 处理鼠标经过方块 | ||||||
|  |  * | ||||||
|  |  * @param currentRow 当前行号 | ||||||
|  |  * @param currentCol 当前列号 | ||||||
|  |  */ | ||||||
|  | const handleCellHover = (currentRow: number, currentCol: number) => { | ||||||
|  |   // 当前没有进入热区选择模式 | ||||||
|  |   if (!isHotAreaSelectMode()) return; | ||||||
|  | 
 | ||||||
|  |   // 当前已选的区域 | ||||||
|  |   const currentCube = cubes.value[currentRow]?.[currentCol]; | ||||||
|  |   if (!currentCube) return; | ||||||
|  | 
 | ||||||
|  |   const currentSelectedArea = createRect(hotAreaBeginCube.value!, currentCube); | ||||||
|  |   // 热区不允许重叠 | ||||||
|  |   for (const hotArea of hotAreas.value) { | ||||||
|  |     // 检查是否重叠 | ||||||
|  |     if (isOverlap(hotArea, currentSelectedArea)) { | ||||||
|  |       // 结束热区选择模式 | ||||||
|  |       exitHotAreaSelectMode(); | ||||||
|  | 
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 激活选中区域内部的方块 | ||||||
|  |   eachCube((_, __, cube) => { | ||||||
|  |     cube.active = isContains(currentSelectedArea, cube); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * 处理热区删除 | ||||||
|  |  * | ||||||
|  |  * @param index 热区索引 | ||||||
|  |  */ | ||||||
|  | const handleDeleteHotArea = (index: number) => { | ||||||
|  |   hotAreas.value.splice(index, 1); | ||||||
|  |   // 结束热区选择模式 | ||||||
|  |   exitHotAreaSelectMode(); | ||||||
|  |   // 发送热区变动通知 | ||||||
|  |   emitUpdateModelValue(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 发送热区变动通知 | ||||||
|  | const emitUpdateModelValue = () => emit('update:modelValue', hotAreas.value); | ||||||
|  | 
 | ||||||
|  | // 热区选中 | ||||||
|  | const selectedHotAreaIndex = ref(0); | ||||||
|  | const handleHotAreaSelected = (hotArea: Rect, index: number) => { | ||||||
|  |   selectedHotAreaIndex.value = index; | ||||||
|  |   emit('hotAreaSelected', hotArea, index); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 结束热区选择模式 | ||||||
|  |  */ | ||||||
|  | function exitHotAreaSelectMode() { | ||||||
|  |   // 移除方块激活标记 | ||||||
|  |   eachCube((_, __, cube) => { | ||||||
|  |     if (cube.active) { | ||||||
|  |       cube.active = false; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // 清除起点 | ||||||
|  |   hotAreaBeginCube.value = undefined; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 迭代魔方矩阵 | ||||||
|  |  * @param callback 回调 | ||||||
|  |  */ | ||||||
|  | const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => { | ||||||
|  |   for (const [x, row] of cubes.value.entries()) { | ||||||
|  |     if (!row) continue; | ||||||
|  |     for (const [y, cube] of row.entries()) { | ||||||
|  |       if (cube) { | ||||||
|  |         callback(x, y, cube); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <div class="relative"> | ||||||
|  |     <table class="cube-table"> | ||||||
|  |       <!-- 底层:魔方矩阵 --> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="(rowCubes, row) in cubes" :key="row"> | ||||||
|  |           <td | ||||||
|  |             v-for="(cube, col) in rowCubes" | ||||||
|  |             :key="col" | ||||||
|  |             class="cube" | ||||||
|  |             :class="[{ active: cube.active }]" | ||||||
|  |             :style="{ | ||||||
|  |               width: `${cubeSize}px`, | ||||||
|  |               height: `${cubeSize}px`, | ||||||
|  |             }" | ||||||
|  |             @click="handleCubeClick(row, col)" | ||||||
|  |             @mouseenter="handleCellHover(row, col)" | ||||||
|  |           > | ||||||
|  |             <Icon icon="ep-plus" /> | ||||||
|  |           </td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |       <!-- 顶层:热区 --> | ||||||
|  |       <div | ||||||
|  |         v-for="(hotArea, index) in hotAreas" | ||||||
|  |         :key="index" | ||||||
|  |         class="hot-area" | ||||||
|  |         :style="{ | ||||||
|  |           top: `${cubeSize * hotArea.top}px`, | ||||||
|  |           left: `${cubeSize * hotArea.left}px`, | ||||||
|  |           height: `${cubeSize * hotArea.height}px`, | ||||||
|  |           width: `${cubeSize * hotArea.width}px`, | ||||||
|  |         }" | ||||||
|  |         @click="handleHotAreaSelected(hotArea, index)" | ||||||
|  |         @mouseover="exitHotAreaSelectMode" | ||||||
|  |       > | ||||||
|  |         <!-- 右上角热区删除按钮 --> | ||||||
|  |         <div | ||||||
|  |           v-if=" | ||||||
|  |             selectedHotAreaIndex === index && hotArea.width && hotArea.height | ||||||
|  |           " | ||||||
|  |           class="btn-delete" | ||||||
|  |           @click="handleDeleteHotArea(index)" | ||||||
|  |         > | ||||||
|  |           <IconifyIcon icon="ep:circle-close-filled" /> | ||||||
|  |         </div> | ||||||
|  |         <span v-if="hotArea.width">{{ | ||||||
|  |           `${hotArea.width}×${hotArea.height}` | ||||||
|  |         }}</span> | ||||||
|  |       </div> | ||||||
|  |     </table> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .cube-table { | ||||||
|  |   position: relative; | ||||||
|  |   border-spacing: 0; | ||||||
|  |   border-collapse: collapse; | ||||||
|  | 
 | ||||||
|  |   .cube { | ||||||
|  |     border: 1px solid var(--el-border-color); | ||||||
|  |     text-align: center; | ||||||
|  |     color: var(--el-text-color-secondary); | ||||||
|  |     cursor: pointer; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     &.active { | ||||||
|  |       background: var(--el-color-primary-light-9); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .hot-area { | ||||||
|  |     position: absolute; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     border: 1px solid var(--el-color-primary); | ||||||
|  |     background: var(--el-color-primary-light-8); | ||||||
|  |     color: var(--el-color-primary); | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     border-spacing: 0; | ||||||
|  |     border-collapse: collapse; | ||||||
|  |     cursor: pointer; | ||||||
|  | 
 | ||||||
|  |     .btn-delete { | ||||||
|  |       z-index: 1; | ||||||
|  |       position: absolute; | ||||||
|  |       top: -8px; | ||||||
|  |       right: -8px; | ||||||
|  |       height: 16px; | ||||||
|  |       width: 16px; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       border-radius: 50%; | ||||||
|  |       background-color: #fff; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,72 @@ | ||||||
|  | // 坐标点
 | ||||||
|  | export interface Point { | ||||||
|  |   x: number; | ||||||
|  |   y: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 矩形
 | ||||||
|  | export interface Rect { | ||||||
|  |   // 左上角 X 轴坐标
 | ||||||
|  |   left: number; | ||||||
|  |   // 左上角 Y 轴坐标
 | ||||||
|  |   top: number; | ||||||
|  |   // 右下角 X 轴坐标
 | ||||||
|  |   right: number; | ||||||
|  |   // 右下角 Y 轴坐标
 | ||||||
|  |   bottom: number; | ||||||
|  |   // 矩形宽度
 | ||||||
|  |   width: number; | ||||||
|  |   // 矩形高度
 | ||||||
|  |   height: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 判断两个矩形是否重叠 | ||||||
|  |  * @param a 矩形 A | ||||||
|  |  * @param b 矩形 B | ||||||
|  |  */ | ||||||
|  | export const isOverlap = (a: Rect, b: Rect): boolean => { | ||||||
|  |   return ( | ||||||
|  |     a.left < b.left + b.width && | ||||||
|  |     a.left + a.width > b.left && | ||||||
|  |     a.top < b.top + b.height && | ||||||
|  |     a.height + a.top > b.top | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * 检查坐标点是否在矩形内 | ||||||
|  |  * @param hotArea 矩形 | ||||||
|  |  * @param point 坐标 | ||||||
|  |  */ | ||||||
|  | export const isContains = (hotArea: Rect, point: Point): boolean => { | ||||||
|  |   return ( | ||||||
|  |     point.x >= hotArea.left && | ||||||
|  |     point.x < hotArea.right && | ||||||
|  |     point.y >= hotArea.top && | ||||||
|  |     point.y < hotArea.bottom | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 在两个坐标点中间,创建一个矩形 | ||||||
|  |  * | ||||||
|  |  * 存在以下情况: | ||||||
|  |  * 1. 两个坐标点是同一个位置,只占一个位置的正方形,宽高都为 1 | ||||||
|  |  * 2. X 轴坐标相同,只占一行的矩形,高度为 1 | ||||||
|  |  * 3. Y 轴坐标相同,只占一列的矩形,宽度为 1 | ||||||
|  |  * 4. 多行多列的矩形 | ||||||
|  |  * | ||||||
|  |  * @param a 坐标点一 | ||||||
|  |  * @param b 坐标点二 | ||||||
|  |  */ | ||||||
|  | export const createRect = (a: Point, b: Point): Rect => { | ||||||
|  |   // 计算矩形的范围
 | ||||||
|  |   const [left, left2] = [a.x, b.x].sort(); | ||||||
|  |   const [top, top2] = [a.y, b.y].sort(); | ||||||
|  |   const right = left2 + 1; | ||||||
|  |   const bottom = top2 + 1; | ||||||
|  |   const height = bottom - top; | ||||||
|  |   const width = right - left; | ||||||
|  | 
 | ||||||
|  |   return { left, right, top, bottom, height, width }; | ||||||
|  | }; | ||||||
|  | @ -11,7 +11,7 @@ import type { UploadListType } from './typing'; | ||||||
| 
 | 
 | ||||||
| import type { AxiosProgressEvent } from '#/api/infra/file'; | import type { AxiosProgressEvent } from '#/api/infra/file'; | ||||||
| 
 | 
 | ||||||
| import { ref, toRefs, watch } from 'vue'; | import { nextTick, ref, toRefs, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { CloudUpload } from '@vben/icons'; | import { CloudUpload } from '@vben/icons'; | ||||||
| import { $t } from '@vben/locales'; | import { $t } from '@vben/locales'; | ||||||
|  | @ -33,26 +33,28 @@ const props = withDefaults( | ||||||
|       file: File, |       file: File, | ||||||
|       onUploadProgress?: AxiosProgressEvent, |       onUploadProgress?: AxiosProgressEvent, | ||||||
|     ) => Promise<AxiosResponse<any>>; |     ) => Promise<AxiosResponse<any>>; | ||||||
|  |     // 组件边框圆角 | ||||||
|  |     borderradius?: string; | ||||||
|     // 上传的目录 |     // 上传的目录 | ||||||
|     directory?: string; |     directory?: string; | ||||||
|     disabled?: boolean; |     disabled?: boolean; | ||||||
|  |     // 上传框高度 | ||||||
|  |     height?: number | string; | ||||||
|     helpText?: string; |     helpText?: string; | ||||||
|     listType?: UploadListType; |     listType?: UploadListType; | ||||||
|     // 最大数量的文件,Infinity不限制 |     // 最大数量的文件,Infinity不限制 | ||||||
|     maxNumber?: number; |     maxNumber?: number; | ||||||
|     // 文件最大多少MB |     // 文件最大多少MB | ||||||
|     maxSize?: number; |     maxSize?: number; | ||||||
|  |     modelValue?: string | string[]; | ||||||
|     // 是否支持多选 |     // 是否支持多选 | ||||||
|     multiple?: boolean; |     multiple?: boolean; | ||||||
|     // support xxx.xxx.xx |     // support xxx.xxx.xx | ||||||
|     resultField?: string; |     resultField?: string; | ||||||
|     // 是否显示下面的描述 |     // 是否显示下面的描述 | ||||||
|     showDescription?: boolean; |     showDescription?: boolean; | ||||||
|     modelValue?: string | string[]; |  | ||||||
|     // 上传框宽度 |     // 上传框宽度 | ||||||
|     width?: string | number; |     width?: number | string; | ||||||
|     // 上传框高度 |  | ||||||
|     height?: string | number; |  | ||||||
|   }>(), |   }>(), | ||||||
|   { |   { | ||||||
|     modelValue: () => [], |     modelValue: () => [], | ||||||
|  | @ -69,11 +71,13 @@ const props = withDefaults( | ||||||
|     showDescription: true, |     showDescription: true, | ||||||
|     width: '', |     width: '', | ||||||
|     height: '', |     height: '', | ||||||
|  |     borderradius: '8px', | ||||||
|   }, |   }, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits(['change', 'update:modelValue', 'delete']); | const emit = defineEmits(['change', 'update:modelValue', 'delete']); | ||||||
| const { accept, helpText, maxNumber, maxSize, width, height } = toRefs(props); | const { accept, helpText, maxNumber, maxSize, width, height, borderradius } = | ||||||
|  |   toRefs(props); | ||||||
| const isInnerOperate = ref<boolean>(false); | const isInnerOperate = ref<boolean>(false); | ||||||
| const { getStringAccept } = useUploadType({ | const { getStringAccept } = useUploadType({ | ||||||
|   acceptRef: accept, |   acceptRef: accept, | ||||||
|  | @ -219,7 +223,6 @@ async function customRequest(options: UploadRequestOptions) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getValue() { | function getValue() { | ||||||
|   console.log(fileList.value); |  | ||||||
|   const list = (fileList.value || []) |   const list = (fileList.value || []) | ||||||
|     .filter((item) => item?.status === UploadResultStatus.SUCCESS) |     .filter((item) => item?.status === UploadResultStatus.SUCCESS) | ||||||
|     .map((item: any) => { |     .map((item: any) => { | ||||||
|  | @ -234,58 +237,178 @@ function getValue() { | ||||||
|   } |   } | ||||||
|   return list; |   return list; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // 编辑按钮:触发文件选择 | ||||||
|  | const triggerEdit = () => { | ||||||
|  |   if (props.disabled) return; | ||||||
|  |   // 只查找当前 upload-box 下的 input | ||||||
|  |   nextTick(() => { | ||||||
|  |     const uploadBox = document.querySelector('.upload-box'); | ||||||
|  |     if (uploadBox) { | ||||||
|  |       const input = uploadBox.querySelector( | ||||||
|  |         'input[type="file"]', | ||||||
|  |       ) as HTMLInputElement | null; | ||||||
|  |       if (input) input.click(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div | ||||||
|     <ElUpload |     class="upload-box" | ||||||
|       v-bind="$attrs" |     :style="{ | ||||||
|       v-model:file-list="fileList" |       width: width || '150px', | ||||||
|       :accept="getStringAccept" |       height: height || '150px', | ||||||
|       :before-upload="beforeUpload" |       borderRadius: borderradius, | ||||||
|       :http-request="customRequest" |     }" | ||||||
|       :disabled="disabled" |   > | ||||||
|       :list-type="listType" |     <template | ||||||
|       :limit="maxNumber" |       v-if=" | ||||||
|       :multiple="multiple" |         fileList.length > 0 && | ||||||
|       :on-preview="handlePreview" |         fileList[0] && | ||||||
|       :on-remove="handleRemove" |         fileList[0].status === UploadResultStatus.SUCCESS | ||||||
|       :class="width || height ? 'custom-upload' : ''" |       " | ||||||
|     > |     > | ||||||
|       <div |       <div class="upload-image-wrapper"> | ||||||
|         class="upload-content flex flex-col items-center justify-center" |         <img :src="fileList[0].url" class="upload-image" /> | ||||||
|         :style="{ width: width || '', height: height || '' }" |         <div class="upload-handle"> | ||||||
|       > |           <div class="handle-icon" @click="handlePreview(fileList[0]!)"> | ||||||
|         <CloudUpload /> |             <i class="el-icon el-icon-zoom-in"></i> | ||||||
|         <div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div> |             <span>详情</span> | ||||||
|  |           </div> | ||||||
|  |           <div v-if="!disabled" class="handle-icon" @click="triggerEdit"> | ||||||
|  |             <i class="el-icon el-icon-edit"></i> | ||||||
|  |             <span>编辑</span> | ||||||
|  |           </div> | ||||||
|  |           <div | ||||||
|  |             v-if="!disabled" | ||||||
|  |             class="handle-icon" | ||||||
|  |             @click="handleRemove(fileList[0]!)" | ||||||
|  |           > | ||||||
|  |             <i class="el-icon el-icon-delete"></i> | ||||||
|  |             <span>删除</span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </ElUpload> |     </template> | ||||||
|  |     <template v-else> | ||||||
|  |       <ElUpload | ||||||
|  |         v-bind="$attrs" | ||||||
|  |         v-model:file-list="fileList" | ||||||
|  |         :accept="getStringAccept" | ||||||
|  |         :before-upload="beforeUpload" | ||||||
|  |         :http-request="customRequest" | ||||||
|  |         :disabled="disabled" | ||||||
|  |         :list-type="listType" | ||||||
|  |         :limit="maxNumber" | ||||||
|  |         :multiple="multiple" | ||||||
|  |         :on-preview="handlePreview" | ||||||
|  |         :on-remove="handleRemove" | ||||||
|  |         class="upload" | ||||||
|  |         :style="{ | ||||||
|  |           width: width || '150px', | ||||||
|  |           height: height || '150px', | ||||||
|  |           borderRadius: borderradius, | ||||||
|  |         }" | ||||||
|  |       > | ||||||
|  |         <div class="upload-content flex flex-col items-center justify-center"> | ||||||
|  |           <CloudUpload /> | ||||||
|  |           <div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div> | ||||||
|  |         </div> | ||||||
|  |       </ElUpload> | ||||||
|  |     </template> | ||||||
|     <div v-if="showDescription" class="mt-2 text-xs text-gray-500"> |     <div v-if="showDescription" class="mt-2 text-xs text-gray-500"> | ||||||
|       {{ getStringAccept }} |       {{ getStringAccept }} | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style> | <style lang="scss" scoped> | ||||||
| .ant-upload-select-picture-card { | .upload-box { | ||||||
|   @apply flex items-center justify-center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .custom-upload .el-upload { |  | ||||||
|   width: auto !important; |  | ||||||
|   height: auto !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .custom-upload .el-upload--picture-card { |  | ||||||
|   width: auto !important; |  | ||||||
|   height: auto !important; |  | ||||||
|   line-height: normal !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .custom-upload .upload-content { |  | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|  |   border: 1px dashed var(--el-border-color-darker); | ||||||
|  |   position: relative; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   background: #fafafa; | ||||||
|  |   transition: border-color 0.2s; | ||||||
|  | 
 | ||||||
|  |   .upload { | ||||||
|  |     width: 100% !important; | ||||||
|  |     height: 100% !important; | ||||||
|  |     border: none !important; | ||||||
|  |     background: transparent; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .upload-content { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     cursor: pointer; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .upload-image-wrapper { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     position: relative; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     overflow: hidden; | ||||||
|  |     border-radius: inherit; | ||||||
|  |     background: #fff; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .upload-image { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     object-fit: contain; | ||||||
|  |     border-radius: inherit; | ||||||
|  |     display: block; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .upload-handle { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0; | ||||||
|  |     right: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     background: rgba(0, 0, 0, 0.5); | ||||||
|  |     opacity: 0; | ||||||
|  |     transition: opacity 0.2s; | ||||||
|  |     cursor: pointer; | ||||||
|  |     z-index: 2; | ||||||
|  |     &:hover { | ||||||
|  |       opacity: 1; | ||||||
|  |     } | ||||||
|  |     .handle-icon { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       color: #fff; | ||||||
|  |       margin: 0 8px; | ||||||
|  |       font-size: 18px; | ||||||
|  |       span { | ||||||
|  |         font-size: 12px; | ||||||
|  |         margin-top: 2px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   .upload-image-wrapper:hover .upload-handle { | ||||||
|  |     opacity: 1; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | /** | ||||||
|  |  * 垂直按钮组 | ||||||
|  |  * Element官方的按钮组只支持水平显示,通过重写样式实现垂直布局 | ||||||
|  |  */ | ||||||
|  | defineOptions({ name: 'VerticalButtonGroup' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <el-button-group v-bind="$attrs"> | ||||||
|  |     <slot></slot> | ||||||
|  |   </el-button-group> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | .el-button-group { | ||||||
|  |   display: inline-flex; | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-button-group > :deep(.el-button:first-child) { | ||||||
|  |   border-bottom-color: var(--el-button-divide-border-color); | ||||||
|  |   border-top-right-radius: var(--el-border-radius-base); | ||||||
|  |   border-bottom-right-radius: 0; | ||||||
|  |   border-bottom-left-radius: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-button-group > :deep(.el-button:last-child) { | ||||||
|  |   border-top-color: var(--el-button-divide-border-color); | ||||||
|  |   border-top-right-radius: 0; | ||||||
|  |   border-bottom-left-radius: var(--el-border-radius-base); | ||||||
|  |   border-top-left-radius: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-button-group :deep(.el-button--primary:not(:first-child, :last-child)) { | ||||||
|  |   border-top-color: var(--el-button-divide-border-color); | ||||||
|  |   border-bottom-color: var(--el-button-divide-border-color); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-button-group > :deep(.el-button:not(:last-child)) { | ||||||
|  |   margin-right: 0; | ||||||
|  |   margin-bottom: -1px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -4,16 +4,18 @@ const routes: RouteRecordRaw[] = [ | ||||||
|   { |   { | ||||||
|     path: '/diy', |     path: '/diy', | ||||||
|     name: 'DiyCenter', |     name: 'DiyCenter', | ||||||
|     meta: { hidden: true }, |     meta: { | ||||||
|     component: Layout, |       title: '营销中心', | ||||||
|  |       icon: 'lucide:shopping-bag', | ||||||
|  |       keepAlive: true, | ||||||
|  |       hideInMenu: true, | ||||||
|  |     }, | ||||||
|     children: [ |     children: [ | ||||||
|       { |       { | ||||||
|         path: 'template/decorate/:id', |         path: String.raw`template/decorate/:id(\d+)`, | ||||||
|         name: 'DiyTemplateDecorate', |         name: 'DiyTemplateDecorate', | ||||||
|         meta: { |         meta: { | ||||||
|           title: '模板装修', |           title: '模板装修', | ||||||
|           noCache: false, |  | ||||||
|           hidden: true, |  | ||||||
|           activeMenu: '/mall/promotion/diy-template/diy-template', |           activeMenu: '/mall/promotion/diy-template/diy-template', | ||||||
|         }, |         }, | ||||||
|         component: () => |         component: () => | ||||||
|  |  | ||||||
|  | @ -644,3 +644,24 @@ export enum TaskStatusEnum { | ||||||
|    */ |    */ | ||||||
|   WAIT = 0, |   WAIT = 0, | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // 预设颜色
 | ||||||
|  | export const PREDEFINE_COLORS = [ | ||||||
|  |   '#ff4500', | ||||||
|  |   '#ff8c00', | ||||||
|  |   '#ffd700', | ||||||
|  |   '#90ee90', | ||||||
|  |   '#00ced1', | ||||||
|  |   '#1e90ff', | ||||||
|  |   '#c71585', | ||||||
|  |   '#409EFF', | ||||||
|  |   '#909399', | ||||||
|  |   '#C0C4CC', | ||||||
|  |   '#b7390b', | ||||||
|  |   '#ff7800', | ||||||
|  |   '#fad400', | ||||||
|  |   '#5b8c5f', | ||||||
|  |   '#00babd', | ||||||
|  |   '#1f73c3', | ||||||
|  |   '#711f57', | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | @ -3,6 +3,10 @@ import type { MallSpuApi } from '#/api/mall/product/spu'; | ||||||
| 
 | 
 | ||||||
| import { computed, ref, watch } from 'vue'; | import { computed, ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElImage, ElTooltip } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
| import SpuTableSelect from '#/views/mall/product/spu/components/spu-table-select.vue'; | import SpuTableSelect from '#/views/mall/product/spu/components/spu-table-select.vue'; | ||||||
| 
 | 
 | ||||||
|  | @ -110,23 +114,23 @@ const emitSpuChange = () => { | ||||||
|       :key="spu.id" |       :key="spu.id" | ||||||
|       class="select-box spu-pic" |       class="select-box spu-pic" | ||||||
|     > |     > | ||||||
|       <el-tooltip :content="spu.name"> |       <ElTooltip :content="spu.name"> | ||||||
|         <div class="relative h-full w-full"> |         <div class="relative h-full w-full"> | ||||||
|           <el-image :src="spu.picUrl" class="h-full w-full" /> |           <ElImage :src="spu.picUrl" class="h-full w-full" /> | ||||||
|           <Icon |           <IconifyIcon | ||||||
|             v-show="!disabled" |             v-show="!disabled" | ||||||
|             class="del-icon" |             class="del-icon" | ||||||
|             icon="ep:circle-close-filled" |             icon="ep:circle-close-filled" | ||||||
|             @click="handleRemoveSpu(index)" |             @click="handleRemoveSpu(index)" | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|       </el-tooltip> |       </ElTooltip> | ||||||
|     </div> |     </div> | ||||||
|     <el-tooltip content="选择商品" v-if="canAdd"> |     <ElTooltip content="选择商品" v-if="canAdd"> | ||||||
|       <div class="select-box" @click="openSpuTableSelect"> |       <div class="select-box" @click="openSpuTableSelect"> | ||||||
|         <Icon icon="ep:plus" /> |         <IconifyIcon icon="ep:plus" /> | ||||||
|       </div> |       </div> | ||||||
|     </el-tooltip> |     </ElTooltip> | ||||||
|   </div> |   </div> | ||||||
|   <!-- 商品选择对话框(表格形式) --> |   <!-- 商品选择对话框(表格形式) --> | ||||||
|   <SpuTableSelect |   <SpuTableSelect | ||||||
|  |  | ||||||
|  | @ -6,7 +6,21 @@ import { onMounted, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { handleTree } from '@vben/utils'; | import { handleTree } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
| import { CHANGE_EVENT } from 'element-plus'; | import { | ||||||
|  |   CHANGE_EVENT, | ||||||
|  |   ElButton, | ||||||
|  |   ElCheckbox, | ||||||
|  |   ElDatePicker, | ||||||
|  |   ElDialog, | ||||||
|  |   ElForm, | ||||||
|  |   ElFormItem, | ||||||
|  |   ElImage, | ||||||
|  |   ElInput, | ||||||
|  |   ElRadio, | ||||||
|  |   ElTable, | ||||||
|  |   ElTableColumn, | ||||||
|  |   ElTreeSelect, | ||||||
|  | } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| import * as ProductCategoryApi from '#/api/mall/product/category'; | import * as ProductCategoryApi from '#/api/mall/product/category'; | ||||||
| import * as ProductSpuApi from '#/api/mall/product/spu'; | import * as ProductSpuApi from '#/api/mall/product/spu'; | ||||||
|  | @ -210,30 +224,30 @@ onMounted(async () => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <Dialog |   <ElDialog | ||||||
|     v-model="dialogVisible" |     v-model="dialogVisible" | ||||||
|     :append-to-body="true" |     :append-to-body="true" | ||||||
|     title="选择商品" |     title="选择商品" | ||||||
|     width="70%" |     width="70%" | ||||||
|   > |   > | ||||||
|     <ContentWrap> |     <ContentWrap> | ||||||
|       <el-form |       <ElForm | ||||||
|         :inline="true" |         :inline="true" | ||||||
|         :model="queryParams" |         :model="queryParams" | ||||||
|         class="-mb-15px" |         class="-mb-15px" | ||||||
|         label-width="68px" |         label-width="68px" | ||||||
|       > |       > | ||||||
|         <el-form-item label="商品名称" prop="name"> |         <ElFormItem label="商品名称" prop="name"> | ||||||
|           <el-input |           <ElInput | ||||||
|             v-model="queryParams.name" |             v-model="queryParams.name" | ||||||
|             class="!w-240px" |             class="!w-240px" | ||||||
|             clearable |             clearable | ||||||
|             placeholder="请输入商品名称" |             placeholder="请输入商品名称" | ||||||
|             @keyup.enter="handleQuery" |             @keyup.enter="handleQuery" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="商品分类" prop="categoryId"> |         <ElFormItem label="商品分类" prop="categoryId"> | ||||||
|           <el-tree-select |           <ElTreeSelect | ||||||
|             v-model="queryParams.categoryId" |             v-model="queryParams.categoryId" | ||||||
|             :data="categoryTreeList" |             :data="categoryTreeList" | ||||||
|             :props="{ |             :props="{ | ||||||
|  | @ -248,9 +262,9 @@ onMounted(async () => { | ||||||
|             node-key="id" |             node-key="id" | ||||||
|             placeholder="请选择商品分类" |             placeholder="请选择商品分类" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item label="创建时间" prop="createTime"> |         <ElFormItem label="创建时间" prop="createTime"> | ||||||
|           <el-date-picker |           <ElDatePicker | ||||||
|             v-model="queryParams.createTime" |             v-model="queryParams.createTime" | ||||||
|             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" |             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" | ||||||
|             class="!w-240px" |             class="!w-240px" | ||||||
|  | @ -259,67 +273,67 @@ onMounted(async () => { | ||||||
|             type="daterange" |             type="daterange" | ||||||
|             value-format="YYYY-MM-DD HH:mm:ss" |             value-format="YYYY-MM-DD HH:mm:ss" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|         <el-form-item> |         <ElFormItem> | ||||||
|           <el-button @click="handleQuery"> |           <ElButton @click="handleQuery"> | ||||||
|             <Icon class="mr-5px" icon="ep:search" /> |             <Icon class="mr-5px" icon="ep:search" /> | ||||||
|             搜索 |             搜索 | ||||||
|           </el-button> |           </ElButton> | ||||||
|           <el-button @click="resetQuery"> |           <ElButton @click="resetQuery"> | ||||||
|             <Icon class="mr-5px" icon="ep:refresh" /> |             <Icon class="mr-5px" icon="ep:refresh" /> | ||||||
|             重置 |             重置 | ||||||
|           </el-button> |           </ElButton> | ||||||
|         </el-form-item> |         </ElFormItem> | ||||||
|       </el-form> |       </ElForm> | ||||||
|       <el-table v-loading="loading" :data="list" show-overflow-tooltip> |       <ElTable v-loading="loading" :data="list" show-overflow-tooltip> | ||||||
|         <!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) --> |         <!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) --> | ||||||
|         <el-table-column width="55" v-if="multiple"> |         <ElTableColumn width="55" v-if="multiple"> | ||||||
|           <template #header> |           <template #header> | ||||||
|             <el-checkbox |             <ElCheckbox | ||||||
|               v-model="isCheckAll" |               v-model="isCheckAll" | ||||||
|               :indeterminate="isIndeterminate" |               :indeterminate="isIndeterminate" | ||||||
|               @change="handleCheckAll" |               @change="handleCheckAll" | ||||||
|             /> |             /> | ||||||
|           </template> |           </template> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <el-checkbox |             <ElCheckbox | ||||||
|               v-model="checkedStatus[row.id]" |               v-model="checkedStatus[row.id]" | ||||||
|               @change="(checked: boolean) => handleCheckOne(checked, row, true)" |               @change="(checked: boolean) => handleCheckOne(checked, row, true)" | ||||||
|             /> |             /> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </ElTableColumn> | ||||||
|         <!-- 2. 单选模式 --> |         <!-- 2. 单选模式 --> | ||||||
|         <el-table-column label="#" width="55" v-else> |         <ElTableColumn label="#" width="55" v-else> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <el-radio |             <ElRadio | ||||||
|               :value="row.id" |               :value="row.id" | ||||||
|               v-model="selectedSpuId" |               v-model="selectedSpuId" | ||||||
|               @change="handleSingleSelected(row)" |               @change="handleSingleSelected(row)" | ||||||
|             > |             > | ||||||
|               <!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 --> |               <!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 --> | ||||||
|                 |                 | ||||||
|             </el-radio> |             </ElRadio> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </ElTableColumn> | ||||||
|         <el-table-column |         <ElTableColumn | ||||||
|           key="id" |           key="id" | ||||||
|           align="center" |           align="center" | ||||||
|           label="商品编号" |           label="商品编号" | ||||||
|           prop="id" |           prop="id" | ||||||
|           min-width="60" |           min-width="60" | ||||||
|         /> |         /> | ||||||
|         <el-table-column label="商品图" min-width="80"> |         <ElTableColumn label="商品图" min-width="80"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <el-image |             <ElImage | ||||||
|               :src="row.picUrl" |               :src="row.picUrl" | ||||||
|               class="h-30px w-30px" |               class="h-30px w-30px" | ||||||
|               :preview-src-list="[row.picUrl]" |               :preview-src-list="[row.picUrl]" | ||||||
|               preview-teleported |               preview-teleported | ||||||
|             /> |             /> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </ElTableColumn> | ||||||
|         <el-table-column label="商品名称" min-width="200" prop="name" /> |         <ElTableColumn label="商品名称" min-width="200" prop="name" /> | ||||||
|         <el-table-column label="商品分类" min-width="100" prop="categoryId"> |         <ElTableColumn label="商品分类" min-width="100" prop="categoryId"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <span>{{ |             <span>{{ | ||||||
|               categoryList?.find( |               categoryList?.find( | ||||||
|  | @ -327,8 +341,8 @@ onMounted(async () => { | ||||||
|               )?.name |               )?.name | ||||||
|             }}</span> |             }}</span> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </ElTableColumn> | ||||||
|       </el-table> |       </ElTable> | ||||||
|       <!-- 分页 --> |       <!-- 分页 --> | ||||||
|       <Pagination |       <Pagination | ||||||
|         v-model:limit="queryParams.pageSize" |         v-model:limit="queryParams.pageSize" | ||||||
|  | @ -338,8 +352,8 @@ onMounted(async () => { | ||||||
|       /> |       /> | ||||||
|     </ContentWrap> |     </ContentWrap> | ||||||
|     <template #footer v-if="multiple"> |     <template #footer v-if="multiple"> | ||||||
|       <el-button type="primary" @click="handleEmitChange">确 定</el-button> |       <ElButton type="primary" @click="handleEmitChange">确 定</ElButton> | ||||||
|       <el-button @click="dialogVisible = false">取 消</el-button> |       <ElButton @click="dialogVisible = false">取 消</ElButton> | ||||||
|     </template> |     </template> | ||||||
|   </Dialog> |   </ElDialog> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { useRoute } from 'vue-router'; | ||||||
| import { ElMessage } from 'element-plus'; | import { ElMessage } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| import * as DiyPageApi from '#/api/mall/promotion/diy/page'; | import * as DiyPageApi from '#/api/mall/promotion/diy/page'; | ||||||
| import { PAGE_LIBS } from '#/components/DiyEditor/util'; | import { PAGE_LIBS } from '#/components/diy-editor/util'; | ||||||
| 
 | 
 | ||||||
| /** 装修页面表单 */ | /** 装修页面表单 */ | ||||||
| defineOptions({ name: 'DiyPageDecorate' }); | defineOptions({ name: 'DiyPageDecorate' }); | ||||||
|  |  | ||||||
|  | @ -1,11 +1,21 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import * as DiyPageApi from '@/api/mall/promotion/diy/page'; | import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page'; | ||||||
|  | import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template'; | ||||||
|  | import type { DiyComponentLibrary } from '#/components/diy-editor/util'; // 商城的 DIY 组件,在 DiyEditor 目录下 | ||||||
|  | 
 | ||||||
|  | import { onMounted, reactive, ref, unref } from 'vue'; | ||||||
|  | import { useRouter } from 'vue-router'; | ||||||
|  | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | import { isEmpty } from '@vben/utils'; | ||||||
|  | 
 | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import * as DiyPageApi from '#/api/mall/promotion/diy/page'; | ||||||
| // TODO @疯狂:要不要建个 decorate 目录,然后挪进去,改成 index.vue,这样可以更明确看到是个独立界面哈,更好找 | // TODO @疯狂:要不要建个 decorate 目录,然后挪进去,改成 index.vue,这样可以更明确看到是个独立界面哈,更好找 | ||||||
| import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'; | import * as DiyTemplateApi from '#/api/mall/promotion/diy/template'; | ||||||
| import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'; // 商城的 DIY 组件,在 DiyEditor 目录下 | import DiyEditor from '#/components/diy-editor/index.vue'; | ||||||
| import { useTagsViewStore } from '@/store/modules/tagsView'; | import { PAGE_LIBS } from '#/components/diy-editor/util'; | ||||||
| import { isEmpty } from '@/utils/is'; |  | ||||||
| import { toNumber } from 'lodash-es'; |  | ||||||
| 
 | 
 | ||||||
| /** 装修模板表单 */ | /** 装修模板表单 */ | ||||||
| defineOptions({ name: 'DiyTemplateDecorate' }); | defineOptions({ name: 'DiyTemplateDecorate' }); | ||||||
|  | @ -18,20 +28,18 @@ const templateItems = reactive([ | ||||||
|   { name: '我的', icon: 'ep:user-filled' }, |   { name: '我的', icon: 'ep:user-filled' }, | ||||||
| ]); | ]); | ||||||
| 
 | 
 | ||||||
| const message = useMessage(); // 消息弹窗 |  | ||||||
| 
 |  | ||||||
| const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||||
| const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>(); | const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>(); | ||||||
| const formRef = ref(); // 表单 Ref | const formRef = ref(); // 表单 Ref | ||||||
| // 当前编辑的属性 | // 当前编辑的属性 | ||||||
| const currentFormData = ref< | const currentFormData = ref< | ||||||
|   DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO |   MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty | ||||||
| >({ | >({ | ||||||
|   property: '', |   property: '', | ||||||
| } as DiyPageApi.DiyPageVO); | } as MallDiyPageApi.DiyPage); | ||||||
| // templateItem 对应的缓存 | // templateItem 对应的缓存 | ||||||
| const currentFormDataMap = ref< | const currentFormDataMap = ref< | ||||||
|   Map<string, DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO> |   Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty> | ||||||
| >(new Map()); | >(new Map()); | ||||||
| // 商城 H5 预览地址 | // 商城 H5 预览地址 | ||||||
| const previewUrl = ref(''); | const previewUrl = ref(''); | ||||||
|  | @ -57,11 +65,11 @@ const libs = ref<DiyComponentLibrary[]>(templateLibs); | ||||||
| const handleTemplateItemChange = (val: number) => { | const handleTemplateItemChange = (val: number) => { | ||||||
|   // 缓存模版编辑数据 |   // 缓存模版编辑数据 | ||||||
|   currentFormDataMap.value.set( |   currentFormDataMap.value.set( | ||||||
|     templateItems[selectedTemplateItem.value].name, |     templateItems[selectedTemplateItem.value]?.name || '', | ||||||
|     currentFormData.value!, |     currentFormData.value!, | ||||||
|   ); |   ); | ||||||
|   // 读取模版缓存 |   // 读取模版缓存 | ||||||
|   const data = currentFormDataMap.value.get(templateItems[val].name); |   const data = currentFormDataMap.value.get(templateItems[val]?.name || ''); | ||||||
| 
 | 
 | ||||||
|   // 切换模版 |   // 切换模版 | ||||||
|   selectedTemplateItem.value = val; |   selectedTemplateItem.value = val; | ||||||
|  | @ -69,8 +77,8 @@ const handleTemplateItemChange = (val: number) => { | ||||||
|   if (val === 0) { |   if (val === 0) { | ||||||
|     libs.value = templateLibs; |     libs.value = templateLibs; | ||||||
|     currentFormData.value = (isEmpty(data) ? formData.value : data) as |     currentFormData.value = (isEmpty(data) ? formData.value : data) as | ||||||
|       | DiyPageApi.DiyPageVO |       | MallDiyPageApi.DiyPage | ||||||
|       | DiyTemplateApi.DiyTemplatePropertyVO; |       | MallDiyTemplateApi.DiyTemplateProperty; | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -79,16 +87,17 @@ const handleTemplateItemChange = (val: number) => { | ||||||
|   currentFormData.value = ( |   currentFormData.value = ( | ||||||
|     isEmpty(data) |     isEmpty(data) | ||||||
|       ? formData.value!.pages.find( |       ? formData.value!.pages.find( | ||||||
|           (page: DiyPageApi.DiyPageVO) => page.name === templateItems[val].name, |           (page: MallDiyPageApi.DiyPage) => | ||||||
|  |             page.name === templateItems[val]?.name, | ||||||
|         ) |         ) | ||||||
|       : data |       : data | ||||||
|   ) as DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO; |   ) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // 提交表单 | // 提交表单 | ||||||
| const submitForm = async () => { | const submitForm = async () => { | ||||||
|   // 校验表单 |   // 校验表单 | ||||||
|   if (!formRef) return; |   if (!formRef.value) return; | ||||||
|   // 提交请求 |   // 提交请求 | ||||||
|   formLoading.value = true; |   formLoading.value = true; | ||||||
|   try { |   try { | ||||||
|  | @ -114,7 +123,7 @@ const submitForm = async () => { | ||||||
|         await DiyPageApi.updateDiyPageProperty(data!); |         await DiyPageApi.updateDiyPageProperty(data!); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     message.success('保存成功'); |     ElMessage.success('保存成功'); | ||||||
|   } finally { |   } finally { | ||||||
|     formLoading.value = false; |     formLoading.value = false; | ||||||
|   } |   } | ||||||
|  | @ -131,7 +140,7 @@ const resetForm = () => { | ||||||
|     previewPicUrls: [], |     previewPicUrls: [], | ||||||
|     property: '', |     property: '', | ||||||
|     pages: [], |     pages: [], | ||||||
|   } as DiyTemplateApi.DiyTemplatePropertyVO; |   } as MallDiyTemplateApi.DiyTemplateProperty; | ||||||
|   formRef.value?.resetFields(); |   formRef.value?.resetFields(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -147,17 +156,17 @@ const storePageIndex = () => | ||||||
| // 2. 恢复 | // 2. 恢复 | ||||||
| const recoverPageIndex = () => { | const recoverPageIndex = () => { | ||||||
|   // 恢复重置前的页面,默认是第一个页面 |   // 恢复重置前的页面,默认是第一个页面 | ||||||
|   const pageIndex = toNumber(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0; |   const pageIndex = Number(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0; | ||||||
|   // 移除标记 |   // 移除标记 | ||||||
|   sessionStorage.removeItem(DIY_PAGE_INDEX_KEY); |   sessionStorage.removeItem(DIY_PAGE_INDEX_KEY); | ||||||
| 
 | 
 | ||||||
|   // 重新初始化数据 |   // 重新初始化数据 | ||||||
|   currentFormData.value = formData.value as |   currentFormData.value = formData.value as | ||||||
|     | DiyPageApi.DiyPageVO |     | MallDiyPageApi.DiyPage | ||||||
|     | DiyTemplateApi.DiyTemplatePropertyVO; |     | MallDiyTemplateApi.DiyTemplateProperty; | ||||||
|   currentFormDataMap.value = new Map< |   currentFormDataMap.value = new Map< | ||||||
|     string, |     string, | ||||||
|     DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO |     MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty | ||||||
|   >(); |   >(); | ||||||
|   // 切换页面 |   // 切换页面 | ||||||
|   if (pageIndex !== selectedTemplateItem.value) { |   if (pageIndex !== selectedTemplateItem.value) { | ||||||
|  | @ -168,15 +177,12 @@ const recoverPageIndex = () => { | ||||||
| 
 | 
 | ||||||
| /** 初始化 */ | /** 初始化 */ | ||||||
| const { currentRoute } = useRouter(); // 路由 | const { currentRoute } = useRouter(); // 路由 | ||||||
| const { delView } = useTagsViewStore(); // 视图操作 |  | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|   resetForm(); |   resetForm(); | ||||||
|   if (!currentRoute.value.params.id) { |   if (!currentRoute.value.params.id) { | ||||||
|     message.warning('参数错误,页面编号不能为空!'); |     ElMessage.warning('参数错误,页面编号不能为空!'); | ||||||
|     delView(unref(currentRoute)); |  | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   // 查询详情 |   // 查询详情 | ||||||
|   await getPageDetail(currentRoute.value.params.id); |   await getPageDetail(currentRoute.value.params.id); | ||||||
|   // 恢复重置前的页面 |   // 恢复重置前的页面 | ||||||
|  | @ -192,7 +198,7 @@ onMounted(async () => { | ||||||
|     :show-navigation-bar="selectedTemplateItem !== 0" |     :show-navigation-bar="selectedTemplateItem !== 0" | ||||||
|     :show-page-config="selectedTemplateItem !== 0" |     :show-page-config="selectedTemplateItem !== 0" | ||||||
|     :show-tab-bar="selectedTemplateItem === 0" |     :show-tab-bar="selectedTemplateItem === 0" | ||||||
|     :title="templateItems[selectedTemplateItem].name" |     :title="templateItems[selectedTemplateItem]?.name || ''" | ||||||
|     @reset="handleEditorReset" |     @reset="handleEditorReset" | ||||||
|     @save="submitForm" |     @save="submitForm" | ||||||
|   > |   > | ||||||
|  | @ -208,7 +214,7 @@ onMounted(async () => { | ||||||
|           :content="item.name" |           :content="item.name" | ||||||
|         > |         > | ||||||
|           <el-radio-button :value="index"> |           <el-radio-button :value="index"> | ||||||
|             <Icon :icon="item.icon" :size="24" /> |             <IconifyIcon :icon="item.icon" :size="24" /> | ||||||
|           </el-radio-button> |           </el-radio-button> | ||||||
|         </el-tooltip> |         </el-tooltip> | ||||||
|       </el-radio-group> |       </el-radio-group> | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ import type { MallPointActivityApi } from '#/api/mall/promotion/point'; | ||||||
| 
 | 
 | ||||||
| import { computed, ref, watch } from 'vue'; | import { computed, ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { ElImage, ElTooltip } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as PointActivityApi from '#/api/mall/promotion/point'; | import * as PointActivityApi from '#/api/mall/promotion/point'; | ||||||
| 
 | 
 | ||||||
| import PointTableSelect from './point-table-select.vue'; | import PointTableSelect from './point-table-select.vue'; | ||||||
|  | @ -123,23 +125,23 @@ const emitActivityChange = () => { | ||||||
|       :key="pointActivity.id" |       :key="pointActivity.id" | ||||||
|       class="select-box spu-pic" |       class="select-box spu-pic" | ||||||
|     > |     > | ||||||
|       <el-tooltip :content="pointActivity.spuName"> |       <ElTooltip :content="pointActivity.spuName"> | ||||||
|         <div class="relative h-full w-full"> |         <div class="relative h-full w-full"> | ||||||
|           <el-image :src="pointActivity.picUrl" class="h-full w-full" /> |           <ElImage :src="pointActivity.picUrl" class="h-full w-full" /> | ||||||
|           <Icon |           <IconifyIcon | ||||||
|             v-show="!disabled" |             v-show="!disabled" | ||||||
|             class="del-icon" |             class="del-icon" | ||||||
|             icon="ep:circle-close-filled" |             icon="ep:circle-close-filled" | ||||||
|             @click="handleRemoveActivity(index)" |             @click="handleRemoveActivity(index)" | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|       </el-tooltip> |       </ElTooltip> | ||||||
|     </div> |     </div> | ||||||
|     <el-tooltip v-if="canAdd" content="选择活动"> |     <ElTooltip v-if="canAdd" content="选择活动"> | ||||||
|       <div class="select-box" @click="openSeckillActivityTableSelect"> |       <div class="select-box" @click="openSeckillActivityTableSelect"> | ||||||
|         <Icon icon="ep:plus" /> |         <IconifyIcon icon="ep:plus" /> | ||||||
|       </div> |       </div> | ||||||
|     </el-tooltip> |     </ElTooltip> | ||||||
|   </div> |   </div> | ||||||
|   <!-- 拼团活动选择对话框(表格形式) --> |   <!-- 拼团活动选择对话框(表格形式) --> | ||||||
|   <PointTableSelect |   <PointTableSelect | ||||||
|  |  | ||||||
|  | @ -3,6 +3,10 @@ import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckil | ||||||
| 
 | 
 | ||||||
| import { computed, ref, watch } from 'vue'; | import { computed, ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { ElImage, ElTooltip } from 'element-plus'; | ||||||
|  | 
 | ||||||
| import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity'; | ||||||
| import SeckillTableSelect from '#/views/mall/promotion/seckill/components/seckill-table-select.vue'; | import SeckillTableSelect from '#/views/mall/promotion/seckill/components/seckill-table-select.vue'; | ||||||
| 
 | 
 | ||||||
|  | @ -120,23 +124,23 @@ const emitActivityChange = () => { | ||||||
|       :key="seckillActivity.id" |       :key="seckillActivity.id" | ||||||
|       class="select-box spu-pic" |       class="select-box spu-pic" | ||||||
|     > |     > | ||||||
|       <el-tooltip :content="seckillActivity.name"> |       <ElTooltip :content="seckillActivity.name"> | ||||||
|         <div class="relative h-full w-full"> |         <div class="relative h-full w-full"> | ||||||
|           <el-image :src="seckillActivity.picUrl" class="h-full w-full" /> |           <ElImage :src="seckillActivity.picUrl" class="h-full w-full" /> | ||||||
|           <Icon |           <IconifyIcon | ||||||
|             v-show="!disabled" |             v-show="!disabled" | ||||||
|             class="del-icon" |             class="del-icon" | ||||||
|             icon="ep:circle-close-filled" |             icon="ep:circle-close-filled" | ||||||
|             @click="handleRemoveActivity(index)" |             @click="handleRemoveActivity(index)" | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|       </el-tooltip> |       </ElTooltip> | ||||||
|     </div> |     </div> | ||||||
|     <el-tooltip content="选择活动" v-if="canAdd"> |     <ElTooltip content="选择活动" v-if="canAdd"> | ||||||
|       <div class="select-box" @click="openSeckillActivityTableSelect"> |       <div class="select-box" @click="openSeckillActivityTableSelect"> | ||||||
|         <Icon icon="ep:plus" /> |         <IconifyIcon icon="ep:plus" /> | ||||||
|       </div> |       </div> | ||||||
|     </el-tooltip> |     </ElTooltip> | ||||||
|   </div> |   </div> | ||||||
|   <!-- 拼团活动选择对话框(表格形式) --> |   <!-- 拼团活动选择对话框(表格形式) --> | ||||||
|   <SeckillTableSelect |   <SeckillTableSelect | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 lrl
						lrl