From a09631551b5da04a325fb1dae144403153854ca1 Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Mon, 8 Dec 2025 15:18:37 +0800 Subject: [PATCH] xx --- src/assets/7.wav | Bin 0 -> 16164 bytes src/components/UserSelector/index.vue | 191 ++++++ src/config/menus.js | 90 +-- src/router/index.js | 56 +- src/views/marketing/DiscountGoods.vue | 26 +- src/views/marketing/DiscountUsers.vue | 176 +---- src/views/marketing/UserVoucher.vue | 118 +++- src/views/marketing/Voucher.vue | 12 +- src/views/marketing/VoucherHistory.vue | 186 ++---- src/views/marketing/VoucherHolders.vue | 215 ++---- src/views/marketing/VoucherManagement.vue | 69 ++ src/views/product/ProductList.vue | 432 +++++++++++- src/views/product/ProductParameter.vue | 771 ---------------------- src/views/ticket/TicketChat.vue | 237 ++++--- src/views/user/UserDetail.vue | 95 ++- 15 files changed, 1172 insertions(+), 1502 deletions(-) create mode 100644 src/assets/7.wav create mode 100644 src/components/UserSelector/index.vue create mode 100644 src/views/marketing/VoucherManagement.vue delete mode 100644 src/views/product/ProductParameter.vue diff --git a/src/assets/7.wav b/src/assets/7.wav new file mode 100644 index 0000000000000000000000000000000000000000..2050e2fbdc71c3611ca152fab7285babdf9e5d65 GIT binary patch literal 16164 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 literal 0 HcmV?d00001 diff --git a/src/components/UserSelector/index.vue b/src/components/UserSelector/index.vue new file mode 100644 index 0000000..6be488f --- /dev/null +++ b/src/components/UserSelector/index.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/config/menus.js b/src/config/menus.js index 9cfc27e..24093e7 100644 --- a/src/config/menus.js +++ b/src/config/menus.js @@ -5,20 +5,20 @@ export const menus = [ icon: 'DataBoard' }, { - path : '/ticket', + path: '/ticket', title: '工单处理', icon: 'DataBoard' }, { - path:'/user', + path: '/user', title: '用户管理', icon: 'User', children: [ - { + { path: '/user/list', title: '用户列表' }, - { + { path: '/user/balance', title: '用户余额管理' }, @@ -45,10 +45,7 @@ export const menus = [ path: '/product/group', title: '商品分组' }, - { - path: '/product/parameter', - title: '商品参数' - } + ] }, { @@ -75,36 +72,7 @@ export const menus = [ path: '/marketing/voucher', title: '代金券管理' }, - { - path: '/marketing/user-distribution', - title: '用户分发管理' - }, - - { - id: 'discount-goods', - title: '商品关联管理', - path: '/marketing/discount-goods', - badge: 'NEW' - }, - { - id: 'discount-users', - title: '用户关联管理', - path: '/marketing/discount-users', - badge: 'NEW' - }, - - { - id: 'user-info', - title: '用户信息管理', - path: '/marketing/user-info', - badge: 'NEW' - }, - { - id: 'user-history', - title: '用户使用记录管理', - path: '/marketing/user-history', - badge: 'NEW' - } + ] }, { @@ -141,9 +109,9 @@ export const menus = [ { path: '/acs/images/categories', title: '镜像分类' } ] }, - { - path: '/acs/nodes', - title: '节点管理' + { + path: '/acs/nodes', + title: '节点管理' }, { path: '/acs/guacamole', @@ -158,10 +126,10 @@ export const menus = [ ] }, { - path:'/setting', - title:'全局设置管理', - children:[ - {path:'/setting/global',title:'全局设置'} + path: '/setting', + title: '全局设置管理', + children: [ + { path: '/setting/global', title: '全局设置' } ] } ] @@ -171,31 +139,31 @@ export const menus = [ title: '系统管理', icon: 'Setting', children: [ - { - path: '/system/permission', + { + path: '/system/permission', title: '权限管理', children: [ { path: '/system/permission/route', title: '路由权限' }, { path: '/system/permission/admin', title: '管理员权限' } ] }, - - { - path: '/system/file', - title: '文件管理' + + { + path: '/system/file', + title: '文件管理' }, - - { - path: '/system/domain-whitelist', - title: '域名白名单' + + { + path: '/system/domain-whitelist', + title: '域名白名单' }, - { - path: '/system/setting-group', - title: '配置组管理' + { + path: '/system/setting-group', + title: '配置组管理' }, - { - path: '/system/setting-list', - title: '配置管理' + { + path: '/system/setting-list', + title: '配置管理' } ] } diff --git a/src/router/index.js b/src/router/index.js index 086a24e..571f32a 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -230,14 +230,7 @@ const routes = [ title: '商品分组' } }, - { - path: 'parameter', - name: 'ProductParameter', - component: () => import('../views/product/ProductParameter.vue'), - meta: { - title: '商品参数' - } - } + ] }, // 订单管理路由 @@ -287,49 +280,16 @@ const routes = [ } }, { - path: 'user-distribution', - name: 'UserDistribution', - component: () => import('../views/marketing/UserVoucher.vue'), + path: 'voucher/:id/manage', + name: 'VoucherManagement', + component: () => import('../views/marketing/VoucherManagement.vue'), meta: { - title: '用户分发管理' + title: '代金券详情管理', + hidden: true, + activeMenu: '/marketing/voucher' } }, - { - path: 'discount-goods', - name: 'DiscountGoods', - component: () => import('../views/marketing/DiscountGoods.vue'), - meta: { - title: '商品关联管理', - badge: 'NEW' - } - }, - { - path: 'discount-users', - name: 'DiscountUsers', - component: () => import('../views/marketing/DiscountUsers.vue'), - meta: { - title: '用户关联管理', - badge: 'NEW' - } - }, - { - path: 'user-info', - name: 'UserInfo', - component: () => import('../views/marketing/VoucherHolders.vue'), - meta: { - title: '用户信息管理', - badge: 'NEW' - } - }, - { - path: 'user-history', - name: 'UserHistory', - component: () => import('../views/marketing/VoucherHistory.vue'), - meta: { - title: '用户使用记录管理', - badge: 'NEW' - } - } + ] }, // 活动管理路由 diff --git a/src/views/marketing/DiscountGoods.vue b/src/views/marketing/DiscountGoods.vue index 6bc2d1c..40ce59b 100644 --- a/src/views/marketing/DiscountGoods.vue +++ b/src/views/marketing/DiscountGoods.vue @@ -5,8 +5,8 @@
- - + + - + - + @@ -166,8 +167,10 @@
+ + + diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index 1c29ce7..b79f443 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -99,7 +99,7 @@ @@ -181,6 +181,152 @@ 确定 + + + +
+
+ + 新增参数 + + + 刷新 + +
+
+ + + + + + + + + + + +
+ + + + + + + + + + 字符串 + 数字 + 选择 + + + + + + + + +
+ 参数:{{ currentParam?.name }} + + 添加参数值 + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
@@ -189,8 +335,16 @@ import { ref, reactive, onMounted } from 'vue' import { getFileDetail } from '@/api/admin/file' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue' -import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product' -import { getProductGroupList } from '@/api/admin/product' +import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList, + getProductParameterList, + getProductParameterDetail, + createProductParameter, + updateProductParameter, + deleteProductParameter, + addProductParameterValue, + updateProductParameterValue, + deleteProductParameterValue +} from '@/api/admin/product' // 查询参数 const queryParams = reactive({ @@ -480,6 +634,259 @@ onMounted(() => { fetchProductList() fetchGroupList() }) + +// --------------------------------------------------------------------- +// 参数管理相关逻辑 +// --------------------------------------------------------------------- + +// 状态 +const paramDialogVisible = ref(false) +const paramLoading = ref(false) +const parameterList = ref([]) +const currentProductId = ref(null) + +const paramFormDialogVisible = ref(false) +const paramFormType = ref('add') +const paramFormRef = ref(null) +const paramForm = reactive({ + arg_id: undefined, + arg_name: '', + arg_type: 'string' +}) + +const paramRules = { + arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }], + arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }] +} + +const paramValuesDialogVisible = ref(false) +const paramValuesLoading = ref(false) +const paramValueList = ref([]) +const currentParam = ref(null) + +const paramValueFormDialogVisible = ref(false) +const paramValueFormType = ref('add') +const paramValueFormRef = ref(null) +const paramValueForm = reactive({ + attr_id: undefined, + attr_name: '', + attr_value: '', + attr_price: 0 +}) + +const paramValueRules = { + attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }], + attr_value: [{ required: true, message: '请输入值', trigger: 'blur' }], + attr_price: [{ required: true, message: '请输入价格', trigger: 'blur' }] +} + +// 打开参数管理 +const handleParameter = (row) => { + currentProductId.value = row.id + paramDialogVisible.value = true + fetchParameterList() +} + +// 获取参数列表 +const fetchParameterList = async () => { + if (!currentProductId.value) return + paramLoading.value = true + try { + const res = await getProductParameterList({ good_id: currentProductId.value }) + if (res.data.code === 200) { + parameterList.value = res.data.data || [] + } + } catch (error) { + ElMessage.error('获取参数列表失败') + } finally { + paramLoading.value = false + } +} + +// 参数类型显示 +const getArgTypeText = (type) => { + const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' } + return typeMap[type] || '未知' +} + +const getArgTypeTag = (type) => { + const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' } + return tagMap[type] || 'info' +} + +// 新增参数 +const handleAddParameter = () => { + paramFormType.value = 'add' + paramFormDialogVisible.value = true + Object.assign(paramForm, { + arg_id: undefined, + arg_name: '', + arg_type: 'string' + }) + paramFormRef.value?.resetFields() +} + +// 编辑参数 +const handleEditParameter = (row) => { + paramFormType.value = 'edit' + paramFormDialogVisible.value = true + Object.assign(paramForm, { + arg_id: row.id, + arg_name: row.name, + arg_type: row.type + }) +} + +// 删除参数 +const handleDeleteParameter = (row) => { + ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', { + confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' + }).then(async () => { + try { + const res = await deleteProductParameter({ good_id: currentProductId.value, arg_id: row.id }) + if (res.data.code === 200) { + ElMessage.success('删除成功') + fetchParameterList() + } + } catch (error) { + ElMessage.error('删除失败') + } + }).catch(() => {}) +} + +// 提交参数表单 +const submitParamForm = () => { + paramFormRef.value?.validate(async (valid) => { + if (valid) { + try { + const submitData = { + good_id: Number(currentProductId.value), + arg_name: paramForm.arg_name, + arg_type: paramForm.arg_type + } + if (paramFormType.value === 'edit') { + submitData.arg_id = paramForm.arg_id + } + + const res = paramFormType.value === 'add' + ? await createProductParameter(submitData) + : await updateProductParameter(submitData) + + if (res.data.code === 200) { + ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功') + paramFormDialogVisible.value = false + fetchParameterList() + } + } catch (error) { + ElMessage.error(error.response?.data?.message || '操作失败') + } + } + }) +} + +// 查看参数值 +const handleViewParamValues = (row) => { + currentParam.value = row + paramValuesDialogVisible.value = true + fetchParamValuesList() +} + +// 获取参数值列表 +const fetchParamValuesList = async () => { + if (!currentProductId.value || !currentParam.value) return + paramValuesLoading.value = true + try { + const res = await getProductParameterDetail({ + good_id: currentProductId.value, + arg_id: currentParam.value.id + }) + if (res.data.code === 200) { + paramValueList.value = res.data.data.attrs || [] + } + } catch (error) { + ElMessage.error('获取参数值列表失败') + } finally { + paramValuesLoading.value = false + } +} + +// 添加参数值 +const handleAddParamValue = () => { + paramValueFormType.value = 'add' + paramValueFormDialogVisible.value = true + Object.assign(paramValueForm, { + attr_id: undefined, + attr_name: '', + attr_value: '', + attr_price: 0 + }) + paramValueFormRef.value?.resetFields() +} + +// 编辑参数值 +const handleEditParamValue = (row) => { + paramValueFormType.value = 'edit' + paramValueFormDialogVisible.value = true + Object.assign(paramValueForm, { + attr_id: row.id, + attr_name: row.name, + attr_value: row.value, + attr_price: row.price / 100 + }) +} + +// 删除参数值 +const handleDeleteParamValue = (row) => { + ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', { + confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' + }).then(async () => { + try { + const res = await deleteProductParameterValue({ + good_id: currentProductId.value, + attr_id: row.id + }) + if (res.data.code === 200) { + ElMessage.success('删除成功') + fetchParamValuesList() + } + } catch (error) { + ElMessage.error('删除失败') + } + }).catch(() => {}) +} + +// 提交参数值表单 +const submitParamValueForm = () => { + paramValueFormRef.value?.validate(async (valid) => { + if (valid) { + try { + const submitData = { + good_id: Number(currentProductId.value), + arg_id: Number(currentParam.value.id), + attr_name: paramValueForm.attr_name, + attr_value: paramValueForm.attr_value, + attr_price: paramValueForm.attr_price + } + if (paramValueFormType.value === 'edit') { + submitData.attr_id = paramValueForm.attr_id + } + + const res = paramValueFormType.value === 'add' + ? await addProductParameterValue(submitData) + : await updateProductParameterValue(submitData) + + if (res.data.code === 200) { + ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功') + paramValueFormDialogVisible.value = false + fetchParamValuesList() + } + } catch (error) { + ElMessage.error(error.response?.data?.message || '操作失败') + } + } + }) +} + diff --git a/src/views/product/ProductParameter.vue b/src/views/product/ProductParameter.vue deleted file mode 100644 index 495a829..0000000 --- a/src/views/product/ProductParameter.vue +++ /dev/null @@ -1,771 +0,0 @@ - - - - - diff --git a/src/views/ticket/TicketChat.vue b/src/views/ticket/TicketChat.vue index 67875c6..5ef147e 100644 --- a/src/views/ticket/TicketChat.vue +++ b/src/views/ticket/TicketChat.vue @@ -33,7 +33,7 @@ @click="selectTicket(ticket)" >
- {{ ticket.username.charAt(0) }} + {{ ticket.username.charAt(0) }}
@@ -96,8 +96,8 @@ :class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']" >
- - {{ currentTicket.username.charAt(0) }} + + {{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
@@ -117,7 +117,7 @@
{{ formatMessageTime(message.time) }}
- A + A
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { Search, Plus, Loading } from '@element-plus/icons-vue' import { useRoute, useRouter } from 'vue-router' import { - getTickerList, + getTickerList, getTicketDetail, replyTicket, closeTicket, getUserAvatar, getFileImage, - parseFilesToImages + parseFilesToImages, + getTicketCount } from '@/api/ticket' +import notificationSound from '@/assets/7.wav' +import { useUserStore } from '@/store/userStore' // 路由相关 const route = useRoute() const router = useRouter() -// 管理员ID列表(客服ID) -const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID +// 用户 store +const userStore = useUserStore() // 头像 const adminAvatar = ref('') @@ -271,6 +274,11 @@ const stats = reactive({ isLoadingStats: false }) +// 上一次的待处理数量,用于判断是否有新工单 +const previousPendingCount = ref(0) +// 音频对象 +const audio = new Audio(notificationSound) + // 快捷回复选项 const quickReplies = ref([ { title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' }, @@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => { const mappedTickets = tickets.map(item => ({ id: item.work_id, title: item.name, - username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息 - userId: item.user_id, + username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`, + userId: item.user?.userId, + avatar: item.user?.coverUrl || '', createTime: new Date(item.created_at).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(), status: convertStatusToString(item.status), @@ -368,44 +377,35 @@ const fetchTicketList = async (append = false) => { } } -// 获取单个状态的工单数量 -const fetchStatusStat = async (status) => { - try { - // 将状态字符串转换为API所需的状态值 - let statusValue = ''; - if (status === 'pending') statusValue = '0'; - else if (status === 'processing') statusValue = '1'; - else if (status === 'replied') statusValue = '2'; - else if (status === 'completed') statusValue = '3'; - - const res = await getTickerList(10, 1, statusValue) // 只请求一条数据,但获取总数 - - if (res.code === 200) { - if (status === '') { - stats.total = res.data.all_count - } else { - stats[status] = res.data.all_count - } - } else { - console.error(`获取${status || '全部'}工单统计失败:`, res.message) - } - } catch (error) { - console.error(`获取${status || '全部'}工单统计出错:`, error) - } -} - // 获取所有状态的工单数量 const fetchAllStats = async () => { stats.isLoadingStats = true try { - // 并行获取各个状态的工单数量 - await Promise.all([ - fetchStatusStat(''), // 获取全部工单数量 - fetchStatusStat('pending'), // 待处理 - fetchStatusStat('processing'), // 处理中 - fetchStatusStat('replied'), // 已回复 - fetchStatusStat('completed') // 已完成 - ]) + const res = await getTicketCount() + if (res.code === 200) { + const data = res.data + + // 检查是否有新工单(待处理数量增加) + if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) { + try { + audio.play().catch(e => console.error('播放提示音失败:', e)) + } catch (e) { + console.error('播放提示音出错:', e) + } + } + + // 更新上一次的数量 + previousPendingCount.value = data.wait_count + + stats.total = data.all_count + stats.pending = data.wait_count + stats.replied = data.reply_count + stats.completed = data.close_count + // 计算处理中的数量:总数 - 待处理 - 已回复 - 已完成 + stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count + } else { + console.error('获取工单统计失败:', res.message) + } } catch (error) { console.error('获取工单统计数据出错:', error) } finally { @@ -413,18 +413,12 @@ const fetchAllStats = async () => { } } -// 只刷新当前分类的统计数据(用于定时刷新,减少请求) +// 刷新统计数据(用于定时刷新) const fetchCurrentStatusStat = async () => { - try { - // 只获取当前选中分类的统计数据 - await fetchStatusStat(activeStatus.value) - // 同时获取全部工单数量(因为顶部显示需要) - await fetchStatusStat('') - } catch (error) { - console.error('获取当前分类统计数据出错:', error) - } + await fetchAllStats() } + // 加载更多工单 const loadMoreTickets = () => { if (!hasMore.value || isLoading.value) return @@ -476,9 +470,9 @@ const filteredTickets = computed(() => { }) }) -// 判断是否是客服 +// 判断是否是当前登录的管理员 const isAdmin = (userId) => { - return adminUserIds.includes(userId) + return userId === userStore.userInfo?.user_id } // 状态转换 @@ -540,25 +534,25 @@ const fetchTicketMessages = async (workId) => { } // 处理消息列表 - if (detail.Content && detail.Content.length > 0) { - // 使用Promise.all一次性处理所有消息和图片 - const messagesPromises = detail.Content.map(async (msg) => { - const isAdminMsg = isAdmin(msg.UserId) - const images = await parseFilesToImages(msg.Flies) + if (detail.content && detail.content.length > 0) { + // 处理所有消息 + const messages = detail.content.map((msg) => { + const isAdminMsg = isAdmin(msg.user?.userId) + // 从 flies 数组中提取图片 URL + const images = msg.flies ? msg.flies.map(file => file.url) : [] return { - id: msg.Id, - content: msg.Content !== 'empty' ? msg.Content : null, + id: msg.id, + content: msg.content !== 'empty' ? msg.content : null, images: images, - time: new Date(msg.CreatedAt).toLocaleString(), + time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间 isAdmin: isAdminMsg, isSystem: false, - userId: msg.UserId + userId: msg.user?.userId, + avatar: msg.user?.coverUrl || '' } }) - // 等待所有消息处理完成 - const messages = await Promise.all(messagesPromises) currentMessages.value = messages } } else { @@ -589,23 +583,29 @@ const sendMessage = async () => { const fileIds = [] try { - // 添加一个临时的"正在发送"消息 + // 保存输入内容 + const inputMsg = messageInput.value.trim() + const inputImages = [...selectedImages.value] + + // 清空输入和已选图片 + messageInput.value = '' + selectedImages.value = [] + + // 立即添加消息到界面(不显示 loading) const tempMsg = { - content: messageInput.value.trim() || null, - images: selectedImages.value.length > 0 ? [...selectedImages.value] : null, + id: Date.now(), // 临时 ID + content: inputMsg || null, + images: inputImages.length > 0 ? inputImages : [], time: new Date().toLocaleString(), isAdmin: true, - isLoading: true, + isSystem: false, + userId: userStore.userInfo?.user_id, + avatar: userStore.userInfo?.cover_url || '', isTempMessage: true } currentMessages.value.push(tempMsg) - // 清空输入和已选图片 - const inputMsg = messageInput.value - messageInput.value = '' - selectedImages.value = [] - // 滚动到底部 await nextTick() scrollToBottom() @@ -633,6 +633,7 @@ const sendMessage = async () => { // 恢复输入内容 messageInput.value = inputMsg + selectedImages.value = inputImages ElMessage.error(res.message || '发送失败') } @@ -838,8 +839,9 @@ const refreshTicketList = async () => { const mappedTickets = tickets.map(item => ({ id: item.work_id, title: item.name, - username: `用户${item.user_id}`, - userId: item.user_id, + username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`, + userId: item.user?.userId, + avatar: item.user?.coverUrl || '', createTime: new Date(item.created_at).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(), status: convertStatusToString(item.status), @@ -874,6 +876,39 @@ const refreshTicketList = async () => { } } +// 辅助函数:去除 URL 中的查询参数 +const normalizeUrl = (url) => { + if (!url) return '' + return url.split('?')[0] +} + +// 辅助函数:比较两个消息数组是否相同(忽略 URL 查询参数) +const areMessagesEqual = (messages1, messages2) => { + if (messages1.length !== messages2.length) return false + + for (let i = 0; i < messages1.length; i++) { + const msg1 = messages1[i] + const msg2 = messages2[i] + + // 比较消息 ID 和内容 + if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false + + // 比较图片数量 + if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false + + // 比较图片 URL(去除查询参数) + if (msg1.images && msg2.images) { + for (let j = 0; j < msg1.images.length; j++) { + if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) { + return false + } + } + } + } + + return true +} + // 静默刷新聊天记录(不显示loading状态) const refreshTicketMessages = async (workId) => { try { @@ -886,37 +921,33 @@ const refreshTicketMessages = async (workId) => { if (currentTicket.value) { // 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新 if (currentTicket.value.status !== 'pending') { - currentTicket.value.status = convertStatusToString(detail.Status) + currentTicket.value.status = convertStatusToString(detail.status) } } // 处理消息列表 - if (detail.Content && detail.Content.length > 0) { - // 检查是否有新消息 - const lastMsgId = currentMessages.value.length > 0 ? - currentMessages.value[currentMessages.value.length - 1].id : 0; - const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId); - - if (hasNewMessage) { - // 有新消息时才更新 - const messagesPromises = detail.Content.map(async (msg) => { - const isAdminMsg = isAdmin(msg.UserId) - const images = await parseFilesToImages(msg.Flies) - - return { - id: msg.Id, - content: msg.Content !== 'empty' ? msg.Content : null, - images: images, - time: new Date(msg.CreatedAt).toLocaleString(), - isAdmin: isAdminMsg, - isSystem: false, - userId: msg.UserId - } - }) + if (detail.content && detail.content.length > 0) { + // 构建新消息列表 + const newMessages = detail.content.map((msg) => { + const isAdminMsg = isAdmin(msg.user?.userId) + // 从 flies 数组中提取图片 URL + const images = msg.flies ? msg.flies.map(file => file.url) : [] - // 等待所有消息处理完成 - const messages = await Promise.all(messagesPromises) - currentMessages.value = messages + return { + id: msg.id, + content: msg.content !== 'empty' ? msg.content : null, + images: images, + time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间 + isAdmin: isAdminMsg, + isSystem: false, + userId: msg.user?.userId, + avatar: msg.user?.coverUrl || '' + } + }) + + // 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化) + if (!areMessagesEqual(currentMessages.value, newMessages)) { + currentMessages.value = newMessages // 如果有新消息,滚动到底部 nextTick(() => { diff --git a/src/views/user/UserDetail.vue b/src/views/user/UserDetail.vue index 266c548..3784779 100644 --- a/src/views/user/UserDetail.vue +++ b/src/views/user/UserDetail.vue @@ -304,23 +304,37 @@ - +
- - - -
- -
- 复制 -
-
+ + + + +
+ 正式环境 + www.007yjs.com +
+
+ +
+ 测试环境 + apiserver.s1f.ren +
+
+ +
+ 本地环境 + localhost:5173 +
+
+
+
+
@@ -364,7 +378,7 @@ import AvatarSelector from '@/components/admin/AvatarSelector.vue' import { ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock, UserFilled, Document, Clock, List, Switch, User, Camera, Upload, - UploadFilled, Key, CopyDocument + UploadFilled, Key, Monitor, Setting } from '@element-plus/icons-vue' import { getUserGroupList } from '@/api/admin/user' import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file' @@ -465,6 +479,15 @@ const realnameForm = reactive({ // Token 展示相关 const tokenDialogVisible = ref(false) const loginToken = ref('') +const loginExpire = ref('') +const selectedEnvironment = ref('') + +// 环境配置 +const environments = { + production: 'https://www.007yjs.com', + test: 'https://apiserver.s1f.ren', + local: 'http://localhost:5173' +} // 管理员权限管理相关 const adminDialogVisible = ref(false) @@ -729,8 +752,10 @@ const handleSimulateLogin = async () => { const res = await mockUserLogin({ user_id: userInfo.value.UserId }) if (res.data.code === 200) { loginToken.value = res.data.data.token || '' + loginExpire.value = res.data.data.expire_time || '' + selectedEnvironment.value = '' tokenDialogVisible.value = true - ElMessage.success('模拟登录成功') + //ElMessage.success('模拟登录成功') } else { ElMessage.error(res.data.message || '模拟登录失败') } @@ -739,14 +764,18 @@ const handleSimulateLogin = async () => { } } -// 复制 Token -const copyToken = async () => { - try { - await navigator.clipboard.writeText(loginToken.value) - ElMessage.success('Token 已复制到剪贴板') - } catch (err) { - ElMessage.error('复制失败,请手动复制') +// 确认跳转 +const confirmJump = () => { + if (!selectedEnvironment.value) { + ElMessage.warning('请选择登录环境') + return } + const baseUrl = environments[selectedEnvironment.value] + const url = `${baseUrl}/token-login?token=${loginToken.value}&expire=${loginExpire.value}` + window.open(url, '_blank') + const envName = selectedEnvironment.value === 'production' ? '正式' : selectedEnvironment.value === 'test' ? '测试' : '本地' + ElMessage.success(`正在跳转到${envName}环境`) + tokenDialogVisible.value = false } // 获取实名状态文本 @@ -1166,6 +1195,28 @@ onActivated(() => { font-size: 13px; } +/* Token Dialog */ +.token-container { + padding: 20px 0; +} + +.env-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; +} + +.env-option span:first-child { + font-size: 14px; + color: #303133; +} + +.env-url { + font-size: 12px; + color: #909399; +} + /* Responsive */ @media (max-width: 1024px) { .detail-grid {