diff --git a/README.md b/README.md
index 52e6d73..5bbbd8f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+# 管理员后台pc端
+
# 007UI 后台管理系统
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
diff --git a/src/assets/7.wav b/src/assets/7.wav
new file mode 100644
index 0000000..2050e2f
Binary files /dev/null and b/src/assets/7.wav differ
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/utils/tool.js b/src/utils/tool.js
index 3d0a39f..c54e2dd 100644
--- a/src/utils/tool.js
+++ b/src/utils/tool.js
@@ -51,4 +51,9 @@ export function timeToTimestamp(time) {
}
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000)
+ }
+
+
+ export function reducenum(num){
+ return num / 100
}
\ No newline at end of file
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 @@
-
-
+
+
-
+
{{ row.goodId || row.goodGroupId || '-' }}
@@ -149,7 +149,7 @@
placeholder="请选择代金券"
filterable
clearable
- :disabled="dialogType === 'edit'"
+ :disabled="dialogType === 'edit' || !!codeId"
style="width: 100%"
>
diff --git a/src/views/marketing/Voucher.vue b/src/views/marketing/Voucher.vue
index b5db990..cd070d3 100644
--- a/src/views/marketing/Voucher.vue
+++ b/src/views/marketing/Voucher.vue
@@ -63,9 +63,10 @@
-
+
编辑
+ 管理
查看
删除
@@ -166,8 +167,10 @@
+
+
+
diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue
index 1c29ce7..d7ff893 100644
--- a/src/views/product/ProductList.vue
+++ b/src/views/product/ProductList.vue
@@ -82,8 +82,8 @@
-
- {{ row.inventory_control ? '已启用' : '未启用' }}
+
+ {{ row.inventoryControl ? '已启用' : '未启用' }}
@@ -99,7 +99,7 @@
编辑
-
+ 参数
删除
@@ -125,6 +125,7 @@
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品' : '编辑商品'"
width="700px"
+ style="margin-top: 300px;"
>
-
-
+
+
@@ -181,6 +182,152 @@
确定
+
+
+
+
+
+
+
+
+
+
+
+ {{ getArgTypeText(row.type) }}
+
+
+
+
+
+
+ 编辑
+ 查看参数值
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 字符串
+ 数字
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ¥{{ (row.price / 100).toFixed(2) }}
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -189,8 +336,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({
@@ -344,6 +499,7 @@ const handleAdd = () => {
// 编辑商品
const handleEdit = (row) => {
+ console.log("更新:",row)
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(productForm, {
@@ -353,7 +509,7 @@ const handleEdit = (row) => {
content: row.content,
cover_id: row.coverId,
good_group_id: row.goodGroupId,
- inventory_control: row.inventory_control,
+ inventory_control: row.inventoryControl,
inventory: row.inventory,
price: row.price,
pay_num: row.payNum,
@@ -449,10 +605,12 @@ const submitForm = () => {
good_group_id: Number(productForm.good_group_id), // 确保是数字类型
cover_id: productForm.cover_id || 0,
inventory: productForm.inventory || 0,
- price: productForm.price || 0,
+ price: productForm.price/100 || 0,
pay_num: productForm.pay_num || 1,
expire_time: productForm.expire_time || 0,
- recommend_rebate: productForm.recommend_rebate || 0
+ recommend_rebate: productForm.recommend_rebate || 0,
+ inventory_control:productForm.inventoryControl
+
}
console.log('提交的数据:', submitData) // 调试日志
@@ -480,6 +638,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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 查询
-
- 重置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ getArgTypeText(row.type) }}
-
-
-
-
-
-
- 编辑
- 查看参数值
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 字符串
- 数字
- 选择
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ¥{{ (row.price / 100).toFixed(2) }}
-
-
-
-
-
- 编辑
- 删除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/ticket/TicketChat.vue b/src/views/ticket/TicketChat.vue
index 67875c6..0de9642 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: msg.created_at || msg.updated_at || new Date().toLocaleString(),
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 || '发送失败')
}
@@ -729,42 +730,149 @@ const updateTicketStats = () => {
// 格式化消息时间
const formatMessageTime = (timeStr) => {
- const date = new Date(timeStr)
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ if (!timeStr) return ''
+
+ try {
+ const date = new Date(timeStr)
+ if (isNaN(date.getTime())) return ''
+
+ // 格式化为 HH:MM
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ return `${hours}:${minutes}`
+ } catch (e) {
+ console.error('时间格式化失败:', e)
+ return ''
+ }
}
// 格式化列表项时间
const formatTime = (timeStr) => {
- const date = new Date(timeStr)
- const now = new Date()
- const diff = now - date
-
- // 今天内的消息只显示时间
- if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ if (!timeStr) return ''; // 空值兜底
+
+ // 步骤1:解析中文时间字符串(核心适配点)
+ let date;
+ try {
+ // 先尝试原生解析(兼容ISO格式)
+ date = new Date(timeStr);
+ // 若原生解析失败(返回Invalid Date),解析中文格式
+ if (isNaN(date.getTime())) {
+ // 正则提取中文时间的年、月、日、时、分、秒
+ const cnTimeMatch = timeStr.match(
+ /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/
+ );
+ if (cnTimeMatch) {
+ const [, year, month, day, period, hour, minute, second] = cnTimeMatch;
+ // 处理下午/上午的小时转换(12小时制转24小时制)
+ let hour24 = parseInt(hour, 10);
+ if (period === '下午' && hour24 !== 12) {
+ hour24 += 12;
+ }
+ if (period === '上午' && hour24 === 12) {
+ hour24 = 0; // 上午12点转为0点
+ }
+ // 构造日期(月份从0开始,需-1)
+ date = new Date(
+ parseInt(year, 10),
+ parseInt(month, 10) - 1,
+ parseInt(day, 10),
+ hour24,
+ parseInt(minute, 10),
+ parseInt(second, 10)
+ );
+ } else {
+ return '无效时间'; // 既不是ISO也不是中文格式
+ }
+ }
+ } catch (e) {
+ console.error('时间解析失败:', e);
+ return '无效时间';
}
+
+ const now = new Date();
+ const dateTime = date.getTime();
+ const nowTime = now.getTime();
+ const diff = nowTime - dateTime;
+
+ // 步骤2:判断“今天”(年/月/日完全一致)
+ const isToday = date.getFullYear() === now.getFullYear() &&
+ date.getMonth() === now.getMonth() &&
+ date.getDate() === now.getDate();
- // 一周内的显示星期几
- if (diff < 7 * 24 * 60 * 60 * 1000) {
- const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
- return weekdays[date.getDay()]
+ if (isToday) {
+ // 格式化今天的时间(24小时制,补零)
+ const hour = String(date.getHours()).padStart(2, '0');
+ const minute = String(date.getMinutes()).padStart(2, '0');
+ return `${hour}:${minute}`;
}
-
- // 其他显示日期
- return date.toLocaleDateString()
-}
+
+ // 步骤3:判断“一周内”
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
+ if (diff < oneWeek) {
+ const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+ return weekdays[date.getDay()];
+ }
+
+ // 步骤4:格式化其他日期(补零,统一格式:YYYY/MM/DD)
+ 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}`;
+};
// 格式化日期显示
const formatDate = (timeStr) => {
- const date = new Date(timeStr)
- const now = new Date()
-
- if (date.toDateString() === now.toDateString()) {
- return '今天'
+ console.log("原始时间字符串:", timeStr);
+ if (!timeStr) return ''; // 空值兜底
+
+ let date;
+ // 1. 先尝试原生解析(兼容ISO等标准格式)
+ date = new Date(timeStr);
+
+ // 2. 若原生解析失败,专门解析中文时间格式
+ if (isNaN(date.getTime())) {
+ // 正则匹配:xxxx年xx月xx日 上午/下午 xx:xx:xx
+ const cnTimeReg = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/;
+ const match = timeStr.match(cnTimeReg);
+
+ if (match) {
+ const [, year, month, day, period, hour, minute, second] = match;
+ // 处理12小时制转24小时制(关键适配)
+ let hour24 = parseInt(hour, 10);
+ if (period === '下午') {
+ hour24 = hour24 === 12 ? 12 : hour24 + 12; // 下午12点=12点,下午1-11点+12
+ } else { // 上午
+ hour24 = hour24 === 12 ? 0 : hour24; // 上午12点=0点,上午1-11点不变
+ }
+ // 手动构造合法的Date对象(月份从0开始,需-1)
+ date = new Date(
+ parseInt(year, 10),
+ parseInt(month, 10) - 1,
+ parseInt(day, 10),
+ hour24,
+ parseInt(minute, 10),
+ parseInt(second, 10)
+ );
+ } else {
+ return '无效时间'; // 非目标格式,返回兜底
+ }
}
-
- return date.toLocaleDateString()
-}
+
+ const now = new Date();
+ // 3. 对比“今天”(按日期维度,忽略时分秒)
+ const isToday = date.getFullYear() === now.getFullYear() &&
+ date.getMonth() === now.getMonth() &&
+ date.getDate() === now.getDate();
+
+ if (isToday) {
+ return '今天';
+ }
+
+ // 4. 格式化非今天的日期(统一格式,避免环境差异)
+ const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
+
+ return formattedDate;
+};
// 监听显示工单变化,更新消息和滚动
watch(currentTicket, (newVal) => {
@@ -838,8 +946,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 +983,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 +1028,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: msg.created_at || msg.updated_at || new Date().toLocaleString(),
+ 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 @@
-
+
-
-
- Token 生成成功,请妥善保管。
-
-
-
+
+
+
+
+
+ 正式环境
+ 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 {