前端-胡立永 7 months ago
parent
commit
12b08bda78
19 changed files with 4532 additions and 2564 deletions
  1. +6
    -1
      api/model/index.js
  2. +230
    -0
      compoent/base/banner-swiper.vue
  3. +353
    -0
      compoent/base/rule-popup.vue
  4. +272
    -0
      compoent/base/service-qrcode.vue
  5. +14
    -29
      compoent/base/tabbar.vue
  6. +597
    -0
      compoent/recycle/brand-selector.vue
  7. +533
    -0
      compoent/recycle/product-detail-popup.vue
  8. +362
    -0
      compoent/recycle/product-style-selector.vue
  9. +1
    -1
      config.js
  10. +28
    -442
      pages/component/home.vue
  11. +8
    -38
      pages/component/my.vue
  12. +10
    -46
      pages/component/recycle copy.vue
  13. +789
    -1239
      pages/component/recycle.vue
  14. +170
    -23
      pages/manager/inspect-result.vue
  15. +482
    -450
      pages/manager/inspect.vue
  16. +374
    -112
      pages/manager/order-detail.vue
  17. +7
    -4
      pages/subcomponent/detail.vue
  18. +295
    -179
      pages/subcomponent/pickup.vue
  19. +1
    -0
      uni.scss

+ 6
- 1
api/model/index.js View File

@ -188,6 +188,12 @@ const api = {
method: 'GET',
auth : false
},
// 根据商品标识查询商品品牌下的款式
getGoodsBrandProduct: {
url: '/recycle-admin/applet/class/getGoodsBrandProduct',
method: 'GET',
auth : false
},
// 2.根据分类标识获取分类商品列表带分页
getClassGoodsList: {
url: '/recycle-admin/applet/class/getClassGoodsList',
@ -258,7 +264,6 @@ const api = {
method: 'GET',
auth: true,
},
}
export default api

+ 230
- 0
compoent/base/banner-swiper.vue View File

@ -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 // 01-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>

+ 353
- 0
compoent/base/rule-popup.vue View File

@ -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>

+ 272
- 0
compoent/base/service-qrcode.vue View File

@ -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>

+ 14
- 29
compoent/base/tabbar.vue View File

