visitor.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. <template>
  2. <view :style="{ paddingTop: statusBarHeight + 'px' }" class="bg-white">
  3. <view>
  4. <!-- 新的头部搜索栏 -->
  5. <view class="header-container">
  6. <view class="search-container">
  7. <view class="search-icon">
  8. <uni-icons type="search" size="18" color="#666"></uni-icons>
  9. </view>
  10. <input class="search-input" type="text" placeholder="Search ROMIDO" @input="onInput"
  11. @confirm="onSearch" />
  12. <view class="barcode-icon">
  13. <uni-icons type="scan" size="20" color="#666"></uni-icons>
  14. </view>
  15. </view>
  16. <view class="cart-container">
  17. <view class="message-icon" @tap="goChat">
  18. <uni-icons type="chat-filled" size="25" color="#1351D8"></uni-icons>
  19. <view class="unread-badge" v-if="unreadMessageCount > 0">{{unreadMessageCount}}</view>
  20. </view>
  21. <view class="cart-icon" @tap="goCart">
  22. <uni-icons type="cart-filled" size="25" color="#1351D8"></uni-icons>
  23. <view class="cart-badge" v-if="cartCount > 0">{{cartCount}}</view>
  24. </view>
  25. </view>
  26. </view>
  27. <view class="bg-index-image">
  28. <swiper class="swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500">
  29. <!-- swiper-item组件,每个item代表一张轮播图 -->
  30. <swiper-item v-for="(item, index) in bannerList" :key="index">
  31. <image class="swiper-image" :src="item.image" mode="scaleToFill"></image>
  32. </swiper-item>
  33. </swiper>
  34. <!-- 新品上市 -->
  35. <view class="product-xinpin-bg">
  36. <view class="product">
  37. <view class="font-def pad-t-20 flex-sp-between flex-items">
  38. <view class="mar-l-20 font32 font-bold">
  39. New Arrivals
  40. </view>
  41. <view class="view-all" @click="goAll">
  42. <text>View All</text>
  43. <uni-icons type="right" size="14" color="#666"></uni-icons>
  44. </view>
  45. </view>
  46. <view class="xinpin">
  47. <view class="new-product-module">
  48. <!-- 商品列表 -->
  49. <view class="product-list">
  50. <view class="product-item" v-for="(product, index) in currentProductList"
  51. :key="index" @click="detail(product.id)">
  52. <!-- 商品图片 -->
  53. <image :src=" product.images" mode="aspectFill" class="product-image">
  54. </image>
  55. <!-- 商品名称 -->
  56. <view class="product-name">{{ product.name }}</view>
  57. <!-- 商品价格 -->
  58. <view class="product-price">¥{{ product.price }}</view>
  59. </view>
  60. </view>
  61. <!-- 换一批按钮 -->
  62. <view class="change-button" @click="changeProducts">
  63. <view class="pad-r-10">
  64. <uni-icons color="#007aff" type="refreshempty" size="18"></uni-icons>
  65. </view>
  66. <view>
  67. refresh
  68. </view>
  69. </view>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 限时秒杀 -->
  75. <!-- <view class="product-miaosha-bg">
  76. <view class="product">
  77. <view class="font-def pad-t-20 flex-sp-between flex-items">
  78. <view class="mar-l-20 font32 font-bold">
  79. 限时秒杀
  80. </view>
  81. <view class="mar-r-20 font26" @click="goAll">
  82. 查看全部>
  83. </view>
  84. </view>
  85. <view class="xinpin">
  86. <view class="new-product-module">
  87. <view class="product-list">
  88. <view class="product-item"
  89. v-for="(product, index) in indexProductList.killTimeProductList" :key="index"
  90. @click="detail(product.id)" v-if="index<3">
  91. <view class="product-image-container">
  92. <image :src="product.images" mode="aspectFill" class="product-image">
  93. </image>
  94. <view class="countdown">{{ getCountdown(product.killTime) }}</view>
  95. </view>
  96. <view class="product-name">{{ product.name }}</view>
  97. <view class="product-price">¥{{ product.price }}</view>
  98. <view class="product-stock">剩余{{ product.stock }}件</view>
  99. </view>
  100. </view>
  101. </view>
  102. </view>
  103. </view>
  104. </view> -->
  105. <!-- 更多商品 -->
  106. <view class="product-gengduo-bg">
  107. <view class="font-def pad-t-20 flex-sp-between flex-items">
  108. <view class="mar-l-20 font32 font-bold">
  109. More Products
  110. </view>
  111. <view class="view-all" @click="goAll">
  112. <text>View All</text>
  113. <uni-icons type="right" size="14" color="#666"></uni-icons>
  114. </view>
  115. </view>
  116. <view class="goods-list-container">
  117. <!-- 商品列表 -->
  118. <view class="goods-list">
  119. <view class="goods-item" v-for="(item, index) in goodsList" :key="index"
  120. @tap="detail(item.id)">
  121. <!-- 商品图片 -->
  122. <image :src="item.images" mode="aspectFill" class="gd-goods-image"></image>
  123. <!-- 商品详情 -->
  124. <view class="goods-info">
  125. <!-- 商品名称 -->
  126. <view class="goods-name">{{ item.name }}</view>
  127. <view class="flex-items">
  128. <text>
  129. {{item.productAvg}}
  130. </text>
  131. <uni-rate readonly allow-half :value="item.productAvg" />
  132. <text class="commentCount">
  133. ({{item.commentCount}})
  134. </text>
  135. </view>
  136. <!-- 商品价格 -->
  137. <view class="goods-price">
  138. ¥{{ item.price }}
  139. <text class="original-price"
  140. v-if="item.originalPrice">¥{{ item.originalPrice }}</text>
  141. </view>
  142. <!-- 商品销量 -->
  143. <view class="goods-sales">sales:{{ item.salesTotal }}</view>
  144. <!-- 加号图标 -->
  145. <!-- <view class="add-icon" @click.stop="handleAdd(item)">
  146. <uni-icons color="#1BA4F0" type="plus-filled" size="30"></uni-icons>
  147. </view> -->
  148. </view>
  149. </view>
  150. </view>
  151. <!-- 加载提示 -->
  152. <view v-if="isLoading" class="loading-tip">加载中...</view>
  153. <!-- 没有更多数据提示 -->
  154. <view v-if="!isLoading && noMoreData" class="no-more-tip">——没有更多商品了——</view>
  155. </view>
  156. </view>
  157. </view>
  158. </view>
  159. </view>
  160. </template>
  161. <script>
  162. import {
  163. carouselQueryAll,
  164. getShopIndexList,
  165. productList,
  166. productCartCount,
  167. unreadCount,
  168. markMessagesRead
  169. } from '@/config/api.js';
  170. export default {
  171. data() {
  172. return {
  173. statusBarHeight: 0, // 状态栏高度
  174. cartCount: 0, // 购物车数量
  175. goodsList: [], // 商品列表
  176. params: {
  177. current: 1,
  178. size: 10,
  179. categoriesId: "",
  180. name: "",
  181. sort: "",
  182. arrow: "",
  183. },
  184. isLoading: false, // 是否正在加载
  185. noMoreData: false, // 是否没有更多数据
  186. searchValue: "",
  187. bannerList: [],
  188. indexProductList: [],
  189. currentIndex: 0, // 当前显示的商品起始索引
  190. currentProductList: [], // 当前显示的商品列表
  191. newProductList: [],
  192. user: {},
  193. unreadMessageCount: 0, // Added for unread message count
  194. hasLoaded: false, // 标记页面是否已经加载过
  195. };
  196. },
  197. onLoad() {
  198. // 获取状态栏高度
  199. const systemInfo = uni.getSystemInfoSync();
  200. this.statusBarHeight = systemInfo.statusBarHeight;
  201. this.init();
  202. // 获取购物车数量
  203. this.getCartCount();
  204. // 获取未读消息数量
  205. this.getUnreadMessageCount();
  206. },
  207. onReachBottom() {
  208. if (!this.isLoading && !this.noMoreData) {
  209. this.loadGoods();
  210. }
  211. },
  212. onShow() {
  213. // 每次显示页面时获取最新的购物车数量
  214. // 只有不是首次加载时才更新未读消息数量(避免与onLoad重复调用)
  215. if (this.hasLoaded) {
  216. this.getCartCount();
  217. this.getUnreadMessageCount();
  218. } else {
  219. this.hasLoaded = true;
  220. }
  221. },
  222. methods: {
  223. goCart() {
  224. this.$route('/pages/shop/cart');
  225. },
  226. goChat() {
  227. // 先将所有消息标记为已读
  228. const userInfo = uni.getStorageSync("userInfo");
  229. if (userInfo && userInfo.user_id) {
  230. markMessagesRead({
  231. userId: userInfo.user_id
  232. }).then(res => {
  233. // 标记成功后,将未读消息数量清零
  234. if(res.success) {
  235. this.unreadMessageCount = 0;
  236. }
  237. }).catch(err => {
  238. console.error('标记消息已读失败:', err);
  239. });
  240. }
  241. // 然后跳转到聊天页面
  242. this.$route('/pages/chat/index');
  243. },
  244. // 获取购物车数量
  245. getCartCount() {
  246. productCartCount().then((res) => {
  247. this.cartCount = res.data;
  248. }).catch(err => {
  249. console.error('获取购物车数量失败:', err);
  250. });
  251. },
  252. // 获取未读消息数量
  253. getUnreadMessageCount() {
  254. // 确保用户信息已加载
  255. const userInfo = uni.getStorageSync("userInfo");
  256. if (userInfo && userInfo.user_id) {
  257. unreadCount({
  258. userId: userInfo.user_id
  259. }).then(res => {
  260. this.unreadMessageCount = res.data;
  261. }).catch(err => {
  262. console.error('获取未读消息数量失败:', err);
  263. });
  264. }
  265. },
  266. goBack() {
  267. uni.navigateBack()
  268. },
  269. detail(id) {
  270. this.$route('/packageShop/pages/detail/index?id=' + id)
  271. },
  272. onInput() {
  273. },
  274. onSearch() {
  275. this.$route('/pages/order/search?type=1&searchKey=' + this.params.name);
  276. },
  277. goAll() {
  278. this.$route('/pages/shop/product-type-list')
  279. },
  280. init() {
  281. this.user = uni.getStorageInfoSync("userInfo")
  282. this.getShufflingList();
  283. this.getIndexList();
  284. // 开启倒计时定时器
  285. this.startCountdown();
  286. this.loadGoods();
  287. },
  288. async loadGoods() {
  289. uni.showLoading({
  290. title: '加载中...',
  291. mask: true
  292. });
  293. this.isLoading = true;
  294. try {
  295. const response = await productList(this.params);
  296. const newGoods = response.data.records;
  297. if (newGoods.length === 0) {
  298. this.noMoreData = true;
  299. } else {
  300. this.goodsList = this.goodsList.concat(newGoods);
  301. this.params.current++;
  302. }
  303. } catch (error) {
  304. console.error('加载商品数据失败:', error);
  305. } finally {
  306. uni.hideLoading()
  307. this.isLoading = false;
  308. }
  309. },
  310. // 处理加号点击事件
  311. handleAdd(item) {
  312. console.log('点击了加号,商品信息:', item);
  313. // 这里可以添加你需要的业务逻辑,比如添加到购物车等
  314. },
  315. // 开启倒计时定时器
  316. startCountdown() {
  317. setInterval(() => {
  318. this.$forceUpdate(); // 强制更新组件,重新计算倒计时
  319. }, 1000);
  320. },
  321. // 计算倒计时
  322. getCountdown(endTime) {
  323. const now = new Date().getTime();
  324. const end = new Date(endTime).getTime();
  325. const diff = end - now;
  326. if (diff <= 0) {
  327. return '已结束';
  328. }
  329. const days = Math.floor(diff / (1000 * 60 * 60 * 24));
  330. const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  331. const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
  332. const seconds = Math.floor((diff % (1000 * 60)) / 1000);
  333. if (days > 0) {
  334. return `${days} 天 ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  335. } else {
  336. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  337. }
  338. },
  339. getIndexList() {
  340. getShopIndexList({
  341. type: 2
  342. }).then(res => {
  343. this.indexProductList = res.data;
  344. this.newProduct();
  345. });
  346. },
  347. newProduct() {
  348. this.currentProductList = this.indexProductList.newProductList.slice(this.currentIndex, this.currentIndex +
  349. 3);
  350. },
  351. // 换一批商品
  352. changeProducts() {
  353. if (this.currentIndex + 3 < this.indexProductList.newProductList.length) {
  354. this.currentIndex += 3;
  355. } else {
  356. this.currentIndex = 0;
  357. }
  358. this.newProduct();
  359. },
  360. getShufflingList() {
  361. carouselQueryAll().then(res => {
  362. this.bannerList = res.data;
  363. this.bannerList = this.bannerList.filter((item) => item.type == 2);
  364. });
  365. },
  366. }
  367. };
  368. </script>
  369. <style>
  370. .goods-list-container {
  371. padding: 20rpx;
  372. }
  373. .goods-list {
  374. display: flex;
  375. flex-direction: column;
  376. }
  377. .goods-item {
  378. width: 100%;
  379. background-color: #fff;
  380. border-radius: 16rpx;
  381. margin-bottom: 20rpx;
  382. padding: 20rpx;
  383. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
  384. position: relative;
  385. box-sizing: border-box;
  386. display: flex;
  387. }
  388. .gd-goods-image {
  389. width: 240rpx;
  390. height: 240rpx;
  391. border-radius: 8rpx;
  392. margin-right: 20rpx;
  393. flex-shrink: 0;
  394. }
  395. .goods-info {
  396. flex: 1;
  397. display: flex;
  398. flex-direction: column;
  399. justify-content: space-between;
  400. padding-right: 40rpx;
  401. }
  402. .goods-name {
  403. font-size: 28rpx;
  404. color: #333;
  405. margin-bottom: 10rpx;
  406. overflow: hidden;
  407. text-overflow: ellipsis;
  408. display: -webkit-box;
  409. -webkit-line-clamp: 2;
  410. -webkit-box-orient: vertical;
  411. line-height: 1.4;
  412. }
  413. .goods-price {
  414. font-size: 36rpx;
  415. color: #FF4B4B;
  416. font-weight: bold;
  417. margin: 10rpx 0;
  418. }
  419. .original-price {
  420. font-size: 24rpx;
  421. color: #999;
  422. text-decoration: line-through;
  423. margin-left: 10rpx;
  424. }
  425. .goods-tag {
  426. display: inline-block;
  427. background-color: #FFF5F5;
  428. color: #FF4B4B;
  429. font-size: 20rpx;
  430. padding: 4rpx 12rpx;
  431. border-radius: 6rpx;
  432. margin-bottom: 10rpx;
  433. }
  434. .goods-sales {
  435. font-size: 24rpx;
  436. color: #999;
  437. margin-top: auto;
  438. }
  439. .add-icon {
  440. position: absolute;
  441. bottom: 20rpx;
  442. right: 20rpx;
  443. width: 40rpx;
  444. height: 40rpx;
  445. display: flex;
  446. justify-content: center;
  447. align-items: center;
  448. font-size: 32rpx;
  449. }
  450. .loading-tip {
  451. text-align: center;
  452. font-size: 24rpx;
  453. color: #999;
  454. margin: 20rpx 0;
  455. }
  456. .no-more-tip {
  457. text-align: center;
  458. font-size: 24rpx;
  459. color: #999;
  460. margin: 20rpx 0;
  461. }
  462. .product-image-container {
  463. position: relative;
  464. }
  465. .countdown {
  466. position: absolute;
  467. bottom: 0;
  468. left: 0;
  469. right: 0;
  470. background-color: red;
  471. color: white;
  472. text-align: center;
  473. font-size: 24rpx;
  474. padding: 5rpx;
  475. }
  476. .new-product-module {
  477. background-color: #fff;
  478. padding: 40rpx 40rpx 20rpx 40rpx;
  479. }
  480. .product-list {
  481. display: flex;
  482. justify-content: flex-start;
  483. /* 修改为靠左对齐 */
  484. flex-wrap: wrap;
  485. }
  486. .product-item {
  487. width: calc((100% - 40rpx) / 3);
  488. margin-bottom: 20rpx;
  489. margin-right: 20rpx;
  490. /* 添加右边距,使商品之间有间隔 */
  491. }
  492. /* 去除最后一个商品的右边距 */
  493. .product-item:nth-child(3n) {
  494. margin-right: 0;
  495. }
  496. .product-image {
  497. width: 100%;
  498. height: 200rpx;
  499. border-radius: 8rpx;
  500. }
  501. .product-name {
  502. font-size: 30rpx;
  503. color: #333;
  504. line-height: 32rpx;
  505. height: 64rpx;
  506. overflow: hidden;
  507. text-overflow: ellipsis;
  508. display: -webkit-box;
  509. -webkit-line-clamp: 2;
  510. -webkit-box-orient: vertical;
  511. margin-top: 10rpx;
  512. }
  513. .product-price {
  514. padding-top: 20rpx;
  515. font-size: 30rpx;
  516. color: #f60;
  517. }
  518. .product-stock {
  519. color: #333333;
  520. padding-top: 10rpx;
  521. font-size: 26rpx;
  522. }
  523. .change-button {
  524. display: flex;
  525. align-items: center;
  526. justify-content: center;
  527. font-size: 28rpx;
  528. color: #007aff;
  529. cursor: pointer;
  530. }
  531. .search-container {
  532. flex: 1;
  533. display: flex;
  534. align-items: center;
  535. background-color: #F6F7F9;
  536. border-radius: 40rpx;
  537. padding: 8rpx 20rpx;
  538. margin: 0 15rpx;
  539. }
  540. .header-container {
  541. display: flex;
  542. align-items: center;
  543. padding: 10rpx 20rpx 20rpx 20rpx;
  544. background-color: #fff;
  545. position: fixed;
  546. top: 0;
  547. left: 0;
  548. right: 0;
  549. z-index: 100;
  550. padding-top: v-bind('statusBarHeight + "px"');
  551. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  552. }
  553. .back-button {
  554. padding: 10rpx;
  555. margin-right: 10rpx;
  556. }
  557. .search-icon {
  558. margin-right: 10rpx;
  559. }
  560. .search-input {
  561. flex: 1;
  562. height: 60rpx;
  563. line-height: 60rpx;
  564. font-size: 28rpx;
  565. color: #333;
  566. padding: 0 10rpx;
  567. }
  568. .barcode-icon {
  569. margin-left: 10rpx;
  570. padding: 5rpx;
  571. }
  572. .cart-container {
  573. display: flex;
  574. align-items: center;
  575. }
  576. .message-icon {
  577. margin-right: 20rpx;
  578. position: relative; /* 添加相对定位,以便角标可以相对于图标定位 */
  579. }
  580. .cart-icon {
  581. position: relative;
  582. }
  583. .cart-badge {
  584. position: absolute;
  585. top: -10rpx;
  586. right: -10rpx;
  587. background-color: #FF4B4B;
  588. color: #fff;
  589. font-size: 20rpx;
  590. padding: 2rpx 8rpx;
  591. border-radius: 20rpx;
  592. min-width: 28rpx;
  593. height: 28rpx;
  594. text-align: center;
  595. line-height: 28rpx;
  596. }
  597. .unread-badge {
  598. position: absolute;
  599. top: -10rpx;
  600. right: -10rpx;
  601. background-color: #FF4B4B;
  602. color: #fff;
  603. font-size: 20rpx;
  604. padding: 2rpx 8rpx;
  605. border-radius: 20rpx;
  606. min-width: 28rpx;
  607. height: 28rpx;
  608. text-align: center;
  609. line-height: 28rpx;
  610. }
  611. .commentCount {
  612. font-size: 28rpx;
  613. color: #666;
  614. }
  615. .product-xinpin-bg {
  616. position: absolute;
  617. background: linear-gradient(to bottom, #f8f9fc, #ffffff);
  618. top: 650rpx;
  619. min-height: 100vh;
  620. width: 100%;
  621. }
  622. .product-miaosha-bg {
  623. position: absolute;
  624. top: 1180rpx;
  625. /* 调整位置:新品上市区域位置(650) + 预估高度(530) */
  626. min-height: 100vh;
  627. width: 100%;
  628. }
  629. .product-gengduo-bg {
  630. position: absolute;
  631. top: 1210rpx;
  632. background: linear-gradient(to bottom, #f8f9fc, #ffffff);
  633. min-height: 100vh;
  634. width: 100%;
  635. }
  636. .xinpin {
  637. margin: 20rpx;
  638. background-color: #FFF;
  639. border-radius: 16rpx;
  640. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
  641. }
  642. .product {
  643. margin-top: 30rpx;
  644. min-height: 100vh;
  645. background: linear-gradient(to bottom, #f8f9fc, #ffffff);
  646. border-radius: 40rpx;
  647. }
  648. .product-bg {
  649. position: absolute;
  650. background-color: white;
  651. top: 500rpx;
  652. min-height: 100vh;
  653. width: 100%;
  654. }
  655. .bg-index-image {
  656. background: linear-gradient(to bottom, #f8f9fc, #ffffff);
  657. background-size: cover;
  658. height: 500rpx;
  659. position: relative;
  660. /* margin-top: v-bind('(statusBarHeight + 84) + "px"'); */
  661. }
  662. .swiper {
  663. width: 100%;
  664. height: 550rpx;
  665. position: absolute;
  666. /* 设置为绝对定位 */
  667. top: 106rpx;
  668. /* 调整顶部距离 */
  669. }
  670. .swiper-image {
  671. width: 100%;
  672. height: 100%;
  673. }
  674. .message-icon,
  675. .cart-icon {
  676. color: #5C6B8A;
  677. }
  678. .view-all {
  679. display: flex;
  680. align-items: center;
  681. margin-right: 20rpx;
  682. font-size: 26rpx;
  683. color: #666;
  684. }
  685. .view-all text {
  686. margin-right: 4rpx;
  687. }
  688. </style>