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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user