diff --git a/WxCheckMvc/Controllers/AdminController.cs b/WxCheckMvc/Controllers/AdminController.cs index d3a64ed..613aa02 100644 --- a/WxCheckMvc/Controllers/AdminController.cs +++ b/WxCheckMvc/Controllers/AdminController.cs @@ -190,6 +190,80 @@ namespace WxCheckMvc.Controllers } } } + + [HttpGet] + public async Task QueryStats() + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + // 1) 活跃用户:最近 7 天登录(UpdateTime)且 UserKey/PhoneNumber 不为空 + long activeUsers; + using (MySqlCommand cmd = new(@" + SELECT COUNT(1) + FROM xcx_users + WHERE UpdateTime >= DATE_SUB(NOW(), INTERVAL 7 DAY) + AND UserKey IS NOT NULL AND UserKey <> '' + AND PhoneNumber IS NOT NULL AND PhoneNumber <> ''", _connection)) + { + activeUsers = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + // 2) 总会话记录数 + long totalConversations; + using (MySqlCommand cmd = new("SELECT COUNT(1) FROM xcx_conversation", _connection)) + { + totalConversations = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + // 3) 今日新增会话记录(CreateTime 在今天内) + long todayNewConversations; + using (MySqlCommand cmd = new(@" + SELECT COUNT(1) + FROM xcx_conversation + WHERE CreateTime >= CURDATE() + AND CreateTime < DATE_ADD(CURDATE(), INTERVAL 1 DAY)", _connection)) + { + todayNewConversations = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + // 4) 总用户数:UserKey/PhoneNumber 不为空 + long totalUsers; + using (MySqlCommand cmd = new(@" + SELECT COUNT(1) + FROM xcx_users + WHERE UserKey IS NOT NULL AND UserKey <> '' + AND PhoneNumber IS NOT NULL AND PhoneNumber <> ''", _connection)) + { + totalUsers = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + } + + var data = new AdminStatsResponse + { + ActiveUsers = activeUsers, + TotalConversations = totalConversations, + TodayNewConversations = todayNewConversations, + TotalUsers = totalUsers + }; + + return Ok(new { success = true, data }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } } public class ConversationQueryRequest @@ -240,4 +314,12 @@ namespace WxCheckMvc.Controllers public string AvatarUrl { get; set; } public string Department { get; set; } } + + public class AdminStatsResponse + { + public long ActiveUsers { get; set; } + public long TotalConversations { get; set; } + public long TodayNewConversations { get; set; } + public long TotalUsers { get; set; } + } } diff --git a/WxCheckMvc/Controllers/LoginController.cs b/WxCheckMvc/Controllers/LoginController.cs index c207962..aa62977 100644 --- a/WxCheckMvc/Controllers/LoginController.cs +++ b/WxCheckMvc/Controllers/LoginController.cs @@ -266,6 +266,15 @@ namespace WxCheckMvc.Controllers await insertCmd.ExecuteNonQueryAsync(); } } + else + { + // 用户已存在:更新最后一次接口调用时间 + using (MySqlCommand updateCmd = new MySqlCommand("UPDATE xcx_users SET UpdateTime = NOW() WHERE UserKey = @UserKey", _connection)) + { + updateCmd.Parameters.AddWithValue("@UserKey", openId); + await updateCmd.ExecuteNonQueryAsync(); + } + } } // 获取用户信息 diff --git a/WxCheckMvc/Properties/PublishProfiles/FolderProfile.pubxml.user b/WxCheckMvc/Properties/PublishProfiles/FolderProfile.pubxml.user index 27c9767..4d2ceb7 100644 --- a/WxCheckMvc/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/WxCheckMvc/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>E:\Project_Class\WX_XCX\Wx_WxCheck_Prod\WxCheckMvc\bin\Release\net8.0\publish\ - True|2025-12-24T12:05:02.2999541Z||;True|2025-12-24T16:33:44.2108439+08:00||;True|2025-12-24T15:32:13.8037439+08:00||;True|2025-12-12T11:09:28.8147447+08:00||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||; + True|2025-12-25T06:00:56.3451051Z||;True|2025-12-24T20:05:02.2999541+08:00||;True|2025-12-24T16:33:44.2108439+08:00||;True|2025-12-24T15:32:13.8037439+08:00||;True|2025-12-12T11:09:28.8147447+08:00||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||; \ No newline at end of file diff --git a/admin-web/index.html b/admin-web/index.html index 6d7c6f0..88f97fb 100644 --- a/admin-web/index.html +++ b/admin-web/index.html @@ -4,7 +4,7 @@ - admin-web + 宝来威办公助手后台
diff --git a/admin-web/src/components/Layout/Header.vue b/admin-web/src/components/Layout/Header.vue index 8c04aca..c0d734f 100644 --- a/admin-web/src/components/Layout/Header.vue +++ b/admin-web/src/components/Layout/Header.vue @@ -6,9 +6,9 @@ type="text" :icon="Menu" @click="toggleSidebar" - > + >菜单 @@ -47,6 +47,7 @@ import { Menu, ArrowDown, SwitchButton } from '@element-plus/icons-vue' import { useAuthStore } from '../../store/auth' import ThemeSwitcher from '../ThemeSwitcher.vue' + // 路由实例 const router = useRouter() @@ -56,9 +57,6 @@ const authStore = useAuthStore() // 用户名 const username = computed(() => authStore.username) -// 侧边栏显示状态(手机端) -const isSidebarOpen = ref(false) - // 响应式布局:判断是否为手机端 const isMobile = ref(false) @@ -78,11 +76,9 @@ onUnmounted(() => { window.removeEventListener('resize', handleResize) }) -// 切换侧边栏显示(手机端) +// 打开侧边栏抽屉(手机端) const toggleSidebar = () => { - isSidebarOpen.value = !isSidebarOpen.value - // 发送事件给父组件 - emit('toggleSidebar', isSidebarOpen.value) + emit('open-menu') } // 登出处理 @@ -92,10 +88,10 @@ const handleLogout = () => { } // 定义事件 -const emit = defineEmits(['toggleSidebar']) +const emit = defineEmits(['open-menu']) - diff --git a/admin-web/src/store/auth.js b/admin-web/src/store/auth.js index 7b5fccb..028c70c 100644 --- a/admin-web/src/store/auth.js +++ b/admin-web/src/store/auth.js @@ -2,14 +2,16 @@ import { defineStore } from 'pinia' export const useAuthStore = defineStore('auth', { state: () => ({ - isLoggedIn: false, - username: '' + isLoggedIn: localStorage.getItem('isLoggedIn') === 'true', + username: localStorage.getItem('username') || '' }), actions: { login(username, password) { if (username === 'Admin' && password === 'Admin') { this.isLoggedIn = true this.username = username + localStorage.setItem('isLoggedIn', 'true') + localStorage.setItem('username', username) return true } return false @@ -17,6 +19,8 @@ export const useAuthStore = defineStore('auth', { logout() { this.isLoggedIn = false this.username = '' + localStorage.removeItem('isLoggedIn') + localStorage.removeItem('username') } } }) diff --git a/admin-web/src/views/ConversationList.vue b/admin-web/src/views/ConversationList.vue index 0bf5c08..37b08c9 100644 --- a/admin-web/src/views/ConversationList.vue +++ b/admin-web/src/views/ConversationList.vue @@ -13,9 +13,37 @@ - - + + + + + + + + + + + + + + + @@ -110,12 +138,51 @@ const emptyText = computed(() => loading.value ? '加载中...' : '暂无数据' // 筛选表单数据 const filterForm = reactive({ - dateRange: [], + quickRange: 'last7', + startDate: '', + endDate: '', userKey: '', messageType: '', department: '' }) +const formatDate = (date) => { + 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 addDays = (date, days) => { + const d = new Date(date) + d.setDate(d.getDate() + days) + return d +} + +const applyQuickRange = (range) => { + const today = new Date() + const end = formatDate(today) + let start = end + + if (range === 'today') start = end + if (range === 'last3') start = formatDate(addDays(today, -2)) + if (range === 'last7') start = formatDate(addDays(today, -6)) + if (range === 'last30') start = formatDate(addDays(today, -29)) + + filterForm.startDate = start + filterForm.endDate = end +} + +const handleQuickRangeChange = (value) => { + if (!value) return + applyQuickRange(value) +} + +const handleManualDateChange = () => { + // 手动修改开始/结束日期时,取消快捷范围选中态 + filterForm.quickRange = '' +} + // 会话记录列表 const conversationList = ref([]) @@ -135,12 +202,13 @@ const handleQuery = () => { const handleReset = () => { // 重置表单 Object.keys(filterForm).forEach(key => { - if (key === 'dateRange') { - filterForm[key] = [] - } else { - filterForm[key] = '' - } + filterForm[key] = '' }) + + // 默认最近7天 + filterForm.quickRange = 'last7' + applyQuickRange('last7') + currentPage.value = 1 fetchConversations() } @@ -196,8 +264,8 @@ const fetchConversations = async (isLoadMore = false) => { try { // 构建请求参数 const params = { - startTime: filterForm.dateRange[0] ? new Date(filterForm.dateRange[0]).getTime() : null, - endTime: filterForm.dateRange[1] ? new Date(filterForm.dateRange[1]).getTime() + 24 * 60 * 60 * 1000 - 1 : null, + startTime: filterForm.startDate ? `${filterForm.startDate}T00:00:00` : null, + endTime: filterForm.endDate ? `${filterForm.endDate}T23:59:59.999` : null, userKey: filterForm.userKey, messageType: filterForm.messageType ? Number(filterForm.messageType) : null, department: filterForm.department, @@ -272,6 +340,8 @@ const fetchUsers = async () => { // 页面初始化 onMounted(() => { + // 默认最近7天 + applyQuickRange('last7') fetchUsers() fetchConversations() }) diff --git a/admin-web/src/views/Home.vue b/admin-web/src/views/Home.vue index 82a6f6d..bfe17e3 100644 --- a/admin-web/src/views/Home.vue +++ b/admin-web/src/views/Home.vue @@ -4,13 +4,26 @@
+ + +
+
+ +
+
+

