javacodeadmin/ruoyi-ui/src/views/system/Order/components/OrderDetailDialog.vue

647 lines
17 KiB
Vue
Raw 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>
<el-dialog
:title="`订单明细 - ${orderInfo.orderId}`"
:visible.sync="visible"
width="90%"
:close-on-click-modal="true"
:close-on-press-escape="true"
class="order-detail-dialog"
top="5vh"
>
<div class="order-detail-container">
<el-row :gutter="24">
<!-- 左侧订单基本信息 -->
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>订单基本信息</span>
</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="订单号">
{{ orderInfo.orderId || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="用户姓名">
{{ orderInfo.userName || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="联系电话">
{{ orderInfo.userPhone || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="服务名称">
{{ orderInfo.productName || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="预约数量">
{{ orderInfo.num || '0' }}
</el-descriptions-item>
<el-descriptions-item label="预约时间">
{{ orderInfo.appointmentTime || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="预约地点">
{{ orderInfo.appointmentAddress || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="订单总价">
¥{{ (orderInfo.totalPrice || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="支付金额">
¥{{ (orderInfo.payPrice || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="抵扣金额">
¥{{ (orderInfo.deduction || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ orderInfo.createdAt || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="支付时间">
{{ orderInfo.payTime || '未设置' }}
</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag :type="getStatusTagType(orderInfo.status)">
{{ getStatusLabel(orderInfo.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注说明" v-if="orderInfo.mark">
{{ orderInfo.mark }}
</el-descriptions-item>
</el-descriptions>
<!-- 附件信息 -->
<div v-if="orderInfo.fileData" class="attachment-section">
<h4>订单附件</h4>
<div class="file-list">
<div v-for="(file, index) in getFileList()" :key="index" class="file-item">
<el-image
v-if="isImage(file)"
:src="file"
:preview-src-list="getFileList()"
fit="cover"
class="file-image"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<div v-else class="file-icon">
<i class="el-icon-document"></i>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧:订单日志流水 -->
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>订单日志流水</span>
<div class="header-actions">
<el-button
type="text"
icon="el-icon-refresh"
@click="handleRefreshLogs"
class="action-btn"
>
刷新
</el-button>
<el-button
type="text"
icon="el-icon-download"
@click="handleExportLogs"
class="action-btn"
>
导出
</el-button>
</div>
</div>
<div v-if="orderInfo.logs && orderInfo.logs.length > 0" class="order-timeline">
<div class="timeline-container">
<div
v-for="(log, index) in orderInfo.logs"
:key="log.id || index"
class="timeline-item"
>
<!-- 时间轴节点 -->
<div class="timeline-node" :class="getTimelineNodeClass(log.type)">
<i :class="getTimelineIcon(log.type)"></i>
</div>
<!-- 时间轴内容 -->
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-title">{{ log.title || '未知操作' }}</span>
<span class="timeline-time">{{ formatTime(log.createdAt) }}</span>
</div>
<div class="timeline-body">
<div class="timeline-description">{{ log.content || '无描述信息' }}</div>
<!-- 显示图片 -->
<div v-if="log.images && log.images.length > 0" class="timeline-images">
<el-image
v-for="(img, imgIndex) in log.images"
:key="imgIndex"
:src="img"
:preview-src-list="log.images"
fit="cover"
class="timeline-image"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</div>
<!-- 操作按钮 -->
<div class="timeline-actions" v-if="index === orderInfo.logs.length - 1">
<el-button
v-if="log.type === 'create'"
type="primary"
size="small"
@click="handleDispatch"
>
<i class="el-icon-s-promotion"></i>
派单
</el-button>
<el-button
v-if="log.type === 'dispatch'"
type="success"
size="small"
@click="handleAccept"
>
<i class="el-icon-check"></i>
接单
</el-button>
<el-button
v-if="log.type === 'accept'"
type="warning"
size="small"
@click="handleStartService"
>
<i class="el-icon-video-play"></i>
开始服务
</el-button>
<el-button
v-if="log.type === 'service'"
type="success"
size="small"
@click="handleComplete"
>
<i class="el-icon-circle-check"></i>
完成服务
</el-button>
</div>
</div>
</div>
<!-- 连接线(除了最后一个项目) -->
<div v-if="index < orderInfo.logs.length - 1" class="timeline-connector"></div>
</div>
</div>
</div>
<div v-else class="no-logs">
<el-empty description="暂无日志记录" :image-size="100">
<el-button type="primary" @click="handleRefreshLogs">刷新数据</el-button>
</el-empty>
</div>
</el-card>
</el-col>
</el-row>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false" type="primary" size="medium"> </el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'OrderDetailDialog',
props: {
visible: {
type: Boolean,
default: false
},
orderInfo: {
type: Object,
default: () => ({})
}
},
methods: {
/** 获取状态标签类型 */
getStatusTagType(status) {
if (!status) return 'info'
const statusMap = {
1: 'warning', // 待支付
2: 'primary', // 待服务
3: 'success', // 服务中
4: 'success', // 已完成
5: 'danger', // 已取消
6: 'info', // 已退款
7: 'info' // 已结束
}
return statusMap[status] || 'info'
},
/** 获取订单状态标签 */
getStatusLabel(status) {
if (!status) return '未知状态'
const statusMap = {
1: '待支付',
2: '待服务',
3: '服务中',
4: '已完成',
5: '已取消',
6: '已退款',
7: '已结束'
}
return statusMap[status] || `状态${status}`
},
/** 获取时间轴节点样式类 */
getTimelineNodeClass(type) {
if (!type) return 'timeline-node-default'
const typeMap = {
'create': 'timeline-node-primary',
'pay': 'timeline-node-success',
'dispatch': 'timeline-node-info',
'accept': 'timeline-node-warning',
'service': 'timeline-node-info',
'complete': 'timeline-node-danger'
}
return typeMap[type] || 'timeline-node-default'
},
/** 获取时间轴图标 */
getTimelineIcon(type) {
if (!type) return 'el-icon-info'
const typeMap = {
'create': 'el-icon-plus',
'pay': 'el-icon-check',
'dispatch': 'el-icon-s-promotion',
'accept': 'el-icon-check',
'service': 'el-icon-video-play',
'complete': 'el-icon-circle-check'
}
return typeMap[type] || 'el-icon-info'
},
/** 格式化时间 */
formatTime(time) {
if (!time) return '未知时间'
const date = new Date(time)
return date.toLocaleString()
},
/** 获取文件列表 */
getFileList() {
if (!this.orderInfo.fileData) {
return []
}
try {
let files
if (typeof this.orderInfo.fileData === 'string') {
try {
files = JSON.parse(this.orderInfo.fileData)
} catch (e) {
files = this.orderInfo.fileData.split(',').filter(Boolean)
}
} else if (Array.isArray(this.orderInfo.fileData)) {
files = this.orderInfo.fileData
} else {
files = []
}
return Array.isArray(files) ? files : []
} catch (e) {
console.error('解析文件数据失败:', e)
return []
}
},
/** 判断是否为图片文件 */
isImage(fileUrl) {
if (!fileUrl || typeof fileUrl !== 'string') return false
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const lowerUrl = fileUrl.toLowerCase()
return imageExtensions.some(ext => lowerUrl.includes(ext))
},
/** 刷新日志 */
handleRefreshLogs() {
this.$emit('refresh-logs')
},
/** 导出日志 */
handleExportLogs() {
this.$emit('export-logs')
},
/** 派单操作 */
handleDispatch() {
this.$emit('dispatch-order')
},
/** 接单操作 */
handleAccept() {
this.$emit('accept-order')
},
/** 开始服务操作 */
handleStartService() {
this.$emit('start-service')
},
/** 完成服务操作 */
handleComplete() {
this.$emit('complete-service')
}
}
}
</script>
<style lang="scss" scoped>
.order-detail-dialog {
.el-dialog__body {
padding: 20px 30px;
max-height: 80vh;
overflow-y: auto;
}
.el-dialog__footer {
padding: 15px 30px;
border-top: 1px solid #e4e7ed;
background: #f8f9fa;
}
}
.order-detail-container {
.el-card {
margin-bottom: 20px;
.el-card__header {
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
.clearfix {
display: flex;
align-items: center;
justify-content: space-between;
span {
font-weight: 600;
color: #303133;
font-size: 16px;
}
.header-actions {
.action-btn {
margin-left: 10px;
color: #409EFF;
&:hover {
color: #1890ff;
}
}
}
}
}
.el-card__body {
padding: 20px;
}
}
}
.attachment-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
h4 {
margin: 0 0 15px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
.file-item {
flex-shrink: 0;
}
.file-image {
width: 60px;
height: 60px;
border-radius: 6px;
border: 2px solid #e4e7ed;
cursor: pointer;
transition: border-color 0.2s ease;
&:hover {
border-color: #409eff;
}
}
.file-icon {
width: 60px;
height: 60px;
border-radius: 6px;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
border: 2px solid #e4e7ed;
color: #909399;
}
}
}
/* 订单进度时间轴样式 */
.order-timeline {
padding: 20px 0;
}
.timeline-container {
position: relative;
}
.timeline-item {
position: relative;
display: flex;
align-items: flex-start;
margin-bottom: 30px;
}
.timeline-item:last-child {
margin-bottom: 0;
}
/* 时间轴节点 */
.timeline-node {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
flex-shrink: 0;
position: relative;
z-index: 2;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.timeline-node i {
font-size: 18px;
color: white;
}
.timeline-node-primary {
background: linear-gradient(135deg, #409EFF, #36a3f7);
}
.timeline-node-success {
background: linear-gradient(135deg, #67C23A, #5daf34);
}
.timeline-node-warning {
background: linear-gradient(135deg, #E6A23C, #d49426);
}
.timeline-node-info {
background: linear-gradient(135deg, #909399, #7a7d83);
}
.timeline-node-danger {
background: linear-gradient(135deg, #F56C6C, #e64242);
}
.timeline-node-default {
background: linear-gradient(135deg, #909399, #7a7d83);
}
/* 时间轴内容 */
.timeline-content {
flex: 1;
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #ebeef5;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.timeline-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.timeline-time {
font-size: 12px;
color: #909399;
background: #f5f7fa;
padding: 4px 8px;
border-radius: 12px;
}
.timeline-body {
color: #606266;
line-height: 1.6;
}
.timeline-description {
margin-bottom: 12px;
font-size: 14px;
}
/* 时间轴图片 */
.timeline-images {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.timeline-image {
width: 80px;
height: 80px;
border-radius: 6px;
border: 1px solid #ebeef5;
object-fit: cover;
}
/* 连接线 */
.timeline-connector {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: 30px;
background: linear-gradient(to bottom, #e4e7ed, #c0c4cc);
z-index: 1;
}
/* 无记录状态 */
.no-logs {
text-align: center;
padding: 40px 20px;
}
/* 操作按钮 */
.timeline-actions {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.order-detail-container {
.el-col {
margin-bottom: 20px;
}
}
.timeline-item {
flex-direction: column;
align-items: center;
text-align: center;
}
.timeline-node {
margin-right: 0;
margin-bottom: 15px;
}
.timeline-content {
width: 100%;
text-align: left;
}
.timeline-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.timeline-connector {
left: 50%;
transform: translateX(-50%);
}
}
</style>