refactor: 优化订单详情和宠物信息展示逻辑 fix: 修复订单状态判断和按钮显示条件 docs: 添加数据库事务查询SQL文件 style: 统一时间线组件样式和布局 test: 更新测试用例以适应新功能master
@ -0,0 +1,143 @@ | |||
-- MySQL 数据库事务状态检查和处理 SQL 语句 | |||
-- 1. 查看当前所有连接和进程 | |||
SHOW PROCESSLIST; | |||
-- 2. 查看当前正在运行的事务 | |||
SELECT | |||
trx_id, | |||
trx_state, | |||
trx_started, | |||
trx_requested_lock_id, | |||
trx_wait_started, | |||
trx_weight, | |||
trx_mysql_thread_id, | |||
trx_query | |||
FROM INFORMATION_SCHEMA.INNODB_TRX; | |||
-- 3. 查看锁等待情况 | |||
SELECT | |||
waiting_trx_id, | |||
waiting_thread, | |||
waiting_query, | |||
blocking_trx_id, | |||
blocking_thread, | |||
blocking_query | |||
FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; | |||
-- 4. 查看当前锁定状态 | |||
SELECT | |||
lock_id, | |||
lock_trx_id, | |||
lock_mode, | |||
lock_type, | |||
lock_table, | |||
lock_index, | |||
lock_space, | |||
lock_page, | |||
lock_rec, | |||
lock_data | |||
FROM INFORMATION_SCHEMA.INNODB_LOCKS; | |||
-- 5. 查看详细的锁等待信息(MySQL 8.0+) | |||
SELECT | |||
OBJECT_SCHEMA, | |||
OBJECT_NAME, | |||
LOCK_TYPE, | |||
LOCK_MODE, | |||
LOCK_STATUS, | |||
LOCK_DATA, | |||
ENGINE_TRANSACTION_ID, | |||
THREAD_ID, | |||
EVENT_ID | |||
FROM performance_schema.data_locks; | |||
-- 6. 查看锁等待关系(MySQL 8.0+) | |||
SELECT | |||
requesting_engine_transaction_id, | |||
requesting_thread_id, | |||
requesting_event_id, | |||
blocking_engine_transaction_id, | |||
blocking_thread_id, | |||
blocking_event_id | |||
FROM performance_schema.data_lock_waits; | |||
-- 7. 查看长时间运行的事务(超过30秒) | |||
SELECT | |||
trx_id, | |||
trx_state, | |||
trx_started, | |||
TIMESTAMPDIFF(SECOND, trx_started, NOW()) as duration_seconds, | |||
trx_mysql_thread_id, | |||
trx_query | |||
FROM INFORMATION_SCHEMA.INNODB_TRX | |||
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30; | |||
-- 8. 查看阻塞其他事务的连接 | |||
SELECT | |||
p1.ID as blocking_thread_id, | |||
p1.USER as blocking_user, | |||
p1.HOST as blocking_host, | |||
p1.DB as blocking_db, | |||
p1.COMMAND as blocking_command, | |||
p1.TIME as blocking_time, | |||
p1.STATE as blocking_state, | |||
p1.INFO as blocking_query, | |||
p2.ID as blocked_thread_id, | |||
p2.USER as blocked_user, | |||
p2.HOST as blocked_host, | |||
p2.DB as blocked_db, | |||
p2.COMMAND as blocked_command, | |||
p2.TIME as blocked_time, | |||
p2.STATE as blocked_state, | |||
p2.INFO as blocked_query | |||
FROM INFORMATION_SCHEMA.PROCESSLIST p1 | |||
JOIN INFORMATION_SCHEMA.INNODB_LOCK_WAITS w ON p1.ID = w.blocking_thread | |||
JOIN INFORMATION_SCHEMA.PROCESSLIST p2 ON p2.ID = w.waiting_thread; | |||
-- ===== 处理锁定事务的操作 ===== | |||
-- 9. 杀死指定的连接(替换 CONNECTION_ID 为实际的连接ID) | |||
-- KILL CONNECTION_ID; | |||
-- 示例:杀死连接ID为123的连接 | |||
-- KILL 123; | |||
-- 10. 强制杀死指定连接(即使在事务中) | |||
-- KILL CONNECTION CONNECTION_ID; | |||
-- 11. 回滚指定事务(需要在对应连接中执行) | |||
-- ROLLBACK; | |||
-- 12. 设置锁等待超时(秒) | |||
SET innodb_lock_wait_timeout = 50; | |||
-- 13. 查看当前锁等待超时设置 | |||
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; | |||
-- ===== 预防措施 ===== | |||
-- 14. 查看死锁信息 | |||
SHOW ENGINE INNODB STATUS; | |||
-- 15. 开启死锁检测 | |||
SET GLOBAL innodb_deadlock_detect = ON; | |||
-- 16. 查看事务隔离级别 | |||
SELECT @@tx_isolation; | |||
-- 17. 设置事务隔离级别(如果需要) | |||
-- SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; | |||
-- ===== 使用步骤说明 ===== | |||
/* | |||
1. 首先运行查询1-4来了解当前事务状态 | |||
2. 找出长时间运行或阻塞的事务ID和连接ID | |||
3. 使用KILL命令终止问题连接 | |||
4. 确认锁定已解除 | |||
注意: | |||
- 在生产环境中谨慎使用KILL命令 | |||
- 优先尝试让应用程序正常提交或回滚事务 | |||
- 如果必须强制终止,确保了解可能的数据影响 | |||
*/ |
@ -0,0 +1,551 @@ | |||
<template> | |||
<!-- <view>打卡</view> --> | |||
<view class="box box-size"> | |||
<view class="top box-size" :style="{borderRadius:'16rpx'}"> | |||
<view class="form-title"> | |||
个人准备 | |||
</view> | |||
<view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">手套照片</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(至少1张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10"> | |||
<up-upload :fileList="fileList.glove" @afterRead="afterRead" @delete="deletePic" name="glove" | |||
multiple :maxCount="2" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> | |||
<view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">鞋套照片</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(至少1张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10"> | |||
<up-upload :fileList="fileList.ShoeCover" @afterRead="afterRead" @delete="deletePic" | |||
name="ShoeCover" multiple :maxCount="2" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> | |||
</view> | |||
<view class="top mt24 box-size" :style="{borderRadius:'16rpx'}"> | |||
<view class="form-title"> | |||
宠物状态记录 | |||
</view> | |||
<view v-for="(pet, index) in petList" :key="index"> | |||
<view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">{{ pet.title }}照片</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(至少2张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10 mr20"> | |||
<up-upload :fileList="fileList['pet' + index]" @afterRead="afterRead" @delete="deletePic" | |||
:name="'pet' + index" multiple :maxCount="2" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" | |||
:style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
<view class="top mt24 box-size" :style="{borderRadius:'16rpx'}"> | |||
<view class="form-title"> | |||
基础服务记录 | |||
</view> | |||
<view class="mt32 ml10" :style="{fontSize:'30rpx',fontWeight:'400'}">粮碗前后对比 </view> | |||
<view class="level"> | |||
<view class="mt20 ml10"> | |||
<up-upload :fileList="fileList.foodA" @afterRead="afterRead" @delete="deletePic" name="foodA" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">前</view> | |||
</view> | |||
<view class="mt20 ml10 ml28"> | |||
<up-upload :fileList="fileList.foodB" @afterRead="afterRead" @delete="deletePic" name="foodB" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">后</view> | |||
</view> | |||
</view> | |||
<view class="mt32 ml10" :style="{fontSize:'30rpx',fontWeight:'400'}">水碗前后对比 </view> | |||
<view class="level"> | |||
<view class="mt20 ml10"> | |||
<up-upload :fileList="fileList.waterA" @afterRead="afterRead" @delete="deletePic" name="waterA" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">前</view> | |||
</view> | |||
<view class="mt20 ml10 ml28"> | |||
<up-upload :fileList="fileList.waterB" @afterRead="afterRead" @delete="deletePic" name="waterB" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">后</view> | |||
</view> | |||
</view> | |||
<view class="mt32 ml10" :style="{fontSize:'30rpx',fontWeight:'400'}">猫砂盆、尿垫前后对比 </view> | |||
<view class="level"> | |||
<view class="mt20 ml10"> | |||
<up-upload :fileList="fileList.urinalA" @afterRead="afterRead" @delete="deletePic" name="urinalA" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">前</view> | |||
</view> | |||
<view class="mt20 ml10 ml28"> | |||
<up-upload :fileList="fileList.urinalB" @afterRead="afterRead" @delete="deletePic" name="urinalB" | |||
multiple :maxCount="1" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
<view class="jus-center level" :style="{color:'#999999',fontSize:'22rpx',width:'131rpx'}">后</view> | |||
</view> | |||
</view> | |||
</view> | |||
<view class="top mt24 box-size" :style="{borderRadius:'16rpx'}"> | |||
<view class="form-title"> | |||
定制服务记录 | |||
</view> | |||
<view v-for="(product, pindex) in projectList" | |||
:key="pindex"> | |||
<view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">{{ product.title }}</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(2-3张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10 mr20"> | |||
<up-upload :fileList="fileList['project' + pindex]" @afterRead="afterRead" @delete="deletePic" | |||
:name="'project' + pindex" | |||
multiple :maxCount="3" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- <view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">遛狗</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(2-3张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10 mr20"> | |||
<up-upload :fileList="fileList.testa" @afterRead="afterRead" @delete="deletePic" name="testa" | |||
multiple :maxCount="3" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> --> | |||
<!-- <view class="mt32 ml10"> | |||
<span :style="{fontSize:'30rpx',fontWeight:'400'}">陪玩</span> | |||
<span :style="{fontSize:'26rpx',color:'#C7C7C7'}">(2-3张)</span> | |||
</view> | |||
<view class="level"> | |||
<view class="mt20 ml10 mr20"> | |||
<up-upload :fileList="fileList.testb" @afterRead="afterRead" @delete="deletePic" name="testb" | |||
multiple :maxCount="3" width="131rpx" height="131rpx" :disabled="isRead"> | |||
<image src="/static/images/ydd/add_photo.png" mode="" :style="{width:'131rpx',height:'131rpx'}"> | |||
</image> | |||
</up-upload> | |||
</view> | |||
</view> --> | |||
</view> | |||
<view class="top mt24 box-size" :style="{borderRadius:'16rpx'}"> | |||
<view class="form-title"> | |||
其他补充信息(必填) | |||
</view> | |||
<view class="mt32 ml10" :style="{color:'#999999',fontSize:'30rpx'}"> | |||
可记录一下今日趣事、宠物状况、提醒事项等,最少30个字 | |||
</view> | |||
<view class="mt24"> | |||
<textarea cols="30" rows="10" | |||
placeholder="请输入内容" | |||
v-model="form.notes" | |||
:style="{color:'#999999',fontSize:'30rpx',backgroundColor:'#F5F5F5',borderRadius:'16rpx',width:'681rpx',height:'180rpx'}" | |||
class="pd20 box-size" :disabled="isRead"></textarea> | |||
</view> | |||
</view> | |||
<view class="buttom mt60 box-size" | |||
v-if="!isRead" | |||
style="display: flex;gap: 20rpx;" | |||
> | |||
<view class="buttom-item buttom-item-2 level size-30" | |||
@click="saveDraft(false)" | |||
:style="{borderRadius:'41rpx',color:'#ff842c'}"> | |||
保存草稿 | |||
</view> | |||
<view class="buttom-item level size-30" | |||
@click="submit" | |||
:style="{borderRadius:'41rpx',color:'#fff'}"> | |||
确定提交 | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script setup> | |||
import { | |||
onMounted, | |||
reactive, | |||
ref, | |||
} from "vue"; | |||
import { | |||
onLoad | |||
} from '@dcloudio/uni-app' | |||
import { | |||
ossUpload | |||
} from '@/utils/oss-upload/oss/index.js' | |||
import { | |||
appletOrderDateFrequencyById, | |||
appletOrderDateFrequencyCheck, | |||
} from "@/api/order/frequency.js" | |||
onLoad((options) => { | |||
checkId.value = options.id || null; | |||
GetByOrderId() | |||
}); | |||
const checkId = ref(0) | |||
const orderId = ref(0) | |||
const itemOrderID = ref(0) | |||
const serviceId = ref(0) | |||
const isRead = ref(false) | |||
const content = ref('') | |||
const fileList = reactive({ | |||
// glove: [],//手套 | |||
// ShoeCover: [],//鞋套 | |||
// food: [],//粮碗 | |||
// water: [],//水碗 | |||
// urinal: [],//猫砂盆、尿盆 | |||
}) | |||
const form = ref({}) | |||
const petList = ref([ | |||
// { | |||
// title: '小汪' | |||
// }, | |||
// { | |||
// title: '大黄' | |||
// }, | |||
]) | |||
//附加项目 | |||
const projectList = ref([ | |||
// { | |||
// title: '小汪' | |||
// }, | |||
// { | |||
// title: '大黄' | |||
// }, | |||
]) | |||
// 删除图片 | |||
const deletePic = (event) => { | |||
if (fileList[event.name]) { | |||
fileList[event.name].splice(event.index, 1); | |||
} | |||
}; | |||
// 新增图片 | |||
const afterRead = (event) => { | |||
if (!fileList[event.name]) { | |||
fileList[event.name] = [] | |||
} | |||
event.file.forEach(n => { | |||
ossUpload(n.url) | |||
.then(url => { | |||
console.log(url); | |||
fileList[event.name].push({ | |||
url | |||
}) | |||
}) | |||
}) | |||
}; | |||
function submit() { | |||
// 参数效验 | |||
if (!fileList.glove || fileList.glove.length == 0) { | |||
return msg('请上传鞋套照片') | |||
} | |||
if (!fileList.ShoeCover || fileList.ShoeCover.length == 0) { | |||
return msg('请上传鞋套照片') | |||
} | |||
for (let i = 0; i < petList.value.length; i++) { | |||
if (!fileList['pet' + i] || fileList['pet' + i].length < 2) { | |||
return msg(`请上传${petList.value[i].title}照片`) | |||
} | |||
} | |||
if (!fileList.foodA || fileList.foodA.length < 1 || | |||
!fileList.foodB || fileList.foodB.length < 1) { | |||
return msg('请上传粮碗前后照片') | |||
} | |||
if (!fileList.waterA || fileList.waterA.length < 1 || | |||
!fileList.waterB || fileList.waterB.length < 1) { | |||
return msg('请上传水碗前后照片') | |||
} | |||
if (!fileList.urinalA || fileList.urinalA.length < 1 || | |||
!fileList.urinalB || fileList.urinalB.length < 1) { | |||
return msg('请上传猫砂盆、尿盆照片') | |||
} | |||
for (let i = 0; i < projectList.value.length; i++) { | |||
if (!fileList['project' + i] || fileList['project' + i].length < 2) { | |||
return msg(`请上传${projectList.value[i].title}照片`) | |||
} | |||
} | |||
if(!form.value.notes){ | |||
return msg('请填写补充信息') | |||
} | |||
if(form.value.notes.length < 30){ | |||
return msg('补充信息不少于30字') | |||
} | |||
saveDraft(true) | |||
} | |||
function GetByOrderId(){ | |||
appletOrderDateFrequencyById(checkId.value) | |||
.then(res => { | |||
const data = res.data.check; | |||
const frequency = res.data.frequency; | |||
isRead.value = frequency.status == 2 | |||
if(res.code == 200 && data) { | |||
form.value = data | |||
// 回显手套照片 | |||
if(data.glovePhoto) { | |||
fileList.glove = data.glovePhoto.split(',').map(url => ({ url })); | |||
} | |||
// 回显鞋套照片 | |||
if(data.shoeCoverPhoto) { | |||
fileList.ShoeCover = data.shoeCoverPhoto.split(',').map(url => ({ url })); | |||
} | |||
// 回显宠物照片 | |||
if(data.petPhoto) { | |||
const pets = JSON.parse(data.petPhoto); | |||
petList.value = pets; | |||
pets.forEach((pet, index) => { | |||
if(pet.fileList) { | |||
fileList['pet' + index] = pet.fileList.split(',').map(url => ({ url })); | |||
} | |||
}); | |||
} | |||
// 回显项目照片 | |||
if(data.workDogImage) { | |||
const pList = JSON.parse(data.workDogImage); | |||
projectList.value = pList; | |||
pList.forEach((pet, index) => { | |||
if(pet.fileList) { | |||
fileList['project' + index] = pet.fileList.split(',').map(url => ({ url })); | |||
} | |||
}); | |||
} | |||
// 回显粮碗照片 | |||
if(data.grainBowlFront) fileList.foodA = data.grainBowlFront.split(',').map(url => ({ url })); | |||
if(data.grainBowlAfter) fileList.foodB = data.grainBowlAfter.split(',').map(url => ({ url })); | |||
// 回显水碗照片 | |||
if(data.waterBowlFront) fileList.waterA = data.waterBowlFront.split(',').map(url => ({ url })); | |||
if(data.waterBowlAfter) fileList.waterB = data.waterBowlAfter.split(',').map(url => ({ url })); | |||
// 回显猫砂盆/尿垫照片 | |||
if(data.basinFront) fileList.urinalA = data.basinFront.split(',').map(url => ({ url })); | |||
if(data.basinAfter) fileList.urinalB = data.basinAfter.split(',').map(url => ({ url })); | |||
// 回显定制服务照片 | |||
// if(data.workDogImage) fileList.testa = data.workDogImage.split(',').map(url => ({ url })); | |||
// if(data.workPalyImage) fileList.testb = data.workPalyImage.split(',').map(url => ({ url })); | |||
}else{ | |||
// getOrderPetByIdFN() | |||
let projectNameList = [] | |||
frequency.pets.forEach((n, i) => { | |||
fileList['pet' + i] = [] | |||
n.orderItemList.forEach((item, inde) => { | |||
fileList['project' + inde] = [] | |||
}) | |||
}) | |||
frequency.pets.forEach((n, i) => { | |||
petList.value.push({ | |||
title : n.name, | |||
id : n.id, | |||
}) | |||
n.orderItemList.forEach((item, inde) => { | |||
if(!projectNameList.includes(item.productName)){ | |||
projectNameList.push(item.productName) | |||
projectList.value.push({ | |||
title : item.productName, | |||
ids : [item.id] | |||
}) | |||
}else{ | |||
projectList.value[projectNameList.indexOf(item.productName)].ids.push(item.id) | |||
} | |||
}) | |||
}) | |||
} | |||
}) | |||
} | |||
function getOrderPetByIdFN(){ | |||
getOrderPetById(orderId.value) | |||
.then(res => { | |||
if(res.code == 200){ | |||
res.data.forEach((n, i) => { | |||
fileList['pet' + i] = [] | |||
}) | |||
res.data.forEach((n, i) => { | |||
petList.value.push({ | |||
title : n.name, | |||
id : n.id, | |||
}) | |||
}) | |||
} | |||
}) | |||
} | |||
function saveDraft(flag) { | |||
// 将所有图片数组转换为逗号分隔的字符串 | |||
const params = { | |||
// orderId: orderId.value, | |||
// itemOrderId : itemOrderID.value, | |||
itemDateId : checkId.value, | |||
glovePhoto: fileList.glove?.map(item => item.url).join(',') || '', | |||
shoeCoverPhoto: fileList.ShoeCover?.map(item => item.url).join(',') || '', | |||
// 合并所有宠物照片 | |||
petPhoto: JSON.stringify( | |||
petList.value.map((pet, index) => { | |||
return { | |||
id : pet.id, | |||
title : pet.title, | |||
fileList: fileList['pet' + index]?.map(item => item.url).join(',') || '' | |||
}; | |||
}) | |||
), | |||
workDogImage: JSON.stringify( | |||
projectList.value.map((pet, index) => { | |||
return { | |||
id : pet.id, | |||
title : pet.title, | |||
fileList: fileList['project' + index]?.map(item => item.url).join(',') || '' | |||
}; | |||
}) | |||
), | |||
// 粮碗照片 | |||
grainBowlFront: fileList.foodA?.map(item => item.url).join(',') || '', | |||
grainBowlAfter: fileList.foodB?.map(item => item.url).join(',') || '', | |||
// 水碗照片 | |||
waterBowlFront: fileList.waterA?.map(item => item.url).join(',') || '', | |||
waterBowlAfter: fileList.waterB?.map(item => item.url).join(',') || '', | |||
// 猫砂盆/尿垫照片 | |||
basinFront: fileList.urinalA?.map(item => item.url).join(',') || '', | |||
basinAfter: fileList.urinalB?.map(item => item.url).join(',') || '', | |||
// 定制服务照片 | |||
// workDogImage: fileList.testa?.map(item => item.url).join(',') || '', | |||
// workPalyImage: fileList.testb?.map(item => item.url).join(',') || '', | |||
// 备注信息 | |||
notes: form.value.notes || '', | |||
submitFlag : 1,//草稿 | |||
} | |||
if(form.value.id) { | |||
params.id = form.value.id | |||
} | |||
if(flag) { | |||
params.submitFlag = 2 | |||
} | |||
appletOrderDateFrequencyCheck(params) | |||
.then(res => { | |||
if(res.code === 200) { | |||
uni.showToast({ | |||
title: '提交成功', | |||
icon: 'success' | |||
}) | |||
// 如果是确认提交(非草稿),则返回上一页 | |||
setTimeout(() => { | |||
uni.navigateBack() | |||
}, 1500) | |||
} else { | |||
uni.showToast({ | |||
title: res.msg || '提交失败', | |||
icon: 'none' | |||
}) | |||
} | |||
}) | |||
.catch(err => { | |||
uni.showToast({ | |||
title: '提交失败', | |||
icon: 'none' | |||
}) | |||
}) | |||
} | |||
function msg(content) { | |||
uni.showToast({ | |||
title: content, | |||
icon: 'none' | |||
}) | |||
} | |||
</script> | |||
<style scoped lang="scss"> | |||
@import"index.scss" | |||
</style> |
@ -0,0 +1,579 @@ | |||
<template> | |||
<view class="timeline-container"> | |||
<!-- 日期和状态标签 --> | |||
<view class="date-header"> | |||
<view class="date-box"> | |||
<view class="date-box-color" :style="{'background-color': getTopBgColor()}"></view> | |||
<view class="date-month-day">{{ formatDate(date).month }}-{{ formatDate(date).day }}</view> | |||
</view> | |||
<!-- <view class="status-tag" :class="{'status-tag-pending': status}"> | |||
<image src="/static/images/ydd/icon1.png" | |||
mode="aspectFit" | |||
v-if="status" | |||
class="status-icon"></image> | |||
<image src="/static/images/order/success.png" | |||
mode="aspectFit" | |||
v-else | |||
class="status-icon"></image> | |||
{{ status ? '待上门' : '已完成' }} | |||
</view> --> | |||
</view> | |||
<!-- 空状态显示 --> | |||
<view v-if="!list || list.length === 0" class="empty-state"> | |||
<text class="empty-text">暂无订单数据</text> | |||
</view> | |||
<!-- 时间线主体 --> | |||
<view v-else class="timeline-body" v-for="(item, index) in list" :key="index"> | |||
<view class="timeline-line"></view> | |||
<!-- <view class="time-point"> | |||
<view class="time-icon"> | |||
<image src="/static/images/order/address.png" mode="aspectFit" class="time-image"></image> | |||
</view> | |||
<view class="time-text">{{ item.receiverCity }}</view> | |||
<view class="collapse-icon" @click="toggleServiceCard(index)"> | |||
{{ serviceCardCollapsed[index] ? '展开' : '收起' }} <text class="arrow" :class="{'arrow-up': !serviceCardCollapsed[index]}">▼</text> | |||
</view> | |||
</view> --> | |||
<!-- 服务内容卡片 --> | |||
<view v-if="!serviceCardCollapsed[index]" class="service-card"> | |||
<!-- 服务日期 --> | |||
<view class="service-section"> | |||
<view class="section-title"> | |||
<view class="title-indicator"></view> | |||
<text>服务日期</text> | |||
<text style="margin-left: auto;font-weight: 500;font-size: 24rpx;">订单编号:{{ item.orderId }}</text> | |||
</view> | |||
<view class="section-content date-content" :class="{bgSuccessQ : item.status == '2'}"> | |||
{{ dayjs(item.serviceDate).format('YYYY/MM/DD') }} | |||
</view> | |||
</view> | |||
<!-- 陪伴对象 --> | |||
<view class="service-section"> | |||
<view class="section-title"> | |||
<view class="title-indicator"></view> | |||
<text>陪伴对象</text> | |||
<view class="collapse-icon" @click="togglePetList(index)"> | |||
{{ petListCollapsed[index] ? '展开' : '收起' }} <text class="arrow" :class="{'arrow-up': !petListCollapsed[index]}">▼</text> | |||
</view> | |||
</view> | |||
<view class="section-content pet-list" :class="{bgSuccessQ : item.status == '2'}" v-if="!petListCollapsed[index]"> | |||
<view v-for="(pet, i) in item.pets" :key="i" class="pet-item"> | |||
<view class="pet-avatar"> | |||
<image :src="pet.photo" mode="aspectFill" class="avatar-image"></image> | |||
</view> | |||
<view class="pet-info"> | |||
<view class="pet-name"> | |||
{{ pet.name }} | |||
<text class="pet-gender" :class="{'pet-gender-male': pet.gender === 'male', 'pet-gender-female': pet.gender === 'female'}"> | |||
{{ pet.gender === 'male' ? '♂' : '♀' }} | |||
</text> | |||
</view> | |||
<view class="pet-description"> | |||
{{ pet.breed }}{{ pet.bodyType }} | {{ pet.orderItemList.map(n => n.productName).join(',') }} | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- 上门地址 --> | |||
<!-- <view class="service-section"> | |||
<view class="section-title"> | |||
<view class="title-indicator"></view> | |||
<text>上门地址</text> | |||
</view> | |||
<view class="section-content address-content" :class="{bgSuccessQ : item.status == '2'}"> | |||
{{ item.receiverDetailAddress }} | |||
</view> | |||
</view> --> | |||
<!-- 操作按钮 --> | |||
<view class="action-buttons"> | |||
<view class="btn btn-clock" | |||
v-if="item.status == '2'" | |||
:class="{bgSuccess : item.status == '2'}" | |||
@click="handleClock(item)">查看记录</view> | |||
<view class="btn btn-clock" | |||
v-else | |||
style="background-color: #aaa;color: #fff;" | |||
>暂无记录</view> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import dayjs from '@/utils/lib/dayjs.min.js'; | |||
export default { | |||
name: 'TimelineService', | |||
// 定义组件属性 | |||
props: { | |||
date: { | |||
type: String, | |||
default: '2024-12-08' | |||
}, | |||
orderCount: { | |||
type: Number, | |||
default: 2 | |||
}, | |||
status: { | |||
type: Boolean, | |||
default: true | |||
}, | |||
current: { | |||
type: Number, | |||
default: 0 | |||
}, | |||
list: { | |||
type: Array, | |||
default: () => [] | |||
}, | |||
}, | |||
data() { | |||
return { | |||
// 宠物列表折叠状态 - 使用数组来单独控制每个卡片中的宠物列表 | |||
petListCollapsed: [], | |||
// 服务卡片折叠状态 - 使用数组来单独控制每个卡片 | |||
serviceCardCollapsed: [] | |||
}; | |||
}, | |||
//计算属性 | |||
computed: { | |||
// 判断list中是否所有项的status都等于2 | |||
orderCompleted() { | |||
if (!this.list || this.list.length === 0) { | |||
return false; | |||
} | |||
return !this.list.every(item => item.status == '2'); | |||
} | |||
}, | |||
methods: { | |||
dayjs, | |||
// 切换宠物列表显示状态 | |||
togglePetList(index) { | |||
if (this.petListCollapsed[index] === undefined) { | |||
this.$set(this.petListCollapsed, index, true); | |||
} else { | |||
this.$set(this.petListCollapsed, index, !this.petListCollapsed[index]); | |||
} | |||
}, | |||
// 切换服务卡片显示状态 | |||
toggleServiceCard(index) { | |||
if (this.serviceCardCollapsed[index] === undefined) { | |||
this.$set(this.serviceCardCollapsed, index, true); | |||
} else { | |||
this.$set(this.serviceCardCollapsed, index, !this.serviceCardCollapsed[index]); | |||
} | |||
}, | |||
// 格式化日期 | |||
formatDate(dateString) { | |||
const date = new Date(dateString); | |||
return { | |||
day: date.getDate().toString().padStart(2, '0'), | |||
month: (date.getMonth() + 1).toString().padStart(2, '0') | |||
}; | |||
}, | |||
// 按钮事件处理函数 | |||
handleClock(item) { | |||
uni.navigateTo({ | |||
url: `/pages/personalCenter/orderDetailImage?id=${item.id}`, | |||
}); | |||
}, | |||
getTopBgColor() { | |||
return this.orderCompleted ? '#FFAA48' : '#4CD964'; | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.bgSuccess{ | |||
background-color: #4CD964 !important; | |||
} | |||
.bgSuccessQ{ | |||
background-color: #4CD96422 !important; | |||
} | |||
.timeline-container { | |||
position: relative; | |||
padding: 20rpx; | |||
margin-bottom: 30rpx; | |||
.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; | |||
} | |||
} | |||
.date-header { | |||
display: flex; | |||
align-items: center; | |||
margin-bottom: 20rpx; | |||
.date-box { | |||
width: 80rpx; | |||
background-color: #ffffff; | |||
border: 2px solid #333; | |||
border-radius: 0; | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: center; | |||
align-items: center; | |||
margin-right: 20rpx; | |||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05); | |||
border-radius: 14rpx; | |||
.date-box-color{ | |||
height: 20rpx; | |||
width: 100%; | |||
border-top-left-radius: 14rpx; | |||
border-top-right-radius: 14rpx; | |||
position: relative; | |||
&::before{ | |||
content: ''; | |||
display: block; | |||
background-color: #ddd; | |||
width: 100%; | |||
height: 26rpx; | |||
top: 100%; | |||
left: 0; | |||
position: absolute; | |||
} | |||
} | |||
.date-month-day { | |||
position: relative; | |||
font-size: 26rpx; | |||
font-weight: bold; | |||
color: #333; | |||
height: 50rpx; | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: center; | |||
} | |||
} | |||
.status-tag { | |||
background-color: #4CD96422; | |||
color: #4CD964; | |||
border: 4rpx solid #4CD964; | |||
padding: 16rpx 26rpx; | |||
border-radius: 14rpx; | |||
font-size: 26rpx; | |||
display: flex; | |||
align-items: center; | |||
position: relative; | |||
margin-left: 20rpx; | |||
.status-icon { | |||
width: 32rpx; | |||
height: 32rpx; | |||
margin-right: 8rpx; | |||
} | |||
&::after{ | |||
content: ''; | |||
display: block; | |||
position: absolute; | |||
width: 0; | |||
height: 0; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
left: -16rpx; | |||
border-top: 16rpx solid transparent; | |||
border-bottom: 16rpx solid transparent; | |||
border-right: 16rpx solid #4CD964; | |||
} | |||
&::before{ | |||
content: ''; | |||
display: block; | |||
position: absolute; | |||
width: 0; | |||
height: 0; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
left: -12rpx; | |||
border-top: 12rpx solid transparent; | |||
border-bottom: 12rpx solid transparent; | |||
border-right: 12rpx solid #4CD96422; | |||
z-index: 1; | |||
} | |||
} | |||
.status-tag-pending { | |||
background-color: #FFAA4822; | |||
color: #FFAA48; | |||
border-color: #FFAA48; | |||
&::after{ | |||
border-right-color: #FFAA48; | |||
} | |||
&::before{ | |||
border-right-color: #FFAA4822; | |||
} | |||
} | |||
} | |||
.timeline-body { | |||
position: relative; | |||
padding-left: 40rpx; | |||
padding-bottom: 40rpx; | |||
.timeline-line { | |||
position: absolute; | |||
left: 40rpx; | |||
top: 0; | |||
height: 100%; | |||
width: 0; | |||
border-left: 2rpx dashed #707070; | |||
border-left-style: dashed; | |||
border-image: repeating-linear-gradient(to bottom, #707070 0, #707070 8rpx, transparent 8rpx, transparent 20rpx) 1; | |||
z-index: 0; | |||
&::after{ | |||
content: ''; | |||
display: block; | |||
position: absolute; | |||
width: 8rpx; | |||
height: 8rpx; | |||
background-color: #000; | |||
border: 2rpx solid #707070; | |||
border-radius: 50%; | |||
left: -7rpx; | |||
top: 30rpx; | |||
} | |||
} | |||
.time-point { | |||
display: flex; | |||
align-items: center; | |||
margin-bottom: 20rpx; | |||
position: relative; | |||
z-index: 1; | |||
.time-icon { | |||
width: 60rpx; | |||
height: 60rpx; | |||
background-color: #fff; | |||
border-radius: 50%; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
margin-right: 20rpx; | |||
position: relative; | |||
left: 20rpx; | |||
.time-image { | |||
width: 40rpx; | |||
height: 40rpx; | |||
} | |||
} | |||
.time-text { | |||
font-size: 28rpx; | |||
color: #333; | |||
margin-left: 20rpx; | |||
flex: 1; | |||
} | |||
.collapse-icon { | |||
font-size: 24rpx; | |||
color: #999; | |||
padding: 0 20rpx; | |||
.arrow { | |||
transition: transform 0.3s; | |||
display: inline-block; | |||
} | |||
.arrow-up { | |||
transform: rotate(180deg); | |||
} | |||
} | |||
} | |||
.service-card { | |||
background-color: #fff; | |||
border-radius: 12rpx; | |||
padding: 30rpx; | |||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); | |||
margin-left: 20rpx; | |||
.service-section { | |||
margin-bottom: 30rpx; | |||
.section-title { | |||
display: flex; | |||
align-items: center; | |||
margin-bottom: 15rpx; | |||
.title-indicator { | |||
width: 6rpx; | |||
height: 30rpx; | |||
background-color: #FFAA48; | |||
margin-right: 15rpx; | |||
} | |||
text { | |||
font-size: 28rpx; | |||
color: #333; | |||
font-weight: bold; | |||
} | |||
.collapse-icon { | |||
margin-left: auto; | |||
font-size: 24rpx; | |||
color: #999; | |||
.arrow { | |||
transition: transform 0.3s; | |||
display: inline-block; | |||
} | |||
.arrow-up { | |||
transform: rotate(180deg); | |||
} | |||
} | |||
} | |||
.section-content { | |||
padding: 0 15rpx; | |||
background-color: #FFF9F0; | |||
} | |||
.date-content { | |||
background-color: #FFF9F0; | |||
padding: 20rpx; | |||
border-radius: 8rpx; | |||
font-size: 28rpx; | |||
color: #333; | |||
} | |||
.pet-list { | |||
padding: 15rpx; | |||
.pet-item { | |||
display: flex; | |||
margin-bottom: 20rpx; | |||
&:last-child { | |||
margin-bottom: 0; | |||
} | |||
.pet-avatar { | |||
width: 80rpx; | |||
height: 80rpx; | |||
border-radius: 50%; | |||
overflow: hidden; | |||
margin-right: 20rpx; | |||
.avatar-image { | |||
width: 100%; | |||
height: 100%; | |||
} | |||
} | |||
.pet-info { | |||
flex: 1; | |||
.pet-name { | |||
font-size: 28rpx; | |||
color: #333; | |||
margin-bottom: 8rpx; | |||
.pet-gender { | |||
display: inline-block; | |||
width: 32rpx; | |||
height: 32rpx; | |||
line-height: 32rpx; | |||
text-align: center; | |||
border-radius: 50%; | |||
color: #fff; | |||
font-size: 20rpx; | |||
margin-left: 10rpx; | |||
} | |||
.pet-gender-male { | |||
background-color: #4A90E2; | |||
} | |||
.pet-gender-female { | |||
background-color: #FF6B9A; | |||
} | |||
} | |||
.pet-description { | |||
font-size: 24rpx; | |||
color: #7D8196; | |||
} | |||
} | |||
} | |||
} | |||
.address-content { | |||
padding: 20rpx; | |||
border-radius: 8rpx; | |||
font-size: 28rpx; | |||
color: #7D8196; | |||
} | |||
} | |||
.action-buttons { | |||
display: flex; | |||
justify-content: space-between; | |||
gap: 20rpx; | |||
.btn { | |||
height: 80rpx; | |||
line-height: 80rpx; | |||
text-align: center; | |||
border-radius: 40rpx; | |||
font-size: 28rpx; | |||
flex: 1; | |||
} | |||
.btn-clock { | |||
background-color: #FFAA48; | |||
color: #fff; | |||
} | |||
.btn-pet-file, .btn-service-file { | |||
background-color: #F6F7FB; | |||
color: #333; | |||
border: 1px solid #E5E6EB; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,215 @@ | |||
<template> | |||
<view class="service-record-page"> | |||
<!-- 服务记录列表 --> | |||
<view class="record-content"> | |||
<view v-if="loading && recordList.length === 0" class="loading-state"> | |||
<view class="loading-spinner"></view> | |||
<text>正在加载服务记录...</text> | |||
</view> | |||
<view v-else-if="recordList.length === 0" class="empty-state"> | |||
<image src="/static/images/personal/no-data.png" mode="aspectFit" class="empty-image"></image> | |||
<text class="empty-text">暂无服务记录</text> | |||
</view> | |||
<view v-else> | |||
<timeline-service | |||
v-for="(record, index) in recordList" | |||
:key="index" | |||
:date="record.date" | |||
:list="record.list" | |||
:serviceBtn="false" | |||
/> | |||
</view> | |||
</view> | |||
<!-- 加载更多 --> | |||
<view class="loading-more" v-if="recordList.length > 0 && hasMore && loading"> | |||
<view class="loading-spinner"></view> | |||
<text>加载中...</text> | |||
</view> | |||
<!-- 没有更多数据 --> | |||
<view class="no-more" v-if="recordList.length > 0 && !hasMore"> | |||
<text>没有更多记录了</text> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import { appletOrderDateFrequencyList } from '@/api/order/order.js' | |||
import TimelineService from '../components/order/timelineService.vue' | |||
export default { | |||
components: { | |||
TimelineService | |||
}, | |||
data() { | |||
return { | |||
orderId: '', | |||
recordList: [], | |||
loading: false, | |||
hasMore: true, | |||
page: 1, | |||
size: 10 | |||
} | |||
}, | |||
onLoad(options) { | |||
if (options.orderId) { | |||
this.orderId = options.orderId; | |||
this.getServiceRecords(); | |||
} | |||
}, | |||
onReachBottom() { | |||
this.loadMore(); | |||
}, | |||
onPullDownRefresh() { | |||
this.refresh(); | |||
}, | |||
methods: { | |||
// 获取服务记录 | |||
getServiceRecords() { | |||
if (this.loading) return; | |||
this.loading = true; | |||
const params = { | |||
orderId: this.orderId, | |||
pageNumber: this.page, | |||
pageSize: this.size | |||
}; | |||
appletOrderDateFrequencyList(params).then(res => { | |||
if (res && res.data) { | |||
const newList = res.data; | |||
if (this.page === 1) { | |||
this.recordList = newList; | |||
} else { | |||
this.recordList = [...this.recordList, ...newList]; | |||
} | |||
this.hasMore = newList.length === this.size; | |||
} else { | |||
this.hasMore = false; | |||
} | |||
console.log(this.recordList); | |||
this.loading = false; | |||
}).catch(err => { | |||
console.error('获取服务记录失败:', err); | |||
this.loading = false; | |||
uni.showToast({ | |||
title: '获取服务记录失败', | |||
icon: 'none' | |||
}); | |||
}); | |||
}, | |||
// 加载更多 | |||
loadMore() { | |||
if (this.loading || !this.hasMore) return; | |||
this.page++; | |||
this.getServiceRecords(); | |||
}, | |||
// 下拉刷新 | |||
refresh() { | |||
this.page = 1; | |||
this.recordList = []; | |||
this.hasMore = true; | |||
this.getServiceRecords().then(() => { | |||
uni.stopPullDownRefresh(); | |||
}).catch(() => { | |||
uni.stopPullDownRefresh(); | |||
}); | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.service-record-page { | |||
background-color: #f5f5f5; | |||
min-height: 100vh; | |||
} | |||
.page-header { | |||
background: linear-gradient(135deg, #FFAA48 0%, #FF8A00 100%); | |||
padding: 40rpx 30rpx 30rpx; | |||
color: #FFFFFF; | |||
.header-title { | |||
font-size: 36rpx; | |||
font-weight: bold; | |||
display: block; | |||
margin-bottom: 10rpx; | |||
} | |||
.header-subtitle { | |||
font-size: 26rpx; | |||
opacity: 0.9; | |||
} | |||
} | |||
.record-content { | |||
padding: 20rpx; | |||
} | |||
.loading-state { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
justify-content: center; | |||
padding: 100rpx 40rpx; | |||
color: #999; | |||
font-size: 28rpx; | |||
} | |||
.empty-state { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
justify-content: center; | |||
padding: 100rpx 40rpx; | |||
.empty-image { | |||
width: 200rpx; | |||
height: 200rpx; | |||
margin-bottom: 20rpx; | |||
} | |||
.empty-text { | |||
color: #999; | |||
font-size: 28rpx; | |||
} | |||
} | |||
.loading-more, | |||
.no-more { | |||
text-align: center; | |||
padding: 20rpx 0; | |||
color: #999; | |||
font-size: 24rpx; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
justify-content: center; | |||
} | |||
.loading-spinner { | |||
width: 40rpx; | |||
height: 40rpx; | |||
border: 4rpx solid #f3f3f3; | |||
border-top: 4rpx solid #FFAA48; | |||
border-radius: 50%; | |||
animation: spin 1s linear infinite; | |||
margin-bottom: 16rpx; | |||
} | |||
@keyframes spin { | |||
0% { | |||
transform: rotate(0deg); | |||
} | |||
100% { | |||
transform: rotate(360deg); | |||
} | |||
} | |||
</style> |