feat(web): Phase 5 - 业务页面迁移完成

完成所有业务页面从 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 正常启动
This commit is contained in:
张翔
2026-05-03 15:56:45 +08:00
parent c5547cff06
commit 8163fc39c5
16 changed files with 1333 additions and 13 deletions
+67
View File
@@ -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<PageResponse<LoginLog>>('/logs/login/page', { params }),
getOpLogs: (params: OpLogPageRequest) =>
request.get<PageResponse<OpLog>>('/logs/operation/page', { params }),
getExLogs: (params: ExLogPageRequest) =>
request.get<PageResponse<ExLog>>('/logs/exception/page', { params }),
}
export const noticeApi = {
getPage: (params: NoticePageRequest) =>
request.get<PageResponse<Notice>>('/notice/page', { params }),
@@ -52,6 +52,27 @@ export enum NoticeStatus {
/**
* 状态值映射工具类
*/
export const userStatusMap: Record<number, { label: string; color: string }> = {
[UserStatus.ACTIVE]: { label: '正常', color: 'green' },
[UserStatus.INACTIVE]: { label: '禁用', color: 'red' },
[UserStatus.LOCKED]: { label: '锁定', color: 'orange' },
}
export const roleStatusMap: Record<number, { label: string; color: string }> = {
[RoleStatus.ACTIVE]: { label: '正常', color: 'green' },
[RoleStatus.INACTIVE]: { label: '禁用', color: 'red' },
}
export const menuStatusMap: Record<number, { label: string; color: string }> = {
[MenuStatus.ACTIVE]: { label: '正常', color: 'green' },
[MenuStatus.INACTIVE]: { label: '禁用', color: 'red' },
}
export const noticeStatusMap: Record<string, { label: string; color: string }> = {
[NoticeStatus.ACTIVE]: { label: '已发布', color: 'green' },
[NoticeStatus.INACTIVE]: { label: '草稿', color: 'orange' },
}
export class StatusHelper {
/**
* 判断状态是否为正常
@@ -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 <div>ConfigManagement Page (TODO)</div>
const [configs, setConfigs] = useState<ConfigItem[]>([])
const [loading, setLoading] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [editingConfig, setEditingConfig] = useState<ConfigItem | null>(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<ConfigItem>
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<ConfigItem> = [
{ 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 ? <Tag>{v}</Tag> : '-' },
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
<PermissionGuard permission="system:config:edit"><Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)} /></PermissionGuard>
<PermissionGuard permission="system:config:delete"><Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}><Button type="link" danger icon={<DeleteOutlined />} /></Popconfirm></PermissionGuard>
</Space>
)},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<PermissionGuard permission="system:config:add"><Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button></PermissionGuard>
<Button icon={<ReloadOutlined />} onClick={loadConfigs}></Button>
</Space>
</div>
<Table<ConfigItem> 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) } }} />
<Modal title={editingConfig ? '编辑配置' : '新增配置'} open={modalOpen} onOk={handleSubmit} onCancel={() => setModalOpen(false)} destroyOnClose>
<Form form={form} layout="vertical">
<Form.Item name="configName" label="配置名称" rules={[{ required: true, message: '请输入配置名称' }]}><Input /></Form.Item>
<Form.Item name="configKey" label="配置键" rules={[{ required: true, message: '请输入配置键' }]}><Input /></Form.Item>
<Form.Item name="configValue" label="配置值" rules={[{ required: true, message: '请输入配置值' }]}><Input /></Form.Item>
<Form.Item name="configType" label="类型"><Input /></Form.Item>
<Form.Item name="remark" label="备注"><Input.TextArea rows={3} /></Form.Item>
</Form>
</Modal>
</div>
)
}
@@ -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 <div>DictManagement Page (TODO)</div>
const [dictTypes, setDictTypes] = useState<DictType[]>([])
const [dictData, setDictData] = useState<DictData[]>([])
const [selectedType, setSelectedType] = useState<string>('')
const [loading, setLoading] = useState(false)
const [typeModalOpen, setTypeModalOpen] = useState(false)
const [dataModalOpen, setDataModalOpen] = useState(false)
const [editingType, setEditingType] = useState<DictType | null>(null)
const [editingData, setEditingData] = useState<DictData | null>(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<DictType> = [
{ title: '字典名称', dataIndex: 'dictName', key: 'dictName' },
{ title: '字典类型', dataIndex: 'dictType', key: 'dictType', render: (v: string) => <a onClick={() => handleSelectType(v)}>{v}</a> },
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: number) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '正常' : '停用'}</Tag> },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => { setEditingType(record); typeForm.setFieldsValue(record); setTypeModalOpen(true) }} />
<Popconfirm title="确认删除?" onConfirm={async () => { await dictApi.deleteType(record.id); message.success('删除成功'); loadDictTypes() }}><Button type="link" danger icon={<DeleteOutlined />} /></Popconfirm>
</Space>
)},
]
const dataColumns: ColumnsType<DictData> = [
{ title: '字典标签', dataIndex: 'dictLabel', key: 'dictLabel' },
{ title: '字典值', dataIndex: 'dictValue', key: 'dictValue' },
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80 },
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: number) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '正常' : '停用'}</Tag> },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => { setEditingData(record); dataForm.setFieldsValue(record); setDataModalOpen(true) }} />
<Popconfirm title="确认删除?" onConfirm={async () => { await dictApi.deleteData(record.id); message.success('删除成功'); loadDictData(selectedType) }}><Button type="link" danger icon={<DeleteOutlined />} /></Popconfirm>
</Space>
)},
]
return (
<div style={{ padding: 24 }}>
<Row gutter={16}>
<Col span={10}>
<Card title="字典类型" extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingType(null); typeForm.resetFields(); setTypeModalOpen(true) }}></Button>}>
<Table<DictType> rowKey="id" columns={typeColumns} dataSource={dictTypes} pagination={false} size="small" />
</Card>
</Col>
<Col span={14}>
<Card title={`字典数据 ${selectedType ? `- ${selectedType}` : ''}`} extra={selectedType ? <Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingData(null); dataForm.resetFields(); setDataModalOpen(true) }}></Button> : null}>
{selectedType ? <Table<DictData> rowKey="id" columns={dataColumns} dataSource={dictData} loading={loading} pagination={false} size="small" /> : <p style={{ color: '#999', textAlign: 'center' }}></p>}
</Card>
</Col>
</Row>
<Modal title={editingType ? '编辑字典类型' : '新增字典类型'} open={typeModalOpen} onOk={handleTypeSubmit} onCancel={() => setTypeModalOpen(false)} destroyOnClose>
<Form form={typeForm} layout="vertical">
<Form.Item name="dictName" label="字典名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="dictType" label="字典类型" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="status" label="状态" initialValue={1}><Select options={[{ label: '正常', value: 1 }, { label: '停用', value: 0 }]} /></Form.Item>
<Form.Item name="remark" label="备注"><Input.TextArea /></Form.Item>
</Form>
</Modal>
<Modal title={editingData ? '编辑字典数据' : '新增字典数据'} open={dataModalOpen} onOk={handleDataSubmit} onCancel={() => setDataModalOpen(false)} destroyOnClose>
<Form form={dataForm} layout="vertical">
<Form.Item name="dictLabel" label="字典标签" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="dictValue" label="字典值" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="sort" label="排序" initialValue={0}><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
<Form.Item name="status" label="状态" initialValue={1}><Select options={[{ label: '正常', value: 1 }, { label: '停用', value: 0 }]} /></Form.Item>
<Form.Item name="remark" label="备注"><Input.TextArea /></Form.Item>
</Form>
</Modal>
</div>
)
}
@@ -1,3 +1,100 @@
export default function Dashboard() {
return <div>Dashboard Page (TODO)</div>
import { useEffect, useRef, useState } from 'react'
import { Row, Col, Card, Statistic, Timeline, Spin } from 'antd'
import { UserOutlined, TeamOutlined, FileOutlined, WarningOutlined } from '@ant-design/icons'
import { userApi } from '@/api/user.api'
import { roleApi } from '@/api/role.api'
import { exceptionLogApi } from '@/api/exceptionLog'
import { operationLogApi } from '@/api/operationLog'
interface DashboardData {
userCount: number
roleCount: number
opLogCount: number
exLogCount: number
recentOps: { operation: string; username: string; time: string }[]
}
export default function Dashboard() {
const [data, setData] = useState<DashboardData>({
userCount: 0,
roleCount: 0,
opLogCount: 0,
exLogCount: 0,
recentOps: [],
})
const [loading, setLoading] = useState(true)
const chartRef = useRef<HTMLDivElement>(null)
useEffect(() => {
loadDashboard()
}, [])
async function loadDashboard() {
try {
const [users, roles, opLogs, exLogs] = await Promise.all([
userApi.getAll().catch(() => []),
roleApi.getAll().catch(() => []),
operationLogApi.getCount().catch(() => 0),
exceptionLogApi.getCount().catch(() => 0),
])
setData({
userCount: Array.isArray(users) ? users.length : 0,
roleCount: Array.isArray(roles) ? roles.length : 0,
opLogCount: typeof opLogs === 'number' ? opLogs : 0,
exLogCount: typeof exLogs === 'number' ? exLogs : 0,
recentOps: [],
})
} finally {
setLoading(false)
}
}
if (loading) {
return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />
}
return (
<div style={{ padding: 24 }}>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic title="用户总数" value={data.userCount} prefix={<UserOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic title="角色总数" value={data.roleCount} prefix={<TeamOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic title="操作日志" value={data.opLogCount} prefix={<FileOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic title="异常日志" value={data.exLogCount} prefix={<WarningOutlined />} valueStyle={{ color: data.exLogCount > 0 ? '#cf1322' : undefined }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 24 }}>
<Col xs={24} lg={16}>
<Card title="最近活动" ref={chartRef}>
<div ref={chartRef} style={{ height: 300 }}>
<p style={{ color: '#999', textAlign: 'center', paddingTop: 120 }}>G2 ( @antv/g2)</p>
</div>
</Card>
</Col>
<Col xs={24} lg={8}>
<Card title="最近操作">
<Timeline
items={data.recentOps.length > 0 ? data.recentOps.map((op) => ({ children: `${op.username}: ${op.operation}`, color: 'blue' })) : [{ children: '暂无最近操作记录', color: 'gray' }]}
/>
</Card>
</Col>
</Row>
</div>
)
}
+67 -1
View File
@@ -1,3 +1,69 @@
import { useState, useEffect } from 'react'
import { Table, Button, Modal, Upload, Image, Space, message, Popconfirm, Tag } from 'antd'
import { UploadOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { fileApi } from '@/api/file'
import type { FileInfo, FilePageRequest } from '@/api/file'
import type { PageResponse } from '@/api/user.api'
import PermissionGuard from '@/components/PermissionGuard'
export default function FileManagement() {
return <div>FileManagement Page (TODO)</div>
const [files, setFiles] = useState<FileInfo[]>([])
const [loading, setLoading] = useState(false)
const [previewOpen, setPreviewOpen] = useState(false)
const [previewUrl, setPreviewUrl] = useState('')
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 })
useEffect(() => { loadFiles() }, [])
async function loadFiles() {
setLoading(true)
try {
const params: FilePageRequest = { page: pagination.current - 1, size: pagination.pageSize }
const res = await fileApi.getPage(params)
const data = res as unknown as PageResponse<FileInfo>
setFiles(data.content)
setPagination((prev) => ({ ...prev, total: data.totalElements }))
} catch { message.error('加载文件列表失败') }
finally { setLoading(false) }
}
async function handleUpload(file: File) {
try { await fileApi.upload(file); message.success('上传成功'); loadFiles() }
catch { message.error('上传失败') }
return false
}
async function handleDelete(id: number) {
try { await fileApi.delete(id); message.success('删除成功'); loadFiles() }
catch { message.error('删除失败') }
}
const columns: ColumnsType<FileInfo> = [
{ title: '文件名', dataIndex: 'fileName', key: 'fileName' },
{ title: '类型', dataIndex: 'fileType', key: 'fileType', render: (v: string) => <Tag>{v}</Tag> },
{ title: '大小', dataIndex: 'fileSize', key: 'fileSize', render: (v: number) => `${(v / 1024).toFixed(1)} KB` },
{ title: '上传者', dataIndex: 'uploadedBy', key: 'uploadedBy' },
{ title: '上传时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
{record.mimeType?.startsWith('image/') && <Button type="link" onClick={() => { setPreviewUrl(record.filePath); setPreviewOpen(true) }}></Button>}
<PermissionGuard permission="system:file:delete"><Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}><Button type="link" danger icon={<DeleteOutlined />} /></Popconfirm></PermissionGuard>
</Space>
)},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<Upload beforeUpload={handleUpload} showUploadList={false}><Button type="primary" icon={<UploadOutlined />}></Button></Upload>
<Button icon={<ReloadOutlined />} onClick={loadFiles}></Button>
</Space>
</div>
<Table<FileInfo> rowKey="id" columns={columns} dataSource={files} loading={loading}
pagination={{ ...pagination, showSizeChanger: true, showTotal: (t) => `${t}`, onChange: (p, ps) => { setPagination((prev) => ({ ...prev, current: p, pageSize: ps })); setTimeout(loadFiles, 0) } }} />
<Modal open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null}><Image src={previewUrl} style={{ width: '100%' }} /></Modal>
</div>
)
}
@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react'
import { Table, Input, Space, 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 { ExLog, ExLogPageRequest } from '@/api/loginLog'
import type { PageResponse } from '@/api/user.api'
export default function ExLogPage() {
const [logs, setLogs] = useState<ExLog[]>([])
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: ExLogPageRequest = { page: pagination.current - 1, size: pagination.pageSize, keyword }
const res = await loginLogApi.getExLogs(params)
const data = res as unknown as PageResponse<ExLog>
setLogs(data.content)
setPagination((prev) => ({ ...prev, total: data.totalElements }))
} catch { message.error('加载异常日志失败') }
finally { setLoading(false) }
}
const columns: ColumnsType<ExLog> = [
{ title: '请求路径', dataIndex: 'url', key: 'url', ellipsis: true },
{ title: '请求方法', dataIndex: 'method', key: 'method' },
{ title: '异常信息', dataIndex: 'message', key: 'message', ellipsis: true },
{ title: '异常堆栈', dataIndex: 'stackTrace', key: 'stackTrace', ellipsis: true },
{ title: 'IP地址', dataIndex: 'ip', key: 'ip' },
{ title: '操作人', dataIndex: 'operator', key: 'operator' },
{ title: '发生时间', dataIndex: 'createdAt', key: 'createdAt' },
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<Input placeholder="搜索路径/异常信息" value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={<SearchOutlined />} />
<Button icon={<ReloadOutlined />} onClick={loadLogs}></Button>
</Space>
</div>
<Table<ExLog> 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) } }} />
</div>
)
}
@@ -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<LoginLog[]>([])
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<LoginLog>
setLogs(data.content)
setPagination((prev) => ({ ...prev, total: data.totalElements }))
} catch { message.error('加载登录日志失败') }
finally { setLoading(false) }
}
const columns: ColumnsType<LoginLog> = [
{ 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) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '成功' : '失败'}</Tag> },
{ title: '消息', dataIndex: 'message', key: 'message', ellipsis: true },
{ title: '登录时间', dataIndex: 'loginTime', key: 'loginTime' },
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<Input placeholder="搜索用户名/IP" value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={<SearchOutlined />} />
<Button icon={<ReloadOutlined />} onClick={loadLogs}></Button>
</Space>
</div>
<Table<LoginLog> 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) } }} />
</div>
)
}
@@ -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<OpLog[]>([])
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<OpLog>
setLogs(data.content)
setPagination((prev) => ({ ...prev, total: data.totalElements }))
} catch { message.error('加载操作日志失败') }
finally { setLoading(false) }
}
const columns: ColumnsType<OpLog> = [
{ 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) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '成功' : '失败'}</Tag> },
{ title: '操作时间', dataIndex: 'createdAt', key: 'createdAt' },
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<Input placeholder="搜索操作人/描述" value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={loadLogs} style={{ width: 240 }} prefix={<SearchOutlined />} />
<Button icon={<ReloadOutlined />} onClick={loadLogs}></Button>
</Space>
</div>
<Table<OpLog> 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) } }} />
</div>
)
}
+43 -1
View File
@@ -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 <div>Login Page (TODO)</div>
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 (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: '#f0f2f5' }}>
<Card style={{ width: 400 }} title="Novalon 管理系统">
<Form<LoginFormValues> onFinish={handleLogin} autoComplete="off" size="large">
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined />} placeholder="用户名" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
@@ -0,0 +1,4 @@
export interface LoginFormValues {
username: string
password: string
}
+84 -1
View File
@@ -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 <div>NoticeManagement Page (TODO)</div>
const [notices, setNotices] = useState<Notice[]>([])
const [loading, setLoading] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [editingNotice, setEditingNotice] = useState<Notice | null>(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<Notice>
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<Notice> = [
{ title: '标题', dataIndex: 'title', key: 'title' },
{ title: '类型', dataIndex: 'type', key: 'type', render: (v: string) => <Tag>{v}</Tag> },
{ title: '状态', dataIndex: 'status', key: 'status', render: (s: NoticeStatus) => { const info = noticeStatusMap[s]; return <Tag color={info?.color || 'default'}>{info?.label || s}</Tag> } },
{ title: '创建者', dataIndex: 'createdBy', key: 'createdBy' },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作', key: 'action', render: (_, record) => (
<Space>
<PermissionGuard permission="system:notice:edit"><Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)} /></PermissionGuard>
<PermissionGuard permission="system:notice:delete"><Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}><Button type="link" danger icon={<DeleteOutlined />} /></Popconfirm></PermissionGuard>
</Space>
)},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<PermissionGuard permission="system:notice:add"><Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button></PermissionGuard>
<Button icon={<ReloadOutlined />} onClick={loadNotices}></Button>
</Space>
</div>
<Table<Notice> 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) } }} />
<Modal title={editingNotice ? '编辑通知' : '新增通知'} open={modalOpen} onOk={handleSubmit} onCancel={() => setModalOpen(false)} destroyOnClose width={640}>
<Form form={form} layout="vertical">
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}><Input /></Form.Item>
<Form.Item name="type" label="类型"><Select options={[{ label: '通知', value: 'notice' }, { label: '公告', value: 'announcement' }]} /></Form.Item>
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}><Input.TextArea rows={6} /></Form.Item>
<Form.Item name="status" label="状态" initialValue={NoticeStatus.ACTIVE}><Select options={Object.entries(noticeStatusMap).map(([v, info]) => ({ label: info.label, value: v }))} /></Form.Item>
</Form>
</Modal>
</div>
)
}
@@ -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 <div>MenuManagement Page (TODO)</div>
const [menus, setMenus] = useState<MenuItem[]>([])
const [loading, setLoading] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [editingMenu, setEditingMenu] = useState<MenuItem | null>(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<MenuItem> = [
{ 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<string, { label: string; color: string }> = {
directory: { label: '目录', color: 'blue' },
menu: { label: '菜单', color: 'green' },
button: { label: '按钮', color: 'orange' },
}
const info = map[type]
return info ? <Tag color={info.color}>{info.label}</Tag> : 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 <Tag color={info?.color || 'default'}>{info?.label || status}</Tag>
},
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<PermissionGuard permission="system:menu:add">
<Button type="link" size="small" onClick={() => handleAdd(record.id)}></Button>
</PermissionGuard>
<PermissionGuard permission="system:menu:edit">
<Button type="link" icon={<EditOutlined />} size="small" onClick={() => handleEdit(record)} />
</PermissionGuard>
<PermissionGuard permission="system:menu:delete">
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />} size="small" />
</Popconfirm>
</PermissionGuard>
</Space>
),
},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<PermissionGuard permission="system:menu:add">
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleAdd()}></Button>
</PermissionGuard>
<Button icon={<ReloadOutlined />} onClick={loadMenus}></Button>
</Space>
</div>
<Table<MenuItem>
rowKey="id"
columns={columns}
dataSource={menus}
loading={loading}
expandable={{ defaultExpandAllRows: true }}
pagination={false}
/>
<Modal
title={editingMenu ? '编辑菜单' : '新增菜单'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
destroyOnClose
width={600}
>
<Form form={form} layout="vertical">
<Form.Item name="parentId" label="上级菜单" initialValue={0}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="name" label="菜单名称" rules={[{ required: true, message: '请输入菜单名称' }]}>
<Input />
</Form.Item>
<Form.Item name="type" label="类型" initialValue="menu">
<Select options={[
{ label: '目录', value: 'directory' },
{ label: '菜单', value: 'menu' },
{ label: '按钮', value: 'button' },
]} />
</Form.Item>
<Form.Item name="path" label="路径">
<Input />
</Form.Item>
<Form.Item name="icon" label="图标">
<Input />
</Form.Item>
<Form.Item name="component" label="组件路径">
<Input />
</Form.Item>
<Form.Item name="permission" label="权限标识">
<Input />
</Form.Item>
<Form.Item name="sort" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="status" label="状态" initialValue={MenuStatus.ACTIVE}>
<Select options={Object.entries(menuStatusMap).map(([value, info]) => ({ label: info.label, value }))} />
</Form.Item>
<Form.Item name="visible" label="可见" initialValue={true}>
<Select options={[{ label: '显示', value: true }, { label: '隐藏', value: false }]} />
</Form.Item>
</Form>
</Modal>
</div>
)
}
@@ -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 <div>RoleManagement Page (TODO)</div>
const [roles, setRoles] = useState<Role[]>([])
const [loading, setLoading] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [editingRole, setEditingRole] = useState<Role | null>(null)
const [permissions, setPermissions] = useState<Permission[]>([])
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<Role>
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<Role> = [
{ 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 <Tag color={info?.color || 'default'}>{info?.label || status}</Tag>
},
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<PermissionGuard permission="system:role:edit">
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</PermissionGuard>
<PermissionGuard permission="system:role:delete">
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />} />
</Popconfirm>
</PermissionGuard>
</Space>
),
},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16 }}>
<Space>
<PermissionGuard permission="system:role:add">
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</PermissionGuard>
<Button icon={<ReloadOutlined />} onClick={loadRoles}></Button>
</Space>
</div>
<Table<Role>
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)
},
}}
/>
<Modal
title={editingRole ? '编辑角色' : '新增角色'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
destroyOnClose
width={600}
>
<Form form={form} layout="vertical">
<Form.Item name="roleName" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
<Input />
</Form.Item>
<Form.Item name="roleKey" label="角色标识" rules={[{ required: true, message: '请输入角色标识' }]}>
<Input />
</Form.Item>
<Form.Item name="roleSort" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="status" label="状态" initialValue={RoleStatus.ACTIVE}>
<Select options={Object.entries(roleStatusMap).map(([value, info]) => ({ label: info.label, value }))} />
</Form.Item>
<Form.Item name="permissions" label="权限">
<TreeSelect
treeData={permissionTreeData}
multiple
treeCheckable
showCheckedStrategy={TreeSelect.SHOW_CHILD}
placeholder="请选择权限"
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}
function buildPermissionTree(permissions: Permission[]) {
const grouped: Record<string, Permission[]> = {}
for (const p of permissions) {
const key = p.resource
if (!grouped[key]) grouped[key] = []
grouped[key].push(p)
}
return Object.entries(grouped).map(([resource, items]) => ({
title: resource,
value: `resource-${resource}`,
key: `resource-${resource}`,
children: items.map((item) => ({
title: `${item.name} (${item.action})`,
value: item.id,
key: item.id,
})),
}))
}
@@ -1,3 +1,198 @@
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 { userApi } from '@/api/user.api'
import { roleApi } from '@/api/role.api'
import type { User, CreateUserRequest, UpdateUserRequest, UserPageRequest } from '@/api/user.api'
import type { Role } from '@/api/role.api'
import type { PageResponse } from '@/api/user.api'
import { UserStatus, userStatusMap } from '@/constants/status'
import PermissionGuard from '@/components/PermissionGuard'
export default function UserManagement() {
return <div>UserManagement Page (TODO)</div>
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [editingUser, setEditingUser] = useState<User | null>(null)
const [roles, setRoles] = useState<Role[]>([])
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 })
const [form] = Form.useForm()
useEffect(() => {
loadUsers()
loadRoles()
}, [])
async function loadUsers() {
setLoading(true)
try {
const params: UserPageRequest = { page: pagination.current - 1, size: pagination.pageSize }
const res = await userApi.getPage(params)
const data = res as unknown as PageResponse<User>
setUsers(data.content)
setPagination((prev) => ({ ...prev, total: data.totalElements }))
} catch {
message.error('加载用户列表失败')
} finally {
setLoading(false)
}
}
async function loadRoles() {
try {
const res = await roleApi.getAll()
setRoles(Array.isArray(res) ? res : [])
} catch {}
}
function handleAdd() {
setEditingUser(null)
form.resetFields()
setModalOpen(true)
}
function handleEdit(record: User) {
setEditingUser(record)
form.setFieldsValue({
nickname: record.nickname,
email: record.email,
phone: record.phone,
roles: record.roles,
status: record.status,
})
setModalOpen(true)
}
async function handleDelete(id: string) {
try {
await userApi.delete(id)
message.success('删除成功')
loadUsers()
} catch {
message.error('删除失败')
}
}
async function handleSubmit() {
try {
const values = await form.validateFields()
if (editingUser) {
const data: UpdateUserRequest = { ...values }
await userApi.update(editingUser.id, data)
message.success('更新成功')
} else {
const data: CreateUserRequest = { ...values }
await userApi.create(data)
message.success('创建成功')
}
setModalOpen(false)
loadUsers()
} catch {}
}
const columns: ColumnsType<User> = [
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '手机', dataIndex: 'phone', key: 'phone' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: UserStatus) => {
const info = userStatusMap[status]
return <Tag color={info?.color || 'default'}>{info?.label || status}</Tag>
},
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
render: (roles: string[]) => roles?.map((r) => <Tag key={r}>{r}</Tag>),
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<PermissionGuard permission="system:user:edit">
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
</PermissionGuard>
<PermissionGuard permission="system:user:delete">
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />} />
</Popconfirm>
</PermissionGuard>
</Space>
),
},
]
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Space>
<PermissionGuard permission="system:user:add">
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</PermissionGuard>
<Button icon={<ReloadOutlined />} onClick={loadUsers}></Button>
</Space>
</div>
<Table<User>
rowKey="id"
columns={columns}
dataSource={users}
loading={loading}
pagination={{
...pagination,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPagination((prev) => ({ ...prev, current: page, pageSize }))
setTimeout(loadUsers, 0)
},
}}
/>
<Modal
title={editingUser ? '编辑用户' : '新增用户'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
destroyOnClose
>
<Form form={form} layout="vertical">
{!editingUser && (
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input />
</Form.Item>
)}
{!editingUser && (
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password />
</Form.Item>
)}
<Form.Item name="nickname" label="昵称">
<Input />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input />
</Form.Item>
<Form.Item name="phone" label="手机">
<Input />
</Form.Item>
<Form.Item name="roles" label="角色">
<Select mode="multiple" options={roles.map((r) => ({ label: r.roleName, value: r.roleKey }))} />
</Form.Item>
{editingUser && (
<Form.Item name="status" label="状态">
<Select options={Object.entries(userStatusMap).map(([value, info]) => ({ label: info.label, value }))} />
</Form.Item>
)}
</Form>
</Modal>
</div>
)
}
+3 -3
View File
@@ -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'))