@ -2,7 +2,7 @@
<view class="tabbar-box">
<view class="tabbar">
<view :class="{ 'tabbar-active' : select == item.key}" v-for="(item, index) in list" :key="index"
v-if="!item.isNotShop || !userShop" @click="toPath(item, index)" class="tabbar-item">
@click="toPath(item, index)" class="tabbar-item">
<view class="tabbar-icon">
<image :src="select == item.key ?
item.selectedIconPath :
@ -24,44 +24,29 @@
name: "tabbar",
props: ['select'],
computed: {
...mapGetters(['userShop']),
},
data() {
return {
list: [{
"selectedIconPath": "/static/image/tabbar/home-active.png",
"iconPath": "/static/image/tabbar/home.png",
"pagePath": "/pages/index/index",
"selectedIconPath": "/static/home/首页-点击.png",
"iconPath": "/static/home/首页-未点击.png",
"pagePath": "/pages/component/home",
"title": "首页",
key: 'home',
},
{
"selectedIconPath": "/static/image/tabbar/product-list-active.png",
"iconPath": "/static/image/tabbar/product-list.png",
"pagePath": "/pages/index/category",
"title": "商品列表",
key: 'category',
"selectedIconPath": "/static/home/回收-点击.png",
"iconPath": "/static/home/回收-未点击.png",
"pagePath": "/pages/component/recycle",
"title": "回收",
key: 'recycle',
},
{
"selectedIconPath": "/static/image/tabbar/order-active.png",
"iconPath": "/static/image/tabbar/order.png",
"pagePath": "/pages/index/order",
"title": "订单",
key: 'order',
},
{
"selectedIconPath": "/static/image/tabbar/cart-active.png",
"iconPath": "/static/image/tabbar/cart.png",
"pagePath": "/pages/index/cart",
"title": "购物车",
key: 'cart',
},
{
"selectedIconPath": "/static/image/tabbar/user-center-active.png",
"iconPath": "/static/image/tabbar/user-center.png",
"pagePath": "/pages/index/center",
"selectedIconPath": "/static/home/我的-点击.png",
"iconPath": "/static/home/我的-未点击.png",
"pagePath": "/pages/component/my",
"title": "我的",
key: 'center',
key: 'my',
}
]
};
@ -94,7 +79,7 @@
flex-direction: row;
height: 120rpx;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999999;
z-index: 999;
bottom: 0;
left: 0;
color: #BCBCBC;


+ 597
- 0
compoent/recycle/brand-selector.vue View File

@ -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>

+ 533
- 0
compoent/recycle/product-detail-popup.vue View File

@ -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>

+ 362
- 0
compoent/recycle/product-style-selector.vue View File

@ -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>

+ 1
- 1
config.js View File

@ -1,5 +1,5 @@
// config.js
const type = 'prod'
const type = 'local'
const config = {
local: {


+ 28
- 442
pages/component/home.vue View File

@ -1,54 +1,12 @@
<template>
<view class="container safe-area">
<!-- 顶部banner -->
<view class="banner">
<swiper :indicator-dots="false" :autoplay="true" :interval="3000" :duration="500" circular
style="width: 100%; height: 400rpx;">
<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="`video-${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="showServiceQrcode" />
</swiper-item>
</swiper>
</view>
<banner-swiper
:banner-list="bannerList"
height="400rpx"
video-id-prefix="home-video"
@image-click="onBannerImageClick"
></banner-swiper>
<view class="content">
<!-- 回收流程 -->
<view class="process-section">
@ -172,78 +130,31 @@
</view>
<!-- 根据角色显示不同的导航栏 -->
<uv-tabbar :value="value" :fixed="true" @change="changeTo">
<uv-tabbar-item text="首页">
<template v-slot:active-icon>
<image class="icon" src="/static/home/首页-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/首页-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="回收">
<template v-slot:active-icon>
<image class="icon" src="/static/home/回收-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/回收-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="我的">
<template v-slot:active-icon>
<image class="icon" src="/static/home/我的-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/我的-未点击.png"></image>
</template>
</uv-tabbar-item>
</uv-tabbar>
<tabbar select="home"></tabbar>
<!-- 客服二维码弹窗 -->
<view v-if="showQrcodeModal" class="qrcode-modal-mask" @click="closeQrcodeModal">
<view class="qrcode-modal-content" @click.stop>
<view class="qrcode-modal-header">
<text class="qrcode-modal-title">联系客服</text>
<view class="qrcode-modal-close" @click="closeQrcodeModal">
<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>
<service-qrcode ref="serviceQrcode" @close="closeQrcodeModal"></service-qrcode>
</view>
</template>
<script>
import pullRefreshMixin from '../mixins/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
import tabbar from '../../compoent/base/tabbar.vue'
import serviceQrcode from '../../compoent/base/service-qrcode.vue'
import bannerSwiper from '../../compoent/base/banner-swiper.vue'
export default {
mixins: [pullRefreshMixin],
components: {
tabbar,
serviceQrcode,
bannerSwiper
},
data() {
return {
value: 0,
processes: [],
priceList: [],
records: [],
videoPlayingStates: {}, //
showQrcodeModal: false, //
processes: [],
priceList: [],
records: [],
destinations: [
// {
// icon: '/static/home/.png',
@ -307,26 +218,9 @@
sbk_cion() {
const item = getApp().globalData.configData.find(i => i.keyName === 'sbk_cion')
return item ? item.keyContent : ''
},
serviceQrcodeUrl() {
const item = getApp().globalData.configData.find(i => i.keyName === 'kefu_code')
return item ? item.keyContent : ''
}
},
methods: {
changeTo(e) {
this.value = e
console.log(e, '111')
if (e == 1) {
uni.reLaunch({
url: '/pages/component/recycle'
}, true);
} else if (e == 2) {
uni.reLaunch({
url: '/pages/component/my'
}, true);
}
},
goAbout() {
uni.navigateTo({
url: '/pages/subcomponent/about'
@ -440,147 +334,22 @@
this.sbkCion = sbk ? sbk.keyContent : '';
},
//
playVideoFullscreen(item, index) {
if (!this.videoPlayingStates[index]) {
//
this.$set(this.videoPlayingStates, index, true);
//
this.$nextTick(() => {
setTimeout(() => {
const videoContext = uni.createVideoContext(`video-${index}`, this);
if (videoContext) {
//
videoContext.requestFullScreen({
direction: 0 // 01-1
});
}
}, 200);
});
//
onBannerImageClick({ item, index }) {
//
if (item.type !== 1) {
this.showServiceQrcode()
}
},
//
showServiceQrcode() {
this.showQrcodeModal = true;
this.$refs.serviceQrcode.open()
},
//
closeQrcodeModal() {
this.showQrcodeModal = false;
},
//
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'
});
}
});
},
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('video-', '');
if (e.detail.fullScreen) {
//
console.log('进入全屏模式,方向:', e.detail.direction);
} else {
// 退
console.log('退出全屏模式,回到预览状态');
this.$set(this.videoPlayingStates, videoIndex, false);
}
//
},
//
initializePageData() {
@ -658,86 +427,7 @@
padding-bottom: env(safe-area-inset-bottom);
}
.banner {
width: 100%;
height: 400rpx;
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);
// border: 3rpx solid rgba(255, 255, 255, 0.9);
&: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;
}
}
}
}
}
.content {
// flex: 1;
@ -1266,109 +956,5 @@
}
}
//
.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;
}
}
</style>

+ 8
- 38
pages/component/my.vue View File

