Przeglądaj źródła

Merge branch 'dev' of https://git.citupro.com/zhengnaiwen_citu/menduner into dev

lifanagju_citu 5 miesięcy temu
rodzic
commit
88da10b3a5

+ 1 - 0
package-lock.json

@@ -23,6 +23,7 @@
         "js-cookie": "^3.0.5",
         "js-sha256": "^0.11.0",
         "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
         "nprogress": "^0.2.0",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "js-cookie": "^3.0.5",
     "js-sha256": "^0.11.0",
     "lodash": "^4.17.21",
+    "lodash-es": "^4.17.21",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",

+ 17 - 0
src/api/mall/index.js

@@ -12,4 +12,21 @@ export const getProductByIds = async (ids) => {
   return request.get({
     url: `/app-api/product/spu/list-by-ids?ids=${ids}`
   })
+}
+
+// 提交积分商品兑换
+export const redeemSubmit = async (data) => {
+  return request.post({
+    url: '/app-api/menduner/system/redeem/submit',
+    openEncryption: true,
+    data
+  })
+}
+
+// 获取积分兑换记录
+export const getRedeemPage = async (params) => {
+  return request.get({
+    url: '/app-api/menduner/system/redeem/page',
+    params
+  })
 }

+ 1 - 1
src/layout/index.vue

@@ -10,7 +10,7 @@
         <component :is="Component" :key="router.currentRoute.value.path" v-if="!router.currentRoute.value.meta?.keepAlive"/>
       </router-view>  -->
     </div>
-    <Footers v-if="footerWhiteList.indexOf(router.currentRoute.value.path) === -1" class="mt-10"></Footers>
+    <Footers v-if="footerWhiteList.indexOf(router.currentRoute.value.path) === -1" :class="{'mt-10': !router.currentRoute.value.path.includes('/mall')}"></Footers>
     <Slider v-if="whiteList.indexOf(router.currentRoute.value.path) === -1" class="slider"></Slider>
   </div>
 </template>

+ 1 - 1
src/version.js

@@ -1,2 +1,2 @@
 // 版本号
-export const vue_version = 'v24.12.16.1150'
+export const vue_version = 'v24.12.19.1226'

+ 19 - 17
src/views/mall/home/components/carousel.vue

@@ -1,26 +1,28 @@
 <template>
-  <div>
-    <v-carousel cycle hide-delimiter-background :show-arrows="false" style="width: 100%; max-width: 100%; height: auto;">
-        <v-carousel-item v-for="(item, i) in carouselList" :key="i">
-          <v-img :src="item.url" :lazy-src="item.url">
-            <template v-slot:placeholder>
-                <v-row align="center" class="fill-height ma-0" justify="center">
-                  <v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
-                </v-row>
-            </template>
-          </v-img>
-        </v-carousel-item>
-      </v-carousel>
-  </div>
+  <v-carousel cycle hide-delimiter-background :show-arrows="false" style="height: 100%; min-width: 1184px;">
+    <v-carousel-item v-for="(item, i) in carouselList" :key="i">
+      <!-- <v-img :src="item.imgUrl" :lazy-src="item.imgUrl" style="width: 100%; height: auto;">
+        <template v-slot:placeholder>
+            <v-row align="center" class="fill-height ma-0" justify="center">
+              <v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
+            </v-row>
+        </template>
+      </v-img> -->
+      <figure>
+        <img :src="item.imgUrl" :lazy-src="item.imgUrl" style="width: 100%;" />
+      </figure>
+    </v-carousel-item>
+  </v-carousel>
 </template>
 
 <script setup>
 defineOptions({ name: 'mall-home-carousel'})
+import { ref } from 'vue'
+const props = defineProps({ templateData: Object })
 
-const carouselList = [
-  { url: 'https://wx-static.ydjdev.com/resource-1730081781870-30114.jpg?imageView2/0/w/1920' },
-  { url: 'https://wx-static.ydjdev.com/resource-1730084780292-95821.jpg?imageView2/0/w/1920' },
-]
+const carouselList = ref([])
+const Carousel = props.templateData?.home?.components.find(item => item.id === 'Carousel')
+carouselList.value = Carousel.property.items
 </script>
 
 <style scoped lang="scss">

