refactor: 重构用户头像上传逻辑,改用微信原生选择方式 fix: 修复订单列表和详情页的状态显示问题 perf: 优化日历组件显示节假日价格 docs: 更新价格配置和功能说明文档 style: 调整优惠券组件样式和布局 chore: 更新uni-calendar和ksp-cropper组件版本 build: 添加价格配置JSON文件 ci: 更新pages.json路由配置master
| @ -0,0 +1,299 @@ | |||
| <template> | |||
| <view style="padding:20rpx;"> | |||
| <view> | |||
| <view class="card"> | |||
| <view class="card-left"> | |||
| <view class=""> | |||
| {{switchType(couponData.stockType)}} | |||
| </view> | |||
| </view> | |||
| <view class="card-center"> | |||
| <view class="card-center-top"></view> | |||
| <view class="card-center-bottom"></view> | |||
| </view> | |||
| <view class="card-right"> | |||
| <view class="card-content"> | |||
| <view class="card-info">{{couponData.stockName}}</view> | |||
| <view class="card-type">可用于 | |||
| <text class="card-type-text">专业喂养</text> | |||
| <text class="card-type-text">专业遛狗</text> | |||
| <!-- <text class="card-type-text">{{ couponData.goodsName }}</text> --> | |||
| </view> | |||
| <view class="card-time">有效期至: {{couponData.availableEndTime ? couponData.availableEndTime.slice(0, 16) : ''}}</view> | |||
| </view> | |||
| <view :class="['coupon-btn', { 'coupon-btn-disabled': !canReceiveCoupon }]" @click="handleReceiveCoupon"> | |||
| <text class="coupon-btn-text">{{ getCouponButtonText }}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view class="card-bottom"> | |||
| <view class="card-bottom-text"> | |||
| 优惠券不可兑换现金 | |||
| </view> | |||
| <view class="card-bottom-text" @click="showRulePopup"> | |||
| 查看详细规则> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { receiveCoupon } from "@/api/system/user" | |||
| export default { | |||
| name: 'CouponItem', | |||
| props: { | |||
| // 优惠券数据 | |||
| couponData: { | |||
| type: Object, | |||
| default: () => ({}) | |||
| } | |||
| }, | |||
| computed: { | |||
| // 判断优惠券是否可以领取 | |||
| canReceiveCoupon() { | |||
| // 如果已经领取过,则不能再领取 | |||
| if (this.couponData.alreadyReceived && this.couponData.alreadyReceived >= this.couponData.maxCouponsPerUser) { | |||
| return false; | |||
| } | |||
| return true; | |||
| }, | |||
| // 获取按钮文本 | |||
| getCouponButtonText() { | |||
| if (!this.canReceiveCoupon) { | |||
| return '已领取'; | |||
| } | |||
| return '立即领取'; | |||
| } | |||
| }, | |||
| methods: { | |||
| // 切换优惠券类型显示 | |||
| switchType(type) { | |||
| if (type == 'PNORMAL') { | |||
| return '满减券' | |||
| } | |||
| if (type == 'PDISCOUNT') { | |||
| return '折扣券' | |||
| } | |||
| if (type == 'PTRAIL') { | |||
| return '体验券' | |||
| } | |||
| return '优惠券' | |||
| }, | |||
| // 处理优惠券领取点击事件 | |||
| handleReceiveCoupon() { | |||
| if (!this.canReceiveCoupon) { | |||
| return; // 已领取的优惠券不能再次领取 | |||
| } | |||
| // 直接调用API领取优惠券 | |||
| this.receiveCouponApi(this.couponData.id); | |||
| }, | |||
| // 调用优惠券领取API | |||
| receiveCouponApi(id) { | |||
| let data = { | |||
| stockId: id | |||
| } | |||
| receiveCoupon(data).then(res => { | |||
| console.log("receiveCoupon response:", res) | |||
| if (res.code == 200) { | |||
| // 显示成功提示 | |||
| if (this.$modal && this.$modal.showToast) { | |||
| this.$modal.showToast('优惠券领取成功') | |||
| } else { | |||
| uni.showToast({ | |||
| title: '优惠券领取成功', | |||
| icon: 'success' | |||
| }) | |||
| } | |||
| // 更新本地优惠券状态 | |||
| this.updateLocalCouponStatus(); | |||
| // 通知父组件优惠券已领取 | |||
| this.$emit('coupon-received', this.couponData); | |||
| } else { | |||
| // 显示失败提示 | |||
| if (this.$modal && this.$modal.showToast) { | |||
| this.$modal.showToast('领取优惠券失败') | |||
| } else { | |||
| uni.showToast({ | |||
| title: '领取优惠券失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| }).catch(err => { | |||
| console.error('领取优惠券失败:', err) | |||
| // 显示错误提示 | |||
| if (this.$modal && this.$modal.showToast) { | |||
| this.$modal.showToast('领取优惠券失败') | |||
| } else { | |||
| uni.showToast({ | |||
| title: '领取优惠券失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| }, | |||
| // 更新本地优惠券状态 | |||
| updateLocalCouponStatus() { | |||
| // 创建更新后的优惠券数据副本 | |||
| const updatedCoupon = { ...this.couponData }; | |||
| // 如果alreadyReceived字段不存在,初始化为0 | |||
| if (!updatedCoupon.alreadyReceived) { | |||
| updatedCoupon.alreadyReceived = 0; | |||
| } | |||
| // 累加已领取次数 | |||
| updatedCoupon.alreadyReceived += 1; | |||
| // 通知父组件更新数据 | |||
| this.$emit('update-coupon', updatedCoupon); | |||
| }, | |||
| // 显示优惠券规则弹窗 | |||
| showRulePopup() { | |||
| // 触发父组件的显示规则事件 | |||
| this.$emit('show-rule', this.couponData); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .card { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| padding: 10px 0; | |||
| background: #fff; | |||
| border-radius: 8px 8px 0 0; | |||
| } | |||
| .card-bottom { | |||
| display: flex; | |||
| background-color: #FFF1E0; | |||
| height: 50rpx; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| padding: 0 20rpx 0 20rpx; | |||
| border-radius: 0 0 8px 8px; | |||
| .card-bottom-text { | |||
| color: #AAAAAA; | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| } | |||
| } | |||
| .card-left { | |||
| width: 88px; | |||
| text-align: center; | |||
| color: #FF530A; | |||
| font-size: 28rpx; | |||
| font-weight: 900; | |||
| } | |||
| .card-center { | |||
| display: flex; | |||
| flex-direction: column; | |||
| .card-center-top { | |||
| width: 40rpx; | |||
| height: 20rpx; | |||
| border-radius: 0 0 20rpx 20rpx; | |||
| background-color: #F5F5F7; | |||
| line-height: 20rpx; | |||
| margin-top: -22rpx; | |||
| margin-bottom: 20rpx; | |||
| margin-left: -19rpx; | |||
| } | |||
| .card-center-bottom { | |||
| border-right: 1px dashed #AAAAAA; | |||
| width: 1px; | |||
| height: 120rpx; | |||
| } | |||
| } | |||
| .card-right { | |||
| padding: 0px 12px; | |||
| display: flex; | |||
| flex: 1; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| height: 60px; | |||
| .card-content { | |||
| width: 77%; | |||
| } | |||
| .card-icon { | |||
| position: relative; | |||
| right: -10px; | |||
| top: -10px; | |||
| } | |||
| } | |||
| .card-info { | |||
| margin: 0; | |||
| font-size: 28rpx; | |||
| line-height: 28rpx; | |||
| color: #333333; | |||
| font-weight: 500; | |||
| } | |||
| .card-type { | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| font-weight: 400; | |||
| color: #AAAAAA; | |||
| margin-top: 10rpx; | |||
| .card-type-text { | |||
| color: #FFAA48; | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| border: #FFAA48 1px solid; | |||
| border-radius: 7rpx; | |||
| margin-left: 8rpx; | |||
| } | |||
| } | |||
| .card-time { | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| font-weight: 400; | |||
| color: #AAAAAA; | |||
| margin-top: 10rpx; | |||
| } | |||
| // 优惠券按钮样式 | |||
| .coupon-btn { | |||
| width: 132rpx; | |||
| height: 52rpx; | |||
| background-color: #FFAA48; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border-radius: 56rpx; | |||
| cursor: pointer; | |||
| transition: all 0.3s ease; | |||
| &.coupon-btn-disabled { | |||
| background-color: #CCCCCC; | |||
| cursor: not-allowed; | |||
| opacity: 0.6; | |||
| } | |||
| } | |||
| .coupon-btn-text { | |||
| font-size: 24rpx; | |||
| font-weight: 500; | |||
| color: #FFFFFF; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,198 @@ | |||
| <template> | |||
| <uni-popup ref="popup" type="center"> | |||
| <view class="rule-popup"> | |||
| <view class="rule-popup-title">优惠券详细规则</view> | |||
| <view class="rule-popup-content"> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">名称:</view> | |||
| <view class="rule-value">{{couponData.stockName || ''}}</view> | |||
| </view> | |||
| <!-- <view class="rule-item" v-if="couponData.comment"> | |||
| <view class="rule-label">备注:</view> | |||
| <view class="rule-value">{{couponData.comment}}</view> | |||
| </view> --> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">类型:</view> | |||
| <view class="rule-value">{{getStockTypeText(couponData)}}</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">优惠内容:</view> | |||
| <view class="rule-value">{{getDiscountText(couponData)}}</view> | |||
| </view> | |||
| <view class="rule-item" v-if="couponData.transactionMinimum"> | |||
| <view class="rule-label">消费门槛:</view> | |||
| <view class="rule-value">满{{couponData.transactionMinimum}}元可用</view> | |||
| </view> | |||
| <view class="rule-item" v-if="couponData.goodsName"> | |||
| <view class="rule-label">适用范围:</view> | |||
| <view class="rule-value">{{couponData.goodsName}}</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">有效期:</view> | |||
| <view class="rule-value">{{getValidTimeText(couponData)}}</view> | |||
| </view> | |||
| <!-- <view class="rule-item" v-if="couponData.maxCouponsPerUser"> | |||
| <view class="rule-label">领取限制:</view> | |||
| <view class="rule-value">每人最多可领{{couponData.maxCouponsPerUser}}张</view> | |||
| </view> --> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">特别说明:</view> | |||
| <view class="rule-value">单笔订单仅限使用1张优惠券;优惠券仅限用户本人使用,不可赠送、不可提现、不得找零。</view> | |||
| </view> | |||
| </view> | |||
| <view class="rule-popup-close" @click="close">关闭</view> | |||
| </view> | |||
| </uni-popup> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'CouponRulePopup', | |||
| data() { | |||
| return { | |||
| couponData: {} | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开弹窗 | |||
| open(couponData = {}) { | |||
| this.couponData = couponData; | |||
| this.$refs.popup.open(); | |||
| }, | |||
| // 关闭弹窗 | |||
| close() { | |||
| this.$refs.popup.close(); | |||
| }, | |||
| // 获取批次类型文本 | |||
| getStockTypeText(coupon) { | |||
| if (!coupon || !coupon.stockType) return ''; | |||
| const typeMap = { | |||
| 'PNORMAL': '平台满减券', | |||
| 'PDISCOUNT': '平台折扣券', | |||
| 'PTRAIL': '平台体验券', | |||
| 'NORMAL': '微信满减券', | |||
| 'DISCOUNT': '微信折扣券', | |||
| 'EXCHANGE': '微信换购券' | |||
| }; | |||
| return typeMap[coupon.stockType] || coupon.stockType; | |||
| }, | |||
| // 获取优惠券折扣文本 | |||
| getDiscountText(coupon) { | |||
| if (!coupon || !coupon.stockType) return ''; | |||
| if (coupon.stockType === 'PNORMAL') { | |||
| // 平台满减券 | |||
| if (coupon.discountAmount && coupon.transactionMinimum) { | |||
| return `满${coupon.transactionMinimum}元减${coupon.discountAmount}元`; | |||
| } else if (coupon.discountAmount) { | |||
| return `减${coupon.discountAmount}元`; | |||
| } | |||
| return '满减优惠'; | |||
| } else if (coupon.stockType === 'PDISCOUNT') { | |||
| // 平台折扣券 | |||
| if (coupon.discountPercent) { | |||
| return `${coupon.discountPercent / 10}折优惠`; | |||
| } | |||
| return '折扣优惠'; | |||
| } else if (coupon.stockType === 'PTRAIL') { | |||
| // 平台体验券 | |||
| return '免费体验'; | |||
| } else if (coupon.stockType === 'NORMAL') { | |||
| // 微信满减券 | |||
| if (coupon.discountAmount && coupon.transactionMinimum) { | |||
| return `满${coupon.transactionMinimum}元减${coupon.discountAmount}元`; | |||
| } | |||
| return '满减优惠'; | |||
| } else if (coupon.stockType === 'DISCOUNT') { | |||
| // 微信折扣券 | |||
| if (coupon.discountPercent) { | |||
| return `${coupon.discountPercent}折优惠`; | |||
| } | |||
| return '折扣优惠'; | |||
| } else if (coupon.stockType === 'EXCHANGE') { | |||
| // 微信换购券 | |||
| return '换购优惠'; | |||
| } | |||
| return ''; | |||
| }, | |||
| // 获取有效期文本 | |||
| getValidTimeText(coupon) { | |||
| if (!coupon) return ''; | |||
| const beginTime = coupon.availableBeginTime ? coupon.availableBeginTime.slice(0, 16).replace('T', ' ') : ''; | |||
| const endTime = coupon.availableEndTime ? coupon.availableEndTime.slice(0, 16).replace('T', ' ') : ''; | |||
| if (beginTime && endTime) { | |||
| return `${beginTime} 至 ${endTime}`; | |||
| } else if (endTime) { | |||
| return `截止至 ${endTime}`; | |||
| } else if (beginTime) { | |||
| return `${beginTime} 开始生效`; | |||
| } | |||
| // 如果有领取后生效天数 | |||
| if (coupon.availableDayAfterReceive) { | |||
| return `领取后${coupon.availableDayAfterReceive}天开始生效`; | |||
| } | |||
| return '长期有效'; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .rule-popup { | |||
| width: 600rpx; | |||
| background-color: #FFFFFF; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| } | |||
| .rule-popup-title { | |||
| height: 100rpx; | |||
| line-height: 100rpx; | |||
| text-align: center; | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: #FFFFFF; | |||
| background-color: #FFAA48; | |||
| } | |||
| .rule-popup-content { | |||
| padding: 30rpx; | |||
| } | |||
| .rule-item { | |||
| display: flex; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .rule-label { | |||
| width: 140rpx; | |||
| font-size: 28rpx; | |||
| color: #666666; | |||
| flex-shrink: 0; | |||
| } | |||
| .rule-value { | |||
| flex: 1; | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| line-height: 40rpx; | |||
| } | |||
| .rule-popup-close { | |||
| height: 90rpx; | |||
| line-height: 90rpx; | |||
| text-align: center; | |||
| font-size: 30rpx; | |||
| color: #FFAA48; | |||
| border-top: 1px solid #EEEEEE; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,112 @@ | |||
| # 品种选择功能说明 | |||
| ## 功能概述 | |||
| 根据设计图要求,新建了一个专门的品种选择页面,替换了原有的弹窗式品种选择功能。 | |||
| ## 主要特性 | |||
| ### 1. 页面布局 | |||
| - **系统导航栏**:橙色背景,显示"选择品种"标题 | |||
| - **搜索栏**:支持实时搜索品种名称 | |||
| - **品种列表**:按字母分组显示,支持滚动 | |||
| - **字母索引**:右侧固定位置,支持快速跳转到指定字母 | |||
| ### 2. 功能实现 | |||
| #### 品种数据获取 | |||
| - 根据宠物类型(猫/狗)获取对应的品种数据 | |||
| - 支持从API动态获取品种列表 | |||
| #### 搜索功能 | |||
| - 实时搜索,支持中文和英文搜索 | |||
| - 搜索结果实时更新显示 | |||
| #### 字母分组 | |||
| - 自动按品种名称首字母分组 | |||
| - 支持中文字符的拼音首字母映射 | |||
| - 按字母顺序排列显示 | |||
| - 每个字母组内的品种按中文拼音排序 | |||
| #### 快速导航 | |||
| - 右侧字母索引支持点击跳转 | |||
| - 滚动动画效果 | |||
| - 当前字母高亮显示 | |||
| - 滚动时自动更新激活的字母索引 | |||
| ### 3. 页面路由 | |||
| **新增页面路径**:`/pages/personalCenter/breedSelect` | |||
| **页面配置**: | |||
| ```json | |||
| { | |||
| "path": "pages/personalCenter/breedSelect", | |||
| "style": { | |||
| "navigationBarTitleText": "选择品种", | |||
| "navigationBarBackgroundColor": "#FF6B35", | |||
| "enablePullDownRefresh": false, | |||
| "navigationBarTextStyle": "white" | |||
| } | |||
| } | |||
| ``` | |||
| ### 4. 使用方式 | |||
| 在需要选择品种的地方,调用以下代码跳转到品种选择页面: | |||
| ```javascript | |||
| uni.navigateTo({ | |||
| url: `/pages/personalCenter/breedSelect?petType=${petType}&selectedBreed=${encodeURIComponent(selectedBreed || '')}` | |||
| }); | |||
| ``` | |||
| ### 5. 数据传递 | |||
| - **输入参数**: | |||
| - `petType`:宠物类型('cat' 或 'dog') | |||
| - `selectedBreed`:当前已选择的品种(可选) | |||
| - **输出结果**: | |||
| - 选择品种后自动返回上一页 | |||
| - 自动更新上一页的品种数据 | |||
| ### 6. 样式特点 | |||
| - 使用系统导航栏,符合设计图要求 | |||
| - 简洁的白色背景设计 | |||
| - 流畅的动画效果 | |||
| - 响应式布局 | |||
| - 字母索引激活状态有橙色背景高亮 | |||
| ## 技术实现 | |||
| ### 核心组件 | |||
| - `breedSelect.vue`:品种选择页面主组件 | |||
| - 修改了 `petBaseInfo.vue`:移除原有弹窗,改为页面跳转 | |||
| ### 主要方法 | |||
| - `getPetBreeds()`:获取品种数据 | |||
| - `groupBreedsByLetter()`:按字母分组 | |||
| - `onSearchChange()`:搜索处理 | |||
| - `selectBreed()`:选择品种 | |||
| - `scrollToLetter()`:字母跳转 | |||
| - `onScroll()`:滚动事件处理 | |||
| - `updateCurrentLetter()`:更新当前激活字母 | |||
| ### 数据映射 | |||
| - 中文字符到拼音首字母的映射表 | |||
| - 支持常见的中文品种名称 | |||
| ## 兼容性 | |||
| - 完全兼容原有的品种选择逻辑 | |||
| - 保持数据格式不变 | |||
| - 支持现有API接口 | |||
| ## 注意事项 | |||
| 1. 确保API接口 `getDictList` 正常工作 | |||
| 2. 品种数据需要包含 `dictLabel` 字段 | |||
| 3. 页面需要正确配置在 `pages.json` 中 | |||
| 4. 建议在真机上测试滚动和点击效果 | |||
| @ -0,0 +1,71 @@ | |||
| # 性格选择功能说明 | |||
| ## 功能概述 | |||
| 新增了独立的性格选择页面,用户可以在该页面中: | |||
| 1. 输入宠物的性格描述 | |||
| 2. 选择预设的性格标签 | |||
| 3. 保存后回显到宠物信息页面 | |||
| ## 文件结构 | |||
| ### 新增文件 | |||
| - `pages/personalCenter/personalitySelect.vue` - 性格选择页面 | |||
| ### 修改文件 | |||
| - `pages/personalCenter/components/petBaseInfo.vue` - 修改性格选择逻辑,跳转到新页面 | |||
| - `pages.json` - 添加新页面路由配置 | |||
| ## 功能特点 | |||
| ### 1. 性格描述输入 | |||
| - 提供文本输入框,用户可以自由描述宠物性格 | |||
| - 支持最多200字符输入 | |||
| - 灰色背景,符合设计规范 | |||
| ### 2. 快捷选择 | |||
| - 提供8个预设性格标签 | |||
| - 支持多选功能 | |||
| - 选中状态为橙色背景,白色文字 | |||
| - 2行4列网格布局 | |||
| ### 3. 数据回显 | |||
| - 从API获取性格选项列表 | |||
| - 支持解析已有的性格数据 | |||
| - 保存后自动更新父页面数据 | |||
| ### 4. 页面导航 | |||
| - 从宠物信息页面点击"性格"字段跳转 | |||
| - 保存后自动返回上一页 | |||
| - 显示保存成功提示 | |||
| ## 使用流程 | |||
| 1. 在宠物信息页面点击"性格"字段 | |||
| 2. 跳转到性格选择页面 | |||
| 3. 输入性格描述(可选) | |||
| 4. 选择性格标签(可多选) | |||
| 5. 点击"保存"按钮 | |||
| 6. 自动返回宠物信息页面,数据已更新 | |||
| ## 技术实现 | |||
| ### 数据传递 | |||
| - 使用URL参数传递现有性格数据 | |||
| - 使用页面栈更新父页面数据 | |||
| ### 样式设计 | |||
| - 响应式网格布局 | |||
| - 符合设计图的颜色方案 | |||
| - 流畅的交互动画 | |||
| ### API集成 | |||
| - 从`pet_personality`字典获取性格选项 | |||
| - 支持动态加载和解析 | |||
| ## 注意事项 | |||
| 1. 确保至少选择一项性格特征才能保存 | |||
| 2. 性格描述和快捷选择可以组合使用 | |||
| 3. 数据格式为:描述 + "," + 快捷选择标签 | |||
| 4. 支持编辑现有性格数据 | |||
| @ -1,323 +0,0 @@ | |||
| <template> | |||
| <view class="coupon-list"> | |||
| <view v-for="(item,index) in couponData" style="padding:20rpx;" :key="index"> | |||
| <view> | |||
| <view class="card"> | |||
| <view class="card-left"> | |||
| <view class=""> | |||
| {{switchType(item.stockType)}} | |||
| </view> | |||
| </view> | |||
| <view class="card-center"> | |||
| <view class="card-center-top"></view> | |||
| <view class="card-center-bottom"></view> | |||
| </view> | |||
| <view class="card-right"> | |||
| <view class="card-content"> | |||
| <view class="card-info">{{item.stockName}}</view> | |||
| <view class="card-type">可用于 | |||
| <text class="card-type-text">专业喂养</text> | |||
| <text class="card-type-text">专业遛狗</text> | |||
| <!-- <text class="card-type-text">{{ item.goodsName }}</text> --> | |||
| </view> | |||
| <view class="card-time">有效期至: {{item.availableEndTime.slice(0, 16)}}</view> | |||
| </view> | |||
| <!-- <view style="width: 22%;"> | |||
| <u-button @click="receiveCoupon(item.id)" shape="circle" size="mini" color="#ffaa48" text="立即领取"></u-button> | |||
| </view> --> | |||
| <view style="width: 132rpx;height: 52rpx;background-color: #FFAA48; display: flex;align-items: center;justify-content: center;border-radius: 56rpx;"> | |||
| <text @click="receiveCoupon(item.id)" style="font-size: 24rpx; font-weight: 500; color: #FFFFFF;">立即领取</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view class="card-bottom"> | |||
| <view class="card-bottom-text"> | |||
| 优惠券不可兑换现金 | |||
| </view> | |||
| <view class="card-bottom-text" @click="showRulePopup(item)"> | |||
| 查看详细规则> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 优惠券详细规则弹窗 --> | |||
| <uni-popup ref="rulePopup" type="center"> | |||
| <view class="rule-popup"> | |||
| <view class="rule-popup-title">优惠券详细规则</view> | |||
| <view class="rule-popup-content"> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">名称:</view> | |||
| <view class="rule-value">{{currentCoupon.stockName}}</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">折扣:</view> | |||
| <view class="rule-value">{{getDiscountText(currentCoupon)}}</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">使用规则:</view> | |||
| <view class="rule-value">可用于专业喂养和专业遛狗服务</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">有效日期:</view> | |||
| <view class="rule-value">{{currentCoupon.availableEndTime ? currentCoupon.availableEndTime.slice(0, 16) : ''}}</view> | |||
| </view> | |||
| <view class="rule-item"> | |||
| <view class="rule-label">特别说明:</view> | |||
| <view class="rule-value">单笔订单仅限使用1张优惠券;优惠券仅限用户本人使用,不可赠送、不可提现、不得找零。</view> | |||
| </view> | |||
| </view> | |||
| <view class="rule-popup-close" @click="closeRulePopup">关闭</view> | |||
| </view> | |||
| </uni-popup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { | |||
| getCouponList, | |||
| receiveCoupon, | |||
| } from "@/api/system/user" | |||
| export default { | |||
| data() { | |||
| return { | |||
| couponData: [], | |||
| currentCoupon: {}, // 当前选中的优惠券 | |||
| } | |||
| }, | |||
| onLoad() { | |||
| // this.openIdStr = this.$globalData.openIdStr; | |||
| this.getCouponListAuth() | |||
| }, | |||
| methods: { | |||
| getCouponListAuth() { | |||
| getCouponList().then(res => { | |||
| if (res.code == 200) { | |||
| this.couponData = res.data | |||
| console.log("this.couponData", this.couponData) | |||
| } else { | |||
| this.$modal.showToast('获取优惠券失败') | |||
| } | |||
| }) | |||
| }, | |||
| switchType(type) { | |||
| if (type == 'PNORMAL') { | |||
| return '满减券' | |||
| } | |||
| if (type == 'PDISCOUNT') { | |||
| return '折扣券' | |||
| } | |||
| if (type == 'PTRAIL') { | |||
| return '体验券' | |||
| } | |||
| return '优惠券' | |||
| }, | |||
| receiveCoupon (id) { | |||
| let data = { | |||
| stockId: id | |||
| } | |||
| receiveCoupon(data).then(res => { | |||
| console.log("this.receiveCoupon", res) | |||
| if (res.code == 200) { | |||
| this.$modal.showToast('优惠券领取成功') | |||
| } else { | |||
| this.$modal.showToast('领取优惠券失败') | |||
| } | |||
| }) | |||
| }, | |||
| // 显示优惠券规则弹窗 | |||
| showRulePopup(item) { | |||
| this.currentCoupon = item || {}; | |||
| this.$refs.rulePopup.open(); | |||
| }, | |||
| // 关闭优惠券规则弹窗 | |||
| closeRulePopup() { | |||
| this.$refs.rulePopup.close(); | |||
| }, | |||
| // 获取优惠券折扣文本 | |||
| getDiscountText(coupon) { | |||
| if (!coupon || !coupon.stockType) return ''; | |||
| if (coupon.stockType === 'PNORMAL') { | |||
| return '满100可减10元'; | |||
| } else if (coupon.stockType === 'PDISCOUNT') { | |||
| return '打8折'; | |||
| } else if (coupon.stockType === 'PTRAIL') { | |||
| return '免费体验一次'; | |||
| } | |||
| return ''; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .coupon-list { | |||
| /* 优惠券规则弹窗样式 */ | |||
| .rule-popup { | |||
| width: 600rpx; | |||
| background-color: #FFFFFF; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| } | |||
| .rule-popup-title { | |||
| height: 100rpx; | |||
| line-height: 100rpx; | |||
| text-align: center; | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: #FFFFFF; | |||
| background-color: #FFAA48; | |||
| } | |||
| .rule-popup-content { | |||
| padding: 30rpx; | |||
| } | |||
| .rule-item { | |||
| display: flex; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .rule-label { | |||
| width: 140rpx; | |||
| font-size: 28rpx; | |||
| color: #666666; | |||
| flex-shrink: 0; | |||
| } | |||
| .rule-value { | |||
| flex: 1; | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| line-height: 40rpx; | |||
| } | |||
| .rule-popup-close { | |||
| height: 90rpx; | |||
| line-height: 90rpx; | |||
| text-align: center; | |||
| font-size: 30rpx; | |||
| color: #FFAA48; | |||
| border-top: 1px solid #EEEEEE; | |||
| } | |||
| .card { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| padding: 10px 0; | |||
| background: #fff; | |||
| border-radius: 8px 8px 0 0; | |||
| } | |||
| .card-bottom { | |||
| display: flex; | |||
| background-color: #FFF1E0; | |||
| height: 50rpx; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| padding: 0 20rpx 0 20rpx; | |||
| border-radius: 0 0 8px 8px; | |||
| .card-bottom-text { | |||
| color: #AAAAAA; | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| } | |||
| } | |||
| .card-left { | |||
| width: 88px; | |||
| text-align: center; | |||
| color: #FF530A; | |||
| font-size: 28rpx; | |||
| font-weight: 900; | |||
| } | |||
| .card-center { | |||
| display: flex; | |||
| flex-direction: column; | |||
| // align-items: center; | |||
| .card-center-top { | |||
| width: 40rpx; | |||
| height: 20rpx; | |||
| border-radius: 0 0 20rpx 20rpx; | |||
| background-color: #F5F5F7; | |||
| line-height: 20rpx; | |||
| // border-bottom: 1px solid #FDA714; | |||
| // border-left: 1px solid #FDA714; | |||
| // border-right: 1px solid #FDA714; | |||
| margin-top: -22rpx; | |||
| margin-bottom: 20rpx; | |||
| margin-left: -19rpx; | |||
| } | |||
| .card-center-bottom { | |||
| border-right: 1px dashed #AAAAAA; | |||
| width: 1px; | |||
| height: 120rpx; | |||
| } | |||
| } | |||
| .card-right { | |||
| padding: 0px 12px; | |||
| display: flex; | |||
| flex: 1; | |||
| /* flex-direction: column; */ | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| height: 60px; | |||
| .card-content { | |||
| width: 77%; | |||
| } | |||
| .card-icon { | |||
| position: relative; | |||
| right: -10px; | |||
| top: -10px; | |||
| } | |||
| } | |||
| .card-info { | |||
| margin: 0; | |||
| font-size: 28rpx; | |||
| line-height: 28rpx; | |||
| color: #333333; | |||
| font-weight: 500; | |||
| } | |||
| .card-type { | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| font-weight: 400; | |||
| color: #AAAAAA; | |||
| margin-top: 10rpx; | |||
| .card-type-text { | |||
| color: #FFAA48; | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| border: #FFAA48 1px solid; | |||
| border-radius: 7rpx; | |||
| margin-left: 8rpx; | |||
| } | |||
| } | |||
| .card-time { | |||
| font-size: 24rpx; | |||
| font-weight: 400; | |||
| line-height: 24rpx; | |||
| font-weight: 400; | |||
| color: #AAAAAA; | |||
| margin-top: 10rpx; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,109 @@ | |||
| { | |||
| "basePrice": { | |||
| "normal": 75, | |||
| "holiday": 85, | |||
| "weekend": 80, | |||
| "perKm": 3 | |||
| }, | |||
| "memberDiscount": { | |||
| "new": 0.95, | |||
| "regular": 0.9, | |||
| "silver": 0.88, | |||
| "gold": 0.85 | |||
| }, | |||
| "preFamiliarize": { | |||
| "price": 38, | |||
| "holidayRate": 1.2 | |||
| }, | |||
| "multiService": { | |||
| "two": { | |||
| "price": 46, | |||
| "holidayRate": 1.1 | |||
| }, | |||
| "three": { | |||
| "price": 131, | |||
| "holidayRate": 1.1 | |||
| } | |||
| }, | |||
| "petExtra": { | |||
| "largeDog": { | |||
| "price": 40, | |||
| "holidayRate": 1.1 | |||
| }, | |||
| "mediumDog": { | |||
| "price": 30, | |||
| "holidayRate": 1.1 | |||
| }, | |||
| "smallDog": { | |||
| "price": 15, | |||
| "holidayRate": 1.1 | |||
| }, | |||
| "cat": { | |||
| "price": 10, | |||
| "holidayRate": 1.1 | |||
| } | |||
| }, | |||
| "freeQuota": { | |||
| "threshold": 30, | |||
| "rules": [ | |||
| { | |||
| "type": "cat", | |||
| "count": 3, | |||
| "freeAmount": 30, | |||
| "description": "3只及以上猫免费30元" | |||
| }, | |||
| { | |||
| "type": "smallDog", | |||
| "count": 2, | |||
| "freeAmount": 30, | |||
| "description": "2只及以上小型犬免费30元" | |||
| }, | |||
| { | |||
| "type": "mediumDog", | |||
| "count": 1, | |||
| "freeAmount": 30, | |||
| "description": "1只及以上中型犬免费30元" | |||
| }, | |||
| { | |||
| "type": "mixed", | |||
| "count": 0, | |||
| "freeAmount": 29, | |||
| "description": "以上都不满足则免费25元" | |||
| } | |||
| ] | |||
| }, | |||
| "holidays": [ | |||
| "2025-10-01", | |||
| "2025-10-02", | |||
| "2025-10-03", | |||
| "2025-10-04", | |||
| "2025-10-05", | |||
| "2025-10-06", | |||
| "2025-10-07", | |||
| "2025-07-15", | |||
| "2025-07-22", | |||
| "2025-07-16", | |||
| "2025-07-23", | |||
| "2025-07-17", | |||
| "2025-07-11", | |||
| "2025-07-18", | |||
| "2025-07-25", | |||
| "2025-07-24", | |||
| "2025-09-11" | |||
| ], | |||
| "weekends": [ | |||
| 6, | |||
| 0 | |||
| ], | |||
| "customServices": { | |||
| "priceConfig": {}, | |||
| "holidayRate": 1.1 | |||
| }, | |||
| "cityConfig": { | |||
| "currentCity": "shenzhen", | |||
| "priceRates": { | |||
| "default": 1, | |||
| "长沙": 1.1 | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,405 @@ | |||
| <template> | |||
| <view class="breed-select-container"> | |||
| <!-- 搜索栏 --> | |||
| <view class="search-container"> | |||
| <u-search | |||
| v-model="searchValue" | |||
| placeholder="搜索宠物品种" | |||
| :show-action="false" | |||
| @change="onSearchChange" | |||
| shape="round" | |||
| bg-color="#fff" | |||
| ></u-search> | |||
| </view> | |||
| <!-- 品种列表 --> | |||
| <view class="breed-list-container"> | |||
| <scroll-view | |||
| scroll-y="true" | |||
| class="breed-list" | |||
| :scroll-into-view="scrollIntoView" | |||
| @scroll="onScroll" | |||
| scroll-with-animation="true" | |||
| > | |||
| <view | |||
| v-for="letter in alphabetList" | |||
| :key="letter" | |||
| v-if="groupedBreeds && groupedBreeds[letter] && Array.isArray(groupedBreeds[letter]) && groupedBreeds[letter].length > 0" | |||
| :id="`section-${letter}`" | |||
| class="breed-section" | |||
| > | |||
| <view class="section-header">{{ letter }}</view> | |||
| <view | |||
| v-for="(breed, index) in groupedBreeds[letter]" | |||
| :key="`${letter}-${index}`" | |||
| class="breed-item" | |||
| @click="() => selectBreed(breed)" | |||
| > | |||
| <text class="breed-name">{{ breed }}</text> | |||
| <view class="breed-divider" v-if="index < groupedBreeds[letter].length - 1"></view> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| <!-- 字母索引 --> | |||
| <view class="letter-index"> | |||
| <view | |||
| v-for="letter in alphabetList" | |||
| :key="letter" | |||
| class="letter-item" | |||
| :class="{ 'active': currentLetter === letter, 'has-content': groupedBreeds && groupedBreeds[letter] && Array.isArray(groupedBreeds[letter]) && groupedBreeds[letter].length > 0 }" | |||
| @click="scrollToLetter(letter)" | |||
| > | |||
| {{ letter }} | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { getDictList } from "@/api/system/user" | |||
| export default { | |||
| data() { | |||
| return { | |||
| petType: 'dog', | |||
| searchValue: '', | |||
| breedData: [], | |||
| filteredBreeds: [], | |||
| groupedBreeds: {}, | |||
| alphabetList: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], | |||
| currentLetter: 'A', | |||
| scrollIntoView: '', | |||
| selectedBreed: '', | |||
| scrollTimer: null | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| this.petType = options.petType || 'dog' | |||
| this.selectedBreed = options.selectedBreed || '' | |||
| this.getPetBreeds() | |||
| }, | |||
| methods: { | |||
| // 获取宠物品种数据 | |||
| async getPetBreeds() { | |||
| try { | |||
| const petBreedType = this.petType === 'cat' ? 'pet_brand_cat' : 'pet_brand_dog' | |||
| const res = await getDictList(petBreedType) | |||
| if (res && res.code === 200 && res.data && Array.isArray(res.data)) { | |||
| // 过滤掉空值并去重 | |||
| const validBreeds = res.data | |||
| .filter(e => e && e.dictLabel && typeof e.dictLabel === 'string') | |||
| .map(e => e.dictLabel.trim()) | |||
| .filter(label => label.length > 0) | |||
| this.breedData = Array.from(new Set(validBreeds)).sort((a, b) => a.localeCompare(b, 'zh-CN')) | |||
| this.filteredBreeds = [...this.breedData] | |||
| this.groupBreedsByLetter() | |||
| console.log('品种数据加载成功,共', this.breedData.length, '个品种') | |||
| } else { | |||
| console.error('API返回数据格式异常:', res) | |||
| uni.showToast({ | |||
| title: '获取品种数据失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } catch (error) { | |||
| console.error('获取品种数据失败:', error) | |||
| uni.showToast({ | |||
| title: '获取品种数据失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }, | |||
| // 按字母分组品种 | |||
| groupBreedsByLetter() { | |||
| // 初始化所有字母组 | |||
| this.groupedBreeds = {} | |||
| this.alphabetList.forEach(letter => { | |||
| this.groupedBreeds[letter] = [] | |||
| }) | |||
| // 确保filteredBreeds是数组且不为空 | |||
| if (!this.filteredBreeds || !Array.isArray(this.filteredBreeds)) { | |||
| console.warn('filteredBreeds不是有效数组') | |||
| this.filteredBreeds = [] | |||
| return | |||
| } | |||
| // 按字母分组 | |||
| this.filteredBreeds.forEach(breed => { | |||
| if (breed && typeof breed === 'string' && breed.trim()) { | |||
| const firstChar = this.getFirstChar(breed) | |||
| if (this.groupedBreeds && this.groupedBreeds[firstChar] && Array.isArray(this.groupedBreeds[firstChar])) { | |||
| this.groupedBreeds[firstChar].push(breed) | |||
| } | |||
| } | |||
| }) | |||
| // 对每个字母组内的品种进行排序 | |||
| if (this.groupedBreeds) { | |||
| Object.keys(this.groupedBreeds).forEach(letter => { | |||
| if (this.groupedBreeds[letter] && Array.isArray(this.groupedBreeds[letter])) { | |||
| this.groupedBreeds[letter].sort((a, b) => a.localeCompare(b, 'zh-CN')) | |||
| } | |||
| }) | |||
| } | |||
| }, | |||
| // 获取品种名称的首字母 | |||
| getFirstChar(breed) { | |||
| if (!breed || typeof breed !== 'string' || breed.trim() === '') { | |||
| return 'A' | |||
| } | |||
| const firstChar = breed.charAt(0) | |||
| // 如果是中文字符,返回拼音首字母 | |||
| if (/[\u4e00-\u9fa5]/.test(firstChar)) { | |||
| return this.getPinyinFirstChar(firstChar) | |||
| } | |||
| return firstChar.toUpperCase() | |||
| }, | |||
| // 获取中文字符的拼音首字母 | |||
| getPinyinFirstChar(char) { | |||
| const pinyinMap = { | |||
| '阿': 'A', '艾': 'A', '澳': 'A', '爱': 'A', '安': 'A', '奥': 'A', | |||
| '巴': 'B', '比': 'B', '博': 'B', '边': 'B', '布': 'B', '伯': 'B', | |||
| '藏': 'C', '柴': 'C', '长': 'C', '成': 'C', '查': 'C', '春': 'C', | |||
| '大': 'D', '德': 'D', '杜': 'D', '斗': 'D', '丹': 'D', '道': 'D', | |||
| '俄': 'E', '恩': 'E', '尔': 'E', | |||
| '法': 'F', '芬': 'F', '佛': 'F', '费': 'F', | |||
| '高': 'G', '贵': 'G', '古': 'G', '格': 'G', '哥': 'G', '国': 'G', | |||
| '哈': 'H', '惠': 'H', '黑': 'H', '红': 'H', '华': 'H', '虎': 'H', | |||
| '吉': 'J', '金': 'J', '加': 'J', '杰': 'J', '京': 'J', '基': 'J', | |||
| '可': 'K', '卡': 'K', '克': 'K', '科': 'K', | |||
| '拉': 'L', '罗': 'L', '兰': 'L', '莱': 'L', '利': 'L', '路': 'L', | |||
| '马': 'M', '美': 'M', '牧': 'M', '摩': 'M', '曼': 'M', '米': 'M', '缅': 'M', | |||
| '牛': 'N', '纽': 'N', '南': 'N', '尼': 'N', '纳': 'N', | |||
| '平': 'P', '帕': 'P', '普': 'P', '皮': 'P', | |||
| '秋': 'Q', '奇': 'Q', '丘': 'Q', | |||
| '日': 'R', '瑞': 'R', '若': 'R', '热': 'R', | |||
| '松': 'S', '萨': 'S', '圣': 'S', '苏': 'S', '斯': 'S', '山': 'S', | |||
| '泰': 'T', '土': 'T', '特': 'T', '托': 'T', | |||
| '威': 'W', '魏': 'W', '温': 'W', '沃': 'W', | |||
| '西': 'X', '喜': 'X', '雪': 'X', '新': 'X', | |||
| '约': 'Y', '英': 'Y', '意': 'Y', '伊': 'Y', | |||
| '中': 'Z', '藏': 'Z', '芝': 'Z', '泽': 'Z' | |||
| } | |||
| return pinyinMap[char] || 'A' | |||
| }, | |||
| // 搜索处理 | |||
| onSearchChange(value) { | |||
| this.searchValue = value || '' | |||
| if (!this.breedData || !Array.isArray(this.breedData)) { | |||
| console.warn('breedData不是有效数组') | |||
| this.filteredBreeds = [] | |||
| this.groupBreedsByLetter() | |||
| return | |||
| } | |||
| if (value && value.trim()) { | |||
| this.filteredBreeds = this.breedData.filter(breed => | |||
| breed && typeof breed === 'string' && breed.toLowerCase().includes(value.toLowerCase()) | |||
| ) | |||
| } else { | |||
| this.filteredBreeds = [...this.breedData] | |||
| } | |||
| this.groupBreedsByLetter() | |||
| }, | |||
| // 选择品种 | |||
| selectBreed(breed) { | |||
| try { | |||
| console.log('选择品种:', breed) | |||
| // 使用全局事件总线传递数据,这是最可靠的方式 | |||
| uni.$emit('breedSelected', { | |||
| breed: breed, | |||
| petType: this.petType | |||
| }) | |||
| console.log('触发全局事件 breedSelected:', breed) | |||
| // 返回上一页 | |||
| uni.navigateBack() | |||
| } catch (error) { | |||
| console.error('选择品种时出错:', error) | |||
| uni.showToast({ | |||
| title: '选择失败,请重试', | |||
| icon: 'none' | |||
| }) | |||
| // 即使出错也要返回上一页 | |||
| uni.navigateBack() | |||
| } | |||
| }, | |||
| // 滚动到指定字母 | |||
| scrollToLetter(letter) { | |||
| this.currentLetter = letter | |||
| this.scrollIntoView = `section-${letter}` | |||
| }, | |||
| // 滚动事件处理 | |||
| onScroll(e) { | |||
| // 暂时禁用滚动检测,避免错误 | |||
| // const scrollTop = e.detail.scrollTop | |||
| // 使用节流来优化性能 | |||
| // if (this.scrollTimer) { | |||
| // clearTimeout(this.scrollTimer) | |||
| // } | |||
| // this.scrollTimer = setTimeout(() => { | |||
| // this.updateCurrentLetter(scrollTop) | |||
| // }, 100) | |||
| }, | |||
| // 更新当前字母 | |||
| updateCurrentLetter(scrollTop) { | |||
| // 获取所有字母区域的位置信息 | |||
| const query = uni.createSelectorQuery().in(this) | |||
| query.selectAll('.breed-section').boundingClientRect() | |||
| query.exec((res) => { | |||
| if (res && res[0] && Array.isArray(res[0])) { | |||
| const sections = res[0] | |||
| let currentLetter = 'A' | |||
| // 找到当前滚动位置对应的字母 | |||
| for (let i = 0; i < sections.length; i++) { | |||
| const section = sections[i] | |||
| if (section && typeof section.top === 'number' && typeof section.height === 'number') { | |||
| const sectionTop = section.top | |||
| const sectionHeight = section.height | |||
| // 只有当字母标题被粘性定位悬挂时才激活 | |||
| // 检查字母标题是否在顶部位置(粘性定位生效) | |||
| // 考虑搜索栏的高度,粘性定位是相对于搜索栏的 | |||
| const searchBarHeight = 120 // 搜索栏高度约120rpx | |||
| const stickyTop = searchBarHeight | |||
| // sectionTop <= stickyTop 表示字母标题已经到达粘性定位位置 | |||
| // sectionTop > stickyTop - sectionHeight 表示字母区域还没有完全滚动过去 | |||
| if (sectionTop <= stickyTop && sectionTop > stickyTop - sectionHeight) { | |||
| currentLetter = section.id.replace('section-', '') | |||
| break | |||
| } | |||
| } | |||
| } | |||
| if (this.currentLetter !== currentLetter) { | |||
| this.currentLetter = currentLetter | |||
| console.log('当前激活字母:', currentLetter, '滚动位置:', scrollTop) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .breed-select-container { | |||
| height: 100vh; | |||
| background-color: #f5f5f5; | |||
| position: relative; | |||
| } | |||
| .search-container { | |||
| background-color: #fff; | |||
| padding: 20rpx 30rpx; | |||
| position: relative; | |||
| z-index: 99; | |||
| } | |||
| .breed-list-container { | |||
| flex: 1; | |||
| position: relative; | |||
| height: calc(100vh - 120rpx); | |||
| background-color: #fff; | |||
| } | |||
| .breed-list { | |||
| height: 100%; | |||
| background-color: #fff; | |||
| padding: 0 30rpx; | |||
| } | |||
| .breed-section { | |||
| .section-header { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| padding: 20rpx 0 10rpx 0; | |||
| background-color: #fff; | |||
| position: sticky; | |||
| top: 0; | |||
| z-index: 10; | |||
| } | |||
| .breed-item { | |||
| padding: 20rpx 0; | |||
| cursor: pointer; | |||
| transition: background-color 0.2s; | |||
| .breed-name { | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| line-height: 1.5; | |||
| font-weight: 400; | |||
| } | |||
| .breed-divider { | |||
| height: 1rpx; | |||
| background-color: #f0f0f0; | |||
| margin-top: 20rpx; | |||
| } | |||
| } | |||
| } | |||
| .letter-index { | |||
| position: fixed; | |||
| right: 10rpx; | |||
| top: 50%; | |||
| transform: translateY(-50%); | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| z-index: 1000; | |||
| .letter-item { | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: 24rpx; | |||
| color: #666; | |||
| margin: 2rpx 0; | |||
| transition: all 0.3s; | |||
| font-weight: 400; | |||
| border-radius: 50%; | |||
| &.active { | |||
| color: #fff; | |||
| font-weight: bold; | |||
| background-color: #FFBF60; | |||
| } | |||
| &.has-content { | |||
| color: #333; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,304 @@ | |||
| <template> | |||
| <view class="personality-select-container"> | |||
| <!-- 性格描述 --> | |||
| <view class="personality-description"> | |||
| <view class="section-title">性格描述</view> | |||
| <view class="description-input"> | |||
| <textarea | |||
| v-model="personalityDescription" | |||
| placeholder="请描述您的宠物性格" | |||
| class="description-textarea" | |||
| maxlength="200" | |||
| ></textarea> | |||
| </view> | |||
| </view> | |||
| <!-- 快捷选择 --> | |||
| <view class="quick-selection"> | |||
| <view class="section-title">快捷选择</view> | |||
| <view class="selection-grid"> | |||
| <view | |||
| v-for="(item, index) in personalityOptions" | |||
| :key="index" | |||
| :class="['selection-item', { 'selected': selectedPersonalities.includes(item) }]" | |||
| @click="togglePersonality(item)" | |||
| > | |||
| {{ item }} | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 保存按钮 --> | |||
| <view class="save-button-container"> | |||
| <u-button | |||
| color="#FFBF60" | |||
| @click="savePersonality" | |||
| :loading="loading" | |||
| class="save-button" | |||
| > | |||
| <view style="color: #fff;">保存</view> | |||
| </u-button> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { getDictList } from "@/api/system/user" | |||
| export default { | |||
| data() { | |||
| return { | |||
| loading: false, | |||
| personalityDescription: '', | |||
| selectedPersonalities: [], | |||
| personalityOptions: [], | |||
| originalPersonality: '' | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| // 接收传入的性格数据 | |||
| if (options.personality) { | |||
| this.originalPersonality = decodeURIComponent(options.personality); | |||
| this.parsePersonality(this.originalPersonality); | |||
| } | |||
| }, | |||
| mounted() { | |||
| this.getPersonalityDataList(); | |||
| }, | |||
| methods: { | |||
| // 解析传入的性格数据 | |||
| parsePersonality(personality) { | |||
| if (!personality) return; | |||
| // 等待API数据加载完成后再解析 | |||
| if (this.personalityOptions.length === 0) { | |||
| setTimeout(() => { | |||
| this.parsePersonality(personality); | |||
| }, 100); | |||
| return; | |||
| } | |||
| // 处理数组形式的性格数据 | |||
| if (Array.isArray(personality)) { | |||
| // 分离描述和快捷选择 | |||
| this.selectedPersonalities = personality.filter(item => | |||
| this.personalityOptions.includes(item) | |||
| ); | |||
| // 描述部分是数组中不在快捷选择里的项 | |||
| const descriptions = personality.filter(item => | |||
| !this.personalityOptions.includes(item) | |||
| ); | |||
| this.personalityDescription = descriptions.join(','); | |||
| } else { | |||
| // 兼容字符串形式的数据 | |||
| const parts = personality.split(','); | |||
| if (parts.length > 1) { | |||
| // 最后一部分是快捷选择 | |||
| const quickSelections = parts[parts.length - 1]; | |||
| this.selectedPersonalities = this.personalityOptions.filter(item => | |||
| quickSelections.includes(item) | |||
| ); | |||
| // 前面的部分是描述 | |||
| this.personalityDescription = parts.slice(0, -1).join(','); | |||
| } else { | |||
| // 只有快捷选择 | |||
| this.selectedPersonalities = this.personalityOptions.filter(item => | |||
| personality.includes(item) | |||
| ); | |||
| } | |||
| } | |||
| }, | |||
| // 切换性格选择 | |||
| togglePersonality(personality) { | |||
| const index = this.selectedPersonalities.indexOf(personality); | |||
| if (index > -1) { | |||
| this.selectedPersonalities.splice(index, 1); | |||
| } else { | |||
| this.selectedPersonalities.push(personality); | |||
| } | |||
| }, | |||
| // 获取性格数据列表 | |||
| getPersonalityDataList() { | |||
| getDictList('pet_personality').then(res => { | |||
| if (res.code == 200) { | |||
| this.personalityOptions = Array.from(new Set(res.data.map(e => e.dictLabel))); | |||
| } else { | |||
| this.$modal.showToast('获取性格失败'); | |||
| } | |||
| }).catch(err => { | |||
| console.error('获取性格数据失败:', err); | |||
| this.$modal.showToast('获取性格数据失败'); | |||
| }); | |||
| }, | |||
| // 保存性格选择 | |||
| savePersonality() { | |||
| this.loading = true; | |||
| // 组合性格描述和快捷选择,保持数组形式 | |||
| let finalPersonality = []; | |||
| if (this.personalityDescription.trim()) { | |||
| finalPersonality.push(this.personalityDescription.trim()); | |||
| } | |||
| if (this.selectedPersonalities.length > 0) { | |||
| finalPersonality = finalPersonality.concat(this.selectedPersonalities); | |||
| } | |||
| // 验证是否至少选择了一项 | |||
| if (finalPersonality.length === 0) { | |||
| this.$modal.showToast('请至少选择一项性格特征'); | |||
| this.loading = false; | |||
| return; | |||
| } | |||
| // 延迟一下模拟保存过程 | |||
| setTimeout(() => { | |||
| this.loading = false; | |||
| // 返回上一页并传递选择的性格数据 | |||
| const pages = getCurrentPages(); | |||
| // 安全检查:确保有足够的页面 | |||
| if (!pages || pages.length < 2) { | |||
| console.warn('页面栈不足,无法获取上一页') | |||
| uni.navigateBack(); | |||
| return; | |||
| } | |||
| const prevPage = pages[pages.length - 2]; | |||
| if (prevPage && prevPage.$vm) { | |||
| // 更新父页面的性格数据 | |||
| if (prevPage.$vm.petBaseInfo) { | |||
| prevPage.$vm.petBaseInfo.personality = finalPersonality; | |||
| // 触发父页面的更新事件 | |||
| prevPage.$vm.$emit('update:petBaseInfo', prevPage.$vm.petBaseInfo); | |||
| } else { | |||
| console.warn('上一页没有petBaseInfo') | |||
| } | |||
| } else { | |||
| console.warn('无法找到上一页') | |||
| } | |||
| // uni.showToast({ | |||
| // title: '保存成功', | |||
| // icon: 'success', | |||
| // duration: 1500 | |||
| // }); | |||
| // 返回上一页 | |||
| setTimeout(() => { | |||
| uni.navigateBack(); | |||
| }, 0); | |||
| }, 0); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .personality-select-container { | |||
| min-height: 100vh; | |||
| background-color: #f5f5f5; | |||
| padding: 20px; | |||
| box-sizing: border-box; | |||
| padding-bottom: 120px; | |||
| } | |||
| .section-title { | |||
| font-size: 16px; | |||
| font-weight: bold; | |||
| color: #333; | |||
| margin-bottom: 15px; | |||
| } | |||
| .personality-description { | |||
| background-color: #fff; | |||
| border-radius: 8px; | |||
| padding: 20px; | |||
| margin-bottom: 20px; | |||
| .description-input { | |||
| .description-textarea { | |||
| width: 100%; | |||
| min-height: 100px; | |||
| padding: 15px; | |||
| border: 1px solid #e0e0e0; | |||
| border-radius: 8px; | |||
| font-size: 14px; | |||
| line-height: 1.5; | |||
| background-color: #fafafa; | |||
| box-sizing: border-box; | |||
| resize: none; | |||
| &::placeholder { | |||
| color: #999; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .quick-selection { | |||
| background-color: #fff; | |||
| border-radius: 8px; | |||
| padding: 20px; | |||
| margin-bottom: 100px; | |||
| .selection-grid { | |||
| display: grid; | |||
| grid-template-columns: 1fr 1fr 1fr 1fr; | |||
| gap: 8px; | |||
| .selection-item { | |||
| padding: 10px 4px; | |||
| background-color: #f5f5f5; | |||
| border-radius: 6px; | |||
| text-align: center; | |||
| font-size: 12px; | |||
| color: #666; | |||
| border: 1px solid transparent; | |||
| transition: all 0.3s ease; | |||
| line-height: 1.1; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| min-height: 40px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| &.selected { | |||
| background-color: #FFBF60; | |||
| color: #fff; | |||
| border-color: #FFBF60; | |||
| } | |||
| &:active { | |||
| transform: scale(0.98); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .save-button-container { | |||
| position: fixed; | |||
| bottom: 0; | |||
| left: 0; | |||
| right: 0; | |||
| padding: 20px; | |||
| background-color: #fff; | |||
| border-top: 1px solid #e0e0e0; | |||
| .save-button { | |||
| width: 100%; | |||
| height: 50px; | |||
| border-radius: 8px; | |||
| } | |||
| } | |||
| </style> | |||
| @ -1,382 +1,515 @@ | |||
| <template> | |||
| <view class="personal-pet-cat container"> | |||
| <view class="personal-pet-img"> | |||
| <view class="personal-pet-info-title"> | |||
| 宠物头像 | |||
| </view> | |||
| <view style="display: flex;justify-content: center;"> | |||
| <u-upload | |||
| accept="image" | |||
| :capture="['album','camera']" | |||
| :fileList="fileList" | |||
| @afterRead="afterRead" | |||
| @delete="deletePic" | |||
| :max-count="1" | |||
| name="pet" | |||
| width="60" | |||
| height="60" | |||
| :custom-style="{flex:0}" | |||
| > | |||
| <image src="https://catmdogf.oss-cn-shanghai.aliyuncs.com/CMDF/front/personal/index/cat_new.png" style="width: 60px;height: 60px;"></image> | |||
| </u-upload> | |||
| </view> | |||
| </view> | |||
| <PetBaseInfo :petType="petType" :petBaseInfo.sync="petBaseInfo" /> | |||
| <PetHealthInfo :petType="petType" :petHealthInfo.sync="petHealthInfo"/> | |||
| <view class="personal-pet-info-btns"> | |||
| <view class="personal-pet-btns"> | |||
| <view class="personal-pet-btn"> | |||
| <u-button :disabled="optionType !='edit'" color="#FFF4E4" @click="deletePet"> | |||
| <view style="color: #FFAA48;"> | |||
| 删除 | |||
| </view> | |||
| </u-button> | |||
| </view> | |||
| <view class="personal-pet-btn" @click="save"> | |||
| <u-button color="#FFBF60" :loading="loading"> | |||
| <view style="color: #fff;"> | |||
| 保存 | |||
| </view> | |||
| </u-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <u-modal :show="showDel" | |||
| @confirm="confirmDel" | |||
| @cancel="showDel = false;" | |||
| ref="uModal" | |||
| showCancelButton | |||
| :asyncClose="true" | |||
| :content='delContent'> | |||
| </u-modal> | |||
| </view> | |||
| <view class="personal-pet-cat container"> | |||
| <view class="personal-pet-img"> | |||
| <view class="personal-pet-info-title"> | |||
| 宠物头像 | |||
| </view> | |||
| <view style="display: flex;justify-content: center;"> | |||
| <view class="avatar-container" @click="selectImage"> | |||
| <image v-if="avatarPath" :src="avatarPath" class="avatar-image" mode="aspectFill"></image> | |||
| <image v-else src="https://catmdogf.oss-cn-shanghai.aliyuncs.com/CMDF/front/personal/index/cat_new.png" | |||
| class="avatar-image" mode="aspectFill"></image> | |||
| <view class="avatar-overlay"> | |||
| <text class="avatar-text">点击上传</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <PetBaseInfo :petType="petType" :petBaseInfo.sync="petBaseInfo" /> | |||
| <PetHealthInfo :petType="petType" :petHealthInfo.sync="petHealthInfo" /> | |||
| <view class="personal-pet-info-btns"> | |||
| <view class="personal-pet-btns"> | |||
| <view class="personal-pet-btn"> | |||
| <u-button :disabled="optionType != 'edit'" color="#FFF4E4" @click="deletePet"> | |||
| <view style="color: #FFAA48;"> | |||
| 删除 | |||
| </view> | |||
| </u-button> | |||
| </view> | |||
| <view class="personal-pet-btn" @click="save"> | |||
| <u-button color="#FFBF60" :loading="loading"> | |||
| <view style="color: #fff;"> | |||
| 保存 | |||
| </view> | |||
| </u-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <u-modal :show="showDel" @confirm="confirmDel" @cancel="showDel = false;" ref="uModal" showCancelButton | |||
| :asyncClose="true" :content='delContent'> | |||
| </u-modal> | |||
| <!-- 裁剪组件 --> | |||
| <ksp-cropper | |||
| mode="free" | |||
| :width="200" | |||
| :height="200" | |||
| :maxWidth="1024" | |||
| :maxHeight="1024" | |||
| :url="cropperUrl" | |||
| @cancel="onCropperCancel" | |||
| @ok="onCropperOk"> | |||
| </ksp-cropper> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import {addPet,getPetDetails,updatePet,delPet} from '@/api/system/pet' | |||
| import PetBaseInfo from './components/petBaseInfo.vue' | |||
| import PetHealthInfo from './components/petHealthInfo.vue' | |||
| export default{ | |||
| data(){ | |||
| return { | |||
| loading:false, | |||
| fileList:[], | |||
| petId:'', | |||
| petType:'dog', | |||
| optionType:'add', | |||
| isNewOrder:false, | |||
| delContent:'', | |||
| photo:'', | |||
| petBaseInfo:{ | |||
| name: '', | |||
| gender: '', | |||
| breed: '', | |||
| bodyType: '', | |||
| birthDate: '', | |||
| personality: '' | |||
| }, | |||
| petHealthInfo: { | |||
| vaccineStatus: '', | |||
| dewormingStatus: '', | |||
| sterilization: '', | |||
| doglicenseStatus:'', | |||
| healthStatus: [], | |||
| remark:'' | |||
| }, | |||
| showDel:false, | |||
| } | |||
| }, | |||
| components:{ | |||
| PetBaseInfo, | |||
| PetHealthInfo | |||
| }, | |||
| onLoad: function(option) { | |||
| console.log(option.petType); //打印出上个页面传递的参数。 | |||
| this.petType = option.petType; | |||
| this.optionType=option.optionType; | |||
| if(this.optionType=='edit'){ | |||
| this.petId=option.petId; | |||
| this.getPetDetails(option.petId); | |||
| } | |||
| if(option.isNewOrder){ | |||
| this.isNewOrder=true; | |||
| } | |||
| }, | |||
| methods:{ | |||
| // 删除图片 | |||
| deletePic(event) { | |||
| this.fileList.splice(event.index, 1) | |||
| }, | |||
| // 新增图片 | |||
| async afterRead(event) { | |||
| // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式 | |||
| let lists = [].concat(event.file) | |||
| let fileListLen = this.fileList.length | |||
| lists.map((item) => { | |||
| this.fileList.push({ | |||
| ...item, | |||
| status: 'uploading', | |||
| message: '上传中' | |||
| }) | |||
| }) | |||
| for (let i = 0; i < lists.length; i++) { | |||
| const result = await this.uploadFilePromise(lists[i].url) | |||
| let item = this.fileList[fileListLen] | |||
| this.fileList.splice(fileListLen, 1, Object.assign(item, { | |||
| status: 'success', | |||
| message: '', | |||
| url: result | |||
| })) | |||
| fileListLen++ | |||
| } | |||
| }, | |||
| uploadFilePromise(url) { | |||
| return new Promise((resolve, reject) => { | |||
| let a = uni.uploadFile({ | |||
| url: 'https://store-test.catmdogd.com/test-api/h5/oss/upload', | |||
| filePath: url, | |||
| name: 'file', | |||
| formData: { | |||
| user: 'test' | |||
| }, | |||
| success: (res) => { | |||
| setTimeout(() => { | |||
| if(res&&res.data){ | |||
| let resData = JSON.parse(res.data); | |||
| resolve(resData.url); | |||
| } | |||
| reject("上传失败"); | |||
| }, 1000) | |||
| } | |||
| }); | |||
| }) | |||
| }, | |||
| getPetDetails(petId){ | |||
| getPetDetails(petId).then(res=>{ | |||
| if(res&& res.id){ | |||
| const { photo, name, gender,breed, bodyType, birthDate, personality, vaccineStatus, dewormingStatus, sterilization, doglicenseStatus, healthStatus,remark } = res; | |||
| this.petBaseInfo = { | |||
| name, | |||
| gender, | |||
| breed, | |||
| bodyType, | |||
| birthDate, | |||
| personality | |||
| }; | |||
| this.petHealthInfo = { | |||
| vaccineStatus, | |||
| dewormingStatus, | |||
| sterilization, | |||
| doglicenseStatus, | |||
| healthStatus, | |||
| remark | |||
| } | |||
| this.fileList=[{url:photo}] | |||
| }else{ | |||
| this.$modal.showToast('获取pet失败') | |||
| } | |||
| }) | |||
| }, | |||
| save(){ | |||
| console.log(this.petBaseInfo) | |||
| console.log(this.petHealthInfo) | |||
| if(!(this.fileList.length>0&&this.fileList[0].url)){ | |||
| this.$modal.showToast('请上传宠物照片!') | |||
| return; | |||
| } | |||
| this.photo = this.fileList[0].url; | |||
| let params = { ...{petType:this.petType,photo:this.photo}, ...this.petBaseInfo,...this.petHealthInfo} | |||
| if(!params.name){ | |||
| this.$modal.showToast('请填写宠物昵称!') | |||
| return; | |||
| } | |||
| if(!params.breed){s | |||
| this.$modal.showToast('请选择宠物品种!') | |||
| return; | |||
| } | |||
| if(!params.bodyType){ | |||
| this.$modal.showToast('请选择宠物体重!') | |||
| return; | |||
| } | |||
| if(!params.personality){ | |||
| this.$modal.showToast('请选择宠物性格,可多选!') | |||
| return; | |||
| } | |||
| if(!params.vaccineStatus){ | |||
| this.$modal.showToast('请选择宠物疫苗情况!') | |||
| return; | |||
| } | |||
| if(!params.dewormingStatus){ | |||
| this.$modal.showToast('请选择宠物驱虫情况!') | |||
| return; | |||
| } | |||
| if(params.healthStatus==null || !params.healthStatus.length){ | |||
| this.$modal.showToast('请选择宠物健康情况,可多选!') | |||
| return; | |||
| } | |||
| this.loading=true | |||
| if(this.optionType=='edit'){ | |||
| params.id = this.petId; | |||
| updatePet(params).then(res=>{ | |||
| if(res&&res.code==200){ | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| duration: 3000, | |||
| icon:"none" | |||
| }) | |||
| setTimeout(() => { | |||
| this.loading = false; | |||
| if(this.isNewOrder){ | |||
| uni.redirectTo({url:'/pages/newOrder/petList'}); | |||
| }else{ | |||
| let len = getCurrentPages().length; | |||
| if(len >= 2) { | |||
| uni.navigateBack(); | |||
| }else{ | |||
| uni.redirectTo({url:'/pages/personalCenter/pet'}); | |||
| } | |||
| } | |||
| }, 1000); | |||
| }else { | |||
| this.loading=false | |||
| uni.showToast({ | |||
| title: '更新pet失败', | |||
| duration: 3000, | |||
| icon:"none" | |||
| }) | |||
| } | |||
| }) | |||
| } else if(this.optionType=='add'){ | |||
| addPet(params).then(res=>{ | |||
| if(res&&res==1){ | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| duration: 3000, | |||
| icon:"none" | |||
| }) | |||
| setTimeout(() => { | |||
| this.loading = false; | |||
| if(this.isNewOrder){ | |||
| uni.redirectTo({url:'/pages/newOrder/petList'}); | |||
| }else{ | |||
| let len = getCurrentPages().length; | |||
| if(len >= 2) { | |||
| uni.navigateBack(); | |||
| }else { | |||
| uni.redirectTo({url:'/pages/personalCenter/pet'}); | |||
| } | |||
| } | |||
| }, 1000); | |||
| }else { | |||
| this.loading=false | |||
| uni.showToast({ | |||
| title: '新增pet失败', | |||
| duration: 3000, | |||
| icon:"none" | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| }, | |||
| deletePet(){ | |||
| this.delContent = `确定要删除${this.petBaseInfo.name}?`; | |||
| this.showDel = true; | |||
| }, | |||
| confirmDel(){ | |||
| delPet(this.petId).then(res=>{ | |||
| console.log(res); | |||
| uni.showToast({ | |||
| title: '删除成功', | |||
| duration: 3000, | |||
| icon:"none" | |||
| }) | |||
| this.showDel=false; | |||
| setTimeout(() => { | |||
| let len = getCurrentPages().length; | |||
| this.loading=false | |||
| if(len >= 2) { | |||
| uni.navigateBack(); | |||
| }else { | |||
| uni.redirectTo({url:'/pages/personalCenter/pet'}); | |||
| } | |||
| }, 2000); | |||
| }) | |||
| }, | |||
| } | |||
| } | |||
| import { addPet, getPetDetails, updatePet, delPet } from '@/api/system/pet' | |||
| import PetBaseInfo from './components/petBaseInfo.vue' | |||
| import PetHealthInfo from './components/petHealthInfo.vue' | |||
| export default { | |||
| data() { | |||
| return { | |||
| loading: false, | |||
| fileList: [], | |||
| petId: '', | |||
| petType: 'dog', | |||
| optionType: 'add', | |||
| isNewOrder: false, | |||
| delContent: '', | |||
| photo: '', | |||
| // 新增裁剪相关数据 | |||
| cropperUrl: '', | |||
| avatarPath: '', | |||
| petBaseInfo: { | |||
| name: '', | |||
| gender: '', | |||
| breed: '', | |||
| bodyType: '', | |||
| birthDate: '', | |||
| personality: '' | |||
| }, | |||
| petHealthInfo: { | |||
| vaccineStatus: '', | |||
| dewormingStatus: '', | |||
| sterilization: '', | |||
| doglicenseStatus: '', | |||
| healthStatus: [], | |||
| remark: '' | |||
| }, | |||
| showDel: false, | |||
| } | |||
| }, | |||
| components: { | |||
| PetBaseInfo, | |||
| PetHealthInfo | |||
| }, | |||
| onLoad: function (option) { | |||
| console.log(option.petType); //打印出上个页面传递的参数。 | |||
| this.petType = option.petType; | |||
| this.optionType = option.optionType; | |||
| if (this.optionType == 'edit') { | |||
| this.petId = option.petId; | |||
| this.getPetDetails(option.petId); | |||
| } | |||
| if (option.isNewOrder) { | |||
| this.isNewOrder = true; | |||
| } | |||
| // 监听品种选择事件 | |||
| uni.$on('breedSelected', this.handleBreedSelected); | |||
| }, | |||
| methods: { | |||
| // 选择图片 | |||
| selectImage() { | |||
| uni.chooseImage({ | |||
| count: 1, | |||
| sizeType: ['original', 'compressed'], | |||
| sourceType: ['album', 'camera'], | |||
| success: (res) => { | |||
| // 设置裁剪组件的url,显示裁剪界面 | |||
| this.cropperUrl = res.tempFilePaths[0]; | |||
| }, | |||
| fail: (err) => { | |||
| console.log('选择图片失败:', err); | |||
| uni.showToast({ | |||
| title: '选择图片失败', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| }); | |||
| }, | |||
| // 裁剪取消 | |||
| onCropperCancel() { | |||
| this.cropperUrl = ''; | |||
| }, | |||
| // 裁剪完成 | |||
| async onCropperOk(ev) { | |||
| try { | |||
| this.cropperUrl = ''; | |||
| this.avatarPath = ev.path; | |||
| // 上传裁剪后的图片 | |||
| const uploadResult = await this.uploadFilePromise(ev.path); | |||
| this.photo = uploadResult; | |||
| // 更新fileList用于兼容原有逻辑 | |||
| this.fileList = [{ | |||
| url: uploadResult, | |||
| status: 'success', | |||
| message: '' | |||
| }]; | |||
| uni.showToast({ | |||
| title: '头像上传成功', | |||
| icon: 'success' | |||
| }); | |||
| } catch (error) { | |||
| console.log('上传失败:', error); | |||
| uni.showToast({ | |||
| title: '上传失败,请重试', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| }, | |||
| // 删除图片 | |||
| deletePic(event) { | |||
| this.fileList.splice(event.index, 1) | |||
| this.avatarPath = ''; | |||
| this.photo = ''; | |||
| }, | |||
| // 新增图片 - 保留原有方法以兼容 | |||
| async afterRead(event) { | |||
| // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式 | |||
| let lists = [].concat(event.file) | |||
| let fileListLen = this.fileList.length | |||
| lists.map((item) => { | |||
| this.fileList.push({ | |||
| ...item, | |||
| status: 'uploading', | |||
| message: '上传中' | |||
| }) | |||
| }) | |||
| for (let i = 0; i < lists.length; i++) { | |||
| const result = await this.uploadFilePromise(lists[i].url) | |||
| let item = this.fileList[fileListLen] | |||
| this.fileList.splice(fileListLen, 1, Object.assign(item, { | |||
| status: 'success', | |||
| message: '', | |||
| url: result | |||
| })) | |||
| fileListLen++ | |||
| } | |||
| }, | |||
| uploadFilePromise(url) { | |||
| return new Promise((resolve, reject) => { | |||
| let a = uni.uploadFile({ | |||
| url: 'https://store-test.catmdogd.com/test-api/h5/oss/upload', | |||
| filePath: url, | |||
| name: 'file', | |||
| formData: { | |||
| user: 'test' | |||
| }, | |||
| success: (res) => { | |||
| setTimeout(() => { | |||
| if (res && res.data) { | |||
| let resData = JSON.parse(res.data); | |||
| resolve(resData.url); | |||
| } | |||
| reject("上传失败"); | |||
| }, 1000) | |||
| } | |||
| }); | |||
| }) | |||
| }, | |||
| getPetDetails(petId) { | |||
| getPetDetails(petId).then(res => { | |||
| if (res && res.id) { | |||
| const { photo, name, gender, breed, bodyType, birthDate, personality, vaccineStatus, dewormingStatus, sterilization, doglicenseStatus, healthStatus, remark } = res; | |||
| this.petBaseInfo = { | |||
| name, | |||
| gender, | |||
| breed, | |||
| bodyType, | |||
| birthDate, | |||
| personality | |||
| }; | |||
| this.petHealthInfo = { | |||
| vaccineStatus, | |||
| dewormingStatus, | |||
| sterilization, | |||
| doglicenseStatus, | |||
| healthStatus, | |||
| remark | |||
| } | |||
| this.fileList = [{ url: photo }] | |||
| this.avatarPath = photo; | |||
| this.photo = photo; // 确保photo字段也被正确设置 | |||
| } else { | |||
| this.$modal.showToast('获取pet失败') | |||
| } | |||
| }) | |||
| }, | |||
| save() { | |||
| console.log(this.petBaseInfo) | |||
| console.log(this.petHealthInfo) | |||
| if (!(this.fileList.length > 0 && this.fileList[0].url)) { | |||
| this.$modal.showToast('请上传宠物照片!') | |||
| return; | |||
| } | |||
| // 确保使用最新的头像URL | |||
| this.photo = this.fileList[0].url; | |||
| let params = { | |||
| petType: this.petType, | |||
| photo: this.photo, | |||
| ...this.petBaseInfo, | |||
| ...this.petHealthInfo | |||
| } | |||
| console.log('保存参数:', params); // 添加调试日志 | |||
| if (!params.name) { | |||
| this.$modal.showToast('请填写宠物昵称!') | |||
| return; | |||
| } | |||
| if (!params.breed) { | |||
| this.$modal.showToast('请选择宠物品种!') | |||
| return; | |||
| } | |||
| if (!params.bodyType) { | |||
| this.$modal.showToast('请选择宠物体重!') | |||
| return; | |||
| } | |||
| if (!params.personality) { | |||
| this.$modal.showToast('请选择宠物性格,可多选!') | |||
| return; | |||
| } | |||
| if (!params.vaccineStatus) { | |||
| this.$modal.showToast('请选择宠物疫苗情况!') | |||
| return; | |||
| } | |||
| if (!params.dewormingStatus) { | |||
| this.$modal.showToast('请选择宠物驱虫情况!') | |||
| return; | |||
| } | |||
| if (params.healthStatus == null || !params.healthStatus.length) { | |||
| this.$modal.showToast('请选择宠物健康情况,可多选!') | |||
| return; | |||
| } | |||
| this.loading = true | |||
| if (this.optionType == 'edit') { | |||
| params.id = this.petId; | |||
| updatePet(params).then(res => { | |||
| if (res && res.code == 200) { | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| duration: 3000, | |||
| icon: "none" | |||
| }) | |||
| setTimeout(() => { | |||
| this.loading = false; | |||
| if (this.isNewOrder) { | |||
| uni.redirectTo({ url: '/pages/newOrder/petList' }); | |||
| } else { | |||
| let len = getCurrentPages().length; | |||
| if (len >= 2) { | |||
| uni.navigateBack(); | |||
| } else { | |||
| uni.redirectTo({ url: '/pages/personalCenter/pet' }); | |||
| } | |||
| } | |||
| }, 1000); | |||
| } else { | |||
| this.loading = false | |||
| uni.showToast({ | |||
| title: '更新pet失败', | |||
| duration: 3000, | |||
| icon: "none" | |||
| }) | |||
| } | |||
| }) | |||
| } else if (this.optionType == 'add') { | |||
| addPet(params).then(res => { | |||
| if (res && res == 1) { | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| duration: 3000, | |||
| icon: "none" | |||
| }) | |||
| setTimeout(() => { | |||
| this.loading = false; | |||
| if (this.isNewOrder) { | |||
| uni.redirectTo({ url: '/pages/newOrder/petList' }); | |||
| } else { | |||
| let len = getCurrentPages().length; | |||
| if (len >= 2) { | |||
| uni.navigateBack(); | |||
| } else { | |||
| uni.redirectTo({ url: '/pages/personalCenter/pet' }); | |||
| } | |||
| } | |||
| }, 1000); | |||
| } else { | |||
| this.loading = false | |||
| uni.showToast({ | |||
| title: '新增pet失败', | |||
| duration: 3000, | |||
| icon: "none" | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| }, | |||
| deletePet() { | |||
| this.delContent = `确定要删除${this.petBaseInfo.name}?`; | |||
| this.showDel = true; | |||
| }, | |||
| confirmDel() { | |||
| delPet(this.petId).then(res => { | |||
| console.log(res); | |||
| uni.showToast({ | |||
| title: '删除成功', | |||
| duration: 3000, | |||
| icon: "none" | |||
| }) | |||
| this.showDel = false; | |||
| setTimeout(() => { | |||
| let len = getCurrentPages().length; | |||
| this.loading = false | |||
| if (len >= 2) { | |||
| uni.navigateBack(); | |||
| } else { | |||
| uni.redirectTo({ url: '/pages/personalCenter/pet' }); | |||
| } | |||
| }, 2000); | |||
| }) | |||
| }, | |||
| // 处理品种选择结果 | |||
| handleBreedSelected(data) { | |||
| console.log('接收到品种选择结果:', data); | |||
| if (data && data.breed && data.petType === this.petType) { | |||
| this.petBaseInfo.breed = data.breed; | |||
| console.log('更新品种信息:', data.breed); | |||
| } | |||
| }, | |||
| }, | |||
| onUnload() { | |||
| // 移除事件监听器 | |||
| uni.$off('breedSelected', this.handleBreedSelected); | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .personal-pet-cat{ | |||
| .breed-select{ | |||
| background-color: #fff; | |||
| width: 100%; | |||
| padding: 20px; | |||
| border-radius: 8px 8px 0 0; | |||
| position: absolute; | |||
| bottom: 0; | |||
| .breed-select-btn{ | |||
| display: flex; | |||
| justify-content: space-around; | |||
| align-items: center; | |||
| } | |||
| } | |||
| .personal-pet-info-title{ | |||
| font-size: 14px; | |||
| color: #333; | |||
| font-weight: bold; | |||
| padding-bottom: 10px; | |||
| } | |||
| .border-bottom{ | |||
| border-bottom: 1px solid #efefef; | |||
| } | |||
| .personal-pet-img{ | |||
| width: 100%; | |||
| height: 118px; | |||
| background-color: #fff; | |||
| padding: 10px 20px; | |||
| } | |||
| .personal-pet-basic-info{ | |||
| background-color: #fff; | |||
| margin-top: 10px; | |||
| padding: 10px 20px; | |||
| } | |||
| } | |||
| .personal-pet-cat { | |||
| .breed-select { | |||
| background-color: #fff; | |||
| width: 100%; | |||
| padding: 20px; | |||
| border-radius: 8px 8px 0 0; | |||
| position: absolute; | |||
| bottom: 0; | |||
| .breed-select-btn { | |||
| display: flex; | |||
| justify-content: space-around; | |||
| align-items: center; | |||
| } | |||
| } | |||
| .personal-pet-info-title { | |||
| font-size: 14px; | |||
| color: #333; | |||
| font-weight: bold; | |||
| padding-bottom: 10px; | |||
| } | |||
| .border-bottom { | |||
| border-bottom: 1px solid #efefef; | |||
| } | |||
| .personal-pet-img { | |||
| width: 100%; | |||
| height: 118px; | |||
| background-color: #fff; | |||
| padding: 10px 20px; | |||
| } | |||
| .personal-pet-basic-info { | |||
| background-color: #fff; | |||
| margin-top: 10px; | |||
| padding: 10px 20px; | |||
| } | |||
| } | |||
| .container { | |||
| position: relative; | |||
| height: 100%; | |||
| padding-bottom: 90px; | |||
| .personal-pet-info-btns { | |||
| background-color: #FFFFFF; | |||
| padding: 10px 20px 40px; | |||
| width: 100%; | |||
| height: 90px; | |||
| position: fixed; | |||
| bottom: 0; | |||
| z-index: 100; | |||
| text-align: center; | |||
| .personal-pet-btns { | |||
| display: flex; | |||
| justify-content: space-around; | |||
| align-items: center; | |||
| flex-wrap: nowrap; | |||
| flex-direction: row; | |||
| .personal-pet-btn { | |||
| width: 40%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .container { | |||
| position: relative; | |||
| height: 100%; | |||
| padding-bottom: 90px; | |||
| .personal-pet-info-btns { | |||
| background-color: #FFFFFF; | |||
| padding: 10px 20px 40px; | |||
| width: 100%; | |||
| height: 90px; | |||
| position: fixed; | |||
| bottom: 0; | |||
| z-index: 100; | |||
| text-align: center; | |||
| .personal-pet-btns{ | |||
| display: flex; | |||
| justify-content: space-around; | |||
| align-items: center; | |||
| flex-wrap: nowrap; | |||
| flex-direction: row; | |||
| .personal-pet-btn{ | |||
| width: 40%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 新增头像相关样式 | |||
| .avatar-container { | |||
| position: relative; | |||
| width: 60px; | |||
| height: 60px; | |||
| border-radius: 50%; | |||
| overflow: hidden; | |||
| border: 2px solid #f0f0f0; | |||
| .avatar-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .avatar-overlay { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| background-color: rgba(0, 0, 0, 0.3); | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| opacity: 0; | |||
| transition: opacity 0.3s; | |||
| .avatar-text { | |||
| color: #fff; | |||
| font-size: 10px; | |||
| text-align: center; | |||
| } | |||
| } | |||
| &:active .avatar-overlay { | |||
| opacity: 1; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,225 @@ | |||
| <template> | |||
| <view class="login"> | |||
| <view class="login-logo"> | |||
| <image class="logo" :src="configList && configList.applet_info ? configList.applet_info.paramValueImage : ''" mode="aspectFill"></image> | |||
| <image class="d" :src="configList && configList.logo_icon ? configList.logo_icon.paramValueImage : ''" mode="aspectFill"></image> | |||
| </view> | |||
| <view class="login-submit"> | |||
| <up-button class="mt24" type="primary" text="手机号登录" | |||
| open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber" | |||
| shape="circle" color="#FFBF60" style="margin-top: 20rpx;"></up-button> | |||
| </view> | |||
| <view class="flex-rowc"> | |||
| <view> | |||
| <up-checkbox v-model="isAgree" size="29rpx" icon-size="29rpx" name="agree" usedAlone | |||
| shape="circle" activeColor="#FFBF60"> | |||
| </up-checkbox> | |||
| </view> | |||
| <view class="size-26 agreement"> | |||
| <text>我已阅读并同意猫爸狗妈</text> | |||
| <text @click="privacyPolicy('login_xy')" class="main-color">《注册协议》</text> | |||
| <text>与</text> | |||
| <text @click="privacyPolicy('login_ys')" class="main-color">《隐私政策》</text> | |||
| </view> | |||
| </view> | |||
| <view class="login-footer flex-rowc"> | |||
| <button @click="cancelLogin" class="btn-cancel" plain>暂不登录</button> | |||
| </view> | |||
| </view> | |||
| <up-popup :show="show" @close="close" :round="10"> | |||
| <view style="padding: 10rpx 20rpx;height: 50vh;overflow-y: scroll;" v-html="content"></view> | |||
| </up-popup> | |||
| </template> | |||
| <script> | |||
| import { | |||
| getOpenIdKey, | |||
| getToken, | |||
| setIsLogin, | |||
| setOpenIdKey, | |||
| setStorage, | |||
| setToken | |||
| } from "../../utils/auth"; | |||
| import { | |||
| getOpenId, | |||
| getPersonalInfo, | |||
| getPhoneNumber | |||
| } from "../../api/system/user"; | |||
| import { mapGetters } from 'vuex'; | |||
| export default { | |||
| name: 'AuthIndex', | |||
| data() { | |||
| return { | |||
| value: false, | |||
| show: false, | |||
| content: '', | |||
| isAgree: false | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters(['configList']) | |||
| }, | |||
| mounted() { | |||
| // 组件挂载后的逻辑 | |||
| }, | |||
| methods: { | |||
| // 微信授权登陆 | |||
| loginWx() { | |||
| if (!this.isAgree) { | |||
| return uni.showToast({ | |||
| title: '请先同意隐私协议', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| this.login() | |||
| }, | |||
| login() { | |||
| uni.login({ | |||
| provider: 'weixin', | |||
| success: (loginRes) => { | |||
| this.getOpenId(loginRes.code) | |||
| }, | |||
| fail: function(error) { | |||
| // 授权失败处理 | |||
| uni.showToast('授权失败,请授权后再试') | |||
| } | |||
| }); | |||
| }, | |||
| getOpenId(code) { | |||
| getOpenId(code).then(res => { | |||
| if (res.code == 200 && res.data) { | |||
| let resData = JSON.parse(res.data) | |||
| let token = resData.token; | |||
| let openId = resData.openId; | |||
| setOpenIdKey(openId) | |||
| if(token){ | |||
| setToken(token) | |||
| setIsLogin(true) | |||
| this.getUserInfo() | |||
| }else{ | |||
| // 如果没有token,可能需要获取手机号 | |||
| uni.showToast({ | |||
| title: '请授权手机号登录', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| // 获取用户信息 | |||
| async getUserInfo() { | |||
| try { | |||
| const res = await getPersonalInfo() | |||
| if(res && (res.id || res.id === 0)){ | |||
| setStorage('userInfo', res) | |||
| uni.navigateBack() | |||
| } | |||
| } catch (error) { | |||
| console.error('获取用户信息失败:', error) | |||
| } | |||
| }, | |||
| // 获取token | |||
| onGetPhoneNumber(e) { | |||
| if(e.detail.errMsg=="getPhoneNumber:fail user deny"){ //用户拒绝授权 | |||
| //拒绝授权后弹出一些提示 | |||
| }else{ //允许授权 | |||
| if(getOpenIdKey()){ | |||
| this.getPhoneNumber({"openId":getOpenIdKey(),"code":e.detail.code}) | |||
| } | |||
| } | |||
| }, | |||
| getPhoneNumber(data){ | |||
| getPhoneNumber(data).then(res=>{ | |||
| if(res&&res.code==200){ | |||
| let token = res.data.token | |||
| setToken(token) | |||
| setIsLogin(true) | |||
| this.getUserInfo() | |||
| } else{ | |||
| uni.showToast({ | |||
| icon:'error', | |||
| title:'获取手机号失败' | |||
| }) | |||
| } | |||
| }) | |||
| }, | |||
| cancelLogin() { | |||
| uni.switchTab({ | |||
| url: "/pages/workbenchManage/index" | |||
| }) | |||
| }, | |||
| close() { | |||
| this.show = false | |||
| }, | |||
| privacyPolicy(key) { | |||
| this.content = this.configList && this.configList[key] ? this.configList[key].paramValueArea || "" : ""; | |||
| this.show = true; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .login { | |||
| height: 100vh; | |||
| background: #ffffff; | |||
| &-logo { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: center; | |||
| height: 504rpx; | |||
| background: linear-gradient(360deg, #FFFFFF 0%, #FFBF60 99%); | |||
| .logo { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| } | |||
| .d { | |||
| width: 200rpx; | |||
| height: 80rpx; | |||
| margin-top: 20rpx; | |||
| } | |||
| } | |||
| &-submit { | |||
| padding: 0 76rpx; | |||
| margin: 150rpx 0 40rpx 0; | |||
| } | |||
| } | |||
| .agreement { | |||
| color: #C7C7C7; | |||
| font-size: 22rpx; | |||
| line-height: 29rpx; | |||
| } | |||
| .login-footer { | |||
| width: 100vw; | |||
| position: fixed; | |||
| bottom: 127rpx; | |||
| .btn-cancel { | |||
| color: #C7C7C7; | |||
| font-size: 30rpx; | |||
| line-height: 40rpx; | |||
| border: none; | |||
| display: inline-block; | |||
| padding: 0; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,76 @@ | |||
| <template> | |||
| <view class="coupon-list"> | |||
| <CouponItem | |||
| v-for="(item,index) in couponData" | |||
| :key="index" | |||
| :couponData="item" | |||
| @coupon-received="handleCouponReceived" | |||
| @update-coupon="updateCouponData" | |||
| @show-rule="showRulePopup" | |||
| /> | |||
| <!-- 优惠券详细规则弹窗组件 --> | |||
| <CouponRulePopup ref="couponRulePopup" /> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { | |||
| getCouponList, | |||
| } from "@/api/system/user" | |||
| import CouponRulePopup from '@/components/CouponRulePopup/index.vue' | |||
| import CouponItem from '@/components/CouponItem/index.vue' | |||
| export default { | |||
| components: { | |||
| CouponRulePopup, | |||
| CouponItem | |||
| }, | |||
| data() { | |||
| return { | |||
| couponData: [] | |||
| } | |||
| }, | |||
| onLoad() { | |||
| // this.openIdStr = this.$globalData.openIdStr; | |||
| this.getCouponListAuth() | |||
| }, | |||
| methods: { | |||
| getCouponListAuth() { | |||
| getCouponList().then(res => { | |||
| if (res.code == 200) { | |||
| this.couponData = res.data | |||
| console.log("this.couponData", this.couponData) | |||
| } else { | |||
| this.$modal.showToast('获取优惠券失败') | |||
| } | |||
| }) | |||
| }, | |||
| // 显示优惠券规则弹窗 | |||
| showRulePopup(item) { | |||
| this.$refs.couponRulePopup.open(item || {}); | |||
| }, | |||
| // 处理优惠券领取成功事件 | |||
| handleCouponReceived(couponData) { | |||
| console.log('优惠券领取成功:', couponData); | |||
| // 可以在这里处理一些额外的逻辑,比如刷新列表等 | |||
| }, | |||
| // 更新优惠券数据 | |||
| updateCouponData(updatedCoupon) { | |||
| // 找到对应的优惠券并更新数据 | |||
| const couponIndex = this.couponData.findIndex(item => item.id === updatedCoupon.id); | |||
| if (couponIndex !== -1) { | |||
| // 使用Vue.set或者$set来确保响应式更新 | |||
| this.$set(this.couponData, couponIndex, updatedCoupon); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .coupon-list { | |||
| // 优惠券列表容器样式 | |||
| } | |||
| </style> | |||
| @ -0,0 +1,185 @@ | |||
| { | |||
| "orderId": 6429631920572416, | |||
| "payId": 6429631920572417, | |||
| "orderSn": "250803-6429631920572416", | |||
| "memberId": 214, | |||
| "totalAmount": 171.76, | |||
| "payAmount": 146.00, | |||
| "status": 0, | |||
| "aftersaleStatus": 1, | |||
| "orderItemList": [ | |||
| { | |||
| "createBy": 214, | |||
| "createTime": "2025-08-03 10:15:30", | |||
| "updateBy": null, | |||
| "updateTime": null, | |||
| "id": 6429631924668416, | |||
| "orderId": 6429631920572416, | |||
| "orderServiceId": 6429631921915904, | |||
| "productId": 63, | |||
| "outProductId": "P001", | |||
| "skuId": 404, | |||
| "outSkuId": null, | |||
| "productSnapshotId": null, | |||
| "skuSnapshotId": null, | |||
| "pic": "https://catmdogf.oss-cn-shanghai.aliyuncs.com/2024/01/08ba3980c107c2472397b13ef9769257e1陪玩.png", | |||
| "productName": "专业遛狗", | |||
| "salePrice": 63.75, | |||
| "purchasePrice": null, | |||
| "quantity": 1, | |||
| "productCategoryId": 76, | |||
| "spData": "{\"时间\":\"60\"}", | |||
| "isMainProduct": 1 | |||
| }, | |||
| { | |||
| "createBy": 214, | |||
| "createTime": "2025-08-03 10:15:31", | |||
| "updateBy": null, | |||
| "updateTime": null, | |||
| "id": 6429631929452544, | |||
| "orderId": 6429631920572416, | |||
| "orderServiceId": 6429631926798336, | |||
| "productId": 63, | |||
| "outProductId": "P001", | |||
| "skuId": 404, | |||
| "outSkuId": null, | |||
| "productSnapshotId": null, | |||
| "skuSnapshotId": null, | |||
| "pic": "https://catmdogf.oss-cn-shanghai.aliyuncs.com/2024/01/08ba3980c107c2472397b13ef9769257e1陪玩.png", | |||
| "productName": "专业遛狗", | |||
| "salePrice": 72.25, | |||
| "purchasePrice": null, | |||
| "quantity": 1, | |||
| "productCategoryId": 76, | |||
| "spData": "{\"时间\":\"60\"}", | |||
| "isMainProduct": 1 | |||
| } | |||
| ], | |||
| "orderServiceList": [ | |||
| { | |||
| "createBy": 214, | |||
| "createTime": "2025-08-03 10:15:31", | |||
| "updateBy": null, | |||
| "updateTime": null, | |||
| "id": 6429631926798336, | |||
| "orderId": 6429631920572416, | |||
| "pet": null, | |||
| "serviceFrequency": "1", | |||
| "serviceDate": "2025-09-11", | |||
| "serviceTimeFirst": null, | |||
| "serviceTimeSecond": null, | |||
| "delFlag": 0, | |||
| "petId": 188, | |||
| "expectServiceTime": "", | |||
| "feedCount": null, | |||
| "feedCountPrice": 0.00, | |||
| "petVo": { | |||
| "createBy": null, | |||
| "createTime": null, | |||
| "updateBy": null, | |||
| "updateTime": "2025-07-28 17:09:47", | |||
| "id": 188, | |||
| "name": "xx", | |||
| "petType": "cat", | |||
| "gender": "男生", | |||
| "birthDate": "2025-05", | |||
| "remark": "", | |||
| "photo": "https://cdn.catmdogd.com/2025/07/255102d859a38c4a3ca19a991e7e21aa6bvAZhN0q32GSUf5b3c2ccb949fbdcda8579544239b665.png", | |||
| "breed": "蓝白矮脚", | |||
| "bodyType": "小型(<10 KG)", | |||
| "personality": [ | |||
| "653", | |||
| "害羞腼腆", | |||
| "内向胆小", | |||
| "有攻击性" | |||
| ], | |||
| "vaccineStatus": "每年都免疫", | |||
| "sterilization": "已绝育", | |||
| "dewormingStatus": "未驱虫", | |||
| "doglicenseStatus": "", | |||
| "healthStatus": [ | |||
| "身体健康,无异常" | |||
| ], | |||
| "owner": "214" | |||
| }, | |||
| "pets": null, | |||
| "orderItems": null | |||
| }, | |||
| { | |||
| "createBy": 214, | |||
| "createTime": "2025-08-03 10:15:30", | |||
| "updateBy": null, | |||
| "updateTime": null, | |||
| "id": 6429631921915904, | |||
| "orderId": 6429631920572416, | |||
| "pet": null, | |||
| "serviceFrequency": "1", | |||
| "serviceDate": "2025-09-10", | |||
| "serviceTimeFirst": null, | |||
| "serviceTimeSecond": null, | |||
| "delFlag": 0, | |||
| "petId": 188, | |||
| "expectServiceTime": "", | |||
| "feedCount": null, | |||
| "feedCountPrice": 0.00, | |||
| "petVo": null, | |||
| "pets": null, | |||
| "orderItems": null | |||
| } | |||
| ], | |||
| "service": null, | |||
| "note": null, | |||
| "deliverySn": null, | |||
| "createTime": "2025-08-03 10:15:30", | |||
| "paymentTime": null, | |||
| "receiverName": "hly", | |||
| "receiverPhone": "19330214982", | |||
| "receiverProvince": "湖南省", | |||
| "receiverCity": "郴州市", | |||
| "receiverDistrict": "永兴县", | |||
| "receiverDetailAddress": "大桥路246号永兴县人民政府(大桥路北)12312", | |||
| "timeToPay": null, | |||
| "staffId": null, | |||
| "petVOList": [ | |||
| { | |||
| "createBy": null, | |||
| "createTime": null, | |||
| "updateBy": null, | |||
| "updateTime": "2025-07-28 17:09:47", | |||
| "id": 188, | |||
| "name": "xx", | |||
| "petType": "cat", | |||
| "gender": "男生", | |||
| "birthDate": "2025-05", | |||
| "remark": "", | |||
| "photo": "https://cdn.catmdogd.com/2025/07/255102d859a38c4a3ca19a991e7e21aa6bvAZhN0q32GSUf5b3c2ccb949fbdcda8579544239b665.png", | |||
| "breed": "蓝白矮脚", | |||
| "bodyType": "小型(<10 KG)", | |||
| "personality": [ | |||
| "653", | |||
| "害羞腼腆", | |||
| "内向胆小", | |||
| "有攻击性" | |||
| ], | |||
| "vaccineStatus": "每年都免疫", | |||
| "sterilization": "已绝育", | |||
| "dewormingStatus": "未驱虫", | |||
| "doglicenseStatus": "", | |||
| "healthStatus": [ | |||
| "身体健康,无异常" | |||
| ], | |||
| "owner": "214" | |||
| } | |||
| ], | |||
| "teacherId": null, | |||
| "companionLevel": 1, | |||
| "addressId": 261, | |||
| "longitude": "113.11659", | |||
| "latitude": "26.1272", | |||
| "memberDiscount": 25.76, | |||
| "couponDiscount": 0.00, | |||
| "oldPrice": 146.00, | |||
| "preFamiliarizePrice": 38.00, | |||
| "companionLevelPrice": 10.00, | |||
| "needPreFamiliarize": false | |||
| } | |||
| @ -0,0 +1,36 @@ | |||
| ## 1.1.13(2023-10-26) | |||
| 优化图片初始化逻辑 | |||
| 修护已知bug | |||
| ## 1.1.12(2023-06-27) | |||
| 修护vue3小程序下报错的bug | |||
| ## 1.1.11(2023-05-29) | |||
| 修护了在vue3下报错的bug | |||
| ## 1.1.10(2023-05-26) | |||
| 修改了已知bug | |||
| 暂时取消了vue3的支持 | |||
| ## 1.1.9(2023-03-24) | |||
| 修护bug | |||
| ## 1.1.8(2023-03-24) | |||
| 修护bug | |||
| ## 1.1.7(2022-12-08) | |||
| 修护bug | |||
| ## 1.1.6(2022-11-18) | |||
| 修好app无法拖动问题 | |||
| ## 1.1.5(2022-06-14) | |||
| 填新版HBuilderX的坑,简单测试是没问题了。 | |||
| ## 1.1.4(2022-02-15) | |||
| 修护ios下微信小程序第一次裁剪的bug | |||
| ## 1.1.3(2022-02-10) | |||
| 修护APP点击无效的bug | |||
| ## 1.1.2(2022-01-24) | |||
| 优化一些细节 | |||
| ## 1.1.1(2022-01-19) | |||
| 更新示例项目 | |||
| ## 1.1.0(2022-01-18) | |||
| 新增旋转功能 | |||
| ## 1.0.2(2022-01-13) | |||
| 修护mode="fixed"模式无效的bug | |||
| ## 1.0.1(2021-12-20) | |||
| 修护IOS下,小程序点击没反应的bug | |||
| ## 1.0.0(2021-12-06) | |||
| 图片裁剪工具 | |||
| @ -0,0 +1,81 @@ | |||
| { | |||
| "id": "ksp-cropper", | |||
| "displayName": "ksp-cropper", | |||
| "version": "1.1.13", | |||
| "description": "高性能图片裁剪工具", | |||
| "keywords": [ | |||
| "头像", | |||
| "图片", | |||
| "裁剪" | |||
| ], | |||
| "repository": "", | |||
| "engines": { | |||
| "HBuilderX": "^3.1.0" | |||
| }, | |||
| "dcloudext": { | |||
| "sale": { | |||
| "regular": { | |||
| "price": "0.00" | |||
| }, | |||
| "sourcecode": { | |||
| "price": "0.00" | |||
| } | |||
| }, | |||
| "contact": { | |||
| "qq": "" | |||
| }, | |||
| "declaration": { | |||
| "ads": "无", | |||
| "data": "插件不采集任何数据", | |||
| "permissions": "无" | |||
| }, | |||
| "npmurl": "", | |||
| "type": "component-vue" | |||
| }, | |||
| "uni_modules": { | |||
| "dependencies": [], | |||
| "encrypt": [], | |||
| "platforms": { | |||
| "cloud": { | |||
| "tcb": "y", | |||
| "aliyun": "y" | |||
| }, | |||
| "client": { | |||
| "Vue": { | |||
| "vue2": "y", | |||
| "vue3": "y" | |||
| }, | |||
| "App": { | |||
| "app-vue": "y", | |||
| "app-nvue": "u" | |||
| }, | |||
| "H5-mobile": { | |||
| "Safari": "y", | |||
| "Android Browser": "y", | |||
| "微信浏览器(Android)": "y", | |||
| "QQ浏览器(Android)": "y" | |||
| }, | |||
| "H5-pc": { | |||
| "Chrome": "u", | |||
| "IE": "u", | |||
| "Edge": "u", | |||
| "Firefox": "u", | |||
| "Safari": "u" | |||
| }, | |||
| "小程序": { | |||
| "微信": { | |||
| "minVersion": "2.9.0" | |||
| }, | |||
| "阿里": "n", | |||
| "百度": "n", | |||
| "字节跳动": "n", | |||
| "QQ": "u" | |||
| }, | |||
| "快应用": { | |||
| "华为": "u", | |||
| "联盟": "u" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,78 @@ | |||
| # ksp-cropper | |||
| ## 高性能图片裁剪工具 | |||
| ### 属性说明 | |||
| |属性 |类型 |默认 |备注 | | |||
| | :--------: | :-----: | :----: | :----: | | |||
| | url |String | "" | 需要裁剪的图片路径,为空时控件隐藏,不为空时控件显示| | |||
| | mode |String | "free" | 裁剪模式| | |||
| | width |Number | 200 | 图片裁剪后的宽度,固定大小时有效| | |||
| | height |Number | 200 | 图片裁剪后的高度,固定大小时有效| | |||
| | maxWidth |Number | 1024 | 图片裁剪后的最大宽度 | | |||
| | maxHeight |Number | 1024 | 图片裁剪后的最大高度 | | |||
| ### mode有效值 | |||
| | 模式 |值 |说明 | | |||
| | :-----: | :-----: | :----: | | |||
| | 固定模式 |fixed | 裁剪出指定大小的图片,一般用于头像上传 | | |||
| | 等比缩放 |ratio | 限定宽高比,裁剪大小不固定 | | |||
| | 自由模式 |free | 不限定宽高比,裁剪大小不固定 | | |||
| ### 事件说明 | |||
| |事件名称 |说明 |返回 | | |||
| | :--------: | :-----: | :----: | | |||
| | ok |点击确定按钮 | e:{path} | | |||
| | cancel |点击取消按钮 | - | | |||
| ### 示例 | |||
| ```html | |||
| <template> | |||
| <view> | |||
| <button @click="select">选择图片</button> | |||
| <image mode="widthFix" :src="path"/> | |||
| <ksp-cropper mode="free" :width="200" :height="140" :maxWidth="1024" :maxHeight="1024" :url="url" @cancel="oncancel" @ok="onok"></ksp-cropper> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| data() { | |||
| return { | |||
| url: "", | |||
| path: "" | |||
| } | |||
| }, | |||
| onLoad() { | |||
| }, | |||
| methods: { | |||
| select() { | |||
| uni.chooseImage({ | |||
| count: 1, | |||
| success: (rst) => { | |||
| // 设置url的值,显示控件 | |||
| this.url = rst.tempFilePaths[0]; | |||
| } | |||
| }); | |||
| }, | |||
| onok(ev) { | |||
| this.url = ""; | |||
| this.path = ev.path; | |||
| }, | |||
| oncancel() { | |||
| // url设置为空,隐藏控件 | |||
| this.url = ""; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| ``` | |||
| ### 注意 | |||
| 1.微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。<br/> | |||
| 2.微信小程序真机调试会报错,但正常运行是不会有问题的。<br/> | |||
| 3.uni-app版本不断更新,插件有时无法适应新版本,感谢大家及时提交bug,但希望大家手下留情,不要轻易给差评。 | |||