提交修改,后台管理页面bug修复,已经发布后台管理界面V1.0版本

This commit is contained in:
2025-12-25 17:56:09 +08:00
parent 845f1c6618
commit b1da484431
23 changed files with 614 additions and 257 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin-web</title>
<title>宝来威办公助手后台</title>
</head>
<body>
<div id="app"></div>

View File

@@ -6,9 +6,9 @@
type="text"
:icon="Menu"
@click="toggleSidebar"
></el-button>
>菜单</el-button>
<div class="logo">
<h1>后台管理</h1>
<h1>宝来威办公助手后台</h1>
</div>
</div>
@@ -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'])
</script>
<style scoped>
<style scoped lang="scss">
.header {
display: flex;
justify-content: space-between;
@@ -146,7 +142,7 @@ const emit = defineEmits(['toggleSidebar'])
}
.logo h1 {
font-size: 18px;
display: none;
}
.username {

View File

@@ -1,16 +1,13 @@
<template>
<div class="layout-container">
<!-- 顶部导航栏 -->
<Header @toggle-sidebar="handleToggleSidebar" />
<Header @open-menu="handleOpenMenu" />
<!-- 侧边栏 -->
<Sidebar
:is-open="isSidebarOpen"
@close-sidebar="handleCloseSidebar"
/>
<Sidebar v-model="isMenuOpen" />
<!-- 主内容区域 -->
<main class="main-content" :class="{ 'main-content--open': isSidebarOpen }">
<main class="main-content">
<div class="content-wrapper">
<router-view />
</div>
@@ -24,20 +21,15 @@ import Header from './Header.vue'
import Sidebar from './Sidebar.vue'
// 侧边栏显示状态(手机端)
const isSidebarOpen = ref(false)
const isMenuOpen = ref(false)
// 处理侧边栏切换事件来自Header组件
const handleToggleSidebar = (isOpen) => {
isSidebarOpen.value = isOpen
}
// 处理侧边栏关闭事件来自Sidebar组件
const handleCloseSidebar = () => {
isSidebarOpen.value = false
// Header 触发:打开抽屉菜单(手机端
const handleOpenMenu = () => {
isMenuOpen.value = true
}
</script>
<style scoped>
<style scoped lang="scss">
.layout-container {
display: flex;
flex-direction: column;
@@ -64,13 +56,6 @@ const handleCloseSidebar = () => {
}
}
/* 手机端侧边栏打开时,主内容区域添加左侧边距 */
.main-content--open {
@media (max-width: 767px) {
margin-left: 0;
}
}
.content-wrapper {
max-width: 1200px;
margin: 0 auto;

View File

@@ -1,105 +1,105 @@
<template>
<aside
class="sidebar"
:class="{ 'sidebar--open': isOpen }"
>
<nav class="sidebar-nav">
<ul class="nav-list">
<li class="nav-item" v-for="menu in menuList" :key="menu.path">
<router-link
:to="menu.path"
class="nav-link"
:class="{ 'is-active': $route.path === menu.path }"
@click="handleMenuClick"
>
<el-icon class="nav-icon"><component :is="menu.icon" /></el-icon>
<span class="nav-text">{{ menu.title }}</span>
</router-link>
</li>
</ul>
</nav>
<!-- 遮罩层手机端 -->
<div
v-if="isOpen && isMobile"
class="sidebar-mask"
@click="handleMaskClick"
></div>
<!-- 桌面/平板固定侧边栏不做展开/收起动画 -->
<aside v-if="!isMobile" class="sidebar-desktop">
<el-menu router :default-active="activePath" class="sidebar-menu" @select="handleSelect">
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/conversations">
<el-icon><Message /></el-icon>
<span>会话记录管理</span>
</el-menu-item>
<el-menu-item index="/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
</el-menu>
</aside>
<!-- 手机端抽屉菜单Element Plus 自带隐藏/遮罩/动画 -->
<el-drawer
v-else
v-model="drawerOpen"
direction="ltr"
size="250px"
:with-header="false"
>
<el-menu router :default-active="activePath" class="drawer-menu" @select="handleSelect">
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/conversations">
<el-icon><Message /></el-icon>
<span>会话记录管理</span>
</el-menu-item>
<el-menu-item index="/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
</el-menu>
</el-drawer>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { HomeFilled, Message, User } from '@element-plus/icons-vue'
// 菜单数据
const menuList = [
{
path: '/',
title: '首页',
icon: HomeFilled
},
{
path: '/conversations',
title: '会话记录管理',
icon: Message
},
{
path: '/users',
title: '用户管理',
icon: User
}
]
// 侧边栏显示状态(手机端)
const props = defineProps({
isOpen: {
modelValue: {
type: Boolean,
default: false
}
})
// 定义事件
const emit = defineEmits(['closeSidebar'])
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
// 响应式布局:判断是否为手机端
const isMobile = ref(false)
// 监听窗口大小变化
const handleResize = () => {
isMobile.value = window.innerWidth < 768
// 如果不是手机端,关闭侧边栏
if (!isMobile.value) {
emit('closeSidebar')
// 切回非手机端时,确保抽屉关闭
if (!isMobile.value && props.modelValue) {
emit('update:modelValue', false)
}
}
// 初始化响应式状态
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize)
})
// 清理事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// 菜单点击处理(手机端)
const handleMenuClick = () => {
if (isMobile.value) {
emit('closeSidebar')
}
}
const activePath = computed(() => route.path)
// 遮罩层点击处理(手机端)
const handleMaskClick = () => {
emit('closeSidebar')
// el-drawer v-model 适配
const drawerOpen = computed({
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
const handleSelect = () => {
// 手机端点击菜单项后自动关闭抽屉
if (isMobile.value) {
emit('update:modelValue', false)
}
}
</script>
<style scoped>
.sidebar {
<style scoped lang="scss">
.sidebar-desktop {
position: fixed;
top: 64px;
left: 0;
@@ -107,86 +107,20 @@ const handleMaskClick = () => {
width: 250px;
background-color: var(--background-color);
border-right: 1px solid var(--border-color);
transition: all 0.3s ease;
z-index: 99;
overflow-y: auto;
/* 桌面端默认显示,手机端默认隐藏 */
@media (max-width: 767px) {
transform: translateX(-100%);
}
}
/* 侧边栏打开状态(手机端) */
.sidebar--open {
@media (max-width: 767px) {
transform: translateX(0);
}
}
.sidebar-nav {
padding: 20px 0;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
margin-bottom: 4px;
}
.nav-link {
display: flex;
align-items: center;
padding: 12px 24px;
color: var(--text-color);
text-decoration: none;
transition: all 0.3s;
&:hover {
background-color: var(--border-color-lighter);
color: var(--primary-color);
}
&.is-active {
background-color: rgba(64, 158, 255, 0.1);
color: var(--primary-color);
border-right: 3px solid var(--primary-color);
}
}
.nav-icon {
font-size: 18px;
margin-right: 12px;
width: 20px;
text-align: center;
}
.nav-text {
font-size: 14px;
}
/* 遮罩层(手机端) */
.sidebar-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 98;
@media (min-width: 768px) {
display: none;
}
}
/* 平板端侧边栏宽度调整 */
@media (min-width: 768px) and (max-width: 1023px) {
.sidebar {
@media (max-width: 1023px) {
width: 200px;
}
}
.sidebar-menu,
.drawer-menu {
border-right: none;
}
:deep(.el-drawer__body) {
padding: 0;
}
</style>

View File

@@ -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')
}
}
})

View File

@@ -13,9 +13,37 @@
</div>
</template>
<el-form :model="filterForm" inline label-width="80px" size="small">
<el-form-item label="时间范围">
<el-date-picker v-model="filterForm.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker>
<el-form-item label="快捷范围">
<el-select v-model="filterForm.quickRange" placeholder="请选择" clearable style="width: 150px" @change="handleQuickRangeChange">
<el-option label="今天" value="today"></el-option>
<el-option label="最近3天" value="last3"></el-option>
<el-option label="最近7天" value="last7"></el-option>
<el-option label="最近30天" value="last30"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="filterForm.startDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
class="date-picker"
@change="handleManualDateChange"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="filterForm.endDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
class="date-picker"
@change="handleManualDateChange"
/>
</el-form-item>
<el-form-item label="用户">
@@ -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()
})

View File

@@ -4,13 +4,26 @@
<!-- 系统统计卡片 -->
<div class="stats-container">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon department-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<h3>{{ totalUserCount }}</h3>
<p>总用户数</p>
</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon user-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<h3>{{ userCount }}</h3>
<h3>{{ activeUserCount }}</h3>
<p>活跃用户</p>
</div>
</div>
@@ -22,23 +35,12 @@
<el-icon><Message /></el-icon>
</div>
<div class="stat-info">
<h3>{{ conversationCount }}</h3>
<h3>{{ totalConversationCount }}</h3>
<p>会话记录</p>
</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon department-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<h3>{{ departmentCount }}</h3>
<p>部门数量</p>
</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
@@ -46,8 +48,8 @@
<el-icon><Calendar /></el-icon>
</div>
<div class="stat-info">
<h3>{{ todayCount }}</h3>
<p>今日新增</p>
<h3>{{ todayNewConversationCount }}</h3>
<p>今日会话</p>
</div>
</div>
</el-card>
@@ -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 = []

View File

@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '../store/auth'
@@ -45,7 +45,7 @@ const loading = ref(false)
// 登录表单数据
const loginForm = reactive({
username: '',
username: 'Admin',
password: ''
})
@@ -65,6 +65,13 @@ const router = useRouter()
// 认证状态管理
const authStore = useAuthStore()
// 处理键盘按键事件
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleLogin()
}
}
// 登录处理
const handleLogin = async () => {
// 表单验证
@@ -92,6 +99,16 @@ const handleLogin = async () => {
loading.value = false
}
}
// 组件挂载时添加键盘监听
onMounted(() => {
window.addEventListener('keypress', handleKeyPress)
})
// 组件卸载时移除键盘监听
onUnmounted(() => {
window.removeEventListener('keypress', handleKeyPress)
})
</script>
<style scoped>

View File

@@ -3,7 +3,7 @@
<h2>用户管理</h2>
<!-- 筛选表单 -->
<el-card class="filter-card">
<!-- <el-card class="filter-card">
<el-form :model="filterForm" inline label-width="80px">
<el-form-item label="用户名">
<el-input
@@ -44,7 +44,7 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-card> -->
<!-- 用户列表表格 -->
<el-card class="table-card">