+ 11 - 11
src/views/mall/home/components/hotGoods.vue

@@ -2,16 +2,16 @@
   <div>
     <div class="d-flex justify-space-between color-666">
       <div class="color-primary" style="font-size: 25px;">热门商品</div>
-      <!-- <div>查看更多</div> -->
+      <div class="cursor-pointer">查看更多</div>
     </div>
     <div class="goods-box mt-5">
       <v-card v-for="val in goodList" :key="val.id" class="goods-box-item" hover elevation="2" @click="handleClickGood(val)">
         <v-img :src="val.picUrl" width="100%" height="68%" cover></v-img>
         <div class="pa-3">
-          <p class="ellipsis color-333 text-center">{{ val.name }}</p>
+          <p class="ellipsis color-333">{{ val.name }}</p>
           <p class="color-999 ellipsis font-size-14 mt-1">{{ val.introduction }}</p>
           <div class="mt-1">
-            <div class="goods-box-item-price float-left">¥{{ val.price }}</div>
+            <div class="goods-box-item-price float-left">¥{{ isArray(val.price) ? fen2yuan(val.price[0]) : fen2yuan(val.price) }}</div>
             <div class="float-right font-size-15 mt-1" style="color: #c4c4c4">{{ salesAndStock(val) }}</div>
           </div>
         </div>
@@ -23,19 +23,18 @@
 <script setup>
 defineOptions({ name: 'mall-home-hotGoods'})
 import { ref, computed } from 'vue'
-import { useMallStore } from '@/store/mall'
 import { getProductByIds } from '@/api/mall/index'
-import { formatSales } from '@/hooks/web/useGoods.js'
+import { formatSales, fen2yuan } from '@/hooks/web/useGoods'
+import { isArray } from 'lodash-es'
+import { useRouter } from 'vue-router'
 
