Browse Source

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

芋道源码 1 year ago
parent
commit
d6ff66dc1d
66 changed files with 1543 additions and 67 deletions
  1. 1 1
      src/api/infra/demo/demo01/index.ts
  2. 1 1
      src/api/infra/demo/demo02/index.ts
  3. 1 1
      src/api/infra/demo/demo03/erp/index.ts
  4. 7 3
      src/api/infra/demo/demo03/inner/index.ts
  5. 7 3
      src/api/infra/demo/demo03/normal/index.ts
  6. 1 1
      src/api/mall/promotion/article/index.ts
  7. 198 0
      src/components/AppLinkInput/AppLinkSelectDialog.vue
  8. 246 0
      src/components/AppLinkInput/data.ts
  9. 43 0
      src/components/AppLinkInput/index.vue
  10. 1 1
      src/components/DiyEditor/components/ComponentContainerProperty.vue
  11. 1 1
      src/components/DiyEditor/components/mobile/Carousel/property.vue
  12. 1 1
      src/components/DiyEditor/components/mobile/ImageBar/property.vue
  13. 1 1
      src/components/DiyEditor/components/mobile/MagicCube/property.vue
  14. 1 1
      src/components/DiyEditor/components/mobile/MenuGrid/property.vue
  15. 1 1
      src/components/DiyEditor/components/mobile/MenuList/property.vue
  16. 1 1
      src/components/DiyEditor/components/mobile/MenuSwiper/property.vue
  17. 1 1
      src/components/DiyEditor/components/mobile/NoticeBar/property.vue
  18. 1 1
      src/components/DiyEditor/components/mobile/ProductList/config.ts
  19. 1 1
      src/components/DiyEditor/components/mobile/ProductList/index.vue
  20. 1 1
      src/components/DiyEditor/components/mobile/ProductList/property.vue
  21. 25 0
      src/components/DiyEditor/components/mobile/PromotionArticle/config.ts
  22. 27 0
      src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
  23. 56 0
      src/components/DiyEditor/components/mobile/PromotionArticle/property.vue
  24. 64 0
      src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
  25. 125 0
      src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
  26. 112 0
      src/components/DiyEditor/components/mobile/PromotionCombination/property.vue
  27. 64 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts
  28. 125 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue
  29. 112 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue
  30. 1 1
      src/components/DiyEditor/components/mobile/TabBar/property.vue
  31. 1 1
      src/components/DiyEditor/components/mobile/TitleBar/property.vue
  32. 21 0
      src/components/DiyEditor/components/mobile/UserCard/config.ts
  33. 29 0
      src/components/DiyEditor/components/mobile/UserCard/index.vue
  34. 17 0
      src/components/DiyEditor/components/mobile/UserCard/property.vue
  35. 23 0
      src/components/DiyEditor/components/mobile/UserCoupon/config.ts
  36. 15 0
      src/components/DiyEditor/components/mobile/UserCoupon/index.vue
  37. 17 0
      src/components/DiyEditor/components/mobile/UserCoupon/property.vue
  38. 23 0
      src/components/DiyEditor/components/mobile/UserOrder/config.ts
  39. 13 0
      src/components/DiyEditor/components/mobile/UserOrder/index.vue
  40. 17 0
      src/components/DiyEditor/components/mobile/UserOrder/property.vue
  41. 23 0
      src/components/DiyEditor/components/mobile/UserWallet/config.ts
  42. 15 0
      src/components/DiyEditor/components/mobile/UserWallet/index.vue
  43. 17 0
      src/components/DiyEditor/components/mobile/UserWallet/property.vue
  44. 8 2
      src/components/DiyEditor/util.ts
  45. 7 2
      src/components/RouterSearch/index.vue
  46. 1 1
      src/layout/components/ToolHeader.vue
  47. 22 0
      src/utils/index.ts
  48. 1 1
      src/views/crm/businessStatusType/index.vue
  49. 2 2
      src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue
  50. 5 1
      src/views/infra/codegen/EditTable.vue
  51. 1 1
      src/views/infra/demo/demo01/Demo01ContactForm.vue
  52. 1 1
      src/views/infra/demo/demo02/Demo02CategoryForm.vue
  53. 1 1
      src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
  54. 1 1
      src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
  55. 4 4
      src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
  56. 1 1
      src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
  57. 4 4
      src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
  58. 1 1
      src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
  59. 3 3
      src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
  60. 1 1
      src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
  61. 4 4
      src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
  62. 1 1
      src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
  63. 1 1
      src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
  64. 3 3
      src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
  65. 4 4
      src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
  66. 8 4
      src/views/mall/product/category/components/ProductCategorySelect.vue

+ 1 - 1
src/api/infra/demo/demo01/index.ts

@@ -37,4 +37,4 @@ export const deleteDemo01Contact = async (id: number) => {
 // 导出示例联系人 Excel
 export const exportDemo01Contact = async (params) => {
   return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
-}
+}

+ 1 - 1
src/api/infra/demo/demo02/index.ts

@@ -34,4 +34,4 @@ export const deleteDemo02Category = async (id: number) => {
 // 导出示例分类 Excel
 export const exportDemo02Category = async (params) => {
   return await request.download({ url: `/infra/demo02-category/export-excel`, params })
-}
+}

+ 1 - 1
src/api/infra/demo/demo03/erp/index.ts

@@ -88,4 +88,4 @@ export const deleteDemo03Grade = async (id: number) => {
 // 获得学生班级
 export const getDemo03Grade = async (id: number) => {
   return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id })
-}
+}

+ 7 - 3
src/api/infra/demo/demo03/inner/index.ts

@@ -42,12 +42,16 @@ export const exportDemo03Student = async (params) => {
 
 // 获得学生课程列表
 export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+  return await request.get({
+    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
+  })
 }
 
 // ==================== 子表(学生班级) ====================
 
 // 获得学生班级
 export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
-}
+  return await request.get({
+    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
+  })
+}

+ 7 - 3
src/api/infra/demo/demo03/normal/index.ts

@@ -42,12 +42,16 @@ export const exportDemo03Student = async (params) => {
 
 // 获得学生课程列表
 export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+  return await request.get({
+    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
+  })
 }
 
 // ==================== 子表(学生班级) ====================
 
 // 获得学生班级
 export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
-}
+  return await request.get({
+    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
+  })
+}

+ 1 - 1
src/api/mall/promotion/article/index.ts

@@ -17,7 +17,7 @@ export interface ArticleVO {
 }
 
 // 查询文章管理列表
