detail.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <template>
  2. <Theme>
  3. <view class="wrap">
  4. <Navbar
  5. :bgColor="
  6. scrollTop >= tabInitTop || activeNum == 1
  7. ? 'var(--light)'
  8. : 'transparent'
  9. "
  10. fixed
  11. >
  12. <template #left>
  13. <view class="nav_left">
  14. <up-icon name="arrow-left" size="16" color="var(--light)"></up-icon>
  15. </view>
  16. </template>
  17. <template #right>
  18. <view class="nav_right">
  19. <view class="nav_item" @click.stop="collectClick">
  20. <up-icon
  21. name="star-fill"
  22. size="16"
  23. :color="isCollect ? 'var(--primary)' : 'var(--light)'"
  24. ></up-icon>
  25. </view>
  26. <view class="nav_item" @click.stop="shareClick">
  27. <up-icon name="share" size="16" color="var(--light)"></up-icon>
  28. </view>
  29. </view>
  30. </template>
  31. </Navbar>
  32. <view class="content" id="conts" ref="contRef">
  33. <scroll-view
  34. scroll-with-animation
  35. :scroll-into-view="scrollId"
  36. :scroll-y="true"
  37. class="scroll-view"
  38. >
  39. <view id="scroll_item_0" class="scroll_item0">
  40. <view id="tabInit" v-if="activeNum == 0">
  41. <view class="swiper">
  42. <Swiper
  43. :listArr="swiperImgs"
  44. autoplay
  45. height="100%"
  46. circular
  47. indicator-dots
  48. indicator-active-color="var(--black)"
  49. indicator-color="var(--light)"
  50. urlName="url"
  51. borderRadius="0"
  52. />
  53. </view>
  54. </view>
  55. <view class="info_wrapper">
  56. <view class="price_wrapper">
  57. <view class="texts">
  58. <rich-text
  59. :nodes="MoneyAbouthtml(detail.price, true)"
  60. ></rich-text>
  61. <text class="original-price" v-if="detail.originalprice"
  62. >{{ symbol.symbol }}{{ detail.originalprice }}</text
  63. >
  64. </view>
  65. <view class="sale_right" v-if="detail.sale">
  66. <trans _t="总销量" /> {{ detail.sale }}
  67. </view>
  68. </view>
  69. <view class="info_name">{{
  70. detail.title || detail.goodsName
  71. }}</view>
  72. </view>
  73. </view>
  74. <view id="scroll_item_1" class="scroll_item1" v-if="activeNum == 0">
  75. <view
  76. class="detail_wrapper"
  77. v-if="detail.propArr && detail.propArr.length"
  78. >
  79. <view class="module_title">
  80. <trans _t="规格参数" />
  81. </view>
  82. <Norm :item="detail.propArr" />
  83. </view>
  84. <view class="detail_wrapper" v-if="detail.desc">
  85. <view class="module_title">
  86. <trans _t="产品详情" />
  87. </view>
  88. <view class="desc_imgs">
  89. <rich-text class="desc_cont" :nodes="detail.desc"></rich-text>
  90. </view>
  91. </view>
  92. </view>
  93. </scroll-view>
  94. <view class="footer_btns">
  95. <view class="btn_wrapper">
  96. <view class="btn btn_add" @click="add('add')">
  97. <trans _t="加入购物车" />
  98. </view>
  99. <view class="btn btn_buy" @click="add('buy')">
  100. <trans _t="立即购买" />
  101. </view>
  102. </view>
  103. </view>
  104. </view>
  105. </view>
  106. <quantityPopup :detail="detail" ref="quantityRef" />
  107. </Theme>
  108. </template>
  109. <script setup>
  110. import { ref, onMounted, nextTick, computed } from "vue";
  111. import Navbar from "@/components/navbar";
  112. import { SELLER_GOODS_INFO } from "@/api";
  113. import { onLoad, onPageScroll } from "@dcloudio/uni-app";
  114. import { t } from "@/locale";
  115. import {
  116. useShopStore,
  117. useSystemStore,
  118. useTabbarStore,
  119. useCacheStore,
  120. } from "@/store";
  121. import Swiper from "@/components/swiper";
  122. import {
  123. systemInfo,
  124. openUrl,
  125. refresh,
  126. query,
  127. MoneyAbouthtml,
  128. Toast,
  129. TimeOut,
  130. urlStrToArr,
  131. } from "@/utils";
  132. import Tab from "@/components/tabs";
  133. import ImageGrid from "@/components/ImageGrid.vue";
  134. import selectSpec from "@/pages/shop/components/select_spec";
  135. import Norm from "@/pages/shop/components/norm";
  136. import quantityPopup from "./components/quantity_popup";
  137. const useShop = useShopStore();
  138. const useSystem = useSystemStore();
  139. const useTabbar = useTabbarStore();
  140. const useCache = useCacheStore();
  141. const goodsId = ref(0);
  142. const detail = ref({});
  143. const activeNum = ref(0);
  144. const isCollect = ref(false);
  145. const selectRef = ref(null);
  146. const quantityRef = ref(null);
  147. const tabInitTop = ref(0);
  148. const initTab = ref(0);
  149. const scrollTop = ref(0);
  150. const tabList = ["详情", "评论"];
  151. const contRef = ref(null);
  152. const scrollId = ref("scroll_item_0");
  153. const swiperImgs = computed(() => {
  154. if (detail.value.item_imgs && detail.value.item_imgs.length) {
  155. return detail.value.item_imgs.slice(0, 6);
  156. }
  157. if (detail.value.picurl) {
  158. return [{ url: detail.value.picurl }];
  159. }
  160. return [];
  161. });
  162. const currency = computed(() => useSystem.getCurrency);
  163. const symbol = computed(() => useSystem.getSymbol);
  164. const top = systemInfo().statusBarHeight || 0;
  165. const tabClick = (item, index) => {
  166. activeNum.value = index;
  167. getHeight("scroll_item_" + index);
  168. let newTop =
  169. index == 0
  170. ? 0
  171. : index == 1
  172. ? height0.value - 44 - top - initTab.value
  173. : height0.value + height1.value - 44 - top - initTab.value;
  174. uni.pageScrollTo({
  175. scrollTop: newTop,
  176. duration: 300,
  177. });
  178. };
  179. const height0 = ref(0);
  180. const height1 = ref(0);
  181. const getHeight = async (id) => {
  182. let ids = "#" + id;
  183. try {
  184. const res = await query(ids);
  185. if (id == "scroll_item_0" && !height0.value) {
  186. height0.value = res.height;
  187. }
  188. if (id == "scroll_item_1" && !height1.value) {
  189. height1.value = res.height;
  190. }
  191. } catch (error) {}
  192. };
  193. const collectClick = () => {
  194. // TODO: 实现收藏功能
  195. Toast("收藏功能待实现");
  196. };
  197. const shareClick = () => {
  198. // TODO: 实现分享功能
  199. Toast("分享功能待实现");
  200. };
  201. const add = (type) => {
  202. quantityRef.value && quantityRef.value.open(type);
  203. };
  204. const getDetail = async () => {
  205. const params = {
  206. id: goodsId.value,
  207. };
  208. try {
  209. const res = await SELLER_GOODS_INFO(params);
  210. detail.value = res.data;
  211. isCollect.value = res.data.isCollect ? true : false;
  212. if (res.data.skudesc) {
  213. detail.value.propArr = parseSkuDesc(res.data.skudesc);
  214. }
  215. } catch (error) {
  216. Toast(error.msg);
  217. }
  218. };
  219. // 解析skudesc字段,转换为规格参数数组
  220. const parseSkuDesc = (skudesc) => {
  221. if (!skudesc) return [];
  222. const specs = skudesc.split(";");
  223. const propArr = [];
  224. specs.forEach((spec) => {
  225. if (spec.trim()) {
  226. const [name, value] = spec.split(":");
  227. if (name && value) {
  228. propArr.push({
  229. name: name.trim(),
  230. value: value.trim(),
  231. });
  232. }
  233. }
  234. });
  235. return propArr;
  236. };
  237. onPageScroll((e) => {
  238. scrollTop.value = e.scrollTop;
  239. });
  240. onLoad((options) => {
  241. const { id = 0 } = options;
  242. goodsId.value = id;
  243. });
  244. onMounted(() => {
  245. nextTick(async () => {
  246. getDetail();
  247. const scrollItem0 = await query("#tabInit");
  248. tabInitTop.value = scrollItem0.height;
  249. const scrollTab = await query("#tab");
  250. initTab.value = scrollTab.height;
  251. });
  252. });
  253. </script>
  254. <style lang="less" scoped>
  255. @import url("@/style.less");
  256. .wrap {
  257. background-color: var(--bg);
  258. min-height: 100vh;
  259. .nav_left {
  260. background: rgba(0, 0, 0, 0.6);
  261. border-radius: 10rpx;
  262. width: 50rpx;
  263. height: 50rpx;
  264. .flex_center();
  265. }
  266. .nav_right {
  267. .flex_center();
  268. gap: 12rpx;
  269. .nav_item {
  270. width: 50rpx;
  271. height: 50rpx;
  272. .flex_center();
  273. background: rgba(0, 0, 0, 0.6);
  274. color: var(--light);
  275. border-radius: 10rpx;
  276. }
  277. }
  278. .tab {
  279. z-index: 11;
  280. background-color: var(--light);
  281. &.active_tab {
  282. position: fixed;
  283. top: 44px;
  284. left: 0;
  285. right: 0;
  286. }
  287. }
  288. .content {
  289. padding-bottom: 148rpx;
  290. margin-top: -44px;
  291. .scroll_item0 {
  292. /deep/ .swiper_list {
  293. object-fit: cover;
  294. aspect-ratio: 1 / 1;
  295. }
  296. .swiper {
  297. position: relative;
  298. /deep/ .uni-swiper-dots-horizontal {
  299. bottom: 40px;
  300. }
  301. }
  302. .info_wrapper {
  303. background-color: var(--light);
  304. padding: 24rpx;
  305. border-radius: 60rpx 60rpx 0 0;
  306. margin-top: -30px;
  307. position: relative;
  308. .price_wrapper {
  309. .hor(space-between);
  310. align-items: center;
  311. height: 60rpx;
  312. font-weight: bold;
  313. color: var(--red);
  314. .size(40rpx);
  315. margin-bottom: 24rpx;
  316. .texts {
  317. display: flex;
  318. align-items: baseline;
  319. line-height: 1;
  320. margin-left: 8rpx;
  321. /deep/ div {
  322. .ver(baseline);
  323. .decimal {
  324. .size(24rpx);
  325. }
  326. }
  327. .original-price {
  328. margin-left: 6rpx;
  329. font-size: 24rpx;
  330. color: var(--bor-color1);
  331. text-decoration: line-through;
  332. }
  333. }
  334. .sale_right {
  335. .size(22rpx);
  336. color: var(--text);
  337. }
  338. }
  339. .info_name {
  340. .size(28rpx);
  341. line-height: 48rpx;
  342. font-weight: 700;
  343. color: var(--text);
  344. }
  345. }
  346. .service_wrapper {
  347. padding: 24rpx;
  348. background-color: var(--inputBg);
  349. .spec_title {
  350. .size(28rpx);
  351. font-weight: 700;
  352. color: var(--text);
  353. line-height: 60rpx;
  354. }
  355. .spec_info {
  356. border-radius: 16rpx;
  357. display: grid;
  358. gap: 10rpx;
  359. grid-template-columns: repeat(2, 1fr);
  360. margin-top: 16rpx;
  361. padding: 16rpx 0;
  362. color: var(--text-02);
  363. .size(24rpx);
  364. .spec_item {
  365. .ver();
  366. .spec_value {
  367. align-items: center;
  368. color: var(--text);
  369. display: inline-flex;
  370. font-weight: 500;
  371. margin-left: 12px;
  372. }
  373. }
  374. }
  375. }
  376. }
  377. .scroll_item1 {
  378. .detail_wrapper {
  379. background-color: var(--light);
  380. padding: 24rpx;
  381. margin-top: 24rpx;
  382. .module_title {
  383. .size(28rpx);
  384. font-weight: 700;
  385. line-height: 60rpx;
  386. color: var(--text);
  387. }
  388. .desc_imgs {
  389. .desc_cont {
  390. /deep/ div {
  391. div {
  392. width: 100% !important;
  393. div {
  394. width: 100% !important;
  395. }
  396. }
  397. img {
  398. max-width: 100% !important;
  399. width: 100%;
  400. height: auto;
  401. display: block;
  402. }
  403. }
  404. }
  405. /deep/ img {
  406. max-width: 100%;
  407. }
  408. }
  409. .spec_list {
  410. .spec_item {
  411. display: flex;
  412. justify-content: space-between;
  413. padding: 16rpx 0;
  414. border-bottom: 1px solid var(--border);
  415. .spec_label {
  416. color: var(--text-02);
  417. .size(24rpx);
  418. }
  419. .spec_value {
  420. color: var(--text);
  421. .size(24rpx);
  422. font-weight: 500;
  423. }
  424. }
  425. }
  426. }
  427. }
  428. .scroll_item2 {
  429. padding: 24rpx 16rpx;
  430. .title {
  431. font-weight: 700;
  432. line-height: 60rpx;
  433. .size(28rpx);
  434. }
  435. .comment_item {
  436. width: 100%;
  437. margin-top: 16rpx;
  438. padding: 12rpx;
  439. border: 1px solid var(--border);
  440. border-radius: 10rpx;
  441. .comment_text {
  442. margin-top: 6rpx;
  443. color: var(--text);
  444. }
  445. }
  446. .tips {
  447. text-align: center;
  448. font-size: 28rpx;
  449. color: #333;
  450. }
  451. }
  452. .footer_btns {
  453. position: fixed;
  454. bottom: 0;
  455. left: 0;
  456. right: 0;
  457. z-index: 10;
  458. padding: 12rpx 24rpx;
  459. background-color: var(--light);
  460. .flex_center();
  461. padding-bottom: calc(12rpx + env(safe-area-inset-bottom));
  462. padding-bottom: calc(12rpx + constant(safe-area-inset-bottom));
  463. box-sizing: border-box;
  464. .btn_wrapper {
  465. flex: 1;
  466. .ver();
  467. column-gap: 64rpx;
  468. .btn {
  469. flex: 1;
  470. .size(28rpx);
  471. height: 96rpx;
  472. line-height: 28rpx;
  473. padding: 8rpx 16rpx;
  474. .flex_center();
  475. border: 2px solid var(--black);
  476. color: var(--black);
  477. border-radius: 20rpx;
  478. font-weight: bold;
  479. }
  480. .btn_buy {
  481. background-color: var(--light);
  482. }
  483. .btn_add {
  484. background-color: var(--black);
  485. color: var(--light);
  486. }
  487. }
  488. }
  489. }
  490. }
  491. </style>