Optimize file name

This commit is contained in:
2024-01-05 13:51:38 +08:00
parent 3d8e55cbea
commit 6c8c6088d1
41 changed files with 177 additions and 162 deletions

662
src/pages/System/Group.tsx Normal file
View File

@@ -0,0 +1,662 @@
import React from 'react'
import Icon from '@ant-design/icons'
import {
COLOR_ERROR_SECONDARY,
COLOR_FONT_SECONDARY,
COLOR_PRODUCTION,
DATABASE_DELETE_SUCCESS,
DATABASE_DUPLICATE_KEY,
DATABASE_INSERT_SUCCESS,
DATABASE_SELECT_SUCCESS,
DATABASE_UPDATE_SUCCESS
} from '@/constants/common.constants'
import { useUpdatedEffect } from '@/util/hooks'
import { hasPermission } from '@/util/auth'
import { utcToLocalTime } from '@/util/datetime'
import {
r_sys_group_add,
r_sys_group_change_status,
r_sys_group_delete,
r_sys_group_delete_list,
r_sys_group_get,
r_sys_group_update,
r_sys_role_get_list
} from '@/services/system'
import Permission from '@/components/common/Permission'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
const Group: React.FC = () => {
const [modal, contextHolder] = AntdModal.useModal()
const [form] = AntdForm.useForm<GroupAddEditParam>()
const formValues = AntdForm.useWatch([], form)
const [newFormValues, setNewFormValues] = useState<GroupAddEditParam>()
const [groupData, setGroupData] = useState<GroupWithRoleGetVo[]>([])
const [isLoading, setIsLoading] = useState(false)
const [tableParams, setTableParams] = useState<TableParam>({
pagination: {
current: 1,
pageSize: 20,
position: ['bottomCenter'],
showTotal: (total, range) =>
`${
range[0] === range[1] ? `${range[0]}` : `${range[0]}~${range[1]}`
} 项 共 ${total}`
}
})
const [searchName, setSearchName] = useState('')
const [isUseRegex, setIsUseRegex] = useState(false)
const [isRegexLegal, setIsRegexLegal] = useState(true)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [isDrawerEdit, setIsDrawerEdit] = useState(false)
const [submittable, setSubmittable] = useState(false)
const [roleData, setRoleData] = useState<RoleVo[]>([])
const [isLoadingRole, setIsLoadingRole] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [tableSelectedItem, setTableSelectedItem] = useState<React.Key[]>([])
const dataColumns: _ColumnsType<GroupWithRoleGetVo> = [
{
title: '名称',
dataIndex: 'name',
width: '15%'
},
{
title: '角色',
dataIndex: 'roles',
render: (value: RoleVo[]) =>
value.length ? (
value.map((role) => (
<AntdTag key={role.id} color={role.enable ? 'purple' : 'orange'}>
{role.name}
</AntdTag>
))
) : (
<AntdTag></AntdTag>
)
},
{
title: '创建时间',
dataIndex: 'createTime',
width: '10%',
align: 'center',
render: (value: string) => utcToLocalTime(value)
},
{
title: '修改时间',
dataIndex: 'updateTime',
width: '10%',
align: 'center',
render: (value: string) => utcToLocalTime(value)
},
{
title: '状态',
dataIndex: 'enable',
width: '5%',
align: 'center',
render: (value) =>
value ? <AntdTag color={'success'}></AntdTag> : <AntdTag></AntdTag>
},
{
title: '操作',
dataIndex: 'enable',
width: '15em',
align: 'center',
render: (value, record) => (
<>
<AntdSpace size={'middle'}>
<Permission operationCode={'system:group:modify:status'}>
{value ? (
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnChangStatusBtnClick(record.id, false)}
>
</a>
) : (
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnChangStatusBtnClick(record.id, true)}
>
</a>
)}
</Permission>
<Permission operationCode={'system:group:modify:one'}>
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnEditBtnClick(record)}
>
</a>
</Permission>
<Permission operationCode={'system:group:delete:one'}>
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnDeleteBtnClick(record)}
>
</a>
</Permission>
</AntdSpace>
</>
)
}
]
const handleOnTableChange = (
pagination: _TablePaginationConfig,
filters: Record<string, _FilterValue | null>,
sorter: _SorterResult<GroupWithRoleGetVo> | _SorterResult<GroupWithRoleGetVo>[]
) => {
pagination = { ...tableParams.pagination, ...pagination }
if (Array.isArray(sorter)) {
setTableParams({
pagination,
filters,
sortField: sorter.map((value) => value.field).join(',')
})
} else {
setTableParams({
pagination,
filters,
sortField: sorter.field,
sortOrder: sorter.order
})
}
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
setGroupData([])
}
}
const handleOnTableSelectChange = (selectedRowKeys: React.Key[]) => {
setTableSelectedItem(selectedRowKeys)
}
const handleOnAddBtnClick = () => {
setIsDrawerEdit(false)
setIsDrawerOpen(true)
form.setFieldValue('id', undefined)
form.setFieldValue('name', newFormValues?.name)
form.setFieldValue('roleIds', newFormValues?.roleIds)
form.setFieldValue('enable', newFormValues?.enable ?? true)
if (!roleData || !roleData.length) {
getRoleData()
}
}
const handleOnListDeleteBtnClick = () => {
modal
.confirm({
title: '确定删除',
content: `确定删除选中的 ${tableSelectedItem.length} 个用户组吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_group_delete_list(tableSelectedItem)
.then((res) => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
setTimeout(() => {
getGroup()
})
} else {
void message.error('删除失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
const handleOnEditBtnClick = (value: GroupWithRoleGetVo) => {
return () => {
setIsDrawerEdit(true)
setIsDrawerOpen(true)
form.setFieldValue('id', value.id)
form.setFieldValue('name', value.name)
form.setFieldValue(
'roleIds',
value.roles.map((role) => role.id)
)
form.setFieldValue('enable', value.enable)
if (!roleData || !roleData.length) {
getRoleData()
}
void form.validateFields()
}
}
const handleOnDeleteBtnClick = (value: GroupWithRoleGetVo) => {
return () => {
modal
.confirm({
title: '确定删除',
content: `确定删除角色 ${value.name} 吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_group_delete(value.id)
.then((res) => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
setTimeout(() => {
getGroup()
})
} else {
void message.error('删除失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
}
const handleOnDrawerClose = () => {
setIsDrawerOpen(false)
}
const filterOption = (input: string, option?: { label: string; value: string }) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
const handleOnSubmit = () => {
if (isSubmitting) {
return
}
setIsSubmitting(true)
if (isDrawerEdit) {
void r_sys_group_update(formValues)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_UPDATE_SUCCESS:
setIsDrawerOpen(false)
void message.success('更新成功')
getGroup()
break
case DATABASE_DUPLICATE_KEY:
void message.error('已存在相同名称的角色')
break
default:
void message.error('更新失败,请稍后重试')
}
})
.finally(() => {
setIsSubmitting(false)
})
} else {
void r_sys_group_add(formValues)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_INSERT_SUCCESS:
setIsDrawerOpen(false)
void message.success('添加成功')
setNewFormValues(undefined)
getGroup()
break
case DATABASE_DUPLICATE_KEY:
void message.error('已存在相同名称的角色')
break
default:
void message.error('添加失败,请稍后重试')
}
})
.finally(() => {
setIsSubmitting(false)
})
}
}
const handleOnSearchNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchName(e.target.value)
if (isUseRegex) {
try {
RegExp(e.target.value)
setIsRegexLegal(!(e.target.value.includes('{}') || e.target.value.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnSearchNameKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
getGroup()
}
}
const handleOnUseRegexChange = (e: _CheckboxChangeEvent) => {
setIsUseRegex(e.target.checked)
if (e.target.checked) {
try {
RegExp(searchName)
setIsRegexLegal(!(searchName.includes('{}') || searchName.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnQueryBtnClick = () => {
getGroup()
}
const handleOnChangStatusBtnClick = (id: string, newStatus: boolean) => {
return () => {
if (isLoading) {
return
}
setIsLoading(true)
void r_sys_group_change_status({ id, enable: newStatus })
.then((res) => {
const response = res.data
if (response.code === DATABASE_UPDATE_SUCCESS) {
void message.success('更新成功')
setTimeout(() => {
getGroup()
})
} else {
void message.error('更新失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
}
const getGroup = () => {
if (isLoading) {
return
}
if (!isRegexLegal) {
void message.error('非法正则表达式')
return
}
setIsLoading(true)
void r_sys_group_get({
currentPage: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
sortField:
tableParams.sortField && tableParams.sortOrder
? (tableParams.sortField as string)
: undefined,
sortOrder:
tableParams.sortField && tableParams.sortOrder ? tableParams.sortOrder : undefined,
searchName: searchName.trim().length ? searchName : undefined,
searchRegex: isUseRegex ? isUseRegex : undefined,
...tableParams.filters
})
.then((res) => {
const response = res.data
if (response.code === DATABASE_SELECT_SUCCESS) {
const records = response.data?.records
records && setGroupData(records)
response.data &&
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: response.data.total
}
})
} else {
void message.error('获取失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
const getRoleData = () => {
if (isLoadingRole) {
return
}
setIsLoadingRole(true)
void r_sys_role_get_list()
.then((res) => {
const response = res.data
if (response.code === DATABASE_SELECT_SUCCESS) {
response.data && setRoleData(response.data)
} else {
void message.error('获取角色列表失败,请稍后重试')
}
})
.finally(() => {
setIsLoadingRole(false)
})
}
useEffect(() => {
form.validateFields({ validateOnly: true }).then(
() => {
setSubmittable(true)
},
() => {
setSubmittable(false)
}
)
if (!isDrawerEdit && formValues) {
setNewFormValues({
name: formValues.name,
roleIds: formValues.roleIds,
enable: formValues.enable
})
}
}, [formValues])
useUpdatedEffect(() => {
getGroup()
}, [
JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.sortField),
JSON.stringify(tableParams.sortOrder),
JSON.stringify(tableParams.pagination?.pageSize),
JSON.stringify(tableParams.pagination?.current)
])
const toolbar = (
<FlexBox direction={'horizontal'} gap={10}>
<Permission operationCode={'system:group:add:one'}>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton
type={'primary'}
style={{ padding: '4px 8px' }}
onClick={handleOnAddBtnClick}
>
<Icon component={IconOxygenPlus} style={{ fontSize: '1.2em' }} />
</AntdButton>
</Card>
</Permission>
<Card
hidden={tableSelectedItem.length === 0}
style={{ overflow: 'inherit', flex: '0 0 auto' }}
>
<AntdButton style={{ padding: '4px 8px' }} onClick={handleOnListDeleteBtnClick}>
<Icon component={IconOxygenDelete} style={{ fontSize: '1.2em' }} />
</AntdButton>
</Card>
<Card style={{ overflow: 'inherit' }}>
<AntdInput
addonBefore={
<span
style={{
fontSize: '0.9em',
color: COLOR_FONT_SECONDARY
}}
>
</span>
}
suffix={
<>
{!isRegexLegal ? (
<span style={{ color: COLOR_ERROR_SECONDARY }}></span>
) : undefined}
<AntdCheckbox checked={isUseRegex} onChange={handleOnUseRegexChange}>
<AntdTooltip title={'正则表达式'}>.*</AntdTooltip>
</AntdCheckbox>
</>
}
allowClear
value={searchName}
onChange={handleOnSearchNameChange}
onKeyDown={handleOnSearchNameKeyDown}
status={isRegexLegal ? undefined : 'error'}
/>
</Card>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton onClick={handleOnQueryBtnClick} type={'primary'}>
</AntdButton>
</Card>
</FlexBox>
)
const table = (
<Card>
<AntdTable
dataSource={groupData}
columns={dataColumns}
rowKey={(record) => record.id}
pagination={tableParams.pagination}
loading={isLoading}
onChange={handleOnTableChange}
rowSelection={
hasPermission('system:group:delete:multiple')
? {
type: 'checkbox',
onChange: handleOnTableSelectChange
}
: undefined
}
/>
</Card>
)
const drawerToolbar = (
<AntdSpace>
<AntdTooltip title={'刷新角色列表'}>
<AntdButton onClick={getRoleData} disabled={isSubmitting}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</AntdTooltip>
<AntdButton onClick={handleOnDrawerClose} disabled={isSubmitting}>
</AntdButton>
<AntdButton
type={'primary'}
disabled={!submittable}
loading={isSubmitting}
onClick={handleOnSubmit}
>
</AntdButton>
</AntdSpace>
)
const addAndEditForm = (
<AntdForm form={form} disabled={isSubmitting}>
<AntdForm.Item hidden={!isDrawerEdit} name={'id'} label={'ID'}>
<AntdInput disabled />
</AntdForm.Item>
<AntdForm.Item
name={'name'}
label={'名称'}
rules={[{ required: true, whitespace: false }]}
>
<AntdInput allowClear />
</AntdForm.Item>
<AntdForm.Item name={'roleIds'} label={'角色'}>
<AntdSelect
mode={'multiple'}
allowClear
showSearch
filterOption={filterOption}
options={roleData.map((value) => ({
value: value.id,
label: `${value.name}${!value.enable ? '(已禁用)' : ''}`
}))}
/>
</AntdForm.Item>
<AntdForm.Item
valuePropName={'checked'}
name={'enable'}
label={'启用'}
rules={[{ required: true, type: 'boolean' }]}
>
<AntdSwitch />
</AntdForm.Item>
</AntdForm>
)
return (
<>
<FitFullscreen data-component={'system-group'}>
<HideScrollbar
style={{ padding: 30 }}
isShowVerticalScrollbar
autoHideWaitingTime={500}
>
<FlexBox gap={20}>
{toolbar}
{table}
</FlexBox>
</HideScrollbar>
</FitFullscreen>
<AntdDrawer
title={isDrawerEdit ? '编辑用户组' : '添加用户组'}
width={'36vw'}
onClose={handleOnDrawerClose}
open={isDrawerOpen}
closable={!isSubmitting}
maskClosable={!isSubmitting}
extra={drawerToolbar}
>
{addAndEditForm}
</AntdDrawer>
{contextHolder}
</>
)
}
export default Group

303
src/pages/System/Log.tsx Normal file
View File

@@ -0,0 +1,303 @@
import React from 'react'
import dayjs from 'dayjs'
import { COLOR_FONT_SECONDARY, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { useUpdatedEffect } from '@/util/hooks'
import { dayjsToUtc, utcToLocalTime } from '@/util/datetime'
import { r_sys_log_get } from '@/services/system'
import FitFullscreen from '@/components/common/FitFullscreen'
import Card from '@/components/common/Card'
import HideScrollbar from '@/components/common/HideScrollbar'
import FlexBox from '@/components/common/FlexBox'
const Log: React.FC = () => {
const [logData, setLogData] = useState<SysLogGetVo[]>([])
const [loading, setLoading] = useState(false)
const [tableParams, setTableParams] = useState<TableParam>({
pagination: {
current: 1,
pageSize: 20,
position: ['bottomCenter'],
showTotal: (total, range) =>
`${
range[0] === range[1] ? `${range[0]}` : `${range[0]}~${range[1]}`
} 项 共 ${total}`
}
})
const [searchRequestUrl, setSearchRequestUrl] = useState('')
const [timeRange, setTimeRange] = useState<[string, string]>()
const dataColumns: _ColumnsType<SysLogGetVo> = [
{
title: '类型',
dataIndex: 'logType',
render: (value) =>
value === 'ERROR' ? (
<AntdTag color={'error'}>{value}</AntdTag>
) : (
<AntdTag>{value}</AntdTag>
),
align: 'center',
filters: [
{ text: 'Info', value: 'INFO' },
{ text: 'Login', value: 'LOGIN' },
{ text: 'Logout', value: 'LOGOUT' },
{ text: 'Register', value: 'Register' },
{ text: 'Statistics', value: 'STATISTICS' },
{ text: 'API', value: 'API' },
{ text: 'Error', value: 'ERROR' }
]
},
{
title: '操作者',
dataIndex: 'operateUsername',
align: 'center',
render: (value, record) =>
value ? (
<AntdTag color={'purple'}>{`${value}(${record.operateUserId})`}</AntdTag>
) : (
<AntdTag>Anonymous</AntdTag>
)
},
{
title: '请求方式',
dataIndex: 'requestMethod',
align: 'center',
filters: [
{ text: 'GET', value: 'GET' },
{ text: 'POST', value: 'POST' },
{ text: 'PUT', value: 'PUT' },
{ text: 'PATCH', value: 'PATCH' },
{ text: 'DELETE', value: 'DELETE' },
{ text: 'HEAD', value: 'HEAD' },
{ text: 'OPTIONS', value: 'OPTIONS' }
]
},
{
title: '请求 Url',
render: (_value, record) =>
`${record.requestServerAddress}${record.requestUri}${
record.requestParams ? `?${record.requestParams}` : ''
}`,
onCell: () => ({
style: {
wordBreak: 'break-word'
}
})
},
{
title: '请求 IP',
dataIndex: 'requestIp',
align: 'center'
},
{
title: '开始时间',
dataIndex: 'startTime',
render: (value: string) => utcToLocalTime(value),
align: 'center',
sorter: true
},
{
title: '执行时间',
dataIndex: 'executeTime',
render: (value, record) => (
<AntdTooltip
title={`${utcToLocalTime(record.startTime)} ~ ${utcToLocalTime(
record.endTime
)}`}
>
{`${value}ms`}
</AntdTooltip>
),
align: 'center',
sorter: true
},
{
title: '异常',
dataIndex: 'exception',
render: (value: boolean, record) => (value ? record.exceptionInfo : '无'),
align: 'center',
onCell: () => ({
style: {
wordBreak: 'break-word'
}
})
},
{
title: '用户代理',
dataIndex: 'userAgent',
onCell: () => ({
style: {
wordBreak: 'break-word'
}
})
}
]
const handleOnTableChange = (
pagination: _TablePaginationConfig,
filters: Record<string, _FilterValue | null>,
sorter: _SorterResult<SysLogGetVo> | _SorterResult<SysLogGetVo>[]
) => {
pagination = { ...tableParams.pagination, ...pagination }
if (Array.isArray(sorter)) {
setTableParams({
pagination,
filters,
sortField: sorter.map((value) => value.field).join(',')
})
} else {
setTableParams({
pagination,
filters,
sortField: sorter.field,
sortOrder: sorter.order
})
}
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
setLogData([])
}
}
const handleOnSearchUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchRequestUrl(e.target.value)
}
const handleOnSearchUrlKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
getLog()
}
}
const handleOnDateRangeChange = (dates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
if (dates && dates[0] && dates[1]) {
setTimeRange([dayjsToUtc(dates[0]), dayjsToUtc(dates[1])])
} else {
setTimeRange(undefined)
}
}
const handleOnQueryBtnClick = () => {
getLog()
}
const getLog = () => {
if (loading) {
return
}
setLoading(true)
void r_sys_log_get({
currentPage: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
sortField:
tableParams.sortField && tableParams.sortOrder
? (tableParams.sortField as string)
: undefined,
sortOrder:
tableParams.sortField && tableParams.sortOrder ? tableParams.sortOrder : undefined,
searchRequestUrl: searchRequestUrl.trim().length ? searchRequestUrl : undefined,
searchStartTime: timeRange && timeRange[0],
searchEndTime: timeRange && timeRange[1],
...tableParams.filters
})
.then((res) => {
const response = res.data
if (response.code === DATABASE_SELECT_SUCCESS) {
response.data && setLogData(response.data.records)
response.data &&
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: response.data.total
}
})
} else {
void message.error('获取失败,请稍后重试')
}
})
.finally(() => {
setLoading(false)
})
}
useUpdatedEffect(() => {
getLog()
}, [
JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.sortField),
JSON.stringify(tableParams.sortOrder),
JSON.stringify(tableParams.pagination?.pageSize),
JSON.stringify(tableParams.pagination?.current)
])
const toolbar = (
<FlexBox direction={'horizontal'} gap={10}>
<Card style={{ overflow: 'inherit' }}>
<AntdInput
addonBefore={
<span
style={{
fontSize: '0.9em',
color: COLOR_FONT_SECONDARY
}}
>
Url
</span>
}
allowClear
value={searchRequestUrl}
onChange={handleOnSearchUrlChange}
onKeyDown={handleOnSearchUrlKeyDown}
/>
</Card>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdDatePicker.RangePicker
showTime
allowClear
changeOnBlur
onChange={handleOnDateRangeChange}
/>
</Card>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton onClick={handleOnQueryBtnClick} type={'primary'}>
</AntdButton>
</Card>
</FlexBox>
)
const table = (
<Card>
<AntdTable
dataSource={logData}
columns={dataColumns}
rowKey={(record) => record.id}
pagination={tableParams.pagination}
loading={loading}
onChange={handleOnTableChange}
/>
</Card>
)
return (
<>
<FitFullscreen>
<HideScrollbar
style={{ padding: 30 }}
isShowVerticalScrollbar
autoHideWaitingTime={500}
>
<FlexBox gap={20}>
{toolbar}
{table}
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default Log

669
src/pages/System/Role.tsx Normal file
View File

@@ -0,0 +1,669 @@
import React from 'react'
import Icon from '@ant-design/icons'
import {
COLOR_ERROR_SECONDARY,
COLOR_FONT_SECONDARY,
COLOR_PRODUCTION,
DATABASE_DELETE_SUCCESS,
DATABASE_DUPLICATE_KEY,
DATABASE_INSERT_SUCCESS,
DATABASE_SELECT_SUCCESS,
DATABASE_UPDATE_SUCCESS
} from '@/constants/common.constants'
import { useUpdatedEffect } from '@/util/hooks'
import { utcToLocalTime } from '@/util/datetime'
import { hasPermission, powerListToPowerTree } from '@/util/auth'
import {
r_sys_role_add,
r_sys_role_change_status,
r_sys_power_get_list,
r_sys_role_get,
r_sys_role_update,
r_sys_role_delete,
r_sys_role_delete_list
} from '@/services/system'
import Permission from '@/components/common/Permission'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
const Role: React.FC = () => {
const [modal, contextHolder] = AntdModal.useModal()
const [form] = AntdForm.useForm<RoleAddEditParam>()
const formValues = AntdForm.useWatch([], form)
const [newFormValues, setNewFormValues] = useState<RoleAddEditParam>()
const [roleData, setRoleData] = useState<RoleWithPowerGetVo[]>([])
const [isLoading, setIsLoading] = useState(false)
const [tableParams, setTableParams] = useState<TableParam>({
pagination: {
current: 1,
pageSize: 20,
position: ['bottomCenter'],
showTotal: (total, range) =>
`${
range[0] === range[1] ? `${range[0]}` : `${range[0]}~${range[1]}`
} 项 共 ${total}`
}
})
const [searchName, setSearchName] = useState('')
const [isUseRegex, setIsUseRegex] = useState(false)
const [isRegexLegal, setIsRegexLegal] = useState(true)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [isDrawerEdit, setIsDrawerEdit] = useState(false)
const [submittable, setSubmittable] = useState(false)
const [powerTreeData, setPowerTreeData] = useState<_DataNode[]>([])
const [isLoadingPower, setIsLoadingPower] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [tableSelectedItem, setTableSelectedItem] = useState<React.Key[]>([])
const dataColumns: _ColumnsType<RoleWithPowerGetVo> = [
{
title: '名称',
dataIndex: 'name',
width: '15%'
},
{
title: '权限',
dataIndex: 'tree',
render: (value: _DataNode[]) =>
value.length ? <AntdTree treeData={value} /> : <AntdTag></AntdTag>
},
{
title: '创建时间',
dataIndex: 'createTime',
width: '10%',
align: 'center',
render: (value: string) => utcToLocalTime(value)
},
{
title: '修改时间',
dataIndex: 'updateTime',
width: '10%',
align: 'center',
render: (value: string) => utcToLocalTime(value)
},
{
title: '状态',
dataIndex: 'enable',
width: '5%',
align: 'center',
render: (value) =>
value ? <AntdTag color={'success'}></AntdTag> : <AntdTag></AntdTag>
},
{
title: '操作',
dataIndex: 'enable',
width: '15em',
align: 'center',
render: (value, record) => (
<>
<AntdSpace size={'middle'}>
<Permission operationCode={'system:role:modify:status'}>
{value ? (
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnChangStatusBtnClick(record.id, false)}
>
</a>
) : (
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnChangStatusBtnClick(record.id, true)}
>
</a>
)}
</Permission>
<Permission operationCode={'system:role:modify:one'}>
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnEditBtnClick(record)}
>
</a>
</Permission>
<Permission operationCode={'system:role:delete:one'}>
<a
style={{ color: COLOR_PRODUCTION }}
onClick={handleOnDeleteBtnClick(record)}
>
</a>
</Permission>
</AntdSpace>
</>
)
}
]
const handleOnTableChange = (
pagination: _TablePaginationConfig,
filters: Record<string, _FilterValue | null>,
sorter: _SorterResult<RoleWithPowerGetVo> | _SorterResult<RoleWithPowerGetVo>[]
) => {
pagination = { ...tableParams.pagination, ...pagination }
if (Array.isArray(sorter)) {
setTableParams({
pagination,
filters,
sortField: sorter.map((value) => value.field).join(',')
})
} else {
setTableParams({
pagination,
filters,
sortField: sorter.field,
sortOrder: sorter.order
})
}
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
setRoleData([])
}
}
const handleOnTableSelectChange = (selectedRowKeys: React.Key[]) => {
setTableSelectedItem(selectedRowKeys)
}
const handleOnAddBtnClick = () => {
setIsDrawerEdit(false)
setIsDrawerOpen(true)
form.setFieldValue('id', undefined)
form.setFieldValue('name', newFormValues?.name)
form.setFieldValue('powerIds', newFormValues?.powerIds)
form.setFieldValue('enable', newFormValues?.enable ?? true)
if (!powerTreeData || !powerTreeData.length) {
getPowerTreeData()
}
}
const handleOnListDeleteBtnClick = () => {
modal
.confirm({
title: '确定删除',
content: `确定删除选中的 ${tableSelectedItem.length} 个角色吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_role_delete_list(tableSelectedItem)
.then((res) => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
setTimeout(() => {
getRole()
})
} else {
void message.error('删除失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
const handleOnEditBtnClick = (value: RoleWithPowerGetVo) => {
return () => {
setIsDrawerEdit(true)
setIsDrawerOpen(true)
form.setFieldValue('id', value.id)
form.setFieldValue('name', value.name)
form.setFieldValue(
'powerIds',
value.operations.map((operation) => operation.id)
)
form.setFieldValue('enable', value.enable)
if (!powerTreeData || !powerTreeData.length) {
getPowerTreeData()
}
void form.validateFields()
}
}
const handleOnDeleteBtnClick = (value: RoleWithPowerGetVo) => {
return () => {
modal
.confirm({
title: '确定删除',
content: `确定删除角色 ${value.name} 吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_role_delete(value.id)
.then((res) => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
setTimeout(() => {
getRole()
})
} else {
void message.error('删除失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
}
const handleOnDrawerClose = () => {
setIsDrawerOpen(false)
}
const handleOnSubmit = () => {
if (isSubmitting) {
return
}
setIsSubmitting(true)
if (isDrawerEdit) {
void r_sys_role_update(formValues)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_UPDATE_SUCCESS:
setIsDrawerOpen(false)
void message.success('更新成功')
getRole()
break
case DATABASE_DUPLICATE_KEY:
void message.error('已存在相同名称的角色')
break
default:
void message.error('更新失败,请稍后重试')
}
})
.finally(() => {
setIsSubmitting(false)
})
} else {
void r_sys_role_add(formValues)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_INSERT_SUCCESS:
setIsDrawerOpen(false)
void message.success('添加成功')
setNewFormValues(undefined)
getRole()
break
case DATABASE_DUPLICATE_KEY:
void message.error('已存在相同名称的角色')
break
default:
void message.error('添加失败,请稍后重试')
}
})
.finally(() => {
setIsSubmitting(false)
})
}
}
const handleOnSearchNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchName(e.target.value)
if (isUseRegex) {
try {
RegExp(e.target.value)
setIsRegexLegal(!(e.target.value.includes('{}') || e.target.value.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnSearchNameKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
getRole()
}
}
const handleOnUseRegexChange = (e: _CheckboxChangeEvent) => {
setIsUseRegex(e.target.checked)
if (e.target.checked) {
try {
RegExp(searchName)
setIsRegexLegal(!(searchName.includes('{}') || searchName.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnQueryBtnClick = () => {
getRole()
}
const handleOnChangStatusBtnClick = (id: string, newStatus: boolean) => {
return () => {
if (isLoading) {
return
}
setIsLoading(true)
void r_sys_role_change_status({ id, enable: newStatus })
.then((res) => {
const response = res.data
if (response.code === DATABASE_UPDATE_SUCCESS) {
void message.success('更新成功')
setTimeout(() => {
getRole()
})
} else {
void message.error('更新失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
}
const getRole = () => {
if (isLoading) {
return
}
if (!isRegexLegal) {
void message.error('非法正则表达式')
return
}
setIsLoading(true)
void r_sys_role_get({
currentPage: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
sortField:
tableParams.sortField && tableParams.sortOrder
? (tableParams.sortField as string)
: undefined,
sortOrder:
tableParams.sortField && tableParams.sortOrder ? tableParams.sortOrder : undefined,
searchName: searchName.trim().length ? searchName : undefined,
searchRegex: isUseRegex ? isUseRegex : undefined,
...tableParams.filters
})
.then((res) => {
const response = res.data
if (response.code === DATABASE_SELECT_SUCCESS) {
const records = response.data?.records
records?.map((value) => {
value.tree = powerListToPowerTree(
value.modules,
value.menus,
value.funcs,
value.operations
)
return value
})
records && setRoleData(records)
response.data &&
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: response.data.total
}
})
} else {
void message.error('获取失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
const getPowerTreeData = () => {
if (isLoadingPower) {
return
}
setIsLoadingPower(true)
void r_sys_power_get_list()
.then((res) => {
const response = res.data
if (response.code === DATABASE_SELECT_SUCCESS) {
const powerSet = response.data
powerSet &&
setPowerTreeData(
powerListToPowerTree(
powerSet.moduleList,
powerSet.menuList,
powerSet.funcList,
powerSet.operationList
)
)
} else {
void message.error('获取权限列表失败,请稍后重试')
}
})
.finally(() => {
setIsLoadingPower(false)
})
}
useEffect(() => {
form.validateFields({ validateOnly: true }).then(
() => {
setSubmittable(true)
},
() => {
setSubmittable(false)
}
)
if (!isDrawerEdit && formValues) {
setNewFormValues({
name: formValues.name,
powerIds: formValues.powerIds,
enable: formValues.enable
})
}
}, [formValues])
useUpdatedEffect(() => {
getRole()
}, [
JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.sortField),
JSON.stringify(tableParams.sortOrder),
JSON.stringify(tableParams.pagination?.pageSize),
JSON.stringify(tableParams.pagination?.current)
])
const toolbar = (
<FlexBox direction={'horizontal'} gap={10}>
<Permission operationCode={'system:role:add:one'}>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton
type={'primary'}
style={{ padding: '4px 8px' }}
onClick={handleOnAddBtnClick}
>
<Icon component={IconOxygenPlus} style={{ fontSize: '1.2em' }} />
</AntdButton>
</Card>
</Permission>
<Card
hidden={tableSelectedItem.length === 0}
style={{ overflow: 'inherit', flex: '0 0 auto' }}
>
<AntdButton style={{ padding: '4px 8px' }} onClick={handleOnListDeleteBtnClick}>
<Icon component={IconOxygenDelete} style={{ fontSize: '1.2em' }} />
</AntdButton>
</Card>
<Card style={{ overflow: 'inherit' }}>
<AntdInput
addonBefore={
<span
style={{
fontSize: '0.9em',
color: COLOR_FONT_SECONDARY
}}
>
</span>
}
suffix={
<>
{!isRegexLegal ? (
<span style={{ color: COLOR_ERROR_SECONDARY }}></span>
) : undefined}
<AntdCheckbox checked={isUseRegex} onChange={handleOnUseRegexChange}>
<AntdTooltip title={'正则表达式'}>.*</AntdTooltip>
</AntdCheckbox>
</>
}
allowClear
value={searchName}
onChange={handleOnSearchNameChange}
onKeyDown={handleOnSearchNameKeyDown}
status={isRegexLegal ? undefined : 'error'}
/>
</Card>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton onClick={handleOnQueryBtnClick} type={'primary'}>
</AntdButton>
</Card>
</FlexBox>
)
const table = (
<Card>
<AntdTable
dataSource={roleData}
columns={dataColumns}
rowKey={(record) => record.id}
pagination={tableParams.pagination}
loading={isLoading}
onChange={handleOnTableChange}
rowSelection={
hasPermission('system:role:delete:multiple')
? {
type: 'checkbox',
onChange: handleOnTableSelectChange
}
: undefined
}
/>
</Card>
)
const drawerToolbar = (
<AntdSpace>
<AntdTooltip title={'刷新权限列表'}>
<AntdButton onClick={getPowerTreeData} disabled={isSubmitting}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</AntdTooltip>
<AntdButton onClick={handleOnDrawerClose} disabled={isSubmitting}>
</AntdButton>
<AntdButton
type={'primary'}
disabled={!submittable}
loading={isSubmitting}
onClick={handleOnSubmit}
>
</AntdButton>
</AntdSpace>
)
const addAndEditForm = (
<AntdForm form={form} disabled={isSubmitting}>
<AntdForm.Item hidden={!isDrawerEdit} name={'id'} label={'ID'}>
<AntdInput disabled />
</AntdForm.Item>
<AntdForm.Item
name={'name'}
label={'名称'}
rules={[{ required: true, whitespace: false }]}
>
<AntdInput allowClear />
</AntdForm.Item>
<AntdForm.Item name={'powerIds'} label={'权限'}>
<AntdTreeSelect
treeData={powerTreeData}
treeCheckable
treeNodeLabelProp={'fullTitle'}
allowClear
treeNodeFilterProp={'fullTitle'}
loading={isLoadingPower}
/>
</AntdForm.Item>
<AntdForm.Item
valuePropName={'checked'}
name={'enable'}
label={'启用'}
rules={[{ required: true, type: 'boolean' }]}
>
<AntdSwitch />
</AntdForm.Item>
</AntdForm>
)
return (
<>
<FitFullscreen>
<HideScrollbar
style={{ padding: 30 }}
isShowVerticalScrollbar
autoHideWaitingTime={500}
>
<FlexBox gap={20}>
{toolbar}
{table}
</FlexBox>
</HideScrollbar>
</FitFullscreen>
<AntdDrawer
title={isDrawerEdit ? '编辑角色' : '添加角色'}
width={'36vw'}
onClose={handleOnDrawerClose}
open={isDrawerOpen}
closable={!isSubmitting}
maskClosable={!isSubmitting}
extra={drawerToolbar}
>
{addAndEditForm}
</AntdDrawer>
{contextHolder}
</>
)
}
export default Role

View File

@@ -0,0 +1,81 @@
import React from 'react'
import { useUpdatedEffect } from '@/util/hooks'
import { hasPermission } from '@/util/auth'
import { r_sys_settings_base_get, r_sys_settings_base_update } from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
const Base: React.FC = () => {
const [baseForm] = AntdForm.useForm<BaseSettingsParam>()
const baseFormValues = AntdForm.useWatch([], baseForm)
const [loading, setLoading] = useState(false)
const handleOnReset = () => {
getBaseSettings()
}
const handleOnSave = () => {
void r_sys_settings_base_update(baseFormValues).then((res) => {
const response = res.data
if (response.success) {
void message.success('保存设置成功')
getBaseSettings()
} else {
void message.error('保存设置失败,请稍后重试')
}
})
}
const getBaseSettings = () => {
if (loading) {
return
}
setLoading(true)
void r_sys_settings_base_get().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
data && baseForm.setFieldsValue(data)
setLoading(false)
}
})
}
useUpdatedEffect(() => {
getBaseSettings()
}, [])
return (
<>
<SettingsCard
icon={IconOxygenEmail}
title={'基础'}
loading={loading}
onReset={handleOnReset}
onSave={handleOnSave}
modifyOperationCode={'system:settings:modify:base'}
>
<AntdForm
form={baseForm}
labelCol={{ flex: '7em' }}
disabled={!hasPermission('system:settings:modify:base')}
>
<AntdForm.Item label={'应用名称'} name={'appName'}>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label={'应用 URL'} name={'appUrl'}>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label={'验证邮箱 URL'} name={'verifyUrl'}>
<AntdInput placeholder={'验证码使用 ${verifyCode} 代替'} />
</AntdForm.Item>
<AntdForm.Item label={'找回密码 URL'} name={'retrieveUrl'}>
<AntdInput placeholder={'验证码使用 ${retrieveCode} 代替'} />
</AntdForm.Item>
</AntdForm>
</SettingsCard>
</>
)
}
export default Base

View File

@@ -0,0 +1,155 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { useUpdatedEffect } from '@/util/hooks'
import { hasPermission } from '@/util/auth'
import {
r_sys_settings_mail_get,
r_sys_settings_mail_send,
r_sys_settings_mail_update
} from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
const Mail: React.FC = () => {
const [modal, contextHolder] = AntdModal.useModal()
const [mailForm] = AntdForm.useForm<MailSettingsParam>()
const mailFormValues = AntdForm.useWatch([], mailForm)
const [loading, setLoading] = useState(false)
const [mailSendForm] = AntdForm.useForm<MailSendParam>()
const handleOnTest = () => {
void modal.confirm({
title: '发送测试邮件',
content: (
<>
<AntdForm form={mailSendForm}>
<AntdForm.Item
name={'to'}
label={'接收人'}
style={{ marginTop: 10 }}
rules={[{ required: true, type: 'email' }]}
>
<AntdInput />
</AntdForm.Item>
</AntdForm>
<AntdTag style={{ whiteSpace: 'normal' }}>
使
</AntdTag>
</>
),
onOk: () =>
mailSendForm.validateFields().then(
() => {
return new Promise((resolve) => {
void r_sys_settings_mail_send({
to: mailSendForm.getFieldValue('to') as string
}).then((res) => {
const response = res.data
if (response.success) {
void message.success('发送成功')
resolve(true)
} else {
void message.error('发送失败,请检查配置后重试')
resolve(true)
}
})
})
},
() => {
return new Promise((_, reject) => {
reject('未输入接收者')
})
}
)
})
}
const handleOnReset = () => {
getMailSettings()
}
const handleOnSave = () => {
void r_sys_settings_mail_update(mailFormValues).then((res) => {
const response = res.data
if (response.success) {
void message.success('保存设置成功')
getMailSettings()
} else {
void message.error('保存设置失败,请稍后重试')
}
})
}
const getMailSettings = () => {
if (loading) {
return
}
setLoading(true)
void r_sys_settings_mail_get().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
data && mailForm.setFieldsValue(data)
setLoading(false)
}
})
}
useUpdatedEffect(() => {
getMailSettings()
}, [])
return (
<>
<SettingsCard
icon={IconOxygenEmail}
title={'邮件'}
loading={loading}
onReset={handleOnReset}
onSave={handleOnSave}
modifyOperationCode={'system:settings:modify:mail'}
expand={
<AntdButton onClick={handleOnTest} title={'测试'}>
<Icon component={IconOxygenTest} />
</AntdButton>
}
>
<AntdForm
form={mailForm}
labelCol={{ flex: '8em' }}
disabled={!hasPermission('system:settings:modify:mail')}
>
<AntdForm.Item label={'SMTP 服务器'} name={'host'}>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label={'端口'} name={'port'}>
<AntdInputNumber min={0} max={65535} style={{ width: '100%' }} />
</AntdForm.Item>
<AntdForm.Item label={'安全类型'} name={'securityType'}>
<AntdSelect>
<AntdSelect.Option key={'None'}>None</AntdSelect.Option>
<AntdSelect.Option key={'SSL/TLS'}>SSL/TLS</AntdSelect.Option>
<AntdSelect.Option key={'StartTls'}>StartTls</AntdSelect.Option>
</AntdSelect>
</AntdForm.Item>
<AntdForm.Item label={'用户名'} name={'username'}>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label={'密码'} name={'password'}>
<AntdInput.Password />
</AntdForm.Item>
<AntdForm.Item label={'发送者'} name={'from'}>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label={'发送者名称'} name={'fromName'}>
<AntdInput />
</AntdForm.Item>
</AntdForm>
</SettingsCard>
{contextHolder}
</>
)
}
export default Mail

View File

@@ -0,0 +1,163 @@
import React, { useState } from 'react'
import Icon from '@ant-design/icons'
import { DATABASE_DUPLICATE_KEY, DATABASE_INSERT_SUCCESS } from '@/constants/common.constants'
import { useUpdatedEffect } from '@/util/hooks'
import {
r_sys_settings_sensitive_add,
r_sys_settings_sensitive_delete,
r_sys_settings_sensitive_get,
r_sys_settings_sensitive_update
} from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
const SensitiveWord: React.FC = () => {
const [dataSource, setDataSource] = useState<SensitiveWordVo[]>()
const [targetKeys, setTargetKeys] = useState<string[]>([])
const [selectedKeys, setSelectedKeys] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [isAdding, setIsAdding] = useState(false)
const [newWord, setNewWord] = useState('')
const handleOnReset = () => {
getSensitiveWordSettings()
}
const handleOnSave = () => {
targetKeys &&
void r_sys_settings_sensitive_update({ ids: targetKeys }).then((res) => {
const response = res.data
if (response.success) {
void message.success('保存成功')
getSensitiveWordSettings()
} else {
void message.error('保存失败,请稍后重试')
}
})
}
const handleOnDelete = () => {
void r_sys_settings_sensitive_delete(selectedKeys[0]).then((res) => {
const response = res.data
if (response.success) {
void message.success('删除成功')
getSensitiveWordSettings()
} else {
void message.error('删除失败,请稍后重试')
}
})
}
const getSensitiveWordSettings = () => {
if (loading) {
return
}
setLoading(true)
void r_sys_settings_sensitive_get().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
data && setDataSource(data)
data && setTargetKeys(data.filter((value) => value.enable).map((value) => value.id))
setLoading(false)
}
})
}
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewWord(e.target.value)
}
const handleOnAdd = () => {
if (isAdding) {
return
}
setIsAdding(true)
void r_sys_settings_sensitive_add({ word: newWord, enable: false })
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_INSERT_SUCCESS:
void message.success('添加成功')
setNewWord('')
getSensitiveWordSettings()
break
case DATABASE_DUPLICATE_KEY:
void message.error('该词已存在')
break
default:
void message.error('出错了,请稍后重试')
}
})
.finally(() => {
setIsAdding(false)
})
}
useUpdatedEffect(() => {
getSensitiveWordSettings()
}, [])
return (
<>
<SettingsCard
icon={IconOxygenSensitive}
title={'敏感词'}
loading={loading}
onReset={handleOnReset}
onSave={handleOnSave}
modifyOperationCode={'system:settings:modify:sensitive'}
>
<AntdTransfer
listStyle={{ width: '100%', height: 400 }}
oneWay
showSearch
pagination
disabled={isAdding}
titles={[
<span
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end'
}}
>
{selectedKeys?.length === 1 ? (
<AntdTooltip title={'删除选中项'}>
<Icon
style={{ fontSize: '1.2em' }}
component={IconOxygenDelete}
onClick={handleOnDelete}
/>
</AntdTooltip>
) : undefined}
</span>,
'拦截'
]}
dataSource={dataSource}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={setTargetKeys}
onSelectChange={setSelectedKeys}
rowKey={(item) => item.id}
render={(item) => item.word}
/>
<AntdInput
value={newWord}
onChange={handleOnChange}
onPressEnter={handleOnAdd}
disabled={isAdding}
suffix={
<AntdButton type={'primary'} onClick={handleOnAdd} disabled={isAdding}>
<Icon component={IconOxygenPlus} />
</AntdButton>
}
></AntdInput>
</SettingsCard>
</>
)
}
export default SensitiveWord