-export const getArticlePage = async (params) => {
+export const getArticlePage = async (params: any) => {
   return await request.get({ url: `/promotion/article/page`, params })
 }
 

+ 198 - 0
src/components/AppLinkInput/AppLinkSelectDialog.vue

@@ -0,0 +1,198 @@
+<template>
+  <Dialog v-model="dialogVisible" title="选择链接" width="65%">
+    <div class="h-500px flex gap-8px">
+      <!-- 左侧分组列表 -->
+      <el-scrollbar wrap-class="h-full" ref="groupScrollbar" view-class="flex flex-col">
+        <el-button
+          v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
+          :key="groupIndex"
+          :class="[
+            'm-r-16px m-l-0px! justify-start! w-90px',
+            { active: activeGroup === group.name }
+          ]"
+          ref="groupBtnRefs"
+          :text="activeGroup !== group.name"
+          :type="activeGroup === group.name ? 'primary' : 'default'"
+          @click="handleGroupSelected(group.name)"
+        >
+          {{ group.name }}
+        </el-button>
+      </el-scrollbar>
+      <!-- 右侧链接列表 -->
+      <el-scrollbar class="h-full flex-1" @scroll="handleScroll" ref="linkScrollbar">
+        <div v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" :key="groupIndex">
+          <!-- 分组标题 -->
+          <div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
+          <!-- 链接列表 -->
+          <el-tooltip
+            v-for="(appLink, appLinkIndex) in group.links"
+            :key="appLinkIndex"
+            :content="appLink.path"
+            placement="bottom"
+          >
+            <el-button
+              class="m-b-8px m-r-8px m-l-0px!"
+              :type="isSameLink(appLink.path, activeAppLink) ? 'primary' : 'default'"
+              @click="handleAppLinkSelected(appLink)"
+            >
+              {{ appLink.name }}
+            </el-button>
+          </el-tooltip>
+        </div>
+      </el-scrollbar>
+    </div>
+    <!-- 底部对话框操作按钮 -->
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <Dialog v-model="detailSelectDialog.visible" title="" width="50%">
+    <el-form class="min-h-200px">
+      <el-form-item
+        label="选择分类"
+        v-if="detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST"
+      >
+        <ProductCategorySelect
+          v-model="detailSelectDialog.id"
+          :parent-id="0"
+          @update:model-value="handleProductCategorySelected"
+        />
+      </el-form-item>
+    </el-form>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'
+import { ButtonInstance, ScrollbarInstance } from 'element-plus'
+import { split } from 'lodash-es'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { getUrlNumberValue } from '@/utils'
+
+// APP 链接选择弹框
+defineOptions({ name: 'AppLinkSelectDialog' })
+// 选中的分组,默认选中第一个
+const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
+// 选中的 APP 链接
+const activeAppLink = ref('')
+
+/** 打开弹窗 */
+const dialogVisible = ref(false)
+const open = (link: string) => {
+  activeAppLink.value = link
+  dialogVisible.value = true
+
+  // 滚动到当前的链接
+  const group = APP_LINK_GROUP_LIST.find((group) =>
+    group.links.some((linkItem) => isSameLink(linkItem.path, link))
+  )
+  if (group) {
+    // 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
+    nextTick(() => handleGroupSelected(group.name))
+  }
+}
+defineExpose({ open })
+
+// 处理 APP 链接选中
+const handleAppLinkSelected = (appLink: any) => {
+  if (!isSameLink(appLink.path, activeAppLink.value)) {
+    activeAppLink.value = appLink.path
+  }
+  switch (appLink.type) {
+    case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
+      detailSelectDialog.value.visible = true
+      detailSelectDialog.value.type = appLink.type
+      // 返显
+      detailSelectDialog.value.id =
+        getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value) || undefined
+      break
+    default:
+      break
+  }
+}
+
+// 处理绑定值更新
+const emit = defineEmits<{
+  change: [link: string]
+}>()
+const handleSubmit = () => {
+  dialogVisible.value = false
+  emit('change', activeAppLink.value)
+}
+
+// 分组标题引用列表
+const groupTitleRefs = ref<HTMLInputElement[]>([])
+/**
+ * 处理右侧链接列表滚动
+ * @param scrollTop 滚动条的位置
+ */
+const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
+  const titleEl = groupTitleRefs.value.find((titleEl) => {
+    // 获取标题的位置信息
+    const { offsetHeight, offsetTop } = titleEl
+    // 判断标题是否在可视范围内
+    return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight
+  })
+  // 只需处理一次
+  if (titleEl && activeGroup.value !== titleEl.textContent) {
+    activeGroup.value = titleEl.textContent || ''
+    // 同步左侧的滚动条位置
+    scrollToGroupBtn(activeGroup.value)
+  }
+}
+
+// 右侧滚动条
+const linkScrollbar = ref<ScrollbarInstance>()
+// 处理分组选中
+const handleGroupSelected = (group: string) => {
+  activeGroup.value = group
+  const titleRef = groupTitleRefs.value.find((item) => item.textContent === group)
+  if (titleRef) {
+    // 滚动分组标题
+    linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
+  }
+}
+
+// 分组滚动条
+const groupScrollbar = ref<ScrollbarInstance>()
+// 分组引用列表
+const groupBtnRefs = ref<ButtonInstance[]>([])
+// 自动滚动分组按钮,确保分组按钮保持在可视区域内
+const scrollToGroupBtn = (group: string) => {
+  const groupBtn = groupBtnRefs.value
+    .map((btn) => btn['ref'])
+    .find((ref) => ref.textContent === group)
+  if (groupBtn) {
+    groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
+  }
+}
+
+// 是否为相同的链接(不比较参数,只比较链接)
+const isSameLink = (link1: string, link2: string) => {
+  return split(link1, '?', 1)[0] === split(link2, '?', 1)[0]
+}
+
+// 详情选择对话框
+const detailSelectDialog = ref<{
+  visible: boolean
+  id?: number
+  type?: APP_LINK_TYPE_ENUM
+}>({
+  visible: false,
+  id: undefined,
+  type: undefined
+})
+// 处理详情选择
+const handleProductCategorySelected = (id: number) => {
+  const url = new URL(activeAppLink.value, 'http://127.0.0.1')
+  // 修改 id 参数
+  url.searchParams.set('id', `${id}`)
+  // 排除域名
+  activeAppLink.value = `${url.pathname}${url.search}`
+  // 关闭对话框
+  detailSelectDialog.value.visible = false
+  // 重置 id
+  detailSelectDialog.value.id = undefined
+}
+</script>
+<style lang="scss" scoped></style>

