|
|
@@ -240,14 +240,24 @@
|
|
|
<h4 class="p-title">{{ step.title }}</h4>
|
|
|
<p class="p-desc">{{ step.desc }}</p>
|
|
|
<div class="p-media" v-if="step.media && step.media.length">
|
|
|
- <div v-for="(item, i) in step.media" :key="i" class="media-item">
|
|
|
- <el-image v-if="item.type === 'image'" :src="item.url"
|
|
|
- :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)"
|
|
|
- fit="cover" class="p-img" :preview-teleported="true" />
|
|
|
- <video v-else-if="item.type === 'video'" :src="item.url" controls
|
|
|
- class="p-video"></video>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div v-for="(item, i) in step.media" :key="i" class="media-item">
|
|
|
+ <!-- 图片类型 -->
|
|
|
+ <el-image v-if="item.type === 'image'" :src="item.url"
|
|
|
+ :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)"
|
|
|
+ fit="cover" class="p-img" :preview-teleported="true" />
|
|
|
+
|
|
|
+ <!-- 视频类型 -->
|
|
|
+ <div v-else-if="item.type === 'video'" class="p-video-box"
|
|
|
+ @click="openVideoPreview(item.url)">
|
|
|
+ <video :src="item.url" preload="metadata" class="p-img p-video"></video>
|
|
|
+ <div class="play-icon-overlay">
|
|
|
+ <el-icon>
|
|
|
+ <VideoPlay />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</el-timeline-item>
|
|
|
</el-timeline>
|
|
|
@@ -262,8 +272,9 @@
|
|
|
@click="handleExportLogs">导出日志Excel</el-button>
|
|
|
</div>
|
|
|
<el-timeline>
|
|
|
- <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index" :timestamp="''"
|
|
|
- :type="'primary'" :icon="undefined" placement="top">
|
|
|
+ <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index"
|
|
|
+ :timestamp="log.createTime || log.time || ''" :type="'primary'" :icon="undefined"
|
|
|
+ placement="top">
|
|
|
<div class="log-card">
|
|
|
<div class="l-tit">{{ log.title }}</div>
|
|
|
<div class="l-txt">{{ log.content }}</div>
|
|
|
@@ -272,21 +283,67 @@
|
|
|
</el-timeline>
|
|
|
</div>
|
|
|
</el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab 5: Complaint Records -->
|
|
|
+ <el-tab-pane label="投诉记录" name="complaints">
|
|
|
+ <div class="tab-pane-content">
|
|
|
+ <div v-if="complaintList.length === 0" class="empty-state">
|
|
|
+ <el-result icon="success" title="暂无投诉" sub-title="该订单暂无投诉记录"></el-result>
|
|
|
+ </div>
|
|
|
+ <el-timeline v-else>
|
|
|
+ <el-timeline-item v-for="(complaint, index) in complaintList" :key="index"
|
|
|
+ :timestamp="complaint.createTime" placement="top" color="#f56c6c">
|
|
|
+ <div class="log-card">
|
|
|
+ <div class="l-tit">履约者:{{ complaint.fulfiller }}</div>
|
|
|
+ <div class="l-txt">{{ complaint.reason }}</div>
|
|
|
+ </div>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
</el-tabs>
|
|
|
</div>
|
|
|
<PetDetailDrawer v-model:visible="petDetailVisible" :pet-id="order?.pet || order?.petId" />
|
|
|
</div>
|
|
|
</el-drawer>
|
|
|
+
|
|
|
+ <!-- 视频播放弹窗 -->
|
|
|
+ <el-dialog v-model="videoPreview.visible" title="视频播放" width="800px" append-to-body @closed="videoPreview.url = ''">
|
|
|
+ <div
|
|
|
+ style="width: 100%; display: flex; justify-content: center; background: #000; border-radius: 4px; overflow: hidden;">
|
|
|
+ <video v-if="videoPreview.url" :src="videoPreview.url" controls autoplay
|
|
|
+ style="max-width: 100%; max-height: 70vh;"></video>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, watch, getCurrentInstance } from 'vue'
|
|
|
+import { ref, reactive, computed, watch, getCurrentInstance } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { getPet } from '@/api/archieves/pet'
|
|
|
import { getCustomer } from '@/api/archieves/customer'
|
|
|
-import { listSubOrderLog, exportSubOrderLog } from '@/api/order/subOrderLog'
|
|
|
+import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
|
|
|
+import { listComplaintByOrder } from '@/api/fulfiller/complaint'
|
|
|
import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
|
|
|
|
|
|
+// 视频判定辅助函数
|
|
|
+const isVideo = (url) => {
|
|
|
+ if (!url) return false;
|
|
|
+ const videoExts = ['.mp4', '.mov', '.avi', '.wmv', '.webm', '.ogg'];
|
|
|
+ return videoExts.some(ext => String(url).toLowerCase().endsWith(ext));
|
|
|
+}
|
|
|
+
|
|
|
+// 视频预览状态
|
|
|
+const videoPreview = reactive({
|
|
|
+ visible: false,
|
|
|
+ url: ''
|
|
|
+})
|
|
|
+
|
|
|
+const openVideoPreview = (url) => {
|
|
|
+ videoPreview.url = url;
|
|
|
+ videoPreview.visible = true;
|
|
|
+}
|
|
|
+
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
|
|
|
const props = defineProps({
|
|
|
@@ -308,6 +365,7 @@ const loadSeq = ref(0)
|
|
|
|
|
|
const orderLogs = ref([])
|
|
|
const fulfillerLogs = ref([])
|
|
|
+const complaintList = ref([])
|
|
|
|
|
|
const loadOrderLogs = async (order) => {
|
|
|
const id = order?.id
|
|
|
@@ -326,6 +384,13 @@ const loadOrderLogs = async (order) => {
|
|
|
orderLogs.value = []
|
|
|
fulfillerLogs.value = []
|
|
|
}
|
|
|
+
|
|
|
+ try {
|
|
|
+ const complaintRes = await listComplaintByOrder(id)
|
|
|
+ complaintList.value = complaintRes?.data || []
|
|
|
+ } catch {
|
|
|
+ complaintList.value = []
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const loadPetAndCustomer = async (order) => {
|
|
|
@@ -513,16 +578,16 @@ const serviceProgressSteps = computed(() => {
|
|
|
})
|
|
|
|
|
|
const handleExportLogs = () => {
|
|
|
- const id = order.value?.id
|
|
|
+ const id = props.order?.id;
|
|
|
if (!id) {
|
|
|
- ElMessage.warning('订单号缺失,无法导出')
|
|
|
- return
|
|
|
+ ElMessage.warning('订单信息不完整,无法导出');
|
|
|
+ return;
|
|
|
}
|
|
|
proxy?.download(
|
|
|
- `order/subOrderLog/export/${id}`,
|
|
|
+ exportSubOrderLogUrl(id),
|
|
|
{},
|
|
|
- `OrderLogs_${order.value.orderNo}_${new Date().getTime()}.xlsx`
|
|
|
- )
|
|
|
+ `OrderLogs_${props.order.orderNo}_${new Date().getTime()}.xlsx`
|
|
|
+ );
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
@@ -792,10 +857,7 @@ const handleExportLogs = () => {
|
|
|
}
|
|
|
|
|
|
.p-video {
|
|
|
- width: 140px;
|
|
|
- height: 80px;
|
|
|
- border-radius: 4px;
|
|
|
- background: #000;
|
|
|
+ object-fit: cover;
|
|
|
}
|
|
|
|
|
|
.p-img {
|
|
|
@@ -804,6 +866,36 @@ const handleExportLogs = () => {
|
|
|
border-radius: 4px;
|
|
|
border: 1px solid #e4e7ed;
|
|
|
cursor: pointer;
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.p-video-box {
|
|
|
+ position: relative;
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.play-icon-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: rgba(0, 0, 0, 0.4);
|
|
|
+ color: #fff;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 18px;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.p-video-box:hover .play-icon-overlay {
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ transform: translate(-50%, -50%) scale(1.1);
|
|
|
}
|
|
|
|
|
|
/* New Transport Split Styles */
|