工单详情
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
.chat-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #4b87ff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
|
||||
.separator {
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
padding: 0 24px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #4b87ff;
|
||||
border-bottom-color: #4b87ff;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
height: 420px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.message-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-ai {
|
||||
background: #a855f7;
|
||||
}
|
||||
|
||||
.avatar-guest {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-meta-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-bubble-left {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #374151;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.message-bubble-right {
|
||||
background: #4b87ff;
|
||||
color: #fff;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.summary-container {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
.summary-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="chat-message-container">
|
||||
<!-- 头部信息 -->
|
||||
<header class="chat-header">
|
||||
<div class="header-content">
|
||||
<div class="header-title">{{ chatRoom?.workcaseId || '' }} 对话详情</div>
|
||||
<div class="header-subtitle">
|
||||
<span>客户:{{ chatRoom?.guestName || '未知' }}</span>
|
||||
<span class="separator"> </span>
|
||||
<span>聊天室:{{ chatRoom?.roomName || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="tabs-container">
|
||||
<div class="tabs-wrapper">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'record' }"
|
||||
@click="activeTab = 'record'"
|
||||
>
|
||||
对话记录
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'summary' }"
|
||||
@click="activeTab = 'summary'"
|
||||
>
|
||||
对话纪要
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="chat-main">
|
||||
<!-- 对话记录 -->
|
||||
<div v-if="activeTab === 'record'" class="messages-container">
|
||||
<div class="messages-list">
|
||||
<div v-for="message in messages" :key="message.messageId"
|
||||
class="message-item"
|
||||
:class="message.senderType === 'guest' ? 'message-right' : 'message-left'"
|
||||
>
|
||||
<!-- 客服/AI消息(左侧) -->
|
||||
<template v-if="message.senderType !== 'guest'">
|
||||
<div class="avatar avatar-ai">{{ getAvatarText(message) }}</div>
|
||||
<div class="message-content">
|
||||
<div class="message-meta">{{ message.senderName || '小电' }} {{ formatTime(message.sendTime) }}</div>
|
||||
<div class="message-bubble message-bubble-left">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 客户消息(右侧) -->
|
||||
<template v-else>
|
||||
<div class="message-content">
|
||||
<div class="message-meta message-meta-right">{{ message.senderName || '客户' }} {{ formatTime(message.sendTime) }}</div>
|
||||
<div class="message-bubble message-bubble-right">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar avatar-guest">{{ getAvatarText(message) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="messages.length === 0" class="empty-state">
|
||||
<div class="empty-text">暂无对话记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话纪要 -->
|
||||
<div v-else class="summary-container">
|
||||
<div class="summary-content">
|
||||
<div class="summary-section">
|
||||
<div class="summary-title">问题概述</div>
|
||||
<div class="summary-text">
|
||||
{{ summary.overview || '暂无概述' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-section">
|
||||
<div class="summary-title">客户诉求</div>
|
||||
<div class="summary-text">
|
||||
{{ summary.demand || '暂无诉求' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom'
|
||||
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
||||
|
||||
interface Summary {
|
||||
overview?: string
|
||||
demand?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
chatRoom?: ChatRoomVO
|
||||
roomId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
chatRoom: undefined,
|
||||
roomId: ''
|
||||
})
|
||||
|
||||
const activeTab = ref<'record' | 'summary'>('record')
|
||||
const messages = ref<ChatRoomMessageVO[]>([])
|
||||
const summary = ref<Summary>({
|
||||
overview: '',
|
||||
demand: ''
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr?: string) => {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 获取头像文本
|
||||
const getAvatarText = (message: ChatRoomMessageVO) => {
|
||||
if (message.senderType === 'guest') {
|
||||
return message.senderName?.charAt(0) || '客'
|
||||
}
|
||||
return message.senderName?.charAt(0) || '电'
|
||||
}
|
||||
|
||||
// 加载消息数据
|
||||
const loadMessages = async () => {
|
||||
const targetRoomId = props.roomId || props.chatRoom?.roomId
|
||||
if (!targetRoomId) return
|
||||
|
||||
try {
|
||||
const res = await workcaseChatAPI.getChatMessagePage({
|
||||
filter: { roomId: targetRoomId },
|
||||
pageParam: { page: 1, pageSize: 100 }
|
||||
})
|
||||
|
||||
if (res.success && res.dataList) {
|
||||
// 后端降序返回,需要反转
|
||||
messages.value = [...res.dataList].reverse()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成对话纪要(可以后续接入AI生成)
|
||||
const generateSummary = () => {
|
||||
if (messages.value.length === 0) return
|
||||
|
||||
// 简单提取第一条和最后一条消息作为概述
|
||||
const firstMsg = messages.value[0]
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
|
||||
summary.value = {
|
||||
overview: `客户反馈:${firstMsg.content?.substring(0, 50) || ''}`,
|
||||
demand: lastMsg.content?.substring(0, 100) || ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMessages()
|
||||
generateSummary()
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import url("./ChatMessage.scss");
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export {default as ChatMessage} from './ChatMessage/ChatMessage.vue'
|
||||
@@ -0,0 +1,325 @@
|
||||
.workcase-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workcase-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.workcase-id {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
.status-badge,
|
||||
.urgency-badge {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.urgency-normal {
|
||||
background: #fed7aa;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
.urgency-emergency {
|
||||
background: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.view-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: #4b87ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #3b77ef;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 3fr) minmax(0, 1fr) minmax(0, 3fr);
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-label {
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table-value {
|
||||
padding: 12px;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.table-value-full {
|
||||
grid-column: 2 / 5;
|
||||
}
|
||||
|
||||
// 铭牌照片
|
||||
.nameplate-photo {
|
||||
max-width: 400px;
|
||||
height: 200px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.photos-section,
|
||||
.timeline-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #a855f7;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.photos-grid {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
width: 128px;
|
||||
height: 96px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-upload {
|
||||
width: 128px;
|
||||
height: 96px;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #4b87ff;
|
||||
color: #4b87ff;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
position: relative;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
bottom: 16px;
|
||||
width: 2px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
padding-bottom: 32px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.timeline-dot-system {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
.timeline-dot-manager {
|
||||
background: #fb923c;
|
||||
}
|
||||
|
||||
.timeline-dot-engineer {
|
||||
background: #34d399;
|
||||
}
|
||||
|
||||
.timeline-body {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timeline-actor {
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.timeline-action {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,319 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="workcase-detail">
|
||||
<!-- Header -->
|
||||
<header class="detail-header">
|
||||
<!-- Left Part -->
|
||||
<div class="header-left">
|
||||
<div class="workcase-info">
|
||||
<span class="workcase-id">{{ formData.workcaseId || '新建工单' }}</span>
|
||||
<span v-if="mode === 'view' && formData.status" class="status-badge" :class="statusClass(formData.status)">
|
||||
{{ statusLabel(formData.status) }}
|
||||
</span>
|
||||
<span v-if="mode === 'view' && formData.emergency" class="urgency-badge" :class="urgencyClass(formData.emergency)">
|
||||
{{ urgencyLabel(formData.emergency) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Part -->
|
||||
<div class="header-right">
|
||||
<button v-if="mode === 'view' && formData.workcaseId" class="view-chat-btn" @click="handleViewChat">
|
||||
<MessageSquare :size="14" />
|
||||
查看对话
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="detail-main">
|
||||
<!-- 工单信息表格 -->
|
||||
<div class="info-table">
|
||||
<!-- 客户姓名 & 联系电话 -->
|
||||
<div class="table-row">
|
||||
<div class="table-label">客户姓名</div>
|
||||
<div class="table-value">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.username" placeholder="请输入客户姓名" size="small" />
|
||||
<span v-else>{{ formData.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="table-label">联系电话</div>
|
||||
<div class="table-value">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.phone" placeholder="请输入联系电话" size="small" />
|
||||
<span v-else>{{ formData.phone || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备名称 & 故障类型 -->
|
||||
<div class="table-row">
|
||||
<div class="table-label">设备名称</div>
|
||||
<div class="table-value">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.device" placeholder="请输入设备名称" size="small" />
|
||||
<span v-else>{{ formData.device || '-' }}</span>
|
||||
</div>
|
||||
<div class="table-label">故障类型</div>
|
||||
<div class="table-value">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.type" placeholder="请输入故障类型" size="small" />
|
||||
<span v-else>{{ formData.type || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 现场地址 -->
|
||||
<div class="table-row">
|
||||
<div class="table-label">现场地址</div>
|
||||
<div class="table-value table-value-full">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.address" placeholder="请输入现场地址" size="small" />
|
||||
<span v-else>{{ formData.address || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 故障描述 -->
|
||||
<div class="table-row">
|
||||
<div class="table-label">故障描述</div>
|
||||
<div class="table-value table-value-full">
|
||||
<ElInput
|
||||
v-if="mode !== 'view'"
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请详细描述故障现象"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else>{{ formData.description || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备铭牌 -->
|
||||
<div class="table-row">
|
||||
<div class="table-label">设备铭牌</div>
|
||||
<div class="table-value table-value-full">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.deviceNamePlate" placeholder="请输入设备铭牌" size="small" />
|
||||
<span v-else>{{ formData.deviceNamePlate || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 铭牌照片 -->
|
||||
<div class="table-row" v-if="formData.deviceNamePlateImg">
|
||||
<div class="table-label">铭牌照片</div>
|
||||
<div class="table-value table-value-full">
|
||||
<div class="nameplate-photo" @click="previewNameplateImage">
|
||||
<img :src="formData.deviceNamePlateImg" alt="设备铭牌" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 紧急程度 -->
|
||||
<div class="table-row" v-if="mode !== 'view'">
|
||||
<div class="table-label">紧急程度</div>
|
||||
<div class="table-value table-value-full">
|
||||
<ElSelect v-model="formData.emergency" placeholder="请选择紧急程度" size="small">
|
||||
<ElOption label="普通" value="normal" />
|
||||
<ElOption label="紧急" value="emergency" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处理人 (仅查看模式) -->
|
||||
<div class="table-row" v-if="mode === 'view'">
|
||||
<div class="table-label">处理人</div>
|
||||
<div class="table-value">
|
||||
<span>{{ formData.processorName || '未指派' }}</span>
|
||||
</div>
|
||||
<div class="table-label">创建时间</div>
|
||||
<div class="table-value">
|
||||
<span>{{ formData.createTime || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 故障照片 -->
|
||||
<div class="photos-section" v-if="mode !== 'create' || formData.imgs?.length">
|
||||
<div class="section-title">
|
||||
<ImageIcon :size="18" class="title-icon" />
|
||||
故障照片
|
||||
</div>
|
||||
<div class="photos-grid">
|
||||
<div v-for="(img, index) in formData.imgs" :key="index" class="photo-item">
|
||||
<img :src="img" alt="故障照片" />
|
||||
</div>
|
||||
<div v-if="mode !== 'view'" class="photo-upload">
|
||||
<Plus :size="32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处理记录 (仅查看模式) -->
|
||||
<div class="timeline-section" v-if="mode === 'view' && timeline.length">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
处理记录
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-line"></div>
|
||||
<div v-for="(item, index) in timeline" :key="index" class="timeline-item">
|
||||
<div class="timeline-dot" :class="`timeline-dot-${item.status}`"></div>
|
||||
<div class="timeline-body">
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-actor">{{ item.title.split(' ')[0] }}</span>
|
||||
<span class="timeline-action">{{ item.title.split(' ').slice(1).join(' ') }}</span>
|
||||
</div>
|
||||
<div class="timeline-desc">{{ item.desc }}</div>
|
||||
<div class="timeline-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer Actions -->
|
||||
<footer class="detail-footer">
|
||||
<ElButton @click="handleCancel">{{ mode === 'view' ? '关闭' : '取消' }}</ElButton>
|
||||
<ElButton v-if="mode === 'create'" type="primary" @click="handleSubmit">创建工单</ElButton>
|
||||
<ElButton v-if="mode === 'edit'" type="primary" @click="handleSubmit">保存修改</ElButton>
|
||||
<ElButton v-if="mode === 'view' && formData.status === 'pending'" type="warning" @click="handleAssign">指派工程师</ElButton>
|
||||
<ElButton v-if="mode === 'view' && formData.status === 'processing'" type="success" @click="handleComplete">完成工单</ElButton>
|
||||
</footer>
|
||||
|
||||
<!-- ChatMessage Dialog -->
|
||||
<ElDialog v-model="showChatMessage" title="对话详情" width="800px" class="chat-dialog">
|
||||
<ChatMessage v-if="showChatMessage" :room-id="currentRoomId" />
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ChatMessage } from '@/views/public/ChatRoom/'
|
||||
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage } from 'element-plus'
|
||||
import { MessageSquare, ImageIcon as ImageIcon, Plus } from 'lucide-vue-next'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase/workcase'
|
||||
|
||||
interface TimelineItem {
|
||||
status: 'system' | 'manager' | 'engineer'
|
||||
title: string
|
||||
desc: string
|
||||
time: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mode?: 'view' | 'edit' | 'create'
|
||||
workcase?: TbWorkcaseDTO
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view',
|
||||
workcase: () => ({} as TbWorkcaseDTO)
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
submit: [data: TbWorkcaseDTO]
|
||||
assign: [workcaseId: string]
|
||||
complete: [workcaseId: string]
|
||||
}>()
|
||||
|
||||
const formData = ref<TbWorkcaseDTO>({
|
||||
...props.workcase
|
||||
})
|
||||
|
||||
const showChatMessage = ref(false)
|
||||
const currentRoomId = ref<string>('')
|
||||
const timeline = ref<TimelineItem[]>([
|
||||
{
|
||||
status: 'system',
|
||||
title: '系统 工单创建',
|
||||
desc: '客户通过小电对话提交',
|
||||
time: ''
|
||||
}
|
||||
])
|
||||
|
||||
// 根据 workcaseId 获取聊天室ID
|
||||
const loadChatRoom = async () => {
|
||||
if (!formData.value.workcaseId) return
|
||||
|
||||
try {
|
||||
// TODO: 调用 API 根据 workcaseId 查询聊天室
|
||||
// const res = await workcaseChatAPI.getChatRoomByWorkcaseId(formData.value.workcaseId)
|
||||
// if (res.success && res.data) {
|
||||
// currentRoomId.value = res.data.roomId || ''
|
||||
// }
|
||||
|
||||
// 临时:假设 roomId 和 workcaseId 相关联
|
||||
console.log('需要根据 workcaseId 查询聊天室:', formData.value.workcaseId)
|
||||
} catch (error) {
|
||||
console.error('加载聊天室失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.workcase, (newVal) => {
|
||||
formData.value = { ...newVal }
|
||||
}, { deep: true })
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
done: '已完成'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const statusClass = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'status-pending',
|
||||
processing: 'status-processing',
|
||||
done: 'status-done'
|
||||
}
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const urgencyLabel = (urgency: string) => {
|
||||
const map: Record<string, string> = {
|
||||
normal: '普通',
|
||||
emergency: '紧急'
|
||||
}
|
||||
return map[urgency] || urgency
|
||||
}
|
||||
|
||||
const urgencyClass = (urgency: string) => {
|
||||
const map: Record<string, string> = {
|
||||
normal: 'urgency-normal',
|
||||
emergency: 'urgency-emergency'
|
||||
}
|
||||
return map[urgency] || ''
|
||||
}
|
||||
|
||||
const handleViewChat = () => {
|
||||
showChatMessage.value = true
|
||||
}
|
||||
|
||||
const previewNameplateImage = () => {
|
||||
if (!formData.value.deviceNamePlateImg) return
|
||||
// TODO: 使用图片预览组件
|
||||
window.open(formData.value.deviceNamePlateImg, '_blank')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (props.mode === 'create' || props.mode === 'edit') {
|
||||
if (!formData.value.username || !formData.value.phone) {
|
||||
ElMessage.warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
emit('submit', formData.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssign = () => {
|
||||
emit('assign', formData.value.workcaseId!)
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
emit('complete', formData.value.workcaseId!)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url("./WorkcaseDetail.scss");
|
||||
</style>
|
||||
Reference in New Issue
Block a user