diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue index c4b5d4d..ce79c8c 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -90,7 +90,19 @@ {{ ticketInfo?.username?.charAt(0) || 'U' }}
-
{{ message.content }}
+
+ {{ message.content }} + + + + + +
@@ -179,6 +190,47 @@
+ + + + + + + + +
+
+ 图片 +
×
+
+ + +
+ +
添加图片
+
+
+
+
+
+ +
+ @@ -190,7 +242,8 @@ import { ref, onMounted, nextTick, onBeforeUnmount, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { getTicketDetail, replyTicket, closeTicket, updateTicketInfo } from '@/api/ticket' +import { getTicketDetail, replyTicket, closeTicket, updateTicketInfo, updateTicketReplayInfo } from '@/api/ticket' +import { Plus } from '@element-plus/icons-vue' import { getUserInfo } from '@/api/admin/user' import { uploadFile } from '@/api/admin/file' import { useUserStore } from '@/store/userStore' @@ -223,6 +276,15 @@ const isDragOver = ref(false) const imageViewerVisible = ref(false) const currentViewImage = ref('') +// 编辑消息 +const editDialogVisible = ref(false) +const editMessageContent = ref('') +const editMessageImages = ref([]) +const editMessageFiles = ref([]) +const editOriginalFileIds = ref([]) // 保存原始文件ID +const editingMessage = ref(null) +const isEditingSaving = ref(false) + // 定时刷新 const refreshTimer = ref(null) @@ -317,6 +379,7 @@ const fetchTicketDetail = async (showLoading = true) => { id: msg.id, content: msg.content !== 'empty' ? msg.content : null, images: msg.flies ? msg.flies.map(file => file.url) : [], + fileIds: msg.flies ? msg.flies.map(file => file.id) : [], // 保存文件ID time: msg.created_at || msg.updated_at || new Date().toLocaleString(), isAdmin: isAdmin(msg.user?.userId), userId: msg.user?.userId, @@ -593,6 +656,138 @@ const openImage = (img) => { imageViewerVisible.value = true } +// 编辑消息 +const handleEditMessage = (message) => { + editingMessage.value = message + editMessageContent.value = message.content || '' + editMessageImages.value = message.images ? [...message.images] : [] + editOriginalFileIds.value = message.fileIds ? [...message.fileIds] : [] + editMessageFiles.value = [] + editDialogVisible.value = true +} + +// 处理编辑对话框中的文件选择 +const handleEditFileChange = (file) => { + if (!file) return + + // 验证文件类型 + if (!file.raw.type.startsWith('image/')) { + ElMessage.warning('只支持图片格式') + return + } + + // 验证文件大小(限制10MB) + if (file.raw.size > 10 * 1024 * 1024) { + ElMessage.warning('图片大小不能超过10MB') + return + } + + // 保存原始文件对象用于上传 + editMessageFiles.value.push(file.raw) + + // 读取文件用于预览 + const reader = new FileReader() + reader.onload = (e) => editMessageImages.value.push(e.target.result) + reader.readAsDataURL(file.raw) +} + +// 删除编辑对话框中的图片 +const removeEditImage = (index) => { + const originalImagesCount = editingMessage.value?.images?.length || 0 + + // 如果删除的是原始图片,从原始文件ID列表中删除 + if (index < originalImagesCount) { + editOriginalFileIds.value.splice(index, 1) + } else { + // 如果删除的是新添加的图片,从新文件列表中删除 + const fileIndex = index - originalImagesCount + if (fileIndex >= 0 && fileIndex < editMessageFiles.value.length) { + editMessageFiles.value.splice(fileIndex, 1) + } + } + + // 从预览列表中删除 + editMessageImages.value.splice(index, 1) +} + +// 保存编辑的消息 +const saveEditMessage = async () => { + if (!editMessageContent.value.trim()) { + ElMessage.warning('消息内容不能为空') + return + } + + if (!editingMessage.value || !editingMessage.value.id) { + ElMessage.error('无法获取消息ID') + return + } + + try { + isEditingSaving.value = true + + // 如果有新添加的图片,先上传 + let newFileIds = [] + if (editMessageFiles.value.length > 0) { + try { + const formData = new FormData() + + editMessageFiles.value.forEach((file) => { + formData.append('files', file) + formData.append('file_names', file.name) + }) + + formData.append('update_type', 'work_order') + formData.append('open_down', 'true') + + const uploadRes = await uploadFile(formData) + + if (uploadRes.data?.code === 200) { + const data = uploadRes.data.data + if (Array.isArray(data)) { + newFileIds = data.map(item => String(item.id)) + } else if (data.id) { + newFileIds = [String(data.id)] + } + } else { + ElMessage.error(uploadRes.data?.message || '图片上传失败') + isEditingSaving.value = false + return + } + } catch (error) { + console.error('图片上传失败:', error) + ElMessage.error('图片上传失败,请重试') + isEditingSaving.value = false + return + } + } + + // 合并原始文件ID和新上传的文件ID + const allFileIds = [...editOriginalFileIds.value, ...newFileIds] + + const formData = new FormData() + formData.append('id', editingMessage.value.id) + formData.append('content', editMessageContent.value.trim()) + formData.append('files', allFileIds.join(',')) + + const res = await updateTicketReplayInfo(formData) + + if (res.code === 200) { + ElMessage.success('消息已更新') + editDialogVisible.value = false + + // 重新获取工单详情以确保数据同步 + await fetchTicketDetail(false) + } else { + ElMessage.error(res.message || '更新失败') + } + } catch (error) { + console.error('更新消息出错:', error) + ElMessage.error('网络错误,请稍后重试') + } finally { + isEditingSaving.value = false + } +} + // 快捷回复 const useQuickReply = (reply) => { messageInput.value = reply.content @@ -899,6 +1094,29 @@ onBeforeUnmount(() => { border-radius: 8px; word-break: break-word; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + position: relative; +} + +.message-text .edit-btn { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + padding: 4px; + cursor: pointer; + opacity: 0.6; + transition: all 0.2s; + border-radius: 4px; + vertical-align: middle; +} + +.message-text .edit-btn:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.1); +} + +.message-text:hover .edit-btn { + opacity: 1; } .message-admin .message-text { @@ -906,6 +1124,16 @@ onBeforeUnmount(() => { color: #fff; } +.message-admin .message-text .edit-btn { + color: #fff; + opacity: 0.7; +} + +.message-admin .message-text .edit-btn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.2); +} + .message-images { display: flex; flex-wrap: wrap; @@ -1028,4 +1256,67 @@ onBeforeUnmount(() => { background: #fff; border-top: 1px solid #ebeef5; } + +.edit-images-container { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.edit-preview-item { + position: relative; + width: 100px; + height: 100px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #dcdfe6; +} + +.edit-preview-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.edit-preview-item .delete-preview { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + background: #f56c6c; + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 18px; + line-height: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.add-image-btn { + width: 100px; + height: 100px; + border: 2px dashed #dcdfe6; + border-radius: 4px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + color: #909399; +} + +.add-image-btn:hover { + border-color: #409eff; + color: #409eff; +} + +.add-image-btn .add-text { + font-size: 12px; + margin-top: 4px; +}