Optimize file name
This commit is contained in:
662
src/pages/System/Group.tsx
Normal file
662
src/pages/System/Group.tsx
Normal 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
303
src/pages/System/Log.tsx
Normal 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
669
src/pages/System/Role.tsx
Normal 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
|
||||
81
src/pages/System/Settings/Base.tsx
Normal file
81
src/pages/System/Settings/Base.tsx
Normal 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
|
||||
155
src/pages/System/Settings/Mail.tsx
Normal file
155
src/pages/System/Settings/Mail.tsx
Normal 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
|
||||
163
src/pages/System/Settings/SensitiveWord.tsx
Normal file
163
src/pages/System/Settings/SensitiveWord.tsx
Normal 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
|
||||
79
src/pages/System/Settings/index.tsx
Normal file
79
src/pages/System/Settings/index.tsx
Normal 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
|
||||
192
src/pages/System/Statistics/ActiveInfo.tsx
Normal file
192
src/pages/System/Statistics/ActiveInfo.tsx
Normal 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
|
||||
182
src/pages/System/Statistics/CPUInfo.tsx
Normal file
182
src/pages/System/Statistics/CPUInfo.tsx
Normal 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
|
||||
65
src/pages/System/Statistics/HardwareInfo.tsx
Normal file
65
src/pages/System/Statistics/HardwareInfo.tsx
Normal 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
|
||||
149
src/pages/System/Statistics/OnlineInfo.tsx
Normal file
149
src/pages/System/Statistics/OnlineInfo.tsx
Normal 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
|
||||
70
src/pages/System/Statistics/SoftwareInfo.tsx
Normal file
70
src/pages/System/Statistics/SoftwareInfo.tsx
Normal 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
|
||||
201
src/pages/System/Statistics/StorageInfo.tsx
Normal file
201
src/pages/System/Statistics/StorageInfo.tsx
Normal 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
|
||||
69
src/pages/System/Statistics/index.tsx
Normal file
69
src/pages/System/Statistics/index.tsx
Normal 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
|
||||
128
src/pages/System/Statistics/shared.ts
Normal file
128
src/pages/System/Statistics/shared.ts
Normal 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
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
107
src/pages/System/index.tsx
Normal 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
|
||||
Reference in New Issue
Block a user