staff/pagesA/my_order/detail.vue

772 lines
17 KiB
Vue

<template>
<view class="detail-page">
<!-- 自定义导航栏 -->
<wd-navbar :bordered="false"
custom-style="background: transparent !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;"
safeAreaInsetTop fixed placeholder>
<template #left>
<view class="navbar-content">
<text v-if="hasMultiplePages" class="ri-arrow-left-s-line back-icon"
@click="toPages({type:'nav'})"></text>
<text v-if="!hasMultiplePages" class="ri-home-5-line home-icon"
@click="toPages({type:'home'})"></text>
<text class="page-title">工单详情</text>
</view>
</template>
</wd-navbar>
<!-- 导航栏背景 -->
<view class="nav-bg-layer"></view>
<!-- 工单状态卡片 -->
<view class="ticket-card">
<view class="ticket-header">
<view class="ticket-info">
<view class="ticket-id">
<text class="ri-file-list-line id-icon"></text>
<text class="id-text">{{ticketInfo.ticket_no}}</text>
<text class="ri-file-copy-line copy-icon"></text>
</view>
<view class="create-time">创建时间: {{ticketInfo.create_time}}</view>
</view>
<view class="ticket-type" :class="'type-'+ticketInfo.type">
<text v-if="ticketInfo.type == 'F2'">配送</text>
<text v-if="ticketInfo.type == 'F4'">量房</text>
<text v-if="ticketInfo.type == 'F6'">维修</text>
</view>
</view>
<view class="ticket-status">
<view class="status-icon" :class="'status-'+ticketInfo.status">
<text v-if="ticketInfo.status == 2 || ticketInfo.status == 1"
class="ri-checkbox-circle-fill"></text>
<text v-else-if="ticketInfo.status == 3" class="ri-close-circle-fill"></text>
<text v-else class="ri-time-line"></text>
</view>
<view class="status-text">
<text class="status-title">
{{ticketInfo.status == 0 ? '待接单' :
ticketInfo.status == 1 ? '已接单' :
ticketInfo.status == 2 ? '已完结' :
ticketInfo.status == 3 ? '已取消' : ''}}
</text>
<text class="status-desc">
{{ticketInfo.status == 0 ? '等待服务人员接单' :
ticketInfo.status == 1 ? '服务人员已接单' :
ticketInfo.status == 2 ? '工单已完成' :
ticketInfo.status == 3 ? '工单已取消' : ''}}
</text>
</view>
</view>
</view>
<!-- 联系信息 -->
<view class="info-card">
<view class="card-title">
<text class="ri-user-3-line title-icon"></text>
<text>联系信息</text>
</view>
<view class="info-content">
<view class="info-item">
<text class="item-label">小区名称</text>
<text class="item-value">{{ticketInfo?.order?.village_name}}</text>
</view>
<view class="info-item">
<text class="item-label">联系人</text>
<text class="item-value">{{ticketInfo?.order?.name}}</text>
</view>
<view class="info-item">
<text class="item-label">联系电话</text>
<view @click="btnPhone(ticketInfo?.order?.mobile)" class="phone-value">
<text class="ri-phone-line phone-icon"></text>
<text class="phone-number">{{ticketInfo?.order?.mobile}}</text>
</view>
</view>
<view v-if="ticketInfo?.type == 'F2' " class="info-item product-info">
<text class="item-label">商品信息</text>
<view class="product-list-full">
<!-- 商品列表 -->
<view v-for="(product, index) in ticketInfo?.product" :key="index" class="product-card-full">
<view class="product-main">
<image class="product-image" :src="product.picture" mode="aspectFill"></image>
<view class="product-details">
<text class="product-name">{{product.product_name}}</text>
<text class="product-spec">{{product.spec_name}}</text>
<view class="price-quantity">
<text class="price">¥{{product.price}}</text>
<text class="quantity">x{{product.num}}</text>
</view>
</view>
</view>
</view>
<!-- 商品总数和总价 -->
<view class="order-summary">
<text class="summary-count">共{{ticketInfo?.product.length}}件商品</text>
<text class="summary-price">合计: ¥{{getTotalPrice()}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 报修信息 -->
<view v-if="ticketInfo?.type == 'F6'" class="info-card">
<view class="card-title">
<text class="ri-tools-line title-icon"></text>
<text>报修信息</text>
</view>
<view class="info-content">
<view class="info-item">
<text class="item-label">报修位置</text>
<text
class="item-value">{{ticketInfo?.order?.region_name}}{{ticketInfo?.order?.cell_name}}{{ticketInfo?.order?.house_name}}{{ticketInfo?.order?.address}}</text>
</view>
<view class="info-item">
<text class="item-label">问题描述</text>
<text class="item-value">{{ticketInfo?.order?.content}}</text>
</view>
<view v-if="ticketInfo?.order?.images" class="info-item">
<text class="item-label">现场照片</text>
<view class="scene-images">
<image v-for="(item) in ticketInfo?.order?.images?.split(',')" :key="item" class="scene-image"
:src="item" mode="aspectFill">
</image>
</view>
</view>
</view>
</view>
<!-- 工单记录 -->
<view v-if="ticketInfo?.record?.length>0" class="info-card">
<view class="card-title">
<text class="ri-history-line title-icon"></text>
<text>工单记录</text>
</view>
<view class="timeline">
<view class="timeline-item" v-for="(item,index) in ticketInfo.record">
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-time">{{item.create_time}}</view>
<view class="timeline-text">{{item.remark}}</view>
</view>
</view>
<!-- <view v-if="ticketInfo.status >= 1" class="timeline-item">
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-time">2025-03-13 11:25:18</view>
<view class="timeline-text">工单已接单</view>
</view>
</view>
<view v-if="ticketInfo.status >= 2" class="timeline-item">
<view class="timeline-dot"></view>
<view class="timeline-content">
<view class="timeline-time">2025-03-13 15:42:36</view>
<view class="timeline-text">工单已完成</view>
</view>
</view> -->
</view>
</view>
<!-- 底部按钮 -->
<view class="footer-actions">
<view class="action-buttons">
<wd-button @click="revert" class="action-btn cancel-btn" type="error" :round="true">退单</wd-button>
<view class="li-ml-20">
<wd-button class="action-btn confirm-btn" :round="true">结单</wd-button>
</view>
</view>
</view>
<wd-message-box />
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useNavigation } from '@/hooks/useNavigation'
import { myTicketInfo, ticketRevert } from '@/api/ticket'
import { useToast, useMessage } from '@/uni_modules/wot-design-uni'
const Toast = useToast()
const Message = useMessage()
const loading = ref<boolean>(false)
const bntloading = ref<boolean>(false)
const ticketInfo = ref<any>({
ticket_no: '',
create_time: '',
status: undefined,
type: '',
order: {
name: '',
mobile: '',
region_name: '',
cell_name: '',
house_name: '',
address: '',
appoint_date: '',
appoint_time: '',
building_area: '',
status: 0,
images: '',
remark: '',
products: [] // 商品列表
}
})
// 计算商品总价
const getTotalPrice = () => {
let total = 0
ticketInfo.value?.product.forEach(product => {
total += product.price * product.num
})
return total.toFixed(2)
}
// 使用导航 composable
const {
hasMultiplePages, // 是否有多个页面在路由栈中
isTabBarPage, // 当前页面是否为 tabBar 页面
checkRouteStack // 检查当前路由栈状态的方法
} = useNavigation()
onLoad((q) => {
checkRouteStack()
if (q.ticket_id) {
loadInfo(q.ticket_id)
}
})
const loadInfo = async (ticket_id) => {
const res = await myTicketInfo({ ticket_id })
ticketInfo.value = res.data
}
// 状态文字配置
const getStatusText = (status) => {
const textMap = {
1: '待配送',
2: '已完成',
3: '未入库',
4: '已取消'
}
return textMap[status] || '未知状态'
}
const toPages = (item) => {
console.log(item);
switch (item.type) {
case 'nav':
uni.navigateBack({
delta: 1
});
break;
case 'home':
uni.switchTab({
url: '/pages/index/index'
})
break;
default:
break;
}
}
const handleAction = (action) => {
console.log(action);
}
const revert = () => {
Message.confirm({
msg: '提示',
title: '确定退单吗?'
})
.then(() => {
getTicketRevert()
})
}
const getTicketRevert = async () => {
uni.showLoading({
title: '退单中...'
})
const res = await ticketRevert({
ticket_id: ticketInfo.value.ticket_id
})
uni.hideLoading()
Toast.success('退单成功')
setTimeout(() => {
uni.navigateBack()
}, 400)
}
const btnPhone = (value : any) => {
uni.makePhoneCall({
phoneNumber: value
})
}
</script>
<style lang="scss" scoped>
.detail-page {
width: 100%;
min-height: 100vh;
background: linear-gradient(to bottom,
rgba(233, 245, 255, 0.9) 0%,
rgba(245, 250, 255, 0.9) 30%,
rgba(245, 250, 255, 0.9) 70%,
rgba(233, 245, 255, 0.9) 100%);
background-attachment: fixed;
padding-bottom: 180rpx;
}
.navbar-content {
display: flex;
align-items: center;
padding-left: 20rpx;
.back-icon,
.home-icon {
font-size: 60rpx;
color: #333;
margin-right: 10rpx;
}
.page-title {
font-size: 36rpx;
font-weight: 500;
color: #333;
}
}
/* 导航栏背景层 */
.nav-bg-layer {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--window-top);
background: linear-gradient(to bottom,
rgba(217, 237, 255, 0.95),
rgba(217, 237, 255, 0.85));
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 998;
}
/* 卡片通用样式 */
.ticket-card,
.info-card {
width: 92%;
margin: 0 auto;
margin-bottom: 30rpx;
border-radius: 20rpx;
background: #fff;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
&:active {
transform: scale(0.99);
transition: transform 0.2s;
}
}
/* 工单状态卡片 */
.ticket-card {
margin-top: 40rpx;
padding-bottom: 30rpx;
.ticket-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20rpx;
background-color: #f8faff;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.ticket-info {
.ticket-id {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.id-icon {
font-size: 32rpx;
color: #666;
margin-right: 8rpx;
}
.id-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.copy-icon {
font-size: 28rpx;
color: #009aff;
margin-left: 15rpx;
}
}
.create-time {
font-size: 26rpx;
color: #666;
}
}
.ticket-type {
padding: 6rpx 20rpx;
border-bottom-left-radius: 20rpx;
font-size: 26rpx;
font-weight: 500;
color: #37A5FF;
background-color: #e8f4ff;
&.type-F2 {
color: #ff9d00;
background-color: #fff6e9;
}
&.type-F6 {
color: #00b42a;
background-color: #e8ffea;
}
&.type-F4 {
color: #7e3ff2;
background-color: #f0e8ff;
}
}
}
.ticket-status {
display: flex;
align-items: center;
padding: 20rpx;
.status-icon {
margin-right: 20rpx;
font-size: 40rpx;
color: #009aff;
&.status-2 {
color: #00b42a;
}
&.status-3 {
color: #f42429;
}
&.status-0 {
color: #ff9d00;
}
}
.status-text {
display: flex;
flex-direction: column;
.status-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.status-desc {
font-size: 26rpx;
color: #999;
}
}
}
}
/* 详情卡片样式 */
.info-card {
padding: 0 0 20rpx 0;
.card-title {
display: flex;
align-items: center;
padding: 25rpx 30rpx;
font-size: 30rpx;
font-weight: 600;
color: #333;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.title-icon {
margin-right: 10rpx;
color: #009aff;
}
}
.info-content {
padding: 20rpx 30rpx;
.info-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25rpx;
.item-label {
width: 150rpx;
font-size: 26rpx;
color: #999;
}
.item-value {
width: 400rpx;
font-size: 26rpx;
color: #333;
text-align: right;
word-break: break-all;
}
.phone-value {
display: flex;
align-items: center;
justify-content: flex-end;
.phone-icon {
color: #009aff;
margin-right: 8rpx;
}
.phone-number {
font-size: 26rpx;
color: #009aff;
font-weight: 500;
}
}
&.product-info {
display: block;
margin-top: 30rpx;
.item-label {
display: block;
margin-bottom: 20rpx;
}
.product-list-full {
width: 100%;
background-color: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 15rpx;
padding: 10rpx 0;
// border:solid 1px red;
.product-card-full {
width: 95%;
margin: 20rpx auto;
&:last-child {
margin-bottom: 0;
}
.product-main {
display: flex;
align-items: center;
width: 100%;
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
border: 1px solid rgba(0, 0, 0, 0.05);
background-color: #fff;
}
.product-details {
flex: 1;
padding-left: 20rpx;
.product-name {
font-size: 28rpx;
color: #333;
line-height: 1.4;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-spec {
font-size: 24rpx;
color: #999;
}
.price-quantity {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15rpx;
.price {
font-size: 30rpx;
color: #FF6B00;
font-weight: 600;
}
.quantity {
font-size: 26rpx;
color: #666;
}
}
}
}
}
.order-summary {
display: flex;
justify-content: space-between;
align-items: center;
width: 95%;
margin: 20rpx auto 10rpx;
// border-top: 1px solid #f0f0f0;
border-top: solid 1px #f0f0f0;
// padding: 10rpx 0;
padding-top: 10rpx;
.summary-count {
font-size: 26rpx;
color: #666;
}
.summary-price {
font-size: 30rpx;
color: #FF6B00;
font-weight: 600;
}
}
}
}
.scene-images {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
width: 400rpx;
.scene-image {
width: 110rpx;
height: 110rpx;
border-radius: 10rpx;
margin-left: 20rpx;
margin-bottom: 10rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
}
}
}
}
/* 时间线样式 */
.timeline {
padding: 10rpx 30rpx 10rpx 40rpx;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 50rpx;
width: 2rpx;
height: 100%;
background-color: rgba(0, 154, 255, 0.2);
z-index: 1;
}
.timeline-item {
position: relative;
padding: 15rpx 0 15rpx 30rpx;
.timeline-dot {
position: absolute;
left: -12rpx;
top: 25rpx;
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background-color: #009aff;
border: 4rpx solid rgba(0, 154, 255, 0.3);
z-index: 2;
}
.timeline-content {
.timeline-time {
font-size: 24rpx;
color: #999;
margin-bottom: 6rpx;
}
.timeline-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
}
/* 底部按钮样式 */
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx 30rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
box-shadow: 0 -4rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 99;
.action-buttons {
display: flex;
justify-content: flex-end;
.action-btn {
min-width: 180rpx !important;
height: 80rpx !important;
border-radius: 40rpx !important;
font-size: 28rpx !important;
box-shadow: 0 6rpx 10rpx rgba(0, 0, 0, 0.1);
&.cancel-btn {
background: linear-gradient(to right, #ff6b6b, #ff4757) !important;
}
&.confirm-btn {
background: linear-gradient(to right, #5eafff, #4090ff) !important;
}
}
}
}
// ::v-deep .wd-button {
// width: auto !important;
// height: 80rpx !important;
// min-width: 180rpx !important;
// margin-right: 20rpx !important;
// }
::v-deep .wd-message-box__flex {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>