View File

@@ -0,0 +1,79 @@
import React from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/settings.scss'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import LoadingMask from '@/components/common/LoadingMask'
import Permission from '@/components/common/Permission'
import Base from '@/pages/System/Settings/Base'
import Mail from '@/pages/System/Settings/Mail'
import SensitiveWord from '@/pages/System/Settings/SensitiveWord'
interface SettingsCardProps extends React.PropsWithChildren {
icon: IconComponent
title: string
loading?: boolean
modifyOperationCode?: string
expand?: React.ReactNode
onReset?: () => void
onSave?: () => void
}
export const SettingsCard: React.FC<SettingsCardProps> = (props) => {
return (
<Card>
<FlexBox className={'settings-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{!props.loading ? (
<Permission operationCode={props.modifyOperationCode}>
{props.expand}
<AntdButton onClick={props.onReset} title={'重置'}>
<Icon component={IconOxygenBack} />
</AntdButton>
<AntdButton className={'bt-save'} onClick={props.onSave} title={'保存'}>
<Icon component={IconOxygenSave} />
</AntdButton>
</Permission>
) : undefined}
</FlexBox>
<LoadingMask
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
hidden={!props.loading}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
const Settings: React.FC = () => {
return (
<>
<FitFullscreen data-component={'system-settings'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<FlexBox className={'root-col'}>
<Permission operationCode={'system:settings:query:base'}>
<Base />
</Permission>
<Permission operationCode={'system:settings:query:sensitive'}>
<SensitiveWord />
</Permission>
</FlexBox>
<FlexBox className={'root-col'}>
<Permission operationCode={'system:settings:query:mail'}>
<Mail />
</Permission>
</FlexBox>
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default Settings

View File

@@ -0,0 +1,192 @@
import React from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import { useUpdatedEffect } from '@/util/hooks'
import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_active } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
const ActiveInfo: React.FC = () => {
const activeInfoDivRef = useRef<HTMLDivElement>(null)
const activeInfoEChartsRef = useRef<echarts.EChartsType | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [scope, setScope] = useState('WEAK')
useUpdatedEffect(() => {
const chartResizeObserver = new ResizeObserver(() => {
activeInfoEChartsRef.current?.resize()
})
activeInfoDivRef.current && chartResizeObserver.observe(activeInfoDivRef.current)
return () => {
activeInfoDivRef.current && chartResizeObserver.unobserve(activeInfoDivRef.current)
}
}, [isLoading])
useUpdatedEffect(() => {
getActiveInfo()
}, [])
const handleOnScopeChange = (value: string) => {
setScope(value)
getActiveInfo(value)
}
const handleOnRefresh = () => {
getActiveInfo()
}
const getActiveInfo = (_scope: string = scope) => {
if (isLoading) {
return
}
setIsLoading(true)
void r_sys_statistics_active({ scope: _scope }).then((res) => {
const response = res.data
if (response.success) {
const data = response.data
if (data) {
setIsLoading(false)
setTimeout(() => {
const registerList = data.registerHistory.length
? getTimesBetweenTwoTimes(
data.registerHistory[0].time,
data.registerHistory[data.registerHistory.length - 1].time,
'day'
).map((time) => [
time,
data.registerHistory.find(
(value) =>
value.time.substring(0, 10) === time.substring(0, 10)
)?.count ?? 0
])
: []
const loginList = data.loginHistory.length
? getTimesBetweenTwoTimes(
data.loginHistory[0].time,
data.loginHistory[data.loginHistory.length - 1].time,
'day'
).map((time) => [
time,
data.loginHistory.find(
(value) =>
value.time.substring(0, 10) === time.substring(0, 10)
)?.count ?? 0
])
: []
const verifyList = data.verifyHistory.length
? getTimesBetweenTwoTimes(
data.verifyHistory[0].time,
data.verifyHistory[data.verifyHistory.length - 1].time,
'day'
).map((time) => [
time,
data.verifyHistory.find(
(value) =>
value.time.substring(0, 10) === time.substring(0, 10)
)?.count ?? 0
])
: []
activeInfoEChartsRef.current = echarts.init(
activeInfoDivRef.current,
null,
{ renderer: 'svg' }
)
activeInfoEChartsRef.current?.setOption({
...lineEChartsBaseOption,
useUTC: true,
tooltip: {
...lineEChartsBaseOption.tooltip,
formatter: getTooltipTimeFormatter('yyyy-MM-DD')
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
minValueSpan: 2 * 24 * 60 * 60 * 1000
}
],
series: [
{
name: '注册人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: registerList
},
{
name: '登录人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: loginList
},
{
name: '验证账号人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: verifyList
}
]
})
})
}
}
})
}
return (
<CommonCard
icon={IconOxygenAnalysis}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}></span>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={activeInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
export default ActiveInfo

View File

@@ -0,0 +1,182 @@
import React from 'react'
import * as echarts from 'echarts/core'
import { BarSeriesOption } from 'echarts/charts'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_cpu } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import {
barDefaultSeriesOption,
barEChartsBaseOption,
EChartsOption
} from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
const CPUInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
const cpuInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
const cpuDefaultSeriesOption: BarSeriesOption = {
...barDefaultSeriesOption,
tooltip: {
valueFormatter: (value) => `${((value as number) * 100).toFixed(2)}%`
}
}
useUpdatedEffect(() => {
const chartResizeObserver = new ResizeObserver(() => {
cpuInfoEChartsRef.current.forEach((value) => value.resize())
})
cpuInfoDivRef.current && chartResizeObserver.observe(cpuInfoDivRef.current)
return () => {
cpuInfoDivRef.current && chartResizeObserver.unobserve(cpuInfoDivRef.current)
}
}, [cpuInfoDivRef.current])
useUpdatedEffect(() => {
const intervalId = setInterval(getCpuInfo(), parseInt(refreshInterval) * 1000)
return () => {
clearInterval(intervalId)
}
}, [refreshInterval])
const getCpuInfo = () => {
void r_sys_statistics_cpu().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
if (data) {
if (isLoading) {
setIsLoading(false)
}
setTimeout(() => {
const dataList = data.processors.map((value) =>
cpuInfoVoToCpuInfoData(value)
)
dataList.unshift(cpuInfoVoToCpuInfoData(data))
setCpuInfoEChartsOption(
dataList.map((value, index) => ({
...barEChartsBaseOption,
yAxis: {
...barEChartsBaseOption.yAxis,
data: [index === 0 ? '总占用' : `CPU ${index - 1}`]
},
series: value
}))
)
if (percentDivRef.current) {
percentDivRef.current.innerHTML = ''
dataList.forEach((value) => {
const percentElement = document.createElement('div')
const idle = value.find((item) => item.name === 'idle')?.data[0]
percentElement.innerText =
idle !== undefined
? `${((1 - idle) * 100).toFixed(2)}%`
: 'Unknown'
percentDivRef.current?.appendChild(percentElement)
})
}
if (!cpuInfoEChartsRef.current.length) {
keyDivRef.current && (keyDivRef.current.innerHTML = '')
cpuInfoDivRef.current && (cpuInfoDivRef.current.innerHTML = '')
for (let i = 0; i < dataList.length; i++) {
const keyElement = document.createElement('div')
keyElement.innerText = i === 0 ? '总占用' : `CPU ${i - 1}`
keyDivRef.current?.appendChild(keyElement)
const valueElement = document.createElement('div')
cpuInfoDivRef.current?.appendChild(valueElement)
cpuInfoEChartsRef.current.push(
echarts.init(valueElement, null, { renderer: 'svg' })
)
}
}
})
}
}
})
return getCpuInfo
}
const cpuInfoVoToCpuInfoData = (cpuInfoVo: CpuInfoVo) =>
Object.entries(cpuInfoVo)
.filter(([key]) => !['total', 'processors'].includes(key))
.map(([key, value]) => ({
...cpuDefaultSeriesOption,
name: key,
data: [(value as number) / cpuInfoVo.total]
}))
.sort((a, b) => {
const order = [
'steal',
'irq',
'softirq',
'iowait',
'system',
'nice',
'user',
'idle'
]
return order.indexOf(a.name) - order.indexOf(b.name)
})
useEffect(() => {
cpuInfoEChartsRef.current?.forEach((value, index) => {
try {
value.setOption(cpuInfoEChartsOption[index])
} catch (e) {
/* empty */
}
})
}, [cpuInfoEChartsOption])
return (
<>
<CommonCard
icon={IconOxygenCpu}
title={'CPU 信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
export default CPUInfo

View File

@@ -0,0 +1,65 @@
import React from 'react'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_hardware } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/System/Statistics'
const HardwareInfo: React.FC = () => {
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
useUpdatedEffect(() => {
void r_sys_statistics_hardware().then((res) => {
const response = res.data
if (response.success) {
response.data && setHardwareInfoData(response.data)
} else {
void message.error('获取硬件信息失败,请稍后重试')
}
})
}, [])
return (
<CommonCard
icon={IconOxygenHardware}
title={'硬件信息'}
loading={hardwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div>CPU</div>
<div>CPU </div>
<div></div>
<div>64</div>
<div> CPU</div>
<div></div>
<div></div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={hardwareInfoData?.cpu}>{hardwareInfoData?.cpu}</div>
<div title={hardwareInfoData?.arch}>{hardwareInfoData?.arch}</div>
<div title={hardwareInfoData?.microarchitecture}>
{hardwareInfoData?.microarchitecture}
</div>
<div title={hardwareInfoData?.is64Bit ? '是' : '否'}>
{hardwareInfoData?.is64Bit ? '是' : '否'}
</div>
<div title={hardwareInfoData?.cpuPhysicalPackageCount.toString()}>
{hardwareInfoData?.cpuPhysicalPackageCount}
</div>
<div title={hardwareInfoData?.cpuPhysicalProcessorCount.toString()}>
{hardwareInfoData?.cpuPhysicalProcessorCount}
</div>
<div title={hardwareInfoData?.cpuLogicalProcessorCount.toString()}>
{hardwareInfoData?.cpuLogicalProcessorCount}
</div>
<div title={hardwareInfoData?.memories}>{hardwareInfoData?.memories}</div>
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
export default HardwareInfo

View File

@@ -0,0 +1,149 @@
import React from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import { useUpdatedEffect } from '@/util/hooks'
import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_online } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
const OnlineInfo: React.FC = () => {
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
const onlineInfoEChartsRef = useRef<echarts.EChartsType | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [currentOnlineCount, setCurrentOnlineCount] = useState(-1)
const [scope, setScope] = useState('WEAK')
useUpdatedEffect(() => {
const chartResizeObserver = new ResizeObserver(() => {
onlineInfoEChartsRef.current?.resize()
})
onlineInfoDivRef.current && chartResizeObserver.observe(onlineInfoDivRef.current)
return () => {
onlineInfoDivRef.current && chartResizeObserver.unobserve(onlineInfoDivRef.current)
}
}, [isLoading])
useUpdatedEffect(() => {
getOnlineInfo()
}, [])
const handleOnScopeChange = (value: string) => {
setScope(value)
getOnlineInfo(value)
}
const handleOnRefresh = () => {
getOnlineInfo()
}
const getOnlineInfo = (_scope: string = scope) => {
if (isLoading) {
return
}
setIsLoading(true)
setCurrentOnlineCount(-1)
void r_sys_statistics_online({ scope: _scope }).then((res) => {
const response = res.data
if (response.success) {
const data = response.data
if (data) {
setIsLoading(false)
setCurrentOnlineCount(data.current)
setTimeout(() => {
const dataList = getTimesBetweenTwoTimes(
data.history[0].time,
data.history[data.history.length - 1].time,
'minute'
).map((time) => [
time,
data.history.find(
(value) => value.time.substring(0, 16) === time.substring(0, 16)
)?.record ?? 0
])
onlineInfoEChartsRef.current = echarts.init(
onlineInfoDivRef.current,
null,
{ renderer: 'svg' }
)
onlineInfoEChartsRef.current?.setOption({
...lineEChartsBaseOption,
tooltip: {
...lineEChartsBaseOption.tooltip,
formatter: getTooltipTimeFormatter('yyyy-MM-DD HH:mm')
},
xAxis: {
...lineEChartsBaseOption.xAxis
},
series: [
{
name: '在线人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: dataList
}
]
})
})
}
}
})
}
return (
<CommonCard
icon={IconOxygenOnline}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}>线</span>
<AntdTag>
{currentOnlineCount === -1 ? '获取中...' : `当前 ${currentOnlineCount}`}
</AntdTag>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'DAY'}></AntdSelect.Option>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={onlineInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
export default OnlineInfo

View File

@@ -0,0 +1,70 @@
import React from 'react'
import { useUpdatedEffect } from '@/util/hooks'
import { utcToLocalTime } from '@/util/datetime'
import { r_sys_statistics_software } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/System/Statistics'
const SoftwareInfo: React.FC = () => {
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
useUpdatedEffect(() => {
void r_sys_statistics_software().then((res) => {
const response = res.data
if (response.success) {
response.data && setSoftwareInfoData(response.data)
} else {
void message.error('获取软件信息失败,请稍后重试')
}
})
}, [])
return (
<CommonCard
icon={IconOxygenSoftware}
title={'软件信息'}
loading={softwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div></div>
<div></div>
<div>Java</div>
<div>Java </div>
<div>Runtime</div>
<div>JVM</div>
<div>JVM </div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={softwareInfoData?.os}>{softwareInfoData?.os}</div>
<div title={softwareInfoData?.bitness.toString()}>
{softwareInfoData?.bitness}
</div>
<div
title={`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
>{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}</div>
<div title={softwareInfoData?.javaVendor}>{softwareInfoData?.javaVendor}</div>
<div
title={`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
>{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}</div>
<div
title={`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
>{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}</div>
<div title={softwareInfoData?.jvmVendor}>{softwareInfoData?.jvmVendor}</div>
<div>
{softwareInfoData?.osBootTime &&
utcToLocalTime(softwareInfoData?.osBootTime)}
</div>
<div>
{softwareInfoData?.serverStartupTime &&
utcToLocalTime(softwareInfoData.serverStartupTime)}
</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
export default SoftwareInfo

View File

@@ -0,0 +1,201 @@
import React, { useEffect, useState } from 'react'
import * as echarts from 'echarts/core'
import { BarSeriesOption } from 'echarts/charts'
import { formatByteSize } from '@/util/common'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_storage } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import {
barDefaultSeriesOption,
barEChartsBaseOption,
EChartsOption
} from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
const StorageInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const storageInfoDivRef = useRef<HTMLDivElement>(null)
const storageInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState<EChartsOption[]>([])
const storageDefaultSeriesOption: BarSeriesOption = {
...barDefaultSeriesOption,
tooltip: { valueFormatter: (value) => formatByteSize(value as number) }
}
useUpdatedEffect(() => {
const chartResizeObserver = new ResizeObserver(() => {
storageInfoEChartsRef.current.forEach((value) => value.resize())
})
storageInfoDivRef.current && chartResizeObserver.observe(storageInfoDivRef.current)
return () => {
storageInfoDivRef.current && chartResizeObserver.unobserve(storageInfoDivRef.current)
}
}, [storageInfoDivRef.current])
useUpdatedEffect(() => {
const intervalId = setInterval(getStorageInfo(), parseInt(refreshInterval) * 1000)
return () => {
clearInterval(intervalId)
}
}, [refreshInterval])
const getStorageInfo = () => {
void r_sys_statistics_storage().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
if (data) {
if (isLoading) {
setIsLoading(false)
}
setTimeout(() => {
const eChartsOptions = [
storageInfoVoToStorageEChartsOption(
'物理内存',
data.memoryTotal - data.memoryFree,
data.memoryFree
),
storageInfoVoToStorageEChartsOption(
'虚拟内存',
data.virtualMemoryInUse,
data.virtualMemoryMax - data.virtualMemoryInUse
),
storageInfoVoToStorageEChartsOption(
'swap',
data.swapUsed,
data.swapTotal - data.swapUsed
),
storageInfoVoToStorageEChartsOption(
'jvm 内存',
data.jvmTotal - data.jvmFree,
data.jvmFree
)
]
data.fileStores.forEach((value) =>
eChartsOptions.push(
storageInfoVoToStorageEChartsOption(
value.mount,
value.total - value.free,
value.free
)
)
)
setStorageInfoEChartsOption(eChartsOptions)
if (percentDivRef.current && keyDivRef.current) {
keyDivRef.current.innerHTML = ''
percentDivRef.current.innerHTML = ''
eChartsOptions.forEach((value) => {
const keyElement = document.createElement('div')
const percentElement = document.createElement('div')
keyElement.innerText = value.yAxis.data[0]
percentElement.innerText = `${(
(value.series[0].data[0] /
(value.series[0].data[0] + value.series[1].data[0])) *
100
).toFixed(2)}%`
keyDivRef.current?.appendChild(keyElement)
percentDivRef.current?.appendChild(percentElement)
})
}
if (!storageInfoEChartsRef.current.length) {
storageInfoDivRef.current && (storageInfoDivRef.current.innerHTML = '')
eChartsOptions.forEach(() => {
const element = document.createElement('div')
storageInfoDivRef.current?.appendChild(element)
storageInfoEChartsRef.current.push(
echarts.init(element, null, { renderer: 'svg' })
)
})
}
})
}
}
})
return getStorageInfo
}
const storageInfoVoToStorageEChartsOption = (label: string, used: number, free: number) => ({
...barEChartsBaseOption,
xAxis: {
...barEChartsBaseOption.xAxis,
max: used + free
},
yAxis: {
...barEChartsBaseOption.yAxis,
data: [label]
},
series: [
{
...storageDefaultSeriesOption,
name: 'used',
data: [used]
},
{
...storageDefaultSeriesOption,
name: 'free',
data: [free]
}
]
})
useEffect(() => {
storageInfoEChartsRef.current?.forEach((value, index) => {
try {
value.setOption(storageInfoEChartsOption[index])
} catch (e) {
/* empty */
}
})
}, [storageInfoEChartsOption])
return (
<>
<CommonCard
icon={IconOxygenMemory}
title={'内存信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
export default StorageInfo

View File

@@ -0,0 +1,69 @@
import React from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/statistics.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import LoadingMask from '@/components/common/LoadingMask'
import Permission from '@/components/common/Permission'
import OnlineInfo from '@/pages/System/Statistics/OnlineInfo'
import ActiveInfo from '@/pages/System/Statistics/ActiveInfo'
import SoftwareInfo from '@/pages/System/Statistics/SoftwareInfo'
import HardwareInfo from '@/pages/System/Statistics/HardwareInfo'
import CPUInfo from '@/pages/System/Statistics/CPUInfo'
import StorageInfo from '@/pages/System/Statistics/StorageInfo'
interface CommonCardProps extends React.PropsWithChildren {
icon: IconComponent
title: React.ReactNode
loading?: boolean
expand?: React.ReactNode
}
export const CommonCard: React.FC<CommonCardProps> = (props) => {
return (
<Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{props.expand}
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
const Statistics: React.FC = () => {
return (
<>
<FitFullscreen data-component={'system-statistics'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Permission operationCode={'system:statistics:query:usage'}>
<OnlineInfo />
<ActiveInfo />
</Permission>
<Permission operationCode={'system:statistics:query:base'}>
<HardwareInfo />
<SoftwareInfo />
</Permission>
<Permission operationCode={'system:statistics:query:real'}>
<CPUInfo />
<StorageInfo />
</Permission>
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default Statistics

View File

@@ -0,0 +1,128 @@
import * as echarts from 'echarts/core'
import {
DataZoomComponent,
DataZoomComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption
} from 'echarts/components'
import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import { UniversalTransition } from 'echarts/features'
import { CallbackDataParams } from 'echarts/types/dist/shared'
import { utcToLocalTime } from '@/util/datetime'
echarts.use([
TooltipComponent,
ToolboxComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
BarChart,
LineChart,
SVGRenderer,
UniversalTransition
])
export type EChartsOption = echarts.ComposeOption<
| TooltipComponentOption
| ToolboxComponentOption
| GridComponentOption
| LegendComponentOption
| BarSeriesOption
| DataZoomComponentOption
| LineSeriesOption
>
export const barDefaultSeriesOption: BarSeriesOption = {
type: 'bar',
stack: 'total',
itemStyle: {
color: (params) => {
switch (params.seriesName) {
case 'idle':
case 'free':
return '#F5F5F5'
default:
return params.color ?? echarts.color.random()
}
}
}
}
export const barEChartsBaseOption: EChartsOption = {
tooltip: {},
xAxis: {
show: false
},
yAxis: {
axisLine: {
show: false
},
axisLabel: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisPointer: {
show: false
}
}
}
export const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => {
return (params: CallbackDataParams[]) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
`${utcToLocalTime(params[0].data[0], format)}<br>${params
.map(
(param) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`<span style="display: flex; justify-content: space-between;"><span>${param.marker}${param.seriesName}</span><span style="font-weight: bold; margin-left: 16px;">${param.data[1]}</span></span>`
)
.join('')}`
}
export const lineEChartsBaseOption: EChartsOption = {
tooltip: {
trigger: 'axis'
},
legend: {},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
xAxis: {
type: 'time'
},
yAxis: {
type: 'value',
interval: 1
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
minValueSpan: 2 * 60 * 60 * 1000
}
],
series: [{}]
}

1042
src/pages/System/User.tsx Normal file

File diff suppressed because it is too large Load Diff

107
src/pages/System/index.tsx Normal file
View File

@@ -0,0 +1,107 @@
import React from 'react'
import Icon from '@ant-design/icons'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/pages/system/index.scss'
import HideScrollbar from '@/components/common/HideScrollbar'
import FitFullscreen from '@/components/common/FitFullscreen'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
import Permission from '@/components/common/Permission'
interface CommonCardProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: IconComponent
description?: React.ReactNode
options?: TiltOptions
url?: string
}
const CommonCard = forwardRef<HTMLDivElement, CommonCardProps>(
({
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ref,
icon,
description,
options = {
reverse: true,
max: 8,
glare: true,
scale: 1.03
},
url,
children,
...props
}) => {
const navigate = useNavigate()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
url && navigate(url)
}
return (
<Card
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'common-card'}>
<Icon component={icon} className={'icon'} />
<div className={'text'}>{children}</div>
<div className={'description'}>{description}</div>
</FlexBox>
</Card>
)
}
)
const System: React.FC = () => {
return (
<>
<FitFullscreen data-component={'system'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Permission path={'/system/statistics'}>
<CommonCard icon={IconOxygenAnalysis} url={'statistics'}>
</CommonCard>
</Permission>
<Permission path={'/system/settings'}>
<CommonCard icon={IconOxygenOption} url={'settings'}>
</CommonCard>
</Permission>
<Permission path={'/system/user'}>
<CommonCard icon={IconOxygenUser} url={'user'}>
</CommonCard>
</Permission>
<Permission path={'/system/role'}>
<CommonCard icon={IconOxygenRole} url={'role'}>
</CommonCard>
</Permission>
<Permission path={'/system/group'}>
<CommonCard icon={IconOxygenGroup} url={'group'}>
</CommonCard>
</Permission>
<Permission path={'/system/log'}>
<CommonCard icon={IconOxygenLog} url={'log'}>
</CommonCard>
</Permission>
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default System