feat: 添加系统配置、审计中心、通知中心、文件管理模块
This commit is contained in:
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user