dark
7 months ago
33 changed files with 1437 additions and 594 deletions
-
1.gitignore
-
35src/components/cascader/Cascader.tsx
-
1src/components/cascader/index.ts
-
39src/components/department-tree/DepartmentCascader.tsx
-
17src/components/department-tree/DepartmentTree.tsx
-
2src/components/department-tree/index.ts
-
7src/components/error/404.tsx
-
4src/components/icon/index.tsx
-
2src/components/icon/picker/index.tsx
-
52src/components/role-picker/RolePicker.tsx
-
0src/components/role-picker/index.ts
-
4src/components/switch/index.tsx
-
20src/layout/TreePageLayout.tsx
-
39src/layout/TwoColPageLayout.tsx
-
12src/layout/style.ts
-
20src/pages/system/departments/index.tsx
-
9src/pages/system/departments/style.ts
-
144src/pages/system/logs/login/index.tsx
-
14src/pages/system/logs/login/style.ts
-
31src/pages/system/menus/index.tsx
-
14src/pages/system/roles/index.tsx
-
7src/pages/system/roles/style.ts
-
243src/pages/system/users/index.tsx
-
71src/pages/system/users/style.ts
-
19src/service/system.ts
-
58src/store/logs.ts
-
12src/store/route.ts
-
10src/store/system.ts
-
119src/store/user.ts
-
11src/types/logs.d.ts
-
1src/types/user.d.ts
-
37src/utils/tree.ts
@ -0,0 +1,35 @@ |
|||||
|
import { Cascader as AntCascader, CascaderProps as AntCascaderProps } from 'antd' |
||||
|
import { DefaultOptionType } from 'antd/es/cascader' |
||||
|
import { findValuePath } from '@/utils/tree.ts' |
||||
|
|
||||
|
export type CascaderProps<T extends DefaultOptionType> = AntCascaderProps<T> & { |
||||
|
key?: string |
||||
|
} |
||||
|
|
||||
|
const getValue = (options: DefaultOptionType[], value?: any, fieldNames?: any) => { |
||||
|
if (value === undefined || value === null) return [] |
||||
|
if (Array.isArray(value)) return value |
||||
|
return findValuePath(options as any, value, fieldNames) ?? [] |
||||
|
} |
||||
|
|
||||
|
export const Cascader = ({ value, options = [], fieldNames, ...props }: CascaderProps<DefaultOptionType>) => { |
||||
|
|
||||
|
const f = { |
||||
|
key: fieldNames?.value ?? 'value', |
||||
|
title: fieldNames?.label ?? 'label', |
||||
|
children: fieldNames?.children ?? 'children', |
||||
|
} as any |
||||
|
return ( |
||||
|
<AntCascader {...props} |
||||
|
options={options} |
||||
|
fieldNames={fieldNames} |
||||
|
value={getValue(options, value, f)}> |
||||
|
</AntCascader> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Cascader.displayName = 'Cascader' |
||||
|
Cascader.SHOW_CHILD = AntCascader.SHOW_CHILD |
||||
|
Cascader.SHOW_PARENT = AntCascader.SHOW_PARENT |
||||
|
|
||||
|
export default Cascader |
@ -0,0 +1 @@ |
|||||
|
export * from './Cascader.tsx' |
@ -0,0 +1,39 @@ |
|||||
|
import { Cascader, CascaderProps } from '@/components/cascader' |
||||
|
import { useAtomValue } from 'jotai/index' |
||||
|
import { departTreeAtom } from '@/store/department.ts' |
||||
|
import { usePageStoreOptions } from '@/store' |
||||
|
import { Spin } from 'antd' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
|
||||
|
export interface DepartmentCascaderProps extends Omit<CascaderProps<any>, 'options'> { |
||||
|
onChange?: (value: any) => void |
||||
|
} |
||||
|
|
||||
|
const displayRender = (labels: string[]) => labels[labels.length - 1] |
||||
|
|
||||
|
export const DepartmentCascader = (props: DepartmentCascaderProps) => { |
||||
|
|
||||
|
const { t } = useTranslation() |
||||
|
const { data = [], isLoading } = useAtomValue(departTreeAtom, usePageStoreOptions()) |
||||
|
|
||||
|
const onChange = (value) => { |
||||
|
|
||||
|
props?.onChange?.(value[value.length - 1]) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Spin spinning={isLoading} size={'small'}> |
||||
|
<Cascader changeOnSelect={true} {...props as any} |
||||
|
fieldNames={{ |
||||
|
label: 'name', |
||||
|
value: 'id', |
||||
|
}} |
||||
|
placeholder={t('component.DepartmentCascader.placeholder', '请选择部门')} |
||||
|
onChange={onChange} |
||||
|
displayRender={displayRender} |
||||
|
showCheckedStrategy={Cascader.SHOW_CHILD} |
||||
|
options={data as any}> |
||||
|
</Cascader> |
||||
|
</Spin> |
||||
|
) |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
export * from './DepartmentTree' |
||||
|
export * from './DepartmentCascader.tsx' |
@ -0,0 +1,52 @@ |
|||||
|
import { convertToBool } from '@/utils' |
||||
|
import { Select, SelectProps } from 'antd' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
import { rolesAtom } from '@/store/role.ts' |
||||
|
import { memo } from 'react' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
|
||||
|
export interface RolePickerProps extends SelectProps { |
||||
|
value?: any |
||||
|
onChange?: (value: any) => void |
||||
|
view?: boolean |
||||
|
} |
||||
|
|
||||
|
const formatValue = (value: any) => { |
||||
|
if (value === undefined || value === null) { |
||||
|
return [] |
||||
|
} |
||||
|
if (Array.isArray(value) && typeof value?.[0] === 'object') { |
||||
|
return (value as Array<object>).map(i => i['id']) |
||||
|
} |
||||
|
return value |
||||
|
} |
||||
|
|
||||
|
const RolePicker = memo(({ value, view, ...props }: RolePickerProps) => { |
||||
|
|
||||
|
const { t } = useTranslation() |
||||
|
const { data: roles, isPending } = useAtomValue(rolesAtom) |
||||
|
|
||||
|
if (view) { |
||||
|
return value |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Select showSearch={true} |
||||
|
{...props} |
||||
|
value={formatValue(value)} |
||||
|
mode={'multiple'} |
||||
|
placeholder={t('component.RolePicker.placeholder', '请选择角色')} |
||||
|
options={ |
||||
|
(roles?.rows ?? []).map(i => ({ |
||||
|
label: i.name, |
||||
|
value: i.id, |
||||
|
disabled: !convertToBool(i.status) |
||||
|
})) |
||||
|
} |
||||
|
loading={isPending}/> |
||||
|
</> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export default RolePicker |
@ -1,20 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import { createLazyRoute, Outlet } from '@tanstack/react-router' |
|
||||
|
|
||||
interface ITreePageLayoutProps { |
|
||||
children: React.ReactNode |
|
||||
|
|
||||
} |
|
||||
|
|
||||
const TreePageLayout: React.FC<ITreePageLayoutProps> = (props) => { |
|
||||
return ( |
|
||||
<> |
|
||||
{props.children} |
|
||||
<Outlet/> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export const GenRoute = (id: string) => createLazyRoute(id)({ |
|
||||
component: TreePageLayout, |
|
||||
}) |
|
@ -0,0 +1,39 @@ |
|||||
|
import React from 'react' |
||||
|
import { PageContainer, PageContainerProps } from '@ant-design/pro-components' |
||||
|
import { Flexbox } from 'react-layout-kit' |
||||
|
import { DraggablePanel, DraggablePanelProps } from '@/components/draggable-panel' |
||||
|
import { useStyle } from './style' |
||||
|
|
||||
|
interface ITreePageLayoutProps { |
||||
|
children?: React.ReactNode |
||||
|
draggableProps?: DraggablePanelProps |
||||
|
pageProps?: PageContainerProps |
||||
|
leftPanel?: React.ReactNode |
||||
|
} |
||||
|
|
||||
|
export const TwoColPageLayout: React.FC<ITreePageLayoutProps> = (props) => { |
||||
|
const { styles } = useStyle({ className: 'two-col' }) |
||||
|
return ( |
||||
|
<PageContainer |
||||
|
breadcrumbRender={false} title={false} className={styles.container} |
||||
|
{...props.pageProps} |
||||
|
> |
||||
|
<Flexbox horizontal className={styles.authHeight}> |
||||
|
<DraggablePanel expandable={false} |
||||
|
placement="left" |
||||
|
defaultSize={{ width: 300 }} |
||||
|
maxWidth={500} |
||||
|
style={{ position: 'relative' }} |
||||
|
{...props.draggableProps} |
||||
|
> |
||||
|
{props.leftPanel} |
||||
|
</DraggablePanel> |
||||
|
<Flexbox className={styles.box}> |
||||
|
{props.children} |
||||
|
</Flexbox> |
||||
|
</Flexbox> |
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default TwoColPageLayout |
@ -0,0 +1,144 @@ |
|||||
|
import Switch from '@/components/switch' |
||||
|
import { |
||||
|
ActionType, |
||||
|
PageContainer, |
||||
|
ProColumns, |
||||
|
ProTable, |
||||
|
} from '@ant-design/pro-components' |
||||
|
import { useStyle } from './style.ts' |
||||
|
import { memo, useMemo, useRef, useState } from 'react' |
||||
|
import { useAtom, useAtomValue } from 'jotai' |
||||
|
|
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { Button, Space, Table, Popconfirm } from 'antd' |
||||
|
import { deleteLoginLogAtom, loginLogPageAtom, loginLogsAtom, loginLogSearchAtom } from '@/store/logs.ts' |
||||
|
|
||||
|
const LoginLog = memo(() => { |
||||
|
|
||||
|
const { t } = useTranslation() |
||||
|
const { styles } = useStyle() |
||||
|
const actionRef = useRef<ActionType>() |
||||
|
const [ page, setPage ] = useAtom(loginLogPageAtom) |
||||
|
const [ search, setSearch ] = useAtom(loginLogSearchAtom) |
||||
|
const { data, isLoading, isFetching, refetch } = useAtomValue(loginLogsAtom) |
||||
|
const { mutate: deleteLog, isPending: isDeleting } = useAtomValue(deleteLoginLogAtom) |
||||
|
const [ ids, setIds ] = useState<number[]>([]) |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
|
||||
|
return [ |
||||
|
{ |
||||
|
title: 'id', dataIndex: 'id', |
||||
|
hideInTable: true, |
||||
|
hideInSearch: true, |
||||
|
formItemProps: { |
||||
|
hidden: true |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.username', '登录帐号'), dataIndex: 'username', valueType: 'text', |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.ip', '登录IP'), dataIndex: 'ip', valueType: 'text', |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.user_agent', '浏览器'), dataIndex: 'user_agent', valueType: 'text', |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.status', '状态'), dataIndex: 'status', valueType: 'switch', |
||||
|
render: (_, record) => { |
||||
|
return <Switch value={record.status} size={'small'}/> |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.created_at', '登录时间'), |
||||
|
dataIndex: 'created_at', |
||||
|
valueType: 'dateTime', |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.logs.login.columns.option','操作'), valueType: 'option', |
||||
|
key: 'option', |
||||
|
render: (_, record) => [ |
||||
|
<Popconfirm |
||||
|
key={'del_confirm'} |
||||
|
onConfirm={() => { |
||||
|
deleteLog([ record.id ]) |
||||
|
}} |
||||
|
title={t('message.deleteConfirm')}> |
||||
|
<a key="del"> |
||||
|
{t('actions.delete', '删除')} |
||||
|
</a> |
||||
|
</Popconfirm> |
||||
|
, |
||||
|
], |
||||
|
}, |
||||
|
] as ProColumns[] |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<PageContainer breadcrumbRender={false} title={false} className={styles.container}> |
||||
|
<div className={styles.authHeight}> |
||||
|
<ProTable |
||||
|
rowKey={'id'} |
||||
|
actionRef={actionRef} |
||||
|
headerTitle={t('system.logs.login.title', '登录日志')} |
||||
|
columns={columns} |
||||
|
loading={isLoading || isFetching} |
||||
|
dataSource={data?.rows} |
||||
|
search={false} |
||||
|
rowSelection={{ |
||||
|
onChange: (selectedRowKeys) => { |
||||
|
setIds(selectedRowKeys as number[]) |
||||
|
}, |
||||
|
selectedRowKeys: ids, |
||||
|
selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], |
||||
|
}} |
||||
|
tableAlertOptionRender={() => { |
||||
|
return ( |
||||
|
<Space size={16}> |
||||
|
<Popconfirm |
||||
|
onConfirm={() => { |
||||
|
deleteLog(ids) |
||||
|
}} |
||||
|
title={t('message.batchDelete')}> |
||||
|
<Button type={'link'} |
||||
|
loading={isDeleting}>{t('actions.batchDel')}</Button> |
||||
|
</Popconfirm> |
||||
|
</Space> |
||||
|
) |
||||
|
}} |
||||
|
options={{ |
||||
|
reload: () => { |
||||
|
refetch() |
||||
|
}, |
||||
|
}} |
||||
|
toolbar={{ |
||||
|
search: { |
||||
|
loading: isFetching && !!search.key, |
||||
|
onSearch: (value: string) => { |
||||
|
setSearch({ key: value }) |
||||
|
}, |
||||
|
placeholder: t('system.logs.login.search.placeholder','请输入用户名查询') |
||||
|
}, |
||||
|
actions: [] |
||||
|
}} |
||||
|
pagination={{ |
||||
|
total: data?.total, |
||||
|
current: page.page, |
||||
|
pageSize: page.pageSize, |
||||
|
onChange: (page) => { |
||||
|
|
||||
|
setPage((prev) => { |
||||
|
return { ...prev, page } |
||||
|
}) |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
</PageContainer> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export default LoginLog |
@ -0,0 +1,14 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-logs-login-page` |
||||
|
const authHeight = css`
|
||||
|
min-height: calc(100vh - 122px); |
||||
|
background-color: ${token.colorBgContainer}; |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix), |
||||
|
authHeight, |
||||
|
} |
||||
|
}) |
@ -1,10 +1,245 @@ |
|||||
import { PageContainer } from '@ant-design/pro-components' |
|
||||
|
import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx' |
||||
|
import { |
||||
|
ActionType, |
||||
|
BetaSchemaForm, |
||||
|
ProCard, |
||||
|
ProColumns, |
||||
|
ProFormColumnsType, |
||||
|
ProTable |
||||
|
} from '@ant-design/pro-components' |
||||
|
import { Button, Form, Popconfirm } from 'antd' |
||||
|
import { PlusOutlined } from '@ant-design/icons' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import DepartmentTree from '@/components/department-tree/DepartmentTree.tsx' |
||||
|
import { useAtom, useAtomValue } from 'jotai' |
||||
|
import { |
||||
|
deleteUserAtom, resetPasswordAtom, |
||||
|
saveOrUpdateUserAtom, |
||||
|
userListAtom, |
||||
|
userPageAtom, |
||||
|
userSearchAtom, |
||||
|
userSelectedAtom |
||||
|
} from '@/store/user.ts' |
||||
|
import { useMemo, useRef, useState } from 'react' |
||||
|
import Switch from '@/components/switch' |
||||
|
import { DepartmentCascader } from '@/components/department-tree' |
||||
|
import RolePicker from '@/components/role-picker/RolePicker.tsx' |
||||
|
|
||||
const Users = () => { |
const Users = () => { |
||||
return ( |
|
||||
<PageContainer breadcrumbRender={false}> |
|
||||
|
|
||||
</PageContainer> |
|
||||
|
const { t } = useTranslation() |
||||
|
const [ page, setPage ] = useAtom(userPageAtom) |
||||
|
const [ search, setSearch ] = useAtom(userSearchAtom) |
||||
|
const [ , setCurrent ] = useAtom(userSelectedAtom) |
||||
|
const { mutate: saveOrUpdate, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom) |
||||
|
const { data, isFetching, isLoading, refetch } = useAtomValue(userListAtom) |
||||
|
const { mutate: deleteUser, isPending, } = useAtomValue(deleteUserAtom) |
||||
|
const { mutate: resetPass, isPending: isResetting } = useAtomValue(resetPasswordAtom) |
||||
|
const [ form ] = Form.useForm() |
||||
|
const actionRef = useRef<ActionType>() |
||||
|
const [ open, setOpen ] = useState(false) |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
|
||||
|
return [ |
||||
|
{ |
||||
|
title: 'id', dataIndex: 'id', |
||||
|
hideInTable: true, |
||||
|
hideInSearch: true, |
||||
|
formItemProps: { |
||||
|
hidden: true |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.name', '姓名'), dataIndex: 'name', valueType: 'text', |
||||
|
formItemProps: { |
||||
|
rules: [ { required: true, message: t('message.required') } ] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.username', '用户名'), dataIndex: 'username', valueType: 'text', |
||||
|
formItemProps: { |
||||
|
rules: [ { required: true, message: t('message.required') } ] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.roles', '角色'), dataIndex: 'roles', valueType: 'select', |
||||
|
render: (_, record) => { |
||||
|
return record.roles?.map(role => role.name).join(',') || '' |
||||
|
}, |
||||
|
renderFormItem: (item, config) => { |
||||
|
const { mode, ...other } = config as any |
||||
|
return <RolePicker {...item.fieldProps as any} |
||||
|
view={mode !== 'edit'} {...other}/> |
||||
|
}, |
||||
|
|
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.dept_id', '所属部门'), dataIndex: 'dept_id', |
||||
|
render: (_, record) => { |
||||
|
return record.dept_name || '' |
||||
|
}, |
||||
|
renderFormItem: (item, config) => { |
||||
|
return <DepartmentCascader {...item.fieldProps as any} {...config} /> |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.status', '状态'), dataIndex: 'status', valueType: 'switch', |
||||
|
render: (_, record) => { |
||||
|
return <Switch value={record.status} size={'small'}/> |
||||
|
}, |
||||
|
renderFormItem: (item, config) => { |
||||
|
return <Switch {...item.fieldProps as any} {...config}/> |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.update_at', '更新时间'), |
||||
|
hideInTable: true, |
||||
|
hideInSearch: true, |
||||
|
hideInForm: true, |
||||
|
dataIndex: 'update_at', |
||||
|
valueType: 'dateTime', |
||||
|
}, |
||||
|
{ |
||||
|
title: t('system.users.columns.option', '操作'), valueType: 'option', |
||||
|
key: 'option', |
||||
|
render: (_, record) => [ |
||||
|
<a key="editable" |
||||
|
onClick={() => { |
||||
|
setCurrent(record) |
||||
|
setOpen(true) |
||||
|
form.setFieldsValue(record) |
||||
|
}} |
||||
|
> |
||||
|
{t('actions.edit', '编辑')} |
||||
|
</a>, |
||||
|
<Popconfirm |
||||
|
disabled={isResetting} |
||||
|
key={'reset_password_confirm'} |
||||
|
onConfirm={() => { |
||||
|
resetPass(record.id) |
||||
|
}} |
||||
|
title={ |
||||
|
<span dangerouslySetInnerHTML={{ |
||||
|
__html: t('message.resetPassConfirm', '密码会重置为{{password}}操作不可回滚,是否继续?', { |
||||
|
password: '<span style="color:red;">kk123456</span>' |
||||
|
}) |
||||
|
}}></span> |
||||
|
}> |
||||
|
<a key="del"> |
||||
|
{t('actions.resetPass', '重置密码')} |
||||
|
</a> |
||||
|
</Popconfirm>, |
||||
|
<Popconfirm |
||||
|
key={'del_confirm'} |
||||
|
disabled={isPending} |
||||
|
onConfirm={() => { |
||||
|
deleteUser([ record.id ]) |
||||
|
}} |
||||
|
title={t('message.deleteConfirm')}> |
||||
|
<a key="del"> |
||||
|
{t('actions.delete', '删除')} |
||||
|
</a> |
||||
|
</Popconfirm> |
||||
|
, |
||||
|
], |
||||
|
}, |
||||
|
] as ProColumns[] |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<TwoColPageLayout leftPanel={ |
||||
|
<> |
||||
|
<ProCard title={t('system.users.title', '部门')}> |
||||
|
<DepartmentTree |
||||
|
root={true} |
||||
|
fieldNames={{ |
||||
|
title: 'name', |
||||
|
key: 'id', |
||||
|
}} |
||||
|
onItemClick={(item) => { |
||||
|
setSearch({ |
||||
|
dept_id: item.id, |
||||
|
}) |
||||
|
}} |
||||
|
/> |
||||
|
</ProCard> |
||||
|
</> |
||||
|
}> |
||||
|
<ProTable |
||||
|
rowKey={'id'} |
||||
|
actionRef={actionRef} |
||||
|
headerTitle={t('system.users.title', '用户管理')} |
||||
|
columns={columns} |
||||
|
loading={isLoading || isFetching} |
||||
|
dataSource={data?.rows} |
||||
|
search={false} |
||||
|
options={{ |
||||
|
reload: () => { |
||||
|
refetch() |
||||
|
}, |
||||
|
}} |
||||
|
toolbar={{ |
||||
|
search: { |
||||
|
loading: isFetching && !!search.key, |
||||
|
onSearch: (value: string) => { |
||||
|
setSearch({ key: value }) |
||||
|
}, |
||||
|
placeholder: t('system.users.search.placeholder', '输入用户名') |
||||
|
}, |
||||
|
actions: [ |
||||
|
<Button |
||||
|
key="button" |
||||
|
icon={<PlusOutlined/>} |
||||
|
onClick={() => { |
||||
|
form.resetFields() |
||||
|
form.setFieldsValue({ |
||||
|
id: 0, |
||||
|
dept_id: search.dept_id ?? 0, |
||||
|
}) |
||||
|
setOpen(true) |
||||
|
}} |
||||
|
type="primary" |
||||
|
> |
||||
|
{t('actions.add', '添加')} |
||||
|
</Button>, |
||||
|
] |
||||
|
}} |
||||
|
pagination={{ |
||||
|
total: data?.total, |
||||
|
current: page.page, |
||||
|
pageSize: page.pageSize, |
||||
|
onChange: (page) => { |
||||
|
setPage((prev) => { |
||||
|
return { ...prev, page } |
||||
|
}) |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
<BetaSchemaForm |
||||
|
width={600} |
||||
|
form={form} |
||||
|
layout={'horizontal'} |
||||
|
title={t(`system.users.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '用户编辑' : '用户添加')} |
||||
|
colProps={{ span: 24 }} |
||||
|
labelCol={{ span: 6 }} |
||||
|
wrapperCol={{ span: 16 }} |
||||
|
layoutType={'ModalForm'} |
||||
|
open={open} |
||||
|
modalProps={{ |
||||
|
maskClosable: false, |
||||
|
}} |
||||
|
onOpenChange={(open) => { |
||||
|
setOpen(open) |
||||
|
}} |
||||
|
loading={isSubmitting} |
||||
|
onFinish={async (values) => { |
||||
|
// console.log('values', values)
|
||||
|
saveOrUpdate(values) |
||||
|
return true |
||||
|
}} |
||||
|
columns={columns as ProFormColumnsType[]}/> |
||||
|
</TwoColPageLayout> |
||||
) |
) |
||||
} |
} |
||||
|
|
||||
|
@ -0,0 +1,71 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-user-page`; |
||||
|
|
||||
|
const tree = css`
|
||||
|
.ant-tree { |
||||
|
overflow: auto; |
||||
|
height: 100%; |
||||
|
border-right: 1px solid ${token.colorBorder}; |
||||
|
background: ${token.colorBgContainer}; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.ant-tree-directory .ant-tree-treenode-selected::before { |
||||
|
background: ${token.colorBgTextHover}; |
||||
|
} |
||||
|
|
||||
|
.ant-tree-treenode:before { |
||||
|
border-radius: ${token.borderRadius}px; |
||||
|
} |
||||
|
`
|
||||
|
|
||||
|
const treeNode = css`
|
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
|
||||
|
.actions { |
||||
|
display: none; |
||||
|
padding: 0 10px; |
||||
|
} |
||||
|
|
||||
|
&:hover .actions { { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
`
|
||||
|
const treeActions = css`
|
||||
|
padding: 0 24px 16px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
position: sticky; |
||||
|
bottom: 0; |
||||
|
z-index: 10; |
||||
|
background: ${token.colorBgContainer}; |
||||
|
`
|
||||
|
|
||||
|
const box = css`
|
||||
|
flex: 1; |
||||
|
background: ${token.colorBgContainer}; |
||||
|
`
|
||||
|
const form = css`
|
||||
|
//display: flex;
|
||||
|
//flex-wrap: wrap;
|
||||
|
min-width: 300px; |
||||
|
max-width: 800px; |
||||
|
`
|
||||
|
const emptyForm = css`
|
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix), |
||||
|
box, |
||||
|
form, |
||||
|
emptyForm, |
||||
|
tree, |
||||
|
treeNode, |
||||
|
treeActions |
||||
|
} |
||||
|
}) |
@ -0,0 +1,58 @@ |
|||||
|
import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' |
||||
|
import { atom } from 'jotai/index' |
||||
|
import { IPage } from '@/global' |
||||
|
import systemServ from '@/service/system.ts' |
||||
|
import { message } from 'antd' |
||||
|
import { t } from '@/i18n.ts' |
||||
|
|
||||
|
|
||||
|
export const loginLogPageAtom = atom<IPage>({ |
||||
|
page: 1, |
||||
|
pageSize: 10, |
||||
|
}) |
||||
|
|
||||
|
type LogSearch = { |
||||
|
key?: string, |
||||
|
} |
||||
|
|
||||
|
export const loginLogSearchAtom = atom<LogSearch>({ |
||||
|
key: '' |
||||
|
}) |
||||
|
|
||||
|
export const loginLogsAtom = atomWithQuery((get) => ({ |
||||
|
queryKey: [ 'loginLogs', get(loginLogPageAtom), get(loginLogSearchAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , page, search ] }) => { |
||||
|
return await systemServ.logs.login.list({ |
||||
|
...page as any, |
||||
|
...search as any, |
||||
|
}) |
||||
|
}, |
||||
|
select: (data) => { |
||||
|
return data.data |
||||
|
}, |
||||
|
})) |
||||
|
|
||||
|
export const loginLogIdsAtom = atom<number[]>([]) |
||||
|
|
||||
|
export const deleteLoginLogAtom = atomWithMutation<any, number[]>((get) => ({ |
||||
|
mutationKey: [ 'deleteLoginLog' ], |
||||
|
mutationFn: async (ids) => { |
||||
|
return await systemServ.logs.login.batchDelete(ids) |
||||
|
}, |
||||
|
onSuccess: () => { |
||||
|
message.success(t('message.deleteSuccess', '删除成功')) |
||||
|
get(loginLogsAtom).refetch() |
||||
|
}, |
||||
|
})) |
||||
|
|
||||
|
//clear
|
||||
|
export const clearLoginLogAtom = atomWithMutation<any, { start: string, end: string }>((get) => ({ |
||||
|
mutationKey: [ 'clearLoginLog' ], |
||||
|
mutationFn: async (params) => { |
||||
|
return await systemServ.logs.login.clear(params) |
||||
|
}, |
||||
|
onSuccess: () => { |
||||
|
message.success(t('message.deleteSuccess', '删除成功')) |
||||
|
get(loginLogsAtom).refetch() |
||||
|
} |
||||
|
})) |
@ -1,12 +0,0 @@ |
|||||
import { IRootContext } from '@/global' |
|
||||
import { atom } from 'jotai' |
|
||||
|
|
||||
export const routeContextAtom = atom<IRootContext>({}) |
|
||||
|
|
||||
export const updateRouteContextAtom = atom(null, (set, get, update) => { |
|
||||
console.log(update) |
|
||||
set(routeContextAtom, { |
|
||||
...get(routeContextAtom), |
|
||||
...update, |
|
||||
}) |
|
||||
}) |
|
@ -0,0 +1,11 @@ |
|||||
|
interface ILoginLog { |
||||
|
id: string; |
||||
|
username: string; |
||||
|
ip: string; |
||||
|
user_agent: string; |
||||
|
os: string; |
||||
|
browser: string; |
||||
|
status: string; |
||||
|
note: string; |
||||
|
created_at: string; |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue