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
-
121src/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 = () => { |
|||
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