detail.vue 34 KB

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