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