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

2099 lines
62 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>
<div>
<el-drawer
:title="`订单明细 - ${orderInfo.orderId}`"
:visible.sync="dialogVisible"
size="60%"
append-to-body
:close-on-click-modal="true"
:close-on-press-escape="true"
class="order-detail-dialog"
top="5vh"
@close="handleClose"
>
<div class="order-detail-container">
<el-tabs v-model="activeName">
<el-tab-pane
label="基本信息"
style="height: 100%"
name="first"
></el-tab-pane>
<el-tab-pane
label="流程管理"
style="height: 100%"
name="third"
></el-tab-pane>
</el-tabs>
<!-- 左侧:订单基本信息 -->
<div class="detailleft" v-if="activeName == 'first'">
<el-row :gutter="20">
<el-row :gutter="20">
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>订单基本信息</span>
</div>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="订单号">
{{ info.orderId || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="服务名称">
{{ info.productName || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="订单总价">
¥{{ (info.totalPrice || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="支付金额">
¥{{ (info.payPrice || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="抵扣金额">
¥{{ (info.deduction || 0).toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ info.createdAt || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="支付时间">
{{ info.payTime || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag
:type="getStatusTagType(info.status)"
size="small"
>
{{ getStatusLabel(info.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注说明" v-if="info.mark">
{{ info.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>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="联系姓名">
{{ info.adressjson.name }}
</el-descriptions-item>
<el-descriptions-item label="联系电话">
{{ info.adressjson.phone }}
</el-descriptions-item>
<el-descriptions-item label="详细位置">
{{ info.adressjson.info }}
</el-descriptions-item>
</el-descriptions>
<br />
<AddressSelector
:longitude="info.adressjson.longitude"
:latitude="info.adressjson.latitude"
:address="info.adressjson.addressName"
:disabled="true"
/>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 中间:师傅基本信息 -->
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>师傅基本信息</span>
</div>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="师傅姓名">
<div v-if="loadingWorkerInfo" class="loading-info">
<i class="el-icon-loading"></i> 加载中...
</div>
<span v-else-if="workerInfo">{{
workerInfo.name || "未设置"
}}</span>
<span v-else>{{ orderInfo.workerName || "未分配" }}</span>
</el-descriptions-item>
<el-descriptions-item label="师傅电话">
<div v-if="loadingWorkerInfo" class="loading-info">
<i class="el-icon-loading"></i> 加载中...
</div>
<span v-else-if="workerInfo">{{
workerInfo.phone || "未设置"
}}</span>
<span v-else>{{
orderInfo.workerPhone || "未设置"
}}</span>
</el-descriptions-item>
<el-descriptions-item label="师傅等级">
<div v-if="loadingWorkerInfo" class="loading-info">
<i class="el-icon-loading"></i> 加载中...
</div>
<span v-else-if="workerInfo">{{
getWorkerLevelLabel(workerInfo.level)
}}</span>
<span v-else>{{
orderInfo.workerLevel || "未设置"
}}</span>
</el-descriptions-item>
<!-- <el-descriptions-item label="服务区域">-->
<!-- <div v-if="loadingWorkerInfo" class="loading-info">-->
<!-- <i class="el-icon-loading"></i> 加载中...-->
<!-- </div>-->
<!-- <span v-else-if="workerInfo">{{ getWorkerAreaLabel(workerInfo.serviceCityIds) }}</span>-->
<!-- <span v-else>{{ orderInfo.workerArea || '未设置' }}</span>-->
<!-- </el-descriptions-item>-->
<!-- <el-descriptions-item label="技能标签">-->
<!-- <div v-if="loadingWorkerInfo" class="loading-info">-->
<!-- <i class="el-icon-loading"></i> 加载中...-->
<!-- </div>-->
<!-- <div v-else-if="workerInfo && workerInfo.skillIds" class="skills-tags">-->
<!-- <el-tag -->
<!-- v-for="skill in getWorkerSkillLabels(workerInfo.skillIds)" -->
<!-- :key="skill" -->
<!-- size="mini" -->
<!-- type="info"-->
<!-- style="margin: 2px;"-->
<!-- >-->
<!-- {{ skill }}-->
<!-- </el-tag>-->
<!-- </div>-->
<!-- <div v-else-if="orderInfo.workerSkills && orderInfo.workerSkills.length > 0" class="skills-tags">-->
<!-- <el-tag -->
<!-- v-for="skill in orderInfo.workerSkills" -->
<!-- :key="skill" -->
<!-- size="mini" -->
<!-- type="info"-->
<!-- style="margin: 2px;"-->
<!-- >-->
<!-- {{ skill }}-->
<!-- </el-tag>-->
<!-- </div>-->
<!-- <span v-else>未设置</span>-->
<!-- </el-descriptions-item>-->
<el-descriptions-item label="接单时间">
{{ orderInfo.receiveTime || "未接单" }}
</el-descriptions-item>
<!-- <el-descriptions-item label="派单时间">-->
<!-- {{ orderInfo.dispatchTime || '未派单' }}-->
<!-- </el-descriptions-item>-->
</el-descriptions>
<!-- 师傅头像 -->
<div
v-if="workerInfo && workerInfo.avatar"
class="worker-avatar-section"
>
<h4>师傅头像</h4>
<div class="worker-avatar">
<el-image
:src="workerInfo.avatar"
fit="cover"
class="avatar-image"
>
<div slot="error" class="avatar-slot">
<i class="el-icon-user"></i>
</div>
</el-image>
</div>
</div>
<!-- 占位区域,确保卡片高度一致 -->
<div v-else class="placeholder-section">
<div class="placeholder-content">
<i v-if="loadingWorkerInfo" class="el-icon-loading"></i>
<i v-else class="el-icon-user-solid"></i>
<p v-if="loadingWorkerInfo">加载中...</p>
<p v-else>暂无师傅信息</p>
</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>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="预约数量">
{{ orderInfo.num || "0" }}
</el-descriptions-item>
<el-descriptions-item label="预约时间">
{{ orderInfo.appointmentTime || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="服务类型">
<template v-if="info.odertype == 1"> 拼团订单 </template>
<template v-if="info.odertype == 2">
一口价订单
</template>
<template v-if="info.odertype == 3"> 秒杀订单 </template>
<template v-if="info.odertype == 4"> 报价订单 </template>
<template v-if="info.odertype == 0"> 预约订单 </template>
</el-descriptions-item>
<el-descriptions-item label="服务要求">
{{ orderInfo.serviceRequirement || "无特殊要求" }}
</el-descriptions-item>
<el-descriptions-item label="紧急程度">
<el-tag
:type="getUrgencyTagType(orderInfo.urgency)"
size="small"
>
{{ getUrgencyLabel(orderInfo.urgency) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预计时长">
{{ orderInfo.estimatedDuration || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="特殊说明">
{{ orderInfo.specialNote || "无" }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
</el-row>
</div>
<div class="detailright" v-if="activeName == 'third'">
<el-row :gutter="20" class="boxcontent">
<el-col :span="24">
<el-card class="box-card" shadow="hover">
<div>
<div
v-if="
orderInfo.receiveRecords &&
orderInfo.receiveRecords.length > 0
"
class="order-timeline"
>
<div class="timeline-container">
<div
v-for="(record, index) in orderInfo.receiveRecords"
:key="record.id || index"
class="timeline-item"
>
<!-- 时间轴节点 -->
<div
class="timeline-node"
:class="getTimelineNodeClass(record.title)"
>
<i :class="getTimelineIcon(record.title)"></i>
</div>
<!-- 时间轴内容 -->
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-title">{{
record.title || "未知操作"
}}</span>
<span class="timeline-time">{{
formatTimelineTime(record.createdAt)
}}</span>
</div>
<div class="timeline-body">
<div class="timeline-description">
{{ getContentDescription(record.content) }}
</div>
<!-- 显示报价数据 -->
<div
v-if="record.quote && isJsonString(record.quote)"
class="timeline-quote"
>
<div class="quote-header">
<i class="el-icon-money"></i>
<span>报价详情</span>
</div>
<div class="quote-content">
<pre>{{ formatJson(record.quote) }}</pre>
</div>
</div>
<!-- 显示图片 -->
<div
v-if="
record.contentImage &&
record.contentImage.length > 0
"
class="timeline-images"
>
<el-image
v-for="(img, imgIndex) in record.contentImage"
:key="imgIndex"
:src="img"
:preview-src-list="record.contentImage"
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.receiveRecords.length - 1
"
>
<!-- 根据最后一条日志的type显示不同操作 -->
<el-button
v-if="record.type === 1"
type="primary"
size="small"
@click="handleDispatchFromTimeline(record)"
>
<i class="el-icon-s-promotion"></i>
派单
</el-button>
<!-- type为1.1时显示更换师傅和接单按钮 -->
<el-button
v-if="record.type === 1.1"
type="warning"
size="small"
@click="handleChangeWorker(record)"
style="margin-right: 8px"
>
<i class="el-icon-refresh"></i>
更换师傅
</el-button>
<el-button
v-if="record.type === 1.1"
type="success"
size="small"
@click="handleAcceptOrder(record)"
>
<i class="el-icon-check"></i>
接单
</el-button>
<el-button
v-if="record.type === 2"
type="success"
size="small"
@click="handleDeparture(record)"
>
<i class="el-icon-location"></i>
出发上门
</el-button>
<el-button
v-if="record.type === 3"
type="warning"
size="small"
@click="handleConfirmArrival(record)"
>
<i class="el-icon-location-information"></i>
确认到达
</el-button>
<!-- type为4时显示三个按钮结束订单、开始服务、项目报价 -->
<div
v-if="record.type === 4"
class="timeline-actions-multiple"
>
<el-button
type="danger"
size="small"
@click="handleEndOrder(record)"
style="margin-right: 8px"
>
<i class="el-icon-close"></i>
结束订单
</el-button>
<el-button
type="success"
size="small"
@click="handleStartService(record)"
style="margin-right: 8px"
>
<i class="el-icon-video-play"></i>
开始服务
</el-button>
<el-button
type="primary"
size="small"
@click="handleProjectQuote(record)"
>
<i class="el-icon-money"></i>
项目报价
</el-button>
</div>
<el-button
type="primary"
size="small"
@click="handleProjectQuote(record)"
>
<i class="el-icon-money"></i>
项目报价
</el-button>
<el-button
v-if="record.type === 5"
type="success"
size="small"
@click="handleCompleteService(record)"
>
<i class="el-icon-circle-check"></i>
完成服务
</el-button>
</div>
</div>
</div>
<!-- 连接线(除了最后一个项目) -->
<div
v-if="index < orderInfo.receiveRecords.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>
</div>
</el-card>
</el-col>
</el-row>
<div class="fnbt">
<el-button
type="primary"
size="small"
@click="handleDispatchFromTimeline()"
>
<i class="el-icon-s-promotion"></i>
派单
</el-button>
<!-- type为1.1时显示更换师傅和接单按钮 -->
<el-button
type="warning"
size="small"
@click="handleChangeWorker()"
style="margin-right: 8px"
>
<i class="el-icon-refresh"></i>
更换师傅
</el-button>
<el-button type="success" size="small" @click="handleAcceptOrder()">
<i class="el-icon-check"></i>
接单
</el-button>
<el-button type="success" size="small" @click="handleDeparture()">
<i class="el-icon-location"></i>
出发上门
</el-button>
<el-button
type="warning"
size="small"
@click="handleConfirmArrival()"
>
<i class="el-icon-location-information"></i>
确认到达
</el-button>
<el-button
type="danger"
size="small"
@click="handleEndOrder()"
style="margin-right: 8px"
>
<i class="el-icon-close"></i>
结束订单
</el-button>
<el-button
type="success"
size="small"
@click="handleStartService()"
style="margin-right: 8px"
>
<i class="el-icon-video-play"></i>
开始服务
</el-button>
<el-button
type="primary"
size="small"
@click="handleProjectQuote()"
>
<i class="el-icon-money"></i>
项目报价
</el-button>
</div>
</div>
</div>
</el-drawer>
<!-- 结束订单 - 上门费输入弹窗 -->
<el-dialog
title="结束订单"
:visible.sync="endOrderDialogVisible"
width="420px"
append-to-body
@close="resetEndOrderForm"
>
<el-form
ref="endOrderFormRef"
:model="endOrderForm"
:rules="endOrderRules"
label-width="90px"
>
<el-form-item label="上门费" prop="fee">
<el-input-number
v-model.number="endOrderForm.fee"
:min="0"
:step="1"
placeholder="请输入上门费"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="endOrderDialogVisible = false">取 消</el-button>
<el-button
type="primary"
:loading="endOrderSubmitting"
@click="submitEndOrder"
>确 认</el-button
>
</div>
</el-dialog>
<!-- 更换师傅弹窗 -->
<el-dialog
title="更换师傅"
:visible.sync="changeWorkerDialogVisible"
width="800px"
append-to-body
@close="selectedWorker = null"
>
<!-- 当前师傅信息 -->
<div v-if="orderInfo.workerName" class="current-worker-banner">
<el-alert
:title="`当前师傅: ${orderInfo.workerName}`"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 已选择新师傅提示 -->
<div v-if="selectedWorker" class="selected-worker-banner">
<el-alert
:title="`已选择新师傅: ${selectedWorker.name}`"
type="success"
:closable="false"
show-icon
/>
</div>
<div class="change-worker-content">
<!-- 订单信息 -->
<div class="order-info-section">
<h4>订单信息</h4>
<el-row :gutter="20">
<el-col :span="12">
<div class="info-item">
<label>订单号:</label>
<span>{{ orderInfo.orderId }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label>客户姓名:</label>
<span>{{ orderInfo.userName }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label>联系电话:</label>
<span>{{ orderInfo.userPhone }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label>服务地址:</label>
<span>{{ orderInfo.appointmentAddress || "未设置" }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label>服务项目:</label>
<span>{{ orderInfo.productName }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<label>订单金额</label>
<span class="amount-highlight"
>{{ (orderInfo.payPrice || 0).toFixed(2) }}</span
>
</div>
</el-col>
</el-row>
</div>
<!-- 选择新师傅 -->
<div class="worker-selection-section">
<h4>选择新师傅</h4>
<!-- 搜索条件 -->
<el-form
:model="workerQueryParams"
:inline="true"
style="margin-bottom: 15px"
>
<el-form-item label="昵称">
<el-input
v-model="workerQueryParams.name"
placeholder="请输入师傅昵称"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="电话">
<el-input
v-model="workerQueryParams.phone"
placeholder="请输入联系电话"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="workerQueryParams.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="全部" value="" />
<el-option label="可用" value="1" />
<el-option label="不可用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="searchWorkers"
:loading="loadingWorkers"
>
<i class="el-icon-search"></i> 搜索
</el-button>
<el-button @click="resetWorkerSearch">
<i class="el-icon-refresh"></i> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 师傅列表 -->
<div class="worker-list">
<el-table
:data="availableWorkers"
v-loading="loadingWorkers"
style="width: 100%"
@row-click="selectWorker"
:row-class-name="getWorkerRowClass"
>
<el-table-column prop="name" label="师傅姓名" width="120">
<template slot-scope="scope">
<div class="worker-info">
<el-avatar
:size="32"
:src="scope.row.avatar"
:alt="scope.row.name"
>
<i class="el-icon-user"></i>
</el-avatar>
<span style="margin-left: 8px">{{ scope.row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column
prop="phonenumber"
label="联系电话"
width="120"
/>
<el-table-column prop="level" label="等级" width="80">
<template slot-scope="scope">
<el-tag :type="getLevelTagType(scope.row.level)" size="mini">
{{ getWorkerLevelLabel(scope.row.level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="serviceCityIds"
label="服务区域"
width="150"
>
<template slot-scope="scope">
<span>{{
getWorkerAreaLabel(scope.row.serviceCityIds)
}}</span>
</template>
</el-table-column>
<el-table-column prop="skillIds" label="技能标签" min-width="200">
<template slot-scope="scope">
<div class="skills-tags">
<el-tag
v-for="skill in getWorkerSkillLabels(scope.row.skillIds)"
:key="skill"
size="mini"
type="info"
style="margin: 2px"
>
{{ skill }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button
type="primary"
size="mini"
@click.stop="selectWorker(scope.row)"
:disabled="scope.row.id === orderInfo.workerId"
>
{{
scope.row.id === orderInfo.workerId ? "当前师傅" : "选择"
}}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="changeWorkerDialogVisible = false">取 消</el-button>
<el-button
type="primary"
@click="confirmChangeWorker"
:disabled="!selectedWorker"
>
确认更换
</el-button>
</div>
</el-dialog>
<!-- 开始服务 - 上传图片弹窗 -->
<el-dialog
title="开始服务"
:visible.sync="startServiceDialogVisible"
width="680px"
append-to-body
>
<el-form label-width="90px">
<el-form-item label="服务图片" required>
<ImageUpload
v-model="startServiceImages"
:limit="9"
:file-size="10"
/>
<div class="el-upload__tip" style="margin-top: 6px; color: #909399">
至少上传1张服务开始图片支持 jpg/png/jpeg单张 ≤ 10MB
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="startServiceDialogVisible = false"> </el-button>
<el-button
type="primary"
:loading="startServiceSubmitting"
@click="submitStartService"
> </el-button
>
</div>
</el-dialog>
</div>
</template>
<script>
import { getUsers, listUsers } from "@/api/system/users";
import { getOrder } from "@/api/system/Order";
import request from "@/utils/request";
import ImageUpload from "@/components/ImageUpload";
import AddressSelector from "@/components/AddressSelector";
import { js } from "js-beautify";
export default {
name: "OrderDetailDialog",
components: { ImageUpload, AddressSelector },
props: {
visible: {
type: Boolean,
default: false,
},
orderInfo: {
type: Object,
default: () => ({}),
},
},
data() {
return {
dialogVisible: false,
workerInfo: null, // 存储师傅信息
loadingWorkerInfo: false, // 加载师傅信息的状态
activeName: "first",
// 结束订单弹窗
endOrderDialogVisible: false,
endOrderSubmitting: false,
endOrderForm: { fee: 0 },
endOrderRules: {
fee: [{ required: true, message: "请输入上门费", trigger: "blur" }],
},
// 更换师傅相关数据
changeWorkerDialogVisible: false,
availableWorkers: [],
selectedWorker: null,
loadingWorkers: false,
workerQueryParams: {
pageNum: 1,
pageSize: 10,
name: "",
phone: "",
status: "",
},
// 开始服务弹窗
startServiceDialogVisible: false,
startServiceImages: [],
startServiceSubmitting: false,
info: {}, //订单详情
};
},
watch: {
visible: {
immediate: true,
handler(newVal) {
this.dialogVisible = newVal;
if (newVal && this.orderInfo.workerId) {
this.loadWorkerInfo();
}
},
},
orderInfo: {
immediate: true,
handler() {
this.getinfo();
},
},
"orderInfo.workerId": {
immediate: true,
handler(newWorkerId) {
if (newWorkerId && this.dialogVisible) {
this.loadWorkerInfo();
}
},
},
},
method() {},
methods: {
// 获取订单详情
getinfo() {
if (this.orderInfo.id) {
getOrder(this.orderInfo.id).then((r) => {
r.data.adressjson = JSON.parse(r.data.adressjson);
this.info = r.data;
});
}
},
/** 关闭对话框 */
handleClose() {
this.dialogVisible = false;
this.$emit("update:visible", false);
},
/** 加载师傅信息 */
async loadWorkerInfo() {
if (!this.orderInfo.workerId) {
this.workerInfo = null;
return;
}
try {
this.loadingWorkerInfo = true;
const response = await getUsers(this.orderInfo.workerId);
if (response.code === 200) {
this.workerInfo = response.data;
} else {
this.$message.error("获取师傅信息失败:" + response.msg);
this.workerInfo = null;
}
} catch (error) {
console.error("获取师傅信息失败:", error);
this.$message.error("获取师傅信息失败");
this.workerInfo = null;
} finally {
this.loadingWorkerInfo = false;
}
},
/** 获取状态标签类型 */
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}`;
},
/** 获取紧急程度标签类型 */
getUrgencyTagType(urgency) {
if (!urgency) return "info";
const urgencyMap = {
1: "info", // 普通
2: "warning", // 紧急
3: "danger", // 特急
};
return urgencyMap[urgency] || "info";
},
/** 获取紧急程度标签 */
getUrgencyLabel(urgency) {
if (!urgency) return "普通";
const urgencyMap = {
1: "普通",
2: "紧急",
3: "特急",
};
return urgencyMap[urgency] || "普通";
},
/** 获取时间轴节点样式类 */
getTimelineNodeClass(title) {
if (!title) return "timeline-node-default";
if (title.includes("订单生成") || title.includes("创建"))
return "timeline-node-primary";
if (
title.includes("支付成功") ||
title.includes("接单") ||
title.includes("派单")
)
return "timeline-node-success";
if (title.includes("出发") || title.includes("到达"))
return "timeline-node-warning";
if (title.includes("开始服务")) return "timeline-node-info";
if (title.includes("服务完成") || title.includes("完成"))
return "timeline-node-danger";
return "timeline-node-default";
},
/** 获取时间轴图标 */
getTimelineIcon(title) {
if (!title) return "el-icon-info";
if (title.includes("订单生成") || title.includes("创建"))
return "el-icon-plus";
if (
title.includes("支付成功") ||
title.includes("接单") ||
title.includes("派单")
)
return "el-icon-check";
if (title.includes("出发") || title.includes("到达"))
return "el-icon-location";
if (title.includes("开始服务")) return "el-icon-video-play";
if (title.includes("服务完成") || title.includes("完成"))
return "el-icon-circle-check";
return "el-icon-info";
},
/** 格式化时间轴时间 */
formatTimelineTime(time) {
if (!time) return "未知时间";
const date = new Date(time);
return date.toLocaleString();
},
/** 获取内容描述 */
getContentDescription(content) {
if (!content) return "无描述信息";
try {
const contentData = JSON.parse(content);
if (Array.isArray(contentData)) {
return contentData[0]?.name || content;
} else if (typeof contentData === "object") {
return contentData.name || content;
}
} catch (e) {
// 如果解析失败,直接返回原始内容
}
return content;
},
/** 判断是否为JSON字符串 */
isJsonString(str) {
if (typeof str !== "string") return false;
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
},
/** 格式化JSON显示 */
formatJson(jsonStr) {
try {
const obj = JSON.parse(jsonStr);
return JSON.stringify(obj, null, 2);
} catch (e) {
return jsonStr;
}
},
/** 获取文件列表 */
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");
},
/** 派单操作 */
handleDispatchFromTimeline(record) {
this.$emit("dispatch-order", record);
},
/** 更换师傅操作 */
handleChangeWorker(record) {
console.log("更换师傅按钮被点击,记录:", record);
// 打开更换师傅弹窗
this.changeWorkerDialogVisible = true;
// 加载可用的师傅列表
this.loadAvailableWorkers();
},
/** 接单操作 */
async handleAcceptOrder(record) {
try {
console.log("接单按钮被点击,记录:", record);
// 构建接单参数
const params = {
orderId: this.orderInfo.orderId,
workerId: this.orderInfo.workerId || null,
};
console.log("接单参数:", params);
// 调用接单API
const response = await request({
url: "/system/Order/accept-order",
method: "post",
data: params,
});
if (response.code === 200) {
this.$message.success("接单成功!");
// 刷新订单信息
this.$emit("refresh-order");
// 关闭订单明细弹窗
this.handleClose();
} else {
this.$message.error("接单失败:" + response.msg);
}
} catch (error) {
console.error("接单失败:", error);
this.$message.error("接单失败:" + (error.message || "未知错误"));
}
},
/** 出发上门操作 */
handleDeparture(record) {
this.$emit("departure", record);
},
/** 确认到达操作 */
async handleConfirmArrival(record) {
try {
this.$modal
.confirm("确认要执行确认到达操作吗?")
.then(async () => {
// 调用确认到达API
const response = await request.post(
"/system/Order/confirm-arrival",
{
orderId: this.orderInfo.orderId,
}
);
if (response.code === 200) {
this.$message.success("确认到达成功!");
// 刷新订单信息
this.$emit("refresh-order");
// 关闭订单明细弹窗
this.handleClose();
} else {
this.$message.error("确认到达失败:" + response.msg);
}
})
.catch(() => {
// 用户取消操作
});
} catch (error) {
console.error("确认到达失败:", error);
this.$message.error("确认到达失败:" + (error.message || "未知错误"));
}
},
/** 结束订单操作 */
handleEndOrder(record) {
this.endOrderDialogVisible = true;
},
/** 开始服务操作 */
handleStartService(record) {
this.startServiceImages = [];
this.startServiceDialogVisible = true;
},
/** 提交开始服务 */
async submitStartService() {
if (!this.startServiceImages || this.startServiceImages.length === 0) {
this.$message.warning("请至少上传一张服务图片");
return;
}
try {
this.startServiceSubmitting = true;
const response = await request({
url: "/system/Order/start-service",
method: "post",
data: {
orderId: this.orderInfo.orderId,
image: this.startServiceImages,
},
});
if (response.code === 200) {
this.$message.success("开始服务成功!");
this.startServiceDialogVisible = false;
this.startServiceImages = [];
this.$emit("refresh-order");
} else {
this.$message.error("开始服务失败:" + (response.msg || "未知错误"));
}
} catch (error) {
this.$message.error("开始服务失败:" + (error.message || "网络错误"));
} finally {
this.startServiceSubmitting = false;
}
},
/** 项目报价操作 */
handleProjectQuote(record) {
this.$emit("project-quote", record);
},
/** 完成服务操作 */
handleCompleteService(record) {
this.$emit("complete-service", record);
},
/** 获取师傅等级标签 */
getWorkerLevelLabel(level) {
if (!level) return "未设置";
const levelMap = {
1: "初级",
2: "中级",
3: "高级",
4: "专家",
};
return levelMap[level] || `等级${level}`;
},
/** 获取师傅服务区域标签 */
getWorkerAreaLabel(city) {
if (!city) return "未设置";
try {
const cityData = JSON.parse(city);
if (Array.isArray(cityData)) {
return cityData.map((item) => item.name || item).join(", ");
} else if (typeof cityData === "object") {
return cityData.name || city;
}
} catch (e) {
// 如果解析失败,直接返回原始内容
}
return city;
},
/** 重置结束订单表单 */
resetEndOrderForm() {
this.endOrderForm = { fee: 0 };
this.endOrderSubmitting = false;
},
/** 提交结束订单 */
submitEndOrder() {
this.$refs.endOrderFormRef &&
this.$refs.endOrderFormRef.validate(async (valid) => {
if (!valid) return;
try {
this.endOrderSubmitting = true;
const response = await request({
url: "/system/Order/end-order",
method: "post",
data: {
orderId: this.orderInfo.orderId,
doorFee: this.endOrderForm.fee,
},
});
if (response.code === 200) {
this.$message.success("订单已结束");
this.$emit("refresh-order");
this.endOrderDialogVisible = false;
this.resetEndOrderForm();
this.handleClose();
} else {
this.$message.error(
"结束订单失败:" + (response.msg || "未知错误")
);
}
} catch (e) {
this.$message.error("结束订单失败:" + (e.message || "网络错误"));
} finally {
this.endOrderSubmitting = false;
}
});
},
/** 获取师傅技能标签 */
getWorkerSkillLabels(skillIds) {
if (!skillIds) return [];
try {
const skills = JSON.parse(skillIds);
if (Array.isArray(skills)) {
return skills.map((skill) => skill.name || skill).filter(Boolean);
}
} catch (e) {
// 如果解析失败,直接返回原始内容
}
return [];
},
/** 加载可用的师傅列表 */
async loadAvailableWorkers() {
try {
this.loadingWorkers = true;
// 构建查询参数
const params = {
pageNum: this.workerQueryParams.pageNum,
pageSize: this.workerQueryParams.pageSize,
};
// 只有当有值时才添加搜索条件
if (this.workerQueryParams.name) {
params.name = this.workerQueryParams.name;
}
if (this.workerQueryParams.phone) {
params.phone = this.workerQueryParams.phone;
}
if (this.workerQueryParams.status) {
params.status = this.workerQueryParams.status;
}
console.log("调用getUsers API参数:", params);
// 调用获取可用师傅列表的API
const response = await listUsers(params);
if (response.code === 200) {
this.availableWorkers = response.rows || [];
console.log("获取师傅列表成功,数量:", this.availableWorkers.length);
} else {
this.$message.error("获取师傅列表失败:" + response.msg);
this.availableWorkers = [];
}
} catch (error) {
console.error("获取师傅列表失败:", error);
this.$message.error(
"获取师傅列表失败:" + (error.message || "未知错误")
);
this.availableWorkers = [];
} finally {
this.loadingWorkers = false;
}
},
/** 搜索师傅 */
searchWorkers() {
this.workerQueryParams.pageNum = 1;
this.loadAvailableWorkers();
},
/** 重置师傅搜索 */
resetWorkerSearch() {
this.workerQueryParams = {
pageNum: 1,
pageSize: 10,
name: "",
phone: "",
status: "",
};
this.loadAvailableWorkers();
},
/** 选择师傅 */
selectWorker(worker) {
this.selectedWorker = worker;
this.$message.success(`已选择师傅:${worker.name}`);
},
/** 确认更换师傅 */
confirmChangeWorker() {
if (!this.selectedWorker) {
this.$message.warning("请先选择要更换的师傅");
return;
}
if (this.selectedWorker.id === this.orderInfo.workerId) {
this.$message.warning("不能选择当前已分配的师傅");
return;
}
this.$modal
.confirm(`确认将订单更换给师傅 ${this.selectedWorker.name} 吗?`)
.then(() => {
// 这里调用更换师傅的API
this.processChangeWorker();
})
.catch(() => {
// 用户取消
});
},
/** 处理更换师傅 */
async processChangeWorker() {
try {
// 这里需要调用更换师傅的API
// 您需要根据实际情况实现这个API调用
this.$message.success("更换师傅成功!");
this.changeWorkerDialogVisible = false;
this.selectedWorker = null;
// 刷新师傅信息
if (this.orderInfo.workerId) {
this.loadWorkerInfo();
}
// 通知父组件刷新数据
this.$emit("refresh-order");
} catch (error) {
console.error("更换师傅失败:", error);
this.$message.error("更换师傅失败:" + (error.message || "未知错误"));
}
},
/** 获取等级标签类型 */
getLevelTagType(level) {
if (!level) return "info";
const levelMap = {
1: "info", // 初级
2: "success", // 中级
3: "warning", // 高级
4: "danger", // 专家
};
return levelMap[level] || "info";
},
/** 获取师傅行样式类 */
getWorkerRowClass({ row }) {
if (row.id === this.orderInfo.workerId) {
return "current-worker-row";
}
if (row.id === this.selectedWorker?.id) {
return "selected-worker-row";
}
return "";
},
},
};
</script>
<style lang="scss" scoped>
.detailleft {
height: calc(100% - 54px);
overflow-y: auto;
overflow-x: hidden;
}
.detailright {
height: calc(100% - 54px);
.boxcontent {
height: calc(100% - 60px);
overflow-y: auto;
}
.fnbt {
display: flex;
height: 60px;
align-items: center;
background: #fff;
border-top: 1px solid #eee;
margin: 0px -20px -20px -20px;
box-shadow: 0px 0px 10px 5px rgba(0,0,0,.1);
padding: 0px 20px;
position: relative;
bottom: -20px;
justify-content: flex-end;
}
}
.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 {
// 确保三个卡片在同一行时高度一致
height: 100%;
.el-col {
display: flex;
.el-card {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 20px;
min-height: 0; // 重要允许flex子项收缩
.el-card__header {
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
flex-shrink: 0; // 防止header收缩
.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;
flex: 1; // 占据剩余空间
display: flex;
flex-direction: column;
min-height: 0; // 允许收缩
}
}
}
// 其他卡片样式
.el-row:not(:first-child) {
.el-card {
margin-bottom: 20px;
}
}
// 强制三个卡片高度一致
.el-row:first-child {
.el-col {
.el-card {
height: auto !important;
min-height: 400px; // 设置最小高度确保一致性
}
}
}
}
.attachment-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 120px; // 确保最小高度
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: 100%;
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;
}
.timeline-actions-multiple {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 报价详情样式 */
.timeline-quote {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.quote-header {
display: flex;
align-items: center;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}
.quote-header i {
margin-right: 6px;
color: #28a745;
}
.quote-content {
background: white;
padding: 8px;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.quote-content pre {
margin: 0;
font-size: 12px;
color: #6c757d;
white-space: pre-wrap;
word-break: break-word;
}
/* 技能标签样式 */
.skills-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 师傅头像样式 */
.worker-avatar-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 120px; // 确保最小高度
h4 {
margin: 0 0 15px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
.worker-avatar {
display: flex;
justify-content: center;
.avatar-image {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid #e4e7ed;
cursor: pointer;
transition: border-color 0.2s ease;
&:hover {
border-color: #409eff;
}
}
.avatar-slot {
width: 80px;
height: 80px;
border-radius: 50%;
background: #f5f7fa;
border: 3px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #909399;
}
}
}
/* 占位区域样式 */
.placeholder-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 120px; // 确保最小高度
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 10px;
display: block;
}
p {
margin: 0;
font-size: 14px;
}
}
}
/* 加载状态样式 */
.loading-info {
display: flex;
align-items: center;
color: #909399;
font-size: 14px;
i {
margin-right: 6px;
font-size: 16px;
}
}
/* 响应式设计 */
@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%);
}
}
/* 更换师傅弹窗样式 */
.current-worker-banner,
.selected-worker-banner {
margin-bottom: 20px;
}
.change-worker-content {
.order-info-section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
h4 {
margin: 0 0 15px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 12px;
label {
font-weight: 600;
color: #606266;
width: 80px;
flex-shrink: 0;
}
span {
color: #303133;
flex: 1;
}
.amount-highlight {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
.worker-selection-section {
h4 {
margin: 0 0 15px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.worker-list {
.worker-info {
display: flex;
align-items: center;
}
.skills-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
}
}
}
/* 师傅行样式 */
.current-worker-row {
background-color: #f0f9ff !important;
.el-button {
background-color: #909399;
border-color: #909399;
color: white;
cursor: not-allowed;
}
}
.selected-worker-row {
background-color: #f0f9ff !important;
border: 2px solid #409eff;
}
</style>