work-order/work-order-uniapp/pages/task/detail.vue
2025-07-27 20:34:15 +08:00

624 lines
16 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="app-container">
<HeaderBar title="任务详情" enable-back text-align="center" />
<view class="task-detail">
<!-- 基本信息卡片 -->
<view class="info-card">
<view class="card-header">
<view class="order-no">
任务编号{{ detail.no }}
<text class="tag type-tag">{{ TaskType.parseName(detail.type) }}</text>
<text :class="['tag', 'status-tag', getStatusClass(detail.status)]">
{{ TaskStatus.parseNameForUser(detail.status) }}
</text>
</view>
</view>
<view class="info-list">
<template v-if="showMore">
<template v-if="detail.carId">
<view class="info-item">
<text class="label">联系人</text>
<view class="value flex-row">
<view>
{{ detail.createName | dv }}
</view>
<view class="edit-btn" v-if="mode === 'user' && TaskStatus.canUpdate().includes(detail.status)" @click="handleMore">
<u-icon name="edit-pen" size="18" color="#2979ff"></u-icon>
修改信息
</view>
</view>
</view>
<view class="info-item">
<text class="label">联系电话</text>
<text class="value phone-number" @click="handleCall">{{ detail.createMobile | dv }}</text>
</view>
<view class="info-item">
<text class="label">车牌号</text>
<view class="value address-wrap">
<text>{{ detail.carNo | dv}}</text>
</view>
</view>
</template>
<template v-else-if="mode === 'user' && TaskStatus.canUpdate().includes(detail.status)">
<view class="add-placeholder" @click="handleMore">
<u-icon name="plus" size="24" color="#2979ff"></u-icon>
<text class="add-text">请选择您的爱车</text>
</view>
</template>
<view class="info-item">
<text class="label">下单时间</text>
<text class="value">{{ detail.createTime | dv }}</text>
</view>
<view class="info-item">
<text class="label">地区</text>
<view class="value address-wrap">
<text>{{ detail.regionMergerName | dv}}</text>
</view>
</view>
<view class="info-item">
<text class="label">详细地址</text>
<view class="value address-wrap">
<text>{{ detail.createAddress | dv}}</text>
</view>
</view>
</template>
<view class="info-item">
<text class="label">备注</text>
<text class="value">{{ detail.remark | dv }}</text>
</view>
<template v-if="detail.mchId">
<view class="info-item">
<text class="label">商家</text>
<text class="value">{{ detail.mchName | dv }}</text>
</view>
<view class="info-item">
<text class="label">接单时间</text>
<text class="value">{{ detail.receiveTime | dv }}</text>
</view>
</template>
<view class="info-item" v-if="detail.status === TaskStatus.CANCEL">
<text class="label">取消原因</text>
<text class="value">{{ detail.cancelRemark | dv }}</text>
</view>
</view>
</view>
<!-- 视频播放 -->
<view class="video-card" v-if="detail.video && isVideo(detail.video) && (showMore || isVip2)">
<view class="card-header">视频信息</view>
<video
:src="parseLocalUrl(detail.video)"
class="video-player"
controls
/>
</view>
<!-- 图片 -->
<view class="video-card" v-if="detail.video && isImage(detail.video) && (showMore || isVip2)">
<view class="card-header">图片信息</view>
<image-upload
:value="detail.video"
disabled
:limit="1"
:deletable="false"
/>
</view>
<!-- 位置信息 -->
<view class="map-card" v-if="detail.createLat && detail.createLon && showMore">
<view class="card-header">
<text class="card-title">位置信息</text>
<button class="nav-btn" @click="openLocation">
<text class="nav-text">导航</text>
</button>
</view>
<map
class="map"
:scale="16"
:show-location="true"
:latitude="Number(detail.createLat)"
:longitude="Number(detail.createLon)"
:markers="markers"
/>
</view>
<template v-if="mode === 'mch'">
<view class="info-item price-section">
<view class="price-content">
<view class="price-row total">
<text class="price-label">合计金额</text>
<text class="price-value">¥ {{ detail.receiveAmount | fix2 | dv }}</text>
</view>
<!-- <view class="price-row">
<text class="price-label">基础价格</text>
<text class="price-value">¥ {{ detail.basePrice | fix2 | dv }}</text>
</view>
<view class="price-row" v-if="detail.userLevelName">
<text class="price-label">{{ detail.userLevelName | dv }}</text>
<text class="price-value">¥ {{ detail.userLevelExtraPrice | fix2 | dv }}</text>
</view>
<view class="price-row" v-if="detail.shareLevelName">
<text class="price-label">{{ detail.shareLevelName | dv }}</text>
<text class="price-value">¥ {{ detail.shareLevelExtraPrice | fix2 | dv }}</text>
</view> -->
</view>
</view>
</template>
</view>
<!-- 底部固定按钮 -->
<view class="bottom-actions">
<template v-if="mode === 'mch'">
<button class="action-btn" v-if="TaskStatus.canReceive().includes(detail.status)" @click="handleReceive">¥{{ detail.receiveAmount | fix2 | dv}} 立即接单</button>
<button class="action-btn" v-if="TaskStatus.canComplete().includes(detail.status)" @click="handleComplete">完成任务</button>
</template>
<template v-else>
<button v-if="detail.mchId" class="action-btn plain" @click="handleContact">联系客服</button>
<button class="action-btn plain" v-if="TaskStatus.canCancel().includes(detail.status)" @click="handleCancel">取消任务</button>
<button class="action-btn" v-if="TaskStatus.canAfterSale().includes(detail.status)" @click="handleAfterSale">申请售后</button>
</template>
</view>
</view>
</template>
<script>
import HeaderBar from '@/components/HeaderBar.vue'
import { appGetTaskDetail, appAfterSaleTask, appCancelTask } from '@/api/app/task'
import { TaskType, TaskStatus } from '@/utils/enums'
import { parseLocalUrl, isImage, isVideo } from '@/utils/index'
import { mchReceiveTask, mchCompleteTask } from '@/api/mch/task'
import { getMchDetail } from '@/api/app/mch'
import { mapGetters } from 'vuex'
export default {
components: {
HeaderBar
},
data() {
return {
id: null,
detail: {},
TaskType,
TaskStatus,
markers: [],
mode: 'user',
mch: {}
}
},
computed: {
...mapGetters(['mchId', 'userId']),
showMore() {
return this.mode === 'user' || this.detail.mchId === this.mchId;
},
isVip2() {
return this.mch.vipLevel === '2'
}
},
onLoad(options) {
this.id = options.id;
if (options.mode) {
this.mode = options.mode;
}
},
onShow() {
this.getDetail()
this.getMchInfo()
},
methods: {
getMchInfo() {
getMchDetail(this.mchId).then(res => {
this.mch = res.data
})
},
isImage,
isVideo,
handleMore() {
uni.navigateTo({ url: `/pages/car/index?taskId=${this.id}` })
},
parseLocalUrl,
getDetail() {
appGetTaskDetail(this.id).then(res => {
this.detail = res.data
// 设置地图标记点
if (this.detail.createLat && this.detail.createLon) {
this.markers = [{
id: 1,
latitude: Number(this.detail.createLat),
longitude: Number(this.detail.createLon),
width: 24,
height: 32,
callout: {
content: this.detail.createAddress || '任务位置',
color: '#333333',
fontSize: 14,
borderRadius: 4,
padding: 8,
display: 'ALWAYS'
}
}]
}
})
},
openLocation() {
if (this.detail.createLat && this.detail.createLon) {
uni.openLocation({
latitude: Number(this.detail.createLat),
longitude: Number(this.detail.createLon),
name: '任务地点',
address: this.detail.createAddress,
success: () => {
console.log('导航打开成功')
},
fail: (err) => {
console.error('导航打开失败', err)
uni.showToast({
title: '导航打开失败',
icon: 'none'
})
}
})
}
},
getStatusClass(status) {
switch (status) {
case TaskStatus.WAIT_RECEIVE:
return 'status-waiting'
case TaskStatus.RECEIVED:
return 'status-received'
case TaskStatus.FINISHED:
return 'status-finished'
case TaskStatus.CANCEL:
return 'status-cancel'
case TaskStatus.AFTER_SALE:
return 'status-after-sale'
default:
return ''
}
},
handleReceive() {
uni.showModal({
title: '提示',
content: '确定要接单吗?',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const params = { id: this.id, mchId: this.$store.getters.mchId };
mchReceiveTask(params).then(res => {
if (res.code === 200) {
uni.showToast({ title: '接单成功', icon: 'success' });
this.getDetail(); // 刷新详情
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
});
}
}
});
},
handleComplete() {
uni.showModal({
title: '提示',
content: '确定要完成任务吗?',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const params = { id: this.id };
mchCompleteTask(params).then(res => {
if (res.code === 200) {
uni.showToast({ title: '完成成功', icon: 'success' });
this.getDetail(); // 刷新详情
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
});
}
}
});
},
handleCancel() {
uni.showModal({
title: '提示',
content: '确定要取消任务吗?',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const params = { id: this.id };
appCancelTask(params).then(res => {
if (res.code === 200) {
uni.showToast({ title: '取消成功', icon: 'success' });
this.getDetail(); // 刷新详情
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
});
}
}
})
},
handleContact() {
uni.navigateTo({ url: `/pages/mchCustom/index?mchId=${this.detail.mchId}&mode=user` })
},
handleAfterSale() {
uni.showModal({
title: '提示',
content: '确定要申请售后吗?',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const params = { id: this.id };
appAfterSaleTask(params).then(res => {
if (res.code === 200) {
uni.showToast({ title: '申请售后成功', icon: 'success' });
this.getDetail(); // 刷新详情
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
});
}
}
})
},
handleCall() {
if (this.detail.createMobile) {
uni.makePhoneCall({
phoneNumber: this.detail.createMobile,
success: () => {
console.log('拨打电话成功');
},
fail: (err) => {
console.error('拨打电话失败', err);
}
});
}
}
}
}
</script>
<style lang="scss" scoped>
.task-detail {
padding: 20rpx;
padding-bottom: 120rpx;
}
.info-card, .video-card, .map-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f8f9fc;
}
.card-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.nav-btn {
min-width: 120rpx;
height: 52rpx;
line-height: 52rpx;
background: #2979ff;
border-radius: 26rpx;
padding: 0 24rpx;
border: none;
margin: 0;
&::after {
border: none;
}
.nav-text {
color: #fff;
font-size: 24rpx;
}
}
.tag {
padding: 4rpx 16rpx;
border-radius: 24rpx;
font-size: 24rpx;
}
.type-tag {
background-color: #e6f4ff;
color: #1890ff;
}
.status-tag {
&.status-waiting {
color: #E6A23C;
background-color: #fdf6ec;
}
&.status-received {
color: #409EFF;
background-color: #ecf5ff;
}
&.status-finished {
color: #67C23A;
background-color: #f0f9eb;
}
&.status-cancel {
color: #909399;
background-color: #f4f4f5;
}
&.status-after-sale {
color: #F56C6C;
background-color: #fef0f0;
}
}
.price-section {
flex-direction: column;
background: #fff;
border-radius: 8rpx;
padding: 24rpx;
margin: 20rpx 0;
.price-content {
margin-top: 16rpx;
}
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
font-size: 28rpx;
&:last-child {
margin-bottom: 0;
}
&.total {
margin-bottom: 16rpx;
.price-label {
font-weight: 500;
color: #333;
}
.price-value {
font-size: 36rpx;
font-weight: 600;
color: #2979ff;
}
}
.price-label {
color: #666;
}
.price-value {
color: #333;
}
}
}
.info-list {
.info-item {
display: flex;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.label {
width: 140rpx;
color: #666;
font-size: 28rpx;
}
.value {
flex: 1;
color: #333;
font-size: 28rpx;
&.phone-number {
color: #007aff;
text-decoration: underline;
cursor: pointer;
}
}
.flex-row {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
.edit-btn {
display: flex;
align-items: center;
gap: 4rpx;
color: #2979ff;
}
}
.address-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
}
}
.add-placeholder {
width: 100%;
background: #ffffff71;
border: 2rpx dashed #2979ff;
border-radius: 10rpx;
padding: 40rpx;
text-align: center;
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
min-height: 200rpx;
box-sizing: border-box;
margin: 20rpx 0;
}
.video-player {
width: 100%;
height: 400rpx;
background: #000;
}
.map {
width: 100%;
height: 500rpx;
border-radius: 12rpx;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
display: flex;
justify-content: flex-end;
gap: 24rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 100;
}
.action-btn {
background: #007aff;
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
padding: 0 40rpx;
height: 64rpx;
line-height: 64rpx;
border: none;
width: fit-content;
margin: 0;
}
</style>