Browse Source

feat(订单管理): 添加订单详情服务项目组件及多项优化

refactor(用户管理): 显示角色标签并隐藏手机号
chore(环境配置): 切换默认环境至trial
fix(地址认证): 增加接单范围验证和地区选择校验
feat(订单列表): 添加空状态显示
perf(我的订单): 优化请求处理防止重复加载
style(订单详情): 调整服务内容展示样式并添加宠物体型显示
master
前端-胡立永 20 hours ago
parent
commit
d72cd427e6
7 changed files with 525 additions and 34 deletions
  1. +352
    -0
      components/order/ServiceItems.vue
  2. +14
    -6
      otherPages/authentication/connectAddress/detail.vue
  3. +93
    -10
      otherPages/orderTakingManage/detail/index.vue
  4. +23
    -3
      pages/myOrdersManage/index.vue
  5. +38
    -10
      pages/orderTakingManage/components/list.vue
  6. +2
    -2
      pages/userManage/index.vue
  7. +3
    -3
      utils/getUrl.js

+ 352
- 0
components/order/ServiceItems.vue View File

@ -0,0 +1,352 @@
<template>
<view class="service-items-card">
<view class="card-title">
<text>服务项目及费用</text>
</view>
<!-- 服务项目列表 -->
<view class="service-items-list">
<view class="service-item" v-for="(item, index) in items" :key="index">
<view class="item-header">
<view class="item-id">{{item.day}}</view>
<text style="margin: 0 10rpx;">|</text>
<view class="item-name">
<text>{{ item.itemsText[0] }}</text>
<text v-if="item.itemsText.length >= 2">
{{ item.itemsText[0] }}+{{ item.itemsText[item.itemsText.length - 1] }}{{ item.itemsText.length }}
</text>
</view>
<view class="item-price-action">
<view class="item-price">¥{{item.price.toFixed(2)}}</view>
<!-- 展开按钮 -->
<view class="expand-btn" @click="toggleExpand(index)" v-if="item.pet || (item.pets && item.pets.length > 0)">
<!-- <text>{{ expandedItems.includes(index) ? '收起' : '展开' }}</text> -->
<view class="expand-icon" :class="{'expanded': expandedItems.includes(index)}">
<text class="icon-arrow">{{ expandedItems.includes(index) ? '∧' : '∨' }}</text>
</view>
</view>
</view>
</view>
<!-- 详细信息区域 -->
<view class="detail-area" v-if="expandedItems.includes(index)">
<view class="detail-area-item" v-for="(pet, petIndex) in item.pets" :key="petIndex">
<!-- 宠物名称和头像 -->
<view class="item-pet" v-if="pet">
<view class="pet-avatar">
<image :src="pet.photo" mode="aspectFill"></image>
</view>
<text>{{pet.name}}</text>
</view>
<!-- 定制服务 -->
<view class="custom-services-list" v-if="pet.itemList && pet.itemList.length > 0">
<view class="custom-service-item" v-for="(t, itemIndex) in pet.itemList" :key="itemIndex">
<view class="service-name">{{t.productName}}</view>
<view class="service-price">¥{{ t.salePrice && t.salePrice.toFixed(2) }} × {{t.quantity}} </view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 费用合计 -->
<view class="cost-summary" v-if="totalAmount">
<view class="cost-item">
<text class="cost-label">费用合计</text>
<text class="cost-value">¥{{totalAmount.toFixed(2)}}</text>
</view>
<view class="cost-item discount" v-if="discount > 0">
<text class="cost-label">平台优惠</text>
<text class="cost-value">- ¥{{discount.toFixed(2)}}</text>
</view>
<view class="cost-item discount" v-if="memberDiscount > 0">
<text class="cost-label">会员优惠</text>
<text class="cost-value">- ¥{{memberDiscount.toFixed(2)}}</text>
</view>
<view class="cost-item total">
<text class="cost-label">应付金额</text>
<text class="cost-value">¥{{finalAmount.toFixed(2)}}</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
totalAmount: {
type: Number,
default: 0
},
discount: {
type: Number,
default: 0
},
memberDiscount: {
type: Number,
default: 0
},
finalAmount: {
type: Number,
default: 0
}
},
data() {
return {
expandedItems: [] //
}
},
methods: {
toggleExpand(index) {
const position = this.expandedItems.indexOf(index);
if (position === -1) {
//
this.expandedItems.push(index);
} else {
//
this.expandedItems.splice(position, 1);
}
}
}
}
</script>
<style lang="scss" scoped>
.service-items-card {
padding: 30rpx 10rpx;
padding-bottom: 0;
}
.card-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 8rpx;
height: 32rpx;
background-color: #FFAA48;
margin-right: 16rpx;
border-radius: 4rpx;
}
}
.service-items-list {
.service-item {
padding: 20rpx 0;
border-bottom: 1px solid #EEEEEE;
&:last-child {
border-bottom: none;
}
.item-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.item-id {
font-size: 26rpx;
margin-right: 10rpx;
}
.item-name {
font-size: 26rpx;
color: #333;
flex: 1;
display: flex;
}
.item-price-action {
display: flex;
align-items: center;
gap: 15rpx;
.item-price {
font-size: 28rpx;
font-weight: bold;
}
}
}
//
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
// padding: 6rpx 12rpx;
width: 40rpx;
height: 40rpx;
font-size: 24rpx;
color: #666;
background-color: #FFF5E6;
border-radius: 20rpx;
border: 1px solid #FFAA48;
.expand-icon {
// margin-left: 6rpx;
transition: transform 0.3s ease;
&.expanded {
transform: rotate(180deg);
}
.icon-arrow {
font-size: 24rpx;
color: #FFAA48;
}
}
}
//
.detail-area {
animation: fadeIn 0.3s ease;
display: flex;
flex-direction: column;
gap: 10rpx;
.detail-area-item{
background-color: #F8F8F8;
border-radius: 10rpx;
margin-top: 10rpx;
padding: 15rpx;
}
.item-pet {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
.pet-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
text {
font-weight: 500;
font-size: 32rpx;
}
}
.custom-services-list {
.custom-service-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
border-bottom: 1px solid #F0F0F0;
&:last-child {
border-bottom: none;
}
.service-name {
font-size: 28rpx;
color: #333;
font-weight: 400;
}
.service-price {
font-size: 28rpx;
color: #999;
font-weight: 400;
}
}
}
}
.custom-services {
padding: 10rpx 0 10rpx 20rpx;
.custom-service-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
.service-name {
font-size: 24rpx;
color: #666;
}
.service-price {
font-size: 24rpx;
color: #999;
}
}
}
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.cost-summary {
margin-top: 30rpx;
padding-top: 20rpx;
border-top: 1px dashed #EEEEEE;
.cost-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.cost-label {
font-size: 26rpx;
color: #666;
}
.cost-value {
font-size: 26rpx;
color: #333;
}
&.discount {
.cost-value {
color: #FF5252;
}
}
&.total {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1px dashed #EEEEEE;
.cost-label {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.cost-value {
font-size: 32rpx;
font-weight: bold;
color: #FF5252;
}
}
}
}
</style>