{{ totalUserCount }}

+

总用户数

+
+
+
+
-

{{ userCount }}

+

{{ activeUserCount }}

活跃用户

@@ -22,23 +35,12 @@
-

{{ conversationCount }}

+

{{ totalConversationCount }}

会话记录

- - -
-
- -
-
-

{{ departmentCount }}

-

部门数量

-
-
-
+
@@ -46,8 +48,8 @@
-

{{ todayCount }}

-

今日新增

+

{{ todayNewConversationCount }}

+

今日会话

@@ -95,51 +97,25 @@ const authStore = useAuthStore() const username = computed(() => authStore.username) // 统计数据 -const userCount = ref(0) -const conversationCount = ref(0) -const departmentCount = ref(0) -const todayCount = ref(0) +const activeUserCount = ref(0) +const totalConversationCount = ref(0) +const totalUserCount = ref(0) +const todayNewConversationCount = ref(0) // 最近会话记录 const recentConversations = ref([]) -// 数据转换:将后端返回的会话数据转换为前端所需格式 -const convertConversationData = (data) => { - return data.map(item => ({ - id: item.Id, - recordTime: new Date(item.RecordTime).toLocaleString('zh-CN'), - recordTimeUTCStamp: item.RecordTimeUTCStamp, - userName: item.UserName, - conversationContent: item.ConversationContent, - messageType: item.MessageType - })) -} // 获取系统统计数据 const fetchStats = async () => { try { - // 获取用户列表数据,用于统计 - const usersData = await request.get('/Admin/QueryUsers') - const users = Array.isArray(usersData) ? usersData : usersData.data || [] - - // 获取会话记录数据,用于统计 - const conversationsData = await request.post('/Admin/QueryConversations', {}) - const conversations = Array.isArray(conversationsData) ? conversationsData : conversationsData.data || [] - - // 计算统计数据 - userCount.value = users.length - conversationCount.value = conversations.length - - // 计算部门数量 - const departments = new Set(users.map(user => user.Department)) - departmentCount.value = departments.size - - // 计算今日新增会话数 - const today = new Date().toDateString() - const todayConversations = conversations.filter(conv => { - return new Date(conv.RecordTime).toDateString() === today - }) - todayCount.value = todayConversations.length + const res = await request.get('/Admin/QueryStats') + const stats = res?.data ?? res + + activeUserCount.value = stats?.activeUsers ?? 0 + totalConversationCount.value = stats?.totalConversations ?? 0 + totalUserCount.value = stats?.totalUsers ?? 0 + todayNewConversationCount.value = stats?.todayNewConversations ?? 0 // 获取最近会话记录 await fetchRecentConversations() @@ -152,21 +128,28 @@ const fetchStats = async () => { // 获取最近会话记录 const fetchRecentConversations = async () => { try { - // 调用API获取最近的5条会话记录 - const data = await request.post('/Admin/QueryConversations', {}) - + const params = { + startTime: null, + endTime: null, + userKey: '', + messageType: null, + department: '', + page: 1, + pageSize: 8 + } + + // 调用API获取会话记录 + const data = await request.post('/Admin/QueryConversations', JSON.stringify(params)) + // 检查返回数据格式,确保是数组 const rawData = Array.isArray(data) ? data : data.data || [] - // 转换数据格式 - const convertedData = convertConversationData(rawData) - // 按时间排序(最新的记录在前面),并只显示最近5条 - recentConversations.value = convertedData + recentConversations.value = rawData .sort((a, b) => { return b.recordTimeUTCStamp - a.recordTimeUTCStamp }) - .slice(0, 5) + .slice(0, 10) } catch (error) { console.error('获取最近会话记录失败:', error) recentConversations.value = [] diff --git a/admin-web/src/views/Login.vue b/admin-web/src/views/Login.vue index f61af36..fce2202 100644 --- a/admin-web/src/views/Login.vue +++ b/admin-web/src/views/Login.vue @@ -32,7 +32,7 @@