-let template = ref(JSON.parse(localStorage.getItem('mallTemplate')) || {})
-useMallStore().$subscribe((mutation, state) => {
-  if (state.template && Object.keys(state.template).length) template.value = state?.template
-})
+const props = defineProps({ templateData: Object })
+const router = useRouter()
 
 // 根据id获取商品列表
 const goodList = ref([])
 const getGoodsList = async () => {
-  const productCard = template.value?.home?.components.find(item => item.id === 'ProductCard')
+  const productCard = props.templateData?.home?.components.find(item => item.id === 'ProductCard')
   const ids = productCard.property.spuIds
   if (!ids.length) return
   const data = await getProductByIds(ids)
@@ -52,7 +51,8 @@ const salesAndStock = computed(() => (data) => {
 
 // 商品详情
 const handleClickGood = (val) => {
-  console.log(val, 'click-val')
+  if (!val.id) return
+  router.push(`/wareDetails/${val.id}`)
 }
 </script>
 

+ 191 - 0
src/views/mall/home/components/pointExchange/index.vue

@@ -0,0 +1,191 @@
+<template>
+  <div>
+    <div class="d-flex justify-space-between color-666">
+      <div class="color-primary" style="font-size: 25px;">积分兑换</div>
+      <div>
+        <span class="cursor-pointer active" @click="router.push('/recruit/personal/personalCenter/wallet')">当前账户积分:{{ point }}</span>
+        <span class="septal-line"></span>
+        <span class="cursor-pointer active">兑换记录</span>
+      </div>
+    </div>
+    <div class="goods-box mt-5">
+      <v-card v-for="val in dataList" :key="val.id" class="goods-box-item" elevation="2" @click="handleShowDetail(val)">
+        <v-img :src="val.url" width="100%" height="68%"></v-img>
+        <div class="pa-3">
+          <p class="ellipsis color-333">{{ val.name }}</p>
+          <div class="mt-1">
+            <div class="font-size-15 color-666">
+              消耗积分
+              <span class="color-primary font-size-20">{{ val.point }}</span>
+            </div>
+          </div>
+        </div>
+      </v-card>
+    </div>
+  </div>
+
+  <CtDialog :visible="showDetail" titleClass="text-h6" :footer="point >= detailItem?.point" :widthType="3" title="详情说明" @submit="handleSubmit" @close="showDetail = false">
+    <div class="color-primary font-size-20">{{ detailItem.name }}</div>
+    <div class="tips">
+      <div>使用说明:积分一经兑换概不退回,敬请谅解。</div>
+    </div>
+    <div class="d-flex align-center my-5">
+      <div class="mr-1">消耗积分</div>
+      <div class="color-primary">{{ detailItem.point }}</div>
+    </div>
+    <div v-if="point >= detailItem.point">
+      <div class="color-666 mb-5 mt-10">{{ detailItem?.type  ? '收货人信息填写' : '联系电话填写' }}</div>
+      <CtForm ref="CtFormRef" :items="formItems"></CtForm>
+    </div>
+    <div v-if="point < detailItem.point" class="text-end color-error font-size-14 cursor-pointer text-decoration-underline" @click="router.push('/recruit/personal/personalCenter/memberBenefits/taskCenter')">
+      积分不足,快去赚取积分吧
+    </div>
+  </CtDialog>
+</template>
+
+<script setup>
+defineOptions({ name: 'mall-point-exchange'})
+import { ref } from 'vue'
+import { getToken } from '@/utils/auth'
+import { redeemSubmit } from '@/api/mall/index.js'
+import { useUserStore } from '@/store/user'
+import { getDict } from '@/hooks/web/useDictionaries'
+import { checkPersonBaseInfo } from '@/utils/check'
+import dialogExtend from '@/plugins/dialogExtend'
+import Snackbar from '@/plugins/snackbar'
+import { useRouter } from 'vue-router'
+
+const emit = defineEmits(['login'])
+defineProps({
+  point: {
+    type: Number,
+    default: 0
+  }
+})
+
+const router = useRouter()
+// 数据
+const dataList = ref([
+  { name: '房券-高端酒店房券', point: 12000, url: 'https://minio.menduner.com/dev/menduner/hotalRoomVoucher.png', type: 1 },
+  { name: '门墩儿酒店英语学习年卡', point: 8000, url: 'https://minio.citupro.com/dev/menduner/englishCourses.jpg', type: 0 },
+  { name: '红酒-经典年份葡萄酒', point: 5000, url: 'https://minio.menduner.com/dev/menduner/redWine.png', type: 1 },
+  { name: '瑞幸咖啡券-瑞幸咖啡精致享受券', point: 2000, url: 'https://minio.menduner.com/dev/menduner/coffee.png', type: 0 },
+  { name: '减压捏捏乐', point: 500, url: 'https://minio.menduner.com/dev/menduner/pinchMusic.png', type: 1 }
+])
+
+const CtFormRef = ref()
+const formItems = ref({
+  options: [
+    {
+      type: 'text',
+      key: 'contactName',
+      value: null,
+      default: localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')).name : '',
+      hide: false,
+      label: '收货人姓名 *',
+      rules: [v => !!v || '请输入收货人姓名'],
+    },
+    {
+      type: 'text',
+      key: 'contactPhone',
+      value: null,
+      default: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')).phone : '',
+      label: '收货人联系电话 *',
+      outlined: true,
+      rules: [v => !!v || '请填写收货人联系电话']
+    },
+    {
+      type: 'cascade',
+      key: 'address',
+      value: [],
+      default: [],
+      label: '收货地址',
+      itemText: 'name',
+      itemValue: 'id',
+      required: true,
+      clearable: false,
+      disabled: true,
+      emitPath: true,
+      items: []
+    },
+    {
+      type: 'textarea',
+      key: 'contactAddress',
+      value: '',
+      default: null,
+      hide: false,
+      label: '收货详细地址 *',
+      rules: [ v => !!v || '请填写收货详细地址' ]
+    }
+  ]
+})
+// 期望城市、其它感兴趣的城市
+getDict('areaTreeData', null, 'areaTreeData').then(({ data }) => {
+  data = data?.length && data || []
+  formItems.value.options.find(e => e.key === 'address').items = data
+})
+
+// 详情说明弹窗
+const showDetail = ref(false)
+const detailItem = ref({})
+const handleShowDetail = (item) =>{
+  if (!getToken()) {
+    Snackbar.warning('请先登录')
+    emit('login')
+    return
+  }
+  if (!checkPersonBaseInfo()) { // 强制填写个人信息
+    dialogExtend('necessaryInfoDialog').then(() => {
+      handleShowDetail()
+    })
+    return
+  }
+  detailItem.value = item
+  formItems.value.options.forEach(e => {
+    if (e.key !== 'contactPhone') e.hide = item.type ? false : true
+    e.value = e.default
+  })
+  showDetail.value = true
+}
+
+// 兑换提交
+const handleSubmit = async () =>{
+  const { valid } = await CtFormRef.value.formRef.validate()
+  if (!valid) return
+  const obj = {
+    ...detailItem.value
+  }
+  formItems.value.options.forEach(e => {
+    if (!e.hide) obj[e.key] = e.value
+  })
+  if (obj.type && (!obj.address || !obj.address.length)) return Snackbar.warning('请选择收货地址')
+  if (obj.type) obj.contactAddress = obj.address.join('') + obj.contactAddress
+  delete obj.address 
+  if (!obj.contactName) obj.contactName = localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')).name : '--'
+  await redeemSubmit(obj)
+  Snackbar.success('提交成功')
+  showDetail.value = false
+  detailItem.value = {}
+  await useUserStore().getUserAccountInfo()
+}
+
+</script>
+
+<style scoped lang="scss">
+.active:hover {
+  color: var(--v-primary-base) !important;
+}
+.goods-box {
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  &-item {
+    height: 380px;
+    width: calc((100% - 48px) / 5);
+    margin: 0 12px 12px 0;
+    &:nth-child(5n) {
+      margin-right: 0;
+    }
+  }
+}
+</style>

+ 96 - 22
src/views/mall/home/index.vue

@@ -2,61 +2,135 @@
   <div style="min-width: 1184px;" class="white-bgc">
     <!-- 搜索框 -->
     <div class="py-5 stickyBox">
-      <div class="search d-flex align-center">
-        <v-text-field
-          v-model="inputVal"
-          placeholder="请输入关键词"
-          color="primary"
-          variant="plain"
-          density="compact"
-          clearable
-          :hide-details="true"
-          class="ml-3 px-2"
-          style="height: 100%; line-height: 100%;"
-          @keyup.enter="handleSearch"
-        ></v-text-field>
-        <v-btn class="searchBtn" prepend-icon="mdi-shopping-search-outline" @click="handleSearch">搜索</v-btn>
+      <div class="default-width d-flex align-center justify-space-between">
+        <div class="header-link">
+          <span class="cursor-pointer" :class="{'active-route' : isActive('/mall')}" @click="router.push('/mall')">首页</span>
+          <span class="mx-8 cursor-pointer">积分兑换</span>
+          <span class="cursor-pointer">订单中心</span>
+          <span class="mx-8 cursor-pointer">
+            <v-icon size="20">mdi-cart-outline</v-icon>
+            购物车
+          </span>
+        </div>
+        <div class="search d-flex align-center">
+          <v-text-field
+            v-model="inputVal"
+            placeholder="请输入关键词"
+            color="primary"
+            variant="plain"
+            density="compact"
+            clearable
+            :hide-details="true"
+            class="ml-3 px-2"
+            style="height: 100%; line-height: 100%;"
+            @keyup.enter="handleSearch"
+          ></v-text-field>
+          <v-btn class="searchBtn" prepend-icon="mdi-shopping-search-outline" @click="handleSearch">搜索</v-btn>
+        </div>
       </div>
     </div>
 
     <!-- 轮播图 -->
-    <Carousel />
+    <Carousel :templateData="template" class="mb-10" />
 
-    <!-- 热门商品 -->
-    <HotGoods class="my-10 default-width" />
+    <div class="default-width">
+      <!-- 热门商品 -->
+      <HotGoods :templateData="template" class="mb-10" />
+
+      <!-- 积分兑换 -->
+      <PointExchange :point="accountData.point" @login="handleLogin" />
+    </div>
   </div>
+
+  <!-- 快速登录 -->
+  <loginPage v-if="showLogin" @loginSuccess="loginSuccess" @close="loginClose"></loginPage>
 </template>
 
 <script setup>
 defineOptions({ name: 'mall-home-index'})
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
 import Carousel from './components/carousel.vue'
 import HotGoods from './components/hotGoods.vue'
+import PointExchange from './components/pointExchange'
+import loginPage from '@/views/common/loginDialog.vue'
 import { useMallStore } from '@/store/mall'
+import { useUserStore } from '@/store/user'
+import { useRouter } from 'vue-router'
 
 // 获取装修模版
 useMallStore().getMallDiyTemplate()
 
+const router = useRouter()
+const isActive = computed(() => (path) => {
+  const currentPath = router.currentRoute.value.path
+  return currentPath.includes(path)
+})
+
+let template = ref(JSON.parse(localStorage.getItem('mallTemplate')) || {})
+useMallStore().$subscribe((mutation, state) => {
+  if (state.template && Object.keys(state.template).length) template.value = state?.template
+})
+
+const userStore = useUserStore()
+let accountData = ref(JSON.parse(localStorage.getItem('userAccount')) || {})
+
+userStore.$subscribe((mutation, state) => {
+  if (Object.keys(state.userAccount).length) accountData.value = state.userAccount
+})
+
+const showLogin = ref(false)
+const handleLogin = () => {
+  showLogin.value = true // 打开快速登录弹窗
+}
+
+// 快速登录
+const loginSuccess = () => {
+  showLogin.value = false
+  Snackbar.success('登录成功')
+}
+
+const loginClose = () => {
+  showLogin.value = false
+  Snackbar.warning('您已取消登录')
+}
+
+
 const inputVal = ref('')
 const handleSearch = () => {}
 </script>
 
 <style scoped lang="scss">
+.active-route {
+  position: relative;
+  color: var(--v-primary-base);
+  &::before {
+    content: '';
+    width: 100%;
+    height: 3px;
+    position: absolute;
+    bottom: -8px;
+    left: 0;
+    background-color: var(--v-primary-base);
+  }
+}
+.header-link span:hover {
+  color: var(--v-primary-base);
+}
 .stickyBox {
   position: sticky;
   top: 48px;
   z-index: 999;
   background-color: #fff;
+  // border-bottom: 1px solid #00897B;
 }
 .search {
   height: 50px;
-  width: 800px;
+  width: 350px;
   border: 2px solid var(--v-primary-base);
   border-radius: 5px;
-  margin: 0 auto;
   .searchBtn {
-    width: 180px;
-    height: 50px;
+    width: 100px;
+    height: 49px;
     line-height: 48px;
     text-align: center;
     font-size: 18px;

+ 3 - 3
src/views/recruit/personal/home/components/advertisement/dynamic/intercontinental.vue

@@ -175,9 +175,9 @@ const brandIntroduction = ref([
     img: 'https://minio.citupro.com/dev/menduner/brandIntruduction/13_brand.png'
   },
   {
-    title: 'Atwell Suites™',
-    desc: '我们的全套房酒店处处为宾客提供丰富选择,满足宾客的各种住宿需求。酒店拥有设计体贴的公共场所、别致倾心的酒吧以及格调高雅的套房,我们恭候宾客下榻,探索新的旅行目的地。',
-    img: 'https://minio.citupro.com/dev/menduner/brandIntruduction/14_brand.png'
+    title: '筑格酒店™',
+    desc: '筑格酒店是专为追求精致生活方式的旅客所打造, 致力于提供一种全新的旅途体验,让你在其中既能享受熟悉的家般惬意舒适,同时带给你预料之外的愉悦惊喜。',
+    img: 'https://minio.citupro.com/dev/menduner/brandIntruduction/p4_14%401x.jpg'
   },
   {
     title: 'Staybridge Suites®',