فهرست منبع

```
feat: 添加意见反馈和商品收藏功能

- 新增意见反馈页面及相关的意见类型、提交、列表接口
- 新增商品收藏功能包括添加、取消、批量取消等接口
- 在用户中心页面增加意见反馈入口
- 在星闪豹模块添加商品收藏入口
- 更新视频权益订单状态显示,新增待支付和已取消状态
- 修复积分明细展示问题,调整为正确的业务场景格式
- 优化视频权益订单支付流程,添加取消订单功能
- 调整首页导航栏显示逻辑,确保视频权益始终可见
- 修改开发环境配置地址
```

zouzexu 9 ساعت پیش
والد
کامیت
3a9594fa23

+ 19 - 1
src/api/api.type.d.ts

@@ -486,6 +486,24 @@ namespace Api {
     weightUnit?: string
     [property: string]: any
   }
+  interface xsbCollectItem {
+    id: number
+    memberId: number
+    channelId: number
+    skuId: number | null
+    shopId: number
+    tenantId: number
+    shopName: string
+    skuName: string
+    prodId: number
+    spec: string
+    pic: string
+    isDelete: string
+    shopSkuStocks: string
+    saleStatus: number
+    price: number
+    [property: string]: any
+  }
   interface xsbFindUserPointsPage {
     /**
      * 渠道(企业ID)
@@ -3039,7 +3057,7 @@ namespace Api {
      */
     rechargeAccount?: string
     /**
-     * 充值状态:1-处理中 2-成功 3-失败
+     * 充值状态:0-待支付 1-处理中 2-成功 3-失败 4-已取消
      */
     rechargeStatus?: number
     /**

+ 10 - 2
src/api/apiDefinitions.ts

@@ -40,12 +40,15 @@ export default {
   'sys.userVipInfo':['GET', '/smqjh-system/app-api/v1/member/current'],
   'sys.giftsList': ['GET', '/smqjh-system/app-api/v1/member/gifts'],
   'sys.giftsReceive': ['POST', '/smqjh-system/app-api/v1/member/gifts/receive'],
+  'sys.opinionTypeList': ['GET', '/smqjh-system/api/v1/opinionFeedback/opinionTypeList'],
+  'sys.addOpinionFeedback': ['POST', '/smqjh-system/api/v1/opinionFeedback/addSmOpinionFeedback'],
+  'sys.myOpinionFeedback': ['GET', '/smqjh-system/api/v1/opinionFeedback/myOpinionFeedback'],
 
   'xsb.categories':['GET', '/smqjh-pms/app-api/v1/categories'],
   'xsb.getCategoryProductList':['POST', '/smqjh-pms/app-api/v1/spu/getCategoryProductList'],
   'xsb.getProductDetail':['GET', '/smqjh-pms/app-api/v1/spu/getProductDetails'],
-  'xsb.findUserPointsPage':['PUT', '/smqjh-system/app-api/v1/points/findUserPointsPage'], //积分明细老接口
-  // 'xsb.findUserPointsPage':['GET', '/smqjh-system/app-api/v1/points/getPointsUserRecord'], //积分明细新接口
+  // 'xsb.findUserPointsPage':['PUT', '/smqjh-system/app-api/v1/points/findUserPointsPage'], //积分明细老接口
+  'xsb.findUserPointsPage':['GET', '/smqjh-system/app-api/v1/points/getPointsUserRecord'], //积分明细新接口
   'xsb.getUserPointsExpiryDate':['GET', '/smqjh-system/app-api/v1/points/getUserPointsExpiryDate'],
   'xsb.findUserPoints':['GET', '/smqjh-system/app-api/v1/points/findUserPoints'],
   'xsb.getSearchProductList':['POST', '/smqjh-pms/app-api/v1/spu/getSearchProductList'],
@@ -76,6 +79,10 @@ export default {
   'xsb.memberCouponPage':['GET', '/smqjh-system/app-api/memberCoupon/getPageList'],
   'xsb.orderCoupons':['GET', '/smqjh-oms/api/v1/order/orderCoupons'],
   'xsb.newCouponPopup':['GET', '/smqjh-system/sys-api/coupon/newCouponPopup'],
+  'xsb.addCollect':['POST', '/smqjh-oms/app-api/v1/collect/addCollect'],
+  'xsb.cancelCollect':['GET', '/smqjh-oms/app-api/v1/collect/goodsInfoCancelCollect'],
+  'xsb.myCollect':['GET', '/smqjh-oms/app-api/v1/collect/myCollect'],
+  'xsb.batchCancelCollect':['DELETE', '/smqjh-oms/app-api/v1/collect/{ids}'],
 
   'common.myShoppingCart':['GET', '/smqjh-oms/app-api/v1/shoppingCart/myShoppingCart'],
   'common.addShoppingCart':['POST', '/smqjh-oms/app-api/v1/shoppingCart/addShoppingCart'],
@@ -128,6 +135,7 @@ export default {
   'videoRight.orderCreate':['POST', '/smqjh-oms/app-api/v1/order/virtual'],
   'videoRight.orderDetail':['GET', '/smqjh-oms/app-api/v1/order/virtual/detail'],
   'videoRight.orderList':['GET', '/smqjh-oms/app-api/v1/order/virtual/list'],
+  'videoRight.orderCancel':['POST', '/smqjh-oms/app-api/v1/order/virtual/cancel'],
 
   'djk.appletGoodsList':['GET', '/smqjh-pms/app-api/v1/goods/appletGoodsList'],
   'djk.appletShopInfo':['GET', '/smqjh-pms/app-api/v1/goods/appletShopInfo'],

+ 83 - 0
src/api/globals.d.ts

@@ -255,6 +255,36 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<Api.giftsReceiveModel>, 'sys.giftsReceive', Config>;
+
+      opinionTypeList<
+      Config extends Alova2MethodConfig<apiResData<any>> & {}
+    >(
+      config: Config
+    ): Alova2Method<apiResData<any>, 'sys.opinionTypeList', Config>;
+
+      addOpinionFeedback<
+      Config extends Alova2MethodConfig<apiResData<any>> & {
+        data: {
+          opinionType: string;
+          feedbackDesc: string;
+          img: string;
+          contact?: string;
+        };
+      }
+    >(
+      config: Config
+    ): Alova2Method<apiResData<any>, 'sys.addOpinionFeedback', Config>;
+
+      myOpinionFeedback<
+      Config extends Alova2MethodConfig<apiResData<any>> & {
+        params: {
+          pageNum: number;
+          pageSize: number;
+        };
+      }
+    >(
+      config: Config
+    ): Alova2Method<apiResData<{ list: any[]; total: number }>, 'sys.myOpinionFeedback', Config>;
     }
     xsb: {
       orderCoupons<
@@ -274,6 +304,49 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<Api.AppMemberCouponVO[]>, 'xsb.newCouponPopup', Config>;
+      addCollect<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          data: {
+            channelId: number;
+            prodId: number | string;
+            shopId: number;
+          };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'xsb.addCollect', Config>;
+      cancelCollect<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          data: {
+            channelId: number;
+            prodId: number | string;
+            shopId: number;
+          };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'xsb.cancelCollect', Config>;
+      myCollect<
+        Config extends Alova2MethodConfig<apiResData<Api.xsbCollectItem[]>> & {
+          params: {
+            skuName?: string;
+            channelId: number;
+            pageNum: number;
+            pageSize: number;
+          };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<Api.xsbCollectItem[]>, 'xsb.myCollect', Config>;
+      batchCancelCollect<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          pathParams: {
+            ids: string;
+          };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'xsb.batchCancelCollect', Config>;
       popupConfig<
         Config extends Alova2MethodConfig<apiResData<any>> & {}
       >(
@@ -1214,6 +1287,16 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<Api.DataVirtualOrderListVo>, 'videoRight.orderList', Config>;
+
+      orderCancel<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          params: {
+            orderNumber: string
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'videoRight.orderCancel', Config>;
     }
     djk: {
       appletGoodsList<

+ 3 - 3
src/config/index.ts

@@ -4,17 +4,17 @@ const mapEnvVersion = {
    */
   // develop: 'http://192.168.1.166:8080', // 张
   // develop: 'http://192.168.1.101:8080',
-  // develop: 'http://192.168.0.157:8080',
+  develop: 'http://192.168.0.157:8080', // 明
   // develop: 'http://192.168.1.253:8080',
   // develop: 'http://192.168.0.19:8080', // 邓
   // develop: 'http://192.168.1.20:8080', // 黄
   // develop: 'http://192.168.0.11:8081', // 王
   // develop: 'http://192.168.1.21:8080', // 田
   // develop: 'http://74949mkfh190.vicp.fun', // 付
-  develop: 'http://47.109.84.152:8081', // 测试代理
+  // develop: 'http://47.109.84.152:8081', // 测试代理
   // develop: 'https://5ed0f7cc.r9.vip.cpolar.cn',
   // develop: 'https://25740642.r3.cpolar.top',
-  develop: 'https://smqjh.api.zswlgz.com',
+  // develop: 'https://smqjh.api.zswlgz.com',
   /**
    * 体验版
    */

+ 25 - 0
src/pages.json

@@ -138,6 +138,15 @@
             "navigationBarTitleText": "星闪豹商品详情"
           }
         },
