|
|
@@ -17,209 +17,270 @@
|
|
|
<!-- 订单号与状态 -->
|
|
|
<view class="order-header">
|
|
|
<view class="order-id-row">
|
|
|
- <text class="order-id">{{ order.code || order.id }}</text>
|
|
|
- <text :class="['status-badge', `badge-${order.statusKey}`]">{{ order.statusText }}</text>
|
|
|
- <text class="service-badge">{{ currentServiceName }}</text>
|
|
|
+ <text class="order-id">{{ order.code || order.id }}</text>
|
|
|
+ <text :class="['status-badge', `badge-${order.statusKey}`]">{{ order.statusText }}</text>
|
|
|
+ <text class="service-badge">{{ currentServiceName }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
- <!-- 状态进度条 -->
|
|
|
- <view class="progress-card">
|
|
|
- <view class="progress-steps">
|
|
|
- <view v-for="(step, i) in progressSteps" :key="i"
|
|
|
- :class="['step-item', { done: step.done, active: step.active }]">
|
|
|
- <view class="step-circle">
|
|
|
- <uni-icons v-if="step.done" type="checkmarkempty" size="12" color="#fff"></uni-icons>
|
|
|
- <text v-else class="step-num">{{ i + 1 }}</text>
|
|
|
+ <!-- 状态进度条 -->
|
|
|
+ <view class="progress-card">
|
|
|
+ <view class="progress-steps">
|
|
|
+ <view v-for="(step, i) in progressSteps" :key="i"
|
|
|
+ :class="['step-item', { done: step.done, active: step.active }]">
|
|
|
+ <view class="step-circle">
|
|
|
+ <uni-icons v-if="step.done" type="checkmarkempty" size="12" color="#fff"></uni-icons>
|
|
|
+ <text v-else class="step-num">{{ i + 1 }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="step-line" v-if="i < progressSteps.length - 1" :class="{ done: step.done }"></view>
|
|
|
+ <text class="step-label">{{ step.label }}</text>
|
|
|
+ <text class="step-time">{{ step.time }}</text>
|
|
|
</view>
|
|
|
- <view class="step-line" v-if="i < progressSteps.length - 1" :class="{ done: step.done }"></view>
|
|
|
- <text class="step-label">{{ step.label }}</text>
|
|
|
- <text class="step-time">{{ step.time }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
- <!-- 宠物档案 + 用户信息 -->
|
|
|
- <view class="info-row-cards">
|
|
|
- <view class="info-card pet-card">
|
|
|
- <text class="card-label">宠物档案</text>
|
|
|
- <view class="pet-header">
|
|
|
- <view class="pet-avatar">
|
|
|
- <image v-if="order.petAvatarUrl" :src="order.petAvatarUrl" mode="aspectFill" class="avatar-img"></image>
|
|
|
- <text v-else>{{ (order.petName || '宠')[0] }}</text>
|
|
|
- </view>
|
|
|
- <view class="pet-basic">
|
|
|
- <text class="pet-name">
|
|
|
- {{ order.petName || '-' }}
|
|
|
- <text class="gender-male" v-if="order.petGender === 'male'">♂</text>
|
|
|
- <text class="gender-female" v-else-if="order.petGender === 'female'">♀</text>
|
|
|
- </text>
|
|
|
- <view class="pet-tags">
|
|
|
- <text class="mini-tag" v-if="order.petAge">{{ order.petAge }}</text>
|
|
|
- <text class="mini-tag" v-if="order.petWeight">{{ order.petWeight }}</text>
|
|
|
- <text class="breed-badge" v-if="order.petBreed">{{ order.petBreed }}</text>
|
|
|
+ <!-- 宠物档案 + 用户信息 -->
|
|
|
+ <view class="info-row-cards">
|
|
|
+ <view class="info-card pet-card">
|
|
|
+ <text class="card-label">宠物档案</text>
|
|
|
+ <view class="pet-header">
|
|
|
+ <view class="pet-avatar">
|
|
|
+ <image v-if="order.petAvatarUrl" :src="order.petAvatarUrl" mode="aspectFill"
|
|
|
+ class="avatar-img"></image>
|
|
|
+ <text v-else>{{ (order.petName || '宠')[0] }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="pet-basic">
|
|
|
+ <text class="pet-name">
|
|
|
+ {{ order.petName || '-' }}
|
|
|
+ <text class="gender-male" v-if="order.petGender === 'male'">♂</text>
|
|
|
+ <text class="gender-female" v-else-if="order.petGender === 'female'">♀</text>
|
|
|
+ </text>
|
|
|
+ <view class="pet-tags">
|
|
|
+ <text class="mini-tag" v-if="order.petAge">{{ order.petAge }}</text>
|
|
|
+ <text class="mini-tag" v-if="order.petWeight">{{ order.petWeight }}</text>
|
|
|
+ <text class="breed-badge" v-if="order.petBreed">{{ order.petBreed }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
- <view class="pet-attrs">
|
|
|
- <view class="attr-item">
|
|
|
- <text class="attr-label">品种</text>
|
|
|
- <text class="attr-val">{{ order.petBreed || '-' }}</text>
|
|
|
- </view>
|
|
|
- <view class="attr-item">
|
|
|
- <text class="attr-label">疫苗状态</text>
|
|
|
- <text class="attr-val highlight">{{ order.petVaccine || '-' }}</text>
|
|
|
- </view>
|
|
|
- <view class="attr-item full">
|
|
|
- <text class="attr-label">性格特点</text>
|
|
|
- <text class="attr-val">{{ order.petCharacter || '-' }}</text>
|
|
|
+ <view class="pet-attrs">
|
|
|
+ <view class="attr-item">
|
|
|
+ <text class="attr-label">品种</text>
|
|
|
+ <text class="attr-val">{{ order.petBreed || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="attr-item">
|
|
|
+ <text class="attr-label">疫苗状态</text>
|
|
|
+ <text class="attr-val highlight">{{ order.petVaccine || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="attr-item full">
|
|
|
+ <text class="attr-label">性格特点</text>
|
|
|
+ <text class="attr-val">{{ order.petCharacter || '-' }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
- <view class="info-card user-card">
|
|
|
- <text class="card-label">用户信息</text>
|
|
|
- <view class="user-header">
|
|
|
- <view class="user-avatar">
|
|
|
- <image v-if="order.userAvatarUrl" :src="order.userAvatarUrl" mode="aspectFill" class="avatar-img"></image>
|
|
|
- <uni-icons v-else type="person" size="26" color="#aaa"></uni-icons>
|
|
|
+ <view class="info-card user-card">
|
|
|
+ <text class="card-label">用户信息</text>
|
|
|
+ <view class="user-header">
|
|
|
+ <view class="user-avatar">
|
|
|
+ <image v-if="order.userAvatarUrl" :src="order.userAvatarUrl" mode="aspectFill"
|
|
|
+ class="avatar-img"></image>
|
|
|
+ <uni-icons v-else type="person" size="26" color="#aaa"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <view class="user-basic">
|
|
|
+ <text class="user-name-text">{{ order.userName }}</text>
|
|
|
+ <text class="user-phone">{{ order.userPhone }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="user-basic">
|
|
|
- <text class="user-name-text">{{ order.userName }}</text>
|
|
|
- <text class="user-phone">{{ order.userPhone }}</text>
|
|
|
+ <view class="service-address-box">
|
|
|
+ <text class="addr-label">服务地址</text>
|
|
|
+ <text class="addr-text">{{ order.address }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view class="service-address-box">
|
|
|
- <text class="addr-label">服务地址</text>
|
|
|
- <text class="addr-text">{{ order.address }}</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 标签页 -->
|
|
|
- <view class="detail-tabs-wrap">
|
|
|
- <view class="tab-nav">
|
|
|
- <view v-for="tab in tabList" :key="tab.name"
|
|
|
- :class="['tab-nav-item', { active: activeTab === tab.name }]" @click="activeTab = tab.name">
|
|
|
- <text>{{ tab.title }}</text>
|
|
|
- </view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 任务详情扩展板块 -->
|
|
|
- <view class="tab-content" v-if="activeTab === 'base'">
|
|
|
- <view class="base-info-grid">
|
|
|
- <view class="bi-item" v-for="item in baseInfoList" :key="item.label">
|
|
|
- <text class="bi-label">{{ item.label }}</text>
|
|
|
- <text :class="['bi-val', item.highlight ? 'highlight' : '']">{{ item.value }}</text>
|
|
|
+ <!-- 标签页 -->
|
|
|
+ <view class="detail-tabs-wrap">
|
|
|
+ <view class="tab-nav">
|
|
|
+ <view v-for="tab in tabList" :key="tab.name"
|
|
|
+ :class="['tab-nav-item', { active: activeTab === tab.name }]" @click="activeTab = tab.name">
|
|
|
+ <text>{{ tab.title }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 接送任务详情 -->
|
|
|
- <block v-if="order.type === 'transport'">
|
|
|
- <text class="sub-title">接送任务详情</text>
|
|
|
- <view class="task-card transport-card">
|
|
|
- <view class="task-header">
|
|
|
- <text class="type-tag" :class="getTransportClass(order.subOrderType)">
|
|
|
- {{ getTransportLabel(order.subOrderType) }}
|
|
|
- </text>
|
|
|
- <text class="task-time">{{ order.serviceTime }}</text>
|
|
|
+ <!-- 任务详情扩展板块 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'base'">
|
|
|
+ <view class="base-info-grid">
|
|
|
+ <view class="bi-item" v-for="item in baseInfoList" :key="item.label">
|
|
|
+ <text class="bi-label">{{ item.label }}</text>
|
|
|
+ <text :class="['bi-val', item.highlight ? 'highlight' : '']">{{ item.value }}</text>
|
|
|
</view>
|
|
|
- <view class="task-body">
|
|
|
- <view class="task-row">
|
|
|
- <text class="task-label">起点</text>
|
|
|
- <text class="task-value">{{ order.fromAddress || '-' }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 接送任务详情 -->
|
|
|
+ <block v-if="order.type === 'transport'">
|
|
|
+ <text class="sub-title">接送任务详情</text>
|
|
|
+ <view class="task-card transport-card">
|
|
|
+ <view class="task-header">
|
|
|
+ <text class="type-tag" :class="getTransportClass(order.subOrderType)">
|
|
|
+ {{ getTransportLabel(order.subOrderType) }}
|
|
|
+ </text>
|
|
|
+ <text class="task-time">{{ order.serviceTime }}</text>
|
|
|
</view>
|
|
|
- <view class="task-row">
|
|
|
- <text class="task-label">终点</text>
|
|
|
- <text class="task-value">{{ order.toAddress || '-' }}</text>
|
|
|
+ <view class="task-body">
|
|
|
+ <view class="task-row">
|
|
|
+ <text class="task-label">起点</text>
|
|
|
+ <text class="task-value">{{ order.fromAddress || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="task-row">
|
|
|
+ <text class="task-label">终点</text>
|
|
|
+ <text class="task-value">{{ order.toAddress || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="task-row contact-row">
|
|
|
+ <text class="task-value">{{ order.userName }} — {{ order.userPhone }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="task-row contact-row">
|
|
|
- <text class="task-value">{{ order.userName }} — {{ order.userPhone }}</text>
|
|
|
+ </view>
|
|
|
+ </block>
|
|
|
+
|
|
|
+ <!-- 上门服务执行要求 -->
|
|
|
+ <block v-if="['feeding', 'washing'].includes(order.type)">
|
|
|
+ <text class="sub-title">服务执行要求</text>
|
|
|
+ <view class="task-card req-card">
|
|
|
+ <view class="req-item">
|
|
|
+ <text class="req-label">服务地址</text>
|
|
|
+ <text class="req-value">{{ order.address }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
+ </block>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 指派履约者 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'assignee'">
|
|
|
+ <view class="empty-state" v-if="order.statusKey === 'wait_dispatch'">
|
|
|
+ <uni-icons type="clock" size="40" color="#ccc"></uni-icons>
|
|
|
+ <text class="empty-text">等待派单中...</text>
|
|
|
</view>
|
|
|
- </block>
|
|
|
-
|
|
|
- <!-- 上门服务执行要求 -->
|
|
|
- <block v-if="['feeding', 'washing'].includes(order.type)">
|
|
|
- <text class="sub-title">服务执行要求</text>
|
|
|
- <view class="task-card req-card">
|
|
|
- <view class="req-item">
|
|
|
- <text class="req-label">服务地址</text>
|
|
|
- <text class="req-value">{{ order.address }}</text>
|
|
|
+ <view class="assignee-card" v-else>
|
|
|
+ <view class="assignee-header">
|
|
|
+ <view class="assignee-avatar">
|
|
|
+ <image v-if="order.assigneeAvatarUrl" :src="order.assigneeAvatarUrl" mode="aspectFill"
|
|
|
+ class="avatar-img"></image>
|
|
|
+ <uni-icons v-else type="person" size="30" color="#aaa"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <view class="assignee-info">
|
|
|
+ <text class="assignee-name">{{ order.assigneeName }}</text>
|
|
|
+ <text class="assignee-phone">联系电话:{{ order.assigneePhone }}</text>
|
|
|
+ <text class="assignee-zone">归属区域:{{ order.assigneeZone }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </block>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 指派履约者 -->
|
|
|
- <view class="tab-content" v-if="activeTab === 'assignee'">
|
|
|
- <view class="empty-state" v-if="order.statusKey === 'wait_dispatch'">
|
|
|
- <uni-icons type="clock" size="40" color="#ccc"></uni-icons>
|
|
|
- <text class="empty-text">等待派单中...</text>
|
|
|
</view>
|
|
|
- <view class="assignee-card" v-else>
|
|
|
- <view class="assignee-header">
|
|
|
- <view class="assignee-avatar">
|
|
|
- <image v-if="order.assigneeAvatarUrl" :src="order.assigneeAvatarUrl" mode="aspectFill" class="avatar-img"></image>
|
|
|
- <uni-icons v-else type="person" size="30" color="#aaa"></uni-icons>
|
|
|
- </view>
|
|
|
- <view class="assignee-info">
|
|
|
- <text class="assignee-name">{{ order.assigneeName }}</text>
|
|
|
- <text class="assignee-phone">联系电话:{{ order.assigneePhone }}</text>
|
|
|
- <text class="assignee-zone">归属区域:{{ order.assigneeZone }}</text>
|
|
|
+
|
|
|
+ <!-- 服务进度 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'progress'">
|
|
|
+ <view class="empty-state"
|
|
|
+ v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey) || serviceTimeline.length === 0">
|
|
|
+ <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
+ <text class="empty-text">服务尚未开始或暂无进度</text>
|
|
|
+ </view>
|
|
|
+ <view class="timeline" v-else>
|
|
|
+ <view class="tl-item" v-for="(tl, i) in serviceTimeline" :key="i">
|
|
|
+ <view class="tl-dot"></view>
|
|
|
+ <view class="tl-body">
|
|
|
+ <text class="tl-time">{{ tl.time }}</text>
|
|
|
+ <text class="tl-title">{{ tl.title }}</text>
|
|
|
+ <text class="tl-desc">{{ tl.desc }}</text>
|
|
|
+ <view class="tl-media" v-if="tl.media && tl.media.length">
|
|
|
+ <view v-for="(item, idx) in tl.media" :key="idx" class="media-item">
|
|
|
+ <image v-if="item.type === 'image'" mode="aspectFill" :src="item.url"
|
|
|
+ class="p-img" @click="previewImage(item.url, tl.media)"></image>
|
|
|
+ <view v-else-if="item.type === 'video'" class="p-video-box"
|
|
|
+ @click="openVideoPreview(item.url)">
|
|
|
+ <image src="/static/video-placeholder.png" mode="aspectFill" class="p-img"
|
|
|
+ style="background:#000;"></image>
|
|
|
+ <view class="play-icon-overlay">
|
|
|
+ <uni-icons type="videocam-filled" size="30" color="#fff"></uni-icons>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
- <!-- 服务进度 -->
|
|
|
- <view class="tab-content" v-if="activeTab === 'progress'">
|
|
|
- <view class="empty-state" v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey) || serviceTimeline.length === 0">
|
|
|
- <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
- <text class="empty-text">服务尚未开始或暂无进度</text>
|
|
|
+ <!-- 订单日志 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'log'">
|
|
|
+ <view class="empty-state" v-if="orderLogs.length === 0">
|
|
|
+ <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
+ <text class="empty-text">暂无订单日志</text>
|
|
|
+ </view>
|
|
|
+ <view class="timeline" v-else>
|
|
|
+ <view class="tl-item" v-for="(log, i) in orderLogs" :key="i">
|
|
|
+ <view class="tl-dot log-dot"></view>
|
|
|
+ <view class="tl-body">
|
|
|
+ <text class="tl-time">{{ log.time }}</text>
|
|
|
+ <text class="tl-title">{{ log.title }}</text>
|
|
|
+ <text class="tl-desc">{{ log.desc }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="timeline" v-else>
|
|
|
- <view class="tl-item" v-for="(tl, i) in serviceTimeline" :key="i">
|
|
|
- <view class="tl-dot"></view>
|
|
|
- <view class="tl-body">
|
|
|
- <text class="tl-time">{{ tl.time }}</text>
|
|
|
- <text class="tl-title">{{ tl.title }}</text>
|
|
|
- <text class="tl-desc">{{ tl.desc }}</text>
|
|
|
- <view class="tl-media" v-if="tl.media && tl.media.length">
|
|
|
- <view v-for="(item, idx) in tl.media" :key="idx" class="media-item">
|
|
|
- <image v-if="item.type === 'image'" mode="aspectFill" :src="item.url" class="p-img" @click="previewImage(item.url, tl.media)"></image>
|
|
|
- <view v-else-if="item.type === 'video'" class="p-video-box" @click="openVideoPreview(item.url)">
|
|
|
- <image src="/static/video-placeholder.png" mode="aspectFill" class="p-img" style="background:#000;"></image>
|
|
|
- <view class="play-icon-overlay">
|
|
|
- <uni-icons type="videocam-filled" size="30" color="#fff"></uni-icons>
|
|
|
- </view>
|
|
|
+
|
|
|
+ <!-- 投诉记录 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'complaint'">
|
|
|
+ <view class="empty-state" v-if="complaintList.length === 0">
|
|
|
+ <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
+ <text class="empty-text">暂无投诉记录</text>
|
|
|
+ </view>
|
|
|
+ <view class="timeline" v-else>
|
|
|
+ <view class="tl-item" v-for="(complaint, i) in complaintList" :key="i">
|
|
|
+ <view class="tl-dot" style="background: #f56c6c;"></view>
|
|
|
+ <view class="tl-body">
|
|
|
+ <text class="tl-time">{{ complaint.createTime }}</text>
|
|
|
+ <text class="tl-title">投诉原因:{{ complaint.reason }}</text>
|
|
|
+ <view v-if="complaint.photoUrls" class="tl-media">
|
|
|
+ <view v-for="(url, idx) in (complaint.photoUrls || '').split(',')" :key="idx"
|
|
|
+ class="media-item">
|
|
|
+ <image mode="aspectFill" :src="url" class="p-img"
|
|
|
+ @click="previewImage(url, (complaint.photoUrls || '').split(','))"></image>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
- <!-- 订单日志 -->
|
|
|
- <view class="tab-content" v-if="activeTab === 'log'">
|
|
|
- <view class="empty-state" v-if="orderLogs.length === 0">
|
|
|
- <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
- <text class="empty-text">暂无订单日志</text>
|
|
|
- </view>
|
|
|
- <view class="timeline" v-else>
|
|
|
- <view class="tl-item" v-for="(log, i) in orderLogs" :key="i">
|
|
|
- <view class="tl-dot log-dot"></view>
|
|
|
- <view class="tl-body">
|
|
|
- <text class="tl-time">{{ log.time }}</text>
|
|
|
- <text class="tl-title">{{ log.title }}</text>
|
|
|
- <text class="tl-desc">{{ log.desc }}</text>
|
|
|
+ <!-- 服务变更记录 -->
|
|
|
+ <view class="tab-content" v-if="activeTab === 'serviceChange'">
|
|
|
+ <view class="empty-state" v-if="serviceChangeList.length === 0">
|
|
|
+ <uni-icons type="info" size="40" color="#ccc"></uni-icons>
|
|
|
+ <text class="empty-text">暂无服务变更记录</text>
|
|
|
+ </view>
|
|
|
+ <view class="timeline" v-else>
|
|
|
+ <view class="tl-item" v-for="(change, i) in serviceChangeList" :key="i">
|
|
|
+ <view class="tl-dot" style="background: #409eff;"></view>
|
|
|
+ <view class="tl-body">
|
|
|
+ <text class="tl-time">{{ change.createTime }}</text>
|
|
|
+ <text class="tl-title">服务变更 - {{ change.service }}</text>
|
|
|
+ <text class="tl-desc">申诉理由:{{ change.reason }}</text>
|
|
|
+ <text class="tl-desc" v-if="change.auditStatus === 1"
|
|
|
+ style="color:#67c23a;">审核状态:已通过</text>
|
|
|
+ <text class="tl-desc" v-else-if="change.auditStatus === 2"
|
|
|
+ style="color:#f56c6c;">审核状态:已驳回</text>
|
|
|
+ <text class="tl-desc" v-else style="color:#e6a23c;">审核状态:待审核</text>
|
|
|
+ <view v-if="change.photoUrls" class="tl-media">
|
|
|
+ <view v-for="(url, idx) in (change.photoUrls || '').split(',')" :key="idx"
|
|
|
+ class="media-item">
|
|
|
+ <image mode="aspectFill" :src="url" class="p-img"
|
|
|
+ @click="previewImage(url, (change.photoUrls || '').split(','))"></image>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
@@ -231,9 +292,13 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 底部取消按钮 -->
|
|
|
- <view class="cancel-bar safe-bottom" v-if="!loading && ['wait_dispatch', 'wait_accept'].includes(order.statusKey)">
|
|
|
- <button class="cancel-order-btn" @click="onCancelOrder">取消订单</button>
|
|
|
+ <!-- 底部操作按钮 -->
|
|
|
+ <view class="cancel-bar safe-bottom"
|
|
|
+ v-if="!loading && (['wait_dispatch', 'wait_accept'].includes(order.statusKey) || ['serving', 'done'].includes(order.statusKey) && order.fulfiller)">
|
|
|
+ <button v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey)" class="cancel-order-btn"
|
|
|
+ @click="onCancelOrder">取消订单</button>
|
|
|
+ <button v-if="['serving', 'done'].includes(order.statusKey) && order.fulfiller" class="complaint-btn"
|
|
|
+ @click="onComplaint">投诉订单</button>
|
|
|
</view>
|
|
|
|
|
|
<!-- 自定义取消订单弹窗 -->
|
|
|
@@ -242,8 +307,10 @@
|
|
|
<view class="modal-content">
|
|
|
<view class="modal-title">提示</view>
|
|
|
<view class="modal-body">
|
|
|
- <view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ order.id }}] 吗?</view>
|
|
|
- <textarea class="cancel-input" v-model="cancelReason" placeholder="必填,请输入取消原因" placeholder-class="ph-color" :show-confirm-bar="false"></textarea>
|
|
|
+ <view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ order.id }}] 吗?
|
|
|
+ </view>
|
|
|
+ <textarea class="cancel-input" v-model="cancelReason" placeholder="必填,请输入取消原因"
|
|
|
+ placeholder-class="ph-color" :show-confirm-bar="false"></textarea>
|
|
|
</view>
|
|
|
<view class="modal-footer">
|
|
|
<view class="modal-btn btn-cancel" @click="closeCancelModal">取消</view>
|
|
|
@@ -265,6 +332,7 @@ import { getCustomer } from '@/api/archieves/customer'
|
|
|
import { getFulfiller } from '@/api/fulfiller/fulfiller'
|
|
|
import { listSubOrderLog } from '@/api/order/subOrderLog'
|
|
|
import { listComplaintByOrder } from '@/api/fulfiller/complaint'
|
|
|
+import { listSubOrderAppealByOrderId } from '@/api/order/subOrderAppeal'
|
|
|
|
|
|
const activeTab = ref('base')
|
|
|
const activeService = ref('transport')
|
|
|
@@ -275,7 +343,9 @@ const tabList = [
|
|
|
{ title: '基础信息', name: 'base' },
|
|
|
{ title: '履约者', name: 'assignee' },
|
|
|
{ title: '服务进度', name: 'progress' },
|
|
|
- { title: '订单日志', name: 'log' }
|
|
|
+ { title: '订单日志', name: 'log' },
|
|
|
+ { title: '服务变更', name: 'serviceChange' },
|
|
|
+ { title: '投诉记录', name: 'complaint' }
|
|
|
]
|
|
|
|
|
|
const currentServiceName = computed(() => {
|
|
|
@@ -286,8 +356,8 @@ const currentServiceName = computed(() => {
|
|
|
const order = reactive({
|
|
|
id: '',
|
|
|
code: '',
|
|
|
- statusKey: 'serving',
|
|
|
- statusText: '服务中',
|
|
|
+ statusKey: 'pending_service',
|
|
|
+ statusText: '待服务',
|
|
|
status: 2,
|
|
|
petName: '',
|
|
|
petBreed: '',
|
|
|
@@ -330,6 +400,7 @@ const order = reactive({
|
|
|
const orderLogsData = ref([])
|
|
|
const fulfillerLogsData = ref([])
|
|
|
const complaintList = ref([])
|
|
|
+const serviceChangeList = ref([])
|
|
|
|
|
|
const loadOrderDetail = async (id) => {
|
|
|
if (!id) return
|
|
|
@@ -366,11 +437,12 @@ const loadOrderDetail = async (id) => {
|
|
|
if (res.usrPet) tasks.push(loadPetInfo(res.usrPet))
|
|
|
if (res.usrCustomer) tasks.push(loadCustomerInfo(res.usrCustomer))
|
|
|
if (res.fulfiller) tasks.push(loadFulfillerInfo(res.fulfiller))
|
|
|
-
|
|
|
+
|
|
|
await Promise.all([
|
|
|
...tasks,
|
|
|
loadOrderLogs(id),
|
|
|
- loadComplaints(id)
|
|
|
+ loadComplaints(id),
|
|
|
+ loadServiceChanges(id)
|
|
|
])
|
|
|
}
|
|
|
} catch (error) {
|
|
|
@@ -457,13 +529,23 @@ const loadComplaints = async (id) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const loadServiceChanges = async (id) => {
|
|
|
+ try {
|
|
|
+ const res = await listSubOrderAppealByOrderId(id)
|
|
|
+ serviceChangeList.value = res || []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载服务变更记录失败:', error)
|
|
|
+ serviceChangeList.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const getStatusKey = (status) => {
|
|
|
- const map = { 0: 'wait_dispatch', 1: 'wait_accept', 2: 'serving', 3: 'confirming', 4: 'done', 5: 'cancel' }
|
|
|
+ const map = { 0: 'wait_dispatch', 1: 'wait_accept', 2: 'pending_service', 3: 'serving', 4: 'done', 5: 'cancel', 6: 'rejected', 7: 'closed' }
|
|
|
return map[status] || 'serving'
|
|
|
}
|
|
|
|
|
|
const getStatusName = (status) => {
|
|
|
- const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
|
|
|
+ const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消', 6: '已拒绝', 7: '已关闭' }
|
|
|
return map[status] || '-'
|
|
|
}
|
|
|
|
|
|
@@ -500,14 +582,7 @@ onLoad((options) => {
|
|
|
})
|
|
|
|
|
|
const progressSteps = computed(() => {
|
|
|
- const status = order.status
|
|
|
- const steps = [
|
|
|
- { label: '商户下单', time: '' },
|
|
|
- { label: '运营派单', time: '' },
|
|
|
- { label: '履约接单', time: '' },
|
|
|
- { label: '服务中', time: '' },
|
|
|
- { label: '已完成', time: '' }
|
|
|
- ]
|
|
|
+ const status = Number(order.status)
|
|
|
const getSystemLogTime = (step) => {
|
|
|
const log = (orderLogsData.value || []).find(l => parseInt(l.step) === step)
|
|
|
return log ? (log.createTime || log.time) : ''
|
|
|
@@ -516,58 +591,76 @@ const progressSteps = computed(() => {
|
|
|
const log = (fulfillerLogsData.value || []).find(l => parseInt(l.step) === step)
|
|
|
return log ? (log.createTime || log.time) : ''
|
|
|
}
|
|
|
- const fulfillerCustomLogTime = () => {
|
|
|
- const log = [...(fulfillerLogsData.value || [])].reverse().find(l => {
|
|
|
- const s = parseInt(l.step)
|
|
|
- return s >= 1 && s <= 98
|
|
|
- })
|
|
|
- return log ? (log.createTime || log.time) : ''
|
|
|
- }
|
|
|
-
|
|
|
- let active = 0
|
|
|
|
|
|
+ // 已取消 — 特殊两步流程
|
|
|
if (status === 5) {
|
|
|
const cancelTime = getSystemLogTime(5) || order.cancelTime || ''
|
|
|
return [
|
|
|
- { label: '商户下单', time: getSystemLogTime(0) || order.createTime || '', done: true, active: false },
|
|
|
- { label: '已取消', time: cancelTime, done: true, active: true }
|
|
|
- ].map(s => ({ ...s, time: s.time ? s.time.substring(5, 16) : '' }))
|
|
|
+ { label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
|
|
|
+ { label: '已取消', time: cancelTime.substring(5, 16), done: true, active: true }
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
- steps[0].time = getSystemLogTime(0) || order.createTime || ''
|
|
|
- if (steps[0].time) active = 1
|
|
|
-
|
|
|
- steps[1].time = getSystemLogTime(1) || ''
|
|
|
- if (steps[1].time || status >= 1) active = 2
|
|
|
-
|
|
|
- steps[2].time = getSystemLogTime(2) || getFulfillerLogTime(0) || ''
|
|
|
- if (status === 1) {
|
|
|
- steps[2].label = '待履约者接单'
|
|
|
- } else if (status >= 2) {
|
|
|
- steps[2].label = '履约者已接单'
|
|
|
- if (steps[2].time) active = 3
|
|
|
+ // 已拒绝 — 特殊两步流程
|
|
|
+ if (status === 6) {
|
|
|
+ return [
|
|
|
+ { label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
|
|
|
+ { label: '已拒绝', time: '', done: true, active: true }
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
- const cTime = fulfillerCustomLogTime()
|
|
|
- steps[3].time = getSystemLogTime(3) || cTime || ''
|
|
|
- if (status === 2) {
|
|
|
- steps[3].label = '待服务'
|
|
|
- if (cTime) active = 4
|
|
|
- } else if (status >= 3) {
|
|
|
- steps[3].label = '服务进行中'
|
|
|
- if (steps[3].time) active = 4
|
|
|
+ // 已关闭 — 特殊两步流程
|
|
|
+ if (status === 7) {
|
|
|
+ return [
|
|
|
+ { label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
|
|
|
+ { label: '已关闭', time: '', done: true, active: true }
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
- if (status === 4 || getSystemLogTime(4) || getFulfillerLogTime(99)) {
|
|
|
- steps[4].time = getSystemLogTime(4) || getFulfillerLogTime(99) || ''
|
|
|
- active = 5
|
|
|
+ // 六步流程(全四字):商户下单 → 运营派单 → 履约接单 → 等待服务 → 服务进行 → 订单完成
|
|
|
+ const steps = [
|
|
|
+ { label: '商户下单', time: getSystemLogTime(0) || order.createTime || '' },
|
|
|
+ { label: '运营派单', time: getSystemLogTime(1) || '' },
|
|
|
+ { label: '履约接单', time: getSystemLogTime(2) || getFulfillerLogTime(0) || '' },
|
|
|
+ { label: '等待服务', time: getSystemLogTime(3) || '' },
|
|
|
+ { label: '服务进行', time: getFulfillerLogTime(99) || '' },
|
|
|
+ { label: '订单完成', time: getSystemLogTime(4) || '' }
|
|
|
+ ]
|
|
|
+
|
|
|
+ let active = 1 // 默认停在「运营派单」
|
|
|
+
|
|
|
+ switch (status) {
|
|
|
+ case 0: // 待派单
|
|
|
+ active = 1
|
|
|
+ break
|
|
|
+ case 1: // 待接单
|
|
|
+ active = 2
|
|
|
+ steps[2].label = '等待接单'
|
|
|
+ break
|
|
|
+ case 2: // 待服务
|
|
|
+ active = 3
|
|
|
+ steps[2].label = '已确认接'
|
|
|
+ steps[3].label = '等待服务'
|
|
|
+ break
|
|
|
+ case 3: // 服务中(到达打卡)
|
|
|
+ active = 4
|
|
|
+ steps[2].label = '已确认接'
|
|
|
+ steps[3].label = '已到达点'
|
|
|
+ steps[4].label = '服务进行'
|
|
|
+ break
|
|
|
+ case 4: // 已完成
|
|
|
+ active = 6
|
|
|
+ steps[2].label = '已确认接'
|
|
|
+ steps[3].label = '已到达点'
|
|
|
+ steps[4].label = '服务进行'
|
|
|
+ break
|
|
|
}
|
|
|
|
|
|
return steps.map((s, i) => ({
|
|
|
label: s.label,
|
|
|
time: s.time ? s.time.substring(5, 16) : '',
|
|
|
done: i < active,
|
|
|
- active: i === active
|
|
|
+ active: i === active
|
|
|
}))
|
|
|
})
|
|
|
|
|
|
@@ -609,7 +702,19 @@ const closeVideoPreview = () => {
|
|
|
}
|
|
|
|
|
|
const previewImage = (url, mediaList) => {
|
|
|
- const imgUrls = mediaList.filter(m => m.type === 'image').map(m => m.url);
|
|
|
+ let imgUrls = []
|
|
|
+ if (Array.isArray(mediaList)) {
|
|
|
+ if (mediaList.length > 0 && typeof mediaList[0] === 'string') {
|
|
|
+ // 直接的URL数组(服务变更记录)
|
|
|
+ imgUrls = mediaList
|
|
|
+ } else {
|
|
|
+ // 包含type属性的对象数组
|
|
|
+ imgUrls = mediaList.filter(m => m.type === 'image').map(m => m.url)
|
|
|
+ }
|
|
|
+ } else if (typeof mediaList === 'string') {
|
|
|
+ // 单个URL
|
|
|
+ imgUrls = [mediaList]
|
|
|
+ }
|
|
|
uni.previewImage({
|
|
|
current: url,
|
|
|
urls: imgUrls
|
|
|
@@ -667,7 +772,7 @@ const confirmCancelOrder = async () => {
|
|
|
uni.hideLoading()
|
|
|
uni.showToast({ title: '订单已取消', icon: 'success' })
|
|
|
showCancelModal.value = false
|
|
|
-
|
|
|
+
|
|
|
// 重新初始化页面数据
|
|
|
loadOrderDetail(order.id)
|
|
|
} catch (error) {
|
|
|
@@ -676,6 +781,12 @@ const confirmCancelOrder = async () => {
|
|
|
uni.showToast({ title: '取消失败', icon: 'none' })
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+const onComplaint = () => {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/my/complaint/submit/index?orderId=${order.id}&fulfillerId=${order.fulfiller}&orderCode=${order.code}`
|
|
|
+ })
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -719,6 +830,11 @@ const confirmCancelOrder = async () => {
|
|
|
color: #ff9800;
|
|
|
}
|
|
|
|
|
|
+.badge-pending_service {
|
|
|
+ background: #e3f2fd;
|
|
|
+ color: #49a3ff;
|
|
|
+}
|
|
|
+
|
|
|
.badge-serving {
|
|
|
background: #e3f2fd;
|
|
|
color: #2196f3;
|
|
|
@@ -849,10 +965,23 @@ const confirmCancelOrder = async () => {
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
|
|
|
-.pet-avatar, .user-avatar, .assignee-avatar {
|
|
|
- width: 80rpx; height: 80rpx; border-radius: 50%; background: #f0f2f5;
|
|
|
- display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0;
|
|
|
- .avatar-img { width: 100%; height: 100%; }
|
|
|
+.pet-avatar,
|
|
|
+.user-avatar,
|
|
|
+.assignee-avatar {
|
|
|
+ width: 80rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #f0f2f5;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: hidden;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .avatar-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.pet-basic {
|
|
|
@@ -1296,13 +1425,33 @@ const confirmCancelOrder = async () => {
|
|
|
background: transparent;
|
|
|
border-radius: 48rpx;
|
|
|
line-height: 92rpx;
|
|
|
- &::after { border: none; }
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.complaint-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 96rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ border: 2rpx solid #ff9800;
|
|
|
+ color: #ff9800;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 48rpx;
|
|
|
+ line-height: 92rpx;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 骨架屏动画与样式 */
|
|
|
.skeleton-page {
|
|
|
padding: 24rpx;
|
|
|
}
|
|
|
+
|
|
|
.skeleton-box {
|
|
|
background: #e0e0e0;
|
|
|
border-radius: 16rpx;
|
|
|
@@ -1311,33 +1460,40 @@ const confirmCancelOrder = async () => {
|
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
|
background-size: 400% 100%;
|
|
|
}
|
|
|
+
|
|
|
@keyframes skeleton-shimmer {
|
|
|
0% {
|
|
|
background-position: 100% 0;
|
|
|
}
|
|
|
+
|
|
|
100% {
|
|
|
background-position: -100% 0;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
.skeleton-header {
|
|
|
height: 80rpx;
|
|
|
border-radius: 20rpx;
|
|
|
}
|
|
|
+
|
|
|
.skeleton-progress {
|
|
|
height: 120rpx;
|
|
|
border-radius: 28rpx;
|
|
|
}
|
|
|
+
|
|
|
.skeleton-row-cards {
|
|
|
display: flex;
|
|
|
gap: 20rpx;
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
+
|
|
|
.skeleton-card {
|
|
|
flex: 1;
|
|
|
height: 280rpx;
|
|
|
border-radius: 28rpx;
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
+
|
|
|
.skeleton-content {
|
|
|
height: 500rpx;
|
|
|
border-radius: 28rpx;
|
|
|
@@ -1347,11 +1503,13 @@ const confirmCancelOrder = async () => {
|
|
|
.fade-in {
|
|
|
animation: fadeIn 0.4s ease-out forwards;
|
|
|
}
|
|
|
+
|
|
|
@keyframes fadeIn {
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
transform: translateY(10rpx);
|
|
|
}
|
|
|
+
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
transform: translateY(0);
|
|
|
@@ -1365,6 +1523,7 @@ const confirmCancelOrder = async () => {
|
|
|
gap: 16rpx;
|
|
|
margin-top: 16rpx;
|
|
|
}
|
|
|
+
|
|
|
.media-item {
|
|
|
width: 160rpx;
|
|
|
height: 160rpx;
|
|
|
@@ -1373,10 +1532,13 @@ const confirmCancelOrder = async () => {
|
|
|
background: #f0f0f0;
|
|
|
position: relative;
|
|
|
}
|
|
|
-.p-img, .p-video-box {
|
|
|
+
|
|
|
+.p-img,
|
|
|
+.p-video-box {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
}
|
|
|
+
|
|
|
.play-icon-overlay {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
@@ -1405,10 +1567,12 @@ const confirmCancelOrder = async () => {
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
+
|
|
|
.preview-video {
|
|
|
width: 100%;
|
|
|
height: 60vh;
|
|
|
}
|
|
|
+
|
|
|
.close-video-btn {
|
|
|
position: absolute;
|
|
|
top: 100rpx;
|
|
|
@@ -1434,6 +1598,7 @@ const confirmCancelOrder = async () => {
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
+
|
|
|
.modal-mask {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
@@ -1442,6 +1607,7 @@ const confirmCancelOrder = async () => {
|
|
|
height: 100%;
|
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
|
}
|
|
|
+
|
|
|
.modal-content {
|
|
|
position: relative;
|
|
|
width: 80%;
|
|
|
@@ -1450,6 +1616,7 @@ const confirmCancelOrder = async () => {
|
|
|
overflow: hidden;
|
|
|
z-index: 1000;
|
|
|
}
|
|
|
+
|
|
|
.modal-title {
|
|
|
padding: 30rpx 0 20rpx;
|
|
|
text-align: center;
|
|
|
@@ -1457,9 +1624,11 @@ const confirmCancelOrder = async () => {
|
|
|
font-weight: bold;
|
|
|
color: #333;
|
|
|
}
|
|
|
+
|
|
|
.modal-body {
|
|
|
padding: 10rpx 40rpx 30rpx;
|
|
|
}
|
|
|
+
|
|
|
.cancel-input {
|
|
|
width: 100%;
|
|
|
height: 160rpx;
|
|
|
@@ -1470,13 +1639,16 @@ const confirmCancelOrder = async () => {
|
|
|
box-sizing: border-box;
|
|
|
color: #333;
|
|
|
}
|
|
|
+
|
|
|
.ph-color {
|
|
|
color: #999;
|
|
|
}
|
|
|
+
|
|
|
.modal-footer {
|
|
|
display: flex;
|
|
|
border-top: 2rpx solid #EEEEEE;
|
|
|
}
|
|
|
+
|
|
|
.modal-btn {
|
|
|
flex: 1;
|
|
|
height: 90rpx;
|
|
|
@@ -1485,10 +1657,12 @@ const confirmCancelOrder = async () => {
|
|
|
font-size: 30rpx;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
+
|
|
|
.btn-cancel {
|
|
|
color: #666;
|
|
|
border-right: 2rpx solid #EEEEEE;
|
|
|
}
|
|
|
+
|
|
|
.btn-confirm {
|
|
|
color: #2196f3;
|
|
|
}
|