From 32bb4502e77072ebbe396a29b5bf16bdbcf5f9f7 Mon Sep 17 00:00:00 2001 From: shiran <2488252513@qq.com> Date: Fri, 12 Dec 2025 21:19:12 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/build?= =?UTF-8?q?-service-server.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-service-server.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-service-server.yaml b/.gitea/workflows/build-service-server.yaml index 9c09511..c68901b 100644 --- a/.gitea/workflows/build-service-server.yaml +++ b/.gitea/workflows/build-service-server.yaml @@ -37,7 +37,7 @@ jobs: deploy: needs: build - runs-on: ubuntu-latest + runs-on: hongKong steps: - name: Download Artifact uses: actions/download-artifact@v3 From 6859753470e3e4b9dbf20656a21158a057659888 Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Mon, 15 Dec 2025 16:25:16 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=94=B9=E4=B8=BA=E5=88=97=E8=A1=A8=E5=BD=A2=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E7=82=B9=E5=87=BB=E5=9B=9E=E5=A4=8D=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/menus.js | 10 +- src/router/index.js | 22 +- src/views/ticket/TicketDetail.vue | 592 ++++++++++++++++++++++++++++++ src/views/ticket/TicketList.vue | 388 ++++++++++++++++++++ 4 files changed, 1009 insertions(+), 3 deletions(-) create mode 100644 src/views/ticket/TicketDetail.vue create mode 100644 src/views/ticket/TicketList.vue diff --git a/src/config/menus.js b/src/config/menus.js index 24093e7..842fb99 100644 --- a/src/config/menus.js +++ b/src/config/menus.js @@ -6,8 +6,14 @@ export const menus = [ }, { path: '/ticket', - title: '工单处理', - icon: 'DataBoard' + title: '工单管理', + icon: 'Tickets', + children: [ + { + path: '/ticket/list', + title: '工单列表' + } + ] }, { path: '/user', diff --git a/src/router/index.js b/src/router/index.js index 571f32a..4f4f41b 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -39,7 +39,27 @@ const routes = [ title: '工单管理', icon: 'Tickets' }, - component: () => import('../views/ticket/TicketChat.vue'), + redirect: '/ticket/list', + children: [ + { + path: 'list', + name: 'TicketList', + component: () => import('../views/ticket/TicketList.vue'), + meta: { + title: '工单列表' + } + }, + { + path: 'detail', + name: 'TicketDetail', + component: () => import('../views/ticket/TicketDetail.vue'), + meta: { + title: '工单详情', + hidden: true, + activeMenu: '/ticket/list' + } + } + ] }, // ACS管理路由 diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue new file mode 100644 index 0000000..3dcbf04 --- /dev/null +++ b/src/views/ticket/TicketDetail.vue @@ -0,0 +1,592 @@ + + + + + diff --git a/src/views/ticket/TicketList.vue b/src/views/ticket/TicketList.vue new file mode 100644 index 0000000..e578ae5 --- /dev/null +++ b/src/views/ticket/TicketList.vue @@ -0,0 +1,388 @@ + + + + + From ab2df50c0dba7d3796a5e56845ab51c9c789d2b1 Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Mon, 15 Dec 2025 20:34:02 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=92=8C=E5=B7=A5=E5=8D=95=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化用户列表页面,移除头像批量加载导致的大量detail请求 - 移除工单列表自动刷新功能,避免页面跳转问题 - 将用户余额管理整合到用户列表操作菜单中 - 重构用户余额管理页面,采用现代化企业扁平化设计 - 移除用户余额管理独立菜单项 - 优化页面交互体验和视觉效果 --- src/config/menus.js | 4 - src/views/ticket/TicketDetail.vue | 208 +++- src/views/ticket/TicketList.vue | 193 ++-- src/views/user/UserBalance.vue | 1596 +++++++---------------------- src/views/user/UserDetail.vue | 6 + src/views/user/UserList.vue | 12 + 6 files changed, 662 insertions(+), 1357 deletions(-) diff --git a/src/config/menus.js b/src/config/menus.js index 842fb99..923d5cb 100644 --- a/src/config/menus.js +++ b/src/config/menus.js @@ -24,10 +24,6 @@ export const menus = [ path: '/user/list', title: '用户列表' }, - { - path: '/user/balance', - title: '用户余额管理' - }, { path: '/user/group', title: '用户组管理' diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue index 3dcbf04..2a35748 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -1,9 +1,60 @@ - - - diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue index 3784779..f9f456e 100644 --- a/src/views/user/UserDetail.vue +++ b/src/views/user/UserDetail.vue @@ -393,6 +393,10 @@ const Edit = EditIcon const route = useRoute() const router = useRouter() +// 引入tagsViewStore +import { useTagsViewStore } from '@/store/tagsViewStore' +const tagsViewStore = useTagsViewStore() + // 用户信息 const userInfo = ref({}) const loading = ref(false) @@ -546,6 +550,8 @@ const refreshData = () => { // 返回上一页 const goBack = () => { + // 关闭当前tab + tagsViewStore.delVisitedView(route) router.go(-1) } diff --git a/src/views/user/UserList.vue b/src/views/user/UserList.vue index f298ae6..259d12f 100644 --- a/src/views/user/UserList.vue +++ b/src/views/user/UserList.vue @@ -129,6 +129,7 @@ 修改密码 修改用户组 实名信息 + 余额管理 登录记录 操作记录 模拟登录 @@ -676,6 +677,9 @@ const handleCommand = (command, row) => { case 'realname': handleRealnameModify(row) break + case 'balance': + handleBalanceManage(row) + break case 'loginHistory': handleLoginHistory(row) break @@ -691,6 +695,14 @@ const handleCommand = (command, row) => { } } +// 余额管理 +const handleBalanceManage = (row) => { + router.push({ + path: '/user/balance', + query: { user_id: row.UserId } + }) +} + // 模拟登录 const handleSimulateLogin = async (row) => { From 54f78e15fea28f0afe63e6a4aeb3c96c8550898a Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Tue, 16 Dec 2025 11:29:52 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E5=92=8C=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工单模块改为列表形式,支持点击进入详情页回复 - 新增工单列表页面(TicketList.vue)和详情页面(TicketDetail.vue) - 工单详情页支持图片上传、快捷回复、定时刷新 - 消息按ID排序,时间显示优化(今天/昨天/星期/完整日期) - 定时刷新时不显示loading,且只在数据变化时更新UI - 用户列表直接使用API返回的cover字段作为头像,减少HTTP请求 - 修复用户余额页面balance_type参数undefined问题 --- src/views/ticket/TicketDetail.vue | 193 ++++++++++++++++++++++++++---- src/views/user/UserBalance.vue | 1 + src/views/user/UserList.vue | 40 +++---- 3 files changed, 189 insertions(+), 45 deletions(-) diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue index 2a35748..4d123e1 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -176,6 +176,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { getTicketDetail, replyTicket, closeTicket } from '@/api/ticket' import { getUserInfo } from '@/api/admin/user' +import { uploadFile } from '@/api/admin/file' import { useUserStore } from '@/store/userStore' import { useTagsViewStore } from '@/store/tagsViewStore' @@ -197,6 +198,7 @@ const isLoadingUser = ref(false) // 输入相关 const messageInput = ref('') const selectedImages = ref([]) +const selectedFiles = ref([]) // 存储原始文件对象 const messagesContainer = ref(null) // 图片查看 @@ -235,8 +237,36 @@ const isAdmin = (userId) => { return userId === userStore.userInfo?.user_id } +// 比较两个消息列表是否相同 +const messagesEqual = (oldMessages, newMessages) => { + if (oldMessages.length !== newMessages.length) return false + + for (let i = 0; i < oldMessages.length; i++) { + const oldMsg = oldMessages[i] + const newMsg = newMessages[i] + + // 比较关键字段 + if (oldMsg.id !== newMsg.id || + oldMsg.content !== newMsg.content || + oldMsg.images?.length !== newMsg.images?.length) { + return false + } + + // 比较图片URL + if (oldMsg.images && newMsg.images) { + for (let j = 0; j < oldMsg.images.length; j++) { + if (oldMsg.images[j] !== newMsg.images[j]) { + return false + } + } + } + } + + return true +} + // 获取工单详情 -const fetchTicketDetail = async () => { +const fetchTicketDetail = async (showLoading = true) => { const workId = route.query.id if (!workId) { // 没有ID时静默跳转到列表页 @@ -245,7 +275,9 @@ const fetchTicketDetail = async () => { } try { - isLoadingMessages.value = true + if (showLoading) { + isLoadingMessages.value = true + } const res = await getTicketDetail(workId) if (res.code === 200) { @@ -260,34 +292,50 @@ const fetchTicketDetail = async () => { status: convertStatusToString(detail.status) } - // 处理消息列表 + // 处理消息列表并按ID排序 if (detail.content && detail.content.length > 0) { - messages.value = detail.content.map((msg) => ({ - id: msg.id, - content: msg.content !== 'empty' ? msg.content : null, - images: msg.flies ? msg.flies.map(file => file.url) : [], - time: msg.created_at || msg.updated_at || new Date().toLocaleString(), - isAdmin: isAdmin(msg.user?.userId), - userId: msg.user?.userId, - avatar: msg.user?.coverUrl || '' - })) + const newMessages = detail.content + .map((msg) => ({ + id: msg.id, + content: msg.content !== 'empty' ? msg.content : null, + images: msg.flies ? msg.flies.map(file => file.url) : [], + time: msg.created_at || msg.updated_at || new Date().toLocaleString(), + isAdmin: isAdmin(msg.user?.userId), + userId: msg.user?.userId, + avatar: msg.user?.coverUrl || '' + })) + .sort((a, b) => a.id - b.id) // 按ID从小到大排序 + + // 比较新旧消息列表,只有在有变化时才更新 + const hasChanges = !messagesEqual(messages.value, newMessages) + + if (hasChanges) { + const shouldScroll = showLoading || newMessages.length > messages.value.length + messages.value = newMessages + + if (shouldScroll) { + nextTick(() => scrollToBottom()) + } + } } - - nextTick(() => scrollToBottom()) } else { ElMessage.error(res.message || '获取工单详情失败') } } catch (error) { console.error('获取工单详情出错:', error) - ElMessage.error('网络错误,请稍后重试') + if (showLoading) { + ElMessage.error('网络错误,请稍后重试') + } } finally { - isLoadingMessages.value = false + if (showLoading) { + isLoadingMessages.value = false + } } } // 发送消息 const sendMessage = async () => { - if ((!messageInput.value.trim() && selectedImages.length === 0) || isSending.value) return + if ((!messageInput.value.trim() && selectedImages.value.length === 0) || isSending.value) return const workId = route.query.id const content = messageInput.value.trim() || 'empty' @@ -296,9 +344,56 @@ const sendMessage = async () => { isSending.value = true const inputMsg = messageInput.value.trim() const inputImages = [...selectedImages.value] + const inputFiles = [...selectedFiles.value] + + // 上传图片并获取文件ID + let fileIds = [] + if (inputFiles.length > 0) { + try { + const formData = new FormData() + + // 添加所有文件 + inputFiles.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) { + // 从返回的数据中提取文件ID(字段名是 id) + const data = uploadRes.data.data + if (Array.isArray(data)) { + fileIds = data.map(item => String(item.id)) + } else if (data.id) { + fileIds = [String(data.id)] + } + + if (fileIds.length === 0) { + ElMessage.error('未获取到文件ID') + isSending.value = false + return + } + } else { + ElMessage.error(uploadRes.data?.message || '图片上传失败') + isSending.value = false + return + } + } catch (error) { + console.error('图片上传失败:', error) + ElMessage.error('图片上传失败,请重试') + isSending.value = false + return + } + } messageInput.value = '' selectedImages.value = [] + selectedFiles.value = [] // 临时消息 const tempMsg = { @@ -315,7 +410,7 @@ const sendMessage = async () => { await nextTick() scrollToBottom() - const res = await replyTicket(workId, content, '') + const res = await replyTicket(workId, content, fileIds.join(',')) if (res.code === 200) { messages.value = messages.value.filter(msg => !msg.isTempMessage) @@ -325,6 +420,7 @@ const sendMessage = async () => { messages.value = messages.value.filter(msg => !msg.isTempMessage) messageInput.value = inputMsg selectedImages.value = inputImages + selectedFiles.value = inputFiles ElMessage.error(res.message || '发送失败') } } catch (error) { @@ -360,12 +456,20 @@ const handleComplete = () => { // 图片处理 const handleFileChange = (file) => { if (!file) return + + // 保存原始文件对象用于上传 + selectedFiles.value.push(file.raw) + + // 读取文件用于预览 const reader = new FileReader() reader.onload = (e) => selectedImages.value.push(e.target.result) reader.readAsDataURL(file.raw) } -const removeImage = (index) => selectedImages.value.splice(index, 1) +const removeImage = (index) => { + selectedImages.value.splice(index, 1) + selectedFiles.value.splice(index, 1) +} const openImage = (img) => { currentViewImage.value = img @@ -390,7 +494,54 @@ const formatMessageTime = (timeStr) => { try { const date = new Date(timeStr) if (isNaN(date.getTime())) return '' - return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` + + const now = new Date() + const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` + + // 判断是否是今天 + const isToday = date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate() + + if (isToday) { + return time + } + + // 判断是否是昨天 + const yesterday = new Date(now) + yesterday.setDate(yesterday.getDate() - 1) + const isYesterday = date.getFullYear() === yesterday.getFullYear() && + date.getMonth() === yesterday.getMonth() && + date.getDate() === yesterday.getDate() + + if (isYesterday) { + return `昨天 ${time}` + } + + // 判断是否是本周(周一到周日) + // 获取本周一的日期(0点) + const currentDay = now.getDay() // 0-6,0是周日 + const mondayOffset = currentDay === 0 ? -6 : 1 - currentDay // 周日的话往前推6天,其他往前推到周一 + const monday = new Date(now) + monday.setDate(now.getDate() + mondayOffset) + monday.setHours(0, 0, 0, 0) + + // 获取本周日的日期(23:59:59) + const sunday = new Date(monday) + sunday.setDate(monday.getDate() + 6) + sunday.setHours(23, 59, 59, 999) + + // 判断消息日期是否在本周范围内 + if (date >= monday && date <= sunday) { + const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] + return `${weekdays[date.getDay()]} ${time}` + } + + // 其他情况显示完整日期时间 + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day} ${time}` } catch (e) { return '' } @@ -430,7 +581,7 @@ const goToUserDetail = () => { const startAutoRefresh = () => { refreshTimer.value = setInterval(() => { if (ticketInfo.value?.status !== 'completed') { - fetchTicketDetail() + fetchTicketDetail(false) // 定时刷新时不显示 loading } }, 10000) } diff --git a/src/views/user/UserBalance.vue b/src/views/user/UserBalance.vue index 8504664..252784a 100644 --- a/src/views/user/UserBalance.vue +++ b/src/views/user/UserBalance.vue @@ -187,6 +187,7 @@ const queryParams = reactive({ // 余额记录查询参数 const recordParams = reactive({ user_id: '', + balance_type: '', // 初始化为空字符串,避免 undefined page: 1, count: 10 }) diff --git a/src/views/user/UserList.vue b/src/views/user/UserList.vue index 259d12f..d3c8083 100644 --- a/src/views/user/UserList.vue +++ b/src/views/user/UserList.vue @@ -513,12 +513,24 @@ const fetchUserList = async () => { const res = await getUserList(queryParams) console.log("获取用户列表:", res) if (res.data.code === 200) { - userList.value = res.data.data.data || [] + // 映射 API 返回的字段到组件使用的字段格式 + userList.value = (res.data.data.data || []).map(user => ({ + UserId: user.user_id, + UserName: user.user_name, + Phone: user.phone, + Email: user.email, + Sex: user.sex, + Age: user.age, + IsAdmin: user.is_admin, + CoverID: user.cover_id, + avatarUrl: user.cover || '', // 直接使用 cover 字段作为头像 URL + UserGroup: user.user_group, + RealName: user.real_name, + IsDeleted: user.is_deleted, + CreatedAt: user.created_at + })) console.log("用户列表:", userList.value) total.value = res.data.data.all_count || 0 - - // 为每个用户加载头像URL - await loadAvatarsForUsers() } } catch (error) { ElMessage.error('获取用户列表失败') @@ -527,26 +539,6 @@ const fetchUserList = async () => { } } -// 为用户列表加载头像URL -const loadAvatarsForUsers = async () => { - const promises = userList.value.map(async (user) => { - if (user.CoverID) { - try { - const res = await getFileDetail({ file_id: user.CoverID }) - if (res.data.code === 200) { - user.avatarUrl = res.data.data.url - } - } catch (error) { - console.error('加载头像失败:', error) - user.avatarUrl = '' - } - } else { - user.avatarUrl = '' - } - }) - await Promise.all(promises) -} - // 查询用户列表 const handleQuery = () => { queryParams.page = 1 From 978b18d5d5fa0eb4a5ad5bb8b13c982af2a9e272 Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Wed, 17 Dec 2025 15:42:14 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BC=98=E5=8C=96=20-=20=E6=94=B9=E4=B8=BA=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=BD=A2=E5=BC=8F=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E3=80=81=E7=8A=B6=E6=80=81=E4=BF=AE=E6=94=B9=E3=80=81?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E7=B2=98=E8=B4=B4=E6=8B=96=E6=8B=BD=E7=AD=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ticket.js | 9 +- src/views/product/ProductList.vue | 20 +++- src/views/ticket/TicketDetail.vue | 176 ++++++++++++++++++++++++++++-- src/views/ticket/TicketList.vue | 53 ++++++++- 4 files changed, 236 insertions(+), 22 deletions(-) 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..c4b5d4d 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -56,9 +56,17 @@
工单号: {{ ticketInfo.id }} - - {{ getStatusText(ticketInfo.status) }} - + + + + + + -
+
@@ -174,7 +190,7 @@ 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 } from '@/api/ticket' import { getUserInfo } from '@/api/admin/user' import { uploadFile } from '@/api/admin/file' import { useUserStore } from '@/store/userStore' @@ -200,6 +216,8 @@ 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) @@ -283,7 +301,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, @@ -432,6 +450,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 +506,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) => { @@ -606,10 +723,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 +776,10 @@ onBeforeUnmount(() => { .ticket-id { font-weight: 500; - color: #606266; + color: #303133; + font-size: 14px; + white-space: nowrap; + flex-shrink: 0; } .user-info { @@ -849,6 +983,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 { 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 }} +
+ + + + + + + + + + +