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