+        {
+          "path": "goodsCollection/index",
+          "name": "xsb-goods-collect",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "商品收藏",
+            "navigationStyle": "default"
+          }
+        },
         {
           "path": "order/index",
           "name": "xsb-order",
@@ -217,6 +226,22 @@
             "navigationStyle": "custom"
           }
         },
+        {
+          "path": "feedback/index",
+          "name": "common-feedback",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "意见反馈"
+          }
+        },
+        {
+          "path": "feedbackList/index",
+          "name": "common-feedback-list",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "我的反馈"
+          }
+        },
         {
           "path": "integral/index",
           "name": "common-integral",

+ 1 - 1
src/pages/index/index.vue

@@ -60,7 +60,7 @@ const navList = computed(() => {
     { icon: `${StaticUrl}/xsb.png`, title: '星闪豹', name: 'xsb-homeTabbar', show: true },
     { icon: `${StaticUrl}/smqjh-index-cd.png`, title: '充电', name: 'charge-index', show: true },
     { icon: `${StaticUrl}/smqjh-sp.png`, title: '电影演出', name: 'film-index', show: true },
-    { icon: `${StaticUrl}/smqjh-vip.png`, title: '视频权益', name: 'video-rights-tabbar', show: !isOnlineAudit.value },
+    { icon: `${StaticUrl}/smqjh-vip.png`, title: '视频权益', name: 'video-rights-tabbar', show: true },
     { icon: `${StaticUrl}/smqjh-djk.png`, title: '大健康', name: 'djk-homeTabbar', show: true },
     { icon: `${StaticUrl}/smqjh-jiayou.png`, title: '加油', name: 'refueling-tabbar', show: true },
     { icon: `${StaticUrl}/smqjh-attractions.png`, title: '景区', name: 'attractions-tabbar', show: true },

+ 1 - 1
src/pages/my/index.vue

@@ -156,7 +156,7 @@ function handleGo(item: { name: string, status: string }) {
               <image :src="`${StaticUrl}/7.png`" class="h-50rpx w-50rpx" />
             </template>
           </wd-cell>
-          <wd-cell title="评价" custom-title-class="cell-title" clickable is-link>
+          <wd-cell title="意见反馈" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'common-feedback' })">
             <template #icon>
               <image :src="`${StaticUrl}/8.png`" class="h-50rpx w-50rpx" />
             </template>

+ 132 - 0
src/subPack-common/feedback/index.vue

@@ -0,0 +1,132 @@
+<script setup lang="ts">
+import router from '@/router'
+
+const { themeVars } = storeToRefs(useManualThemeStore())
+definePage({
+  name: 'common-feedback',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '意见反馈',
+  },
+})
+
+const feedbackTypes = ref()
+const activeType = ref(0)
+const content = ref('')
+const contact = ref('')
+const fileList = ref<{ url: string }[]>([])
+
+onMounted(() => {
+  getOpinionTypeList()
+})
+
+async function getOpinionTypeList() {
+  const res = await Apis.sys.opinionTypeList({})
+  feedbackTypes.value = res.data
+}
+function selectType(index: any) {
+  activeType.value = index
+}
+
+const submitLoading = ref(false)
+async function handleSubmit() {
+  if (!content.value.trim()) {
+    useGlobalToast().show({ msg: '请输入反馈内容' })
+    return
+  }
+  submitLoading.value = true
+  const imgUrls = fileList.value.map(item => item.url).join(',')
+  await Apis.sys.addOpinionFeedback({
+    data: {
+      opinionType: feedbackTypes.value[activeType.value]?.name || '',
+      feedbackDesc: content.value,
+      img: imgUrls,
+      contact: contact.value,
+    },
+  })
+  useGlobalToast().show({ msg: '提交成功' })
+  submitLoading.value = false
+  activeType.value = 0
+  content.value = ''
+  contact.value = ''
+  fileList.value = []
+  setTimeout(() => {
+    router.back()
+  }, 500)
+}
+</script>
+
+<template>
+  <view class="page-wraper">
+    <view class="px-24rpx">
+      <view class="pt-24rpx">
+        <view class="text-32rpx font-bold">
+          意见反馈
+        </view>
+        <view class="mt-20rpx flex flex-wrap gap-20rpx">
+          <view
+            v-for="(item, index) in feedbackTypes"
+            :key="index"
+            class="rounded-16rpx px-28px py-14rpx text-28rpx"
+            :class="activeType === index ? 'text-#FFF font-bold' : 'bg-#E6E6E6 text-#333'"
+            :style="activeType === index ? { backgroundColor: themeVars.colorTheme } : {}"
+            @click="selectType(index)"
+          >
+            {{ item.name }}
+          </view>
+        </view>
+      </view>
+
+      <view class="mt-32rpx">
+        <view class="text-28rpx font-bold">
+          反馈内容
+        </view>
+        <view class="mt-20rpx rounded-32rpx bg-#FFF p-24rpx">
+          <textarea
+            v-model="content"
+            placeholder="请详细描述您遇到的问题"
+            :maxlength="500"
+            class="h-240rpx w-full text-26rpx text-#333"
+            placeholder-class="text-#999 text-26rpx"
+          />
+          <view class="text-right text-24rpx text-#999">
+            {{ content.length }}/500
+          </view>
+        </view>
+      </view>
+
+      <view class="mt-20rpx rounded-32rpx bg-#FFF p-24rpx">
+        <view class="text-28rpx text-#AAA">
+          您可以上传相关图片,至多4张
+        </view>
+        <view class="upload mt18rpx">
+          <Zupload v-model:value="fileList" />
+        </view>
+      </view>
+
+      <view class="mt-32rpx">
+        <view class="text-28rpx font-bold">
+          联系方式
+        </view>
+        <view class="mt-20rpx rounded-32rpx bg-#FFF p-24rpx">
+          <input
+            v-model="contact"
+            placeholder="请输入您的手机号或微信号"
+            class="w-full text-26rpx text-#333"
+            placeholder-class="text-#999 text-26rpx"
+          >
+        </view>
+      </view>
+      <view class="mt-20rpx text-center text-28rpx decoration-underline" @click="router.push({ name: 'common-feedback-list' })">
+        我的反馈
+      </view>
+      <view class="mt-60rpx">
+        <wd-button :loading="submitLoading" size="large" block @click="handleSubmit">
+          提交
+        </wd-button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 61 - 0
src/subPack-common/feedbackList/index.vue

@@ -0,0 +1,61 @@
+<script setup lang="ts">
+const { themeVars } = storeToRefs(useManualThemeStore())
+definePage({
+  name: 'common-feedback-list',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '我的反馈',
+  },
+})
+
+const { data: feedbackList, isLastPage, page } = usePagination(
+  (pageNum, pageSize) => Apis.sys.myOpinionFeedback({ params: { pageNum, pageSize } }),
+  { data: resp => resp.data?.list, initialPage: 1, initialPageSize: 10, immediate: true, append: true },
+)
+
+onReachBottom(() => {
+  if (!isLastPage.value) {
+    page.value++
+  }
+})
+</script>
+
+<template>
+  <view class="page-wraper px-24rpx">
+    <view v-for="(item, index) in feedbackList" :key="item.id">
+      <view v-if="index === 0" class="h-20rpx" />
+      <view class="mb-20rpx rounded-32rpx bg-#FFF p-24rpx">
+        <view class="relative mt-42rpx flex items-center justify-between">
+          <view class="absolute left-[-24rpx] px-24rpx py-14rpx text-#FFF" :style="{ backgroundColor: themeVars.colorTheme, borderTopRightRadius: '16rpx', borderBottomRightRadius: '16rpx' }">
+            {{ item.opinionType }}
+          </view>
+          <view class="absolute right-0 text-28rpx text-#AAA">
+            {{ item.createTime }}
+          </view>
+        </view>
+        <view class="mt-54rpx text-28rpx text-#AAA">
+          {{ item.feedbackDesc }}
+        </view>
+        <view v-if="item.img" class="mt-50rpx flex items-center gap-20rpx">
+          <image
+            v-for="(url, imgIndex) in item.img.split(',')"
+            :key="imgIndex"
+            class="h-120rpx w-120rpx rounded-32rpx"
+            :src="url"
+            mode="scaleToFill"
+          />
+        </view>
+        <view v-if="item.replyContent" class="mt-20rpx rounded-16rpx bg-#F0F0F0 p-20rpx text-24rpx">
+          <text>回复:{{ item.replyContent }}</text>
+          <text class="py-60rpx" />
+          <text class="ml-120rpx text-#AAA">
+            {{ item.replyTime }}
+          </text>
+        </view>
+      </view>
+    </view>
+    <StatusTip v-if="!feedbackList.length" tip="暂无反馈" />
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 1 - 2
src/subPack-common/integral/index.vue

