爱简收旧衣按件回收前端代码仓库
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.

532 lines
11 KiB

2 weeks ago
  1. <template>
  2. <!-- 商品明细弹窗 -->
  3. <view v-if="showDetailPopup" class="detail-popup-mask" @click.self="close">
  4. <view class="detail-popup" @click.stop>
  5. <view class="detail-popup-header">
  6. <text class="detail-popup-close" @click="close">关闭</text>
  7. <text class="detail-popup-title">商品明细</text>
  8. </view>
  9. <!-- 商品基本信息 -->
  10. <view class="product-basic-info">
  11. <image :src="productInfo.image" class="product-image" mode="aspectFit" />
  12. <view class="product-info">
  13. <text class="product-name">{{ productInfo.name }}</text>
  14. <text class="brand-name">品牌{{ brandInfo.name }}</text>
  15. </view>
  16. </view>
  17. <!-- 款式列表 -->
  18. <scroll-view class="style-list" scroll-y>
  19. <view v-for="(style, index) in styleList" :key="style.id" class="style-item">
  20. <image :src="style.image" class="style-image" mode="aspectFit" />
  21. <view class="style-info">
  22. <text class="style-name">{{ style.name }}</text>
  23. <view class="style-price">
  24. <text class="price-symbol">¥</text>
  25. <text class="price-value" v-if="style.minPrice === style.maxPrice">{{ style.minPrice }}</text>
  26. <text class="price-value" v-else>{{ style.minPrice }}-{{ style.maxPrice }}</text>
  27. <text class="price-unit">/</text>
  28. </view>
  29. </view>
  30. <view class="style-quantity">
  31. <button class="btn-minus" @click="updateQuantity(index, -1)">-</button>
  32. <text class="quantity">{{ style.quantity || 0 }}</text>
  33. <button class="btn-plus" @click="updateQuantity(index, 1)">+</button>
  34. </view>
  35. </view>
  36. </scroll-view>
  37. <!-- 价格统计 -->
  38. <view class="price-summary">
  39. <view class="summary-left">
  40. <text class="summary-label">已选 <text class="summary-count">{{ totalQuantity }}</text> 预计可得</text>
  41. <view class="amount-row">
  42. <text class="amount" v-if="totalPriceRange.min === totalPriceRange.max">¥{{ totalPriceRange.min }}</text>
  43. <text class="amount" v-else>¥{{ totalPriceRange.min }}-{{ totalPriceRange.max }}</text>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 底部按钮 -->
  48. <view class="detail-popup-footer">
  49. <button class="confirm-btn" @click="confirmChanges">确认修改</button>
  50. </view>
  51. <!-- 浮窗按钮 -->
  52. <view class="floating-btn" @click="openStyleSelector">
  53. <text class="floating-btn-text">+</text>
  54. </view>
  55. <product-style-selector ref="styleSelector" @style-confirm="onStyleConfirm" @close="onStyleSelectorClose"></product-style-selector>
  56. <!-- 款式选择器组件 -->
  57. </view>
  58. </view>
  59. </template>
  60. <script>
  61. import productStyleSelector from './product-style-selector.vue'
  62. export default {
  63. name: 'ProductDetailPopup',
  64. components: {
  65. productStyleSelector
  66. },
  67. data() {
  68. return {
  69. showDetailPopup: false,
  70. productInfo: {
  71. id: '',
  72. name: '',
  73. image: ''
  74. },
  75. brandInfo: {
  76. id: '',
  77. name: '',
  78. logo: ''
  79. },
  80. styleList: [],
  81. originalQuantities: {} // 保存原始数量,用于取消时恢复
  82. }
  83. },
  84. computed: {
  85. // 计算总价格范围
  86. totalPriceRange() {
  87. const result = this.styleList.reduce((sum, style) => {
  88. const quantity = style.quantity || 0
  89. if (quantity > 0) {
  90. const minPrice = Number(style.minPrice) || 0
  91. const maxPrice = Number(style.maxPrice) || Number(style.minPrice) || 0
  92. sum.min += quantity * minPrice
  93. sum.max += quantity * maxPrice
  94. }
  95. return sum
  96. }, { min: 0, max: 0 })
  97. return {
  98. min: result.min.toFixed(1),
  99. max: result.max.toFixed(1)
  100. }
  101. },
  102. // 计算总数量
  103. totalQuantity() {
  104. return this.styleList.reduce((sum, style) => sum + (style.quantity || 0), 0)
  105. }
  106. },
  107. methods: {
  108. // 打开商品明细弹窗
  109. open(productInfo, brandInfo, existingQuantities = {}, styleCache = {}) {
  110. if (!productInfo || !brandInfo) {
  111. console.error('productInfo and brandInfo are required')
  112. return
  113. }
  114. this.productInfo = productInfo
  115. this.brandInfo = brandInfo
  116. this.originalQuantities = { ...existingQuantities }
  117. // 直接基于已选择的款式构建列表
  118. this.buildStyleListFromExisting(existingQuantities, styleCache)
  119. this.showDetailPopup = true
  120. },
  121. // 关闭弹窗
  122. close() {
  123. this.showDetailPopup = false
  124. this.styleList = []
  125. this.productInfo = { id: '', name: '', image: '' }
  126. this.brandInfo = { id: '', name: '', logo: '' }
  127. this.originalQuantities = {}
  128. this.$emit('close')
  129. },
  130. // 更新数量
  131. updateQuantity(index, delta) {
  132. const style = this.styleList[index]
  133. if (!style) return
  134. let newQuantity = (style.quantity || 0) + delta
  135. if (newQuantity < 0) newQuantity = 0
  136. this.$set(style, 'quantity', newQuantity)
  137. },
  138. // 基于已选择的款式构建列表
  139. buildStyleListFromExisting(existingQuantities, styleCache) {
  140. const styleList = []
  141. // 从父组件传入的已选择数据中提取款式信息
  142. Object.entries(existingQuantities).forEach(([uniqueKey, quantity]) => {
  143. if (quantity > 0 && uniqueKey.startsWith(`${this.brandInfo.id}_`)) {
  144. // 从styleCache中获取完整的款式信息
  145. const cacheInfo = styleCache[uniqueKey]
  146. if (cacheInfo && cacheInfo.styleInfo) {
  147. const styleInfo = cacheInfo.styleInfo
  148. styleList.push({
  149. id: styleInfo.id,
  150. name: styleInfo.name,
  151. image: styleInfo.image || '/static/default-product.png',
  152. minPrice: styleInfo.minPrice,
  153. maxPrice: styleInfo.maxPrice,
  154. brandId: styleInfo.brandId || this.brandInfo.id,
  155. shopId: styleInfo.shopId,
  156. quantity: quantity
  157. })
  158. }
  159. }
  160. })
  161. this.styleList = styleList
  162. },
  163. // 确认修改
  164. confirmChanges() {
  165. const updatedStyles = this.styleList.filter(style => (style.quantity || 0) > 0)
  166. this.$emit('confirm-changes', {
  167. productInfo: this.productInfo,
  168. brandInfo: this.brandInfo,
  169. updatedStyles: updatedStyles
  170. })
  171. this.close()
  172. },
  173. // 打开款式选择器
  174. openStyleSelector() {
  175. // 获取当前品牌下已有的款式数量
  176. const existingQuantities = {}
  177. this.styleList.forEach(style => {
  178. if (style.quantity > 0) {
  179. const uniqueKey = `${this.brandInfo.id}_${style.id}`
  180. existingQuantities[uniqueKey] = style.quantity
  181. }
  182. })
  183. // 直接打开内部的款式选择器
  184. this.$refs.styleSelector.open(this.brandInfo, this.productInfo.id, existingQuantities)
  185. },
  186. // 处理款式确认事件
  187. onStyleConfirm(data) {
  188. if (data.selectedStyles && data.selectedStyles.length > 0) {
  189. // 更新当前的款式列表
  190. const newStyleList = []
  191. data.selectedStyles.forEach(style => {
  192. if (style.quantity > 0) {
  193. newStyleList.push({
  194. id: style.id,
  195. name: style.name,
  196. image: style.image || '/static/default-product.png',
  197. minPrice: style.minPrice,
  198. maxPrice: style.maxPrice,
  199. brandId: style.brandId || this.brandInfo.id,
  200. shopId: style.shopId,
  201. quantity: style.quantity
  202. })
  203. }
  204. })
  205. this.styleList = newStyleList
  206. // 不关闭当前弹窗,保持商品明细弹窗打开状态
  207. }
  208. },
  209. // 处理款式选择器关闭事件
  210. onStyleSelectorClose() {
  211. // 款式选择器关闭时的处理逻辑
  212. }
  213. }
  214. }
  215. </script>
  216. <style lang="scss" scoped>
  217. .detail-popup-mask {
  218. position: fixed;
  219. left: 0;
  220. right: 0;
  221. top: 0;
  222. bottom: 0;
  223. background: rgba(0,0,0,0.25);
  224. z-index: 5000;
  225. display: flex;
  226. align-items: flex-end;
  227. justify-content: center;
  228. }
  229. .detail-popup {
  230. position: relative;
  231. width: 100%;
  232. max-width: 750px;
  233. background: #fff;
  234. border-radius: 32rpx 32rpx 0 0;
  235. box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08);
  236. padding-bottom: 40rpx;
  237. height: 84vh;
  238. display: flex;
  239. flex-direction: column;
  240. overflow: hidden;
  241. }
  242. .detail-popup-header {
  243. display: flex;
  244. align-items: center;
  245. justify-content: center;
  246. padding: 32rpx 24rpx 0 24rpx;
  247. font-size: 32rpx;
  248. font-weight: bold;
  249. position: relative;
  250. border-bottom: 1px solid #f0f0f0;
  251. padding-bottom: 20rpx;
  252. }
  253. .detail-popup-close {
  254. position: absolute;
  255. left: 24rpx;
  256. font-size: 28rpx;
  257. color: #888;
  258. }
  259. .detail-popup-title {
  260. font-size: 32rpx;
  261. color: #222;
  262. font-weight: bold;
  263. }
  264. .product-basic-info {
  265. display: flex;
  266. align-items: center;
  267. padding: 24rpx;
  268. border-bottom: 1px solid #f0f0f0;
  269. }
  270. .product-image {
  271. width: 80rpx;
  272. height: 80rpx;
  273. border-radius: 12rpx;
  274. margin-right: 20rpx;
  275. background: #f8f8f8;
  276. flex-shrink: 0;
  277. }
  278. .product-info {
  279. flex: 1;
  280. display: flex;
  281. flex-direction: column;
  282. }
  283. .product-name {
  284. font-size: 30rpx;
  285. color: #222;
  286. font-weight: bold;
  287. margin-bottom: 8rpx;
  288. }
  289. .brand-name {
  290. font-size: 26rpx;
  291. color: #666;
  292. }
  293. .style-list {
  294. flex: 1;
  295. overflow-y: auto;
  296. padding: 0 24rpx;
  297. box-sizing: border-box;
  298. max-height: calc(84vh - 200rpx);
  299. scrollbar-width: none;
  300. -ms-overflow-style: none;
  301. &::-webkit-scrollbar {
  302. width: 0 !important;
  303. display: none;
  304. }
  305. }
  306. .style-item {
  307. display: flex;
  308. align-items: center;
  309. padding: 20rpx 0;
  310. border-bottom: 1px solid #f0f0f0;
  311. &:last-child {
  312. border-bottom: none;
  313. }
  314. }
  315. .style-image {
  316. width: 100rpx;
  317. height: 100rpx;
  318. border-radius: 12rpx;
  319. margin-right: 20rpx;
  320. background: #f8f8f8;
  321. flex-shrink: 0;
  322. }
  323. .style-info {
  324. flex: 1;
  325. display: flex;
  326. flex-direction: column;
  327. justify-content: center;
  328. min-width: 0;
  329. }
  330. .style-name {
  331. font-size: 28rpx;
  332. color: #222;
  333. font-weight: bold;
  334. margin-bottom: 8rpx;
  335. overflow: hidden;
  336. text-overflow: ellipsis;
  337. white-space: nowrap;
  338. }
  339. .style-price {
  340. display: flex;
  341. align-items: baseline;
  342. }
  343. .price-symbol {
  344. font-size: 24rpx;
  345. color: #ff7a0e;
  346. }
  347. .price-value {
  348. font-size: 28rpx;
  349. color: #ff7a0e;
  350. font-weight: bold;
  351. margin: 0 4rpx;
  352. }
  353. .price-unit {
  354. font-size: 24rpx;
  355. color: #999;
  356. }
  357. .style-quantity {
  358. display: flex;
  359. align-items: center;
  360. flex-shrink: 0;
  361. }
  362. .btn-minus, .btn-plus {
  363. width: 60rpx;
  364. height: 60rpx;
  365. border-radius: 50%;
  366. background: #f8f8f8;
  367. border: 1px solid #e0e0e0;
  368. color: #666;
  369. font-size: 28rpx;
  370. display: flex;
  371. align-items: center;
  372. justify-content: center;
  373. margin: 0;
  374. padding: 0;
  375. &::after {
  376. border: none;
  377. }
  378. &:active {
  379. background: #e0e0e0;
  380. }
  381. }
  382. .quantity {
  383. width: 60rpx;
  384. text-align: center;
  385. font-size: 28rpx;
  386. color: #333;
  387. margin: 0 16rpx;
  388. }
  389. .price-summary {
  390. padding: 20rpx 24rpx;
  391. border-top: 1px solid #f0f0f0;
  392. background: #fff;
  393. }
  394. .summary-left {
  395. display: flex;
  396. flex-direction: column;
  397. justify-content: center;
  398. }
  399. .summary-label {
  400. font-size: 26rpx;
  401. color: #333;
  402. }
  403. .summary-count {
  404. color: #ff9c00;
  405. font-weight: bold;
  406. font-size: 28rpx;
  407. }
  408. .amount-row {
  409. display: flex;
  410. align-items: center;
  411. margin-top: 4rpx;
  412. }
  413. .amount {
  414. color: #ff9c00;
  415. font-size: 44rpx;
  416. font-weight: bold;
  417. vertical-align: middle;
  418. }
  419. .detail-popup-footer {
  420. padding: 24rpx;
  421. border-top: 1px solid #f0f0f0;
  422. background: #fff;
  423. }
  424. .confirm-btn {
  425. width: 100%;
  426. height: 88rpx;
  427. background: linear-gradient(to right, #ffd01e, #ff8917);
  428. border-radius: 44rpx;
  429. color: #fff;
  430. font-size: 32rpx;
  431. font-weight: bold;
  432. border: none;
  433. display: flex;
  434. align-items: center;
  435. justify-content: center;
  436. &::after {
  437. border: none;
  438. }
  439. &:active {
  440. opacity: 0.9;
  441. }
  442. }
  443. .floating-btn {
  444. position: absolute;
  445. right: 24rpx;
  446. bottom: 180rpx;
  447. width: 100rpx;
  448. height: 100rpx;
  449. background: linear-gradient(to right, #ffd01e, #ff8917);
  450. border-radius: 50%;
  451. box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.3);
  452. display: flex;
  453. align-items: center;
  454. justify-content: center;
  455. z-index: 10;
  456. &:active {
  457. opacity: 0.9;
  458. transform: scale(0.95);
  459. }
  460. }
  461. .floating-btn-text {
  462. font-size: 48rpx;
  463. color: #fff;
  464. font-weight: bold;
  465. line-height: 1;
  466. }
  467. </style>