detail.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. <template>
  2. <div class="w-full min-h-[100%] bg-[#fff] pb-[20px] detail" v-loading="loading">
  3. <div class="main-container" v-if="Object.keys(detail).length">
  4. <div class="pt-[30px]">
  5. <div class="flex ">
  6. <div class="w-[530px] flex flex-col">
  7. <el-image class="w-[530px] h-[530px] rounded-[var(--rounded-big)]" :src="img(detail.goods.goods_image_thumb_big.length ? detail.goods.goods_image_thumb_big[imgActive] : '')" >
  8. <template #error>
  9. <img src="@/assets/images/goods_default.png" class="w-[530px] h-[530px] rounded-[var(--rounded-big)]">
  10. </template>
  11. </el-image>
  12. <div class="flex justify-between mt-[20px]">
  13. <div class="h-[90px] w-[20px] flex items-center justify-center" @click="imgListClick('left')">
  14. <el-icon color="#ccc" size="14">
  15. <ArrowLeftBold />
  16. </el-icon>
  17. </div>
  18. <div class="relative flex-1 overflow-hidden h-[90px]">
  19. <div class="absolute flex items-center h-[100%] transition-all ease-in-out duration-500" :style="{ 'left': imgListLeft + 'px' }">
  20. <template v-if="detail.goods.goods_image_thumb_small">
  21. <div v-for="(item, index) in detail.goods.goods_image_thumb_small" :key="index" @mouseover="imgListHover(index)">
  22. <el-image :class="['w-[86px] h-[86px] mx-[7px] box-border border-[2px] border-solid cursor-pointer align-middle rounded-[var(--rounded-small)]', imgActive == index ? 'border-[var(--el-color-primary)]' : 'border-transparent']" :src="img(item)" alt="" fit="contain">
  23. </el-image>
  24. </div>
  25. </template>
  26. </div>
  27. </div>
  28. <div class="h-[90px] w-[20px] flex items-center justify-center" @click="imgListClick('right')">
  29. <el-icon color="#ccc" size="14">
  30. <ArrowRightBold />
  31. </el-icon>
  32. </div>
  33. </div>
  34. </div>
  35. <div class="flex-1 ml-[40px] pt-[20px]">
  36. <div class="text-[18px] leading-[28px] text-[#333] truncate max-w-[570px] mb-[20px]">{{ detail.goods.goods_name }}</div>
  37. <div class="text-[14px] leading-[18px] text-[#999] truncate max-w-[570px] mb-[30px]" v-if="detail.goods.sub_title">{{ detail.goods.sub_title }}</div>
  38. <div class="w-[630px] bg-[#F3F3F3] rounded-[var(--rounded-mid)] overflow-hidden mb-[20px] ">
  39. <div v-if="priceType == 'discount_price'" class="discount-bg h-[52px] box-border px-[10px] py-[15px]">
  40. <div class="flex items-center">
  41. <div class="h-[16px]">
  42. <img class="h-[16px] w-auto align-middle" src="@/assets/images/addon/discount_price.png" alt="">
  43. </div>
  44. <div class="flex items-baseline text-[#fff] leading-[18px] ml-[10px]">
  45. <span class="text-[14px] mr-[6px] ">{{ t('endOfRange') }}</span>
  46. <el-countdown :value="discountTime" format="HH:mm:ss" :value-style="{ fontSize: '14px',color:'#fff',letterSpacing: '1px' }"/>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="px-[20px] py-[20px] border-box bg-img">
  51. <div class="flex items-center justify-between flex-1">
  52. <div class="flex items-baseline">
  53. <div class="inline-block">
  54. <span class="text-[14px] mr-[2px] text-[#EF000C] price-font">¥</span>
  55. <span class="text-[#EF000C] text-[24px] price-font">{{parseFloat(goodsPrice).toFixed(2) }}</span>
  56. </div>
  57. <img v-if="priceType == 'member_price'" class="h-[14px] ml-[6px] w-[24px]" src="@/assets/images/addon/VIP.png" />
  58. <div class="text-[12px] text-[#666] line-through price-font ml-[14px]" v-if="Number(detail.market_price)">¥{{ detail.market_price }}</div>
  59. </div>
  60. <div class="text-[12px] leading-[16px]">
  61. <span class="text-[#999] oppoSans-R">{{ t('totalSales') }} {{ detail.goods.sale_num }}{{ detail.goods.unit }}</span>
  62. </div>
  63. </div>
  64. <div class="flex items-center justify-between" v-if="couponList.length">
  65. <div class="flex flex-nowrap overflow-hidden">
  66. <template v-for="(item, index) in couponList" :key="index">
  67. <div class="square bg-primary text-[12px] whitespace-nowrap px-[10px] leading-[24px] text-[#fff] mr-[10px] mt-[20px] ">
  68. <span>{{ item.title }}</span>
  69. </div>
  70. </template>
  71. </div>
  72. <div class="text-[12px] cursor-pointer text-primary mt-[20px] flex-shrink-0 ml-[40px]" @click="couponListShow = true">领劵</div>
  73. </div>
  74. </div>
  75. </div>
  76. <div class="flex items-center mb-[20px]" v-if="detail.label_info">
  77. <span class="text-[14px] w-[80px] mr-[10px] text-[#999]">{{ t('goodsLabel') }}</span>
  78. <div class="flex flex-nowrap overflow-hidden max-w-[540px]">
  79. <template v-for="(item, index) in detail.label_info" :key="index">
  80. <div class="tag-item px-[10px] leading-[20px] whitespace-nowrap border-[1px] border-solid border-[var(--el-color-primary)] text-[var(--el-color-primary)] mr-[10px]">
  81. {{ item.label_name }}
  82. </div>
  83. </template>
  84. </div>
  85. </div>
  86. <div class="flex items-center mb-[20px]" v-if="detail.brand_info">
  87. <span class="text-[14px] w-[80px] mr-[10px] text-[#999]">{{ t('goodsBrand') }}</span>
  88. <div class="flex-1 text-[14px] text-[#666]">{{detail.brand_info.brand_name}}</div>
  89. </div>
  90. <div class="flex items-center mb-[20px]" v-if="detail.service && detail.service.length">
  91. <span class="text-[14px] w-[80px] mr-[10px] text-[#999]">{{t('service')}}</span>
  92. <div class="flex-1 text-[14px] text-[#666] truncate max-w-[540px]">
  93. <template v-for="(item,index) in detail.service">
  94. <span>{{item.service_name}}</span>
  95. <span v-if="index < detail.service.length-1">,</span>
  96. </template>
  97. </div>
  98. </div>
  99. <!-- 规格 -->
  100. <template v-for="(item, index) in goodsDetail.goodsSpec" :key="index">
  101. <div class="flex">
  102. <div class="h-[32px] leading-[32px] text-[14px] text-[#999] w-[80px] box-border flex-shrink-0 mr-[10px] truncate">{{ item.spec_name }}</div>
  103. <div class="flex items-center flex-wrap">
  104. <template v-for="(subItem, subIndex) in item.values" :key="subIndex">
  105. <div class="px-[20px] h-[36px] text-[12px] leading-[36px] box-border border-[1px] border-solid border-[#d3d3d3] mr-[14px] mb-[14px] relative cursor-pointer rounded-[var(--rounded-small)] hover:border-[var(--el-color-primary)] hover:text-[var(--el-color-primary)]"
  106. :class="{ '!border-[var(--el-color-primary)] text-[var(--el-color-primary)] ': subItem.selected }" @click="change(subItem, index)">
  107. <span>{{ subItem.name }}</span>
  108. </div>
  109. </template>
  110. </div>
  111. </div>
  112. </template>
  113. <div class="flex mb-[20px]">
  114. <div class="h-[32px] leading-[32px] text-[14px] text-[#999] w-[80px] box-border flex-shrink-0 mr-[10px]">{{ t('num') }}</div>
  115. <div class="flex items-center">
  116. <el-input-number v-model.number="buyNum" :max="maxBuy" :min="minBuy" :step="1" step-strictly size="large" @blur="inputNum(buyNum)"/>
  117. <div class="text-[14px] ml-[14px] text-[#5a5a5a]">
  118. <span>{{t('stock')}}{{ detail.stock }}{{ t('unit') }}</span>
  119. <span v-if="maxBuyShow > 0 && minBuyShow > 1">({{ minBuyShow }}{{ detail.goods.unit }}起售,限购{{ maxBuyShow }}{{ detail.goods.unit }})</span>
  120. <span v-else-if="maxBuyShow > 0">(限购{{ maxBuyShow }}{{ detail.goods.unit }})</span>
  121. <span v-else-if="minBuyShow > 1" >({{ minBuyShow }}{{ detail.goods.unit }}起售)</span>
  122. </div>
  123. </div>
  124. </div>
  125. <div class="flex items-center ">
  126. <div class="flex items-center" v-if="detail.goods.status == 1">
  127. <block v-if="maxBuy > 0 || maxBuy == -1">
  128. <!-- 立即购买 -->
  129. <el-button v-if="isShowSingleSku" plain @click="buyFn('buy_now')" class="w-[150px] !h-[50px] !border-primary !text-primary !text-[16px] !rounded-[100px] !ml-[0] mr-[10px] oppoSans-M">{{t('buyNow')}}</el-button>
  130. <!-- 加入购物车 -->
  131. <el-button v-if="goodsDetail.goods.goods_type == 'real' || (goodsDetail.goods.goods_type == 'virtual' && goodsDetail.goods.virtual_receive_type != 'verify')" type="primary" class="!h-[50px] !w-[150px] !text-[16px] !rounded-[100px] !m-0 oppoSans-M" @click="buyFn('join_cart')">
  132. <span class="text-[#fff]">{{t('addCart')}}</span>
  133. </el-button>
  134. <!-- 已售罄 -->
  135. <el-button v-if="!isShowSingleSku" class="w-[150px] !h-[50px] !border-none !bg-[#ccc] !ml-[10px] !text-[#fff] !text-[16px] !rounded-[100px] oppoSans-M">{{ t('soldOut') }}</el-button>
  136. </block>
  137. <el-button v-else-if="isShowSingleSku && maxBuy == 0" class="w-[150px] !h-[50px] !border-none !bg-[#ccc] !ml-[10px] !text-[#fff] !text-[16px] !rounded-[100px] oppoSans-M">超出限购数量</el-button>
  138. </div>
  139. <div v-else>
  140. <!-- 该商品已下架 -->
  141. <el-button class="w-[150px] !h-[50px] !border-none !bg-[#ccc] !m-0 !text-[16px] !rounded-[100px] oppoSans-M">
  142. <span class="text-[#fff]">{{ t('goodsDelist') }}</span>
  143. </el-button>
  144. </div>
  145. <div class="ml-[10px] border-[1px] border-solid border-[var(--el-color-primary)] rounded-[4px]">
  146. <el-popover placement="bottom" :width="170" trigger="hover">
  147. <template #reference>
  148. <div class="h-[46px] w-[46px] flex items-center justify-center cursor-pointer">
  149. <span class="iconfont icon-fenxiang !text-[24px] !text-[var(--el-color-primary)] "></span>
  150. </div>
  151. </template>
  152. <div class="text-center flex items-center justify-center">
  153. <span class="mx-[10px]">{{ t('codesShare') }}</span>
  154. </div>
  155. <div class="promote-img flex justify-center items-center bg-[#f8f8f8] w-[146px] h-[146px]">
  156. <el-image :src="wapImage" />
  157. </div>
  158. </el-popover>
  159. </div>
  160. <div class="mx-[20px] w-[1px] h-[16px] bg-[#dcdfe6]"></div>
  161. <div>
  162. <div class="cursor-pointer" v-if="!isCollect" @click="collectFn">
  163. <span class="iconfont icon-shoucang1 !text-[16px] text-[#333] mr-[6px]"></span>
  164. <span class="text-[14px] text-[#333] oppoSans-R">{{ t('collect') }}</span>
  165. </div>
  166. <div class="cursor-pointer" v-else @click="collectFn">
  167. <span class="iconfont icon-yishoucang !text-[16px] text-[var(--el-color-primary)] mr-[6px]"></span>
  168. <span class="text-[14px] text-[var(--el-color-primary)] oppoSans-R ">{{ t('collected') }}</span>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <div class="mt-[70px] flex">
  175. <div class="w-[210px] mr-[20px]">
  176. <div class="flex flex-col w-full p-[20px] bg-[#fff] border-[1px] border-[#eee] border-solid box-border rounded-[var(--rounded-mid)]">
  177. <el-image class="w-[60px] h-[60px] rounded-[50%] mx-auto" :src="img(detail.shop_info.front_end_logo ? detail.shop_info.front_end_logo : '')" fit="cover">
  178. <template #error>
  179. <img src="@/assets/images/shop_default.png" alt="">
  180. </template>
  181. </el-image>
  182. <div class="w-[170px] flex-center mt-[16px] mb-[20px]">
  183. <span class="w-[32px] h-[18px] leading-[18px] text-center text-[#fff] bg-[var(--el-color-primary)] rounded-[2px] px-[4px] mr-[6px] text-[12px] flex-shrink-0" v-if="detail.shop_info.is_self">{{ t('self') }}</span>
  184. <span class="text-[14px] truncate">{{ detail.shop_info.site_name }}</span>
  185. </div>
  186. <div>
  187. <div class="border-0 border-b-[1px] border-[#efefef] border-dashed">
  188. <div class="flex items-center text-[12px] mt-[8px] mb-[20px]">
  189. <span class="text-[#999] mr-[15px]">{{ t('shopType') }}</span>
  190. <span class="text-[#333]">{{ detail.shop_info.category_name }}</span>
  191. </div>
  192. <div class="flex items-center text-[12px] mb-[20px]">
  193. <span class="text-[#999] mr-[15px]">{{ t('phone') }}</span>
  194. <span class="text-[#333]">{{ detail.shop_info.phone }}</span>
  195. </div>
  196. </div>
  197. <div class="flex items-center justify-between mt-[20px]">
  198. <div class="w-[78px] h-[30px] leading-[28px] mr-[10px] text-[12px] border-solid border-[1px] border-[#ccc] text-center rounded-full cursor-pointer" @click="goShopDetail(detail.shop_info.site_id)">{{ t('shopAround') }}</div>
  199. <div class="w-[78px] h-[30px] leading-[28px] text-[12px] border-solid border-[1px] border-primary text-primary text-center rounded-full cursor-pointer"
  200. v-if="!isFollow" @click="collectShop(detail.shop_info.site_id, 1)">{{ t('followShop') }}</div>
  201. <div
  202. class="w-[78px] h-[30px] leading-[30px] bg-primary text-[#fff] text-[12px] text-center rounded-full cursor-pointer"
  203. v-else @click="collectShop(detail.shop_info.site_id, 0)">{{ t('followed') }}</div>
  204. </div>
  205. </div>
  206. </div>
  207. <div class="w-full box-border mt-[20px] pt-[20px] px-[20px] border-[1px] border-solid border-[#eee] rounded-[var(--rounded-mid)]">
  208. <div class="flex items-center justify-between mb-[10px]">
  209. <div class="w-[35px] h-[1px] bg-[#dcdfe6]"></div>
  210. <div class="text-[#5a5a5a]">{{ t('shopRecommend')}}</div>
  211. <div class="w-[35px] h-[1px] bg-[#dcdfe6]"></div>
  212. </div>
  213. <div>
  214. <div class="mb-[20px] cursor-pointer" v-for="(item,index) in recommendList" :key="index" @click="toDetail(item.goods_id)">
  215. <div class="w-[170px] h-[170px] rounded-[var(--rounded-med)] overflow-hidden">
  216. <el-image class="w-[170px] h-[170px] rounded-[var(--rounded-med)]" :src="img(item.goods_cover_thumb_mid ? item.goods_cover_thumb_mid : '')" fit="cover" />
  217. </div>
  218. <div>
  219. <div class="my-[15px] text-[14px] text-[#333] truncate">{{ item.goods_name }}</div>
  220. <div class="flex justify-between items-center">
  221. <div class="text-[var(--el-price)] price-font">
  222. <span class="text-[12px]">¥</span>
  223. <span class="text-[20px]">{{ item.goodsSku.price }}</span>
  224. </div>
  225. <div class="text-[#999] text-[12px]">{{ t('saled') }}{{ item.sale_num }}{{item.unit || t('unit')}}</div>
  226. </div>
  227. </div>
  228. </div>
  229. </div>
  230. </div>
  231. </div>
  232. <div class="w-[970px] mb-[62px] pb-5">
  233. <div class="flex items-center">
  234. <div class="cursor-pointer leading-[16px] px-[30px] text-[16px]" :class="{'text-primary':detailActiveName == 'detail'}" @click="hangeChange('detail')">{{ t('goodsDetail') }}</div>
  235. <div class="cursor-pointer leading-[16px] px-[30px] text-[16px]" :class="{'text-primary':detailActiveName == 'attribute'}" v-if="detail.goods && detail.goods.attr_format && Object.keys(detail.goods.attr_format).length" @click="hangeChange('attribute')">{{ t('goodsArguments') }}</div>
  236. <div class="cursor-pointer leading-[16px] px-[30px] text-[16px]" :class="{'text-primary':detailActiveName == 'evalate'}" @click="hangeChange('evalate')">{{ t('goodsEvaluate') }}</div>
  237. </div>
  238. <el-divider />
  239. <div v-if="detailActiveName == 'detail'">
  240. <div v-html="detail.goods.goods_desc" v-if="detail.goods.goods_desc"></div>
  241. </div>
  242. <div v-else-if="detailActiveName == 'attribute'">
  243. <el-row>
  244. <template v-for="(item,index) in detail.goods.attr_format" :key="index">
  245. <el-col :span="12">
  246. <div class="flex items-center mb-[12px] leading-[32px] text-[14px]">
  247. <div class="w-[150px] text-[#999] mr-[12px] text-right truncate">{{item.attr_value_name}}</div>
  248. <div class="input-width">{{ Array.isArray(item.attr_child_value_name) ? item.attr_child_value_name.join(',') : item.attr_child_value_name }}</div>
  249. </div>
  250. </el-col>
  251. </template>
  252. </el-row>
  253. </div>
  254. <div v-else-if="detailActiveName == 'evalate'">
  255. <div class="">
  256. <div class="flex items-center justify-between">
  257. <div class="flex">
  258. <span v-for="(item, index) in evaluateSort" :key="index" @click="handelClick(item)"
  259. class="px-[26px] text-[14px] text-[#282828] leading-[34px] mr-[10px] cursor-pointer bg-[#f7f7f7] rounded-[var(--rounded-xl)]" :class="{ '!bg-[var(--el-color-primary)] !text-[#fff]': item.status == evaluateTableData.searchParam.currentIndex }">{{item.name }}</span>
  260. </div>
  261. </div>
  262. <div v-loading="evaluateTableData.loading" class="min-h-[300px]">
  263. <div v-if="evaluateTableData.data.length" class="mt-[30px]">
  264. <div>
  265. <div v-for="(item, index) in evaluateTableData.data" :key="index" class="box-border pb-[20px]">
  266. <div class="flex items-center justify-between">
  267. <div class="flex items-center">
  268. <img src="@/assets/images/user.png" v-if="item.is_anonymous == 1 || item.member_head == ''" class="rounded-full mr-[20rpx] w-[40px] h-[40px]">
  269. <el-image :src="img(item.member_head ? item.member_head : '')" v-else fit="cover" lazy class="rounded-full mr-[20rpx] w-[40px] h-[40px]">
  270. <template #error>
  271. <img src="@/assets/images/user.png" class="rounded-full w-[40px] h-[40px]">
  272. </template>
  273. </el-image>
  274. <div class="ml-[10px]">
  275. <div class="text-[16px] text-[#333] leading-[16px] mb-[8px]">{{ item.member_name }}</div>
  276. <div class="text-[12px] text-[#999]">{{ item.create_time }}</div>
  277. </div>
  278. </div>
  279. <div>
  280. <el-rate v-model="item.scores" text-color="var(--el-color-primary)" :colors="['var(--el-color-primary)', 'var(--el-color-primary)', 'var(--el-color-primary)']" disabled />
  281. </div>
  282. </div>
  283. <div class="border-b-[1px] border-dashed border-[#e3e3e3] ml-[52px] text-[#333] text-[14px]">
  284. <div class="my-[20px]">{{ item.content }}</div>
  285. <div class="flex flex-wrap" v-if="item.images && item.images.length">
  286. <div v-for="(subItem, subIndex) in item.images" :key="subIndex" class="mr-[8px] mb-[10px]">
  287. <el-image style="width: 86px; height: 86px" class="rounded-[var(--rounded-med)]" :src="img(subItem)" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :initial-index="subIndex" :preview-src-list="item.imagesList" fit="cover" :hide-on-click-modal="true" />
  288. </div>
  289. </div>
  290. <div class="mt-[10px] mb-[30px] bg-[#f7f7f7] rounded-[var(--rounded-small)]" v-if="item.explain_first">
  291. <div class="text-[14px] text-[#333] py-[7px] px-[10px] break-all leading-[20px]">
  292. <span class="text-primary">{{ t('merchantReply') }}</span>
  293. <span>{{ item.explain_first}}</span>
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. </div>
  299. <div class="mt-[16px] flex justify-end">
  300. <el-pagination v-model:current-page="evaluateTableData.page" v-model:page-size="evaluateTableData.limit" :total="evaluateTableData.total" @current-change="getEvaluateListFn" />
  301. </div>
  302. </div>
  303. <div v-else>
  304. <el-empty :description="t('noEvaluate')" :image-size="200" :image="img('static/resource/images/system/empty.png')"/>
  305. </div>
  306. </div>
  307. </div>
  308. </div>
  309. </div>
  310. </div>
  311. </div>
  312. </div>
  313. <!-- 优惠券列表 -->
  314. <el-drawer v-model="couponListShow" size="400" direction="rtl" custom-class="!bg-[#F6F6F6]">
  315. <template #header>
  316. <div>{{t('coupon')}}</div>
  317. </template>
  318. <template #default>
  319. <div>
  320. <div class="mb-[15px] flex items-center bg-[#fff] rounded-[8px] " v-for="(item, index) in couponList" :key="index">
  321. <div class="flex flex-col items-center justify-center bg-primary w-[86px] h-[80px] rounded-[8px] relative coupon-item">
  322. <div class="text-[#fff] ">
  323. <span class="text-[14px] price-font">¥</span>
  324. <span class="text-[26px] price-font">{{ item.price }}</span>
  325. </div>
  326. <span class="text-[12px] mt-[3px] text-[#fff] truncate max-w-[86px] border-box px-[4px]">{{ Number(item.min_condition_money) ? ('满' + item.min_condition_money + '元可以使用') : '无门槛优惠券' }}</span>
  327. </div>
  328. <div class="ml-[10px] flex-1 flex flex-col">
  329. <span class="text-[18px] text-[#333] leading-[24px]">{{ item.title }}</span>
  330. <span class="text-[14px] text-[#999] mt-[8px] leading-[18px]">{{ item.valid_type == 1 &&('领取之日起' + item.length + '天内有效') || item.valid_type == 2 && ('领取之日起至' + item.valid_end_time) }}</span>
  331. </div>
  332. <span v-if="item.btnType === 'collecting'" class="bg-primary w-[54px] text-center rounded-2xl text-[#fff] text-[12px] mr-[10px] py-[4px] cursor-pointer" @click="getCouponFn(item, index)">领取</span>
  333. <span v-else class="!bg-[#E28B8F] w-[54px] text-center rounded-2xl text-[#fff] text-[12px] mr-[10px] py-[4px]">{{item.btnType === 'collected' ? '已领完' : '已领取' }}</span>
  334. </div>
  335. </div>
  336. </template>
  337. </el-drawer>
  338. </div>
  339. </template>
  340. <script lang="ts" setup>
  341. import { ref, reactive, computed, watch } from 'vue'
  342. import storage from '@/utils/storage'
  343. import { getGoodsDetail, getEvaluateList, collect, cancelCollect, getRecommendGoods } from '@/addon/mall/api/goods'
  344. import { addBrowse } from '@/addon/mall/api/member'
  345. import { getMallGoodsCoupon, getCoupon } from '@/addon/mall/api/coupon'
  346. import { editShopCollect } from '@/addon/mall/api/shop'
  347. import { useRoute, useRouter } from 'vue-router'
  348. import useCartStore from '@/addon/mall/stores/cart'
  349. import useMemberStore from '@/stores/member'
  350. import { img ,getToken} from '@/utils/common'
  351. import QRCode from 'qrcode'
  352. const route = useRoute()
  353. const router = useRouter()
  354. let id = route.query.id
  355. let type = route.query.type
  356. // 会员信息
  357. const memberStore = useMemberStore()
  358. const userInfo = computed(() => memberStore.info)
  359. // 购物车数量
  360. const cartStore = useCartStore();
  361. cartStore.getList()
  362. const cartList = computed(() => cartStore.cartList)
  363. const detail = ref<any>({})
  364. let imgActive = ref(0)
  365. let loading = ref(true);
  366. let imgListLeft = ref(0)
  367. let detailActiveName = ref('detail')
  368. let discountTime = ref(0) //限时折扣倒计时
  369. // 商品详情
  370. const getGoodsDetailFn = (data: any) => {
  371. getGoodsDetail(data).then((res: any) => {
  372. if (JSON.stringify(res.data) === '[]' || !res.data.goods) {
  373. ElMessage.error('找不到该商品')
  374. setTimeout(() => {
  375. router.push({path:'/'})
  376. }, 1000)
  377. return false
  378. }
  379. detail.value = res.data
  380. // 商品属性
  381. if(detail.value.goods.attr_format){
  382. detail.value.goods.attr_format = detail.value.goods.attr_format.filter((item: any, index: any) => {
  383. return Array.isArray(item.attr_child_value_name) ? item.attr_child_value_name.length : item.attr_child_value_name
  384. })
  385. }
  386. // 折扣信息
  387. if(Object.keys(detail.value.goods).length && detail.value.type == 'discount' && detail.value.goods.is_discount && Object.keys(detail.value.discount_info).length){
  388. let now = new Date();
  389. let timestamp = now.getTime();
  390. discountTime.value = Date.now() + (detail.value.discount_info.active.end_time * 1000 - timestamp)
  391. }
  392. // 商品限购
  393. goodsMaxBuy(detail.value);
  394. isFollow.value = detail.value.member_info.is_follow //关注店铺
  395. isCollect.value = detail.value.goods.is_collect // 收藏商品
  396. loading.value = false
  397. evaluateTableData.searchParam.goods_id = detail.value.goods_id
  398. page.value = detail.value.goods_id
  399. getMallCouponListFn() // 获取优惠劵
  400. getEvaluateListFn()//评价列表
  401. getRecommendFn() // 店铺推荐
  402. addBrowseFn() // 添加足迹
  403. loadQrcode()
  404. })
  405. }
  406. getGoodsDetailFn({ goods_id: id})
  407. // 商品限购
  408. const maxBuy = ref(-1);
  409. const minBuy = ref(0); // 起售
  410. const maxBuyShow = ref(0); // 限购
  411. const minBuyShow = ref(0); // 起售
  412. const goodsMaxBuy = (data:any = {})=>{
  413. // 限购 - 是否开启限购
  414. if(data.goods.is_limit){
  415. if(data.goods.max_buy){
  416. let max_buy = 0;
  417. if(data.goods.limit_type == 1){ //单次限购
  418. max_buy = data.goods.max_buy;
  419. }else{ // 单人限购
  420. let buyVal = data.goods.max_buy - (data.goods.has_buy||0);
  421. max_buy = buyVal > 0 ? buyVal : 0;
  422. }
  423. if(max_buy > data.goods.stock){
  424. maxBuy.value = data.goods.stock <= 0 ? data.goods.min_buy : data.goods.stock
  425. }else if(max_buy <= data.goods.stock){
  426. maxBuy.value = max_buy;
  427. }
  428. // 限购开启且最大购买变为零时,初始值也应该是零
  429. if(maxBuy.value == 0){
  430. buyNum.value = 0;
  431. }
  432. }
  433. // 仅用于展示
  434. maxBuyShow.value = data.goods.max_buy; // 限购
  435. }else{
  436. maxBuy.value = data.goods.stock <= 0 ? data.goods.min_buy : data.goods.stock;
  437. }
  438. // 起售
  439. minBuy.value = data.goods.min_buy > 0 ? data.goods.min_buy : 1;
  440. // 起售大于库存,初始值也应该是零
  441. if((minBuy.value > data.goods.stock) && (data.goods.stock > 0)){
  442. buyNum.value = 0;
  443. }else{
  444. buyNum.value = minBuy.value;
  445. }
  446. // 仅用于展示
  447. minBuyShow.value = data.goods.min_buy;
  448. }
  449. // 判断单规格库存是否为0
  450. const isShowSingleSku = computed(() => {
  451. let isSingleSpec = false // 是否为单规格,true:多规格,false:单规格
  452. detail.value.skuList.forEach((item: any, index: any) => {
  453. if (item.sku_spec_format) {
  454. isSingleSpec = true
  455. }
  456. })
  457. // 单规格,库存为0,显示已售罄
  458. if (!isSingleSpec && detail.value.stock <= 0) {
  459. return false;
  460. } else if (!isSingleSpec && detail.value.stock > 0) {
  461. // 单规格,库存大于0,可以购买
  462. return true;
  463. }
  464. return true;
  465. })
  466. // 数量改变
  467. const inputNum = (val: any) => {
  468. if (val == undefined || val == null) {
  469. buyNum.value = 1
  470. }
  471. }
  472. // 当前选中值
  473. let currSpec = ref({
  474. skuId: "",
  475. name: []
  476. })
  477. let buyNum = ref(1)
  478. const goodsDetail = computed(() => {
  479. let data = JSON.parse(JSON.stringify(detail.value))
  480. if (Object.keys(data).length) {
  481. if (!Object.keys(currSpec.value.name).length) currSpec.value.name = data.sku_spec_format.split(",")
  482. data.goodsSpec.forEach((item: any, index: number) => {
  483. let specName = item.spec_values.split(",");
  484. item.values = [];
  485. specName.forEach((specItem: any, specIndex: number) => {
  486. item.values[specIndex] = {};
  487. item.values[specIndex].name = specItem;
  488. item.values[specIndex].selected = false;
  489. item.values[specIndex].disabled = false;
  490. // 选中规格
  491. currSpec.value.name.forEach((currSpecItem, currSpecIndex) => {
  492. if (currSpecIndex == index && currSpecItem == specItem) {
  493. item.values[specIndex].selected = true;
  494. }
  495. })
  496. getSkuId()
  497. // 当前详情内容
  498. if (data.skuList && Object.keys(data.skuList).length) {
  499. data.skuList.forEach((idItem: any, idIndex: number) => {
  500. if (idItem.sku_id == currSpec.value.skuId) {
  501. data.detail = idItem;
  502. }
  503. })
  504. }
  505. })
  506. })
  507. }
  508. return data;
  509. })
  510. const getSkuId = () => {
  511. detail.value.skuList.forEach((skuItem: any, skuIndex: any) => {
  512. if (skuItem.sku_spec_format == currSpec.value.name.toString()) {
  513. currSpec.value.skuId = skuItem.sku_id
  514. detail.value.skuList.forEach((item: any, index: any) => {
  515. if (item.sku_id == skuItem.sku_id) {
  516. Object.assign(detail.value, item);
  517. }
  518. })
  519. }
  520. })
  521. }
  522. // 切换规格
  523. const change = (data: any, index:number) => {
  524. currSpec.value.name[index] = data.name;
  525. getSkuId();
  526. }
  527. //图片list hover事件
  528. const imgListHover = (index: number) => {
  529. imgActive.value = index
  530. }
  531. //图片list 上一页下一页点击事件
  532. const imgListClick = (val: any) => {
  533. let maxLeft = (-100 * detail.value.goods.goods_image_thumb_small.length) + 490 > 0 ? 0 : (-100 * detail.value.goods.goods_image_thumb_small.length) + 490
  534. if (val == 'right') {
  535. imgListLeft.value = imgListLeft.value - 490 < maxLeft ? maxLeft : imgListLeft.value - 490
  536. } else {
  537. imgListLeft.value = imgListLeft.value + 490 > 0 ? 0 : imgListLeft.value + 490
  538. }
  539. }
  540. // 去店铺详情
  541. const goShopDetail = (id: number) => {
  542. router.push({
  543. path: '/shop/index',
  544. query: {
  545. site_id: id
  546. }
  547. })
  548. }
  549. // 加入购物车
  550. const buyFn = (type: string) => {
  551. if(buyNum.value < 1) return;
  552. // 检测是否登录
  553. if (!userInfo.value) {
  554. memberStore.logOpen()
  555. return false
  556. }
  557. // 加入购物车
  558. if (type == 'join_cart') {
  559. let num = 0;
  560. let limitNum = 0;
  561. let cartId = ""
  562. if (cartList.value['goods_' + goodsDetail.value.goods_id] && cartList.value['goods_' + goodsDetail.value.goods_id]['sku_' + goodsDetail.value.sku_id]) {
  563. num = toRaw(cartList.value['goods_' + goodsDetail.value.goods_id]['sku_' + goodsDetail.value.sku_id].num);
  564. cartId = toRaw(cartList.value['goods_' + goodsDetail.value.goods_id]['sku_' + goodsDetail.value.sku_id].id)
  565. }
  566. if (cartList.value['goods_' + goodsDetail.value.goods_id] && cartList.value['goods_' + goodsDetail.value.goods_id]) {
  567. limitNum = toRaw(cartList.value['goods_' + goodsDetail.value.goods_id].totalNum);
  568. }
  569. num += buyNum.value;
  570. limitNum += Number(buyNum.value);
  571. /************** 限购-start **************/
  572. if(goodsDetail.value.goods.is_limit){
  573. let tips = `该商品单次限购${goodsDetail.value.goods.max_buy}件`;
  574. if(goodsDetail.value.goods.limit_type != 1){ //单次限购
  575. tips = `该商品每人限购${goodsDetail.value.goods.max_buy}件`;
  576. if(goodsDetail.value.goods.max_buy - maxBuy.value){
  577. tips += `,已购${goodsDetail.value.goods.max_buy - maxBuy.value}件`;
  578. }
  579. }
  580. if(limitNum > maxBuy.value){
  581. ElMessage.error(tips);
  582. return false;
  583. }
  584. }
  585. /************** 限购-end **************/
  586. cartStore.increase({
  587. id: cartId || '',
  588. goods_id: goodsDetail.value.goods_id,
  589. sku_id: goodsDetail.value.sku_id,
  590. stock: goodsDetail.value.stock,
  591. sale_price: goodsDetail.value.sale_price,
  592. site_id: goodsDetail.value.site_id,
  593. num: num
  594. }, 0, () => {
  595. ElMessage({
  596. message: '加入购物车成功',
  597. type: 'success',
  598. })
  599. })
  600. } else if (type == 'buy_now') {
  601. var data = {
  602. sku_id: goodsDetail.value.sku_id,
  603. num: buyNum.value
  604. }
  605. storage.set({ key: 'orderCreateData', data: { body: { [goodsDetail.value.shop_info.site_id]: { sku_data: [data] } } } })
  606. router.push('/order/payment')
  607. }
  608. }
  609. // 收藏店铺
  610. let isFollow = ref(0);
  611. const collectShop = (id: any, is_follow: any) => {
  612. // 检测是否登录
  613. if (!userInfo.value) {
  614. memberStore.logOpen()
  615. return false
  616. }
  617. editShopCollect({
  618. site_id: id,
  619. is_follow: is_follow
  620. }).then(res => {
  621. isFollow.value = !isFollow.value
  622. if (isFollow.value) {
  623. ElMessage({
  624. message: '收藏成功',
  625. type: 'success',
  626. })
  627. } else {
  628. ElMessage({
  629. message: '取消收藏',
  630. type: 'success',
  631. })
  632. }
  633. })
  634. }
  635. // 收藏商品
  636. let isCollect = ref(0)
  637. const collectFn = () => {
  638. // 检测是否登录
  639. if (!userInfo.value) {
  640. memberStore.logOpen()
  641. }
  642. let api = isCollect.value ? cancelCollect({ goods_ids: [detail.value.goods_id] }) : collect(detail.value.goods_id);
  643. api.then(res => {
  644. isCollect.value = !isCollect.value;
  645. if (isCollect.value) {
  646. ElMessage({
  647. message: '收藏成功',
  648. type: 'success',
  649. })
  650. } else {
  651. ElMessage({
  652. message: '取消收藏成功',
  653. type: 'success',
  654. })
  655. }
  656. })
  657. }
  658. // 价格类型
  659. let priceType = ref('') //''=>原价,discount_price=>折扣价,member_price=>会员价
  660. // 商品价格
  661. let goodsPrice = computed(() =>{
  662. let price = "0.00";
  663. if(Object.keys(detail.value).length && Object.keys(detail.value.goods).length && detail.value.goods.is_discount && goodsDetail.value.sale_price != goodsDetail.value.price){
  664. // 折扣价
  665. price = detail.value.sale_price ? detail.value.sale_price : detail.value.price;
  666. priceType.value = 'discount_price'
  667. }else if(Object.keys(detail.value).length && Object.keys(detail.value.goods).length && detail.value.goods.member_discount && getToken() && goodsDetail.value.member_price != goodsDetail.value.price){
  668. // 会员价
  669. price = detail.value.member_price ? detail.value.member_price : detail.value.price;
  670. priceType.value = 'member_price'
  671. }else{
  672. price = detail.value.price
  673. priceType.value = ''
  674. }
  675. return price;
  676. })
  677. // 领取优惠劵
  678. let couponListShow = ref<boolean>(false) //优惠券
  679. let couponList = ref([]);
  680. const getMallCouponListFn = () => {
  681. getMallGoodsCoupon({
  682. site_id: detail.value.shop_info.site_id,
  683. category_id: detail.value.goods.goods_category || '',
  684. mall_category_id: goodsDetail.value.goods.goods_mall_category || '',
  685. goods_id: detail.value.goods_id || '',
  686. brand_id: goodsDetail.value.goods.brand_id || ''
  687. }).then(res => {
  688. couponList.value = res.data.data.map((el: any) => {
  689. if (!userInfo.value) {
  690. if (el.sum_count != -1 && el.receive_count === el.sum_count) {
  691. el.btnType = 'collected'//已领完
  692. } else {
  693. el.btnType = 'collecting'//领用
  694. }
  695. } else {
  696. if (el.is_receive) {
  697. if (el.member_receive_count < el.limit_count) {
  698. if (el.need_receive) {
  699. el.btnType = 'collecting'//领用
  700. } else {
  701. el.btnType = 'using'//去使用
  702. }
  703. } else {
  704. if (el.need_receive) {
  705. el.btnType = 'used'//已使用
  706. } else {
  707. el.btnType = 'using'//去使用
  708. }
  709. }
  710. } else {
  711. if (el.sum_count != -1 && el.receive_count === el.sum_count) {
  712. el.btnType = 'collected'//已领完
  713. } else {
  714. el.btnType = 'collecting'//领用
  715. }
  716. }
  717. }
  718. return el
  719. });
  720. })
  721. }
  722. // 领取优惠券
  723. const getCouponFn = (data:any) => {
  724. // 检测是否登录
  725. if (!userInfo.value) {
  726. memberStore.logOpen()
  727. couponListShow.value = false
  728. return false
  729. }
  730. getCoupon({
  731. coupon_id: data.id || '',
  732. number: 1,
  733. }).then(res => {
  734. getMallCouponListFn();
  735. couponListShow.value = false
  736. })
  737. }
  738. let page = ref('')
  739. const wapImage = ref('')
  740. const wapPreview = ref('')
  741. const loadQrcode = () => {
  742. wapPreview.value = `${location.origin}/wap/addon/mall/pages/goods/detail?goods_id=${page.value}`
  743. // errorCorrectionLevel:密度容错率L(低)H(高)
  744. QRCode.toDataURL(wapPreview.value, { errorCorrectionLevel: 'L', margin: 0, width: 120 }).then(url => {
  745. wapImage.value = url
  746. })
  747. }
  748. const hangeChange = (data:any)=>{
  749. detailActiveName.value = data
  750. }
  751. // 评论 start
  752. let evaluateSort = ref<Array<any>>([{
  753. name: '全部',
  754. status: '',
  755. scores: []
  756. },
  757. {
  758. name: '好评',
  759. status: 'good_evaluate',
  760. scores: [4, 5]
  761. },
  762. {
  763. name: '中评',
  764. status: 'center_evaluate',
  765. scores: [2, 3]
  766. },
  767. {
  768. name: '差评',
  769. status: 'bad_evaluate',
  770. scores: [1]
  771. }])
  772. let evaluateTableData = reactive<any>({
  773. data: [],
  774. page: 1,
  775. limit: 10,
  776. total: 0,
  777. loading: false,
  778. searchParam:{
  779. goods_id: 0,
  780. currentIndex: '',
  781. scores: [],
  782. }
  783. })
  784. // 切换tab
  785. const handelClick = (data: any) => {
  786. evaluateTableData.data = []
  787. evaluateTableData.searchParam.currentIndex = data.status
  788. evaluateTableData.searchParam.scores = []
  789. evaluateTableData.searchParam.scores = data.scores
  790. evaluateTableData.page = 1
  791. getEvaluateListFn()
  792. }
  793. // 评价列表
  794. const getEvaluateListFn = (page:number = 1) => {
  795. evaluateTableData.page = page
  796. evaluateTableData.loading = true
  797. getEvaluateList({
  798. page: evaluateTableData.page,
  799. limit: evaluateTableData.limit,
  800. ...evaluateTableData.searchParam
  801. }).then((res: any) => {
  802. evaluateTableData.total = res.data.total
  803. evaluateTableData.data = res.data.data.map((item: any) => {
  804. if(item.images && item.images.length){
  805. item.imagesList = item.images.map((el: any) => {
  806. return img(el)
  807. })
  808. }
  809. return item
  810. });
  811. evaluateTableData.loading = false
  812. }).catch(() => {
  813. evaluateTableData.loading = false
  814. })
  815. }
  816. // 店铺推荐
  817. let recommendList = ref<Array<any>>([])
  818. const getRecommendFn = () => {
  819. getRecommendGoods({ site_id: detail.value.site_id, limit: 4 }).then(res => {
  820. recommendList.value = res.data
  821. })
  822. }
  823. //添加足迹
  824. const addBrowseFn = () => {
  825. addBrowse({ goods_id:detail.value.goods_id,sku_id:detail.value.sku_id }).then(res => {})
  826. }
  827. //路由跳转不刷新页面,重新加载数据
  828. watch(route, (to, from) => {
  829. router.go(0)
  830. })
  831. const toDetail = (goods_id: number) => {
  832. router.push(`/goods/detail?id=${goods_id}`)
  833. }
  834. </script>
  835. <style lang="scss" scoped>
  836. .discount-bg{
  837. background-image: url(@/assets/images/addon/discount-bg.png);
  838. background-position: center center;
  839. background-size: 100% 100%;
  840. background-repeat: no-repeat;
  841. }
  842. .square{
  843. position: relative;
  844. ::before{
  845. content: '';
  846. width: 6px;
  847. height: 6px;
  848. background-color: #f3f3f3;
  849. position: absolute;
  850. top: 50%;
  851. left: -3px;
  852. border-radius:3px;
  853. transform: translateY(-50%);
  854. }
  855. ::after{
  856. content: '';
  857. width: 6px;
  858. height: 6px;
  859. background-color: #f3f3f3;
  860. position: absolute;
  861. top: 50%;
  862. right: -3px;
  863. border-radius:3px;
  864. transform: translateY(-50%);
  865. }
  866. }
  867. .coupon-bg{
  868. background-image: url(@/assets/images/addon/coupon-bg.png);
  869. background-position: center center;
  870. background-size: 100% 100%;
  871. background-repeat: no-repeat;
  872. }
  873. :deep(.page-form .el-form-item__label){
  874. color: #999;
  875. overflow: hidden;
  876. text-overflow: ellipsis;
  877. white-space: nowrap;
  878. }
  879. /* 多行超出隐藏 */
  880. .multi-hidden {
  881. word-break: break-all;
  882. text-overflow: ellipsis;
  883. overflow: hidden;
  884. display: -webkit-box;
  885. -webkit-line-clamp: 2;
  886. -webkit-box-orient: vertical;
  887. }
  888. .coupon-item{
  889. ::before{
  890. content: '';
  891. display: block;
  892. width: 10px;
  893. height: 10px;
  894. background-color: #f5f5f5;
  895. position: absolute;
  896. top: 50%;
  897. left: -5px;
  898. border-radius:5px;
  899. transform: translateY(-50%);
  900. }
  901. ::after{
  902. content: '';
  903. display: block;
  904. width: 10px;
  905. height: 10px;
  906. background-color: #fff;
  907. position: absolute;
  908. top: 50%;
  909. right: -5px;
  910. border-radius:5px;
  911. transform: translateY(-50%);
  912. }
  913. }
  914. :deep(.el-input-number__decrease){
  915. background: #FAFCFF;
  916. }
  917. :deep(.el-input-number__increase){
  918. background: #FAFCFF;
  919. }
  920. </style>