feat: 优化工单和用户管理功能
- 工单模块改为列表形式,支持点击进入详情页回复 - 新增工单列表页面(TicketList.vue)和详情页面(TicketDetail.vue) - 工单详情页支持图片上传、快捷回复、定时刷新 - 消息按ID排序,时间显示优化(今天/昨天/星期/完整日期) - 定时刷新时不显示loading,且只在数据变化时更新UI - 用户列表直接使用API返回的cover字段作为头像,减少HTTP请求 - 修复用户余额页面balance_type参数undefined问题
This commit is contained in:
@@ -176,6 +176,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getTicketDetail, replyTicket, closeTicket } from '@/api/ticket'
|
import { getTicketDetail, replyTicket, closeTicket } from '@/api/ticket'
|
||||||
import { getUserInfo } from '@/api/admin/user'
|
import { getUserInfo } from '@/api/admin/user'
|
||||||
|
import { uploadFile } from '@/api/admin/file'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { useTagsViewStore } from '@/store/tagsViewStore'
|
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||||
|
|
||||||
@@ -197,6 +198,7 @@ const isLoadingUser = ref(false)
|
|||||||
// 输入相关
|
// 输入相关
|
||||||
const messageInput = ref('')
|
const messageInput = ref('')
|
||||||
const selectedImages = ref([])
|
const selectedImages = ref([])
|
||||||
|
const selectedFiles = ref([]) // 存储原始文件对象
|
||||||
const messagesContainer = ref(null)
|
const messagesContainer = ref(null)
|
||||||
|
|
||||||
// 图片查看
|
// 图片查看
|
||||||
@@ -235,8 +237,36 @@ const isAdmin = (userId) => {
|
|||||||
return userId === userStore.userInfo?.user_id
|
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
|
const workId = route.query.id
|
||||||
if (!workId) {
|
if (!workId) {
|
||||||
// 没有ID时静默跳转到列表页
|
// 没有ID时静默跳转到列表页
|
||||||
@@ -245,7 +275,9 @@ const fetchTicketDetail = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoadingMessages.value = true
|
if (showLoading) {
|
||||||
|
isLoadingMessages.value = true
|
||||||
|
}
|
||||||
const res = await getTicketDetail(workId)
|
const res = await getTicketDetail(workId)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
@@ -260,34 +292,50 @@ const fetchTicketDetail = async () => {
|
|||||||
status: convertStatusToString(detail.status)
|
status: convertStatusToString(detail.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息列表
|
// 处理消息列表并按ID排序
|
||||||
if (detail.content && detail.content.length > 0) {
|
if (detail.content && detail.content.length > 0) {
|
||||||
messages.value = detail.content.map((msg) => ({
|
const newMessages = detail.content
|
||||||
id: msg.id,
|
.map((msg) => ({
|
||||||
content: msg.content !== 'empty' ? msg.content : null,
|
id: msg.id,
|
||||||
images: msg.flies ? msg.flies.map(file => file.url) : [],
|
content: msg.content !== 'empty' ? msg.content : null,
|
||||||
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
images: msg.flies ? msg.flies.map(file => file.url) : [],
|
||||||
isAdmin: isAdmin(msg.user?.userId),
|
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
|
||||||
userId: msg.user?.userId,
|
isAdmin: isAdmin(msg.user?.userId),
|
||||||
avatar: msg.user?.coverUrl || ''
|
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 {
|
} else {
|
||||||
ElMessage.error(res.message || '获取工单详情失败')
|
ElMessage.error(res.message || '获取工单详情失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取工单详情出错:', error)
|
console.error('获取工单详情出错:', error)
|
||||||
ElMessage.error('网络错误,请稍后重试')
|
if (showLoading) {
|
||||||
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingMessages.value = false
|
if (showLoading) {
|
||||||
|
isLoadingMessages.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
const sendMessage = async () => {
|
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 workId = route.query.id
|
||||||
const content = messageInput.value.trim() || 'empty'
|
const content = messageInput.value.trim() || 'empty'
|
||||||
@@ -296,9 +344,56 @@ const sendMessage = async () => {
|
|||||||
isSending.value = true
|
isSending.value = true
|
||||||
const inputMsg = messageInput.value.trim()
|
const inputMsg = messageInput.value.trim()
|
||||||
const inputImages = [...selectedImages.value]
|
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 = ''
|
messageInput.value = ''
|
||||||
selectedImages.value = []
|
selectedImages.value = []
|
||||||
|
selectedFiles.value = []
|
||||||
|
|
||||||
// 临时消息
|
// 临时消息
|
||||||
const tempMsg = {
|
const tempMsg = {
|
||||||
@@ -315,7 +410,7 @@ const sendMessage = async () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
const res = await replyTicket(workId, content, '')
|
const res = await replyTicket(workId, content, fileIds.join(','))
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
messages.value = messages.value.filter(msg => !msg.isTempMessage)
|
messages.value = messages.value.filter(msg => !msg.isTempMessage)
|
||||||
@@ -325,6 +420,7 @@ const sendMessage = async () => {
|
|||||||
messages.value = messages.value.filter(msg => !msg.isTempMessage)
|
messages.value = messages.value.filter(msg => !msg.isTempMessage)
|
||||||
messageInput.value = inputMsg
|
messageInput.value = inputMsg
|
||||||
selectedImages.value = inputImages
|
selectedImages.value = inputImages
|
||||||
|
selectedFiles.value = inputFiles
|
||||||
ElMessage.error(res.message || '发送失败')
|
ElMessage.error(res.message || '发送失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -360,12 +456,20 @@ const handleComplete = () => {
|
|||||||
// 图片处理
|
// 图片处理
|
||||||
const handleFileChange = (file) => {
|
const handleFileChange = (file) => {
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
|
// 保存原始文件对象用于上传
|
||||||
|
selectedFiles.value.push(file.raw)
|
||||||
|
|
||||||
|
// 读取文件用于预览
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => selectedImages.value.push(e.target.result)
|
reader.onload = (e) => selectedImages.value.push(e.target.result)
|
||||||
reader.readAsDataURL(file.raw)
|
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) => {
|
const openImage = (img) => {
|
||||||
currentViewImage.value = img
|
currentViewImage.value = img
|
||||||
@@ -390,7 +494,54 @@ const formatMessageTime = (timeStr) => {
|
|||||||
try {
|
try {
|
||||||
const date = new Date(timeStr)
|
const date = new Date(timeStr)
|
||||||
if (isNaN(date.getTime())) return ''
|
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) {
|
} catch (e) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -430,7 +581,7 @@ const goToUserDetail = () => {
|
|||||||
const startAutoRefresh = () => {
|
const startAutoRefresh = () => {
|
||||||
refreshTimer.value = setInterval(() => {
|
refreshTimer.value = setInterval(() => {
|
||||||
if (ticketInfo.value?.status !== 'completed') {
|
if (ticketInfo.value?.status !== 'completed') {
|
||||||
fetchTicketDetail()
|
fetchTicketDetail(false) // 定时刷新时不显示 loading
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ const queryParams = reactive({
|
|||||||
// 余额记录查询参数
|
// 余额记录查询参数
|
||||||
const recordParams = reactive({
|
const recordParams = reactive({
|
||||||
user_id: '',
|
user_id: '',
|
||||||
|
balance_type: '', // 初始化为空字符串,避免 undefined
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 10
|
count: 10
|
||||||
})
|
})
|
||||||
|
|||||||
+16
-24
@@ -513,12 +513,24 @@ const fetchUserList = async () => {
|
|||||||
const res = await getUserList(queryParams)
|
const res = await getUserList(queryParams)
|
||||||
console.log("获取用户列表:", res)
|
console.log("获取用户列表:", res)
|
||||||
if (res.data.code === 200) {
|
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)
|
console.log("用户列表:", userList.value)
|
||||||
total.value = res.data.data.all_count || 0
|
total.value = res.data.data.all_count || 0
|
||||||
|
|
||||||
// 为每个用户加载头像URL
|
|
||||||
await loadAvatarsForUsers()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.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 = () => {
|
const handleQuery = () => {
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user