diff --git a/src/api/ticket.js b/src/api/ticket.js index 47f9dbd..7ea0668 100644 --- a/src/api/ticket.js +++ b/src/api/ticket.js @@ -5,8 +5,13 @@ import request from "@/utils/request.js"; * @returns {Promise} */ -export function getTickerList(count, page, status) { - return request.get('/api/v1/admin/work_order/list', { count, page, status }) +export function getTickerList(count, page, status, orderBy, order) { + const params = { count, page } + if (status !== undefined && status !== '') params.status = status + if (orderBy) params.orderBy = orderBy + if (order) params.order = order + console.log('工单列表请求参数:', params) // 调试日志 + return request.get('/api/v1/admin/work_order/list', params) } // 待处理 diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index f658d7d..e0da797 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -678,7 +678,10 @@ const paramValueForm = reactive({ attr_id: undefined, attr_name: '', attr_value: '', - attr_price: 0 + attr_price: 0, + index: 0, + attr_range: 0, + range_type: 'equal' }) const paramValueRules = { @@ -825,7 +828,10 @@ const handleAddParamValue = () => { attr_id: undefined, attr_name: '', attr_value: '', - attr_price: 0 + attr_price: 0, + index: 0, + attr_range: 0, + range_type: 'equal' }) paramValueFormRef.value?.resetFields() } @@ -838,7 +844,10 @@ const handleEditParamValue = (row) => { attr_id: row.id, attr_name: row.name, attr_value: row.value, - attr_price: row.price / 100 + attr_price: row.price / 100, + index: row.index || 0, + attr_range: row.attr_range || 0, + range_type: row.range_type || 'equal' }) } @@ -872,7 +881,10 @@ const submitParamValueForm = () => { arg_id: Number(currentParam.value.id), attr_name: paramValueForm.attr_name, attr_value: paramValueForm.attr_value, - attr_price: paramValueForm.attr_price + attr_price: paramValueForm.attr_price, + index: Number(paramValueForm.index), + attr_range: Number(paramValueForm.attr_range), + range_type: paramValueForm.range_type } if (paramValueFormType.value === 'edit') { submitData.attr_id = paramValueForm.attr_id diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue index 4d123e1..ce79c8c 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -56,9 +56,17 @@
工单号: {{ ticketInfo.id }} - - {{ getStatusText(ticketInfo.status) }} - + + + + + + {{ ticketInfo?.username?.charAt(0) || 'U' }}
-
{{ message.content }}
+
+ {{ message.content }} + + + + + +
-
+
@@ -163,6 +190,47 @@
+ + + + + + + + +
+
+ 图片 +
×
+
+ + +
+ +
添加图片
+
+
+
+
+
+ +
+ @@ -174,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 } 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' @@ -200,11 +269,22 @@ const messageInput = ref('') const selectedImages = ref([]) const selectedFiles = ref([]) // 存储原始文件对象 const messagesContainer = ref(null) +const textareaRef = ref(null) +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) @@ -283,7 +363,7 @@ const fetchTicketDetail = async (showLoading = true) => { if (res.code === 200) { const detail = res.data ticketInfo.value = { - id: detail.work_id, + id: detail.id, title: detail.name, username: detail.user?.userName || `用户${detail.user?.userId || 'Unknown'}`, userId: detail.user?.userId, @@ -299,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, @@ -432,6 +513,38 @@ const sendMessage = async () => { } } +// 修改工单状态 +const handleStatusChange = async (newStatus) => { + const statusMap = { + 'pending': 0, + 'processing': 1, + 'replied': 2, + 'completed': 3 + } + + const oldStatus = ticketInfo.value.status + + try { + const formData = new FormData() + formData.append('work_id', route.query.id) + formData.append('Status', statusMap[newStatus]) + + const res = await updateTicketInfo(formData) + + if (res.code === 200) { + ElMessage.success('工单状态已更新') + ticketInfo.value.status = newStatus + } else { + ElMessage.error(res.message || '更新失败') + ticketInfo.value.status = oldStatus // 恢复原状态 + } + } catch (error) { + console.error('更新工单状态出错:', error) + ElMessage.error('网络错误,请稍后重试') + ticketInfo.value.status = oldStatus // 恢复原状态 + } +} + // 结束工单 const handleComplete = () => { ElMessageBox.confirm('确定要结束此工单吗?结束后将无法继续回复。', '确认操作', { @@ -456,14 +569,81 @@ const handleComplete = () => { // 图片处理 const handleFileChange = (file) => { if (!file) return + addImageFile(file.raw) +} + +// 添加图片文件(统一处理函数) +const addImageFile = (file) => { + // 验证文件类型 + if (!file.type.startsWith('image/')) { + ElMessage.warning('只支持图片格式') + return + } + + // 验证文件大小(限制10MB) + if (file.size > 10 * 1024 * 1024) { + ElMessage.warning('图片大小不能超过10MB') + return + } // 保存原始文件对象用于上传 - selectedFiles.value.push(file.raw) + selectedFiles.value.push(file) // 读取文件用于预览 const reader = new FileReader() reader.onload = (e) => selectedImages.value.push(e.target.result) - reader.readAsDataURL(file.raw) + reader.readAsDataURL(file) +} + +// 处理粘贴事件 +const handlePaste = (e) => { + const items = e.clipboardData?.items + if (!items) return + + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.type.indexOf('image') !== -1) { + e.preventDefault() + const file = item.getAsFile() + if (file) { + addImageFile(file) + ElMessage.success('图片已添加') + } + } + } +} + +// 处理拖拽进入 +const handleDragOver = (e) => { + isDragOver.value = true +} + +// 处理拖拽离开 +const handleDragLeave = (e) => { + isDragOver.value = false +} + +// 处理文件拖放 +const handleDrop = (e) => { + isDragOver.value = false + + const files = e.dataTransfer?.files + if (!files || files.length === 0) return + + let addedCount = 0 + for (let i = 0; i < files.length; i++) { + const file = files[i] + if (file.type.startsWith('image/')) { + addImageFile(file) + addedCount++ + } + } + + if (addedCount > 0) { + ElMessage.success(`已添加 ${addedCount} 张图片`) + } else { + ElMessage.warning('未找到图片文件') + } } const removeImage = (index) => { @@ -476,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 @@ -606,10 +918,24 @@ watch( onMounted(() => { fetchTicketDetail() startAutoRefresh() + + // 绑定粘贴事件到原生 textarea + nextTick(() => { + const textarea = textareaRef.value?.$el?.querySelector('textarea') + if (textarea) { + textarea.addEventListener('paste', handlePaste) + } + }) }) onBeforeUnmount(() => { stopAutoRefresh() + + // 移除粘贴事件监听 + const textarea = textareaRef.value?.$el?.querySelector('textarea') + if (textarea) { + textarea.removeEventListener('paste', handlePaste) + } }) @@ -645,7 +971,10 @@ onBeforeUnmount(() => { .ticket-id { font-weight: 500; - color: #606266; + color: #303133; + font-size: 14px; + white-space: nowrap; + flex-shrink: 0; } .user-info { @@ -765,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 { @@ -772,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; @@ -849,6 +1211,28 @@ onBeforeUnmount(() => { display: flex; flex-direction: column; gap: 12px; + position: relative; + transition: all 0.3s; +} + +.input-area.drag-over { + background: #f0f9ff; + border: 2px dashed #409eff; + border-radius: 4px; + padding: 8px; +} + +.input-area.drag-over::before { + content: '释放以添加图片'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #409eff; + font-size: 14px; + font-weight: 500; + pointer-events: none; + z-index: 1; } .input-actions { @@ -872,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; +} diff --git a/src/views/ticket/TicketList.vue b/src/views/ticket/TicketList.vue index c09bc62..64fd0d9 100644 --- a/src/views/ticket/TicketList.vue +++ b/src/views/ticket/TicketList.vue @@ -3,9 +3,6 @@
-
- 全部 {{ stats.total }} -
待处理 {{ stats.pending }}
@@ -18,8 +15,22 @@
已完成 {{ stats.completed }}
+
+ 全部 {{ stats.total }} +
+ + + + + + + + + + +