宠物详情页面更新,优化了这个页面的动画效果,新增小刷新动画

This commit is contained in:
aiShuiJiaoDeXioShou 2024-09-10 22:13:52 +08:00
parent 10335b8b2c
commit 7638324bb9
13 changed files with 368 additions and 106 deletions

16
.idea/addr.iml Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/acdr-ui/node_modules/conventional-changelog-angular/templates" />
</list>
</option>
</component>
</module>

View File

@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -2,7 +2,7 @@
<view
:class="[
'pos-fixed z-999 left-[1px] flex gap-[5px] items-center content-center justify-center',
isH5 ? 'top-[20px]' : 'top-[40px]',
isH5 ? 'top-[3vw]' : 'top-[6vw]',
]"
>
<image src="/static/addresscell/location.png" class="w-[30px]" mode="widthFix" />

View File

@ -1,66 +1,62 @@
<template>
<view
class="loading-overlay"
:catchtouchmove="modelValue"
@click.stop="stopEvent"
v-if="modelValue"
>
<view class="loading-animation">
<view
v-for="color in colors"
:key="color"
:style="{ backgroundColor: color }"
class="ball"
></view>
<wd-overlay :show="modelValue">
<view class="flex flex-col items-center justify-center content-center w-full h-full">
<view class="loading-animation">
<view
v-for="color in colors"
:key="color"
:style="{ backgroundColor: color }"
class="ball"
></view>
</view>
</view>
</view>
</wd-overlay>
</template>
<script setup>
import { defineProps, watchEffect } from 'vue'
import { defineProps } from 'vue'
const props = defineProps({
modelValue: Boolean,
timeout: {
// timeout
type: Number,
default: 5000, // 5
},
})
const stopEvent = (event) => {
event.preventDefault() //
event.stopPropagation() //
}
const emit = defineEmits(['update:modelValue'])
const preventTouchMove = (event) => {
event.preventDefault()
}
// H5APP
// #ifdef H5 || APP-PLUS
watchEffect(() => {
if (props.modelValue) {
document.addEventListener('touchmove', preventTouchMove, { passive: false })
} else {
document.removeEventListener('touchmove', preventTouchMove)
}
})
// #endif
let timer = null
const colors = ['#FF6347', '#4682B4', '#32CD32', '#FFD700', '#FF69B4', '#00FA9A']
//
const startTimeout = () => {
clearTimeout(timer)
timer = setTimeout(() => {
stopLoading() //
}, props.timeout)
}
const stopLoading = () => {
// emit
clearTimeout(timer)
emit('update:modelValue', false)
}
onLoad(() => {
//
startTimeout()
})
onUnload(() => {
//
clearTimeout(timer)
})
</script>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto; /* 添加这行代码 */
background: rgba(0, 0, 0, 0.5);
}
.loading-animation {
display: flex;
gap: 5px;

View File

@ -0,0 +1,68 @@
<template>
<view class="w-full h-[20px] flex flex-col items-center" v-if="modelValue">
<view class="loading-animation">
<view
v-for="color in colors"
:key="color"
:style="{ backgroundColor: color }"
class="ball"
></view>
</view>
</view>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
modelValue: Boolean,
})
const colors = ['#FF6347', '#4682B4', '#32CD32', '#FFD700', '#FF69B4', '#00FA9A']
</script>
<style scoped>
.loading-animation {
display: flex;
gap: 5px;
justify-content: space-around;
width: 100px;
}
.ball {
width: 15px;
height: 15px;
border-radius: 50%;
animation: bounce 1.2s infinite ease-in-out;
}
.ball:nth-child(1) {
animation-delay: -0.32s;
}
.ball:nth-child(2) {
animation-delay: -0.16s;
}
.ball:nth-child(3) {
animation-delay: -0.13s;
}
.ball:nth-child(4) {
animation-delay: -0.07s;
}
.ball:nth-child(5) {
animation-delay: -0.04s;
}
.ball:nth-child(6) {
animation-delay: 0s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>

View File

@ -94,7 +94,7 @@ const clickmap = () => {
//
const getLocation = () => {
uni.showLoading({ title: '正在获取定位' })
// uni.showLoading({ title: '' })
uni.getLocation({
type: 'gcj02',
timeout: 1000,
@ -114,7 +114,7 @@ const getLocation = () => {
},
fail: (err) => {
console.log(err)
uni.hideLoading()
// uni.hideLoading()
uni.showModal({
title: '提示',
content: '位置信息获取失败(请确定定位功能是否打开)',

View File

@ -11,7 +11,8 @@
</view>
</view>
</view>
<EmptyState v-else />
<LocalLoader v-model="loading" />
<EmptyState v-if="services.length == 0 && !loading" />
</template>
<script lang="js" setup>
@ -21,12 +22,14 @@ import { imgUrl } from '@/utils/commUtils'
import { getNearbyServices } from '@/service/personalService'
import { getLocation } from '@/service/mapService'
import EmptyState from '@/components/EmptyState.vue'
import LocalLoader from '@/components/LocalLoader.vue'
const toDetail = (item) => {
uni.navigateTo({
url: '/pages/service/detail?id=' + item.id,
})
}
const loading = ref(false)
const services = ref([])
@ -39,7 +42,9 @@ const getQuickServices = async () => {
}
onLoad(async () => {
loading.value = true
await getQuickServices()
loading.value = false
})
</script>

View File

@ -12,7 +12,7 @@
<Banner />
<AddressCell />
<!-- 这里放搜索框 -->
<view class="w-[70vw] pos-absolute top-[3vw] right-0">
<view :class="['w-[70vw] pos-absolute right-0', isH5 ? 'top-[1vw]' : 'top-[3vw]']">
<Search />
</view>
<loading-animation v-model="isLoading" />
@ -86,6 +86,7 @@ import { httpGet } from '@/utils/http'
import Tabbar from '@/components/Tabbar.vue'
import AddressCell from '@/components/AddressCell.vue'
import Search from '@/components/Search.vue'
import { isH5 } from '@/utils/platform'
const isLoading = ref(false)
const petInfo = ref({})

View File

@ -30,8 +30,8 @@
<view class="bg-white p-4">
<text class="text-2xl font-bold">{{ serviceData.userName }}</text>
<view class="flex items-center mt-2">
<text class="text-sm text-gray-500 mr-2">认证1年10个月</text>
<text class="text-sm text-gray-500">服务过200+</text>
<text class="text-sm text-gray-500 mr-2">认证{{ serviceData.certificationTime }}</text>
<text class="text-sm text-gray-500">服务过 {{ serviceData.serviceNumber }} </text>
</view>
<view class="flex items-center mt-2">
<wd-icon name="location" size="20" class="text-[#ffc107]"></wd-icon>
@ -82,20 +82,22 @@
<!-- 用户评价 -->
<view class="bg-white p-4">
<text class="text-lg font-bold mb-2">用户评价</text>
<view class="mb-4">
<view class="mb-4" v-if="serviceData.comment">
<view class="flex items-center mb-2">
<text class="text-pink-500 text-lg">5.0</text>
<text class="text-pink-500 text-lg">{{ serviceData.comment.star }}</text>
<view class="flex items-center ml-2">
<wd-icon name="star-on" size="20" class="text-[#ffc107]"></wd-icon>
<wd-icon name="star-on" size="20" class="text-[#ffc107]"></wd-icon>
<wd-icon name="star-on" size="20" class="text-[#ffc107]"></wd-icon>
<wd-icon name="star-on" size="20" class="text-[#ffc107]"></wd-icon>
<wd-icon name="star-on" size="20" class="text-[#ffc107]"></wd-icon>
<wd-icon
v-for="num in serviceData.comment.star"
name="star-on"
size="20"
class="text-[#ffc107]"
></wd-icon>
</view>
<text class="text-gray-500 ml-2">(14条评论)</text>
<text class="text-gray-500 ml-2">({{ serviceData.comment.commentNum }}条评论)</text>
</view>
<text class="text-gray-600">阿落超级细心下次一定还找阿落超级安心</text>
<text class="text-gray-600">{{ serviceData.comment.comment }}</text>
</view>
<view class="text-gray-500 pt-2 w-full text-center" v-else>暂无评论</view>
</view>
<!-- 底部地图展示 -->
@ -132,14 +134,14 @@
<!-- 预约弹窗 -->
<view
v-if="showReservationModal"
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
class="z-999 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<view class="bg-white rounded-lg p-4 w-11/12">
<view class="text-lg font-bold mb-4">选择预约信息</view>
<!-- 服务宠物选择 -->
<text class="text-sm text-gray-500 mb-2">服务宠物</text>
<view class="flex space-x-4 mb-4 scroll-x overflow-x-auto" scroll-x>
<view v-if="pets.length > 0" class="flex space-x-4 mb-4 scroll-x overflow-x-auto" scroll-x>
<view
v-for="pet in pets"
:key="pet.id"
@ -154,6 +156,11 @@
<text class="text-sm">{{ pet.name }}</text>
</view>
</view>
<view v-else class="pet-item add-pet" @click="toPath('/pages/pet/pet-add-page')">
<w-avatar :size="80" class="pet-avatar add-avatar">
<view class="add-icon">+</view>
</w-avatar>
</view>
<!-- 预约时间 -->
<text class="text-sm text-gray-500 mb-2">预约时间</text>
@ -180,7 +187,11 @@
{{ selectedAddress.display || '请选择地址' }}
</view>
</picker>
<view v-else>请选择地址</view>
<view v-else class="pet-item add-pet" @click="toPath('/pages/address/index')">
<w-avatar :size="80" class="pet-avatar add-avatar">
<view class="add-icon">+</view>
</w-avatar>
</view>
<!-- 操作按钮 -->
<view class="fixed bottom-0 left-0 w-full p-4 bg-white shadow-up">
@ -201,15 +212,18 @@
</view>
</view>
</view>
<LoadingAnimation v-model="loading" />
</template>
<script lang="js" setup>
import { ref } from 'vue'
import { httpGet } from '@/utils/http'
import { baseUrl, toast } from '@/utils/commUtils'
import { baseUrl, toast, toPath } from '@/utils/commUtils'
import { pay } from '@/logic/pay'
import TopBar from '@/components/TopBar.vue'
import Map from '@/components/Map.vue'
import LoadingAnimation from '@/components/LoadingAnimation.vue'
//
const nowday = new Date()
@ -217,7 +231,7 @@ const year = nowday.getFullYear()
const month = (nowday.getMonth() + 1).toString().padStart(2, '0') // 01
const day = nowday.getDate().toString().padStart(2, '0')
const reservationDate = ref(`${year}-${month}-${day}`)
const loading = ref(false)
const serviceData = ref({})
const pets = ref([])
const addressList = ref([])
@ -264,12 +278,12 @@ const setDateRange = () => {
weekFromToday.value = weekLater.toISOString().split('T')[0]
}
// Handle the date change event
//
const handleDateChange = (e) => {
reservationDate.value = e.detail.value
}
// Handle the address change event
//
const handleAddressChange = (e) => {
selectedAddress.value = addressList.value[e.detail.value]
}
@ -301,48 +315,85 @@ const confirmReservation = async () => {
//
onLoad(async (options) => {
if (!options.id) {
toast('该服务不存在!')
//
uni.navigateBack()
return
}
const id = options.id
personalServiceId.value = options.id
setDateRange()
loading.value = true
try {
if (!options.id) {
toast('该服务不存在!')
//
uni.navigateBack()
return
}
const id = options.id
personalServiceId.value = options.id
setDateRange()
//
const serviceResponse = await httpGet(`/personal-service/service/${id}`)
if (serviceResponse.code === 200) {
serviceData.value = serviceResponse.data
} else {
uni.showToast({ title: '加载服务数据失败', icon: 'none' })
//
const serviceResponse = await httpGet(`/personal-service/service/${id}`)
if (serviceResponse.code === 200) {
serviceData.value = serviceResponse.data
} else {
toast(serviceResponse.message)
}
//
const petsResponse = await httpGet('/petInfo/my')
if (petsResponse.code === 200) {
pets.value = petsResponse.data
if (pets.value.length !== 0) selectedPetId.value = pets.value[0].id
} else {
toast(petsResponse.message)
}
//
const addressResponse = await httpGet('/china-address/my')
if (addressResponse.code === 200) {
addressList.value = addressResponse.data
addressList.value.forEach((item) => {
item.display = `${item.province} ${item.city} ${item.district} ${item.detailAddress}`
})
selectedAddress.value = addressList.value[0]
} else {
toast(addressResponse.message)
}
} catch (e) {
console.log(e)
toast('获取服务详情失败!')
}
//
const petsResponse = await httpGet('/petInfo/my')
if (petsResponse.code === 200) {
pets.value = petsResponse.data
selectedPetId.value = pets.value[0].id
} else {
uni.showToast({ title: '加载宠物数据失败', icon: 'none' })
}
//
const addressResponse = await httpGet('/china-address/my')
if (addressResponse.code === 200) {
addressList.value = addressResponse.data
addressList.value.forEach((item) => {
item.display = `${item.province} ${item.city} ${item.district} ${item.detailAddress}`
})
selectedAddress.value = addressList.value[0]
} else {
uni.showToast({ title: '加载地址数据失败', icon: 'none' })
}
loading.value = false
})
</script>
<style scoped>
.pet-item {
display: flex;
flex-direction: column;
}
.pet-avatar {
border: 2px solid #fcd038;
border-radius: 50%;
}
.add-avatar {
width: 102rpx;
height: 102rpx;
background-color: #fff;
border: 2px dashed #fcd038;
}
.pet-label {
font-size: 14px;
color: #666;
}
.add-icon {
font-size: 36px;
line-height: 45px;
color: #fcd038;
text-align: center;
}
.options {
position: fixed;
bottom: 0;

View File

@ -94,4 +94,5 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return this.update(null, updateWrapper);
}
}

View File

@ -7,10 +7,15 @@ import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.yskj.acdr.common.response.GlobalResponse;
import com.yskj.acdr.master.address.service.ServiceAddressService;
import com.yskj.acdr.master.order.entity.Order;
import com.yskj.acdr.master.order.orderenum.OrderState;
import com.yskj.acdr.master.order.service.OrderService;
import com.yskj.acdr.master.personal.entity.PersonalService;
import com.yskj.acdr.master.personal.service.PersonalServiceService;
import com.yskj.acdr.master.pet.entity.PetInfo;
import com.yskj.acdr.master.pet.entity.PetSpecialistCertificate;
import com.yskj.acdr.master.pet.service.PetInfoService;
import com.yskj.acdr.master.pet.service.PetSpecialistCertificateService;
import com.yskj.acdr.master.user.entity.Users;
import com.yskj.acdr.master.user.service.UsersService;
import io.swagger.annotations.*;
@ -21,6 +26,9 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -51,6 +59,12 @@ public class PersonalServiceController {
@Resource
private ServiceAddressService mapService;
@Resource
private PetSpecialistCertificateService pscs;
@Resource
private OrderService orderService;
@PostMapping("/push")
public GlobalResponse<Boolean> push(
@ -140,6 +154,30 @@ public class PersonalServiceController {
* @param id 查询服务id
* @author linghe
* 展示宠物个人服务详细信息
* 返回如下数据
* // 将这些数据都打散装配到Map当中
* var map = MapUtil.<String, Object>builder()
* .put("serviceId", p.getId())
* .put("serviceName", p.getServiceName())
* .put("bgUrl", p.getUrl())
* .put("description", p.getDescription())
* .put("serviceType", p.getType())
* .put("latitude", p.getLatitude())
* .put("longitude", p.getLongitude())
* .put("address", p.getAddress())
* .put("serviceState", p.getState())
* .put("createTime", p.getCreateTime())
* .put("updateTime", p.getUpdateTime())
* .put("price", p.getPrice())
* .put("serviceUserId", p.getUserId()) // 服务者ID
* .put("userName", user.getNickname())
* .put("userAvatar", user.getAvatar())
* .put("pets", pets)
* // 添加认证时间
* .put("certificationTime", calculateTimeDifference(LocalDateTime.now(), psc.getCreateTime()))
* .put("serviceNumber", psc.getServiceNumber())
* .put("comments", comments)
* .map();
*/
@GetMapping("/service/{id}")
public GlobalResponse<Map<String, Object>> personalDetail(@PathVariable Long id) {
@ -160,6 +198,49 @@ public class PersonalServiceController {
"profileUrl", petInfo.getProfileUrl()))
.toList();
// 获取该宠托师其他信息服务次数认证年龄
PetSpecialistCertificate psc = pscs.getOne(new LambdaQueryWrapper<PetSpecialistCertificate>()
.eq(PetSpecialistCertificate::getUserId, userId)
// 小于过期时间
.gt(PetSpecialistCertificate::getExpiredTime, LocalDateTime.now()));
if (psc == null) {
return GlobalResponse.failure("该用户没有宠物师证书,或者宠物师证书被吊销!");
}
// 获取该宠托师以往的订单评论信息, 只获取历史数据的前一条
// 获取服务次数
long commitNums = orderService.count(new LambdaQueryWrapper<Order>()
.eq(Order::getPersonalServiceUserId, userId)
.eq(Order::getState, OrderState.EVALUATED)
.isNotNull(Order::getFeedback)
.isNotNull(Order::getStar));
// 获取之后获取该用户的评论信息
Order commentsOrder = null;
if(commitNums > 0) {
commentsOrder = orderService.lambdaQuery()
.eq(Order::getPersonalServiceUserId, userId)
.eq(Order::getState, OrderState.EVALUATED)
.isNotNull(Order::getFeedback)
.isNotNull(Order::getStar)
.orderByDesc(Order::getStar)
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1")
.one();
}
Map<String, Object> comments = null;
if (commentsOrder != null) {
comments = new HashMap<>();
Users users = usersService.getById(commentsOrder.getUserId());
comments.put("star", commentsOrder.getStar());
comments.put("comment", commentsOrder.getFeedback());
comments.put("commentNum", commitNums);
comments.put("userName", users.getNickname());
comments.put("userAvatar", users.getAvatar());
}
// 将这些数据都打散装配到Map当中
var map = MapUtil.<String, Object>builder()
.put("serviceId", p.getId())
@ -175,14 +256,43 @@ public class PersonalServiceController {
.put("updateTime", p.getUpdateTime())
.put("price", p.getPrice())
.put("serviceUserId", p.getUserId()) // 服务者ID
.put("userName", user.getName())
.put("userName", user.getNickname())
.put("userAvatar", user.getAvatar())
.put("pets", pets)
// 添加认证时间
.put("certificationTime", calculateTimeDifference(LocalDateTime.now(), psc.getCreateTime()))
.put("serviceNumber", psc.getServiceNumber())
.put("comments", comments)
.map();
return GlobalResponse.success(map);
}
public static String calculateTimeDifference(LocalDateTime startDate, LocalDateTime endDate) {
// 计算年差
long years = ChronoUnit.YEARS.between(startDate, endDate);
LocalDateTime tempDateTime = startDate.plusYears(years);
// 计算月差
long months = ChronoUnit.MONTHS.between(tempDateTime, endDate);
tempDateTime = tempDateTime.plusMonths(months);
// 计算天差
long days = ChronoUnit.DAYS.between(tempDateTime, endDate);
// 如果天数小于一个月按一个月计算
if (days >= 0 && days < 30) {
months++;
}
if (years == 0) {
return months + "个月";
}
// 返回 xx年xx月 格式
return years + "" + months + "个月";
}
/**
* 根据用户的位置获取附近服务的位置
*

View File

@ -1,6 +1,7 @@
package com.yskj.acdr.master.pet.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -162,6 +163,7 @@ public class PetController {
public GlobalResponse<PetInfo> index() {
List<PetInfo> one = petInfoService.list(new LambdaQueryWrapper<PetInfo>()
.eq(PetInfo::getUserId, StpUtil.getLoginIdAsLong()));
if (one == null || one.isEmpty()) return GlobalResponse.success();
return GlobalResponse.success(one.getFirst());
}
}