+ 14
- 6
otherPages/authentication/connectAddress/detail.vue View File

@ -129,10 +129,8 @@
// latitude: eidtItem?.value?.latitude || null,
// longitude: eidtItem?.value?.longitude || null
// }
});
onShow(() => {
getDetail()
})
});
const setAddress = (res) => {
form.value.latitude = res.latitude
@ -164,14 +162,23 @@
const onSave = async () => {
// 3-15
if (form.value.rangeNo < 3 || form.value.rangeNo > 15) {
if (isNaN(form.value.rangeNo - 1) || form.value.rangeNo < 3 || form.value.rangeNo > 15) {
uni.showToast({
title: '接单范围必须在3-15公里之间',
title: '接单范围必须是3-15公里之间的数字',
icon: 'none'
})
return
}
//
if (!form.value.area) {
uni.showToast({
title: '请选择地区',
icon: 'none'
})
return
}
let code = null;
if (eidtItem?.value?.id) {
let result = await udpateAddress({
@ -208,7 +215,8 @@
address: eidtItem?.value?.address || null,
status: eidtItem?.value?.status == 'true' || null,
latitude: form.value.latitude || eidtItem?.value?.latitude || null,
longitude: form.value.latitude || eidtItem?.value?.longitude || null
longitude: form.value.latitude || eidtItem?.value?.longitude || null,
rangeNo: form.value.rangeNo || eidtItem?.value?.rangeNo || null
}
})
}


+ 93
- 10
otherPages/orderTakingManage/detail/index.vue View File

@ -61,7 +61,7 @@
</view>
</view>
<view class="service box-size" :style="{ borderRadius: '8rpx' }">
<view class="horizontal_distribution service_top">
<!-- <view class="horizontal_distribution service_top">
<view class="form-title">
服务内容和酬劳
</view>
@ -96,12 +96,15 @@
<text>{{ it.name }}</text>
<text style="color: #999;padding-left: 20rpx">上门1次</text>
</view>
<!-- <view class="pet-level">
<view class="pet-level">
12-15
<text>专业喂养</text>
上门1次
</view> -->
</view>
</view>
</view> -->
<ServiceItems :items="serviceList"/>
<view class="line1">
</view>
<view class="flex money_total">
@ -205,7 +208,7 @@
</view>
<view class="desc">
<view v-for="pet in orderDetail.order.h5OrderVO.petVOList" :key="pet.id">
{{ pet.breed }}
{{ pet.breed }}{{ pet.bodyType }}
</view>
</view>
</view>
@ -273,6 +276,8 @@
useStore
} from "vuex"
import ServiceItems from '@/components/order/ServiceItems.vue'
import dayjs from "dayjs";
import configPopup from '@/components/configPopup.vue'
const configPopupRef = ref(null)
@ -291,6 +296,7 @@
const store = useStore();
const orderId = ref(null);
const orderDetail = ref({});
const serviceList = ref([]);
const showConfirmOrder = ref(false);
const showOrderSuccess = ref(false);
const value = ref("45619491656")
@ -315,7 +321,7 @@
},
{
title : '个人订单',
content : '订单为系统派发,请确认订单信息后再抢单',
content : '订单为个人订单,请确认订单信息后再接单',
},
{
title : '流失订单',
@ -386,6 +392,78 @@
});
if (response.code == 200 && response.data) {
orderDetail.value = response.data;
let data = response.data.order.h5OrderVO;
//==========================================
let items = []
//1
let days = [...new Set(data.orderServiceList.map(item => item.serviceDate))]
days.forEach(day => {
let price = 0;
let itemsText = []
//2
let dayItems = data.orderServiceList.filter(item => item.serviceDate === day)
//3
let pets = data.petVOList.filter(item => dayItems.some(dayItem => dayItem.petId === item.id))
//pets
pets = JSON.parse(JSON.stringify(pets))
//4
pets.forEach(pet => {
pet.serviceList = dayItems.filter(item => item.petId === pet.id)
let itemList = []
//5
pet.serviceList.forEach(item => {
itemList.push(...(data.orderItemList.filter(n => n.orderServiceId == item.id)))
})
itemList.forEach(p => {
price += p.salePrice * p.quantity
itemsText = [...new Set([...itemsText, p.productName])]
})
pet.itemList = itemList
})
//6items
items.push({
price,
pets,
day,
itemsText,
})
})
//
items = items.sort((a, b) => dayjs(a.day).valueOf() - dayjs(b.day).valueOf())
items.forEach(n => {
n.day = dayjs(n.day).format('MM-DD')
})
console.log(items);
serviceList.value = items
//==========================================
orderDetail.value.order.h5OrderVO.petVOList.forEach(pet => {
pet.productNameText = getProductNameText(pet.id, orderDetail.value.order.h5OrderVO.orderItemList,
@ -445,9 +523,11 @@
}
function getTextList(){
return orderDetail.value.order?.h5OrderVO
.orderServiceList.map(item =>
dayjs(item.serviceDate).format('MM-DD'))
// return orderDetail.value.order?.h5OrderVO
// .orderServiceList.map(item =>
// dayjs(item.serviceDate).format('MM-DD'))
return serviceList.value.map(n => n.day)
}
const toDetail = (id) => {
@ -527,6 +607,8 @@
.desc {
color: #999999;
font-size: 28rpx;
margin-top: 20rpx;
}
}
@ -542,7 +624,8 @@
background: #FFBF60;
color: white;
width: 100%;
height: 60rpx;
font-size: 26rpx;
padding: 15rpx 20rpx;
border-radius: 30rpx;
}
}


+ 23
- 3
pages/myOrdersManage/index.vue View File

@ -100,7 +100,12 @@
import {
useStore
} from "vuex"
import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
onLoad(() => {
requestId.value = 0;
activeIndex.value = 1;
})
onShow(() => {
if (!getLoginStatus()) return;
@ -112,6 +117,7 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
const list = ref([])
const dateOrderList = ref([])
const loading = ref(false) // loading
const requestId = ref(0) //
const store = useStore();
const userInfo = computed(() => {
@ -154,6 +160,8 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
function getList() {
loading.value = true; //
requestId.value++; //
const currentRequestId = requestId.value; // ID
let index = current.value;
@ -163,6 +171,11 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
userId: userInfo.value.userId
})
.then(res => {
//
if (currentRequestId !== requestId.value) {
return; //
}
if (res.code == 200) {
list.value = res.data.rows
@ -177,7 +190,8 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
}
})
.finally(() => {
if(activeIndex.value == 1){
// loading
if(activeIndex.value == 1 && currentRequestId === requestId.value){
loading.value = false; //
}
})
@ -189,6 +203,11 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
masterId: userInfo.value.userId
})
.then(res => {
//
if (currentRequestId !== requestId.value) {
return; //
}
if (res.code == 200) {
dateOrderList.value = res.data
//
@ -200,7 +219,8 @@ import { getOrderServiceText, getProductNameText } from '@/utils/serviceTime.js'
}
})
.finally(() => {
if(activeIndex.value == 2){
// loading
if(activeIndex.value == 2 && currentRequestId === requestId.value){
loading.value = false; //
}
})


+ 38
- 10
pages/orderTakingManage/components/list.vue View File

@ -1,15 +1,23 @@
<template>
<up-list @scrolltolower="scrolltolower">
<up-list-item v-for="item in orderList">
<view class="mb28 container-list-item">
<view class="flex-between flex" style="background: #FFF4E5;padding: 22rpx 42rpx">
<view>
<text v-if="current != 2">待接单</text>
</view>
<view>本单酬劳
<text style="color: #FF530A">{{ item.orderGive }}</text>
<view>
<!-- 空状态 -->
<view v-if="!orderList || orderList.length === 0" class="empty-state">
<image src="/static/images/ydd/empty.png" mode="aspectFit" class="empty-image"></image>
<text class="empty-text">暂无订单数据</text>
</view>
<!-- 订单列表 -->
<up-list v-else @scrolltolower="scrolltolower">
<up-list-item v-for="item in orderList">
<view class="mb28 container-list-item">
<view class="flex-between flex" style="background: #FFF4E5;padding: 22rpx 42rpx">
<view>
<text v-if="current != 2">待接单</text>
</view>
<view>本单酬劳
<text style="color: #FF530A">{{ item.orderGive }}</text>
</view>
</view>
</view>
<view class="container-list">
<view class="flex mb28"
@click="toDetail(pet.id)"
@ -60,6 +68,7 @@
</view>
</up-list-item>
</up-list>
</view>
<Modal @confirm="confirm" @cancel="cancel" ref="modal">
<template>
@ -231,4 +240,23 @@ const noneOrder = async () => {
text-overflow: ellipsis;
-o-text-overflow:ellipsis;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
}
</style>

+ 2
- 2
pages/userManage/index.vue View File

@ -11,11 +11,11 @@
<view class="base-leavel">
<text>{{getIsLogin()?userInfo.userName:"欢迎来到版宠师"}}</text>
</view>
<!-- <view>
<view>
<image v-if="petMaster" class="role-tag" :src="petMaster" mode="aspectFill"></image>
<image v-if="partnerMaster" class="role-tag" :src="partnerMaster" mode="aspectFill"></image>
</view>
<view>
<!-- <view>
手机号:{{ userInfo?.userTelephone }}
</view> -->
</view>


+ 3
- 3
utils/getUrl.js View File

@ -1,4 +1,4 @@
let current = "develop";
let current = "trial";
const accountInfo = wx.getAccountInfoSync();
// current = accountInfo.miniProgram.envVersion;
@ -6,9 +6,9 @@ const api = {
develop:"http://127.0.0.1:8002",
// develop:"http://h5.xzaiyp.top",
// develop:"https://api.catmdogd.com/prod-api",
// develop:"https://pet-admin.hhlm1688.com/api",
trial:"https://pet-admin.hhlm1688.com/api",
// develop: "http://youyi-test.natapp1.cc/prod-api", // 开发
trial: "https://api.catmdogd.com/prod-api", //测试
// trial: "https://api.catmdogd.com/prod-api", //测试
release: "https://api.catmdogd.com/prod-api",
}


Loading…
Cancel
Save