爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2646 lines
75 KiB

2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
4 weeks ago
2 months ago
4 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
4 weeks ago
4 weeks ago
2 months ago
1 month ago
2 months ago
2 months ago
4 weeks ago
1 month ago
2 months ago
1 month ago
4 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
4 weeks ago
2 months ago
4 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
2 months ago
4 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
4 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
4 weeks ago
4 weeks ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
4 weeks ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
  1. <template>
  2. <view class="container">
  3. <!-- 顶部banner -->
  4. <view class="banner">
  5. <swiper
  6. :indicator-dots="false"
  7. :autoplay="true"
  8. :interval="3000"
  9. :duration="500"
  10. circular
  11. style="width: 100%; height: 320rpx;"
  12. >
  13. <swiper-item v-for="(item, index) in bannerList" :key="item.id || index">
  14. <view v-if="item.type == 1" class="video-container">
  15. <!-- 预览状态显示封面图 -->
  16. <image
  17. v-if="!videoPlayingStates[index]"
  18. :src="item.image || ''"
  19. mode="aspectFill"
  20. style="width: 100%; height: 100%;"
  21. class="video-poster"
  22. @click="playVideoFullscreen(item, index)"
  23. />
  24. <!-- 播放状态显示视频 -->
  25. <video
  26. v-else
  27. :id="`recycle-video-${index}`"
  28. :src="item.voUrl"
  29. :autoplay="true"
  30. :muted="false"
  31. :loop="false"
  32. :controls="true"
  33. :show-play-btn="true"
  34. :show-center-play-btn="false"
  35. :show-fullscreen-btn="true"
  36. :show-progress="true"
  37. :show-mute-btn="true"
  38. :enable-progress-gesture="true"
  39. :enable-play-gesture="true"
  40. object-fit="cover"
  41. style="width: 100%; height: 100%;"
  42. @fullscreenchange="onFullscreenChange"
  43. @play="onVideoPlay(index)"
  44. @pause="onVideoPause(index)"
  45. @ended="onVideoEnded(index)"
  46. ></video>
  47. <!-- 播放按钮覆盖层 -->
  48. <view v-if="!videoPlayingStates[index]" class="video-overlay" @click="playVideoFullscreen(item, index)">
  49. <view class="play-button-large">
  50. <view class="play-triangle"></view>
  51. </view>
  52. </view>
  53. </view>
  54. <image v-else :src="item.image" mode="aspectFill" style="width: 100%; height: 100%;" />
  55. </swiper-item>
  56. </swiper>
  57. <!-- 备用静态图片当没有轮播图数据时显示 -->
  58. <image v-if="!bannerList || bannerList.length === 0" :src="recycle_banner" mode="aspectFill" style="width: 100%; height: 100%;" />
  59. </view>
  60. <!-- 商品列表 -->
  61. <view class="goods-list">
  62. <!-- 左侧分类导航 -->
  63. <view class="category-nav">
  64. <view
  65. v-for="(category, index) in categories"
  66. :key="category.id || index"
  67. class="category-item"
  68. :class="{ active: currentCategory === index }"
  69. @click="switchCategory(index)"
  70. >
  71. <view class="category-dot" v-if="getCategoryItemCount(index) > 0">{{ getCategoryItemCount(index) }}</view>
  72. {{ category.title }}
  73. </view>
  74. </view>
  75. <!-- 右侧商品列表 -->
  76. <scroll-view class="goods-content" scroll-y @scrolltolower="loadMoreGoods">
  77. <view class="goods-section">
  78. <view class="goods-item" v-for="(item, index) in recycleList" :key="index">
  79. <image v-if="item.image" :src="item.image" class="goods-item-img" mode="aspectFit" />
  80. <view class="goods-info-wrap">
  81. <view class="goods-header">
  82. <text class="goods-name">{{item.name}}</text>
  83. <view class="brand-check-placeholder" v-if="item.isPin === 'Y'">
  84. <view class="brand-check" @click="checkBrand(index)">
  85. <text>查看品牌</text>
  86. <uni-icons type="right" size="12" color="#ff7a0e"></uni-icons>
  87. </view>
  88. </view>
  89. </view>
  90. <text class="goods-desc">{{item.service}}</text>
  91. <view class="rules-link" @click="showRules(item)">
  92. <view class="rules">
  93. <text>回收规则</text>
  94. <uni-icons type="right" size="14" color="#999"></uni-icons>
  95. </view>
  96. </view>
  97. <view class="goods-info">
  98. <view class="price-info">
  99. <text class="price-symbol">¥</text>
  100. <text class="price-value" v-if="!item.maxPrice || item.maxPrice == item.price">{{item.price}}</text>
  101. <text class="price-value" v-else>{{item.price}}-{{item.maxPrice}}</text>
  102. <text class="price-unit">/{{ item.unit || '件' }}</text>
  103. </view>
  104. <view class="quantity-control">
  105. <button class="btn-minus" @click="updateQuantity(index, -1)">-</button>
  106. <text class="quantity">{{getItemTotalQuantity(item)}}</text>
  107. <button class="btn-plus" @click="updateQuantity(index, 1)">+</button>
  108. </view>
  109. </view>
  110. </view>
  111. </view>
  112. </view>
  113. <!-- 不可回收商品 -->
  114. <view class="other-unrecycle-card">
  115. <image class="other-unrecycle-img" src="/static/回收/衣物.png" mode="aspectFit" />
  116. <view class="other-unrecycle-info">
  117. <view class="other-unrecycle-title">其他上衣需咨询顾问暂不回收</view>
  118. <view class="other-unrecycle-desc">需连线回收顾问筛选</view>
  119. <view class="other-unrecycle-price-row">
  120. <text class="other-unrecycle-price">¥ /</text>
  121. </view>
  122. </view>
  123. <button class="other-unrecycle-btn" open-type="contact">+</button>
  124. </view>
  125. <view v-if="loadingMore" class="loading-more">加载中...</view>
  126. <view v-else-if="finished" class="loading-more">没有更多了</view>
  127. </scroll-view>
  128. </view>
  129. <!-- 固定底部区域 -->
  130. <view class="fixed-bottom-wrap" v-if="!showDetailPanel" >
  131. <view class="green-tip-bar">
  132. 回收范围仅支持回收以上品类按件回收预计比称重回收多
  133. <text class="tip-highlight"> {{extraRecycleAmount.display}}</text>
  134. </view>
  135. <view class="bottom-bar">
  136. <view class="bottom-left">
  137. <view class="summary-row">
  138. <text class="summary-label">已选 <text class="summary-count">{{totalCount}}</text> {{ totalUnitText }} 预计回收可得</text>
  139. <uni-icons type="help" size="18" color="#b2b2b2" style="margin: 0 8rpx;" @tap="showPriceInfoPopups" />
  140. </view>
  141. <view class="amount-row">
  142. <uni-icons :type="showDetailPanel ? 'up' : 'down'" size="18" color="#5e5e5e" style="margin-right: 8rpx;vertical-align: middle;" @click="toggleDetailPanel" />
  143. <text class="amount" v-if="priceRange.min === priceRange.max">¥{{priceRange.min}}</text>
  144. <text class="amount" v-else>¥{{priceRange.min}}-{{priceRange.max}}</text>
  145. </view>
  146. </view>
  147. <button class="submit-btn" @click="submitOrder">预约上门取件</button>
  148. </view>
  149. <view class="bottom-bar-divider"></view>
  150. </view>
  151. <!-- 明细弹窗遮罩和弹窗 -->
  152. <view v-if="showDetailPanel" class="detail-popup-mask" @click.self="toggleDetailPanel">
  153. <view class="detail-popup" @click.stop>
  154. <view class="detail-popup-close" @click="toggleDetailPanel">×</view>
  155. <view class="green-tip-bar popup-green-tip">
  156. 回收范围仅支持回收以上品类按件回收预计比称重回收多
  157. <text class="tip-highlight"> {{extraRecycleAmount.display}}</text>
  158. </view>
  159. <view class="panel-header">
  160. <text class="panel-title">已选商品明细</text>
  161. </view>
  162. <scroll-view class="panel-list popup-panel-list" scroll-y>
  163. <view v-for="(item, idx) in selectedProducts" :key="item.uniqueKey || idx" class="panel-item">
  164. <image v-if="item.image" :src="item.image" class="panel-item-img" mode="aspectFit" />
  165. <view class="panel-item-info">
  166. <text class="panel-item-name">{{item.name}}</text>
  167. <text class="panel-item-desc" v-if="item.brandName">品牌{{item.brandName}}</text>
  168. <text class="panel-item-desc" v-else>{{item.service}}</text>
  169. <text class="panel-item-price" v-if="!item.maxPrice || item.maxPrice == item.price">¥{{item.price}}/{{ item.unit || '' }}</text>
  170. <text class="panel-item-price" v-else>¥{{item.price}}-{{item.maxPrice}}/{{ item.unit || '' }}</text>
  171. </view>
  172. <view class="panel-quantity-control">
  173. <button class="btn-minus" @click="updateQuantityByProduct(item, -1)">-</button>
  174. <text class="quantity">{{item.quantity}}</text>
  175. <button class="btn-plus" @click="updateQuantityByProduct(item, 1)">+</button>
  176. </view>
  177. </view>
  178. </scroll-view>
  179. <view class="popup-bottom-bar">
  180. <view class="bottom-left">
  181. <view class="summary-row">
  182. <text class="summary-label">已选 <text class="summary-count">{{totalCount}}</text> {{ totalUnitText }} 预计回收可得</text>
  183. <uni-icons type="help" size="18" color="#b2b2b2" style="margin: 0 8rpx;" @tap="showPriceInfoPopups" />
  184. </view>
  185. <view class="amount-row">
  186. <uni-icons :type="showDetailPanel ? 'up' : 'down'" size="18" color="#5e5e5e" style="margin-right: 8rpx;vertical-align: middle;" @click="toggleDetailPanel" />
  187. <text class="amount" v-if="priceRange.min === priceRange.max">¥{{priceRange.min}}</text>
  188. <text class="amount" v-else>¥{{priceRange.min}}-{{priceRange.max}}</text>
  189. </view>
  190. </view>
  191. <button class="submit-btn" @click="submitOrder">预约上门取件</button>
  192. </view>
  193. </view>
  194. </view>
  195. <!-- 价格说明弹窗 -->
  196. <view v-if="showPriceInfoPopup" class="price-info-popup-mask" @click.self="closePriceInfoPopup">
  197. <view class="price-info-popup">
  198. <view class="price-info-popup-title">回收规则</view>
  199. <scroll-view class="price-info-popup-content" scroll-y>
  200. <!-- <view class="price-info-section">
  201. <view class="price-info-heading">关于旧衣质检</view>
  202. <view class="price-info-text">请确认本次回收旧衣是可以进行二次穿着的程度如回收旧衣有破损磨损开线变形起球发黄染色污渍配饰脱落或款式老旧等问题无法通过质检</view>
  203. </view>
  204. <view class="price-info-section">
  205. <view class="price-info-heading">质检报告</view>
  206. <view class="price-info-text">回收商收到衣后1-3个工作日内完成衣质检报告</view>
  207. </view>
  208. <view class="price-info-section">
  209. <view class="price-info-heading">质检结果与回收价格</view>
  210. <view class="price-info-text">若回收旧衣质检通过质检价格与用户提交订单时的预估价格一致回收商将按照预估价打款至您的小程序账户余额</view>
  211. </view> -->
  212. <uv-parse :content="huodong_text"></uv-parse>
  213. </scroll-view>
  214. <button class="price-info-popup-btn" @click="closePriceInfoPopup">我知道了</button>
  215. </view>
  216. </view>
  217. <!-- 根据角色显示不同的导航栏 -->
  218. <uv-tabbar
  219. v-if="ishow"
  220. :value="value"
  221. :fixed="true"
  222. @change="changeTo"
  223. class="uv-tabbar"
  224. >
  225. <uv-tabbar-item text="首页" >
  226. <template v-slot:active-icon>
  227. <image class="icon" src="/static/home/首页-点击.png"></image>
  228. </template>
  229. <template v-slot:inactive-icon>
  230. <image class="icon" src="/static/home/首页-未点击.png"></image>
  231. </template>
  232. </uv-tabbar-item>
  233. <uv-tabbar-item text="回收" >
  234. <template v-slot:active-icon>
  235. <image class="icon" src="/static/home/回收-点击.png"></image>
  236. </template>
  237. <template v-slot:inactive-icon>
  238. <image class="icon" src="/static/home/回收-未点击.png"></image>
  239. </template>
  240. </uv-tabbar-item>
  241. <uv-tabbar-item text="我的" >
  242. <template v-slot:active-icon>
  243. <image class="icon" src="/static/home/我的-点击.png"></image>
  244. </template>
  245. <template v-slot:inactive-icon>
  246. <image class="icon" src="/static/home/我的-未点击.png"></image>
  247. </template>
  248. </uv-tabbar-item>
  249. </uv-tabbar>
  250. <!-- 品牌索引弹窗 -->
  251. <view v-if="showBrandPopup" class="brand-popup-mask">
  252. <view class="brand-popup">
  253. <view class="brand-popup-header">
  254. <text class="brand-popup-close" @click="closeBrandPopup">关闭</text>
  255. <text class="brand-popup-title">可回收的品牌</text>
  256. </view>
  257. <view class="brand-popup-search">
  258. <input class="brand-search-input" v-model="brandSearch" placeholder="请输入要查询的内容" @input="onBrandSearchInput" />
  259. </view>
  260. <scroll-view class="brand-popup-list" scroll-y :scroll-into-view="scrollToView">
  261. <view v-for="letter in brandIndexList" :key="letter" :id="'brand-letter-' + letter">
  262. <view class="brand-letter">{{letter}}</view>
  263. <view v-for="brand in filteredBrandList.filter(b => b.letter === letter)" :key="brand.name" class="brand-item" @click="openBrandConfirm(brand)">
  264. <image :src="brand.logo" class="brand-logo" mode="aspectFit" />
  265. <text class="brand-name">{{brand.name}}</text>
  266. </view>
  267. </view>
  268. </scroll-view>
  269. <view class="brand-index-bar">
  270. <text v-for="letter in brandIndexList" :key="letter" :class="{active: currentLetter === letter}" @click="scrollToLetter(letter)">{{letter}}</text>
  271. </view>
  272. </view>
  273. </view>
  274. <!-- 回收规则弹窗 -->
  275. <view v-if="showRulePopup" class="rule-popup-mask" @click.self="handleRulePopupMaskClick">
  276. <view class="rule-popup">
  277. <view class="rule-popup-title">回收规则</view>
  278. <scroll-view class="rule-popup-content" scroll-y @scroll="onRuleContentScroll">
  279. <uv-parse :content="ruleHtml"></uv-parse>
  280. <view class="rule-content-bottom-indicator"></view>
  281. </scroll-view>
  282. <view v-if="!hasScrolledToBottom" class="scroll-tip">请滚动到底部阅读完整内容</view>
  283. <button class="rule-popup-btn" :class="{ disabled: !hasScrolledToBottom }" @click="closeRulePopup">我知道了</button>
  284. </view>
  285. </view>
  286. <!-- 预约上门取件弹窗 -->
  287. <view v-if="showPickupConfirm" class="pickup-confirm-mask">
  288. <view class="pickup-confirm-popup">
  289. <view class="pickup-confirm-title">温馨提示</view>
  290. <view class="pickup-confirm-content">
  291. <uv-parse :content="recycle_toast"></uv-parse>
  292. <!-- <view class="confirm-item confirm-item-red">1.当前回收快递免费上门由于快递成本较高为避免不必要的成本及资源二次浪费不属于可回收品类或不符合回收标准的物品请勿寄出</view>
  293. <view class="confirm-item">2.已通过的回收物品将正常结算不符合回收要求的物品可选择快递取回运费自付请在订单结算后48小时内联系在线客服安排取回逾期未联系将默认捐赠无法再次取回</view>
  294. <view class="confirm-item">3.若用户寄出大量不可回收的物品平台有权限制下次回收权限或取消下次包邮服务</view>
  295. <view class="confirm-item">4.对于合格率高的回收订单平台将根据实际情况给予额外回收奖励</view> -->
  296. </view>
  297. <view class="pickup-confirm-btn-row">
  298. <button class="pickup-confirm-btn" @click="handlePickupCancel">取消回收</button>
  299. <button class="pickup-confirm-btn agree" @click="handlePickupAgree">我同意</button>
  300. </view>
  301. </view>
  302. </view>
  303. <!-- 品牌确认弹窗 -->
  304. <view v-if="showBrandConfirm" class="brand-confirm-mask" @click.self="closeBrandConfirm">
  305. <view class="brand-confirm-popup">
  306. <view class="brand-confirm-title">品牌确认提示</view>
  307. <view class="brand-confirm-logo-wrap">
  308. <image :src="brandConfirmInfo.logo" class="brand-confirm-logo" mode="aspectFit" />
  309. </view>
  310. <view class="brand-confirm-name">{{ brandConfirmInfo.name }}</view>
  311. <view class="brand-confirm-desc">请确认所选品牌是否与实物品牌信息一致否则将无法进行回收</view>
  312. <view class="brand-confirm-btn-row">
  313. <button class="brand-confirm-btn retry" @click="closeBrandConfirm">重新选择</button>
  314. <button class="brand-confirm-btn confirm" @click="confirmBrand">确认一致</button>
  315. </view>
  316. </view>
  317. </view>
  318. <!-- 减少数量时的品牌选择弹窗 -->
  319. <view v-if="showBrandReducePopup" class="brand-reduce-popup-mask" @click.self="closeBrandReducePopup">
  320. <view class="brand-reduce-popup">
  321. <view class="brand-reduce-popup-header">
  322. <text class="brand-reduce-popup-close" @click="closeBrandReducePopup">关闭</text>
  323. <text class="brand-reduce-popup-title">选择要减少的品牌</text>
  324. </view>
  325. <scroll-view class="brand-reduce-popup-list" scroll-y>
  326. <view v-for="brand in reduceBrandList" :key="brand.brandId" class="brand-item" @click="selectReduceBrand(brand)">
  327. <image :src="brand.logo" class="brand-logo" mode="aspectFit" />
  328. <text class="brand-name">{{brand.name}}</text>
  329. </view>
  330. </scroll-view>
  331. </view>
  332. </view>
  333. </view>
  334. </template>
  335. <script>
  336. import tabBarMixin from '../mixins/tabBarMixin.js'
  337. import { pinyin } from '../../utils/pinyin.js'
  338. export default {
  339. mixins: [tabBarMixin],
  340. data() {
  341. return {
  342. value:1,
  343. ishow:true,
  344. // 动态数据
  345. allProducts: {}, // { [categoryId]: [商品数组] }
  346. allProductsPage: {}, // { [categoryId]: 当前已加载页码 }
  347. allProductsTotal: {}, // { [categoryId]: 总数 }
  348. pageSize: 10,
  349. currentCategory: 0,
  350. tabbarHeight: 0,
  351. videoPlayingStates: {}, // 记录每个视频的播放状态
  352. showDetailPanel: false,
  353. showBrandPopup: false,
  354. showRulePopup: false,
  355. ruleImgUrl: '/static/回收/回收规则.png',
  356. showPickupConfirm: false,
  357. showBrandConfirm: false,
  358. brandConfirmInfo: {
  359. logo: '',
  360. name: ''
  361. },
  362. brandList: [],
  363. brandCache: {}, // 为每个商品缓存品牌信息 { productId: [brandList] }
  364. brandIndexList: ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'],
  365. currentLetter: 'A',
  366. scrollToView: '',
  367. brandSearch: '',
  368. ruleHtml: '', // 回收规则富文本内容
  369. loadingMore: false,
  370. finished: false,
  371. pendingBrandIndex: null, // 记录待加一的品牌商品index
  372. showPriceInfoPopup: false,
  373. isWaitingForBrandSelection: false, // 等待品牌选择的标志
  374. showBrandReducePopup: false, // 显示减少数量时的品牌选择弹窗
  375. reduceItem: null, // 待减少数量的商品
  376. reduceBrandList: [], // 可减少的品牌列表
  377. viewedRuleItems: new Set(), // 已查看过规则的商品ID集合
  378. loadOptions: null, // 保存options参数
  379. userInfo: null, // 用户信息
  380. isUserBlacklisted: false, // 用户是否被拉黑
  381. currentProductId: null, // 当前查看品牌的商品ID
  382. searchTimer: null, // 搜索防抖定时器
  383. hasScrolledToBottom: false, // 是否已滚动到底部
  384. }
  385. },
  386. computed: {
  387. recycle_banner() {
  388. const item = getApp().globalData.configData.find(i => i.keyName === 'recycle_banner')
  389. return item ? item.keyContent : ''
  390. },
  391. recycle_toast() {
  392. const item = getApp().globalData.configData.find(i => i.keyName === 'recycle_toast')
  393. return item ? item.keyContent : ''
  394. },
  395. huodong_text() {
  396. const item = getApp().globalData.configData.find(i => i.keyName === 'huodong_text')
  397. return item ? item.keyContent : ''
  398. },
  399. // 当前分类的商品列表
  400. recycleList() {
  401. const currentCategoryId = this.categories[this.currentCategory]?.id
  402. return this.allProducts[currentCategoryId] || []
  403. },
  404. // 计算总数量
  405. totalCount() {
  406. return Object.values(this.allProducts).reduce((total, categoryItems) => {
  407. return total + categoryItems.reduce((sum, item) => {
  408. // 如果商品有品牌数量,汇总所有品牌的数量
  409. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  410. return sum + Object.values(item.brandQuantities).reduce((brandSum, qty) => brandSum + qty, 0)
  411. }
  412. // 否则使用原来的quantity字段
  413. return sum + (item.quantity || 0)
  414. }, 0)
  415. }, 0)
  416. },
  417. // 计算总单位文本
  418. totalUnitText() {
  419. // 获取所有已选商品的单位
  420. const units = new Set()
  421. Object.values(this.allProducts).forEach(categoryItems => {
  422. categoryItems.forEach(item => {
  423. let hasQuantity = false
  424. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  425. hasQuantity = Object.values(item.brandQuantities).some(qty => qty > 0)
  426. } else {
  427. hasQuantity = (item.quantity || 0) > 0
  428. }
  429. if (hasQuantity) {
  430. units.add(item.unit || '件')
  431. }
  432. })
  433. })
  434. // 如果只有一种单位,显示单位;如果混合多种单位,显示"项"
  435. if (units.size === 0) return '件'
  436. if (units.size === 1) return Array.from(units)[0]
  437. return '项'
  438. },
  439. // 计算总价格范围
  440. totalPriceRange() {
  441. const result = Object.values(this.allProducts).reduce((categoryTotal, categoryItems) => {
  442. return categoryItems.reduce((sum, item) => {
  443. let itemQuantity = 0
  444. // 如果商品有品牌数量,汇总所有品牌的数量
  445. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  446. itemQuantity = Object.values(item.brandQuantities).reduce((brandSum, qty) => brandSum + qty, 0)
  447. } else {
  448. itemQuantity = item.quantity || 0
  449. }
  450. if (itemQuantity > 0) {
  451. const minPrice = Number(item.price) || 0
  452. const maxPrice = Number(item.maxPrice) || Number(item.price) || 0
  453. sum.min += itemQuantity * minPrice
  454. sum.max += itemQuantity * maxPrice
  455. }
  456. return sum
  457. }, categoryTotal)
  458. }, { min: 0, max: 0 })
  459. return {
  460. min: result.min.toFixed(1),
  461. max: result.max.toFixed(1)
  462. }
  463. },
  464. // 计算总价格 (保持兼容性,使用最低价格)
  465. totalPrice() {
  466. return this.totalPriceRange.min
  467. },
  468. // 计算价格范围
  469. priceRange() {
  470. if (this.totalCount === 0) {
  471. return {
  472. min: '0.0',
  473. max: '0.0'
  474. }
  475. }
  476. return this.totalPriceRange
  477. },
  478. selectedProducts() {
  479. // 返回所有分类下所有已选商品,按品牌分组
  480. const products = []
  481. Object.values(this.allProducts).flat().forEach(item => {
  482. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  483. // 按品牌分别添加
  484. Object.entries(item.brandQuantities).forEach(([brandId, quantity]) => {
  485. if (quantity > 0) {
  486. const brandInfo = this.getBrandInfo(brandId)
  487. products.push({
  488. ...item,
  489. quantity: quantity,
  490. brandId: brandId,
  491. brandName: brandInfo ? brandInfo.name : '未知品牌',
  492. brandImage: brandInfo ? brandInfo.logo : '',
  493. uniqueKey: `${item.id}_${brandId}` // 用于区分同商品不同品牌
  494. })
  495. }
  496. })
  497. } else if (item.quantity > 0) {
  498. // 没有品牌的商品
  499. products.push({
  500. ...item,
  501. uniqueKey: item.id
  502. })
  503. }
  504. })
  505. return products
  506. },
  507. filteredBrandList() {
  508. return this.brandList
  509. },
  510. bannerList() {
  511. return getApp().globalData.bannerList || []
  512. },
  513. categories() {
  514. const list = getApp().globalData.pricePreviewList || []
  515. return list.filter(item => item.pid === '0').sort((a, b) => a.sort - b.sort)
  516. },
  517. minMoney() {
  518. const config = getApp().globalData.configData || [];
  519. const item = config.find(i => i.keyName === 'min_money');
  520. return item ? parseFloat(item.keyContent) : 0;
  521. },
  522. // 计算比预计回收多的金额(商品回收价格 - (商品回收价格 * 0.066))
  523. extraRecycleAmount() {
  524. const minPrice = parseFloat(this.priceRange.min) || 0;
  525. const maxPrice = parseFloat(this.priceRange.max) || 0;
  526. if (minPrice === 0 && maxPrice === 0) {
  527. return {
  528. min: '0.00',
  529. max: '0.00',
  530. display: '0.00元'
  531. };
  532. }
  533. // 计算减去6.6%后的金额
  534. const minExtra = minPrice - (minPrice * 0.066);
  535. const maxExtra = maxPrice - (maxPrice * 0.066);
  536. // 如果最小值和最大值相等,显示单个值
  537. if (minPrice === maxPrice) {
  538. return {
  539. min: minExtra.toFixed(2),
  540. max: maxExtra.toFixed(2),
  541. display: `${minExtra.toFixed(2)}`
  542. };
  543. }
  544. // 显示区间
  545. return {
  546. min: minExtra.toFixed(2),
  547. max: maxExtra.toFixed(2),
  548. display: `${minExtra.toFixed(2)}-${maxExtra.toFixed(2)}`
  549. };
  550. }
  551. },
  552. methods: {
  553. showPriceInfoPopups() {
  554. console.log('showPriceInfoPopup called');
  555. this.showPriceInfoPopup = true
  556. },
  557. closePriceInfoPopup() {
  558. console.log('closePriceInfoPopup called');
  559. this.showPriceInfoPopup = false
  560. },
  561. changeTo(e){
  562. this.value = e
  563. if(e==2){
  564. uni.reLaunch({
  565. url: '/pages/component/my'
  566. });
  567. }else if(e==0){
  568. console.log(e,'111')
  569. uni.reLaunch({
  570. url: '/pages/component/home'
  571. });
  572. }
  573. },
  574. fetchGoodsList(categoryId, page = 1, callback) {
  575. this.$api('getClassGoodsList', {
  576. classId: categoryId,
  577. pageNo: page,
  578. pageSize: this.pageSize
  579. }, res => {
  580. if (res.code === 200 && res.result && Array.isArray(res.result.records)) {
  581. const oldList = this.allProducts[categoryId] || []
  582. const newList = page === 1 ? res.result.records : oldList.concat(res.result.records)
  583. this.$set(this.allProducts, categoryId, newList)
  584. this.$set(this.allProductsPage, categoryId, page)
  585. this.$set(this.allProductsTotal, categoryId, res.result.total)
  586. }
  587. if (callback) callback()
  588. })
  589. },
  590. // 获取分类商品总数
  591. getCategoryItemCount(index) {
  592. const categoryId = this.categories[index]?.id
  593. const categoryItems = this.allProducts[categoryId] || []
  594. return categoryItems.reduce((sum, item) => {
  595. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  596. return sum + Object.values(item.brandQuantities).reduce((brandSum, qty) => brandSum + qty, 0)
  597. }
  598. return sum + (item.quantity || 0)
  599. }, 0)
  600. },
  601. // 切换分类
  602. switchCategory(index) {
  603. this.currentCategory = index
  604. this.loadingMore = false
  605. this.finished = false
  606. const categoryId = this.categories[index]?.id
  607. // console.log(categoryId,'switchCategory')
  608. if (!this.allProducts[categoryId]) {
  609. this.fetchGoodsList(categoryId, 1)
  610. }
  611. },
  612. // 更新商品数量
  613. updateQuantity(index, delta) {
  614. const categoryId = this.categories[this.currentCategory]?.id
  615. const item = this.allProducts[categoryId]?.[index]
  616. if (!item) return
  617. // 如果是减少数量且delta为负数
  618. if (delta < 0) {
  619. // 检查是否有多个品牌
  620. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 1) {
  621. // 有多个品牌,显示选择弹窗
  622. this.reduceItem = { item, index, delta }
  623. this.reduceBrandList = Object.entries(item.brandQuantities)
  624. .filter(([brandId, quantity]) => quantity > 0)
  625. .map(([brandId, quantity]) => {
  626. const brandInfo = this.getBrandInfo(brandId)
  627. return {
  628. brandId,
  629. quantity,
  630. name: brandInfo ? brandInfo.name : '未知品牌',
  631. logo: brandInfo ? brandInfo.logo : ''
  632. }
  633. })
  634. this.showBrandReducePopup = true
  635. return
  636. } else if (item.brandQuantities && Object.keys(item.brandQuantities).length === 1) {
  637. // 只有一个品牌,直接减少
  638. const brandId = Object.keys(item.brandQuantities)[0]
  639. const currentQty = item.brandQuantities[brandId] || 0
  640. const newQty = Math.max(0, currentQty + delta)
  641. this.$set(item.brandQuantities, brandId, newQty)
  642. // 如果数量为0,删除该品牌
  643. if (newQty === 0) {
  644. this.$delete(item.brandQuantities, brandId)
  645. }
  646. return
  647. } else {
  648. // 没有品牌数量,使用原来的逻辑
  649. let newQuantity = (item.quantity || 0) + delta
  650. if (newQuantity < 0) newQuantity = 0
  651. this.$set(item, 'quantity', newQuantity)
  652. return
  653. }
  654. }
  655. // 品牌商品且数量为0且加一时
  656. if ((item.quantity || 0) === 0 && delta > 0) {
  657. this.pendingBrandIndex = index
  658. this.isWaitingForBrandSelection = true;
  659. this.showRules(item); // 先显示回收规则
  660. return
  661. }
  662. },
  663. // 选择要减少的品牌
  664. selectReduceBrand(brandInfo) {
  665. const { item, index, delta } = this.reduceItem
  666. const currentQty = item.brandQuantities[brandInfo.brandId] || 0
  667. const newQty = Math.max(0, currentQty + delta)
  668. this.$set(item.brandQuantities, brandInfo.brandId, newQty)
  669. // 如果数量为0,删除该品牌
  670. if (newQty === 0) {
  671. this.$delete(item.brandQuantities, brandInfo.brandId)
  672. }
  673. this.closeBrandReducePopup()
  674. },
  675. // 关闭减少品牌选择弹窗
  676. closeBrandReducePopup() {
  677. this.showBrandReducePopup = false
  678. this.reduceItem = null
  679. this.reduceBrandList = []
  680. },
  681. // 获取品牌信息
  682. getBrandInfo(brandId) {
  683. // 从所有商品的品牌缓存中查找品牌信息
  684. for (const productId in this.brandCache) {
  685. const brandInfo = this.brandCache[productId].find(brand => brand.id === brandId)
  686. if (brandInfo) {
  687. return brandInfo
  688. }
  689. }
  690. // 如果缓存中没有找到,也从当前brandList中查找(兼容性)
  691. return this.brandList.find(brand => brand.id === brandId)
  692. },
  693. // 获取商品的总数量(所有品牌)
  694. getItemTotalQuantity(item) {
  695. if (item.brandQuantities && Object.keys(item.brandQuantities).length > 0) {
  696. return Object.values(item.brandQuantities).reduce((sum, qty) => sum + qty, 0)
  697. }
  698. return item.quantity || 0
  699. },
  700. // 显示回收规则
  701. showRules(item) {
  702. // 检查该商品是否已经查看过规则
  703. if (this.viewedRuleItems.has(item.id)) {
  704. // 如果已经查看过,直接跳过规则弹窗,进入品牌选择
  705. this.isWaitingForBrandSelection = false;
  706. this.getGoodsBrandList(item.id);
  707. this.showBrandPopup = true;
  708. return;
  709. }
  710. // 重置滚动状态
  711. this.hasScrolledToBottom = false;
  712. // 获取回收规则富文本
  713. this.$api('getGoodsRecycleRule', { goodsId: item.id }, res => {
  714. if (res.code === 200 && res.result) {
  715. this.ruleHtml = res.result
  716. } else {
  717. this.ruleHtml = '<p>暂无回收规则</p>'
  718. }
  719. this.showRulePopup = true
  720. })
  721. },
  722. showMore() {
  723. uni.showToast({
  724. title: '更多规则请咨询客服',
  725. icon: 'none'
  726. })
  727. },
  728. submitOrder() {
  729. // 检查用户是否被拉入黑名单
  730. if (this.isUserBlacklisted) {
  731. uni.showModal({
  732. title: '提示',
  733. content: '您的账户已被限制使用回收服务,如有疑问请联系客服。',
  734. showCancel: false,
  735. confirmText: '我知道了'
  736. })
  737. return
  738. }
  739. if (this.totalCount < 3 || Number(this.totalPrice) < this.minMoney) {
  740. uni.showToast({
  741. title: `各品类混合需要满3${this.totalUnitText}并且各品类混合需要满${this.minMoney}元才能回收哦`,
  742. icon: 'none'
  743. })
  744. return
  745. }
  746. this.showPickupConfirm = true;
  747. },
  748. handlePickupCancel() {
  749. this.showPickupConfirm = false;
  750. },
  751. handlePickupAgree() {
  752. this.showPickupConfirm = false;
  753. uni.showLoading({
  754. title: '提交中...'
  755. })
  756. setTimeout(() => {
  757. uni.hideLoading()
  758. uni.showToast({
  759. title: '预约成功',
  760. icon: 'success'
  761. })
  762. this.goToPickup()
  763. }, 1500)
  764. },
  765. goToPickup() {
  766. // 获取所有选中的衣物(所有分类)
  767. const selectedItems = this.selectedProducts.map(item => {
  768. const baseItem = {
  769. id: item.id,
  770. name: item.name,
  771. icon: item.image,
  772. quantity: item.quantity,
  773. unitPrice: item.price,
  774. maxPrice: item.maxPrice,
  775. desc: item.brandName ? `品牌:${item.brandName}` : '允许脏破烂,160码以上'
  776. }
  777. // 如果有品牌信息,添加品牌相关字段
  778. if (item.brandId) {
  779. baseItem.brandId = item.brandId
  780. baseItem.brandName = item.brandName
  781. baseItem.brandImage = item.brandImage
  782. }
  783. return baseItem
  784. })
  785. const itemsStr = encodeURIComponent(JSON.stringify(selectedItems))
  786. uni.navigateTo({
  787. url: `/pages/subcomponent/pickup?fromRecycle=true&items=${itemsStr}`
  788. })
  789. },
  790. checkBrand(index) {
  791. const categoryId = this.categories[this.currentCategory]?.id
  792. const item = this.allProducts[categoryId]?.[index]
  793. if (item?.id) {
  794. this.pendingBrandIndex = index
  795. this.getGoodsBrandList(item.id)
  796. this.showBrandPopup = true
  797. }
  798. },
  799. closeBrandPopup() {
  800. this.showBrandPopup = false
  801. // 如果用户取消品牌选择,重置状态
  802. this.pendingBrandIndex = null;
  803. this.isWaitingForBrandSelection = false;
  804. // 清理搜索状态
  805. this.brandSearch = ''
  806. this.currentProductId = null
  807. if (this.searchTimer) {
  808. clearTimeout(this.searchTimer)
  809. this.searchTimer = null
  810. }
  811. },
  812. scrollToLetter(letter) {
  813. this.currentLetter = letter
  814. this.scrollToView = 'brand-letter-' + letter
  815. },
  816. // 添加下拉刷新方法
  817. async refreshData() {
  818. try {
  819. // 这里可以添加刷新数据的逻辑,比如重新获取商品列表等
  820. // 示例:重新初始化数据
  821. this.currentCategory = 0
  822. Object.values(this.allProducts).forEach(categoryItems => {
  823. categoryItems.forEach(item => {
  824. item.quantity = 0
  825. if (item.brandQuantities) {
  826. item.brandQuantities = {}
  827. }
  828. })
  829. })
  830. // 清空已查看规则的记录
  831. this.viewedRuleItems.clear()
  832. // 清空品牌缓存
  833. this.brandCache = {}
  834. // 模拟网络请求延迟
  835. await new Promise(resolve => setTimeout(resolve, 1000))
  836. uni.showToast({
  837. title: '刷新成功',
  838. icon: 'success'
  839. })
  840. } catch (error) {
  841. uni.showToast({
  842. title: '刷新失败',
  843. icon: 'none'
  844. })
  845. } finally {
  846. // 停止下拉刷新动画
  847. uni.stopPullDownRefresh()
  848. }
  849. },
  850. toggleDetailPanel() {
  851. this.showDetailPanel = !this.showDetailPanel
  852. },
  853. fetchUserInfo() {
  854. if(uni.getStorageSync('token')){
  855. this.login_status = getApp().globalData.login_status;
  856. this.$api("getUserByToken",{},(res)=>{
  857. if(res.code == 200){
  858. this.userInfo = res.result
  859. // 检查用户是否被拉入黑名单
  860. this.isUserBlacklisted = res.result.isBlack === 'Y'
  861. // isTuiType 为0用户,1推广达人,2推广大使
  862. }
  863. })
  864. } else {
  865. this.login_status = false;
  866. }
  867. },
  868. updateQuantityByProduct(item, delta) {
  869. // 在明细弹窗中更新数量
  870. if (item.brandId) {
  871. // 有品牌ID的商品
  872. const originalItem = this.findOriginalItem(item.id)
  873. if (originalItem && originalItem.brandQuantities) {
  874. const currentQty = originalItem.brandQuantities[item.brandId] || 0
  875. const newQty = Math.max(0, currentQty + delta)
  876. this.$set(originalItem.brandQuantities, item.brandId, newQty)
  877. // 如果数量为0,删除该品牌
  878. if (newQty === 0) {
  879. this.$delete(originalItem.brandQuantities, item.brandId)
  880. }
  881. // 同步更新显示的数量
  882. item.quantity = newQty
  883. }
  884. } else {
  885. // 没有品牌的商品
  886. if (!item.quantity) item.quantity = 0
  887. item.quantity += delta
  888. if (item.quantity < 0) item.quantity = 0
  889. // 同步到原商品
  890. const originalItem = this.findOriginalItem(item.id)
  891. if (originalItem) {
  892. this.$set(originalItem, 'quantity', item.quantity)
  893. }
  894. }
  895. },
  896. // 查找原始商品对象
  897. findOriginalItem(itemId) {
  898. for (const categoryItems of Object.values(this.allProducts)) {
  899. const item = categoryItems.find(i => i.id === itemId)
  900. if (item) return item
  901. }
  902. return null
  903. },
  904. openRulePopup() {
  905. this.showRulePopup = true
  906. },
  907. closeRulePopup() {
  908. // 如果还没有滚动到底部,显示提示
  909. if (!this.hasScrolledToBottom) {
  910. uni.showToast({
  911. title: '请阅读完整回收规则',
  912. icon: 'none'
  913. })
  914. return
  915. }
  916. this.showRulePopup = false;
  917. this.hasScrolledToBottom = false; // 重置滚动状态
  918. // 如果是在等待品牌选择的状态下关闭规则弹窗,则接着打开品牌选择
  919. if (this.isWaitingForBrandSelection) {
  920. this.isWaitingForBrandSelection = false; // 清除等待状态
  921. const categoryId = this.categories[this.currentCategory]?.id;
  922. const item = this.allProducts[categoryId]?.[this.pendingBrandIndex];
  923. // 记录该商品的规则已被查看
  924. this.viewedRuleItems.add(item.id);
  925. this.getGoodsBrandList(item.id);
  926. this.showBrandPopup = true; // 打开品牌索引弹窗
  927. }
  928. },
  929. // 处理规则弹窗遮罩点击
  930. handleRulePopupMaskClick() {
  931. if (!this.hasScrolledToBottom) {
  932. uni.showToast({
  933. title: '请阅读完整回收规则',
  934. icon: 'none'
  935. })
  936. return
  937. }
  938. this.closeRulePopup()
  939. },
  940. // 监听规则内容滚动
  941. onRuleContentScroll(e) {
  942. const { scrollTop, scrollHeight, deltaY } = e.detail
  943. const containerHeight = 500 // 大概的容器高度,单位rpx转换后的px
  944. // 判断是否滚动到底部(留一些余量)
  945. if (scrollTop + containerHeight >= scrollHeight - 20) {
  946. this.hasScrolledToBottom = true
  947. }
  948. },
  949. loadMoreGoods() {
  950. const categoryId = this.categories[this.currentCategory]?.id
  951. const page = (this.allProductsPage[categoryId] || 1) + 1
  952. const total = this.allProductsTotal[categoryId] || 0
  953. const loaded = (this.allProducts[categoryId] || []).length
  954. if (this.loadingMore || this.finished) return
  955. if (loaded < total) {
  956. this.loadingMore = true
  957. this.fetchGoodsList(categoryId, page, () => {
  958. this.loadingMore = false
  959. // 判断是否加载完
  960. const newLoaded = (this.allProducts[categoryId] || []).length
  961. this.finished = newLoaded >= (this.allProductsTotal[categoryId] || 0)
  962. })
  963. } else {
  964. this.finished = true
  965. }
  966. },
  967. openBrandConfirm(brand) {
  968. this.brandConfirmInfo = {
  969. id: brand.id,
  970. logo: brand.logo,
  971. name: brand.name
  972. }
  973. this.showBrandConfirm = true
  974. },
  975. closeBrandConfirm() {
  976. this.showBrandConfirm = false
  977. },
  978. confirmBrand() {
  979. this.showBrandConfirm = false
  980. this.showBrandPopup = false
  981. // 确认后将待加一的商品数量+1,并记录品牌ID
  982. if (this.pendingBrandIndex !== null) {
  983. const categoryId = this.categories[this.currentCategory]?.id
  984. const item = this.allProducts[categoryId]?.[this.pendingBrandIndex]
  985. if (item) {
  986. // 初始化品牌数量对象
  987. if (!item.brandQuantities) {
  988. this.$set(item, 'brandQuantities', {})
  989. }
  990. // 增加该品牌的数量
  991. const currentQty = item.brandQuantities[this.brandConfirmInfo.id] || 0
  992. this.$set(item.brandQuantities, this.brandConfirmInfo.id, currentQty + 1)
  993. // 清除原来的quantity(如果存在)
  994. if (item.quantity) {
  995. this.$set(item, 'quantity', 0)
  996. }
  997. }
  998. this.pendingBrandIndex = null
  999. }
  1000. },
  1001. getGoodsBrandList(productId, searchName = '') {
  1002. this.currentProductId = productId
  1003. const params = { productId }
  1004. if (searchName.trim()) {
  1005. params.name = searchName.trim()
  1006. }
  1007. this.$api('getGoodsBrandList', params, res => {
  1008. // console.log(res,'res')
  1009. if (res && res.success && res.result && res.result.records) {
  1010. const brandList = res.result.records.map(item => {
  1011. // 获取品牌名称的拼音首字母
  1012. const firstChar = this.getPinyinFirstLetter(item.name)
  1013. return {
  1014. id: item.id,
  1015. logo: item.image || '/static/brand/alexander.png',
  1016. name: item.name,
  1017. letter: firstChar
  1018. }
  1019. })
  1020. // 缓存到对应商品的品牌列表中
  1021. this.$set(this.brandCache, productId, brandList)
  1022. // 同时更新当前显示的brandList(用于弹窗显示)
  1023. this.brandList = brandList
  1024. // console.log(this.brandList,'this.brandList')
  1025. }
  1026. })
  1027. },
  1028. // 获取中文拼音首字母
  1029. getPinyinFirstLetter(str) {
  1030. if (!str) return '#'
  1031. const firstChar = str.charAt(0)
  1032. // 遍历pinyin对象,查找包含该汉字的拼音
  1033. for (let key in pinyin) {
  1034. const chars = pinyin[key]
  1035. if (chars && chars.indexOf(firstChar) !== -1) {
  1036. return key.charAt(0).toUpperCase()
  1037. }
  1038. }
  1039. let index = this.brandIndexList.indexOf(firstChar.toUpperCase())
  1040. if (index != -1) {
  1041. return this.brandIndexList[index]
  1042. }
  1043. return '#'
  1044. },
  1045. // 品牌搜索输入事件处理
  1046. onBrandSearchInput(e) {
  1047. const searchValue = e.detail.value
  1048. // 清除之前的定时器
  1049. if (this.searchTimer) {
  1050. clearTimeout(this.searchTimer)
  1051. }
  1052. // 设置防抖,500ms后执行搜索
  1053. this.searchTimer = setTimeout(() => {
  1054. if (this.currentProductId) {
  1055. this.getGoodsBrandList(this.currentProductId, searchValue)
  1056. }
  1057. }, 500)
  1058. },
  1059. // 视频相关方法
  1060. playVideoFullscreen(item, index) {
  1061. if (!this.videoPlayingStates[index]) {
  1062. // 第一次点击:显示视频并开始播放
  1063. this.$set(this.videoPlayingStates, index, true);
  1064. // 等待视频元素渲染后再进行操作
  1065. this.$nextTick(() => {
  1066. setTimeout(() => {
  1067. const videoContext = uni.createVideoContext(`recycle-video-${index}`, this);
  1068. if (videoContext) {
  1069. // 直接进入全屏播放
  1070. videoContext.requestFullScreen({
  1071. direction: -1 // 自动选择方向
  1072. });
  1073. }
  1074. }, 200);
  1075. });
  1076. }
  1077. },
  1078. onVideoPlay(index) {
  1079. this.$set(this.videoPlayingStates, index, true);
  1080. },
  1081. onVideoPause(index) {
  1082. this.$set(this.videoPlayingStates, index, false);
  1083. },
  1084. onVideoEnded(index) {
  1085. // 视频播放结束,回到预览状态
  1086. this.$set(this.videoPlayingStates, index, false);
  1087. },
  1088. onFullscreenChange(e) {
  1089. console.log('全屏状态改变:', e.detail);
  1090. const videoIndex = e.target.id.replace('recycle-video-', '');
  1091. if (e.detail.fullScreen) {
  1092. // 进入全屏时,不做任何操作
  1093. console.log('进入全屏模式,方向:', e.detail.direction);
  1094. } else {
  1095. // 退出全屏时,回到预览状态
  1096. console.log('退出全屏模式,回到预览状态');
  1097. this.$set(this.videoPlayingStates, videoIndex, false);
  1098. }
  1099. },
  1100. // 初始化页面数据的方法
  1101. initializePageData() {
  1102. const options = this.loadOptions || {}
  1103. if (options && options.categoryId) {
  1104. const idx = this.categories.findIndex(c => c.id == options.categoryId)
  1105. if (idx !== -1) this.currentCategory = idx
  1106. }
  1107. if (this.categories.length > 0) {
  1108. this.fetchGoodsList(this.categories[this.currentCategory].id, 1)
  1109. }
  1110. uni.$on('bannerListUpdated', () => {
  1111. this.$forceUpdate && this.$forceUpdate()
  1112. })
  1113. if (getApp().globalData.bannerList && getApp().globalData.bannerList.length > 0) {
  1114. this.$forceUpdate && this.$forceUpdate()
  1115. }
  1116. // 检查全局清空标志(兼容 reLaunch)
  1117. if (getApp().globalData.shouldClearRecycle) {
  1118. Object.values(this.allProducts).forEach(categoryItems => {
  1119. categoryItems.forEach(item => {
  1120. this.$set(item, 'quantity', 0)
  1121. if (item.brandQuantities) {
  1122. this.$set(item, 'brandQuantities', {})
  1123. }
  1124. })
  1125. })
  1126. // 清空已查看规则的记录
  1127. this.viewedRuleItems.clear()
  1128. this.showDetailPanel = false
  1129. this.brandCache = {} // 清空品牌缓存
  1130. this.$forceUpdate()
  1131. getApp().globalData.shouldClearRecycle = false
  1132. }
  1133. },
  1134. },
  1135. created() {
  1136. this.currentCategory = 0
  1137. this.$nextTick(() => {
  1138. if (this.categories.length > 0) {
  1139. const firstCategoryId = this.categories[0]?.id
  1140. if (firstCategoryId) {
  1141. this.fetchGoodsList(firstCategoryId, 1)
  1142. }
  1143. }
  1144. })
  1145. },
  1146. mounted() {
  1147. this.$nextTick(() => {
  1148. const query = uni.createSelectorQuery().in(this)
  1149. query.select('.uv-tabbar').boundingClientRect(rect => {
  1150. if (rect && rect.height) {
  1151. this.tabbarHeight = rect.height
  1152. } else {
  1153. this.tabbarHeight = uni.upx2px ? uni.upx2px(95) : 45
  1154. }
  1155. // console.log(this.tabbarHeight,'tabbarHeight')
  1156. }).exec()
  1157. })
  1158. },
  1159. onLoad(options) {
  1160. // 保存options参数
  1161. this.loadOptions = options
  1162. // 检查App数据是否已经准备好
  1163. if (!getApp().globalData.isAppDataReady) {
  1164. // 显示加载状态
  1165. uni.showLoading({
  1166. title: '加载中...',
  1167. mask: true
  1168. })
  1169. // 监听App数据准备完成事件
  1170. uni.$on('appDataReady', () => {
  1171. uni.hideLoading()
  1172. this.initializePageData()
  1173. })
  1174. } else {
  1175. // App数据已经准备好,直接初始化页面数据
  1176. this.initializePageData()
  1177. }
  1178. },
  1179. onUnload() {
  1180. uni.$off('bannerListUpdated')
  1181. // 移除事件监听
  1182. uni.$off('clearRecycleOrderData')
  1183. },
  1184. onShow() {
  1185. // 获取用户信息,检查黑名单状态
  1186. this.fetchUserInfo()
  1187. const id = getApp().globalData.targetRecycleCategoryId
  1188. if (id) {
  1189. const trySwitch = () => {
  1190. if (this.categories.length > 0) {
  1191. const idx = this.categories.findIndex(c => String(c.id) === String(id))
  1192. if (idx !== -1) {
  1193. this.currentCategory = idx
  1194. const categoryId = this.categories[idx]?.id
  1195. if (categoryId && !this.allProducts[categoryId]) {
  1196. this.loadingMore = false
  1197. this.finished = false
  1198. this.fetchGoodsList(categoryId, 1)
  1199. }
  1200. }
  1201. getApp().globalData.targetRecycleCategoryId = null
  1202. } else {
  1203. setTimeout(trySwitch, 100)
  1204. }
  1205. }
  1206. trySwitch()
  1207. }
  1208. // 检查全局清空标志
  1209. if (getApp().globalData.shouldClearRecycle) {
  1210. Object.values(this.allProducts).forEach(categoryItems => {
  1211. categoryItems.forEach(item => {
  1212. this.$set(item, 'quantity', 0)
  1213. if (item.brandQuantities) {
  1214. this.$set(item, 'brandQuantities', {})
  1215. }
  1216. })
  1217. })
  1218. // 清空已查看规则的记录
  1219. this.viewedRuleItems.clear()
  1220. this.showDetailPanel = false
  1221. this.brandCache = {} // 清空品牌缓存
  1222. this.$forceUpdate()
  1223. getApp().globalData.shouldClearRecycle = false
  1224. }
  1225. // 监听清除订单数据的事件
  1226. uni.$on('clearRecycleOrderData', () => {
  1227. // 清除所有商品的选中数量,保证响应式
  1228. Object.values(this.allProducts).forEach(categoryItems => {
  1229. categoryItems.forEach(item => {
  1230. this.$set(item, 'quantity', 0)
  1231. if (item.brandQuantities) {
  1232. this.$set(item, 'brandQuantities', {})
  1233. }
  1234. })
  1235. })
  1236. // 清空已查看规则的记录
  1237. this.viewedRuleItems.clear()
  1238. // 清空品牌缓存
  1239. this.brandCache = {}
  1240. // 重置其他相关数据
  1241. this.showDetailPanel = false
  1242. this.$forceUpdate()
  1243. })
  1244. },
  1245. watch: {
  1246. categories(newVal) {
  1247. const id = getApp().globalData.targetRecycleCategoryId
  1248. const idx = newVal.findIndex(c => String(c.id) === String(id))
  1249. if (id && newVal.length > 0 && idx !== -1) {
  1250. this.currentCategory = idx
  1251. getApp().globalData.targetRecycleCategoryId = null
  1252. // 自动加载右侧商品
  1253. const categoryId = newVal[idx]?.id
  1254. if (categoryId && !this.allProducts[categoryId]) {
  1255. this.loadingMore = false
  1256. this.finished = false
  1257. this.fetchGoodsList(categoryId, 1)
  1258. }
  1259. }
  1260. }
  1261. },
  1262. }
  1263. </script>
  1264. <style lang="scss" scoped>
  1265. .container {
  1266. display: flex;
  1267. flex-direction: column;
  1268. height: 100vh;
  1269. background-color: #f5f5f5;
  1270. overflow: hidden;
  1271. }
  1272. .banner {
  1273. background: linear-gradient(135deg, #ff9500,#ff5e00);
  1274. position: relative;
  1275. height: 350rpx;
  1276. z-index: 1;
  1277. image {
  1278. width: 100%;
  1279. height: 90%;
  1280. }
  1281. .video-container {
  1282. position: relative;
  1283. width: 100%;
  1284. height: 100%;
  1285. .video-poster {
  1286. cursor: pointer;
  1287. transition: transform 0.2s ease;
  1288. &:active {
  1289. transform: scale(0.98);
  1290. }
  1291. }
  1292. .video-overlay {
  1293. position: absolute;
  1294. top: 0;
  1295. left: 0;
  1296. right: 0;
  1297. bottom: 0;
  1298. background: rgba(0, 0, 0, 0.4);
  1299. z-index: 10;
  1300. cursor: pointer;
  1301. display: flex;
  1302. justify-content: center;
  1303. align-items: center;
  1304. .play-button-large {
  1305. width: 120rpx;
  1306. height: 120rpx;
  1307. background: transparent;
  1308. border-radius: 50%;
  1309. display: flex;
  1310. justify-content: center;
  1311. align-items: center;
  1312. backdrop-filter: blur(10rpx);
  1313. transition: all 0.3s ease;
  1314. box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.3);
  1315. &:active {
  1316. transform: scale(0.9);
  1317. background: rgba(255, 255, 255, 0.1);
  1318. }
  1319. .play-triangle {
  1320. width: 0;
  1321. height: 0;
  1322. border-left: 24rpx solid #fff;
  1323. border-top: 18rpx solid transparent;
  1324. border-bottom: 18rpx solid transparent;
  1325. margin-left: 8rpx;
  1326. transition: all 0.3s ease;
  1327. filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3));
  1328. }
  1329. }
  1330. &:hover .play-button-large {
  1331. transform: scale(1.1);
  1332. box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4);
  1333. .play-triangle {
  1334. border-left-color: #ff8917;
  1335. }
  1336. }
  1337. }
  1338. }
  1339. }
  1340. .goods-list {
  1341. // flex: 1;
  1342. display: flex;
  1343. position: relative;
  1344. height: calc(110vh - 320rpx - 160rpx - env(safe-area-inset-bottom)); /* 减去banner、底部栏和绿色提示条的高度 */
  1345. margin-top: -10rpx;
  1346. z-index: 2;
  1347. border-radius: 20rpx 20rpx 0 0;
  1348. overflow: hidden;
  1349. padding: 30rpx;
  1350. box-shadow: 0 -4rpx 8rpx rgba(0, 0, 0, 0.05);
  1351. background: linear-gradient(to bottom, #fff7e8, 20%,#ffffff);
  1352. .category-nav {
  1353. width: 20%;
  1354. background: #ffffff;
  1355. height: 100%;
  1356. border-right: 1rpx solid rgba(255, 126, 14, 0.1);
  1357. margin: 1rpx;
  1358. border-radius: 20rpx 0 0 0;
  1359. margin-right: 20rpx;
  1360. overflow-y: auto;
  1361. scrollbar-width: none; /* Firefox */
  1362. -ms-overflow-style: none; /* IE and Edge */
  1363. &::-webkit-scrollbar {
  1364. width: 0 !important;
  1365. display: none; /* Chrome, Safari, Opera */
  1366. }
  1367. .category-item {
  1368. position: relative;
  1369. padding: 28rpx 20rpx;
  1370. text-align: center;
  1371. font-family: PingFang SC;
  1372. font-weight: 600;
  1373. font-size: 15px;
  1374. line-height: 100%;
  1375. letter-spacing: 0px;
  1376. color: #666;
  1377. .category-dot {
  1378. position: absolute;
  1379. top: 15rpx;
  1380. right: 15rpx;
  1381. min-width: 32rpx;
  1382. height: 32rpx;
  1383. padding: 0 6rpx;
  1384. background: #ff7a0e;
  1385. border-radius: 16rpx;
  1386. color: #fff;
  1387. font-size: 20rpx;
  1388. text-align: center;
  1389. line-height: 32rpx;
  1390. box-sizing: border-box;
  1391. }
  1392. &.active {
  1393. color: #ff7a0e;
  1394. font-weight: bold;
  1395. background: #fff7e8;
  1396. position: relative;
  1397. &::before {
  1398. content: '';
  1399. position: absolute;
  1400. left: 0;
  1401. top: 50%;
  1402. transform: translateY(-50%);
  1403. width: 6rpx;
  1404. height: 36rpx;
  1405. background: #ff7a0e;
  1406. border-radius: 3rpx;
  1407. }
  1408. }
  1409. }
  1410. }
  1411. .goods-content {
  1412. flex: 1;
  1413. height: 100%;
  1414. padding: 0 30rpx 180rpx 30rpx; /* 添加底部padding,为固定底部栏预留空间 */
  1415. background: #ffffff;
  1416. width: 70%;
  1417. margin: 1rpx;
  1418. margin-left: 0;
  1419. border-radius: 0 20rpx 0 0;
  1420. overflow-y: auto;
  1421. box-sizing: border-box;
  1422. scrollbar-width: none; /* Firefox */
  1423. -ms-overflow-style: none; /* IE and Edge */
  1424. &::-webkit-scrollbar {
  1425. width: 0 !important;
  1426. display: none; /* Chrome, Safari, Opera */
  1427. }
  1428. }
  1429. }
  1430. .goods-item {
  1431. display: flex;
  1432. align-items: flex-start;
  1433. padding: 30rpx 0;
  1434. border-bottom: 1rpx solid #f5f5f5;
  1435. .goods-item-img {
  1436. width: 120rpx;
  1437. height: 120rpx;
  1438. border-radius: 24rpx;
  1439. background: #f8f8f8;
  1440. margin-right: 28rpx;
  1441. object-fit: contain;
  1442. flex-shrink: 0;
  1443. }
  1444. .goods-info-wrap {
  1445. flex: 1;
  1446. display: flex;
  1447. flex-direction: column;
  1448. justify-content: center;
  1449. min-width: 0;
  1450. }
  1451. .goods-header {
  1452. display: flex;
  1453. justify-content: space-between;
  1454. align-items: center;
  1455. margin-bottom: 10rpx;
  1456. }
  1457. .goods-name {
  1458. font-family: PingFang SC;
  1459. font-weight: 500;
  1460. font-size: 14px;
  1461. line-height: 140%;
  1462. letter-spacing: 0%;
  1463. vertical-align: middle;
  1464. color: #333;
  1465. font-weight: bold;
  1466. flex-shrink: 1;
  1467. flex-grow: 1;
  1468. }
  1469. .brand-check-placeholder {
  1470. flex-shrink: 0;
  1471. margin-left: 10rpx;
  1472. }
  1473. .brand-check {
  1474. display: flex;
  1475. flex-direction: row;
  1476. align-items: center;
  1477. justify-content: center;
  1478. border: 1px solid #f8a01d;
  1479. border-radius: 8rpx;
  1480. color: #ff7a0e;
  1481. font-family: PingFang SC;
  1482. font-weight: 400;
  1483. font-size: 12px;
  1484. padding: 4rpx 10rpx;
  1485. line-height: 1;
  1486. white-space: nowrap;
  1487. text {
  1488. margin-right: 4rpx;
  1489. }
  1490. }
  1491. .goods-desc {
  1492. font-size: 20rpx;
  1493. color: #999;
  1494. display: block;
  1495. margin-bottom: 20rpx;
  1496. white-space: nowrap;
  1497. overflow: hidden;
  1498. text-overflow: ellipsis;
  1499. }
  1500. .goods-info {
  1501. display: flex;
  1502. justify-content: space-between;
  1503. align-items: center;
  1504. flex-wrap: nowrap;
  1505. gap: 10rpx;
  1506. }
  1507. .price-info {
  1508. display: flex;
  1509. align-items: baseline;
  1510. white-space: nowrap;
  1511. flex-shrink: 0;
  1512. .price-symbol {
  1513. font-size: 24rpx;
  1514. color: #ff7a0e;
  1515. }
  1516. .price-value {
  1517. font-size: 36rpx;
  1518. color: #ff7a0e;
  1519. font-weight: bold;
  1520. margin: 0 4rpx;
  1521. white-space: nowrap;
  1522. }
  1523. .price-unit {
  1524. font-size: 24rpx;
  1525. color: #999;
  1526. white-space: nowrap;
  1527. }
  1528. }
  1529. .quantity-control {
  1530. display: flex;
  1531. align-items: center;
  1532. flex-shrink: 0;
  1533. white-space: nowrap;
  1534. button {
  1535. width: 60rpx;
  1536. height: 60rpx;
  1537. padding: 0;
  1538. margin: 0;
  1539. display: flex;
  1540. align-items: center;
  1541. justify-content: center;
  1542. font-size: 28rpx;
  1543. color: #666;
  1544. background: #ffffff;
  1545. border: none;
  1546. border-radius: 50%;
  1547. &::after {
  1548. border: none;
  1549. }
  1550. &:active {
  1551. opacity: 0.8;
  1552. }
  1553. }
  1554. .quantity {
  1555. width: 50rpx;
  1556. text-align: center;
  1557. font-size: 32rpx;
  1558. color: #333;
  1559. }
  1560. }
  1561. .rules-link {
  1562. margin-top: 20rpx;
  1563. }
  1564. .rules-link .rules {
  1565. display: flex;
  1566. align-items: center;
  1567. font-family: PingFang SC;
  1568. font-weight: 400;
  1569. font-size: 12px;
  1570. line-height: 140%;
  1571. letter-spacing: 0%;
  1572. color: #666;
  1573. }
  1574. }
  1575. .other-unrecycle-card {
  1576. display: flex;
  1577. align-items: center;
  1578. background: #fff;
  1579. border-radius: 24rpx;
  1580. box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.06);
  1581. padding: 30rpx 30rpx 30rpx 30rpx;
  1582. margin: 30rpx 0 0 0;
  1583. }
  1584. .other-unrecycle-img {
  1585. width: 120rpx;
  1586. height: 120rpx;
  1587. border-radius: 24rpx;
  1588. background: #f8f8f8;
  1589. margin-right: 28rpx;
  1590. object-fit: contain;
  1591. flex-shrink: 0;
  1592. }
  1593. .other-unrecycle-info {
  1594. flex: 1;
  1595. display: flex;
  1596. flex-direction: column;
  1597. justify-content: center;
  1598. min-width: 0;
  1599. overflow: hidden;
  1600. }
  1601. .other-unrecycle-title {
  1602. font-size: 30rpx;
  1603. color: #222;
  1604. font-weight: bold;
  1605. margin-bottom: 8rpx;
  1606. white-space: nowrap;
  1607. overflow: hidden;
  1608. text-overflow: ellipsis;
  1609. }
  1610. .other-unrecycle-desc {
  1611. font-size: 24rpx;
  1612. color: #999;
  1613. margin-bottom: 12rpx;
  1614. text-overflow: ellipsis;
  1615. overflow: hidden;
  1616. white-space: nowrap;
  1617. }
  1618. .other-unrecycle-price-row {
  1619. display: flex;
  1620. align-items: center;
  1621. white-space: nowrap;
  1622. }
  1623. .other-unrecycle-price {
  1624. font-size: 28rpx;
  1625. color: #ff9c00;
  1626. font-weight: bold;
  1627. white-space: nowrap;
  1628. }
  1629. .other-unrecycle-btn {
  1630. width: 60rpx;
  1631. height: 60rpx;
  1632. margin-left: 24rpx;
  1633. border-radius: 50%;
  1634. background: #fff;
  1635. color: #666;
  1636. font-size: 36rpx;
  1637. border: none;
  1638. display: flex;
  1639. align-items: center;
  1640. justify-content: center;
  1641. }
  1642. .fixed-bottom-wrap {
  1643. position: fixed;
  1644. left: 0;
  1645. right: 0;
  1646. bottom: calc(v-bind('tabbarHeight + "px"') + env(safe-area-inset-bottom));
  1647. width: 100vw;
  1648. z-index: 100;
  1649. background: transparent;
  1650. box-sizing: border-box;
  1651. pointer-events: auto;
  1652. }
  1653. .bottom-bar-divider {
  1654. width: 100%;
  1655. height: 1px;
  1656. background: #f0f0f0;
  1657. position: absolute;
  1658. left: 0;
  1659. bottom: 0;
  1660. z-index: 1;
  1661. }
  1662. .green-tip-bar {
  1663. width: 100%;
  1664. background: #eaffea;
  1665. color: #13ac47;
  1666. font-size: 20rpx;
  1667. padding: 8rpx 30rpx;
  1668. box-sizing: border-box;
  1669. text-align: center;
  1670. display: flex;
  1671. align-items: center;
  1672. justify-content: center;
  1673. line-height: 1.4;
  1674. min-height: 40rpx;
  1675. .tip-highlight {
  1676. color: #ff9c00;
  1677. font-weight: bold;
  1678. font-size: 20rpx;
  1679. }
  1680. }
  1681. .bottom-bar {
  1682. width: 100%;
  1683. background-color: #fff;
  1684. display: flex;
  1685. align-items: center;
  1686. justify-content: space-between;
  1687. padding: 0 30rpx;
  1688. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  1689. height: 120rpx;
  1690. border-top-left-radius: 0;
  1691. border-top-right-radius: 0;
  1692. border-bottom-left-radius: env(safe-area-inset-bottom);
  1693. border-bottom-right-radius: env(safe-area-inset-bottom);
  1694. .bottom-left {
  1695. // flex: 1;
  1696. display: flex;
  1697. flex-direction: column;
  1698. justify-content: center;
  1699. .summary-row {
  1700. display: flex;
  1701. align-items: center;
  1702. font-size: 26rpx;
  1703. color: #333;
  1704. .summary-label {
  1705. color: #333;
  1706. }
  1707. .summary-count {
  1708. color: #ff9c00;
  1709. font-weight: bold;
  1710. font-size: 28rpx;
  1711. }
  1712. }
  1713. .amount-row {
  1714. display: flex;
  1715. align-items: center;
  1716. margin-top: 4rpx;
  1717. .amount {
  1718. color: #ff9c00;
  1719. font-size: 44rpx;
  1720. font-weight: bold;
  1721. vertical-align: middle;
  1722. }
  1723. }
  1724. }
  1725. .submit-btn {
  1726. width: 300rpx;
  1727. height: 88rpx;
  1728. background: linear-gradient(to right, #ffd01e, #ff8917);
  1729. border-radius: 44rpx;
  1730. color: #fff;
  1731. font-size: 32rpx;
  1732. font-weight: bold;
  1733. display: flex;
  1734. align-items: center;
  1735. justify-content: center;
  1736. border: none;
  1737. // margin-left: 0rpx;
  1738. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08);
  1739. &::after {
  1740. border: none;
  1741. }
  1742. &:active {
  1743. opacity: 0.9;
  1744. }
  1745. }
  1746. }
  1747. .detail-popup-mask {
  1748. position: fixed;
  1749. left: 0;
  1750. right: 0;
  1751. top: 0;
  1752. bottom: calc(90rpx + env(safe-area-inset-bottom)); /* tabbar高度+安全区 */
  1753. background: rgba(0,0,0,0.35);
  1754. z-index: 8;
  1755. display: flex;
  1756. align-items: flex-end;
  1757. justify-content: center;
  1758. }
  1759. .detail-popup {
  1760. width: 100vw;
  1761. max-width: none;
  1762. background: #fff;
  1763. border-radius: 48rpx 48rpx 0 0;
  1764. box-shadow: 0 8rpx 48rpx rgba(0,0,0,0.18);
  1765. display: flex;
  1766. flex-direction: column;
  1767. align-items: stretch;
  1768. position: relative;
  1769. padding: 0;
  1770. overflow: hidden;
  1771. min-height: 520rpx;
  1772. max-height: 80vh;
  1773. bottom: 0;
  1774. }
  1775. .detail-popup-close {
  1776. position: absolute;
  1777. right: 36rpx;
  1778. top: 36rpx;
  1779. font-size: 36rpx;
  1780. color: #bbb;
  1781. z-index: 2;
  1782. }
  1783. .popup-green-tip {
  1784. border-radius: 48rpx 48rpx 0 0;
  1785. font-size: 20rpx;
  1786. padding: 24rpx 30rpx 0 30rpx;
  1787. background: #eaffea;
  1788. color: #13ac47;
  1789. text-align: left;
  1790. }
  1791. .panel-header {
  1792. display: flex;
  1793. align-items: center;
  1794. justify-content: center;
  1795. font-size: 32rpx;
  1796. font-weight: bold;
  1797. padding: 40rpx 36rpx 0 36rpx;
  1798. background: #fff;
  1799. position: relative;
  1800. }
  1801. .panel-title {
  1802. font-size: 32rpx;
  1803. color: #222;
  1804. font-weight: bold;
  1805. text-align: center;
  1806. flex: 1;
  1807. }
  1808. .popup-panel-list {
  1809. flex: 1;
  1810. overflow-y: auto;
  1811. max-height: 36vh;
  1812. padding: 0 24rpx;
  1813. scrollbar-width: none; /* Firefox */
  1814. -ms-overflow-style: none; /* IE and Edge */
  1815. &::-webkit-scrollbar {
  1816. width: 0 !important;
  1817. display: none; /* Chrome, Safari, Opera */
  1818. }
  1819. }
  1820. .panel-item {
  1821. display: flex;
  1822. align-items: center;
  1823. justify-content: flex-start;
  1824. padding: 24rpx 0;
  1825. border-bottom: 1px solid #f0f0f0;
  1826. }
  1827. .panel-item-img {
  1828. width: 100rpx;
  1829. height: 100rpx;
  1830. margin-right: 20rpx;
  1831. border-radius: 16rpx;
  1832. background: #f8f8f8;
  1833. }
  1834. .panel-item-info {
  1835. flex: 1;
  1836. display: flex;
  1837. flex-direction: column;
  1838. justify-content: center;
  1839. min-width: 0;
  1840. }
  1841. .panel-item-name {
  1842. font-size: 30rpx;
  1843. color: #222;
  1844. font-weight: bold;
  1845. margin-bottom: 4rpx;
  1846. text-overflow: ellipsis;
  1847. overflow: hidden;
  1848. white-space: nowrap;
  1849. }
  1850. .panel-item-desc {
  1851. font-size: 24rpx;
  1852. color: #999;
  1853. margin-bottom: 4rpx;
  1854. text-overflow: ellipsis;
  1855. overflow: hidden;
  1856. white-space: nowrap;
  1857. }
  1858. .panel-item-price {
  1859. font-size: 26rpx;
  1860. color: #ff9c00;
  1861. margin-top: 2rpx;
  1862. }
  1863. .panel-quantity-control {
  1864. display: flex;
  1865. align-items: center;
  1866. margin-left: 20rpx;
  1867. margin-right: 30rpx;
  1868. flex-shrink: 0;
  1869. }
  1870. .panel-quantity-control button {
  1871. width: 48rpx;
  1872. height: 48rpx;
  1873. padding: 0;
  1874. margin: 0 8rpx;
  1875. display: flex;
  1876. align-items: center;
  1877. justify-content: center;
  1878. font-size: 32rpx;
  1879. color: #666;
  1880. background: #ffffff;
  1881. border: none;
  1882. border-radius: 50%;
  1883. &::after {
  1884. border: none;
  1885. }
  1886. &:active {
  1887. opacity: 0.8;
  1888. }
  1889. }
  1890. .panel-quantity-control .quantity {
  1891. width: 30rpx;
  1892. text-align: center;
  1893. font-size: 28rpx;
  1894. color: #333;
  1895. }
  1896. .popup-bottom-bar {
  1897. width: 100%;
  1898. background-color: #fff;
  1899. display: flex;
  1900. align-items: center;
  1901. justify-content: space-between;
  1902. padding: 0 30rpx;
  1903. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  1904. height: 120rpx;
  1905. border-top: 1px solid #f0f0f0;
  1906. border-bottom-left-radius: 48rpx;
  1907. border-bottom-right-radius: 48rpx;
  1908. // padding-bottom: env(safe-area-inset-bottom);
  1909. .bottom-left {
  1910. display: flex;
  1911. flex-direction: column;
  1912. justify-content: center;
  1913. .summary-row {
  1914. display: flex;
  1915. align-items: center;
  1916. font-size: 26rpx;
  1917. color: #333;
  1918. .summary-label {
  1919. color: #333;
  1920. }
  1921. .summary-count {
  1922. color: #ff9c00;
  1923. font-weight: bold;
  1924. font-size: 28rpx;
  1925. }
  1926. }
  1927. .amount-row {
  1928. display: flex;
  1929. align-items: center;
  1930. margin-top: 4rpx;
  1931. .amount {
  1932. color: #ff9c00;
  1933. font-size: 44rpx;
  1934. font-weight: bold;
  1935. vertical-align: middle;
  1936. }
  1937. }
  1938. }
  1939. .submit-btn {
  1940. width: 300rpx;
  1941. height: 88rpx;
  1942. background: linear-gradient(to right, #ffd01e, #ff8917);
  1943. border-radius: 44rpx;
  1944. color: #fff;
  1945. font-size: 32rpx;
  1946. font-weight: bold;
  1947. display: flex;
  1948. align-items: center;
  1949. justify-content: center;
  1950. border: none;
  1951. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08);
  1952. &::after {
  1953. border: none;
  1954. }
  1955. &:active {
  1956. opacity: 0.9;
  1957. }
  1958. }
  1959. }
  1960. .brand-popup-mask {
  1961. position: fixed;
  1962. left: 0;
  1963. right: 0;
  1964. top: 0;
  1965. bottom: 0;
  1966. background: rgba(0,0,0,0.35);
  1967. z-index: 3000;
  1968. display: flex;
  1969. align-items: flex-end;
  1970. justify-content: center;
  1971. }
  1972. .brand-popup {
  1973. position: relative;
  1974. width: 100%;
  1975. max-width: 750px;
  1976. background: #fff;
  1977. border-radius: 32rpx 32rpx 0 0;
  1978. box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08);
  1979. padding-bottom: 40rpx;
  1980. max-height: 90vh;
  1981. display: flex;
  1982. flex-direction: column;
  1983. overflow: hidden;
  1984. }
  1985. .brand-popup-header {
  1986. display: flex;
  1987. align-items: center;
  1988. justify-content: center;
  1989. padding: 32rpx 24rpx 0 24rpx;
  1990. font-size: 32rpx;
  1991. font-weight: bold;
  1992. position: relative;
  1993. }
  1994. .brand-popup-close {
  1995. position: absolute;
  1996. left: 24rpx;
  1997. font-size: 28rpx;
  1998. color: #888;
  1999. }
  2000. .brand-popup-title {
  2001. font-size: 32rpx;
  2002. color: #222;
  2003. font-weight: bold;
  2004. }
  2005. .brand-popup-search {
  2006. padding: 20rpx 24rpx 0 24rpx;
  2007. }
  2008. .brand-search-input {
  2009. width: 100%;
  2010. height: 60rpx;
  2011. border-radius: 30rpx;
  2012. background: #f5f5f5;
  2013. border: none;
  2014. padding-left: 40rpx;
  2015. font-size: 28rpx;
  2016. color: #888;
  2017. }
  2018. .brand-popup-list {
  2019. flex: 1;
  2020. overflow-y: auto;
  2021. max-height: 60vh;
  2022. padding: 0 24rpx;
  2023. scrollbar-width: none; /* Firefox */
  2024. -ms-overflow-style: none; /* IE and Edge */
  2025. &::-webkit-scrollbar {
  2026. width: 0 !important;
  2027. display: none; /* Chrome, Safari, Opera */
  2028. }
  2029. }
  2030. .brand-letter {
  2031. font-size: 28rpx;
  2032. color: #888;
  2033. margin: 24rpx 0 8rpx 0;
  2034. font-weight: bold;
  2035. }
  2036. .brand-item {
  2037. display: flex;
  2038. align-items: center;
  2039. padding: 16rpx 0;
  2040. border-bottom: 1px solid #f0f0f0;
  2041. }
  2042. .brand-logo {
  2043. width: 60rpx;
  2044. height: 60rpx;
  2045. margin-right: 20rpx;
  2046. border-radius: 8rpx;
  2047. background: #f8f8f8;
  2048. }
  2049. .brand-name {
  2050. font-size: 28rpx;
  2051. color: #222;
  2052. }
  2053. .brand-index-bar {
  2054. position: absolute;
  2055. right: 12rpx;
  2056. top: 120rpx;
  2057. width: 32rpx;
  2058. display: flex;
  2059. flex-direction: column;
  2060. align-items: center;
  2061. z-index: 10;
  2062. }
  2063. .brand-index-bar text {
  2064. font-size: 22rpx;
  2065. color: #bbb;
  2066. margin: 4rpx 0;
  2067. font-weight: bold;
  2068. &.active {
  2069. color: #ff9c00;
  2070. }
  2071. }
  2072. .rule-popup-mask {
  2073. position: fixed;
  2074. left: 0;
  2075. right: 0;
  2076. top: 0;
  2077. bottom: 0;
  2078. background: rgba(0,0,0,0.35);
  2079. z-index: 4000;
  2080. display: flex;
  2081. align-items: center;
  2082. justify-content: center;
  2083. }
  2084. .rule-popup {
  2085. width: 95vw;
  2086. max-width: 750rpx;
  2087. max-height: 85vh;
  2088. background: #fff;
  2089. border-radius: 48rpx;
  2090. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  2091. display: flex;
  2092. flex-direction: column;
  2093. align-items: center;
  2094. position: relative;
  2095. padding-bottom: 40rpx;
  2096. }
  2097. .rule-popup-title {
  2098. font-size: 36rpx;
  2099. color: #222;
  2100. font-weight: bold;
  2101. text-align: center;
  2102. margin-top: 48rpx;
  2103. margin-bottom: 16rpx;
  2104. }
  2105. .rule-popup-content {
  2106. width: 100%;
  2107. max-height: 60vh;
  2108. min-height: 400rpx;
  2109. padding: 0 40rpx;
  2110. box-sizing: border-box;
  2111. overflow-y: auto;
  2112. scrollbar-width: none; /* Firefox */
  2113. -ms-overflow-style: none; /* IE and Edge */
  2114. &::-webkit-scrollbar {
  2115. width: 0 !important;
  2116. display: none; /* Chrome, Safari, Opera */
  2117. }
  2118. }
  2119. .rule-content-bottom-indicator {
  2120. height: 20rpx;
  2121. width: 100%;
  2122. }
  2123. .scroll-tip {
  2124. font-size: 24rpx;
  2125. color: #ff6b35;
  2126. text-align: center;
  2127. margin: 16rpx 0 8rpx 0;
  2128. animation: tipPulse 2s infinite;
  2129. }
  2130. @keyframes tipPulse {
  2131. 0%, 100% { opacity: 1; }
  2132. 50% { opacity: 0.6; }
  2133. }
  2134. .rule-popup-desc {
  2135. font-size: 26rpx;
  2136. color: #888;
  2137. text-align: center;
  2138. margin-bottom: 24rpx;
  2139. margin-top: 0;
  2140. }
  2141. .rule-popup-warning {
  2142. width: 100%;
  2143. border: 2rpx solid #ffb800;
  2144. color: #ffb800;
  2145. background: #fffbe6;
  2146. border-radius: 32rpx;
  2147. font-size: 28rpx;
  2148. text-align: center;
  2149. padding: 16rpx 0;
  2150. margin-bottom: 24rpx;
  2151. font-weight: bold;
  2152. }
  2153. .rule-popup-img {
  2154. display: block;
  2155. margin: 0 auto 24rpx auto;
  2156. max-width: 80%;
  2157. max-height: 300rpx;
  2158. border-radius: 16rpx;
  2159. object-fit: contain;
  2160. }
  2161. .rule-popup-btn {
  2162. width: 80%;
  2163. height: 88rpx;
  2164. background: linear-gradient(to right, #ffd01e, #ff8917);
  2165. border-radius: 44rpx;
  2166. color: #fff;
  2167. font-size: 32rpx;
  2168. font-weight: bold;
  2169. display: flex;
  2170. align-items: center;
  2171. justify-content: center;
  2172. border: none;
  2173. margin: 0 auto;
  2174. margin-top: 16rpx;
  2175. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08);
  2176. transition: all 0.3s ease;
  2177. &::after {
  2178. border: none;
  2179. }
  2180. &:active {
  2181. opacity: 0.9;
  2182. }
  2183. &.disabled {
  2184. background: #ccc;
  2185. color: #999;
  2186. box-shadow: none;
  2187. opacity: 0.6;
  2188. }
  2189. }
  2190. .rule-popup-close {
  2191. position: absolute;
  2192. right: 32rpx;
  2193. bottom: 32rpx;
  2194. width: 72rpx;
  2195. height: 72rpx;
  2196. background: #ff5a5f;
  2197. border-radius: 50%;
  2198. display: flex;
  2199. align-items: center;
  2200. justify-content: center;
  2201. z-index: 10;
  2202. box-shadow: 0 2rpx 8rpx rgba(255,90,95,0.12);
  2203. }
  2204. /* 预约上门取件弹窗样式 */
  2205. .pickup-confirm-mask {
  2206. position: fixed;
  2207. left: 0;
  2208. right: 0;
  2209. top: 0;
  2210. bottom: 0;
  2211. background: rgba(0,0,0,0.35);
  2212. z-index: 5000;
  2213. display: flex;
  2214. align-items: center;
  2215. justify-content: center;
  2216. }
  2217. .pickup-confirm-popup {
  2218. width: 90vw;
  2219. max-width: 600rpx;
  2220. background: #fff;
  2221. border-radius: 48rpx;
  2222. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  2223. display: flex;
  2224. flex-direction: column;
  2225. align-items: center;
  2226. position: relative;
  2227. padding: 48rpx 36rpx 40rpx 36rpx;
  2228. }
  2229. .pickup-confirm-title {
  2230. font-size: 36rpx;
  2231. color: #222;
  2232. font-weight: bold;
  2233. text-align: center;
  2234. margin-bottom: 24rpx;
  2235. }
  2236. .pickup-confirm-content {
  2237. width: 100%;
  2238. font-size: 26rpx;
  2239. color: #333;
  2240. text-align: left;
  2241. line-height: 1.7;
  2242. margin-bottom: 36rpx;
  2243. .confirm-item {
  2244. margin-bottom: 24rpx;
  2245. &:last-child {
  2246. margin-bottom: 0;
  2247. }
  2248. }
  2249. .confirm-item-red {
  2250. color: red;
  2251. }
  2252. }
  2253. .pickup-confirm-btn-row {
  2254. width: 100%;
  2255. display: flex;
  2256. justify-content: space-between;
  2257. gap: 32rpx;
  2258. }
  2259. .pickup-confirm-btn {
  2260. flex: 1;
  2261. height: 88rpx;
  2262. border-radius: 44rpx;
  2263. font-size: 32rpx;
  2264. font-weight: bold;
  2265. display: flex;
  2266. align-items: center;
  2267. justify-content: center;
  2268. border: 2rpx solid #ffd01e;
  2269. background: #fff;
  2270. color: #ff9c00;
  2271. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08);
  2272. }
  2273. .pickup-confirm-btn:not(.agree) {
  2274. background: #fff0d2;
  2275. }
  2276. .pickup-confirm-btn.agree {
  2277. background: linear-gradient(to right, #ffd01e, #ff8917);
  2278. color: #fff;
  2279. border: none;
  2280. }
  2281. .uv-tabbar {
  2282. z-index: 1000;
  2283. }
  2284. .loading-more {
  2285. text-align: center;
  2286. color: #999;
  2287. padding: 20rpx 0;
  2288. font-size: 26rpx;
  2289. }
  2290. .brand-confirm-mask {
  2291. position: fixed;
  2292. left: 0;
  2293. right: 0;
  2294. top: 0;
  2295. bottom: 0;
  2296. background: rgba(0,0,0,0.25);
  2297. z-index: 5001;
  2298. display: flex;
  2299. align-items: center;
  2300. justify-content: center;
  2301. }
  2302. .brand-confirm-popup {
  2303. width: 70vw;
  2304. max-width: 270px;
  2305. background: #fff;
  2306. border-radius: 32rpx;
  2307. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  2308. display: flex;
  2309. flex-direction: column;
  2310. align-items: center;
  2311. padding: 48rpx 20rpx 36rpx 20rpx;
  2312. position: relative;
  2313. }
  2314. .brand-confirm-title {
  2315. font-size: 36rpx;
  2316. color: #222;
  2317. font-weight: bold;
  2318. text-align: center;
  2319. margin-bottom: 24rpx;
  2320. }
  2321. .brand-confirm-logo-wrap {
  2322. width: 120rpx;
  2323. height: 120rpx;
  2324. background: #f8f8f8;
  2325. border-radius: 50%;
  2326. display: flex;
  2327. align-items: center;
  2328. justify-content: center;
  2329. margin-bottom: 18rpx;
  2330. }
  2331. .brand-confirm-logo {
  2332. width: 80rpx;
  2333. height: 80rpx;
  2334. border-radius: 50%;
  2335. }
  2336. .brand-confirm-name {
  2337. font-size: 28rpx;
  2338. color: #222;
  2339. font-weight: bold;
  2340. text-align: center;
  2341. margin-bottom: 16rpx;
  2342. }
  2343. .brand-confirm-desc {
  2344. font-size: 24rpx;
  2345. color: #999;
  2346. text-align: center;
  2347. margin-bottom: 32rpx;
  2348. line-height: 1.6;
  2349. }
  2350. .brand-confirm-btn-row {
  2351. width: 100%;
  2352. display: flex;
  2353. justify-content: space-between;
  2354. gap: 24rpx;
  2355. }
  2356. .brand-confirm-btn {
  2357. flex: 1;
  2358. height: 72rpx;
  2359. border-radius: 36rpx;
  2360. font-size: 28rpx;
  2361. font-weight: bold;
  2362. display: flex;
  2363. align-items: center;
  2364. justify-content: center;
  2365. border: none;
  2366. margin: 0 0;
  2367. }
  2368. .brand-confirm-btn.retry {
  2369. background: #fff;
  2370. color: #ff9c00;
  2371. border: 2rpx solid #ff9c00;
  2372. }
  2373. .brand-confirm-btn.confirm {
  2374. background: linear-gradient(to right, #ffd01e, #ff8917);
  2375. color: #fff;
  2376. border: none;
  2377. }
  2378. .price-info-popup-mask {
  2379. position: fixed;
  2380. left: 0;
  2381. right: 0;
  2382. top: 0;
  2383. bottom: 0;
  2384. background: rgba(0,0,0,0.4);
  2385. z-index: 6000;
  2386. display: flex;
  2387. align-items: center;
  2388. justify-content: center;
  2389. }
  2390. .price-info-popup {
  2391. width: 85vw;
  2392. max-width: 600rpx;
  2393. background: #fff;
  2394. border-radius: 24rpx;
  2395. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  2396. display: flex;
  2397. flex-direction: column;
  2398. align-items: center;
  2399. position: relative;
  2400. padding: 40rpx 40rpx;
  2401. .price-info-popup-title {
  2402. font-size: 34rpx;
  2403. color: #333;
  2404. font-weight: bold;
  2405. text-align: center;
  2406. margin-bottom: 30rpx;
  2407. }
  2408. .price-info-popup-content {
  2409. width: 100%;
  2410. max-height: 50vh;
  2411. .price-info-section {
  2412. margin-bottom: 30rpx;
  2413. &:last-child {
  2414. margin-bottom: 0;
  2415. }
  2416. .price-info-heading {
  2417. font-size: 28rpx;
  2418. color: #333;
  2419. font-weight: 500;
  2420. margin-bottom: 15rpx;
  2421. }
  2422. .price-info-text {
  2423. font-size: 26rpx;
  2424. color: #666;
  2425. line-height: 1.6;
  2426. }
  2427. }
  2428. }
  2429. .price-info-popup-btn {
  2430. width: 100%;
  2431. height: 88rpx;
  2432. background: linear-gradient(to right, #ffd01e, #ff8917);
  2433. border-radius: 44rpx;
  2434. color: #fff;
  2435. font-size: 32rpx;
  2436. font-weight: bold;
  2437. display: flex;
  2438. align-items: center;
  2439. justify-content: center;
  2440. border: none;
  2441. margin-top: 40rpx;
  2442. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08);
  2443. &::after {
  2444. border: none;
  2445. }
  2446. &:active {
  2447. opacity: 0.9;
  2448. }
  2449. }
  2450. }
  2451. /* 减少数量时的品牌选择弹窗 */
  2452. .brand-reduce-popup-mask {
  2453. position: fixed;
  2454. left: 0;
  2455. right: 0;
  2456. top: 0;
  2457. bottom: 0;
  2458. background: rgba(0,0,0,0.35);
  2459. z-index: 5001;
  2460. display: flex;
  2461. align-items: center;
  2462. justify-content: center;
  2463. }
  2464. .brand-reduce-popup {
  2465. width: 70vw;
  2466. max-width: 270px;
  2467. background: #fff;
  2468. border-radius: 32rpx;
  2469. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  2470. display: flex;
  2471. flex-direction: column;
  2472. align-items: center;
  2473. padding: 48rpx 20rpx 36rpx 20rpx;
  2474. position: relative;
  2475. }
  2476. .brand-reduce-popup-header {
  2477. display: flex;
  2478. align-items: center;
  2479. justify-content: center;
  2480. padding: 32rpx 24rpx 0 24rpx;
  2481. font-size: 32rpx;
  2482. font-weight: bold;
  2483. position: relative;
  2484. }
  2485. .brand-reduce-popup-close {
  2486. position: absolute;
  2487. left: 24rpx;
  2488. font-size: 28rpx;
  2489. color: #888;
  2490. }
  2491. .brand-reduce-popup-title {
  2492. font-size: 32rpx;
  2493. color: #222;
  2494. font-weight: bold;
  2495. }
  2496. .brand-reduce-popup-list {
  2497. flex: 1;
  2498. overflow-y: auto;
  2499. max-height: 60vh;
  2500. padding: 0 24rpx;
  2501. scrollbar-width: none; /* Firefox */
  2502. -ms-overflow-style: none; /* IE and Edge */
  2503. &::-webkit-scrollbar {
  2504. width: 0 !important;
  2505. display: none; /* Chrome, Safari, Opera */
  2506. }
  2507. }
  2508. .brand-item {
  2509. display: flex;
  2510. align-items: center;
  2511. padding: 16rpx 0;
  2512. border-bottom: 1px solid #f0f0f0;
  2513. }
  2514. .brand-logo {
  2515. width: 60rpx;
  2516. height: 60rpx;
  2517. margin-right: 20rpx;
  2518. border-radius: 8rpx;
  2519. background: #f8f8f8;
  2520. }
  2521. .brand-name {
  2522. font-size: 28rpx;
  2523. color: #222;
  2524. }
  2525. </style>