@ -156,40 +156,19 @@
</view>
</view>
<!-- 根据角色显示不同的导航栏 -->
<uv-tabbar :value="value" :fixed="true" @change="changeTo">
<uv-tabbar-item text="首页">
<template v-slot:active-icon>
<image class="icon" src="/static/home/首页-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/首页-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="回收">
<template v-slot:active-icon>
<image class="icon" src="/static/home/回收-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/回收-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="我的">
<template v-slot:active-icon>
<image class="icon" src="/static/home/我的-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/我的-未点击.png"></image>
</template>
</uv-tabbar-item>
</uv-tabbar>
<tabbar select="my"></tabbar>
</view>
</template>
<script>
import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
import tabbar from '../../compoent/base/tabbar.vue'
export default {
mixins: [pullRefreshMixin],
components: {
tabbar
},
data() {
return {
value: 2,
@ -224,18 +203,9 @@ export default {
throw error //
}
},
changeTo(e) {
this.value = e
console.log(e, '111')
if (e == 0) {
uni.reLaunch({
url: '/pages/component/home'
});
} else if (e == 1) {
uni.reLaunch({
url: '/pages/component/recycle'
});
}
getSelectKey() {
const keys = ['home', 'recycle', 'my']
return keys[this.value] || 'my'
},
goWithdraw() {
uni.navigateTo({


+ 10
- 46
pages/component/recycle copy.vue View File

@ -186,38 +186,7 @@
</view>
<!-- 根据角色显示不同的导航栏 -->
<uv-tabbar
v-if="ishow"
:value="value"
:fixed="true"
@change="changeTo"
class="uv-tabbar"
>
<uv-tabbar-item text="首页" >
<template v-slot:active-icon>
<image class="icon" src="/static/home/首页-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/首页-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="回收" >
<template v-slot:active-icon>
<image class="icon" src="/static/home/回收-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/回收-未点击.png"></image>
</template>
</uv-tabbar-item>
<uv-tabbar-item text="我的" >
<template v-slot:active-icon>
<image class="icon" src="/static/home/我的-点击.png"></image>
</template>
<template v-slot:inactive-icon>
<image class="icon" src="/static/home/我的-未点击.png"></image>
</template>
</uv-tabbar-item>
</uv-tabbar>
<tabbar v-if="ishow" :select="getSelectKey()"></tabbar>
<!-- 品牌索引弹窗 -->
<view v-if="showBrandPopup" class="brand-popup-mask">
@ -296,8 +265,12 @@
<script>
import tabBarMixin from '../mixins/tabBarMixin.js'
import { pinyin } from '../../utils/pinyin.js'
import tabbar from '../../compoent/base/tabbar.vue'
export default {
mixins: [tabBarMixin],
components: {
tabbar
},
data() {
return {
value:1,
@ -395,19 +368,10 @@ export default {
console.log('closePriceInfoPopup called');
this.showPriceInfoPopup = false
},
changeTo(e){
this.value = e
if(e==2){
uni.reLaunch({
url: '/pages/component/my'
});
}else if(e==0){
console.log(e,'111')
uni.reLaunch({
url: '/pages/component/home'
});
}
},
getSelectKey() {
const keys = ['home', 'recycle', 'my']
return keys[this.value] || 'recycle'
},
fetchGoodsList(categoryId, page = 1, callback) {
this.$api('getClassGoodsList', {
classId: categoryId,
@ -1924,4 +1888,4 @@ export default {
}
}
}
</style>
</style>

+ 789
- 1239
pages/component/recycle.vue
File diff suppressed because it is too large
View File


+ 170
- 23
pages/manager/inspect-result.vue View File

@ -14,20 +14,22 @@
<!-- 合格产品卡片 -->
<view v-if="hasQualifiedItems" class="result-card">
<view class="card-title">合格产品</view>
<view v-for="item in qualifiedList" :key="item.shopId" class="result-group">
<view v-for="(item, index) in qualifiedList" :key="item.uniqueKey || index" class="result-group">
<view class="row-main">
<view class="goods-name-section">
<text class="goods-name">{{ item.shopId ? getGoodsName(item.shopId) : (item.id === 'unrecyclable' ? '不可回收'
: (item.id === 'quality_issue' ? '质量问题' : ''))}}</text>
<text class="goods-brand">{{ getItemBrand(item.shopId) }}</text>
<text class="goods-name">{{ getGoodsName(item.shopId) }}</text>
<text class="goods-brand">{{ getBrandStyleInfo(item) }}</text>
</view>
<text class="row-count">x{{ item.count }}</text>
<text class="row-total">¥{{ item.total }}</text>
</view>
<view class="price-input-row">
<text class="price-label">总金额</text>
<input class="price-input" v-model="item.total" type="digit" placeholder="请输入金额" />
</view>
<!-- 选择理由模块 -->
<view v-if="item.items && item.items.length > 0">
<view v-for="(commonItem, index) in item.items" :key="commonItem.id || index" class="row-reason">
<text class="reason-label">{{ getGoodsName(item.shopId) }}{{ index + 1 }}</text>
<view v-for="(commonItem, itemIndex) in item.items" :key="commonItem.id || itemIndex" class="row-reason">
<text class="reason-label">{{ getQualifiedItemLabel(item, itemIndex) }}</text>
<view class="reason-select" @click="selectReasonForQualified(commonItem, item)">
<text class="reason-placeholder" :class="{ 'selected': hasSelectedReason(commonItem) }">
{{ hasSelectedReason(commonItem) ? '已选择' : '请选择理由(选填)' }}
@ -48,7 +50,10 @@
<text class="goods-brand"></text>
</view>
<text class="row-count">x{{ group.count }}</text>
<text class="row-total">¥{{ group.total }}</text>
</view>
<view class="price-input-row">
<text class="price-label">总金额</text>
<input class="price-input" v-model="group.total" type="digit" placeholder="请输入金额" />
</view>
<!-- 选择理由模块 -->
<view v-if="inspectData && inspectData.list">
@ -75,7 +80,10 @@
<text class="goods-brand"></text>
</view>
<text class="row-count">x{{ item.count }}</text>
<text class="row-total">¥{{ item.total }}</text>
</view>
<view class="price-input-row">
<text class="price-label">总金额</text>
<input class="price-input" v-model="item.total" type="digit" placeholder="请输入金额" />
</view>
<!-- 选择理由模块 -->
<view v-if="inspectData && inspectData.list">
@ -222,15 +230,28 @@ export default {
const uniqueIds = Array.from(new Set(allIds.filter(Boolean)))
console.log('uniqueIds', uniqueIds)
await this.fetchGoodsNames(uniqueIds)
//
// -
this.inspectData["list"].forEach(item => {
if (item.qualifiedNum > 0 && item.id !== 'unrecyclable' && item.id !== 'quality_issue') {
//
const uniqueKey = item.uniqueKey || `${item.shopId}_${item.pinId || 'no-brand'}_${item.styleId || 'no-style'}`
this.qualifiedList.push({
shopId: item.shopId,
count: item.qualifiedNum,
total: item.price,
total: item.price || 0,
classId: item.categoryId,
items: item.commonOrderList ? [...item.commonOrderList] : []
brandName: item.brandName || '',
styleName: item.styleName || '',
pinId: item.pinId || '',
styleId: item.styleId || '',
uniqueKey: uniqueKey,
originalItem: item, //
items: item.commonOrderList ? [...item.commonOrderList] : [{
testingInstructions: '',
testingImages: '',
testingStatus: 0
}]
})
console.log('qualifiedList', this.qualifiedList)
}
@ -283,6 +304,29 @@ export default {
uni.navigateBack()
},
finishInspect() {
//
//
for (const item of this.qualifiedList) {
if (!item.total || item.total === '' || parseFloat(item.total) <= 0) {
uni.showToast({ title: '请填写合格产品金额', icon: 'none' })
return
}
}
//
for (const group of this.unqualifiedGroups) {
if (!group.total || group.total === '' || parseFloat(group.total) <= 0) {
uni.showToast({ title: '请填写不合格产品金额', icon: 'none' })
return
}
}
//
for (const item of this.unrecyclableList) {
if (!item.total || item.total === '' || parseFloat(item.total) <= 0) {
uni.showToast({ title: '请填写不可回收产品金额', icon: 'none' })
return
}
}
//
// 1.
// 2.
@ -311,6 +355,9 @@ export default {
return
}
}
// inspectData
this.syncPriceToInspectData()
// id
const processedList = this.inspectData["list"].map(item => {
if (item.id === 'quality_issue' || item.id === 'unrecyclable') {
@ -564,6 +611,79 @@ export default {
//
return (item.testingInstructions && item.testingInstructions.trim() !== '') ||
(item.testingImages && item.testingImages.trim() !== '')
},
getBrandStyleInfo(item) {
//
const brandName = item.brandName || ''
const styleName = item.styleName || ''
if (brandName && styleName) {
return `品牌:${brandName} | 款式:${styleName}`
} else if (brandName) {
return `品牌:${brandName}`
} else if (styleName) {
return `款式:${styleName}`
} else {
return this.getItemBrand(item.shopId)
}
},
getQualifiedItemLabel(item, index) {
//
const goodsName = this.getGoodsName(item.shopId)
const brandName = item.brandName || ''
const styleName = item.styleName || ''
if (brandName && styleName) {
return `(${brandName}-${styleName})`
} else if (brandName) {
return `(${brandName})`
} else {
return `${goodsName}`
}
},
syncPriceToInspectData() {
// -
this.qualifiedList.forEach(qualifiedItem => {
// 使uniqueKeyshopId+pinId+styleId
const inspectItem = this.inspectData.list.find(item => {
if (qualifiedItem.uniqueKey && item.uniqueKey) {
return item.uniqueKey === qualifiedItem.uniqueKey
}
//
return item.shopId === qualifiedItem.shopId &&
(item.pinId || '') === (qualifiedItem.pinId || '') &&
(item.styleId || '') === (qualifiedItem.styleId || '') &&
item.id !== 'unrecyclable' &&
item.id !== 'quality_issue'
})
if (inspectItem) {
inspectItem.price = parseFloat(qualifiedItem.total) || 0
//
if (qualifiedItem.items && qualifiedItem.items.length > 0) {
inspectItem.commonOrderList = qualifiedItem.items
}
}
})
//
this.unqualifiedGroups.forEach(group => {
const inspectItem = this.inspectData.list.find(item => item.id === 'quality_issue')
if (inspectItem) {
inspectItem.price = parseFloat(group.total) || 0
}
})
//
this.unrecyclableList.forEach(unrecyclableItem => {
const inspectItem = this.inspectData.list.find(item => item.id === 'unrecyclable')
if (inspectItem) {
inspectItem.price = parseFloat(unrecyclableItem.total) || 0
}
})
}
}
}
@ -598,8 +718,8 @@ export default {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
.back {
padding: 20rpx;
margin-left: -20rpx;
padding: 22rpx;
margin-left: -22rpx;
}
.nav-title {
@ -652,7 +772,7 @@ export default {
color: #ffb400;
font-size: 12px;
border-radius: 12px;
padding: 2px 14px;
padding: 2px 26rpx;
font-weight: 400;
height: 22px;
display: flex;
@ -674,14 +794,14 @@ export default {
margin-right: 8px;
.goods-name {
font-size: 14px;
font-size: 26rpx;
font-weight: bold;
color: #222;
line-height: 1.2;
}
.goods-brand {
font-size: 11px;
font-size: 22rpx;
color: #999;
font-weight: normal;
line-height: 1.2;
@ -690,7 +810,7 @@ export default {
}
.row-name {
font-size: 14px;
font-size: 26rpx;
font-weight: bold;
color: #222;
margin-right: 8px;
@ -703,13 +823,40 @@ export default {
}
.row-total {
font-size: 14px;
font-size: 26rpx;
color: #222;
font-weight: bold;
margin-left: auto;
}
}
.price-input-row {
display: flex;
align-items: center;
margin-top: 12px;
margin-bottom: 8px;
.price-label {
font-size: 26rpx;
color: #888;
min-width: 80px;
flex-shrink: 0;
}
.price-input {
flex: 1;
height: 40px;
border-radius: 12px;
background: #f6f6f6;
border: none;
font-size: 16px;
color: #222;
padding: 0 16px;
margin-left: 12px;
font-weight: bold;
}
}
.row-reason {
display: flex;
align-items: center;
@ -727,7 +874,7 @@ export default {
border-radius: 12px;
background: #f6f6f6;
border: none;
font-size: 14px;
font-size: 26rpx;
color: #222;
padding-left: 12px;
margin-left: 8px;
@ -740,7 +887,7 @@ export default {
height: 36px;
border-radius: 12px;
background: #f6f6f6;
font-size: 14px;
font-size: 26rpx;
color: #bbb;
padding-left: 12px;
margin-left: 8px;
@ -749,8 +896,8 @@ export default {
.reason-placeholder {
color: #bbb;
font-size: 14px;
font-size: 26rpx;
flex-shrink: 0;
&.selected {
color: #52c41a;
font-weight: 500;


+ 482
- 450
pages/manager/inspect.vue
File diff suppressed because it is too large
View File


+ 374
- 112
pages/manager/order-detail.vue
File diff suppressed because it is too large
View File


+ 7
- 4
pages/subcomponent/detail.vue View File

@ -89,13 +89,13 @@
<text class="order-value">{{ orderDetail && orderDetail.ordeNo ? orderDetail.ordeNo : orderId }}</text>
</view>
<view class="order-divider"></view>
<template v-if="status < 2">
<template>
<view class="order-row">
<text class="order-label">预估回收</text>
<text class="order-value order-highlight">¥ {{ estimatePrice }}</text>
</view>
</template>
<template v-else-if="status === 3">
<template v-if="status === 3">
<view class="order-row">
<text class="order-label">合格结算</text>
<text class="order-value order-highlight">¥ {{ finalPrice }}</text>
@ -116,7 +116,10 @@
<view class="goods-info">
<text class="goods-name">{{ item.title }}</text>
<text class="desc">{{ item.pinName }}</text>
<text class="goods-desc">{{ item.details }}</text>
<view class="desc" v-if="item.brandName && item.styleName">品牌{{ item.brandName }} | 款式{{ item.styleName }}</view>
<view class="desc" v-else-if="item.brandName">品牌{{ item.brandName }}</view>
<view class="desc" v-else-if="item.styleName">款式{{ item.styleName }}</view>
<text class="goods-desc" v-else>{{ item.details }}</text>
<view class="goods-meta">
<text class="goods-price" v-if="shouldShowSinglePrice(item.estimatedPrice)">¥ {{ item.onePrice }}<text class="goods-unit"> /{{item.unit}}</text></text>
<text class="goods-price" v-else>¥ {{ getSinglePriceRange(item.estimatedPrice, item.num) }}<text class="goods-unit"> /{{item.unit}}</text></text>
@ -1008,4 +1011,4 @@ letter-spacing: 0%;
margin-top: calc(150rpx + var(--status-bar-height));
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
}
</style>
</style>

+ 295
- 179
pages/subcomponent/pickup.vue View File

@ -1,7 +1,7 @@
<template>
<view class="pickup-container" :style="{paddingTop: navBarHeightRpx + 'rpx'}">
<view class="pickup-container" :style="{ paddingTop: navBarHeightRpx + 'rpx' }">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{height: (statusBarHeight + 88) + 'rpx', paddingTop: statusBarHeight + 'px'}">
<view class="nav-bar" :style="{ height: (statusBarHeight + 88) + 'rpx', paddingTop: statusBarHeight + 'px' }">
<view class="back" @tap="goBack">
<uni-icons type="left" size="20"></uni-icons>
</view>
@ -14,11 +14,7 @@
<view class="card process-card">
<view class="card-title process-title">回收流程</view>
<view class="process-steps">
<view
class="process-step-card"
v-for="(step, i) in steps"
:key="i"
>
<view class="process-step-card" v-for="(step, i) in steps" :key="i">
<image :src="step.icon" class="step-icon" mode="aspectFit" />
<view v-if="i === 0" class="step-bottom-bar">
<view class="step-num-bar">
@ -36,14 +32,14 @@
<view class="info-item" @tap="selectAddress">
<text class="label">取件地址</text>
<view class="value">
<text class="text" :class="{placeholder: !displayAddress}">{{ displayAddress || '请选择' }}</text>
<text class="text" :class="{ placeholder: !displayAddress }">{{ displayAddress || '请选择' }}</text>
<text class="arrow">></text>
</view>
</view>
<view class="info-item" @tap="openTimePicker">
<text class="label">上门时间</text>
<view class="value">
<text class="text" :class="{placeholder: !selectedTime}">{{ selectedTime || '请选择' }}</text>
<text class="text" :class="{ placeholder: !selectedTime }">{{ selectedTime || '请选择' }}</text>
<text class="arrow">></text>
</view>
</view>
@ -54,7 +50,8 @@
<view class="card order-card">
<view class="card-title process-title">订单详情</view>
<view class="order-items">
<view class="order-item" v-for="(item, index) in showAllItems ? selectedItems : selectedItems.slice(0, 3)" :key="index">
<view class="order-item" v-for="(item, index) in showAllItems ? selectedItems : selectedItems.slice(0, 3)"
:key="index">
<view class="item-left">
<image :src="item.icon" mode="aspectFit"></image>
<image v-if="item.brandImage" :src="item.brandImage" class="brand-logo" mode="aspectFit"></image>
@ -64,13 +61,20 @@
<text class="name">{{ item.name }}</text>
<text v-if="item.brandName" class="brand-tag">{{ item.brandName }}</text>
</view>
<!-- <view class="desc">{{ item.desc }}</view> -->
<view class="desc" v-if="item.brandName && item.styleName">品牌{{ item.brandName }} | 款式{{ item.styleName
}}</view>
<view class="desc" v-else-if="item.brandName">品牌{{ item.brandName }}</view>
<view class="desc" v-else-if="item.styleName">款式{{ item.styleName }}</view>
<view class="desc" v-else>{{ item.desc }}</view>
<view class="price-row">
<text class="price" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{ item.price || item.unitPrice }}/{{item.unit}}</text>
<text class="price" v-else>{{ item.price || item.unitPrice }}~{{ item.maxPrice }}/{{item.unit}}</text>
<text class="price" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{
item.price || item.unitPrice }}/{{ item.unit }}</text>
<text class="price" v-else>{{ item.price || item.unitPrice }}~{{ item.maxPrice }}/{{ item.unit }}</text>
<text class="count">x{{ item.quantity }}</text>
<text class="amount" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{ ((item.price || item.unitPrice) * item.quantity).toFixed(2) }}</text>
<text class="amount" v-else>{{ ((item.price || item.unitPrice) * item.quantity).toFixed(2) }}~{{ ((item.maxPrice) * item.quantity).toFixed(2) }}</text>
<text class="amount" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{
((item.price || item.unitPrice) * item.quantity).toFixed(2) }}</text>
<text class="amount" v-else>{{ ((item.price || item.unitPrice) * item.quantity).toFixed(2) }}~{{
((item.maxPrice) * item.quantity).toFixed(2) }}</text>
</view>
</view>
</view>
@ -92,7 +96,7 @@
<!-- 底部提交栏 -->
<view class="agreement-bar">
<view class="checkbox" :class="{active: agreed}" @tap="toggleAgreement">
<view class="checkbox" :class="{ active: agreed }" @tap="toggleAgreement">
<text v-if="agreed"></text>
</view>
<text>我已阅读并同意</text>
@ -105,7 +109,7 @@
<text>已选 {{ totalCount }} 预估回收可得</text>
<text class="amount">{{ totalPriceRange }}</text>
</view>
<button class="main-btn" @tap="submitOrder">预约上门取件</button>
<button class="main-btn" @tap="submitOrder">预约上门取件</button>
</view>
<!-- 时间选择弹窗 -->
@ -119,29 +123,22 @@
<view class="picker-section">
<view class="section-title">选择日期</view>
<view class="date-btns">
<view
v-for="(tab, index) in dateTabs"
:key="index"
:class="['date-btn', {active: currentDateTab === index}]"
@tap="selectDateTab(index)"
>
<view v-for="(tab, index) in dateTabs" :key="index"
:class="['date-btn', { active: currentDateTab === index }]" @tap="selectDateTab(index)">
{{ tab.label }}
</view>
</view>
</view>
<view class="picker-section">
<view class="section-title">选择时间</view>
<view class="time-btns">
<view
v-for="(slot, idx) in timeSlots"
:key="idx"
:class="['time-btn', {active: selectedTimeSlot === idx, disabled: !availableTimeSlots[idx]}]"
@tap="availableTimeSlots[idx] ? selectTimeSlot(idx) : null"
>
{{ slot }}
</view>
</view>
</view>
<view class="section-title">选择时间</view>
<view class="time-btns">
<view v-for="(slot, idx) in timeSlots" :key="idx"
:class="['time-btn', { active: selectedTimeSlot === idx, disabled: !availableTimeSlots[idx] }]"
@tap="availableTimeSlots[idx] ? selectTimeSlot(idx) : null">
{{ slot }}
</view>
</view>
</view>
<view class="confirm-btn" @tap="confirmTime">确认</view>
</view>
</view>
@ -188,7 +185,7 @@ export default {
onLoad(options) {
//
this.fromRecycle = options.fromRecycle === 'true'
//
if (this.fromRecycle && options.items) {
try {
@ -220,7 +217,7 @@ export default {
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
navBarHeight = menuButtonInfo.bottom + menuButtonInfo.top - sysInfo.statusBarHeight
} catch (e) {}
} catch (e) { }
this.navBarHeight = navBarHeight
this.navBarHeightRpx = Math.round(navBarHeight * 750 / sysInfo.windowWidth)
@ -238,38 +235,38 @@ export default {
},
computed: {
availableTimeSlots() {
const tab = this.dateTabs[this.currentDateTab];
if (!tab) return this.timeSlots.map(() => true);
const dateObj = tab.date;
const now = new Date();
return this.timeSlots.map(slot => {
const startTime = slot.split('~')[0];
const [h, m] = startTime.split(':');
const slotDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), h, m);
return slotDate > now;
});
},
const tab = this.dateTabs[this.currentDateTab];
if (!tab) return this.timeSlots.map(() => true);
const dateObj = tab.date;
const now = new Date();
return this.timeSlots.map(slot => {
const startTime = slot.split('~')[0];
const [h, m] = startTime.split(':');
const slotDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), h, m);
return slotDate > now;
});
},
totalCount() {
return this.selectedItems.reduce((sum, item) => sum + item.quantity, 0)
},
totalPriceRange() {
if (this.selectedItems.length === 0) return '0.0'
let minTotal = 0
let maxTotal = 0
this.selectedItems.forEach(item => {
const minPrice = item.price || item.unitPrice || 0
const maxPrice = item.maxPrice || minPrice || 0
minTotal += minPrice * item.quantity
maxTotal += maxPrice * item.quantity
})
//
if (minTotal === maxTotal) {
return minTotal.toFixed(2)
}
return `${minTotal.toFixed(2)}~${maxTotal.toFixed(2)}`
},
canSubmit() {
@ -311,29 +308,29 @@ export default {
this.currentDateTab = index
},
selectTimeSlot(index) {
if (this.availableTimeSlots[index]) {
this.selectedTimeSlot = index;
}
},
confirmTime() {
if (this.selectedTimeSlot === -1) {
uni.showToast({ title: '请选择可用时间段', icon: 'none' });
return;
}
const tab = this.dateTabs[this.currentDateTab];
const dateObj = tab.date;
const timeStr = this.timeSlots[this.selectedTimeSlot];
const startTime = timeStr.split('~')[0];
const yyyy = dateObj.getFullYear();
const mm = (dateObj.getMonth() + 1).toString().padStart(2, '0');
const dd = dateObj.getDate().toString().padStart(2, '0');
this.selectedTime = `${yyyy}-${mm}-${dd} ${startTime}:00`;
this.closeTimePicker();
},
resetPicker() {
this.currentDateTab = 0;
this.selectedTimeSlot = -1;
},
if (this.availableTimeSlots[index]) {
this.selectedTimeSlot = index;
}
},
confirmTime() {
if (this.selectedTimeSlot === -1) {
uni.showToast({ title: '请选择可用时间段', icon: 'none' });
return;
}
const tab = this.dateTabs[this.currentDateTab];
const dateObj = tab.date;
const timeStr = this.timeSlots[this.selectedTimeSlot];
const startTime = timeStr.split('~')[0];
const yyyy = dateObj.getFullYear();
const mm = (dateObj.getMonth() + 1).toString().padStart(2, '0');
const dd = dateObj.getDate().toString().padStart(2, '0');
this.selectedTime = `${yyyy}-${mm}-${dd} ${startTime}:00`;
this.closeTimePicker();
},
resetPicker() {
this.currentDateTab = 0;
this.selectedTimeSlot = -1;
},
toggleAgreement() {
this.agreed = !this.agreed
},
@ -343,66 +340,70 @@ export default {
showPrivacyPolicy() {
uni.showModal({ title: '隐私政策', content: '这里展示隐私政策内容' })
},
submitOrder() {
if (!this.agreed) {
uni.showToast({ title: '请先同意服务协议', icon: 'none' })
return
}
if (!this.displayAddress || this.displayAddress === '请选择取件地址') {
uni.showToast({ title: '请选择取件地址', icon: 'none' })
return
}
if (!this.selectedTime) {
uni.showToast({ title: '请选择上门时间', icon: 'none' })
return
}
if (this.selectedItems.length === 0) {
uni.showToast({ title: '请选择回收物品', icon: 'none' })
return
}
const list = this.selectedItems.map(item => {
const orderItem = {
shopId: item.id,
num: item.quantity
};
// ID
if (item.brandId) {
orderItem.pinId = item.brandId;
} else if (item.pinId) {
orderItem.pinId = item.pinId;
}
return orderItem;
});
console.log({
addressId: this.addressId,
strTime: this.selectedTime,
list: list
},'createOrder');
//
uni.showLoading({ title: '提交中...' })
this.$api('createOrder', {
addressId: this.addressId,
strTime: this.selectedTime,
list: JSON.stringify(list)
}, (res) => {
if (res && res.success) {
console.log(res,'createOrder-res');
uni.showToast({ title: '预约成功', icon: 'success' })
uni.redirectTo({
url: `/pages/subcomponent/detail?id=${res.result.id}`
})
submitOrder() {
if (!this.agreed) {
uni.showToast({ title: '请先同意服务协议', icon: 'none' })
return
}
if (!this.displayAddress || this.displayAddress === '请选择取件地址') {
uni.showToast({ title: '请选择取件地址', icon: 'none' })
return
}
})
// setTimeout(() => {
// uni.hideLoading()
// uni.showToast({ title: '', icon: 'success' })
// setTimeout(() => {
// uni.navigateBack()
// }, 1500)
// }, 1000)
},
if (!this.selectedTime) {
uni.showToast({ title: '请选择上门时间', icon: 'none' })
return
}
if (this.selectedItems.length === 0) {
uni.showToast({ title: '请选择回收物品', icon: 'none' })
return
}
const list = this.selectedItems.map(item => {
const orderItem = {
shopId: item.id,
num: item.quantity
};
// ID
if (item.brandId) {
orderItem.pinId = item.brandId;
} else if (item.pinId) {
orderItem.pinId = item.pinId;
}
// ID
if (item.styleId) {
orderItem.styleId = item.styleId;
}
return orderItem;
});
console.log({
addressId: this.addressId,
strTime: this.selectedTime,
list: list
}, 'createOrder');
//
uni.showLoading({ title: '提交中...' })
this.$api('createOrder', {
addressId: this.addressId,
strTime: this.selectedTime,
list: JSON.stringify(list)
}, (res) => {
if (res && res.success) {
console.log(res, 'createOrder-res');
uni.showToast({ title: '预约成功', icon: 'success' })
uni.redirectTo({
url: `/pages/subcomponent/detail?id=${res.result.id}`
})
}
})
// setTimeout(() => {
// uni.hideLoading()
// uni.showToast({ title: '', icon: 'success' })
// setTimeout(() => {
// uni.navigateBack()
// }, 1500)
// }, 1000)
},
toggleExpandOrder() {
this.showAllItems = !this.showAllItems
},
@ -472,10 +473,12 @@ export default {
align-items: center;
background: #fff;
padding: 0 30rpx;
.back {
padding: 20rpx;
margin-left: -20rpx;
}
.title {
flex: 1;
text-align: center;
@ -483,10 +486,13 @@ export default {
font-weight: 500;
color: #222;
}
.right-btns {
display: flex;
gap: 30rpx;
.more, .scan {
.more,
.scan {
font-size: 40rpx;
color: #333;
}
@ -498,23 +504,26 @@ export default {
}
.card {
background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
background: linear-gradient(to bottom, #fff3db 0%, #fffefb 40%);
border-radius: 20rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.03);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
}
.process-card {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 149, 0, 0.08);
padding: 0 0 20rpx 0;
}
.process-steps {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0 30rpx 30rpx;
.process-step-card {
background: #FFF8ED;
border-radius: 24rpx;
@ -525,11 +534,13 @@ export default {
align-items: center;
margin-right: 24rpx;
position: relative;
.step-icon {
width: 64rpx;
height: 64rpx;
margin: 24rpx 0 18rpx 0;
}
.step-bottom-bar {
position: absolute;
left: 0;
@ -541,11 +552,13 @@ export default {
display: flex;
align-items: center;
justify-content: center;
.step-num-bar {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
.num-main {
width: 32rpx;
height: 32rpx;
@ -559,6 +572,7 @@ export default {
font-weight: 600;
margin-right: 10rpx;
}
.text-main {
color: #fff;
font-size: 26rpx;
@ -566,17 +580,19 @@ export default {
}
}
}
.step-label-gray {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 56rpx;
background: #FFF8ED;
border-radius: 0 0 24rpx 24rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 56rpx;
background: #FFF8ED;
border-radius: 0 0 24rpx 24rpx;
display: flex;
align-items: center;
justify-content: center;
.num-gray {
width: 32rpx;
height: 32rpx;
@ -590,6 +606,7 @@ export default {
font-weight: 600;
margin-right: 10rpx;
}
.text-gray {
color: #bbb;
font-size: 26rpx;
@ -597,31 +614,41 @@ export default {
}
}
}
.process-step-card:last-child {
margin-right: 0;
}
}
.divider {
height: 1rpx;
background: rgba(0, 0, 0, 0.05);
margin: 0 30rpx;
}
.pickup-info {
padding: 0 30rpx;
.info-item {
padding: 30rpx 0;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
&:last-child { border-bottom: none; }
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
display: block;
}
.value {
display: flex;
justify-content: space-between;
align-items: center;
.text {
flex: 1;
font-size: 28rpx;
@ -630,7 +657,11 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
}
.text.placeholder { color: #ccc; }
.text.placeholder {
color: #ccc;
}
.arrow {
color: #999;
font-size: 28rpx;
@ -639,27 +670,36 @@ export default {
}
}
}
.order-card {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.04);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.04);
margin-bottom: 20rpx;
.order-items {
padding: 0 30rpx;
.order-item {
display: flex;
align-items: flex-start;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child { border-bottom: none; }
&:last-child {
border-bottom: none;
}
.item-left {
position: relative;
margin-right: 20rpx;
image {
width: 80rpx;
height: 80rpx;
image {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
}
.brand-logo {
position: absolute;
bottom: -8rpx;
@ -671,19 +711,23 @@ export default {
background: #fff;
}
}
.item-info {
flex: 1;
.name-brand-row {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 4rpx;
.name {
font-size: 30rpx;
color: #333;
font-weight: 500;
.name {
font-size: 30rpx;
color: #333;
font-weight: 500;
margin-right: 12rpx;
}
.brand-tag {
background: #FFE8CC;
color: #FF9500;
@ -693,47 +737,99 @@ export default {
border: 1rpx solid #FFD4A0;
}
}
.desc { font-size: 24rpx; color: #999; margin: 4rpx 0 8rpx 0; }
.desc {
font-size: 24rpx;
color: #999;
margin: 4rpx 0 8rpx 0;
}
.price-row {
display: flex;
align-items: center;
.price { color: #FF9500; font-size: 26rpx; margin-right: 10rpx; }
.count { color: #999; font-size: 24rpx; margin-right: 10rpx; }
.amount { color: #333; font-size: 28rpx; margin-left: auto; }
.price {
color: #FF9500;
font-size: 26rpx;
margin-right: 10rpx;
}
.count {
color: #999;
font-size: 24rpx;
margin-right: 10rpx;
}
.amount {
color: #333;
font-size: 28rpx;
margin-left: auto;
}
}
}
}
}
.expand-btn {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 10rpx 0;
.arrow { font-size: 20rpx; }
.arrow {
font-size: 20rpx;
}
}
}
.agreement-bar {
display: flex;
align-items: center;
background: #fffbe6;
padding: 20rpx 30rpx;
font-size: 24rpx;
.checkbox {
width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid #FFB74D;
margin-right: 10rpx; display: flex; align-items: center; justify-content: center;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #FFB74D;
margin-right: 10rpx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
&.active { background: #FFB74D; color: #fff; }
&.active {
background: #FFB74D;
color: #fff;
}
}
.link {
color: #FFB74D;
}
.link { color: #FFB74D; }
}
.bottom-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 20rpx 30rpx calc(40rpx + env(safe-area-inset-bottom));
.summary { color: #666; font-size: 26rpx; }
.amount { color: #FF9500; font-size: 32rpx; font-weight: bold; margin-left: 10rpx; }
.summary {
color: #666;
font-size: 26rpx;
}
.amount {
color: #FF9500;
font-size: 32rpx;
font-weight: bold;
margin-left: 10rpx;
}
.main-btn {
background: #FFB74D;
color: #fff;
@ -744,15 +840,20 @@ export default {
height: 80rpx;
display: flex;
justify-content: center;
&[disabled] { opacity: 0.5; }
&[disabled] {
opacity: 0.5;
}
}
}
.order-desc {
color: #999;
font-size: 22rpx;
padding: 0 30rpx 20rpx 30rpx;
line-height: 1.7;
}
.time-picker {
position: fixed;
left: 0;
@ -760,6 +861,7 @@ export default {
top: 0;
bottom: 0;
z-index: 1000;
.mask {
position: absolute;
left: 0;
@ -768,6 +870,7 @@ export default {
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.picker-content {
position: absolute;
left: 0;
@ -776,16 +879,19 @@ export default {
background: #fff;
border-radius: 20rpx 20rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
.picker-header {
padding: 30rpx 0 0 0;
display: flex;
align-items: center;
border-bottom: 1rpx solid #eee;
.reset {
color: #bbb;
font-size: 28rpx;
margin-left: 30rpx;
}
.title {
flex: 1;
text-align: center;
@ -795,19 +901,25 @@ export default {
margin-right: 60rpx;
}
}
.picker-section {
padding: 30rpx 30rpx 0 30rpx;
.section-title {
font-size: 28rpx;
color: #222;
margin-bottom: 20rpx;
}
.date-btns, .time-btns {
.date-btns,
.time-btns {
display: flex;
flex-wrap: wrap;
gap: 20rpx 20rpx;
}
.date-btn, .time-btn {
.date-btn,
.time-btn {
width: 200rpx;
height: 70rpx;
background: #f5f5f5;
@ -820,13 +932,16 @@ export default {
border: 2rpx solid transparent;
margin-bottom: 10rpx;
}
.date-btn.active, .time-btn.active {
.date-btn.active,
.time-btn.active {
background: #fff;
color: #FFB74D;
border: 2rpx solid #FFB74D;
font-weight: 500;
}
}
.confirm-btn {
margin: 40rpx 30rpx 30rpx 30rpx;
height: 90rpx;
@ -841,10 +956,11 @@ export default {
}
}
}
.process-title {
font-size: 32rpx;
font-weight: bold;
background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
background: linear-gradient(to bottom, #fff3db 0%, #fffefb 40%);
color: #222;
text-align: left;
padding: 36rpx 0 24rpx 30rpx;
@ -858,4 +974,4 @@ export default {
border-color: #eee;
pointer-events: none;
}
</style>
</style>

+ 1
- 0
uni.scss View File

@ -13,6 +13,7 @@
*/
/* 颜色变量 */
$uni-color: #000;
/* 行为相关颜色 */
$uni-color-primary: #007aff;


Loading…
Cancel
Save