| @ -0,0 +1,230 @@ | |||
| <template> | |||
| <view class="banner" :style="{ height: height }"> | |||
| <swiper | |||
| :indicator-dots="false" | |||
| :autoplay="true" | |||
| :interval="3000" | |||
| :duration="500" | |||
| circular | |||
| :style="{ width: '100%', height: height }" | |||
| > | |||
| <swiper-item v-for="(item, index) in bannerList" :key="item.id || index"> | |||
| <view v-if="item.type == 1" class="video-container"> | |||
| <!-- 预览状态:显示封面图 --> | |||
| <image | |||
| v-if="!videoPlayingStates[index]" | |||
| :src="item.image || ''" | |||
| mode="aspectFill" | |||
| style="width: 100%; height: 100%;" | |||
| class="video-poster" | |||
| @click="playVideoFullscreen(item, index)" | |||
| /> | |||
| <!-- 播放状态:显示视频 --> | |||
| <video | |||
| v-else | |||
| :id="`${videoIdPrefix}-${index}`" | |||
| :src="item.voUrl" | |||
| :autoplay="true" | |||
| :muted="false" | |||
| :loop="false" | |||
| :controls="true" | |||
| :show-play-btn="true" | |||
| :show-center-play-btn="false" | |||
| :show-fullscreen-btn="true" | |||
| :show-progress="true" | |||
| :show-mute-btn="true" | |||
| :enable-progress-gesture="true" | |||
| :enable-play-gesture="true" | |||
| object-fit="cover" | |||
| style="width: 100%; height: 100%;" | |||
| @fullscreenchange="onFullscreenChange" | |||
| @play="onVideoPlay(index)" | |||
| @pause="onVideoPause(index)" | |||
| @ended="onVideoEnded(index)" | |||
| ></video> | |||
| <!-- 播放按钮覆盖层 --> | |||
| <view v-if="!videoPlayingStates[index]" class="video-overlay" @click="playVideoFullscreen(item, index)"> | |||
| <view class="play-button-large"> | |||
| <view class="play-triangle"></view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <image v-else :src="item.image" mode="aspectFill" style="width: 100%; height: 100%;" @click="onImageClick(item, index)" /> | |||
| </swiper-item> | |||
| </swiper> | |||
| <!-- 备用静态图片,当没有轮播图数据时显示 --> | |||
| <image v-if="!bannerList || bannerList.length === 0" :src="fallbackImage" mode="aspectFill" style="width: 100%; height: 100%;" /> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'BannerSwiper', | |||
| props: { | |||
| // 轮播图数据 | |||
| bannerList: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| // 轮播图高度 | |||
| height: { | |||
| type: String, | |||
| default: '400rpx' | |||
| }, | |||
| // 备用图片 | |||
| fallbackImage: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| // 视频ID前缀,用于区分不同页面的视频 | |||
| videoIdPrefix: { | |||
| type: String, | |||
| default: 'video' | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| videoPlayingStates: {} // 记录每个视频的播放状态 | |||
| } | |||
| }, | |||
| methods: { | |||
| // 视频全屏播放 | |||
| playVideoFullscreen(item, index) { | |||
| if (!this.videoPlayingStates[index]) { | |||
| // 第一次点击:显示视频并开始播放 | |||
| this.$set(this.videoPlayingStates, index, true) | |||
| // 等待视频元素渲染后再进行操作 | |||
| this.$nextTick(() => { | |||
| setTimeout(() => { | |||
| const videoContext = uni.createVideoContext(`${this.videoIdPrefix}-${index}`, this) | |||
| if (videoContext) { | |||
| // 直接进入全屏播放,强制竖屏 | |||
| videoContext.requestFullScreen({ | |||
| direction: 0 // 竖屏方向,0代表竖屏,1代表横屏,-1自动 | |||
| }) | |||
| } | |||
| }, 200) | |||
| }) | |||
| } | |||
| }, | |||
| // 图片点击事件 | |||
| onImageClick(item, index) { | |||
| this.$emit('image-click', { item, index }) | |||
| }, | |||
| // 视频播放事件 | |||
| onVideoPlay(index) { | |||
| this.$set(this.videoPlayingStates, index, true) | |||
| }, | |||
| // 视频暂停事件 | |||
| onVideoPause(index) { | |||
| this.$set(this.videoPlayingStates, index, false) | |||
| }, | |||
| // 视频结束事件 | |||
| onVideoEnded(index) { | |||
| // 视频播放结束,回到预览状态 | |||
| this.$set(this.videoPlayingStates, index, false) | |||
| }, | |||
| // 全屏状态改变事件 | |||
| onFullscreenChange(e) { | |||
| console.log('全屏状态改变:', e.detail) | |||
| const videoIndex = e.target.id.replace(`${this.videoIdPrefix}-`, '') | |||
| if (e.detail.fullScreen) { | |||
| // 进入全屏时,不做任何操作 | |||
| console.log('进入全屏模式,方向:', e.detail.direction) | |||
| } else { | |||
| // 退出全屏时,回到预览状态 | |||
| console.log('退出全屏模式,回到预览状态') | |||
| this.$set(this.videoPlayingStates, videoIndex, false) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .banner { | |||
| width: 100%; | |||
| position: relative; | |||
| overflow: hidden; | |||
| border-radius: 0 0 30rpx 30rpx; | |||
| image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .video-container { | |||
| position: relative; | |||
| width: 100%; | |||
| height: 100%; | |||
| .video-poster { | |||
| cursor: pointer; | |||
| transition: transform 0.2s ease; | |||
| &:active { | |||
| transform: scale(0.98); | |||
| } | |||
| } | |||
| .video-overlay { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| background: rgba(0, 0, 0, 0.4); | |||
| z-index: 10; | |||
| cursor: pointer; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| .play-button-large { | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| background: transparent; | |||
| border-radius: 50%; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| backdrop-filter: blur(10rpx); | |||
| transition: all 0.3s ease; | |||
| box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.3); | |||
| &:active { | |||
| transform: scale(0.9); | |||
| background: rgba(255, 255, 255, 0.1); | |||
| } | |||
| .play-triangle { | |||
| width: 0; | |||
| height: 0; | |||
| border-left: 24rpx solid #fff; | |||
| border-top: 18rpx solid transparent; | |||
| border-bottom: 18rpx solid transparent; | |||
| margin-left: 8rpx; | |||
| transition: all 0.3s ease; | |||
| filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3)); | |||
| } | |||
| } | |||
| &:hover .play-button-large { | |||
| transform: scale(1.1); | |||
| box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4); | |||
| .play-triangle { | |||
| border-left-color: #ff8917; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,353 @@ | |||
| <template> | |||
| <!-- 回收规则弹窗 --> | |||
| <view v-if="showRulePopup" class="rule-popup-mask" @click.self="handleRulePopupMaskClick"> | |||
| <view class="rule-popup"> | |||
| <view class="rule-popup-title">{{ ruleTitle }}</view> | |||
| <scroll-view class="rule-popup-content" scroll-y @scroll="onRuleContentScroll" | |||
| @scrolltolower="onRuleScrollToLower"> | |||
| <uv-parse :content="ruleHtml" @ready="onRuleContentReady"></uv-parse> | |||
| <view class="rule-content-bottom-indicator"></view> | |||
| </scroll-view> | |||
| <view v-if="!hasScrolledToBottom" class="scroll-tip">请滚动到底部阅读完整内容</view> | |||
| <button class="rule-popup-btn" :class="{ disabled: !hasScrolledToBottom }" :disabled="!hasScrolledToBottom" | |||
| @click.stop="closeRulePopup">我知道了</button> | |||
| </view> | |||
| </view> | |||
| <!-- 预约上门取件弹窗 --> | |||
| <view v-if="showPickupConfirm" class="pickup-confirm-mask"> | |||
| <view class="pickup-confirm-popup"> | |||
| <view class="pickup-confirm-title">{{ confirmTitle }}</view> | |||
| <view class="pickup-confirm-content"> | |||
| <uv-parse :content="confirmContent"></uv-parse> | |||
| </view> | |||
| <view class="pickup-confirm-btn-row"> | |||
| <button class="pickup-confirm-btn" @click="handlePickupCancel">{{ cancelText }}</button> | |||
| <button class="pickup-confirm-btn agree" @click="handlePickupAgree">{{ confirmText }}</button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'RulePopup', | |||
| props: { | |||
| // 回收规则弹窗标题 | |||
| ruleTitle: { | |||
| type: String, | |||
| default: '回收规则' | |||
| }, | |||
| // 确认弹窗标题 | |||
| confirmTitle: { | |||
| type: String, | |||
| default: '温馨提示' | |||
| }, | |||
| // 确认弹窗内容 | |||
| confirmContent: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| // 取消按钮文本 | |||
| cancelText: { | |||
| type: String, | |||
| default: '取消回收' | |||
| }, | |||
| // 确认按钮文本 | |||
| confirmText: { | |||
| type: String, | |||
| default: '我同意' | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| showRulePopup: false, | |||
| showPickupConfirm: false, | |||
| ruleHtml: '', | |||
| hasScrolledToBottom: false | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开回收规则弹窗 | |||
| openRulePopup(ruleContent = '') { | |||
| this.ruleHtml = ruleContent || '<p>暂无回收规则</p>' | |||
| this.hasScrolledToBottom = false | |||
| this.showRulePopup = true | |||
| // 弹窗显示后主动判断内容是否需要滚动 | |||
| this.$nextTick(() => { | |||
| const query = uni.createSelectorQuery().in(this) | |||
| query.select('.rule-popup-content').boundingClientRect(rect => { | |||
| if (rect && rect.height && rect.scrollHeight && rect.scrollHeight <= rect.height + 10) { | |||
| this.hasScrolledToBottom = true | |||
| } | |||
| }).exec() | |||
| }) | |||
| }, | |||
| // 打开确认弹窗 | |||
| openConfirmPopup() { | |||
| this.showPickupConfirm = true | |||
| }, | |||
| // 关闭回收规则弹窗 | |||
| closeRulePopup() { | |||
| if (!this.hasScrolledToBottom) { | |||
| uni.showToast({ | |||
| title: '请阅读完整回收规则', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| this.showRulePopup = false | |||
| this.hasScrolledToBottom = false | |||
| this.$emit('rule-confirm') | |||
| }, | |||
| // 关闭确认弹窗 | |||
| closeConfirmPopup() { | |||
| this.showPickupConfirm = false | |||
| }, | |||
| // 处理规则弹窗遮罩点击 | |||
| handleRulePopupMaskClick() { | |||
| if (!this.hasScrolledToBottom) { | |||
| uni.showToast({ | |||
| title: '请阅读完整回收规则', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| this.closeRulePopup() | |||
| }, | |||
| // 处理取消回收 | |||
| handlePickupCancel() { | |||
| this.closeConfirmPopup() | |||
| this.$emit('pickup-cancel') | |||
| }, | |||
| // 处理确认回收 | |||
| handlePickupAgree() { | |||
| this.closeConfirmPopup() | |||
| this.$emit('pickup-confirm') | |||
| }, | |||
| // 监听规则内容滚动 | |||
| onRuleContentScroll(e) { | |||
| const { scrollTop, scrollHeight, clientHeight, height } = e.detail | |||
| const h = clientHeight || height | |||
| // 内容高度不够,无需滚动,直接允许 | |||
| if (scrollHeight <= h + 10) { | |||
| this.hasScrolledToBottom = true | |||
| return | |||
| } | |||
| if (scrollTop + h >= scrollHeight - 20) { | |||
| this.hasScrolledToBottom = true | |||
| } | |||
| }, | |||
| // 规则内容滚动到底部 | |||
| onRuleScrollToLower() { | |||
| this.hasScrolledToBottom = true | |||
| }, | |||
| // 规则内容准备完成 | |||
| onRuleContentReady() { | |||
| this.$nextTick(() => { | |||
| const query = uni.createSelectorQuery().in(this) | |||
| query.select('.rule-popup-content').boundingClientRect(rect => { | |||
| query.select('.rule-popup-content').scrollOffset(scroll => { | |||
| // 只有内容高度小于等于可视高度时才点亮 | |||
| if (scroll.scrollHeight <= rect.height + 1) { | |||
| this.hasScrolledToBottom = true | |||
| } | |||
| }) | |||
| }).exec() | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .rule-popup-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0, 0, 0, 0.35); | |||
| z-index: 4000; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .rule-popup { | |||
| width: 95vw; | |||
| max-width: 750rpx; | |||
| max-height: 85vh; | |||
| background: #fff; | |||
| border-radius: 48rpx; | |||
| box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| position: relative; | |||
| padding-bottom: 40rpx; | |||
| } | |||
| .rule-popup-title { | |||
| font-size: 36rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| text-align: center; | |||
| margin-top: 48rpx; | |||
| margin-bottom: 16rpx; | |||
| } | |||
| .rule-popup-content { | |||
| width: 100%; | |||
| max-height: 60vh; | |||
| min-height: 400rpx; | |||
| padding: 0 40rpx; | |||
| box-sizing: border-box; | |||
| overflow-y: auto; | |||
| scrollbar-width: none; | |||
| /* Firefox */ | |||
| -ms-overflow-style: none; | |||
| /* IE and Edge */ | |||
| &::-webkit-scrollbar { | |||
| width: 0 !important; | |||
| display: none; | |||
| /* Chrome, Safari, Opera */ | |||
| } | |||
| } | |||
| .rule-content-bottom-indicator { | |||
| height: 20rpx; | |||
| width: 100%; | |||
| } | |||
| .scroll-tip { | |||
| font-size: 24rpx; | |||
| color: #ff6b35; | |||
| text-align: center; | |||
| margin: 16rpx 0 8rpx 0; | |||
| animation: tipPulse 2s infinite; | |||
| } | |||
| @keyframes tipPulse { | |||
| 0%, | |||
| 100% { | |||
| opacity: 1; | |||
| } | |||
| 50% { | |||
| opacity: 0.6; | |||
| } | |||
| } | |||
| .rule-popup-btn { | |||
| width: 80%; | |||
| height: 88rpx; | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| border-radius: 44rpx; | |||
| color: #fff; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: none; | |||
| margin: 0 auto; | |||
| margin-top: 16rpx; | |||
| box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08); | |||
| transition: all 0.3s ease; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:active { | |||
| opacity: 0.9; | |||
| } | |||
| &.disabled { | |||
| background: #ccc; | |||
| color: #999; | |||
| box-shadow: none; | |||
| opacity: 0.6; | |||
| } | |||
| } | |||
| /* 预约上门取件弹窗样式 */ | |||
| .pickup-confirm-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0, 0, 0, 0.35); | |||
| z-index: 5000; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .pickup-confirm-popup { | |||
| width: 90vw; | |||
| max-width: 600rpx; | |||
| background: #fff; | |||
| border-radius: 48rpx; | |||
| box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| position: relative; | |||
| padding: 48rpx 36rpx 40rpx 36rpx; | |||
| } | |||
| .pickup-confirm-title { | |||
| font-size: 36rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| text-align: center; | |||
| margin-bottom: 24rpx; | |||
| } | |||
| .pickup-confirm-content { | |||
| width: 100%; | |||
| font-size: 26rpx; | |||
| color: #333; | |||
| text-align: left; | |||
| line-height: 1.7; | |||
| margin-bottom: 36rpx; | |||
| } | |||
| .pickup-confirm-btn-row { | |||
| width: 100%; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| gap: 32rpx; | |||
| } | |||
| .pickup-confirm-btn { | |||
| flex: 1; | |||
| height: 88rpx; | |||
| border-radius: 44rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: 2rpx solid #ffd01e; | |||
| background: #fff; | |||
| color: #ff9c00; | |||
| box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.08); | |||
| &:not(.agree) { | |||
| background: #fff0d2; | |||
| } | |||
| &.agree { | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| color: #fff; | |||
| border: none; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,272 @@ | |||
| <template> | |||
| <!-- 客服二维码弹窗 --> | |||
| <view v-if="showModal" class="qrcode-modal-mask" @click="closeModal"> | |||
| <view class="qrcode-modal-content" @click.stop> | |||
| <view class="qrcode-modal-header"> | |||
| <text class="qrcode-modal-title">联系客服</text> | |||
| <view class="qrcode-modal-close" @click="closeModal"> | |||
| <uni-icons type="close" size="24" color="#999"></uni-icons> | |||
| </view> | |||
| </view> | |||
| <view class="qrcode-modal-body"> | |||
| <image | |||
| v-if="serviceQrcodeUrl" | |||
| :src="serviceQrcodeUrl" | |||
| mode="aspectFit" | |||
| class="qrcode-modal-img" | |||
| :show-menu-by-longpress="true" | |||
| @longpress="onQrcodeLongPress" | |||
| ></image> | |||
| <view v-else class="qrcode-placeholder"> | |||
| <text>二维码加载中...</text> | |||
| </view> | |||
| <text class="qrcode-modal-tip">长按识别二维码添加客服微信</text> | |||
| <!-- <view class="qrcode-actions"> | |||
| <button class="save-btn" @click="saveQrcodeToAlbum" v-if="serviceQrcodeUrl"> | |||
| 保存到相册 | |||
| </button> | |||
| </view> --> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'ServiceQrcode', | |||
| data() { | |||
| return { | |||
| showModal: false | |||
| } | |||
| }, | |||
| computed: { | |||
| serviceQrcodeUrl() { | |||
| const item = getApp().globalData.configData.find(i => i.keyName === 'kefu_code') | |||
| return item ? item.keyContent : '' | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开客服二维码弹窗 | |||
| open() { | |||
| this.showModal = true | |||
| }, | |||
| // 关闭客服二维码弹窗 | |||
| close() { | |||
| this.showModal = false | |||
| this.$emit('close') | |||
| }, | |||
| // 关闭二维码弹窗(内部调用) | |||
| closeModal() { | |||
| this.close() | |||
| }, | |||
| // 处理二维码长按事件 | |||
| onQrcodeLongPress() { | |||
| console.log('长按二维码') | |||
| // 在微信小程序中,show-menu-by-longpress="true" 会自动处理长按识别 | |||
| // 这里可以添加一些反馈提示 | |||
| uni.showToast({ | |||
| title: '长按识别二维码', | |||
| icon: 'none', | |||
| duration: 1500 | |||
| }) | |||
| }, | |||
| // 保存二维码到相册 | |||
| saveQrcodeToAlbum() { | |||
| if (!this.serviceQrcodeUrl) { | |||
| uni.showToast({ | |||
| title: '二维码还未加载完成', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| // 先授权相册权限 | |||
| uni.getSetting({ | |||
| success: (res) => { | |||
| if (!res.authSetting['scope.writePhotosAlbum']) { | |||
| // 没有权限,申请权限 | |||
| uni.authorize({ | |||
| scope: 'scope.writePhotosAlbum', | |||
| success: () => { | |||
| this.doSaveQrcodeImage() | |||
| }, | |||
| fail: () => { | |||
| // 权限被拒绝,引导用户手动开启 | |||
| uni.showModal({ | |||
| title: '提示', | |||
| content: '需要您授权保存相册权限', | |||
| showCancel: false, | |||
| success: () => { | |||
| uni.openSetting() | |||
| } | |||
| }) | |||
| } | |||
| }) | |||
| } else { | |||
| // 已有权限,直接保存 | |||
| this.doSaveQrcodeImage() | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| // 执行保存二维码图片 | |||
| doSaveQrcodeImage() { | |||
| uni.downloadFile({ | |||
| url: this.serviceQrcodeUrl, | |||
| success: (res) => { | |||
| if (res.statusCode === 200) { | |||
| uni.saveImageToPhotosAlbum({ | |||
| filePath: res.tempFilePath, | |||
| success: () => { | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| icon: 'success' | |||
| }) | |||
| }, | |||
| fail: (err) => { | |||
| console.log('保存失败', err) | |||
| uni.showToast({ | |||
| title: '保存失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| }, | |||
| fail: (err) => { | |||
| console.log('下载失败', err) | |||
| uni.showToast({ | |||
| title: '下载失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // 客服二维码弹窗样式 | |||
| .qrcode-modal-mask { | |||
| position: fixed; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| background: rgba(0, 0, 0, 0.6); | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| z-index: 9999; | |||
| backdrop-filter: blur(5rpx); | |||
| } | |||
| .qrcode-modal-content { | |||
| background: #fff; | |||
| border-radius: 24rpx; | |||
| width: 600rpx; | |||
| max-width: 90vw; | |||
| animation: fadeInScale 0.3s ease; | |||
| overflow: hidden; | |||
| } | |||
| .qrcode-modal-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| padding: 40rpx 40rpx 20rpx 40rpx; | |||
| border-bottom: 1rpx solid #f0f0f0; | |||
| } | |||
| .qrcode-modal-title { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| } | |||
| .qrcode-modal-close { | |||
| padding: 10rpx; | |||
| margin: -10rpx; | |||
| } | |||
| .qrcode-modal-body { | |||
| padding: 40rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| } | |||
| .qrcode-modal-img { | |||
| width: 400rpx; | |||
| height: 400rpx; | |||
| border-radius: 16rpx; | |||
| margin-bottom: 30rpx; | |||
| box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | |||
| } | |||
| .qrcode-placeholder { | |||
| width: 400rpx; | |||
| height: 400rpx; | |||
| border-radius: 16rpx; | |||
| margin-bottom: 30rpx; | |||
| background: #f5f5f5; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| text { | |||
| color: #999; | |||
| font-size: 28rpx; | |||
| } | |||
| } | |||
| .qrcode-modal-tip { | |||
| font-size: 28rpx; | |||
| color: #666; | |||
| text-align: center; | |||
| line-height: 1.4; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .qrcode-actions { | |||
| display: flex; | |||
| justify-content: center; | |||
| margin-top: 20rpx; | |||
| } | |||
| .save-btn { | |||
| background: linear-gradient(90deg, #ff8917, #ffd01e); | |||
| color: #fff; | |||
| border: none; | |||
| border-radius: 25rpx; | |||
| padding: 16rpx 40rpx; | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:active { | |||
| opacity: 0.8; | |||
| } | |||
| } | |||
| @keyframes fadeInScale { | |||
| from { | |||
| opacity: 0; | |||
| transform: scale(0.8); | |||
| } | |||
| to { | |||
| opacity: 1; | |||
| transform: scale(1); | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,597 @@ | |||
| <template> | |||
| <!-- 品牌索引弹窗 --> | |||
| <view v-if="showBrandPopup" class="brand-popup-mask"> | |||
| <view class="brand-popup"> | |||
| <view class="brand-popup-header"> | |||
| <text class="brand-popup-close" @click="close">关闭</text> | |||
| <text class="brand-popup-title">可回收的品牌</text> | |||
| </view> | |||
| <view class="brand-popup-search"> | |||
| <input class="brand-search-input" v-model="brandSearch" placeholder="请输入要查询的内容" @input="onBrandSearchInput" /> | |||
| </view> | |||
| <scroll-view class="brand-popup-list" scroll-y :scroll-into-view="scrollToView"> | |||
| <!-- 热门品牌区域 --> | |||
| <view v-if="hotBrandList.length > 0 && !brandSearch" class="hot-brands-section"> | |||
| <view class="hot-brands-title">热门品牌</view> | |||
| <view class="hot-brands-grid"> | |||
| <view v-for="brand in hotBrandList" :key="brand.id" class="hot-brand-item" @click="openBrandConfirm(brand)"> | |||
| <image :src="brand.logo" class="hot-brand-logo" mode="aspectFit" /> | |||
| <text class="hot-brand-name">{{brand.name}}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view v-for="letter in brandIndexList" :key="letter" :id="'brand-letter-' + letter"> | |||
| <view class="brand-letter">{{letter}}</view> | |||
| <view v-for="brand in filteredBrandList.filter(b => b.letter === letter)" :key="brand.name" class="brand-item" @click="openBrandConfirm(brand)"> | |||
| <image :src="brand.logo" class="brand-logo" mode="aspectFit" /> | |||
| <text class="brand-name">{{brand.name}}</text> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| <view class="brand-index-bar"> | |||
| <text v-for="letter in brandIndexList" :key="letter" :class="{active: currentLetter === letter}" @click="scrollToLetter(letter)">{{letter}}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 品牌确认弹窗 --> | |||
| <view v-if="showBrandConfirm" class="brand-confirm-mask" @click.self="closeBrandConfirm"> | |||
| <view class="brand-confirm-popup"> | |||
| <view class="brand-confirm-title">品牌确认提示</view> | |||
| <view class="brand-confirm-logo-wrap"> | |||
| <image :src="brandConfirmInfo.logo" class="brand-confirm-logo" mode="aspectFit" /> | |||
| </view> | |||
| <view class="brand-confirm-name">{{ brandConfirmInfo.name }}</view> | |||
| <view class="brand-confirm-desc">请确认所选品牌是否与实物品牌信息一致,否则将无法进行回收。</view> | |||
| <view class="brand-confirm-btn-row"> | |||
| <button class="brand-confirm-btn retry" @click="closeBrandConfirm">重新选择</button> | |||
| <button class="brand-confirm-btn confirm" @click="confirmBrand">确认一致</button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 减少数量时的品牌选择弹窗 --> | |||
| <view v-if="showBrandReducePopup" class="brand-reduce-popup-mask" @click.self="closeBrandReducePopup"> | |||
| <view class="brand-reduce-popup"> | |||
| <view class="brand-reduce-popup-header"> | |||
| <text class="brand-reduce-popup-close" @click="closeBrandReducePopup">关闭</text> | |||
| <text class="brand-reduce-popup-title">选择要减少的品牌</text> | |||
| </view> | |||
| <scroll-view class="brand-reduce-popup-list" scroll-y> | |||
| <view v-for="brand in reduceBrandList" :key="brand.brandId" class="brand-item" @click="selectReduceBrand(brand)"> | |||
| <image :src="brand.logo" class="brand-logo" mode="aspectFit" /> | |||
| <text class="brand-name">{{brand.name}}</text> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| </view> | |||
| <!-- 商品款式选择组件 --> | |||
| <product-style-selector ref="styleSelector" @style-confirm="onStyleConfirm" @close="onStyleSelectorClose"></product-style-selector> | |||
| </template> | |||
| <script> | |||
| import { pinyin } from '../../utils/pinyin.js' | |||
| import ProductStyleSelector from './product-style-selector.vue' | |||
| export default { | |||
| name: 'BrandSelector', | |||
| components: { | |||
| ProductStyleSelector | |||
| }, | |||
| data() { | |||
| return { | |||
| showBrandPopup: false, | |||
| showBrandConfirm: false, | |||
| showBrandReducePopup: false, | |||
| brandConfirmInfo: { | |||
| logo: '', | |||
| name: '', | |||
| id: '' | |||
| }, | |||
| brandList: [], | |||
| hotBrandList: [], // 热门品牌列表 | |||
| currentLetter: 'A', | |||
| scrollToView: '', | |||
| brandSearch: '', | |||
| reduceBrandList: [], | |||
| currentProductId: null, | |||
| searchTimer: null | |||
| } | |||
| }, | |||
| computed: { | |||
| filteredBrandList() { | |||
| return this.brandList | |||
| }, | |||
| // 动态生成品牌字母索引,只显示有品牌的字母 | |||
| brandIndexList() { | |||
| const letters = new Set() | |||
| let hasSharp = false | |||
| this.brandList.forEach(b => { | |||
| if (b.letter && /^[A-Z]$/.test(b.letter)) { | |||
| letters.add(b.letter) | |||
| } else { | |||
| letters.add('#') | |||
| hasSharp = true | |||
| } | |||
| }) | |||
| const arr = Array.from(letters).filter(l => l !== '#').sort() | |||
| if (hasSharp) arr.push('#') | |||
| return arr | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开品牌选择弹窗 | |||
| open(productId) { | |||
| if (!productId) { | |||
| console.error('productId is required') | |||
| return | |||
| } | |||
| this.currentProductId = productId | |||
| this.getGoodsBrandList(productId) | |||
| this.showBrandPopup = true | |||
| }, | |||
| // 关闭品牌选择弹窗 | |||
| close() { | |||
| this.showBrandPopup = false | |||
| this.showBrandConfirm = false | |||
| this.showBrandReducePopup = false | |||
| // 清理搜索状态 | |||
| this.brandSearch = '' | |||
| this.currentProductId = null | |||
| // 清空品牌列表 | |||
| this.brandList = [] | |||
| this.hotBrandList = [] | |||
| if (this.searchTimer) { | |||
| clearTimeout(this.searchTimer) | |||
| this.searchTimer = null | |||
| } | |||
| this.$emit('close') | |||
| }, | |||
| // 打开减少品牌选择弹窗 | |||
| openReducePopup(brandList) { | |||
| this.reduceBrandList = brandList || [] | |||
| this.showBrandReducePopup = true | |||
| }, | |||
| // 关闭减少品牌选择弹窗 | |||
| closeBrandReducePopup() { | |||
| this.showBrandReducePopup = false | |||
| this.reduceBrandList = [] | |||
| this.$emit('reduce-close') | |||
| }, | |||
| // 选择要减少的品牌 | |||
| selectReduceBrand(brandInfo) { | |||
| this.closeBrandReducePopup() | |||
| this.$emit('reduce-select', brandInfo) | |||
| }, | |||
| // 滚动到指定字母 | |||
| scrollToLetter(letter) { | |||
| this.currentLetter = letter | |||
| this.scrollToView = 'brand-letter-' + letter | |||
| }, | |||
| // 打开品牌确认弹窗 | |||
| openBrandConfirm(brand) { | |||
| this.brandConfirmInfo = { | |||
| id: brand.id, | |||
| logo: brand.logo, | |||
| name: brand.name | |||
| } | |||
| this.showBrandConfirm = true | |||
| }, | |||
| // 关闭品牌确认弹窗 | |||
| closeBrandConfirm() { | |||
| this.showBrandConfirm = false | |||
| }, | |||
| // 确认品牌 | |||
| confirmBrand() { | |||
| this.showBrandConfirm = false | |||
| // 打开商品款式选择弹窗,传递已有数量 | |||
| this.$emit('get-existing-quantities', this.brandConfirmInfo.id, (existingQuantities) => { | |||
| this.$refs.styleSelector.open(this.brandConfirmInfo, this.currentProductId, existingQuantities) | |||
| }) | |||
| }, | |||
| // 处理款式确认事件 | |||
| onStyleConfirm(data) { | |||
| this.showBrandPopup = false | |||
| this.$emit('brand-confirm', { | |||
| brandInfo: data.brandInfo, | |||
| selectedStyles: data.selectedStyles | |||
| }) | |||
| }, | |||
| // 处理款式选择器关闭事件 | |||
| onStyleSelectorClose() { | |||
| // 款式选择器关闭时的处理逻辑 | |||
| }, | |||
| // 获取商品品牌列表 | |||
| getGoodsBrandList(productId, searchName = '') { | |||
| this.currentProductId = productId | |||
| const params = { productId } | |||
| if (searchName.trim()) { | |||
| params.name = searchName.trim() | |||
| } | |||
| this.$api('getGoodsBrandList', params, res => { | |||
| if (res && res.success && res.result && res.result.records) { | |||
| const allBrands = res.result.records.map(item => { | |||
| // 获取品牌名称的拼音首字母 | |||
| const firstChar = this.getPinyinFirstLetter(item.name) | |||
| return { | |||
| id: item.id, | |||
| logo: item.image || '/static/brand/alexander.png', | |||
| name: item.name, | |||
| letter: firstChar, | |||
| isPin: item.isPin, | |||
| hot: item.hot || 0, | |||
| ...item | |||
| } | |||
| }) | |||
| // 分离热门品牌和普通品牌 | |||
| this.hotBrandList = allBrands.filter(brand => brand.hot == 1) | |||
| this.brandList = allBrands.filter(brand => brand.hot != 1) | |||
| } | |||
| }) | |||
| }, | |||
| // 获取中文拼音首字母 | |||
| getPinyinFirstLetter(str) { | |||
| if (!str) return '#' | |||
| const firstChar = str.charAt(0) | |||
| // 遍历pinyin对象,查找包含该汉字的拼音 | |||
| for (let key in pinyin) { | |||
| const chars = pinyin[key] | |||
| if (chars && chars.indexOf(firstChar) !== -1) { | |||
| return key.charAt(0).toUpperCase() | |||
| } | |||
| } | |||
| // 英文首字母 | |||
| if (/^[A-Za-z]$/.test(firstChar)) { | |||
| return firstChar.toUpperCase() | |||
| } | |||
| return '#' | |||
| }, | |||
| // 品牌搜索输入事件处理 | |||
| onBrandSearchInput(e) { | |||
| const searchValue = e.detail.value | |||
| // 清除之前的定时器 | |||
| if (this.searchTimer) { | |||
| clearTimeout(this.searchTimer) | |||
| } | |||
| // 设置防抖,500ms后执行搜索 | |||
| this.searchTimer = setTimeout(() => { | |||
| if (this.currentProductId) { | |||
| this.getGoodsBrandList(this.currentProductId, searchValue) | |||
| } | |||
| }, 500) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .brand-popup-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0,0,0,0.35); | |||
| z-index: 3000; | |||
| display: flex; | |||
| align-items: flex-end; | |||
| justify-content: center; | |||
| } | |||
| .brand-popup { | |||
| position: relative; | |||
| width: 100%; | |||
| max-width: 750px; | |||
| background: #fff; | |||
| border-radius: 32rpx 32rpx 0 0; | |||
| box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08); | |||
| padding-bottom: 40rpx; | |||
| height: 94vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| } | |||
| .brand-popup-header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| padding: 32rpx 24rpx 0 24rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| position: relative; | |||
| } | |||
| .brand-popup-close { | |||
| position: absolute; | |||
| left: 24rpx; | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| } | |||
| .brand-popup-title { | |||
| font-size: 32rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| } | |||
| .brand-popup-search { | |||
| padding: 20rpx 24rpx 0 24rpx; | |||
| } | |||
| .brand-search-input { | |||
| height: 60rpx; | |||
| border-radius: 30rpx; | |||
| background: #f5f5f5; | |||
| border: none; | |||
| padding-left: 40rpx; | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| } | |||
| .brand-popup-list { | |||
| flex: 1; | |||
| overflow-y: auto; | |||
| max-height: calc(94vh - 160rpx); | |||
| padding: 0 24rpx; | |||
| scrollbar-width: none; /* Firefox */ | |||
| -ms-overflow-style: none; /* IE and Edge */ | |||
| &::-webkit-scrollbar { | |||
| width: 0 !important; | |||
| display: none; /* Chrome, Safari, Opera */ | |||
| } | |||
| } | |||
| .brand-letter { | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| background-color: #f8f8f8; | |||
| margin: 24rpx 0 0 0; | |||
| padding: 8rpx 8rpx; | |||
| font-weight: bold; | |||
| } | |||
| .brand-item { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 16rpx 0; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| } | |||
| .brand-logo { | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| margin-right: 20rpx; | |||
| border-radius: 8rpx; | |||
| background: #f8f8f8; | |||
| } | |||
| .brand-name { | |||
| font-size: 28rpx; | |||
| color: #222; | |||
| } | |||
| .brand-index-bar { | |||
| position: absolute; | |||
| right: 12rpx; | |||
| top: 120rpx; | |||
| width: 32rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| z-index: 10; | |||
| } | |||
| .brand-index-bar text { | |||
| font-size: 22rpx; | |||
| color: #bbb; | |||
| margin: 4rpx 0; | |||
| font-weight: bold; | |||
| &.active { | |||
| color: #ff9c00; | |||
| } | |||
| } | |||
| /* 热门品牌样式 */ | |||
| .hot-brands-section { | |||
| padding: 20rpx 0; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| } | |||
| .hot-brands-title { | |||
| font-size: 28rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .hot-brands-grid { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 10rpx; | |||
| } | |||
| .hot-brand-item { | |||
| width: calc((100% - 120rpx) / 3); | |||
| display: flex; | |||
| // flex-direction: column; | |||
| align-items: center; | |||
| padding: 2rpx 4rpx; | |||
| border-radius: 40rpx; | |||
| background: #f3f3f3; | |||
| border: 1px solid #eee; | |||
| &:active { | |||
| background: #eee; | |||
| } | |||
| } | |||
| .hot-brand-logo { | |||
| width: 48rpx; | |||
| height: 48rpx; | |||
| border-radius: 8rpx; | |||
| margin-bottom: 8rpx; | |||
| background: #fff; | |||
| flex-shrink: 0; | |||
| } | |||
| .hot-brand-name { | |||
| font-size: 22rpx; | |||
| color: #333; | |||
| text-align: center; | |||
| line-height: 1.2; | |||
| max-width: 100%; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .brand-confirm-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0,0,0,0.25); | |||
| z-index: 5001; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .brand-confirm-popup { | |||
| width: 70vw; | |||
| max-width: 270px; | |||
| background: #fff; | |||
| border-radius: 32rpx; | |||
| box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| padding: 48rpx 20rpx 36rpx 20rpx; | |||
| position: relative; | |||
| } | |||
| .brand-confirm-title { | |||
| font-size: 36rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| text-align: center; | |||
| margin-bottom: 24rpx; | |||
| } | |||
| .brand-confirm-logo-wrap { | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| background: #f8f8f8; | |||
| border-radius: 50%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-bottom: 18rpx; | |||
| } | |||
| .brand-confirm-logo { | |||
| width: 80rpx; | |||
| height: 80rpx; | |||
| border-radius: 50%; | |||
| } | |||
| .brand-confirm-name { | |||
| font-size: 28rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| text-align: center; | |||
| margin-bottom: 16rpx; | |||
| } | |||
| .brand-confirm-desc { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| text-align: center; | |||
| margin-bottom: 32rpx; | |||
| line-height: 1.6; | |||
| } | |||
| .brand-confirm-btn-row { | |||
| width: 100%; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| gap: 24rpx; | |||
| } | |||
| .brand-confirm-btn { | |||
| flex: 1; | |||
| height: 72rpx; | |||
| border-radius: 36rpx; | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: none; | |||
| margin: 0 0; | |||
| } | |||
| .brand-confirm-btn.retry { | |||
| background: #fff; | |||
| color: #ff9c00; | |||
| border: 2rpx solid #ff9c00; | |||
| } | |||
| .brand-confirm-btn.confirm { | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| color: #fff; | |||
| border: none; | |||
| } | |||
| /* 减少数量时的品牌选择弹窗 */ | |||
| .brand-reduce-popup-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0,0,0,0.35); | |||
| z-index: 5001; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .brand-reduce-popup { | |||
| width: 70vw; | |||
| max-width: 270px; | |||
| background: #fff; | |||
| border-radius: 32rpx; | |||
| box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| padding: 48rpx 20rpx 36rpx 20rpx; | |||
| position: relative; | |||
| } | |||
| .brand-reduce-popup-header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| padding: 32rpx 24rpx 0 24rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| position: relative; | |||
| } | |||
| .brand-reduce-popup-close { | |||
| position: absolute; | |||
| left: 24rpx; | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| } | |||
| .brand-reduce-popup-title { | |||
| font-size: 32rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| } | |||
| .brand-reduce-popup-list { | |||
| flex: 1; | |||
| overflow-y: auto; | |||
| max-height: 60vh; | |||
| padding: 0 24rpx; | |||
| scrollbar-width: none; /* Firefox */ | |||
| -ms-overflow-style: none; /* IE and Edge */ | |||
| &::-webkit-scrollbar { | |||
| width: 0 !important; | |||
| display: none; /* Chrome, Safari, Opera */ | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,533 @@ | |||
| <template> | |||
| <!-- 商品明细弹窗 --> | |||
| <view v-if="showDetailPopup" class="detail-popup-mask" @click.self="close"> | |||
| <view class="detail-popup" @click.stop> | |||
| <view class="detail-popup-header"> | |||
| <text class="detail-popup-close" @click="close">关闭</text> | |||
| <text class="detail-popup-title">商品明细</text> | |||
| </view> | |||
| <!-- 商品基本信息 --> | |||
| <view class="product-basic-info"> | |||
| <image :src="productInfo.image" class="product-image" mode="aspectFit" /> | |||
| <view class="product-info"> | |||
| <text class="product-name">{{ productInfo.name }}</text> | |||
| <text class="brand-name">品牌:{{ brandInfo.name }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 款式列表 --> | |||
| <scroll-view class="style-list" scroll-y> | |||
| <view v-for="(style, index) in styleList" :key="style.id" class="style-item"> | |||
| <image :src="style.image" class="style-image" mode="aspectFit" /> | |||
| <view class="style-info"> | |||
| <text class="style-name">{{ style.name }}</text> | |||
| <view class="style-price"> | |||
| <text class="price-symbol">¥</text> | |||
| <text class="price-value" v-if="style.minPrice === style.maxPrice">{{ style.minPrice }}</text> | |||
| <text class="price-value" v-else>{{ style.minPrice }}-{{ style.maxPrice }}</text> | |||
| <text class="price-unit">/件</text> | |||
| </view> | |||
| </view> | |||
| <view class="style-quantity"> | |||
| <button class="btn-minus" @click="updateQuantity(index, -1)">-</button> | |||
| <text class="quantity">{{ style.quantity || 0 }}</text> | |||
| <button class="btn-plus" @click="updateQuantity(index, 1)">+</button> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| <!-- 价格统计 --> | |||
| <view class="price-summary"> | |||
| <view class="summary-left"> | |||
| <text class="summary-label">已选 <text class="summary-count">{{ totalQuantity }}</text> 件,预计可得</text> | |||
| <view class="amount-row"> | |||
| <text class="amount" v-if="totalPriceRange.min === totalPriceRange.max">¥{{ totalPriceRange.min }}</text> | |||
| <text class="amount" v-else>¥{{ totalPriceRange.min }}-{{ totalPriceRange.max }}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 底部按钮 --> | |||
| <view class="detail-popup-footer"> | |||
| <button class="confirm-btn" @click="confirmChanges">确认修改</button> | |||
| </view> | |||
| <!-- 浮窗按钮 --> | |||
| <view class="floating-btn" @click="openStyleSelector"> | |||
| <text class="floating-btn-text">+</text> | |||
| </view> | |||
| <product-style-selector ref="styleSelector" @style-confirm="onStyleConfirm" @close="onStyleSelectorClose"></product-style-selector> | |||
| <!-- 款式选择器组件 --> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import productStyleSelector from './product-style-selector.vue' | |||
| export default { | |||
| name: 'ProductDetailPopup', | |||
| components: { | |||
| productStyleSelector | |||
| }, | |||
| data() { | |||
| return { | |||
| showDetailPopup: false, | |||
| productInfo: { | |||
| id: '', | |||
| name: '', | |||
| image: '' | |||
| }, | |||
| brandInfo: { | |||
| id: '', | |||
| name: '', | |||
| logo: '' | |||
| }, | |||
| styleList: [], | |||
| originalQuantities: {} // 保存原始数量,用于取消时恢复 | |||
| } | |||
| }, | |||
| computed: { | |||
| // 计算总价格范围 | |||
| totalPriceRange() { | |||
| const result = this.styleList.reduce((sum, style) => { | |||
| const quantity = style.quantity || 0 | |||
| if (quantity > 0) { | |||
| const minPrice = Number(style.minPrice) || 0 | |||
| const maxPrice = Number(style.maxPrice) || Number(style.minPrice) || 0 | |||
| sum.min += quantity * minPrice | |||
| sum.max += quantity * maxPrice | |||
| } | |||
| return sum | |||
| }, { min: 0, max: 0 }) | |||
| return { | |||
| min: result.min.toFixed(1), | |||
| max: result.max.toFixed(1) | |||
| } | |||
| }, | |||
| // 计算总数量 | |||
| totalQuantity() { | |||
| return this.styleList.reduce((sum, style) => sum + (style.quantity || 0), 0) | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开商品明细弹窗 | |||
| open(productInfo, brandInfo, existingQuantities = {}, styleCache = {}) { | |||
| if (!productInfo || !brandInfo) { | |||
| console.error('productInfo and brandInfo are required') | |||
| return | |||
| } | |||
| this.productInfo = productInfo | |||
| this.brandInfo = brandInfo | |||
| this.originalQuantities = { ...existingQuantities } | |||
| // 直接基于已选择的款式构建列表 | |||
| this.buildStyleListFromExisting(existingQuantities, styleCache) | |||
| this.showDetailPopup = true | |||
| }, | |||
| // 关闭弹窗 | |||
| close() { | |||
| this.showDetailPopup = false | |||
| this.styleList = [] | |||
| this.productInfo = { id: '', name: '', image: '' } | |||
| this.brandInfo = { id: '', name: '', logo: '' } | |||
| this.originalQuantities = {} | |||
| this.$emit('close') | |||
| }, | |||
| // 更新数量 | |||
| updateQuantity(index, delta) { | |||
| const style = this.styleList[index] | |||
| if (!style) return | |||
| let newQuantity = (style.quantity || 0) + delta | |||
| if (newQuantity < 0) newQuantity = 0 | |||
| this.$set(style, 'quantity', newQuantity) | |||
| }, | |||
| // 基于已选择的款式构建列表 | |||
| buildStyleListFromExisting(existingQuantities, styleCache) { | |||
| const styleList = [] | |||
| // 从父组件传入的已选择数据中提取款式信息 | |||
| Object.entries(existingQuantities).forEach(([uniqueKey, quantity]) => { | |||
| if (quantity > 0 && uniqueKey.startsWith(`${this.brandInfo.id}_`)) { | |||
| // 从styleCache中获取完整的款式信息 | |||
| const cacheInfo = styleCache[uniqueKey] | |||
| if (cacheInfo && cacheInfo.styleInfo) { | |||
| const styleInfo = cacheInfo.styleInfo | |||
| styleList.push({ | |||
| id: styleInfo.id, | |||
| name: styleInfo.name, | |||
| image: styleInfo.image || '/static/default-product.png', | |||
| minPrice: styleInfo.minPrice, | |||
| maxPrice: styleInfo.maxPrice, | |||
| brandId: styleInfo.brandId || this.brandInfo.id, | |||
| shopId: styleInfo.shopId, | |||
| quantity: quantity | |||
| }) | |||
| } | |||
| } | |||
| }) | |||
| this.styleList = styleList | |||
| }, | |||
| // 确认修改 | |||
| confirmChanges() { | |||
| const updatedStyles = this.styleList.filter(style => (style.quantity || 0) > 0) | |||
| this.$emit('confirm-changes', { | |||
| productInfo: this.productInfo, | |||
| brandInfo: this.brandInfo, | |||
| updatedStyles: updatedStyles | |||
| }) | |||
| this.close() | |||
| }, | |||
| // 打开款式选择器 | |||
| openStyleSelector() { | |||
| // 获取当前品牌下已有的款式数量 | |||
| const existingQuantities = {} | |||
| this.styleList.forEach(style => { | |||
| if (style.quantity > 0) { | |||
| const uniqueKey = `${this.brandInfo.id}_${style.id}` | |||
| existingQuantities[uniqueKey] = style.quantity | |||
| } | |||
| }) | |||
| // 直接打开内部的款式选择器 | |||
| this.$refs.styleSelector.open(this.brandInfo, this.productInfo.id, existingQuantities) | |||
| }, | |||
| // 处理款式确认事件 | |||
| onStyleConfirm(data) { | |||
| if (data.selectedStyles && data.selectedStyles.length > 0) { | |||
| // 更新当前的款式列表 | |||
| const newStyleList = [] | |||
| data.selectedStyles.forEach(style => { | |||
| if (style.quantity > 0) { | |||
| newStyleList.push({ | |||
| id: style.id, | |||
| name: style.name, | |||
| image: style.image || '/static/default-product.png', | |||
| minPrice: style.minPrice, | |||
| maxPrice: style.maxPrice, | |||
| brandId: style.brandId || this.brandInfo.id, | |||
| shopId: style.shopId, | |||
| quantity: style.quantity | |||
| }) | |||
| } | |||
| }) | |||
| this.styleList = newStyleList | |||
| // 不关闭当前弹窗,保持商品明细弹窗打开状态 | |||
| } | |||
| }, | |||
| // 处理款式选择器关闭事件 | |||
| onStyleSelectorClose() { | |||
| // 款式选择器关闭时的处理逻辑 | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .detail-popup-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0,0,0,0.25); | |||
| z-index: 5000; | |||
| display: flex; | |||
| align-items: flex-end; | |||
| justify-content: center; | |||
| } | |||
| .detail-popup { | |||
| position: relative; | |||
| width: 100%; | |||
| max-width: 750px; | |||
| background: #fff; | |||
| border-radius: 32rpx 32rpx 0 0; | |||
| box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08); | |||
| padding-bottom: 40rpx; | |||
| height: 84vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| } | |||
| .detail-popup-header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| padding: 32rpx 24rpx 0 24rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| position: relative; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| padding-bottom: 20rpx; | |||
| } | |||
| .detail-popup-close { | |||
| position: absolute; | |||
| left: 24rpx; | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| } | |||
| .detail-popup-title { | |||
| font-size: 32rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| } | |||
| .product-basic-info { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 24rpx; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| } | |||
| .product-image { | |||
| width: 80rpx; | |||
| height: 80rpx; | |||
| border-radius: 12rpx; | |||
| margin-right: 20rpx; | |||
| background: #f8f8f8; | |||
| flex-shrink: 0; | |||
| } | |||
| .product-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .product-name { | |||
| font-size: 30rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| margin-bottom: 8rpx; | |||
| } | |||
| .brand-name { | |||
| font-size: 26rpx; | |||
| color: #666; | |||
| } | |||
| .style-list { | |||
| flex: 1; | |||
| overflow-y: auto; | |||
| padding: 0 24rpx; | |||
| box-sizing: border-box; | |||
| max-height: calc(84vh - 200rpx); | |||
| scrollbar-width: none; | |||
| -ms-overflow-style: none; | |||
| &::-webkit-scrollbar { | |||
| width: 0 !important; | |||
| display: none; | |||
| } | |||
| } | |||
| .style-item { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 20rpx 0; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| &:last-child { | |||
| border-bottom: none; | |||
| } | |||
| } | |||
| .style-image { | |||
| width: 100rpx; | |||
| height: 100rpx; | |||
| border-radius: 12rpx; | |||
| margin-right: 20rpx; | |||
| background: #f8f8f8; | |||
| flex-shrink: 0; | |||
| } | |||
| .style-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| min-width: 0; | |||
| } | |||
| .style-name { | |||
| font-size: 28rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| margin-bottom: 8rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .style-price { | |||
| display: flex; | |||
| align-items: baseline; | |||
| } | |||
| .price-symbol { | |||
| font-size: 24rpx; | |||
| color: #ff7a0e; | |||
| } | |||
| .price-value { | |||
| font-size: 28rpx; | |||
| color: #ff7a0e; | |||
| font-weight: bold; | |||
| margin: 0 4rpx; | |||
| } | |||
| .price-unit { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| } | |||
| .style-quantity { | |||
| display: flex; | |||
| align-items: center; | |||
| flex-shrink: 0; | |||
| } | |||
| .btn-minus, .btn-plus { | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| border-radius: 50%; | |||
| background: #f8f8f8; | |||
| border: 1px solid #e0e0e0; | |||
| color: #666; | |||
| font-size: 28rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin: 0; | |||
| padding: 0; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:active { | |||
| background: #e0e0e0; | |||
| } | |||
| } | |||
| .quantity { | |||
| width: 60rpx; | |||
| text-align: center; | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| margin: 0 16rpx; | |||
| } | |||
| .price-summary { | |||
| padding: 20rpx 24rpx; | |||
| border-top: 1px solid #f0f0f0; | |||
| background: #fff; | |||
| } | |||
| .summary-left { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| } | |||
| .summary-label { | |||
| font-size: 26rpx; | |||
| color: #333; | |||
| } | |||
| .summary-count { | |||
| color: #ff9c00; | |||
| font-weight: bold; | |||
| font-size: 28rpx; | |||
| } | |||
| .amount-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-top: 4rpx; | |||
| } | |||
| .amount { | |||
| color: #ff9c00; | |||
| font-size: 44rpx; | |||
| font-weight: bold; | |||
| vertical-align: middle; | |||
| } | |||
| .detail-popup-footer { | |||
| padding: 24rpx; | |||
| border-top: 1px solid #f0f0f0; | |||
| background: #fff; | |||
| } | |||
| .confirm-btn { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| border-radius: 44rpx; | |||
| color: #fff; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| border: none; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:active { | |||
| opacity: 0.9; | |||
| } | |||
| } | |||
| .floating-btn { | |||
| position: absolute; | |||
| right: 24rpx; | |||
| bottom: 180rpx; | |||
| width: 100rpx; | |||
| height: 100rpx; | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| border-radius: 50%; | |||
| box-shadow: 0 4rpx 16rpx rgba(255, 156, 0, 0.3); | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| z-index: 10; | |||
| &:active { | |||
| opacity: 0.9; | |||
| transform: scale(0.95); | |||
| } | |||
| } | |||
| .floating-btn-text { | |||
| font-size: 48rpx; | |||
| color: #fff; | |||
| font-weight: bold; | |||
| line-height: 1; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,362 @@ | |||
| <template> | |||
| <!-- 商品款式选择弹窗 --> | |||
| <view v-if="showStylePopup" class="style-popup-mask"> | |||
| <view class="style-popup"> | |||
| <view class="style-popup-header"> | |||
| <text class="style-popup-close" @click="close">关闭</text> | |||
| <text class="style-popup-title">选择回收款式</text> | |||
| </view> | |||
| <view class="style-popup-brand-info"> | |||
| <image :src="brandInfo.logo" class="brand-logo" mode="aspectFit" /> | |||
| <text class="brand-name">{{ brandInfo.name }}</text> | |||
| </view> | |||
| <scroll-view class="style-popup-list" scroll-y> | |||
| <view class="style-grid"> | |||
| <view v-for="(item, index) in styleList" :key="item.id" class="style-item" @click="selectStyle(item, index)"> | |||
| <view class="style-item-content"> | |||
| <view class="image-container"> | |||
| <image :src="item.image" class="style-image" mode="aspectFill" /> | |||
| <view class="style-quantity"> | |||
| <button class="btn-minus" @click.stop="updateQuantity(index, -1)">-</button> | |||
| <text class="quantity">{{ item.quantity || 0 }}</text> | |||
| <button class="btn-plus" @click.stop="updateQuantity(index, 1)">+</button> | |||
| </view> | |||
| </view> | |||
| <text class="style-name">{{ item.name }}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| <view class="style-popup-footer"> | |||
| <button class="next-btn" @click="nextStep" :disabled="!hasSelectedItems">下一步</button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'ProductStyleSelector', | |||
| data() { | |||
| return { | |||
| showStylePopup: false, | |||
| brandInfo: { | |||
| id: '', | |||
| name: '', | |||
| logo: '' | |||
| }, | |||
| styleList: [], | |||
| currentProductId: null, | |||
| existingQuantities: {} | |||
| } | |||
| }, | |||
| computed: { | |||
| hasSelectedItems() { | |||
| return this.styleList.some(item => (item.quantity || 0) > 0) | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开款式选择弹窗 | |||
| open(brandInfo, productId, existingQuantities = {}) { | |||
| if (!brandInfo || !productId) { | |||
| console.error('brandInfo and productId are required') | |||
| return | |||
| } | |||
| this.brandInfo = brandInfo | |||
| this.currentProductId = productId | |||
| this.existingQuantities = existingQuantities | |||
| this.getProductStyles(brandInfo.id, productId) | |||
| this.showStylePopup = true | |||
| }, | |||
| // 关闭弹窗 | |||
| close() { | |||
| this.showStylePopup = false | |||
| this.styleList = [] | |||
| this.brandInfo = { id: '', name: '', logo: '' } | |||
| this.currentProductId = null | |||
| this.existingQuantities = {} | |||
| this.$emit('close') | |||
| }, | |||
| // 选择款式 | |||
| selectStyle(item, index) { | |||
| // 点击款式项时的处理逻辑 | |||
| console.log('Selected style:', item) | |||
| }, | |||
| // 更新数量 | |||
| updateQuantity(index, delta) { | |||
| const item = this.styleList[index] | |||
| if (!item) return | |||
| let newQuantity = (item.quantity || 0) + delta | |||
| if (newQuantity < 0) newQuantity = 0 | |||
| this.$set(item, 'quantity', newQuantity) | |||
| }, | |||
| // 获取商品款式列表 | |||
| getProductStyles(brandId, productId) { | |||
| const params = { | |||
| brandId: brandId, | |||
| productId: productId | |||
| } | |||
| this.$api('getGoodsBrandProduct', params, res => { | |||
| if (res && res.success && res.result && res.result) { | |||
| this.styleList = res.result.map(item => { | |||
| const uniqueKey = `${brandId}_${item.id}` | |||
| const existingQty = this.existingQuantities[uniqueKey] || 0 | |||
| return { | |||
| id: item.id, | |||
| name: item.name, | |||
| image: item.image || '/static/default-product.png', | |||
| minPrice: item.minPrice, | |||
| maxPrice: item.maxPrice, | |||
| brandId: item.brandId, | |||
| shopId: item.shopId, | |||
| quantity: existingQty, | |||
| ...item | |||
| } | |||
| }) | |||
| } else { | |||
| this.styleList = [] | |||
| } | |||
| }) | |||
| }, | |||
| // 下一步 | |||
| nextStep() { | |||
| const selectedItems = this.styleList.filter(item => (item.quantity || 0) > 0) | |||
| if (selectedItems.length === 0) { | |||
| uni.showToast({ | |||
| title: '请选择至少一个款式', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| this.$emit('style-confirm', { | |||
| brandInfo: this.brandInfo, | |||
| selectedStyles: selectedItems | |||
| }) | |||
| this.close() | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .style-popup-mask { | |||
| position: fixed; | |||
| left: 0; | |||
| right: 0; | |||
| top: 0; | |||
| bottom: 0; | |||
| background: rgba(0,0,0,0.35); | |||
| z-index: 3000; | |||
| display: flex; | |||
| align-items: flex-end; | |||
| justify-content: center; | |||
| } | |||
| .style-popup { | |||
| position: relative; | |||
| width: 100%; | |||
| max-width: 750px; | |||
| background: #fff; | |||
| border-radius: 32rpx 32rpx 0 0; | |||
| box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08); | |||
| height: 94vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| overflow: hidden; | |||
| } | |||
| .style-popup-header { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| padding: 32rpx 24rpx 0 24rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| position: relative; | |||
| } | |||
| .style-popup-close { | |||
| position: absolute; | |||
| left: 24rpx; | |||
| font-size: 28rpx; | |||
| color: #888; | |||
| } | |||
| .style-popup-title { | |||
| font-size: 32rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| } | |||
| .style-popup-brand-info { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 20rpx 24rpx; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| } | |||
| .brand-logo { | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| background: #f8f8f8; | |||
| } | |||
| .brand-name { | |||
| font-size: 28rpx; | |||
| color: #222; | |||
| font-weight: bold; | |||
| } | |||
| .style-popup-list { | |||
| flex: 1; | |||
| overflow-y: auto; | |||
| padding: 0 24rpx; | |||
| box-sizing: border-box; | |||
| scrollbar-width: none; | |||
| -ms-overflow-style: none; | |||
| &::-webkit-scrollbar { | |||
| width: 0 !important; | |||
| display: none; | |||
| } | |||
| } | |||
| .style-grid { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 20rpx; | |||
| padding: 20rpx 0; | |||
| } | |||
| .style-item { | |||
| width: calc((100% - 40rpx) / 3); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| border-radius: 16rpx; | |||
| background: #fff; | |||
| box-sizing: border-box; | |||
| } | |||
| .style-item-content { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| width: 100%; | |||
| } | |||
| .image-container { | |||
| position: relative; | |||
| width: 100%; | |||
| height: 200rpx; | |||
| margin-bottom: 16rpx; | |||
| } | |||
| .style-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| border-radius: 12rpx; | |||
| background: #f8f8f8; | |||
| } | |||
| .style-name { | |||
| font-size: 24rpx; | |||
| color: #222; | |||
| font-weight: 500; | |||
| text-align: center; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| width: 100%; | |||
| } | |||
| .style-quantity { | |||
| position: absolute; | |||
| bottom: 8rpx; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: rgba(255, 255, 255, 0.9); | |||
| border-radius: 24rpx; | |||
| padding: 4rpx 8rpx; | |||
| } | |||
| .btn-minus, .btn-plus { | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| border-radius: 50%; | |||
| background: #fff; | |||
| border: 1px solid #ddd; | |||
| color: #333; | |||
| font-size: 20rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin: 0; | |||
| padding: 0; | |||
| box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:active { | |||
| background: #f0f0f0; | |||
| } | |||
| } | |||
| .quantity { | |||
| width: 32rpx; | |||
| text-align: center; | |||
| font-size: 20rpx; | |||
| color: #333; | |||
| margin: 0 8rpx; | |||
| font-weight: bold; | |||
| } | |||
| .style-popup-footer { | |||
| padding: 20rpx 24rpx; | |||
| border-top: 1px solid #f0f0f0; | |||
| background: #fff; | |||
| } | |||
| .next-btn { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| background: linear-gradient(to right, #ffd01e, #ff8917); | |||
| border-radius: 44rpx; | |||
| color: #fff; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| border: none; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &:disabled { | |||
| background: #ccc; | |||
| color: #999; | |||
| } | |||
| &:active:not(:disabled) { | |||
| opacity: 0.9; | |||
| } | |||
| } | |||
| </style> | |||