feat: 添加系统配置、审计中心、通知中心、文件管理模块

This commit is contained in:
张翔
2026-03-11 12:11:59 +08:00
commit 52c66444a5
264 changed files with 10109 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<template>
<a-config-provider :locale="locale">
<router-view />
</a-config-provider>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
</script>
@@ -0,0 +1,137 @@
<template>
<a-layout class="default-layout">
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<div class="logo">
<span v-if="!collapsed">Novalon</span>
<span v-else>N</span>
</div>
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
<a-menu-item key="dashboard" @click="router.push('/dashboard')">
<template #icon><DashboardOutlined /></template>
<span>仪表盘</span>
</a-menu-item>
<a-sub-menu key="system">
<template #icon><SettingOutlined /></template>
<template #title>系统管理</template>
<a-menu-item key="users" @click="router.push('/users')">用户管理</a-menu-item>
<a-menu-item key="roles" @click="router.push('/roles')">角色管理</a-menu-item>
<a-menu-item key="menus" @click="router.push('/menus')">菜单管理</a-menu-item>
</a-sub-menu>
<a-sub-menu key="config">
<template #icon><ToolOutlined /></template>
<template #title>系统配置</template>
<a-menu-item key="dict" @click="router.push('/dict')">字典管理</a-menu-item>
<a-menu-item key="sysconfig" @click="router.push('/sysconfig')">参数配置</a-menu-item>
</a-sub-menu>
<a-sub-menu key="audit">
<template #icon><AuditOutlined /></template>
<template #title>审计中心</template>
<a-menu-item key="loginlog" @click="router.push('/loginlog')">登录日志</a-menu-item>
<a-menu-item key="oplog" @click="router.push('/oplog')">操作日志</a-menu-item>
</a-sub-menu>
<a-sub-menu key="notify">
<template #icon><BellOutlined /></template>
<template #title>通知中心</template>
<a-menu-item key="notice" @click="router.push('/notice')">通知公告</a-menu-item>
</a-sub-menu>
<a-sub-menu key="file">
<template #icon><FolderOutlined /></template>
<template #title>文件管理</template>
<a-menu-item key="files" @click="router.push('/files')">文件列表</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header class="header">
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="collapsed = !collapsed" />
<menu-fold-outlined v-else class="trigger" @click="collapsed = !collapsed" />
<div class="header-right">
<a-dropdown>
<a-avatar>{{ username }}</a-avatar>
<template #overlay>
<a-menu>
<a-menu-item key="profile">个人中心</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<a-layout-content class="content">
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
DashboardOutlined, SettingOutlined, ToolOutlined,
AuditOutlined, BellOutlined, FolderOutlined,
MenuUnfoldOutlined, MenuFoldOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const collapsed = ref(false)
const selectedKeys = ref<string[]>([])
const username = ref(localStorage.getItem('username') || 'Admin')
const handleLogout = () => {
localStorage.clear()
router.push('/login')
}
onMounted(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/login')
}
})
</script>
<style scoped lang="scss">
.default-layout {
min-height: 100vh;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
font-weight: bold;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
&:hover { color: #1890ff; }
}
.header-right {
display: flex;
align-items: center;
}
}
.content {
margin: 16px;
padding: 16px;
background: #fff;
min-height: 280px;
}
</style>
+18
View File
@@ -0,0 +1,18 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import router from './router'
import i18n from './i18n'
import App from './App.vue'
import 'ant-design-vue/dist/reset.css'
import './styles/index.scss'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(i18n)
app.use(Antd)
app.mount('#app')
+44
View File
@@ -0,0 +1,44 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/system/Login.vue')
},
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/system/Dashboard.vue')
},
{
path: 'users',
name: 'UserManagement',
component: () => import('@/views/system/UserManagement.vue')
},
{
path: 'roles',
name: 'RoleManagement',
component: () => import('@/views/system/RoleManagement.vue')
},
{
path: 'menus',
name: 'MenuManagement',
component: () => import('@/views/system/MenuManagement.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
+30
View File
@@ -0,0 +1,30 @@
import axios from 'axios'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
@@ -0,0 +1,49 @@
<template>
<div class="login-log">
<a-card>
<template #title>登录日志</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '成功' : '失败' }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: 'IP地址', dataIndex: 'ip', key: 'ip' },
{ title: '登录地点', dataIndex: 'location', key: 'location' },
{ title: '浏览器', dataIndex: 'browser', key: 'browser' },
{ title: '操作系统', dataIndex: 'os', key: 'os' },
{ title: '状态', key: 'status' },
{ title: '登录时间', dataIndex: 'loginTime', key: 'loginTime' }
]
const loading = ref(false)
const dataSource = ref([])
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/logs/login')
dataSource.value = res
} finally {
loading.value = false
}
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,49 @@
<template>
<div class="operation-log">
<a-card>
<template #title>操作日志</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '成功' : '失败' }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '操作人', dataIndex: 'username', key: 'username' },
{ title: '操作模块', dataIndex: 'operation', key: 'operation' },
{ title: '请求方法', dataIndex: 'method', key: 'method' },
{ title: '请求参数', dataIndex: 'params', key: 'params', ellipsis: true },
{ title: '状态', key: 'status' },
{ title: '耗时(ms)', dataIndex: 'duration', key: 'duration' },
{ title: '操作时间', dataIndex: 'createdAt', key: 'createdAt' }
]
const loading = ref(false)
const dataSource = ref([])
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/logs/operation')
dataSource.value = res
} finally {
loading.value = false
}
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,115 @@
<template>
<div class="config-management">
<a-card>
<template #title>
<div class="card-title">
<span>参数配置</span>
<a-button type="primary" @click="handleAdd">新增配置</a-button>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'configType'">
<a-tag :color="record.configType === 'Y' ? 'blue' : 'orange'">
{{ record.configType === 'Y' ? '内置' : '自定义' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
<a-form :model="formState" :label-col="{ span: 6 }">
<a-form-item label="参数名称" name="configName">
<a-input v-model:value="formState.configName" />
</a-form-item>
<a-form-item label="参数键名" name="configKey">
<a-input v-model:value="formState.configKey" />
</a-form-item>
<a-form-item label="参数值" name="configValue">
<a-input v-model:value="formState.configValue" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '参数名称', dataIndex: 'configName', key: 'configName' },
{ title: '参数键名', dataIndex: 'configKey', key: 'configKey' },
{ title: '参数值', dataIndex: 'configValue', key: 'configValue' },
{ title: '类型', key: 'configType' },
{ title: '操作', key: 'action', width: 150 }
]
const loading = ref(false)
const dataSource = ref([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({ id: null, configName: '', configKey: '', configValue: '' })
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/config')
dataSource.value = res
} finally {
loading.value = false
}
}
const handleAdd = () => {
modalTitle.value = '新增配置'
Object.assign(formState, { id: null, configName: '', configKey: '', configValue: '' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑配置'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/config/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/config/${formState.id}`, formState)
} else {
await request.post('/config', formState)
}
message.success('操作成功')
modalVisible.value = false
fetchData()
} catch { message.error('操作失败') }
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.config-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,121 @@
<template>
<div class="dict-management">
<a-card>
<template #title>
<div class="card-title">
<span>字典管理</span>
<a-button type="primary" @click="handleAdd">新增字典</a-button>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '正常' : '停用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</a-table>
</template>
</a-card>
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
<a-form :model="formState" :label-col="{ span: 6 }">
<a-form-item label="字典名称" name="dictName">
<a-input v-model:value="formState.dictName" />
</a-form-item>
<a-form-item label="字典类型" name="dictType">
<a-input v-model:value="formState.dictType" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="0">正常</a-select-option>
<a-select-option value="1">停用</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formState.remark" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '字典名称', dataIndex: 'dictName', key: 'dictName' },
{ title: '字典类型', dataIndex: 'dictType', key: 'dictType' },
{ title: '状态', key: 'status' },
{ title: '备注', dataIndex: 'remark', key: 'remark' },
{ title: '操作', key: 'action', width: 200 }
]
const loading = ref(false)
const dataSource = ref([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({ id: null, dictName: '', dictType: '', status: '0', remark: '' })
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/dict/types')
dataSource.value = res
} finally {
loading.value = false
}
}
const handleAdd = () => {
modalTitle.value = '新增字典'
Object.assign(formState, { id: null, dictName: '', dictType: '', status: '0', remark: '' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑字典'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/dict/types/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/dict/types/${formState.id}`, formState)
} else {
await request.post('/dict/types', formState)
}
message.success('操作成功')
modalVisible.value = false
fetchData()
} catch { message.error('操作失败') }
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.dict-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,118 @@
<template>
<div class="file-management">
<a-card>
<template #title>
<div class="card-title">
<span>文件管理</span>
<a-upload :before-upload="handleUpload" :show-upload-list="false">
<a-button type="primary">
<upload-outlined /> 上传文件
</a-button>
</a-upload>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileType'">
<a-tag :color="getFileTypeColor(record.fileType)">
{{ getFileTypeName(record.fileType) }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleDownload(record)">下载</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { UploadOutlined } from '@ant-design/icons-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '文件名', dataIndex: 'fileName', key: 'fileName' },
{ title: '文件大小', dataIndex: 'fileSize', key: 'fileSize' },
{ title: '文件类型', key: 'fileType' },
{ title: '存储方式', dataIndex: 'storageType', key: 'storageType' },
{ title: '上传时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', width: 150 }
]
const loading = ref(false)
const dataSource = ref([])
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/files')
dataSource.value = res
} finally {
loading.value = false
}
}
const handleUpload = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
try {
await request.post('/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
message.success('上传成功')
fetchData()
} catch { message.error('上传失败') }
return false
}
const handleDownload = (record: any) => {
window.open(record.filePath)
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/files/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const getFileTypeName = (fileType: string) => {
if (!fileType) return '未知'
if (fileType.startsWith('image/')) return '图片'
if (fileType.startsWith('video/')) return '视频'
if (fileType.startsWith('audio/')) return '音频'
if (fileType.includes('pdf')) return 'PDF'
if (fileType.includes('word') || fileType.includes('document')) return 'Word'
if (fileType.includes('excel') || fileType.includes('spreadsheet')) return 'Excel'
return '其他'
}
const getFileTypeColor = (fileType: string) => {
if (!fileType) return 'default'
if (fileType.startsWith('image/')) return 'pink'
if (fileType.startsWith('video/')) return 'purple'
if (fileType.startsWith('audio/')) return 'cyan'
if (fileType.includes('pdf')) return 'red'
if (fileType.includes('word')) return 'blue'
if (fileType.includes('excel')) return 'green'
return 'orange'
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.file-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,129 @@
<template>
<div class="notice-management">
<a-card>
<template #title>
<div class="card-title">
<span>通知公告</span>
<a-button type="primary" @click="handleAdd">新增公告</a-button>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'noticeType'">
<a-tag :color="record.noticeType === '1' ? 'blue' : 'green'">
{{ record.noticeType === '1' ? '通知' : '公告' }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '正常' : '停用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
<a-form :model="formState" :label-col="{ span: 6 }">
<a-form-item label="公告标题" name="noticeTitle">
<a-input v-model:value="formState.noticeTitle" />
</a-form-item>
<a-form-item label="公告类型" name="noticeType">
<a-select v-model:value="formState.noticeType">
<a-select-option value="1">通知</a-select-option>
<a-select-option value="2">公告</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="公告内容" name="noticeContent">
<a-textarea v-model:value="formState.noticeContent" :rows="4" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="0">正常</a-select-option>
<a-select-option value="1">停用</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '公告标题', dataIndex: 'noticeTitle', key: 'noticeTitle' },
{ title: '公告类型', key: 'noticeType', width: 100 },
{ title: '状态', key: 'status', width: 80 },
{ title: '发布时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', width: 150 }
]
const loading = ref(false)
const dataSource = ref([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({ id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/notices')
dataSource.value = res
} finally {
loading.value = false
}
}
const handleAdd = () => {
modalTitle.value = '新增公告'
Object.assign(formState, { id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑公告'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/notices/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/notices/${formState.id}`, formState)
} else {
await request.post('/notices', formState)
}
message.success('操作成功')
modalVisible.value = false
fetchData()
} catch { message.error('操作失败') }
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.notice-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="dashboard">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic title="用户总数" :value="stats.userCount" :prefix="h(UserOutlined)" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="角色总数" :value="stats.roleCount" :prefix="h(TeamOutlined)" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="今日登录" :value="stats.todayLogin" :prefix="h(LogoutOutlined)" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="操作日志" :value="stats.operationLog" :prefix="h(FileTextOutlined)" />
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-card title="最近登录">
<a-timeline>
<a-timeline-item v-for="item in recentLogins" :key="item.id" :color="item.status === '0' ? 'green' : 'red'">
<p>{{ item.username }} - {{ item.ip }}</p>
<p>{{ item.loginTime }}</p>
</a-timeline-item>
</a-timeline>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="系统信息">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="系统版本">1.0.0</a-descriptions-item>
<a-descriptions-item label="Java版本">21</a-descriptions-item>
<a-descriptions-item label="前端框架">Vue 3 + Ant Design Vue</a-descriptions-item>
<a-descriptions-item label="数据库">PostgreSQL</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h } from 'vue'
import { UserOutlined, TeamOutlined, LogoutOutlined, FileTextOutlined } from '@ant-design/icons-vue'
const stats = reactive({
userCount: 0,
roleCount: 0,
todayLogin: 0,
operationLog: 0
})
const recentLogins = ref<any[]>([])
</script>
<style scoped lang="scss">
.dashboard {
padding: 16px;
}
</style>
@@ -0,0 +1,78 @@
<template>
<div class="login-container">
<a-card class="login-card">
<template #title>
<h2>Novalon 管理系统</h2>
</template>
<a-form
:model="formState"
@finish="onFinish"
layout="vertical"
>
<a-form-item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名' }]"
>
<a-input v-model:value="formState.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码' }]"
>
<a-input-password v-model:value="formState.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading" block>
登录
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const router = useRouter()
const loading = ref(false)
const formState = reactive({
username: '',
password: ''
})
const onFinish = async () => {
loading.value = true
try {
const res: any = await request.post('/auth/login', formState)
localStorage.setItem('token', res.token)
localStorage.setItem('userId', res.userId)
message.success('登录成功')
router.push('/')
} catch (error: any) {
message.error(error.response?.data?.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.login-card {
width: 400px;
}
}
</style>
@@ -0,0 +1,148 @@
<template>
<div class="menu-management">
<a-card>
<template #title>
<div class="card-title">
<span>菜单管理</span>
<a-button type="primary" @click="handleAdd">新增菜单</a-button>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="false" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag :color="record.menuType === 'M' ? 'blue' : record.menuType === 'C' ? 'green' : 'orange'">
{{ record.menuType === 'M' ? '目录' : record.menuType === 'C' ? '菜单' : '按钮' }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '显示' : '隐藏' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
<a-form :model="formState" :label-col="{ span: 6 }">
<a-form-item label="菜单名称" name="menuName">
<a-input v-model:value="formState.menuName" />
</a-form-item>
<a-form-item label="父级菜单" name="parentId">
<a-tree-select v-model:value="formState.parentId" :tree-data="menuTree" placeholder="请选择父级菜单" allow-clear />
</a-form-item>
<a-form-item label="菜单类型" name="menuType">
<a-select v-model:value="formState.menuType">
<a-select-option value="M">目录</a-select-option>
<a-select-option value="C">菜单</a-select-option>
<a-select-option value="F">按钮</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="路由地址" name="perms" v-if="formState.menuType !== 'F'">
<a-input v-model:value="formState.perms" />
</a-form-item>
<a-form-item label="组件路径" name="component" v-if="formState.menuType === 'C'">
<a-input v-model:value="formState.component" />
</a-form-item>
<a-form-item label="排序" name="orderNum">
<a-input-number v-model:value="formState.orderNum" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="0">显示</a-select-option>
<a-select-option value="1">隐藏</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
{ title: '菜单类型', key: 'menuType', width: 100 },
{ title: '权限标识', dataIndex: 'perms', key: 'perms' },
{ title: '组件', dataIndex: 'component', key: 'component' },
{ title: '排序', dataIndex: 'orderNum', key: 'orderNum', width: 80 },
{ title: '状态', key: 'status', width: 80 },
{ title: '操作', key: 'action', width: 150 }
]
const loading = ref(false)
const dataSource = ref([])
const menuTree = ref<any[]>([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({
id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0'
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/menus')
dataSource.value = res
menuTree.value = buildTreeSelect(res)
} finally {
loading.value = false
}
}
const buildTreeSelect = (menus: any[]): any[] => {
return menus.map(m => ({ value: m.id, label: m.menuName, children: m.children ? buildTreeSelect(m.children) : undefined }))
}
const handleAdd = () => {
modalTitle.value = '新增菜单'
Object.assign(formState, { id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑菜单'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/menus/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/menus/${formState.id}`, formState)
} else {
await request.post('/menus', formState)
}
message.success('操作成功')
modalVisible.value = false
fetchData()
} catch { message.error('操作失败') }
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.menu-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,122 @@
<template>
<div class="role-management">
<a-card>
<template #title>
<div class="card-title">
<span>角色管理</span>
<a-button type="primary" @click="handleAdd">新增角色</a-button>
</div>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '正常' : '禁用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
<a-form :model="formState" :label-col="{ span: 6 }">
<a-form-item label="角色名称" name="roleName">
<a-input v-model:value="formState.roleName" />
</a-form-item>
<a-form-item label="角色标识" name="roleKey">
<a-input v-model:value="formState.roleKey" />
</a-form-item>
<a-form-item label="排序" name="roleSort">
<a-input-number v-model:value="formState.roleSort" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="0">正常</a-select-option>
<a-select-option value="1">禁用</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '角色名称', dataIndex: 'roleName', key: 'roleName' },
{ title: '角色标识', dataIndex: 'roleKey', key: 'roleKey' },
{ title: '排序', dataIndex: 'roleSort', key: 'roleSort' },
{ title: '状态', key: 'status' },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', width: 200 }
]
const loading = ref(false)
const dataSource = ref([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({ id: null, roleName: '', roleKey: '', roleSort: 0, status: '0' })
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/roles')
dataSource.value = res
} finally {
loading.value = false
}
}
const handleAdd = () => {
modalTitle.value = '新增角色'
Object.assign(formState, { id: null, roleName: '', roleKey: '', roleSort: 0, status: '0' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑角色'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/roles/${record.id}`)
message.success('删除成功')
fetchData()
} catch { message.error('删除失败') }
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/roles/${formState.id}`, formState)
} else {
await request.post('/roles', formState)
}
message.success('操作成功')
modalVisible.value = false
fetchData()
} catch { message.error('操作失败') }
}
onMounted(() => fetchData())
</script>
<style scoped lang="scss">
.role-management .card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,173 @@
<template>
<div class="user-management">
<a-card>
<template #title>
<div class="card-title">
<span>用户管理</span>
<a-button type="primary" @click="handleAdd">新增用户</a-button>
</div>
</template>
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '0' ? 'green' : 'red'">
{{ record.status === '0' ? '正常' : '禁用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
:model="formState"
:label-col="{ span: 6 }"
layout="horizontal"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="0">正常</a-select-option>
<a-select-option value="1">禁用</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
{ title: '状态', key: 'status', width: 100 },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', width: 200 }
]
const loading = ref(false)
const dataSource = ref([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
const modalVisible = ref(false)
const modalTitle = ref('')
const formState = reactive({
id: null as number | null,
username: '',
email: '',
phone: '',
status: '0'
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await request.get('/users', {
params: {
page: pagination.current,
pageSize: pagination.pageSize
}
})
dataSource.value = res.data
pagination.total = res.total
} finally {
loading.value = false
}
}
const handleTableChange = (pag: any) => {
pagination.current = pag.current
fetchData()
}
const handleAdd = () => {
modalTitle.value = '新增用户'
Object.assign(formState, { id: null, username: '', email: '', phone: '', status: '0' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
modalTitle.value = '编辑用户'
Object.assign(formState, record)
modalVisible.value = true
}
const handleDelete = async (record: any) => {
try {
await request.delete(`/users/${record.id}`)
message.success('删除成功')
fetchData()
} catch {
message.error('删除失败')
}
}
const handleModalOk = async () => {
try {
if (formState.id) {
await request.put(`/users/${formState.id}`, formState)
message.success('更新成功')
} else {
await request.post('/users', formState)
message.success('创建成功')
}
modalVisible.value = false
fetchData()
} catch {
message.error('操作失败')
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.user-management {
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>