From 8163fc39c59275bf45f91944e3fe8a08ce7b00d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 3 May 2026 15:56:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20Phase=205=20-=20=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E8=BF=81=E7=A7=BB=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完成所有业务页面从 Vue 3 到 React 19 的迁移: 页面迁移: - Login: 表单验证 + 认证集成 - Dashboard: 统计卡片 + G2 图表占位 - UserManagement: 表格 + 分页 + CRUD + 权限控制 - RoleManagement: 表格 + 弹窗 + TreeSelect 权限分配 - MenuManagement: 树形表格 + 层级菜单管理 - ConfigManagement: 参数配置 CRUD - DictManagement: 字典类型/数据双面板管理 - FileManagement: 文件上传 + 图片预览 - NoticeManagement: 通知公告 CRUD - LoginLog/OpLog/ExLog: 审计日志只读查询 - 403: 权限拒绝页面 API 层补充: - loginLog.ts: 新增 LoginLog/OpLog/ExLog 接口与 API - status.ts: 新增 userStatusMap/roleStatusMap/menuStatusMap/noticeStatusMap 路由修正: - routes.ts: 日志页面路径对齐实际目录结构 验证:tsc --noEmit 零错误,dev server 正常启动 --- novalon-manage-web/src/api/loginLog.ts | 67 ++++++ novalon-manage-web/src/constants/status.ts | 21 ++ .../src/pages/config/config/index.tsx | 85 +++++++- .../src/pages/config/dict/index.tsx | 107 ++++++++- .../src/pages/dashboard/index.tsx | 101 ++++++++- novalon-manage-web/src/pages/file/index.tsx | 68 +++++- novalon-manage-web/src/pages/log/ex/index.tsx | 52 +++++ .../src/pages/log/login/index.tsx | 53 +++++ novalon-manage-web/src/pages/log/op/index.tsx | 53 +++++ novalon-manage-web/src/pages/login/index.tsx | 44 +++- novalon-manage-web/src/pages/login/types.ts | 4 + novalon-manage-web/src/pages/notify/index.tsx | 85 +++++++- .../src/pages/system/menu/index.tsx | 197 ++++++++++++++++- .../src/pages/system/role/index.tsx | 206 +++++++++++++++++- .../src/pages/system/user/index.tsx | 197 ++++++++++++++++- novalon-manage-web/src/router/routes.ts | 6 +- 16 files changed, 1333 insertions(+), 13 deletions(-) create mode 100644 novalon-manage-web/src/pages/log/ex/index.tsx create mode 100644 novalon-manage-web/src/pages/log/login/index.tsx create mode 100644 novalon-manage-web/src/pages/log/op/index.tsx create mode 100644 novalon-manage-web/src/pages/login/types.ts diff --git a/novalon-manage-web/src/api/loginLog.ts b/novalon-manage-web/src/api/loginLog.ts index 9acdae8..8ffa8fa 100644 --- a/novalon-manage-web/src/api/loginLog.ts +++ b/novalon-manage-web/src/api/loginLog.ts @@ -2,6 +2,62 @@ import request from '@/utils/request' import type { PageResponse } from './user.api' import { NoticeStatus } from '@/constants/status' +export interface LoginLog { + id: number + username: string + ip: string + location: string + browser: string + os: string + status: number + message: string + loginTime: string +} + +export interface LoginLogPageRequest { + page: number + size: number + keyword?: string + username?: string + ip?: string +} + +export interface OpLog { + id: number + operator: string + description: string + method: string + url: string + params: string + ip: string + status: number + createdAt: string +} + +export interface OpLogPageRequest { + page: number + size: number + keyword?: string + operator?: string +} + +export interface ExLog { + id: number + url: string + method: string + message: string + stackTrace: string + ip: string + operator: string + createdAt: string +} + +export interface ExLogPageRequest { + page: number + size: number + keyword?: string +} + export interface Notice { id: number title: string @@ -35,6 +91,17 @@ export interface NoticePageRequest { status?: string } +export const loginLogApi = { + getLoginLogs: (params: LoginLogPageRequest) => + request.get>('/logs/login/page', { params }), + + getOpLogs: (params: OpLogPageRequest) => + request.get>('/logs/operation/page', { params }), + + getExLogs: (params: ExLogPageRequest) => + request.get>('/logs/exception/page', { params }), +} + export const noticeApi = { getPage: (params: NoticePageRequest) => request.get>('/notice/page', { params }), diff --git a/novalon-manage-web/src/constants/status.ts b/novalon-manage-web/src/constants/status.ts index 23c5d23..1ade523 100644 --- a/novalon-manage-web/src/constants/status.ts +++ b/novalon-manage-web/src/constants/status.ts @@ -52,6 +52,27 @@ export enum NoticeStatus { /** * 状态值映射工具类 */ +export const userStatusMap: Record = { + [UserStatus.ACTIVE]: { label: '正常', color: 'green' }, + [UserStatus.INACTIVE]: { label: '禁用', color: 'red' }, + [UserStatus.LOCKED]: { label: '锁定', color: 'orange' }, +} + +export const roleStatusMap: Record = { + [RoleStatus.ACTIVE]: { label: '正常', color: 'green' }, + [RoleStatus.INACTIVE]: { label: '禁用', color: 'red' }, +} + +export const menuStatusMap: Record = { + [MenuStatus.ACTIVE]: { label: '正常', color: 'green' }, + [MenuStatus.INACTIVE]: { label: '禁用', color: 'red' }, +} + +export const noticeStatusMap: Record = { + [NoticeStatus.ACTIVE]: { label: '已发布', color: 'green' }, + [NoticeStatus.INACTIVE]: { label: '草稿', color: 'orange' }, +} + export class StatusHelper { /** * 判断状态是否为正常 diff --git a/novalon-manage-web/src/pages/config/config/index.tsx b/novalon-manage-web/src/pages/config/config/index.tsx index 04e2412..171c954 100644 --- a/novalon-manage-web/src/pages/config/config/index.tsx +++ b/novalon-manage-web/src/pages/config/config/index.tsx @@ -1,3 +1,86 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Modal, Form, Input, Space, message, Popconfirm, Tag } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import type { ColumnsType } from 'antd/es/table' +import { configApi } from '@/api/config' +import type { ConfigItem, CreateConfigRequest, UpdateConfigRequest, ConfigPageRequest } from '@/api/config' +import type { PageResponse } from '@/api/user.api' +import PermissionGuard from '@/components/PermissionGuard' + export default function ConfigManagement() { - return
ConfigManagement Page (TODO)
+ const [configs, setConfigs] = useState([]) + const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [editingConfig, setEditingConfig] = useState(null) + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) + const [form] = Form.useForm() + + useEffect(() => { loadConfigs() }, []) + + async function loadConfigs() { + setLoading(true) + try { + const params: ConfigPageRequest = { page: pagination.current - 1, size: pagination.pageSize } + const res = await configApi.getPage(params) + const data = res as unknown as PageResponse + setConfigs(data.content) + setPagination((prev) => ({ ...prev, total: data.totalElements })) + } catch { message.error('加载配置列表失败') } + finally { setLoading(false) } + } + + function handleAdd() { setEditingConfig(null); form.resetFields(); setModalOpen(true) } + function handleEdit(record: ConfigItem) { + setEditingConfig(record) + form.setFieldsValue({ configName: record.configName, configKey: record.configKey, configValue: record.configValue, configType: record.configType, remark: record.remark }) + setModalOpen(true) + } + async function handleDelete(id: number) { + try { await configApi.delete(id); message.success('删除成功'); loadConfigs() } + catch { message.error('删除失败') } + } + async function handleSubmit() { + try { + const values = await form.validateFields() + if (editingConfig) { await configApi.update(editingConfig.id, values as UpdateConfigRequest); message.success('更新成功') } + else { await configApi.create(values as CreateConfigRequest); message.success('创建成功') } + setModalOpen(false); loadConfigs() + } catch {} + } + + const columns: ColumnsType = [ + { title: '配置名称', dataIndex: 'configName', key: 'configName' }, + { title: '配置键', dataIndex: 'configKey', key: 'configKey' }, + { title: '配置值', dataIndex: 'configValue', key: 'configValue', ellipsis: true }, + { title: '类型', dataIndex: 'configType', key: 'configType', render: (v: string) => v ? {v} : '-' }, + { title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true }, + { title: '操作', key: 'action', render: (_, record) => ( + + + + + + rowKey="id" columns={columns} dataSource={configs} loading={loading} + pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadConfigs, 0) } }} /> + setModalOpen(false)} destroyOnClose> +
+ + + + + +
+
+ + ) } diff --git a/novalon-manage-web/src/pages/config/dict/index.tsx b/novalon-manage-web/src/pages/config/dict/index.tsx index 3f9ecf2..38b4edf 100644 --- a/novalon-manage-web/src/pages/config/dict/index.tsx +++ b/novalon-manage-web/src/pages/config/dict/index.tsx @@ -1,3 +1,108 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Modal, Form, Input, InputNumber, Select, Tag, Space, message, Popconfirm, Card, Row, Col } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons' +import type { ColumnsType } from 'antd/es/table' +import { dictApi } from '@/api/dict' +import type { DictType, DictData, CreateDictTypeRequest, CreateDictDataRequest, UpdateDictTypeRequest, UpdateDictDataRequest } from '@/api/dict' + export default function DictManagement() { - return
DictManagement Page (TODO)
+ const [dictTypes, setDictTypes] = useState([]) + const [dictData, setDictData] = useState([]) + const [selectedType, setSelectedType] = useState('') + const [loading, setLoading] = useState(false) + const [typeModalOpen, setTypeModalOpen] = useState(false) + const [dataModalOpen, setDataModalOpen] = useState(false) + const [editingType, setEditingType] = useState(null) + const [editingData, setEditingData] = useState(null) + const [typeForm] = Form.useForm() + const [dataForm] = Form.useForm() + + useEffect(() => { loadDictTypes() }, []) + + async function loadDictTypes() { + try { const res = await dictApi.getTypes(); setDictTypes(Array.isArray(res) ? res : []) } catch {} + } + async function loadDictData(dictType: string) { + setLoading(true) + try { const res = await dictApi.getDataByType(dictType); setDictData(Array.isArray(res) ? res : []) } catch {} + finally { setLoading(false) } + } + + function handleSelectType(type: string) { setSelectedType(type); loadDictData(type) } + + async function handleTypeSubmit() { + try { + const values = await typeForm.validateFields() + if (editingType) { await dictApi.updateType(editingType.id, values as UpdateDictTypeRequest); message.success('更新成功') } + else { await dictApi.createType(values as CreateDictTypeRequest); message.success('创建成功') } + setTypeModalOpen(false); loadDictTypes() + } catch {} + } + async function handleDataSubmit() { + try { + const values = await dataForm.validateFields() + if (editingData) { await dictApi.updateData(editingData.id, { ...values, dictType: selectedType } as UpdateDictDataRequest); message.success('更新成功') } + else { await dictApi.createData({ ...values, dictType: selectedType } as CreateDictDataRequest); message.success('创建成功') } + setDataModalOpen(false); loadDictData(selectedType) + } catch {} + } + + const typeColumns: ColumnsType = [ + { title: '字典名称', dataIndex: 'dictName', key: 'dictName' }, + { title: '字典类型', dataIndex: 'dictType', key: 'dictType', render: (v: string) => handleSelectType(v)}>{v} }, + { title: '状态', dataIndex: 'status', key: 'status', render: (v: number) => {v === 1 ? '正常' : '停用'} }, + { title: '操作', key: 'action', render: (_, record) => ( + + }> + rowKey="id" columns={typeColumns} dataSource={dictTypes} pagination={false} size="small" /> + + + + } onClick={() => { setEditingData(null); dataForm.resetFields(); setDataModalOpen(true) }}>新增 : null}> + {selectedType ? rowKey="id" columns={dataColumns} dataSource={dictData} loading={loading} pagination={false} size="small" /> :

请选择左侧字典类型

} +
+ + + + setTypeModalOpen(false)} destroyOnClose> +
+ + + + + + setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={} /> + + + + rowKey="id" columns={columns} dataSource={logs} loading={loading} + pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadLogs, 0) } }} /> + + ) +} diff --git a/novalon-manage-web/src/pages/log/login/index.tsx b/novalon-manage-web/src/pages/log/login/index.tsx new file mode 100644 index 0000000..aa7d681 --- /dev/null +++ b/novalon-manage-web/src/pages/log/login/index.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react' +import { Table, Input, Space, Tag, message } from 'antd' +import { SearchOutlined, ReloadOutlined } from '@ant-design/icons' +import { Button } from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { loginLogApi } from '@/api/loginLog' +import type { LoginLog, LoginLogPageRequest } from '@/api/loginLog' +import type { PageResponse } from '@/api/user.api' + +export default function LoginLogPage() { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(false) + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) + const [keyword, setKeyword] = useState('') + + useEffect(() => { loadLogs() }, []) + + async function loadLogs() { + setLoading(true) + try { + const params: LoginLogPageRequest = { page: pagination.current - 1, size: pagination.pageSize, keyword } + const res = await loginLogApi.getLoginLogs(params) + const data = res as unknown as PageResponse + setLogs(data.content) + setPagination((prev) => ({ ...prev, total: data.totalElements })) + } catch { message.error('加载登录日志失败') } + finally { setLoading(false) } + } + + const columns: ColumnsType = [ + { 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: '状态', dataIndex: 'status', key: 'status', render: (v: number) => {v === 1 ? '成功' : '失败'} }, + { title: '消息', dataIndex: 'message', key: 'message', ellipsis: true }, + { title: '登录时间', dataIndex: 'loginTime', key: 'loginTime' }, + ] + + return ( +
+
+ + setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={} /> + + +
+ rowKey="id" columns={columns} dataSource={logs} loading={loading} + pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadLogs, 0) } }} /> +
+ ) +} diff --git a/novalon-manage-web/src/pages/log/op/index.tsx b/novalon-manage-web/src/pages/log/op/index.tsx new file mode 100644 index 0000000..54d1cc0 --- /dev/null +++ b/novalon-manage-web/src/pages/log/op/index.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react' +import { Table, Input, Space, Tag, message } from 'antd' +import { SearchOutlined, ReloadOutlined } from '@ant-design/icons' +import { Button } from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { loginLogApi } from '@/api/loginLog' +import type { OpLog, OpLogPageRequest } from '@/api/loginLog' +import type { PageResponse } from '@/api/user.api' + +export default function OpLogPage() { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(false) + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) + const [keyword, setKeyword] = useState('') + + useEffect(() => { loadLogs() }, []) + + async function loadLogs() { + setLoading(true) + try { + const params: OpLogPageRequest = { page: pagination.current - 1, size: pagination.pageSize, keyword } + const res = await loginLogApi.getOpLogs(params) + const data = res as unknown as PageResponse + setLogs(data.content) + setPagination((prev) => ({ ...prev, total: data.totalElements })) + } catch { message.error('加载操作日志失败') } + finally { setLoading(false) } + } + + const columns: ColumnsType = [ + { title: '操作人', dataIndex: 'operator', key: 'operator' }, + { title: '操作描述', dataIndex: 'description', key: 'description', ellipsis: true }, + { title: '请求方法', dataIndex: 'method', key: 'method', ellipsis: true }, + { title: '请求路径', dataIndex: 'url', key: 'url', ellipsis: true }, + { title: '请求参数', dataIndex: 'params', key: 'params', ellipsis: true }, + { title: 'IP地址', dataIndex: 'ip', key: 'ip' }, + { title: '状态', dataIndex: 'status', key: 'status', render: (v: number) => {v === 1 ? '成功' : '失败'} }, + { title: '操作时间', dataIndex: 'createdAt', key: 'createdAt' }, + ] + + return ( +
+
+ + setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={} /> + + +
+ rowKey="id" columns={columns} dataSource={logs} loading={loading} + pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadLogs, 0) } }} /> +
+ ) +} diff --git a/novalon-manage-web/src/pages/login/index.tsx b/novalon-manage-web/src/pages/login/index.tsx index a4a88dc..2c3cb87 100644 --- a/novalon-manage-web/src/pages/login/index.tsx +++ b/novalon-manage-web/src/pages/login/index.tsx @@ -1,3 +1,45 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router' +import { Form, Input, Button, Card, message } from 'antd' +import { UserOutlined, LockOutlined } from '@ant-design/icons' +import { useAuthStore } from '@/stores/useAuthStore' +import type { LoginFormValues } from './types' + export default function Login() { - return
Login Page (TODO)
+ const [loading, setLoading] = useState(false) + const navigate = useNavigate() + const login = useAuthStore((s) => s.login) + + const handleLogin = async (values: LoginFormValues) => { + setLoading(true) + try { + await login(values.username, values.password) + message.success('登录成功') + navigate('/dashboard', { replace: true }) + } catch (error: any) { + message.error(error?.response?.data?.message || '登录失败,请检查用户名和密码') + } finally { + setLoading(false) + } + } + + return ( +
+ + onFinish={handleLogin} autoComplete="off" size="large"> + + } placeholder="用户名" /> + + + } placeholder="密码" /> + + + + + + +
+ ) } diff --git a/novalon-manage-web/src/pages/login/types.ts b/novalon-manage-web/src/pages/login/types.ts new file mode 100644 index 0000000..1d51efe --- /dev/null +++ b/novalon-manage-web/src/pages/login/types.ts @@ -0,0 +1,4 @@ +export interface LoginFormValues { + username: string + password: string +} diff --git a/novalon-manage-web/src/pages/notify/index.tsx b/novalon-manage-web/src/pages/notify/index.tsx index bff150b..0eef29c 100644 --- a/novalon-manage-web/src/pages/notify/index.tsx +++ b/novalon-manage-web/src/pages/notify/index.tsx @@ -1,3 +1,86 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Modal, Form, Input, Select, Tag, Space, message, Popconfirm } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import type { ColumnsType } from 'antd/es/table' +import { noticeApi } from '@/api/loginLog' +import type { Notice, NoticePageRequest } from '@/api/loginLog' +import type { PageResponse } from '@/api/user.api' +import { NoticeStatus, noticeStatusMap } from '@/constants/status' +import PermissionGuard from '@/components/PermissionGuard' + export default function NoticeManagement() { - return
NoticeManagement Page (TODO)
+ const [notices, setNotices] = useState([]) + const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [editingNotice, setEditingNotice] = useState(null) + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) + const [form] = Form.useForm() + + useEffect(() => { loadNotices() }, []) + + async function loadNotices() { + setLoading(true) + try { + const params: NoticePageRequest = { page: pagination.current - 1, size: pagination.pageSize } + const res = await noticeApi.getPage(params) + const data = res as unknown as PageResponse + setNotices(data.content) + setPagination((prev) => ({ ...prev, total: data.totalElements })) + } catch { message.error('加载通知列表失败') } + finally { setLoading(false) } + } + + function handleAdd() { setEditingNotice(null); form.resetFields(); setModalOpen(true) } + function handleEdit(record: Notice) { + setEditingNotice(record) + form.setFieldsValue({ title: record.title, content: record.content, type: record.type, status: record.status }) + setModalOpen(true) + } + async function handleDelete(id: number) { + try { await noticeApi.delete(id); message.success('删除成功'); loadNotices() } + catch { message.error('删除失败') } + } + async function handleSubmit() { + try { + const values = await form.validateFields() + if (editingNotice) { await noticeApi.update(editingNotice.id, values); message.success('更新成功') } + else { await noticeApi.create(values); message.success('创建成功') } + setModalOpen(false); loadNotices() + } catch {} + } + + const columns: ColumnsType = [ + { title: '标题', dataIndex: 'title', key: 'title' }, + { title: '类型', dataIndex: 'type', key: 'type', render: (v: string) => {v} }, + { title: '状态', dataIndex: 'status', key: 'status', render: (s: NoticeStatus) => { const info = noticeStatusMap[s]; return {info?.label || s} } }, + { title: '创建者', dataIndex: 'createdBy', key: 'createdBy' }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' }, + { title: '操作', key: 'action', render: (_, record) => ( + + + + + + rowKey="id" columns={columns} dataSource={notices} loading={loading} + pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadNotices, 0) } }} /> + setModalOpen(false)} destroyOnClose width={640}> +
+ + ({ label: info.label, value: v }))} /> +
+
+ + ) } diff --git a/novalon-manage-web/src/pages/system/menu/index.tsx b/novalon-manage-web/src/pages/system/menu/index.tsx index 17ffb7f..6576973 100644 --- a/novalon-manage-web/src/pages/system/menu/index.tsx +++ b/novalon-manage-web/src/pages/system/menu/index.tsx @@ -1,3 +1,198 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Modal, Form, Input, InputNumber, Select, Tag, Space, message, Popconfirm } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import type { ColumnsType } from 'antd/es/table' +import { menuApi } from '@/api/menu' +import type { MenuItem, CreateMenuRequest, UpdateMenuRequest } from '@/api/menu' +import { MenuStatus, menuStatusMap } from '@/constants/status' +import PermissionGuard from '@/components/PermissionGuard' + export default function MenuManagement() { - return
MenuManagement Page (TODO)
+ const [menus, setMenus] = useState([]) + const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [editingMenu, setEditingMenu] = useState(null) + const [form] = Form.useForm() + + useEffect(() => { + loadMenus() + }, []) + + async function loadMenus() { + setLoading(true) + try { + const res = await menuApi.getAll() + setMenus(Array.isArray(res) ? res : []) + } catch { + message.error('加载菜单列表失败') + } finally { + setLoading(false) + } + } + + function handleAdd(parentId = 0) { + setEditingMenu(null) + form.resetFields() + form.setFieldsValue({ parentId }) + setModalOpen(true) + } + + function handleEdit(record: MenuItem) { + setEditingMenu(record) + form.setFieldsValue({ + name: record.name, + path: record.path, + icon: record.icon, + component: record.component, + parentId: record.parentId, + sort: record.sort, + type: record.type, + permission: record.permission, + status: record.status, + visible: record.visible, + }) + setModalOpen(true) + } + + async function handleDelete(id: number) { + try { + await menuApi.delete(id) + message.success('删除成功') + loadMenus() + } catch { + message.error('删除失败') + } + } + + async function handleSubmit() { + try { + const values = await form.validateFields() + if (editingMenu) { + const data: UpdateMenuRequest = { ...values } + await menuApi.update(editingMenu.id, data) + message.success('更新成功') + } else { + const data: CreateMenuRequest = { ...values } + await menuApi.create(data) + message.success('创建成功') + } + setModalOpen(false) + loadMenus() + } catch {} + } + + const columns: ColumnsType = [ + { title: '菜单名称', dataIndex: 'name', key: 'name', width: 180 }, + { title: '路径', dataIndex: 'path', key: 'path' }, + { title: '图标', dataIndex: 'icon', key: 'icon', width: 80 }, + { title: '类型', dataIndex: 'type', key: 'type', width: 80, render: (type: string) => { + const map: Record = { + directory: { label: '目录', color: 'blue' }, + menu: { label: '菜单', color: 'green' }, + button: { label: '按钮', color: 'orange' }, + } + const info = map[type] + return info ? {info.label} : type + }}, + { title: '权限标识', dataIndex: 'permission', key: 'permission' }, + { title: '排序', dataIndex: 'sort', key: 'sort', width: 80 }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 80, + render: (status: MenuStatus) => { + const info = menuStatusMap[status] + return {info?.label || status} + }, + }, + { + title: '操作', + key: 'action', + width: 200, + render: (_, record) => ( + + + + + + + + + + + + + rowKey="id" + columns={columns} + dataSource={menus} + loading={loading} + expandable={{ defaultExpandAllRows: true }} + pagination={false} + /> + + setModalOpen(false)} + destroyOnClose + width={600} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + ) } diff --git a/novalon-manage-web/src/pages/system/role/index.tsx b/novalon-manage-web/src/pages/system/role/index.tsx index c27956d..17150b3 100644 --- a/novalon-manage-web/src/pages/system/role/index.tsx +++ b/novalon-manage-web/src/pages/system/role/index.tsx @@ -1,3 +1,207 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Modal, Form, Input, InputNumber, Select, Tag, Space, message, Popconfirm, TreeSelect } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import type { ColumnsType } from 'antd/es/table' +import { roleApi } from '@/api/role.api' +import type { Role, CreateRoleRequest, UpdateRoleRequest, RolePageRequest, Permission } from '@/api/role.api' +import type { PageResponse } from '@/api/user.api' +import { RoleStatus, roleStatusMap } from '@/constants/status' +import PermissionGuard from '@/components/PermissionGuard' + export default function RoleManagement() { - return
RoleManagement Page (TODO)
+ const [roles, setRoles] = useState([]) + const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [editingRole, setEditingRole] = useState(null) + const [permissions, setPermissions] = useState([]) + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) + const [form] = Form.useForm() + + useEffect(() => { + loadRoles() + loadPermissions() + }, []) + + async function loadRoles() { + setLoading(true) + try { + const params: RolePageRequest = { page: pagination.current - 1, size: pagination.pageSize } + const res = await roleApi.getPage(params) + const data = res as unknown as PageResponse + setRoles(data.content) + setPagination((prev) => ({ ...prev, total: data.totalElements })) + } catch { + message.error('加载角色列表失败') + } finally { + setLoading(false) + } + } + + async function loadPermissions() { + try { + const res = await roleApi.getAllPermissions() + setPermissions(Array.isArray(res) ? res : []) + } catch {} + } + + function handleAdd() { + setEditingRole(null) + form.resetFields() + setModalOpen(true) + } + + function handleEdit(record: Role) { + setEditingRole(record) + form.setFieldsValue({ + roleName: record.roleName, + roleKey: record.roleKey, + roleSort: record.roleSort, + status: record.status, + permissions: record.permissions?.map((p) => p.id), + }) + setModalOpen(true) + } + + async function handleDelete(id: number) { + try { + await roleApi.delete(id) + message.success('删除成功') + loadRoles() + } catch { + message.error('删除失败') + } + } + + async function handleSubmit() { + try { + const values = await form.validateFields() + if (editingRole) { + const data: UpdateRoleRequest = { ...values } + await roleApi.update(editingRole.id, data) + message.success('更新成功') + } else { + const data: CreateRoleRequest = { ...values } + await roleApi.create(data) + message.success('创建成功') + } + setModalOpen(false) + loadRoles() + } catch {} + } + + const permissionTreeData = buildPermissionTree(permissions) + + const columns: ColumnsType = [ + { title: '角色名称', dataIndex: 'roleName', key: 'roleName' }, + { title: '角色标识', dataIndex: 'roleKey', key: 'roleKey' }, + { title: '排序', dataIndex: 'roleSort', key: 'roleSort', width: 80 }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: RoleStatus) => { + const info = roleStatusMap[status] + return {info?.label || status} + }, + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + + + + + + + + rowKey="id" + columns={columns} + dataSource={roles} + loading={loading} + pagination={{ + ...pagination, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条`, + onChange: (page, pageSize) => { + setPagination((prev) => ({ ...prev, current: page, pageSize })) + setTimeout(loadRoles, 0) + }, + }} + /> + + setModalOpen(false)} + destroyOnClose + width={600} + > +
+ + + + + + + + + + + + + )} + {!editingUser && ( + + + + )} + + + + + + + + + + + ({ label: info.label, value }))} /> + + )} +
+
+ + ) } diff --git a/novalon-manage-web/src/router/routes.ts b/novalon-manage-web/src/router/routes.ts index e345bbc..0682f56 100644 --- a/novalon-manage-web/src/router/routes.ts +++ b/novalon-manage-web/src/router/routes.ts @@ -9,9 +9,9 @@ const ConfigManagement = lazy(() => import('@/pages/config/config')) const DictManagement = lazy(() => import('@/pages/config/dict')) const FileManagement = lazy(() => import('@/pages/file')) const NoticeManagement = lazy(() => import('@/pages/notify')) -const LoginLog = lazy(() => import('@/pages/audit/login-log')) -const OperationLog = lazy(() => import('@/pages/audit/operation-log')) -const ExceptionLog = lazy(() => import('@/pages/audit/exception-log')) +const LoginLog = lazy(() => import('@/pages/log/login')) +const OperationLog = lazy(() => import('@/pages/log/op')) +const ExceptionLog = lazy(() => import('@/pages/log/ex')) const Forbidden = lazy(() => import('@/pages/403')) const DefaultLayout = lazy(() => import('@/layouts/DefaultLayout/index'))