+ 246 - 0
src/components/AppLinkInput/data.ts

@@ -0,0 +1,246 @@
+// APP 链接类型(需要特殊处理,例如商品详情)
+export const enum APP_LINK_TYPE_ENUM {
+  // 拼团活动
+  ACTIVITY_COMBINATION,
+  // 秒杀活动
+  ACTIVITY_SECKILL,
+  // 文章详情
+  ARTICLE_DETAIL,
+  // 优惠券详情
+  COUPON_DETAIL,
+  // 自定义页面详情
+  DIY_PAGE_DETAIL,
+  // 品类列表
+  PRODUCT_CATEGORY_LIST,
+  // 商品列表
+  PRODUCT_LIST,
+  // 商品详情
+  PRODUCT_DETAIL_NORMAL,
+  // 拼团商品详情
+  PRODUCT_DETAIL_COMBINATION,
+  // 积分商品详情
+  PRODUCT_DETAIL_POINT,
+  // 秒杀商品详情
+  PRODUCT_DETAIL_SECKILL
+}
+
+// APP 链接列表(做一下持久化?)
+export const APP_LINK_GROUP_LIST = [
+  {
+    name: '商城',
+    links: [
+      {
+        name: '首页',
+        path: '/pages/index/index'
+      },
+      {
+        name: '商品分类',
+        path: '/pages/index/category',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST
+      },
+      {
+        name: '购物车',
+        path: '/pages/index/cart'
+      },
+      {
+        name: '个人中心',
+        path: '/pages/index/user'
+      },
+      {
+        name: '商品搜索',
+        path: '/pages/index/search'
+      },
+      {
+        name: '自定义页面',
+        path: '/pages/index/page',
+        type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL
+      },
+      {
+        name: '客服',
+        path: '/pages/chat/index'
+      },
+      {
+        name: '系统设置',
+        path: '/pages/public/setting'
+      },
+      {
+        name: '问题反馈',
+        path: '/pages/public/feedback'
+      },
+      {
+        name: '常见问题',
+        path: '/pages/public/faq'
+      }
+    ]
+  },
+  {
+    name: '商品',
+    links: [
+      {
+        name: '商品列表',
+        path: '/pages/goods/list',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_LIST
+      },
+      {
+        name: '商品详情',
+        path: '/pages/goods/index',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL
+      },
+      {
+        name: '拼团商品详情',
+        path: '/pages/goods/groupon',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION
+      },
+      {
+        name: '秒杀商品详情',
+        path: '/pages/goods/seckill',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL
+      },
+      {
+        name: '积分商品详情',
+        path: '/pages/goods/score',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_POINT
+      }
+    ]
+  },
+  {
+    name: '营销活动',
+    links: [
+      {
+        name: '拼团订单',
+        path: '/pages/activity/groupon/order'
+      },
+      {
+        name: '营销商品',
+        path: '/pages/activity/index'
+      },
+      {
+        name: '拼团活动',
+        path: '/pages/activity/groupon/list',
+        type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION
+      },
+      {
+        name: '秒杀活动',
+        path: '/pages/activity/seckill/list',
+        type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
+      },
+      {
+        name: '签到中心',
+        path: '/pages/app/sign'
+      },
+      {
+        name: '积分商城',
+        path: '/pages/app/score-shop'
+      },
+      {
+        name: '优惠券中心',
+        path: '/pages/coupon/list'
+      },
+      {
+        name: '优惠券详情',
+        path: '/pages/coupon/detail',
+        type: APP_LINK_TYPE_ENUM.COUPON_DETAIL
+      },
+      {
+        name: '文章详情',
+        path: '/pages/public/richtext',
+        type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL
+      }
+    ]
+  },
+  {
+    name: '分销商城',
+    links: [
+      {
+        name: '分销中心',
+        path: '/pages/commission/index'
+      },
+      {
+        name: '申请分销商',
+        path: '/pages/commission/apply'
+      },
+      {
+        name: '推广商品',
+        path: '/pages/commission/goods'
+      },
+      {
+        name: '分销订单',
+        path: '/pages/commission/order'
+      },
+      {
+        name: '分享记录',
+        path: '/pages/commission/share-log'
+      },
+      {
+        name: '我的团队',
+        path: '/pages/commission/team'
+      }
+    ]
+  },
+  {
+    name: '支付',
+    links: [
+      {
+        name: '充值余额',
+        path: '/pages/pay/recharge'
+      },
+      {
+        name: '充值记录',
+        path: '/pages/pay/recharge-log'
+      },
+      {
+        name: '申请提现',
+        path: '/pages/pay/withdraw'
+      },
+      {
+        name: '提现记录',
+        path: '/pages/pay/withdraw-log'
+      }
+    ]
+  },
+  {
+    name: '用户中心',
+    links: [
+      {
+        name: '用户信息',
+        path: '/pages/user/info'
+      },
+      {
+        name: '用户订单',
+        path: '/pages/order/list'
+      },
+      {
+        name: '售后订单',
+        path: '/pages/order/aftersale/list'
+      },
+      {
+        name: '商品收藏',
+        path: '/pages/user/goods-collect'
+      },
+      {
+        name: '浏览记录',
+        path: '/pages/user/goods-log'
+      },
+      {
+        name: '地址管理',
+        path: '/pages/user/address/list'
+      },
+      {
+        name: '发票管理',
+        path: '/pages/user/invoice/list'
+      },
+      {
+        name: '用户佣金',
+        path: '/pages/user/wallet/commission'
+      },
+      {
+        name: '用户余额',
+        path: '/pages/user/wallet/money'
+      },
+      {
+        name: '用户积分',
+        path: '/pages/user/wallet/score'
+      }
+    ]
+  }
+]

+ 43 - 0
src/components/AppLinkInput/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-input v-model="appLink" placeholder="输入或选择链接">
+    <template #append>
+      <el-button @click="handleOpenDialog">选择</el-button>
+    </template>
+  </el-input>
+  <AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+// APP 链接输入框
+defineOptions({ name: 'AppLinkInput' })
+// 定义属性
+const props = defineProps({
+  // 当前选中的链接
+  modelValue: propTypes.string.def('')
+})
+// 当前的链接
+const appLink = ref('')
+// 选择对话框
+const dialogRef = ref()
+// 处理打开对话框
+const handleOpenDialog = () => dialogRef.value?.open(appLink.value)
+// 处理 APP 链接选中
+const handleLinkSelected = (link: string) => (appLink.value = link)
+
+// getter
+watch(
+  () => props.modelValue,
+  () => (appLink.value = props.modelValue),
+  { immediate: true }
+)
+
+// setter
+const emit = defineEmits<{
+  'update:modelValue': [link: string]
+}>()
+watch(
+  () => appLink,
+  () => emit('update:modelValue', appLink.value)
+)
+</script>