@@ -35,7 +35,6 @@ getNavList()
 
 const PointsExpiryDate = ref()
 async function openPointsPopup() {
-  return
   showPointsPopup.value = true
   const res = await Apis.xsb.getUserPointsExpiryDate({})
   PointsExpiryDate.value = res.data || ''
@@ -88,7 +87,7 @@ async function openPointsPopup() {
         <view class="flex items-center justify-between text-32rpx font-semibold">
           <view class="text-[#222]">
             <!-- {{ `${item.businessScene}-${item.pointsActionType}` || '未知状态' }} -->
-            {{ item.businessTypeName || '未知状态' }}
+            {{ `${item.businessScene}-${item.pointsActionType}` || '未知状态' }}
           </view>
           <view class="text-[#FF4A39]">
             {{ item?.pointsTypeName }}{{ item?.variablePoints || 0 }}

+ 2 - 0
src/subPack-videoRights/components/StatusDisplay.vue

@@ -8,9 +8,11 @@ const props = defineProps<Props>()
 
 const statusConfig = computed(() => {
   const statusMap = {
+    0: { text: '待支付', color: 'text-#FF4D3A', icon: null },
     1: { text: '处理中', color: 'text-#ff9300', icon: null },
     2: { text: '充值成功', color: 'text-#52C41A', icon: 'videoRight-right.png' },
     3: { text: '充值失败', color: 'text-#FF4D3A', icon: 'videoRight-err.png' },
+    4: { text: '已取消', color: 'text-#AAA', icon: null },
   }
 
   return statusMap[props.status as keyof typeof statusMap] || { text: '', color: '', icon: null }

+ 22 - 5
src/subPack-videoRights/videoRightsOrderInfo/videoRightsOrderInfo.vue

@@ -37,7 +37,7 @@ async function submitOrderPay() {
       // #ifdef MP-WEIXIN
       const res = await useUserStore().handleCommonPayMent(orderNo.value)
       await useUserStore().getWxCommonPayment(res)
-      router.replace({ name: 'attractions-order-detail', params: { orderNo: String(videoRightOrderDetail.value?.orderNumber), ispay: 'true' } })
+      await getOrderDetail()
       // #endif
       // #ifdef H5
       useUserStore().handleCommonWechatPay(orderNo.value)
@@ -45,12 +45,10 @@ async function submitOrderPay() {
       // #endif
     }
     catch {
-      await useUserStore().payError('attractions-tabbar', 'subPack-attractions/commonTab/index')
     }
   }
   else {
     await useUserStore().handleCommonPayMent(orderNo.value)
-    router.replace({ name: 'attractions-order-detail', params: { orderNo: String(videoRightOrderDetail.value?.orderNumber), ispay: 'true' } })
   }
 }
 
@@ -64,6 +62,17 @@ async function handleH5PayResult(orderNumber: string) {
   useGlobalToast().show({ msg: '暂未查询到支付成功,请稍后在订单列表查看' })
 }
 
+async function cancelOrder() {
+  useGlobalMessage().confirm({
+    title: '提示',
+    msg: '确定取消订单吗?',
+    success: async () => {
+      await Apis.videoRight.orderCancel({ params: { orderNumber: orderNo.value } })
+      await getOrderDetail()
+    },
+  })
+}
+
 function orderBack() {
   const vrIndex = 'subPack-videoRights/commonTab/index'
   const pages = getCurrentPages()
@@ -155,12 +164,20 @@ function orderBack() {
             {{ videoRightOrderDetail?.payType }}
           </view>
         </view>
+        <view class="mt-28rpx flex items-center justify-between">
+          <view class="text-28rpx text-#AAA">
+            支付状态
+          </view>
+          <view class="text-28rpx font-bold">
+            {{ videoRightOrderDetail?.rechargeStatusDesc }}
+          </view>
+        </view>
       </view>
-      <view class="mt-20rpx flex items-center justify-between">
+      <view v-if="videoRightOrderDetail?.rechargeStatus === 0" class="mt-20rpx flex items-center justify-between">
         <wd-button custom-class="w-400rpx mt-10rpx" block size="medium" @click="submitOrderPay">
           立即支付
         </wd-button>
-        <wd-button custom-class="w-200rpx mt-10rpx" size="medium" type="info" plain block @click="orderBack">
+        <wd-button custom-class="w-200rpx mt-10rpx" size="medium" type="info" plain block @click="cancelOrder">
           取消订单
         </wd-button>
       </view>

+ 7 - 2
src/subPack-videoRights/videoRightsSubmitOrder/videoRightsSubmitOrder.vue

@@ -103,11 +103,13 @@ function submitPay() {
         }
         catch {
           await useUserStore().payError('video-rights-order-info', 'subPack-videoRights/commonTab/index')
+          console.log('进入取消支付1')
         }
       }
       else {
         await useUserStore().handleCommonPayMent(orderNumber)
         await useUserStore().paySuccess('video-rights-order-info', 'subPack-videoRights/commonTab/index')
+        console.log('进入取消支付2')
       }
     },
   })
@@ -213,8 +215,11 @@ function submitPay() {
               </text>
             </view>
             <view class="text-24rpx">
-              <text>积分抵扣</text> <text class="text-#FF4D3A">
-                -{{ previewGoods?.availablePointsMoney.toFixed(2) }}
+              <text>积分抵扣</text> <text v-if="previewGoods?.availablePointsMoney <= previewGoods?.price" class="text-#FF4D3A">
+                -{{ previewGoods?.availablePointsMoney }}
+              </text>
+              <text v-if="previewGoods?.availablePointsMoney > previewGoods?.price" class="text-#FF4D3A">
+                -{{ previewGoods?.price }}
               </text>
             </view>
           </view>

+ 6 - 1
src/subPack-xsb/commonTab/components/my.vue

@@ -21,7 +21,7 @@ function handleGo(item: { name: string, status: string }) {
 </script>
 
 <template>
-  <view class="page-xsb page-class bg-#F9F9F9 dark:bg-[var(--wot-dark-background)]">
+  <view class="page-class page-xsb bg-#F9F9F9 dark:bg-[var(--wot-dark-background)]">
     <wd-navbar
       title="个人中心" custom-style="background-color: transparent !important;" :bordered="false"
       safe-area-inset-top fixed :z-index="99"
@@ -90,6 +90,11 @@ function handleGo(item: { name: string, status: string }) {
     <view class="item-cell mt20rpx">
       <wd-card custom-class="card">
         <wd-cell-group custom-class="cell-group">
+          <wd-cell title="商品收藏" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'xsb-goods-collect' })">
+            <template #icon>
+              <wd-icon name="heart" color="#DDDDDD" size="25px" />
+            </template>
+          </wd-cell>
           <wd-cell title="收货地址" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'common-addressList' })">
             <template #icon>
               <image :src="`${StaticUrl}/4.png`" class="h50rpx w50rpx" />

