Browse Source

营销:商城装修增加控件【APP 链接选择】

owen 1 year ago
parent
commit
23a6bf5ef5

+ 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/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/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>

+ 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))
+}

+ 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>