+ 1 - 1
src/components/DiyEditor/components/ComponentContainerProperty.vue

@@ -1,6 +1,6 @@
 <template>
   <el-tabs stretch>
-    <el-tab-pane label="内容">
+    <el-tab-pane label="内容" v-if="$slots.default">
       <slot></slot>
     </el-tab-pane>
     <el-tab-pane label="样式" lazy>

+ 1 - 1
src/components/DiyEditor/components/mobile/Carousel/property.vue

@@ -103,7 +103,7 @@
                   </el-form-item>
                 </template>
                 <el-form-item label="链接" class="m-b-8px!" label-width="50px">
-                  <el-input placeholder="链接" v-model="element.url" />
+                  <AppLinkInput v-model="element.url" />
                 </el-form-item>
               </div>
             </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/ImageBar/property.vue

@@ -13,7 +13,7 @@
         </UploadImg>
       </el-form-item>
       <el-form-item label="链接" prop="url">
-        <el-input placeholder="链接" v-model="formData.url" />
+        <AppLinkInput v-model="formData.url" />
       </el-form-item>
     </el-form>
   </ComponentContainerProperty>

+ 1 - 1
src/components/DiyEditor/components/mobile/MagicCube/property.vue

@@ -17,7 +17,7 @@
             <UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
           </el-form-item>
           <el-form-item label="链接" :prop="`list[${index}].url`">
-            <el-input v-model="hotArea.url" placeholder="请输入链接" />
+            <AppLinkInput v-model="hotArea.url" />
           </el-form-item>
         </template>
       </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuGrid/property.vue

@@ -38,7 +38,7 @@
                 <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
               <el-form-item label="显示角标" prop="badge.show">
                 <el-switch v-model="element.badge.show" />

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuList/property.vue

@@ -31,7 +31,7 @@
                 <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
             </div>
           </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue

@@ -48,7 +48,7 @@
                 <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
               <el-form-item label="显示角标" prop="badge.show">
                 <el-switch v-model="element.badge.show" />

+ 1 - 1
src/components/DiyEditor/components/mobile/NoticeBar/property.vue

@@ -35,7 +35,7 @@
             </div>
             <div class="w-full flex flex-col gap-8px">
               <el-input v-model="element.text" placeholder="请输入公告" />
-              <el-input v-model="element.url" placeholder="请输入链接" />
+              <AppLinkInput v-model="element.url" />
             </div>
           </div>
         </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/config.ts

@@ -1,6 +1,6 @@
 import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 