+ 33 - 6
src/subPack-xsb/goods/index.vue

@@ -3,6 +3,7 @@ import selectSku from '../components/select-sku/index.vue'
 import { StaticUrl } from '@/config'
 import router from '@/router'
 
+const { themeVars } = storeToRefs(useManualThemeStore())
 const { statusBarHeight, MenuButtonHeight } = useSysStore()
 definePage({
   name: 'xsb-goods',
@@ -97,7 +98,7 @@ function handleGoCurren(id: number) {
 }
 
 async function getGoodsDetaile(shopId: number, channelId: number) {
-  const res = await Apis.xsb.getProductDetail({ data: { id: goodsId.value, shopId: Number(SelectShopInfo.value?.shopId) || shopId || 2, channelId: userInfo.value.channelId || channelId || 1 } })
+  const res = await Apis.xsb.getProductDetail({ data: { memberId: unref(userInfo)?.id, id: goodsId.value, shopId: Number(SelectShopInfo.value?.shopId) || shopId || 2, channelId: userInfo.value.channelId || channelId || 1 } })
   console.log(res, '请求')
   if (!res.data) {
     useGlobalToast().show('暂无该商品查看权限!')
@@ -106,6 +107,30 @@ async function getGoodsDetaile(shopId: number, channelId: number) {
   goodsInfo.value = res.data
   specId.value = res.data.skuList?.[0].skuId
 }
+async function handleCollect() {
+  await useUserStore().checkLogin()
+  if (goodsInfo.value!.isCollect === 1) {
+    await Apis.xsb.cancelCollect({
+      data: {
+        channelId: userInfo.value.channelId,
+        prodId: goodsInfo.value!.id || '',
+        shopId: goodsInfo.value!.shopId || SelectShopInfo.value?.shopId || 2,
+      },
+    })
+    goodsInfo.value!.isCollect = 0
+    useGlobalToast().show({ msg: '已取消收藏' })
+    return
+  }
+  await Apis.xsb.addCollect({
+    data: {
+      channelId: userInfo.value.channelId,
+      prodId: goodsInfo.value!.id || '',
+      shopId: goodsInfo.value!.shopId || SelectShopInfo.value?.shopId || 2,
+    },
+  })
+  goodsInfo.value!.isCollect = 1
+  useGlobalToast().show({ msg: '收藏成功' })
+}
 async function handleConfimOrder() {
   if (Number(unref(goodsInfo)?.spuStock) < unref(SelectGoodsNum)) {
     useGlobalToast().show('库存不足,请调整购买数量')
@@ -351,13 +376,15 @@ function handleClick() {
       <view class="h180rpx" />
       <view class="ios shadow-fixed fixed bottom-0 left-0 w-full rounded-t-32rpx bg-white">
         <view class="flex items-center justify-between px24rpx py20rpx">
-          <view class="mr36rpx" @click="router.replace({ name: 'xsb-homeTabbar' })">
-            <image
+          <view class="mr36rpx" @click="handleCollect">
+            <!-- <image
               :src="`${StaticUrl}/goods-home.png`"
               class="h44rpx w44rpx"
-            />
+            /> -->
+            <!-- goodsInfo.isCollect 0: 未收藏 1: 已收藏 -->
+            <wd-icon :name="goodsInfo.isCollect === 1 ? 'heart-filled' : 'heart'" :color="goodsInfo.isCollect === 1 ? themeVars.colorTheme : ''" size="28px" />
             <view class="text-20rpx">
-              首页
+              收藏
             </view>
           </view>
           <view class="relative mr36rpx">
@@ -385,7 +412,7 @@ function handleClick() {
 
           <view class="flex items-center">
             <view class="w220rpx">
-              <wd-button hairline plain block @click="selectGoods = true">
+              <wd-button plain hairline block @click="selectGoods = true">
                 加入购物车
               </wd-button>
             </view>

+ 161 - 0
src/subPack-xsb/goodsCollection/index.vue

@@ -0,0 +1,161 @@
+<script setup lang="ts">
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+const { themeVars } = storeToRefs(useManualThemeStore())
+const { userInfo } = storeToRefs(useUserStore())
+
+definePage({
+  name: 'xsb-goods-collect',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '商品收藏',
+    navigationStyle: 'default',
+  },
+})
+
+const isManaging = ref(false)
+const selectedIds = ref<Set<number>>(new Set())
+const keyword = ref('')
+
+const { data: collectList, isLastPage, page, loading, reload } = usePagination(
+  (pageNum, pageSize) => Apis.xsb.myCollect({
+    params: {
+      channelId: userInfo.value.channelId,
+      pageNum,
+      pageSize,
+      ...(keyword.value ? { skuName: keyword.value } : {}),
+    },
+  }),
+  { data: resp => resp.data, initialPage: 1, initialPageSize: 10, immediate: true, append: true },
+)
+
+const isAllSelected = computed(() => {
+  return selectedIds.value.size === collectList.value.length && collectList.value.length > 0
+})
+
+function toggleManage() {
+  isManaging.value = !isManaging.value
+  if (!isManaging.value) {
+    selectedIds.value.clear()
+  }
+}
+
+function toggleSelect(id: number) {
+  const newSet = new Set(selectedIds.value)
+  if (newSet.has(id)) {
+    newSet.delete(id)
+  }
+  else {
+    newSet.add(id)
+  }
+  selectedIds.value = newSet
+}
+
+function toggleSelectAll() {
+  if (isAllSelected.value) {
+    selectedIds.value = new Set()
+  }
+  else {
+    selectedIds.value = new Set(collectList.value.map(item => item.id))
+  }
+}
+
+function handleSearch() {
+  useGlobalLoading().loading({ msg: '加载中...' })
+  reload()
+  useGlobalLoading().close()
+}
+
+function handleScrollBottom() {
+  if (!isLastPage.value) {
+    page.value++
+  }
+}
+
+function handleDelete() {
+  if (!selectedIds.value.size) {
+    useGlobalToast().show({ msg: '请选择要删除的商品' })
+    return
+  }
+  useGlobalMessage().confirm({
+    title: '提示',
+    msg: '确认删除吗?',
+    success: async () => {
+      const ids = [...selectedIds.value].join(',')
+      await Apis.xsb.batchCancelCollect({ pathParams: { ids } })
+      useGlobalToast().show({ msg: '删除成功' })
+      selectedIds.value.clear()
+      isManaging.value = false
+      reload()
+    },
+  })
+}
+</script>
+
+<template>
+  <view class="page-xsb page-class">
+    <view class="fixed top-0 z-99 h-80rpx flex items-center gap-20rpx bg-#FFFFFF px-24rpx py-10rpx">
+      <view class="h-40rpx w-626rpx flex rounded-40rpx bg-#F1F1F1 py-12rpx">
+        <input v-model="keyword" placeholder="请输入商品名称" class="ml-24rpx w-580rpx text-28rpx" placeholder-class="text-#AAA text-28rpx" @blur="handleSearch">
+        <view class="mr-24rpx" @click="handleSearch">
+          <wd-icon name="search" color="#AAA" size="18px" />
+        </view>
+      </view>
+      <view class="w-100rpx text-28rpx font-bold" @click="toggleManage">
+        {{ isManaging ? '取消' : '管理' }}
+      </view>
+    </view>
+    <view class="h-100rpx" />
+    <scroll-view scroll-y class="view" @scrolltolower="handleScrollBottom">
+      <view class="goods-list px-24rpx">
+        <view v-for="item in collectList" :key="item.id" class="mt-20rpx rounded-16rpx bg-#FFF p-24rpx" @click="router.push({ name: 'xsb-goods', params: { id: String(item.prodId) } })">
+          <view class="flex items-center gap-20rpx">
+            <image :src="`${StaticUrl}/order-icon.png`" class="h-36rpx w-36rpx" />
+            <view class="text-28rpx font-bold">
+              {{ item.shopName }}
+            </view>
+          </view>
+          <view class="mt-24rpx h-2rpx w-full bg-#F0F0F0" />
+          <view class="flex items-center gap-20rpx">
+            <view v-show="isManaging" @click.stop="toggleSelect(item.id)">
+              <wd-icon :name="selectedIds.has(item.id) ? 'check-circle-filled' : 'circle1'" :color="selectedIds.has(item.id) ? themeVars.colorTheme : ''" size="16px" />
+            </view>
+            <view class="flex items-center gap-20rpx">
+              <image :src="item.pic || `${StaticUrl}/order-icon.png`" class="h-200rpx w-200rpx rounded-16rpx" />
+              <view>
+                <view class="flex items-start gap-8rpx">
+                  <view class="funt-bold flex-1 break-words text-32rpx">
+                    {{ item.skuName }}
+                  </view>
+                </view>
+                <view class="mt-62rpx text-36rpx text-#FF4D3A font-800">
+                  ¥{{ item.price }}
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+        <StatusTip v-if="!collectList.length && !loading" tip="暂无收藏" />
+        <view class="h-180rpx" />
+      </view>
+    </scroll-view>
+    <view v-show="isManaging" class="fixed bottom-0 z-99 h-166rpx w-full flex items-center justify-between rounded-[16rpx,16rpx,0,0] bg-#FFF shadow-[0rpx_-6rpx_12rpx_2rpx_rgba(0,0,0,0.05)]">
+      <view class="ml-24rpx flex items-center gap-20rpx" @click="toggleSelectAll">
+        <wd-icon :name="isAllSelected ? 'check-circle-filled' : 'circle1'" :color="isAllSelected ? themeVars.colorTheme : ''" size="16px" />
+        <view>全选</view>
+      </view>
+      <view class="mr-24rpx">
+        <wd-button type="error" @click="handleDelete">
+          删除
+        </wd-button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+.view {
+  height: calc(100vh - var(--window-top) - 80rpx);
+}
+</style>

+ 3 - 0
src/uni-pages.d.ts

@@ -9,6 +9,7 @@ interface NavigateToOptions {
        "/subPack-xsb/confirmOrder/index" |
        "/subPack-xsb/coupon/index" |
        "/subPack-xsb/goods/index" |
+       "/subPack-xsb/goodsCollection/index" |
        "/subPack-xsb/order/index" |
        "/subPack-xsb/orderDetaile/index" |
        "/subPack-xsb/search/index" |
@@ -18,6 +19,8 @@ interface NavigateToOptions {
        "/subPack-common/afterSalesDetail/index" |
        "/subPack-common/afterSalesList/index" |
        "/subPack-common/editAddress/index" |
+       "/subPack-common/feedback/index" |
+       "/subPack-common/feedbackList/index" |
        "/subPack-common/integral/index" |
        "/subPack-common/nickName/index" |
        "/subPack-common/payError/index" |