-/** 商品卡片属性 */
+/** 商品属性 */
 export interface ProductListProperty {
   // 布局类型:双列 | 三列 | 水平滑动
   layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/index.vue

@@ -66,7 +66,7 @@
 import { ProductListProperty } from './config'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 
-/** 商品卡片 */
+/** 商品 */
 defineOptions({ name: 'ProductList' })
 // 定义属性
 const props = defineProps<{ property: ProductListProperty }>()

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/property.vue

@@ -88,7 +88,7 @@ import { ProductListProperty } from './config'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
 
-// 商品卡片属性面板
+// 商品属性面板
 defineOptions({ name: 'ProductListProperty' })
 
 const props = defineProps<{ modelValue: ProductListProperty }>()

+ 25 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/config.ts

@@ -0,0 +1,25 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 营销文章属性 */
+export interface PromotionArticleProperty {
+  // 文章编号
+  id: number
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionArticle',
+  name: '营销文章',
+  icon: 'ph:article-medium',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionArticleProperty>

+ 27 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/index.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="min-h-30px" v-html="article.content"></div>
+</template>
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+/** 营销文章 */
+defineOptions({ name: 'PromotionArticle' })
+// 定义属性
+const props = defineProps<{ property: PromotionArticleProperty }>()
+// 商品列表
+const article = ref<ArticleApi.ArticleVO[]>({})
+watch(
+  () => props.property.id,
+  async () => {
+    if (props.property.id) {
+      article.value = await ArticleApi.getArticle(props.property.id)
+    }
+  },
+  {
+    immediate: true
+  }
+)
+</script>
+
+<style scoped lang="scss"></style>

+ 56 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/property.vue

@@ -0,0 +1,56 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="40px" :model="formData">
+      <el-form-item label="文章" prop="id">
+        <el-select
+          v-model="formData.id"
+          placeholder="请选择文章"
+          class="w-full"
+          filterable
+          remote
+          :remote-method="queryArticleList"
+          :loading="loading"
+        >
+          <el-option
+            v-for="article in articles"
+            :key="article.id"
+            :label="article.title"
+            :value="article.id"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+// 营销文章属性面板
+defineOptions({ name: 'PromotionArticleProperty' })
+
+const props = defineProps<{ modelValue: PromotionArticleProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 文章列表
+const articles = ref<ArticleApi.ArticleVO>([])
+
+// 加载中
+const loading = ref(false)
+// 查询文章列表
+const queryArticleList = async (title?: string) => {
+  loading.value = true
+  const { list } = await ArticleApi.getArticlePage({ title, pageSize: 10 })
+  articles.value = list
+  loading.value = false
+}
+
+// 初始化
+onMounted(() => {
+  queryArticleList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 64 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts

@@ -0,0 +1,64 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 拼团属性 */
+export interface PromotionCombinationProperty {
+  // 布局类型:单列 | 三列
+  layoutType: 'oneCol' | 'threeCol'
+  // 商品字段
+  fields: {
+    // 商品名称
+    name: PromotionCombinationFieldProperty
+    // 商品价格
+    price: PromotionCombinationFieldProperty
+  }
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标图片
+    imgUrl: string
+  }
+  // 上圆角
+  borderRadiusTop: number
+  // 下圆角
+  borderRadiusBottom: number
+  // 间距
+  space: number
+  // 拼团活动编号
+  activityId: number
+  // 组件样式
+  style: ComponentStyle
+}
+// 商品字段
+export interface PromotionCombinationFieldProperty {
+  // 是否显示
+  show: boolean
+  // 颜色
+  color: string
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionCombination',
+  name: '拼团',
+  icon: 'mdi:account-group',
+  property: {
+    activityId: undefined,
+    layoutType: 'oneCol',
+    fields: {
+      name: { show: true, color: '#000' },
+      price: { show: true, color: '#ff3000' }
+    },
+    badge: { show: false, imgUrl: '' },
+    borderRadiusTop: 8,
+    borderRadiusBottom: 8,
+    space: 8,
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionCombinationProperty>

+ 125 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+    <!-- 商品网格 -->
+    <div
+      class="grid overflow-x-auto"
+      :style="{
+        gridGap: `${property.space}px`,
+        gridTemplateColumns,
+        width: scrollbarWidth
+      }"
+    >
+      <!-- 商品 -->
+      <div
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        :style="{
+          borderTopLeftRadius: `${property.borderRadiusTop}px`,
+          borderTopRightRadius: `${property.borderRadiusTop}px`,
+          borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+          borderBottomRightRadius: `${property.borderRadiusBottom}px`
+        }"
+        v-for="(spu, index) in spuList"
+        :key="index"
+      >
+        <!-- 角标 -->
+        <div
+          v-if="property.badge.show"
+          class="absolute left-0 top-0 z-1 items-center justify-center"
+        >
+          <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+        </div>
+        <!-- 商品封面图 -->
+        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <div
+          :class="[
+            'flex flex-col gap-8px p-8px box-border',
+            {
+              'w-[calc(100%-64px)]': columns === 2,
+              'w-full': columns === 3
+            }
+          ]"
+        >
+          <!-- 商品名称 -->
+          <div
+            v-if="property.fields.name.show"
+            class="truncate text-12px"
+            :style="{ color: property.fields.name.color }"
+          >
+            {{ spu.name }}
+          </div>
+          <div>
+            <!-- 商品价格 -->
+            <span
+              v-if="property.fields.price.show"
+              class="text-12px"
+              :style="{ color: property.fields.price.color }"
+            >
+              ¥{{ spu.price }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+
+/** 拼团 */
+defineOptions({ name: 'PromotionCombination' })
+// 定义属性
+const props = defineProps<{ property: PromotionCombinationProperty }>()
+// 商品列表
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+  () => props.property.activityId,
+  async () => {
+    if (!props.property.activityId) return
+    const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId)
+    if (!activity?.spuId) return
+    spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+// 手机宽度
+const phoneWidth = ref(375)
+// 容器
+const containerRef = ref()
+// 商品的列数
+const columns = ref(2)
+// 滚动条宽度
+const scrollbarWidth = ref('100%')
+// 商品图大小
+const imageSize = ref('0')
+// 商品网络列数
+const gridTemplateColumns = ref('')
+// 计算布局参数
+watch(
+  () => [props.property, phoneWidth, spuList.value.length],
+  () => {
+    // 计算列数
+    columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
+    // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
+    const productWidth =
+      (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
+    // 商品图布局:2列时,左右布局 3列时,上下布局
+    imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
+    // 指定列数
+    gridTemplateColumns.value = `repeat(${columns.value}, auto)`
+    // 不滚动
+    scrollbarWidth.value = '100%'
+  },
+  { immediate: true, deep: true }
+)
+onMounted(() => {
+  // 提取手机宽度
+  phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 112 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue

@@ -0,0 +1,112 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="80px" :model="formData">
+      <el-card header="拼团活动" class="property-group" shadow="never">
+        <el-form-item label="拼团活动" prop="activityId">
+          <el-select v-model="formData.activityId">
+            <el-option
+              v-for="activity in activityList"
+              :key="activity.id"
+              :label="activity.name"
+              :value="activity.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="布局" prop="type">
+          <el-radio-group v-model="formData.layoutType">
+            <el-tooltip class="item" content="单列" placement="bottom">
+              <el-radio-button label="oneCol">
+                <Icon icon="fluent:text-column-one-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip class="item" content="三列" placement="bottom">
+              <el-radio-button label="threeCol">
+                <Icon icon="fluent:text-column-three-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="商品名称" prop="fields.name.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.name.color" />
+            <el-checkbox v-model="formData.fields.name.show" />
+          </div>
+        </el-form-item>
+        <el-form-item label="商品价格" prop="fields.price.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.price.color" />
+            <el-checkbox v-model="formData.fields.price.show" />
+          </div>
+        </el-form-item>
+      </el-card>
+      <el-card header="角标" class="property-group" shadow="never">
+        <el-form-item label="角标" prop="badge.show">
+          <el-switch v-model="formData.badge.show" />
+        </el-form-item>
+        <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
+          <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+            <template #tip> 建议尺寸:36 * 22 </template>
+          </UploadImg>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="上圆角" prop="borderRadiusTop">
+          <el-slider
+            v-model="formData.borderRadiusTop"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="下圆角" prop="borderRadiusBottom">
+          <el-slider
+            v-model="formData.borderRadiusBottom"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="间隔" prop="space">
+          <el-slider
+            v-model="formData.space"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+      </el-card>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+
+// 拼团属性面板
+defineOptions({ name: 'PromotionCombinationProperty' })
+
+const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 活动列表
+const activityList = ref<CombinationActivityApi.CombinationActivityVO>([])
+onMounted(async () => {
+  const { list } = await CombinationActivityApi.getCombinationActivityPage({
+    status: CommonStatusEnum.ENABLE
+  })
+  activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 64 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts

@@ -0,0 +1,64 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 秒杀属性 */
+export interface PromotionSeckillProperty {
+  // 布局类型:单列 | 三列
+  layoutType: 'oneCol' | 'threeCol'
+  // 商品字段
+  fields: {
+    // 商品名称
+    name: PromotionSeckillFieldProperty
+    // 商品价格
+    price: PromotionSeckillFieldProperty
+  }
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标图片
+    imgUrl: string
+  }
+  // 上圆角
+  borderRadiusTop: number
+  // 下圆角
+  borderRadiusBottom: number
+  // 间距
+  space: number
+  // 秒杀活动编号
+  activityId: number
+  // 组件样式
+  style: ComponentStyle
+}
+// 商品字段
+export interface PromotionSeckillFieldProperty {
+  // 是否显示
+  show: boolean
+  // 颜色
+  color: string
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionSeckill',
+  name: '秒杀',
+  icon: 'mdi:calendar-time',
+  property: {
+    activityId: undefined,
+    layoutType: 'oneCol',
+    fields: {
+      name: { show: true, color: '#000' },
+      price: { show: true, color: '#ff3000' }
+    },
+    badge: { show: false, imgUrl: '' },
+    borderRadiusTop: 8,
+    borderRadiusBottom: 8,
+    space: 8,
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionSeckillProperty>

+ 125 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+    <!-- 商品网格 -->
+    <div
+      class="grid overflow-x-auto"
+      :style="{
+        gridGap: `${property.space}px`,
+        gridTemplateColumns,
+        width: scrollbarWidth
+      }"
+    >
+      <!-- 商品 -->
+      <div
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        :style="{
+          borderTopLeftRadius: `${property.borderRadiusTop}px`,
+          borderTopRightRadius: `${property.borderRadiusTop}px`,
+          borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+          borderBottomRightRadius: `${property.borderRadiusBottom}px`
+        }"
+        v-for="(spu, index) in spuList"
+        :key="index"
+      >
+        <!-- 角标 -->
+        <div
+          v-if="property.badge.show"
+          class="absolute left-0 top-0 z-1 items-center justify-center"
+        >
+          <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+        </div>
+        <!-- 商品封面图 -->
+        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <div
+          :class="[
+            'flex flex-col gap-8px p-8px box-border',
+            {
+              'w-[calc(100%-64px)]': columns === 2,
+              'w-full': columns === 3
+            }
+          ]"
+        >
+          <!-- 商品名称 -->
+          <div
+            v-if="property.fields.name.show"
+            class="truncate text-12px"
+            :style="{ color: property.fields.name.color }"
+          >
+            {{ spu.name }}
+          </div>
+          <div>
+            <!-- 商品价格 -->
+            <span
+              v-if="property.fields.price.show"
+              class="text-12px"
+              :style="{ color: property.fields.price.color }"
+            >
+              ¥{{ spu.price }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+
+/** 秒杀 */
+defineOptions({ name: 'PromotionSeckill' })
+// 定义属性
+const props = defineProps<{ property: PromotionSeckillProperty }>()
+// 商品列表
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+  () => props.property.activityId,
+  async () => {
+    if (!props.property.activityId) return
+    const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
+    if (!activity?.spuId) return
+    spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+// 手机宽度
+const phoneWidth = ref(375)
+// 容器
+const containerRef = ref()
+// 商品的列数
+const columns = ref(2)
+// 滚动条宽度
+const scrollbarWidth = ref('100%')
+// 商品图大小
+const imageSize = ref('0')
+// 商品网络列数
+const gridTemplateColumns = ref('')
+// 计算布局参数
+watch(
+  () => [props.property, phoneWidth, spuList.value.length],
+  () => {
+    // 计算列数
+    columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
+    // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
+    const productWidth =
+      (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
+    // 商品图布局:2列时,左右布局 3列时,上下布局
+    imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
+    // 指定列数
+    gridTemplateColumns.value = `repeat(${columns.value}, auto)`
+    // 不滚动
+    scrollbarWidth.value = '100%'
+  },
+  { immediate: true, deep: true }
+)
+onMounted(() => {
+  // 提取手机宽度
+  phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 112 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue

@@ -0,0 +1,112 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="80px" :model="formData">
+      <el-card header="秒杀活动" class="property-group" shadow="never">
+        <el-form-item label="秒杀活动" prop="activityId">
+          <el-select v-model="formData.activityId">
+            <el-option
+              v-for="activity in activityList"
+              :key="activity.id"
+              :label="activity.name"
+              :value="activity.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="布局" prop="type">
+          <el-radio-group v-model="formData.layoutType">
+            <el-tooltip class="item" content="单列" placement="bottom">
+              <el-radio-button label="oneCol">
+                <Icon icon="fluent:text-column-one-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip class="item" content="三列" placement="bottom">
+              <el-radio-button label="threeCol">
+                <Icon icon="fluent:text-column-three-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="商品名称" prop="fields.name.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.name.color" />
+            <el-checkbox v-model="formData.fields.name.show" />
+          </div>
+        </el-form-item>
+        <el-form-item label="商品价格" prop="fields.price.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.price.color" />
+            <el-checkbox v-model="formData.fields.price.show" />
+          </div>
+        </el-form-item>
+      </el-card>
+      <el-card header="角标" class="property-group" shadow="never">
+        <el-form-item label="角标" prop="badge.show">
+          <el-switch v-model="formData.badge.show" />
+        </el-form-item>
+        <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
+          <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+            <template #tip> 建议尺寸:36 * 22 </template>
+          </UploadImg>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="上圆角" prop="borderRadiusTop">
+          <el-slider
+            v-model="formData.borderRadiusTop"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="下圆角" prop="borderRadiusBottom">
+          <el-slider
+            v-model="formData.borderRadiusBottom"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="间隔" prop="space">
+          <el-slider
+            v-model="formData.space"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+      </el-card>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+
+// 秒杀属性面板
+defineOptions({ name: 'PromotionSeckillProperty' })
+
+const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 活动列表
+const activityList = ref<SeckillActivityApi.SeckillActivityVO>([])
+onMounted(async () => {
+  const { list } = await SeckillActivityApi.getSeckillActivityPage({
+    status: CommonStatusEnum.ENABLE
+  })
+  activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 1 - 1
src/components/DiyEditor/components/mobile/TabBar/property.vue

@@ -88,7 +88,7 @@
                 <el-input v-model="element.text" placeholder="请输入文字" />
               </el-form-item>
               <el-form-item prop="url" label-width="0" class="m-b-0!">
-                <el-input v-model="element.url" placeholder="请选择链接" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
             </div>
           </div>

+ 1 - 1
src/components/DiyEditor/components/mobile/TitleBar/property.vue

@@ -92,7 +92,7 @@
           <el-input v-model="formData.more.text" />
         </el-form-item>
         <el-form-item label="跳转链接" prop="more.url">
-          <el-input v-model="formData.more.url" placeholder="请输入跳转链接" />
+          <AppLinkInput v-model="formData.more.url" />
         </el-form-item>
       </template>
     </el-form>

+ 21 - 0
src/components/DiyEditor/components/mobile/UserCard/config.ts

@@ -0,0 +1,21 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户卡片属性 */
+export interface UserCardProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserCard',
+  name: '用户卡片',
+  icon: 'mdi:user-card-details',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserCardProperty>

+ 29 - 0
src/components/DiyEditor/components/mobile/UserCard/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="flex flex-col">
+    <div class="flex items-center justify-between p-x-18px p-y-24px">
+      <div class="flex flex-1 items-center gap-16px">
+        <el-avatar :size="60">
+          <Icon icon="ep:avatar" :size="60" />
+        </el-avatar>
+        <span class="text-18px font-bold">芋道源码</span>
+      </div>
+      <Icon icon="tdesign:qrcode" :size="20" />
+    </div>
+    <div
+      class="flex items-center justify-between justify-between bg-white p-x-20px p-y-8px text-12px"
+    >
+      <span class="color-#ff690d">点击绑定手机号</span>
+      <span class="rounded-26px bg-#ff6100 p-x-8px p-y-5px color-white">去绑定</span>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+
+/** 用户卡片 */
+defineOptions({ name: 'UserCard' })
+// 定义属性
+defineProps<{ property: UserCardProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserCard/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户卡片属性面板
+defineOptions({ name: 'UserCardProperty' })
+
+const props = defineProps<{ modelValue: UserCardProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserCoupon/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户卡券属性 */
+export interface UserCouponProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserCoupon',
+  name: '用户卡券',
+  icon: 'ep:ticket',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserCouponProperty>

+ 15 - 0
src/components/DiyEditor/components/mobile/UserCoupon/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <el-image
+    src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/couponCardStyle.png"
+  />
+</template>
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+
+/** 用户卡券 */
+defineOptions({ name: 'UserCoupon' })
+// 定义属性
+defineProps<{ property: UserCouponProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserCoupon/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户卡券属性面板
+defineOptions({ name: 'UserCouponProperty' })
+
+const props = defineProps<{ modelValue: UserCouponProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserOrder/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户订单属性 */
+export interface UserOrderProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserOrder',
+  name: '用户订单',
+  icon: 'ep:list',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserOrderProperty>

+ 13 - 0
src/components/DiyEditor/components/mobile/UserOrder/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <el-image src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/orderCardStyle.png" />
+</template>
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+
+/** 用户订单 */
+defineOptions({ name: 'UserOrder' })
+// 定义属性
+defineProps<{ property: UserOrderProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserOrder/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户订单属性面板
+defineOptions({ name: 'UserOrderProperty' })
+
+const props = defineProps<{ modelValue: UserOrderProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserWallet/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户资产属性 */
+export interface UserWalletProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserWallet',
+  name: '用户资产',
+  icon: 'ep:wallet-filled',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserWalletProperty>

+ 15 - 0
src/components/DiyEditor/components/mobile/UserWallet/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <el-image
+    src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/walletCardStyle.png"
+  />
+</template>
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+
+/** 用户资产 */
+defineOptions({ name: 'UserWallet' })
+// 定义属性
+defineProps<{ property: UserWalletProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserWallet/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户资产属性面板
+defineOptions({ name: 'UserWalletProperty' })
+
+const props = defineProps<{ modelValue: UserWalletProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

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

@@ -109,13 +109,19 @@ export const PAGE_LIBS = [
   },
   { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
   {
-    name: '会员组件',
+    name: '用户组件',
     extended: true,
     components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon']
   },
   {
     name: '营销组件',
     extended: true,
-    components: ['CombinationCard', 'SeckillCard', 'PointCard', 'CouponCard']
+    components: [
+      'PromotionCombination',
+      'PromotionSeckill',
+      'PromotionPoint',
+      'CouponCard',
+      'PromotionArticle'
+    ]
   }
 ] as DiyComponentLibrary[]

+ 7 - 2
src/components/RouterSearch/index.vue

@@ -29,7 +29,12 @@
       :class="showTopSearch ? 'w-220px ml2' : 'w-0'"
       @change="handleChange"
     >
-      <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+      <el-option
+        v-for="item in options"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      />
     </el-select>
   </div>
 </template>
@@ -73,7 +78,7 @@ function remoteMethod(data) {
 
 function handleChange(path) {
   router.push({ path })
-  hiddenTopSearch();
+  hiddenTopSearch()
 }
 
 function hiddenTopSearch() {

+ 1 - 1
src/layout/components/ToolHeader.vue

@@ -65,7 +65,7 @@ export default defineComponent({
           {screenfull.value ? (
             <Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
           ) : undefined}
-          {search.value ? (<RouterSearch isModal={false} />) : undefined}
+          {search.value ? <RouterSearch isModal={false} /> : undefined}
           {size.value ? (
             <SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
           ) : undefined}

+ 22 - 0
src/utils/index.ts

@@ -1,3 +1,5 @@
+import { toNumber } from 'lodash-es'
+
 /**
  *
  * @param component 需要注册的组件
@@ -263,3 +265,23 @@ export const calculateRelativeRate = (value?: number, reference?: number) => {
 
   return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
 }
+
+/**
+ * 获取链接的参数值
+ * @param key 参数键名
+ * @param urlStr 链接地址,默认为当前浏览器的地址
+ */
+export const getUrlValue = (key: string, urlStr: string = location.href): string => {
+  if (!urlStr || !key) return ''
+  const url = new URL(decodeURIComponent(urlStr))
+  return url.searchParams.get(key) ?? ''
+}
+
+/**
+ * 获取链接的参数值(值类型)
+ * @param key 参数键名
+ * @param urlStr 链接地址,默认为当前浏览器的地址
+ */
+export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => {
+  return toNumber(getUrlValue(key, urlStr))
+}

+ 1 - 1
src/views/crm/businessStatusType/index.vue

@@ -95,7 +95,7 @@ const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
-  pageSize: 10,
+  pageSize: 10
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中

+ 2 - 2
src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue

@@ -12,9 +12,9 @@
           v-model="formData.userIds"
           :data="userTree"
           :props="defaultProps"
-          check-on-click-node
           multiple
           filterable
+          check-on-click-node
           node-key="id"
           placeholder="请选择规则适用人群"
         />
@@ -25,8 +25,8 @@
           :data="deptTree"
           :props="defaultProps"
           multiple
-          check-strictly
           filterable
+          check-strictly
           node-key="id"
           placeholder="请选择规则适用部门"
         />

+ 5 - 1
src/views/infra/codegen/EditTable.vue

@@ -8,7 +8,11 @@
         <colum-info-form ref="columInfoRef" :columns="formData.columns" />
       </el-tab-pane>
       <el-tab-pane label="生成信息" name="generateInfo">
-        <generate-info-form ref="generateInfoRef" :table="formData.table" :columns="formData.columns" />
+        <generate-info-form
+          ref="generateInfoRef"
+          :table="formData.table"
+          :columns="formData.columns"
+        />
       </el-tab-pane>
     </el-tabs>
     <el-form>

+ 1 - 1
src/views/infra/demo/demo01/Demo01ContactForm.vue

@@ -123,4 +123,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo02/Demo02CategoryForm.vue

@@ -111,4 +111,4 @@ const getDemo02CategoryTree = async () => {
   root.children = handleTree(data, 'id', 'parentId')
   demo02CategoryTree.value.push(root)
 }
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/erp/Demo03StudentForm.vue

@@ -118,4 +118,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue

@@ -96,4 +96,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 4 - 4
src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue

@@ -11,7 +11,7 @@
     </el-button>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
-       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="名字" align="center" prop="name" />
       <el-table-column label="分数" align="center" prop="score" />
       <el-table-column
         label="创建时间"
@@ -49,8 +49,8 @@
       @pagination="getList"
     />
   </ContentWrap>
-    <!-- 表单弹窗:添加/修改 -->
-    <Demo03CourseForm ref="formRef" @success="getList" />
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo03CourseForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
@@ -123,4 +123,4 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue

@@ -96,4 +96,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 4 - 4
src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue

@@ -11,7 +11,7 @@
     </el-button>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
-       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="名字" align="center" prop="name" />
       <el-table-column label="班主任" align="center" prop="teacher" />
       <el-table-column
         label="创建时间"
@@ -49,8 +49,8 @@
       @pagination="getList"
     />
   </ContentWrap>
-    <!-- 表单弹窗:添加/修改 -->
-    <Demo03GradeForm ref="formRef" @success="getList" />
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo03GradeForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
@@ -123,4 +123,4 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/inner/Demo03StudentForm.vue

@@ -150,4 +150,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 3 - 3
src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue

@@ -9,7 +9,7 @@
   >
     <el-table :data="formData" class="-mt-10px">
       <el-table-column label="序号" type="index" width="100" />
-       <el-table-column label="名字" min-width="150">
+      <el-table-column label="名字" min-width="150">
         <template #default="{ row, $index }">
           <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
             <el-input v-model="row.name" placeholder="请输入名字" />
@@ -57,7 +57,7 @@ watch(
     formData.value = []
     // 2. val 非空,则加载数据
     if (!val) {
-      return;
+      return
     }
     try {
       formLoading.value = true
@@ -97,4 +97,4 @@ const getData = () => {
 }
 
 defineExpose({ validate, getData })
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue

@@ -48,4 +48,4 @@ const handleQuery = () => {
 onMounted(() => {
   getList()
 })
-</script>
+</script>

+ 4 - 4
src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue

@@ -6,7 +6,7 @@
     label-width="100px"
     v-loading="formLoading"
   >
-     <el-form-item label="名字" prop="name">
+    <el-form-item label="名字" prop="name">
       <el-input v-model="formData.name" placeholder="请输入名字" />
     </el-form-item>
     <el-form-item label="班主任" prop="teacher">
@@ -38,11 +38,11 @@ watch(
       id: undefined,
       studentId: undefined,
       name: undefined,
-      teacher: undefined,
+      teacher: undefined
     }
     // 2. val 非空,则加载数据
     if (!val) {
-      return;
+      return
     }
     try {
       formLoading.value = true
@@ -69,4 +69,4 @@ const getData = () => {
 }
 
 defineExpose({ validate, getData })
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue

@@ -52,4 +52,4 @@ const handleQuery = () => {
 onMounted(() => {
   getList()
 })
-</script>
+</script>

+ 1 - 1
src/views/infra/demo/demo03/normal/Demo03StudentForm.vue

@@ -150,4 +150,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>

+ 3 - 3
src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue

@@ -9,7 +9,7 @@
   >
     <el-table :data="formData" class="-mt-10px">
       <el-table-column label="序号" type="index" width="100" />
-       <el-table-column label="名字" min-width="150">
+      <el-table-column label="名字" min-width="150">
         <template #default="{ row, $index }">
           <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
             <el-input v-model="row.name" placeholder="请输入名字" />
@@ -57,7 +57,7 @@ watch(
     formData.value = []
     // 2. val 非空,则加载数据
     if (!val) {
-      return;
+      return
     }
     try {
       formLoading.value = true
@@ -97,4 +97,4 @@ const getData = () => {
 }
 
 defineExpose({ validate, getData })
-</script>
+</script>

+ 4 - 4
src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue

@@ -6,7 +6,7 @@
     label-width="100px"
     v-loading="formLoading"
   >
-     <el-form-item label="名字" prop="name">
+    <el-form-item label="名字" prop="name">
       <el-input v-model="formData.name" placeholder="请输入名字" />
     </el-form-item>
     <el-form-item label="班主任" prop="teacher">
@@ -38,11 +38,11 @@ watch(
       id: undefined,
       studentId: undefined,
       name: undefined,
-      teacher: undefined,
+      teacher: undefined
     }
     // 2. val 非空,则加载数据
     if (!val) {
-      return;
+      return
     }
     try {
       formLoading.value = true
@@ -69,4 +69,4 @@ const getData = () => {
 }
 
 defineExpose({ validate, getData })
-</script>
+</script>

+ 8 - 4
src/views/mall/product/category/components/ProductCategorySelect.vue

@@ -20,8 +20,12 @@ import { propTypes } from '@/utils/propTypes'
 defineOptions({ name: 'ProductCategorySelect' })
 
 const props = defineProps({
-  modelValue: oneOfType([propTypes.number.def(undefined), propTypes.array.def([])]).def(undefined), // 选中的ID
-  multiple: propTypes.bool.def(false) // 是否多选
+  // 选中的ID
+  modelValue: oneOfType<number | number[]>([Number, Array<Number>]),
+  // 是否多选
+  multiple: propTypes.bool.def(false),
+  // 上级品类的编号
+  parentId: propTypes.number.def(undefined)
 })
 
 /** 选中的分类 ID */
@@ -38,10 +42,10 @@ const selectCategoryId = computed({
 const emit = defineEmits(['update:modelValue'])
 
 /** 初始化 **/
-const categoryList = ref([]) // 分类树
+const categoryList = ref<ProductCategoryApi.CategoryVO[]>([]) // 分类树
 onMounted(async () => {
   // 获得分类树
-  const data = await ProductCategoryApi.getCategoryList({})
+  const data = await ProductCategoryApi.getCategoryList({ parentId: props.parentId })
   categoryList.value = handleTree(data, 'id', 'parentId')
 })
 </script>