From efc54c60c7957b69aa6b8ec86a0adb54546fa50e Mon Sep 17 00:00:00 2001 From: dark Date: Thu, 2 May 2024 00:18:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E7=99=BB=E5=BD=95=E6=97=A5=E5=BF=97=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/components/cascader/Cascader.tsx | 35 ++ src/components/cascader/index.ts | 1 + .../department-tree/DepartmentCascader.tsx | 39 +++ src/components/department-tree/DepartmentTree.tsx | 17 +- src/components/department-tree/index.ts | 2 + src/components/error/404.tsx | 41 ++- src/components/icon/index.tsx | 4 +- src/components/icon/picker/index.tsx | 2 + src/components/role-picker/RolePicker.tsx | 52 +++ src/components/role-picker/index.ts | 0 src/components/switch/index.tsx | 10 +- src/i18n.ts | 68 ++-- src/layout/TreePageLayout.tsx | 20 -- src/layout/TwoColPageLayout.tsx | 39 +++ src/layout/style.ts | 12 + src/pages/system/departments/index.tsx | 166 +++++---- src/pages/system/departments/style.ts | 79 +++-- src/pages/system/logs/login/index.tsx | 144 ++++++++ src/pages/system/logs/login/style.ts | 14 + src/pages/system/menus/index.tsx | 371 ++++++++++----------- src/pages/system/roles/index.tsx | 150 +++++---- src/pages/system/roles/style.ts | 7 +- src/pages/system/users/index.tsx | 243 +++++++++++++- src/pages/system/users/style.ts | 71 ++++ src/service/system.ts | 63 ++-- src/store/logs.ts | 58 ++++ src/store/route.ts | 12 - src/store/system.ts | 48 ++- src/store/user.ts | 121 ++++++- src/types/logs.d.ts | 11 + src/types/user.d.ts | 1 + src/utils/tree.ts | 129 ++++--- 33 files changed, 1437 insertions(+), 594 deletions(-) create mode 100644 src/components/cascader/Cascader.tsx create mode 100644 src/components/cascader/index.ts create mode 100644 src/components/department-tree/DepartmentCascader.tsx create mode 100644 src/components/role-picker/RolePicker.tsx create mode 100644 src/components/role-picker/index.ts delete mode 100644 src/layout/TreePageLayout.tsx create mode 100644 src/layout/TwoColPageLayout.tsx create mode 100644 src/pages/system/logs/login/index.tsx create mode 100644 src/pages/system/logs/login/style.ts create mode 100644 src/pages/system/users/style.ts create mode 100644 src/store/logs.ts delete mode 100644 src/store/route.ts create mode 100644 src/types/logs.d.ts diff --git a/.gitignore b/.gitignore index a547bf3..6c14e47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Logs logs *.log +!src/system/logs npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/src/components/cascader/Cascader.tsx b/src/components/cascader/Cascader.tsx new file mode 100644 index 0000000..36b5a4b --- /dev/null +++ b/src/components/cascader/Cascader.tsx @@ -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 = AntCascaderProps & { + 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) => { + + const f = { + key: fieldNames?.value ?? 'value', + title: fieldNames?.label ?? 'label', + children: fieldNames?.children ?? 'children', + } as any + return ( + + + ) +} + +Cascader.displayName = 'Cascader' +Cascader.SHOW_CHILD = AntCascader.SHOW_CHILD +Cascader.SHOW_PARENT = AntCascader.SHOW_PARENT + +export default Cascader \ No newline at end of file diff --git a/src/components/cascader/index.ts b/src/components/cascader/index.ts new file mode 100644 index 0000000..90e6466 --- /dev/null +++ b/src/components/cascader/index.ts @@ -0,0 +1 @@ +export * from './Cascader.tsx' \ No newline at end of file diff --git a/src/components/department-tree/DepartmentCascader.tsx b/src/components/department-tree/DepartmentCascader.tsx new file mode 100644 index 0000000..90724be --- /dev/null +++ b/src/components/department-tree/DepartmentCascader.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, '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 ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/department-tree/DepartmentTree.tsx b/src/components/department-tree/DepartmentTree.tsx index 06f53d5..832f947 100644 --- a/src/components/department-tree/DepartmentTree.tsx +++ b/src/components/department-tree/DepartmentTree.tsx @@ -11,6 +11,7 @@ import { IDepartment } from '@/types/department' export interface DepartmentTreeProps extends TreeProps { root?: TreeDataNode | boolean | string + onItemClick?: (item: IDepartment) => void } function getTopDataNode(root?: TreeDataNode | boolean | string, fieldNames?: TreeProps['fieldNames']) { @@ -50,6 +51,7 @@ export const DepartmentTree = ({ root, ...props }: DepartmentTreeProps) => { const { t } = useTranslation() const { data = [], isLoading } = useAtomValue(departTreeAtom, usePageStoreOptions()) const flattenMenusRef = useRef([]) + const topData = getTopDataNode(root, props.fieldNames) useDeepCompareEffect(() => { @@ -59,17 +61,18 @@ export const DepartmentTree = ({ root, ...props }: DepartmentTreeProps) => { // @ts-ignore array if (data.length) { // @ts-ignore flattenTree - flattenMenusRef.current = flattenTree(data as any) + flattenMenusRef.current = [ topData ].concat(flattenTree(data as any)) + } else { + flattenMenusRef.current = topData ? [ topData as unknown as IDepartment ] : [] as IDepartment[] } - }, [ data, isLoading ]) + }, [ data, isLoading, topData ]) const renderEmpty = () => { if ((data as any).length > 0 || isLoading) return null return } - const topData = getTopDataNode(root, props.fieldNames) const treeData = topData ? [ topData, ...data as Array ] : data return (<> @@ -84,6 +87,14 @@ export const DepartmentTree = ({ root, ...props }: DepartmentTreeProps) => { // checkable={true} showIcon={false} {...props} + onSelect={(keys, info) => { + if (props.onSelect) { + props.onSelect(keys, info) + } + const current = flattenMenusRef.current?.find((menu) => menu.id === keys[0]) + props?.onItemClick?.(current!) + }} + /> diff --git a/src/components/department-tree/index.ts b/src/components/department-tree/index.ts index e69de29..edb120f 100644 --- a/src/components/department-tree/index.ts +++ b/src/components/department-tree/index.ts @@ -0,0 +1,2 @@ +export * from './DepartmentTree' +export * from './DepartmentCascader.tsx' \ No newline at end of file diff --git a/src/components/error/404.tsx b/src/components/error/404.tsx index fad1e9b..7145e55 100644 --- a/src/components/error/404.tsx +++ b/src/components/error/404.tsx @@ -1,27 +1,34 @@ import { useTranslation } from '@/i18n.ts' import { useNavigate } from '@tanstack/react-router' import { Button, Result } from 'antd' +import { useAtomValue } from 'jotai' +import { userMenuDataAtom } from '@/store/user.ts' const NotFound = () => { - const navigate = useNavigate() - const { t } = useTranslation() + const navigate = useNavigate() + const { t } = useTranslation() + const { data } = useAtomValue(userMenuDataAtom) - return ( - navigate({ - to: '../' - })}> - {t('route.goBack')} - - } - /> - ) + if (!data) { + return null + + } + return ( + navigate({ + to: '../' + })}> + {t('route.goBack')} + + } + /> + ) } export default NotFound \ No newline at end of file diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index d5aead2..4bae6da 100644 --- a/src/components/icon/index.tsx +++ b/src/components/icon/index.tsx @@ -1,4 +1,4 @@ -import IconAll, { ALL_ICON_KEYS, IconType, IIconAllProps } from '@icon-park/react/es/all' +import IconAll, { ALL_ICON_KEYS, IconType as ParkIconType, IIconAllProps } from '@icon-park/react/es/all' import React, { Fragment } from 'react' import * as AntIcons from '@ant-design/icons/es/icons' @@ -35,7 +35,7 @@ export function Icon(props: IconProps) { return icon } - if (ALL_ICON_KEYS.indexOf(type as IconType) < 0) { + if (ALL_ICON_KEYS.indexOf(type as ParkIconType) < 0) { return null } diff --git a/src/components/icon/picker/index.tsx b/src/components/icon/picker/index.tsx index 7975428..fcd72ee 100644 --- a/src/components/icon/picker/index.tsx +++ b/src/components/icon/picker/index.tsx @@ -26,6 +26,8 @@ const IconPicker: FC = (props: PickerProps) => { if (value) { const [ type, componentName ] = value.split(':') selectIcon({ type, componentName } as any) + }else{ + selectIcon(null as any) } }, [ value ]) diff --git a/src/components/role-picker/RolePicker.tsx b/src/components/role-picker/RolePicker.tsx new file mode 100644 index 0000000..b763dc4 --- /dev/null +++ b/src/components/role-picker/RolePicker.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).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 ( + <> + - - - - - - - + +
+ + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + -
-
+ + - - - - + + ) } diff --git a/src/pages/system/departments/style.ts b/src/pages/system/departments/style.ts index a6d561b..70ef0b8 100644 --- a/src/pages/system/departments/style.ts +++ b/src/pages/system/departments/style.ts @@ -5,52 +5,53 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { const tree = css` - .ant-tree { - overflow: auto; - height: 100%; - border-right: 1px solid ${token.colorBorder}; - background: ${token.colorBgContainer}; + .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-directory .ant-tree-treenode-selected::before { + background: ${token.colorBgTextHover}; + } - .ant-tree-treenode:before { - border-radius: ${token.borderRadius}px; - } - ` + .ant-tree-treenode:before { + border-radius: ${token.borderRadius}px; + } + ` const treeNode = css` - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; - .actions { - display: none; - padding: 0 10px; - } + .actions { + display: none; + padding: 0 10px; + } - &:hover .actions { { - display: flex; - } + &: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}; - ` + 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}; - ` + flex: 1; + background: ${token.colorBgContainer}; + ` const form = css` //display: flex; //flex-wrap: wrap; @@ -58,10 +59,14 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { max-width: 800px; ` const emptyForm = css` - ` - + ` + const authHeight = css` + min-height: calc(100vh - 122px); + background-color: ${token.colorBgContainer}; + ` return { container: cx(prefix), + authHeight, box, form, emptyForm, diff --git a/src/pages/system/logs/login/index.tsx b/src/pages/system/logs/login/index.tsx new file mode 100644 index 0000000..1fce3cf --- /dev/null +++ b/src/pages/system/logs/login/index.tsx @@ -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() + 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([]) + + 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 + }, + }, + { + 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) => [ + { + deleteLog([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + , + ], + }, + ] as ProColumns[] + }, []) + + return ( + +
+ { + setIds(selectedRowKeys as number[]) + }, + selectedRowKeys: ids, + selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], + }} + tableAlertOptionRender={() => { + return ( + + { + deleteLog(ids) + }} + title={t('message.batchDelete')}> + + + + ) + }} + 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 } + }) + } + }} + /> +
+ + +
+ ) +}) + +export default LoginLog \ No newline at end of file diff --git a/src/pages/system/logs/login/style.ts b/src/pages/system/logs/login/style.ts new file mode 100644 index 0000000..94fea88 --- /dev/null +++ b/src/pages/system/logs/login/style.ts @@ -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, + } +}) \ No newline at end of file diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx index d45fba3..df27645 100644 --- a/src/pages/system/menus/index.tsx +++ b/src/pages/system/menus/index.tsx @@ -1,211 +1,204 @@ import Glass from '@/components/glass' import { useTranslation } from '@/i18n.ts' - import { PlusOutlined } from '@ant-design/icons' -import { PageContainer, ProCard } from '@ant-design/pro-components' +import { ProCard } from '@ant-design/pro-components' import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert, InputRef, Divider } from 'antd' import { useAtom, useAtomValue } from 'jotai' import { defaultMenu, menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from '@/store/menu.ts' import IconPicker from '@/components/icon/picker' import ButtonTable from './components/ButtonTable.tsx' -import { Flexbox } from 'react-layout-kit' -import { DraggablePanel } from '@/components/draggable-panel' import { useStyle } from './style.ts' import { MenuItem } from '@/global' import MenuTree from './components/MenuTree.tsx' import BatchButton from '@/pages/system/menus/components/BatchButton.tsx' import { useEffect, useRef } from 'react' +import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx' const Menus = () => { - const { styles, cx } = useStyle() - const { t } = useTranslation() - const [ form ] = Form.useForm() - const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom) - const { data = [] } = useAtomValue(menuDataAtom) - const [ currentMenu, setMenuData ] = useAtom(selectedMenuAtom) ?? {} - const menuInputRef = useRef(undefined) - - useEffect(() => { - - if (isError) { - notification.error({ - message: t('message.error', '错误'), - description: (error as any).message ?? t('message.saveFail', '保存失败'), - }) - } - }, [ isError ]) - - useEffect(() => { - if (currentMenu.id === 0 && menuInputRef.current) { - menuInputRef.current.focus() - } - }, [ currentMenu ]) - - return ( - - - - - - - - } - > - - - -
- - -
-
- - - - } - > -
(selectedMenuAtom) ?? {} + const menuInputRef = useRef(undefined) + + useEffect(() => { + + if (isError) { + notification.error({ + message: t('message.error', '错误'), + description: (error as any).message ?? t('message.saveFail', '保存失败'), + }) + } + }, [ isError ]) + + useEffect(() => { + if (currentMenu.id === 0 && menuInputRef.current) { + menuInputRef.current.focus() + } + }, [ currentMenu ]) + + return ( + + + + + } + > + + + +
+ + +
+ } > - - - - - - - - - - - - - - - - - - - - - - - - - - + + } > - - - - - - - - - - { - return prevValues.id !== curValues.id - }}> - - - - - -
-
-
-
- ) +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + return prevValues.id !== curValues.id + }}> + + + + +
+ + + + ) } export default Menus \ No newline at end of file diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 9960264..11be0ee 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -88,9 +88,12 @@ const Roles = memo(() => { }, { title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch', - render: (text) => { - return - } + render: (_,record) => { + return + }, + renderFormItem: (item, config) => { + return + }, }, { title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit', @@ -144,81 +147,84 @@ const Roles = memo(() => { return ( - { - setRoleIds(selectedRowKeys as number[]) - }, - selectedRowKeys: roleIds, - selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], - }} - tableAlertOptionRender={() => { - return ( - - { - deleteRole(roleIds) +
+ { + setRoleIds(selectedRowKeys as number[]) + }, + selectedRowKeys: roleIds, + selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], }} - title={t('message.batchDelete')}> - - - - ) - }} - options={{ - reload: () => { - refetch() - }, - }} - toolbar={{ - search: { - loading: isFetching && !!search.key, - onSearch: (value: string) => { - setSearch({ key: value }) - }, - placeholder: t('system.roles.search.placeholder') - }, - actions: [ - , - ] - }} - pagination={{ - total: data?.total, - current: page.page, - pageSize: page.pageSize, - onChange: (page) => { + tableAlertOptionRender={() => { + return ( + + { + deleteRole(roleIds) + }} + title={t('message.batchDelete')}> + + + + ) + }} + options={{ + reload: () => { + refetch() + }, + }} + toolbar={{ + search: { + loading: isFetching && !!search.key, + onSearch: (value: string) => { + setSearch({ key: value }) + }, + placeholder: t('system.roles.search.placeholder') + }, + actions: [ + , + ] + }} + pagination={{ + total: data?.total, + current: page.page, + pageSize: page.pageSize, + onChange: (page) => { + + setPage((prev) => { + return { ...prev, page } + }) + } + }} + /> +
- setPage((prev) => { - return { ...prev, page } - }) - } - }} - /> { - const prefix = `${prefixCls}-${token?.proPrefix}-role-page`; + const prefix = `${prefixCls}-${token?.proPrefix}-role-page` const box = css` @@ -13,9 +13,14 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { flex-wrap: wrap; min-width: 500px; ` + const authHeight = css` + min-height: calc(100vh - 122px); + background-color: ${token.colorBgContainer}; + ` return { container: cx(prefix), + authHeight, box, form, } diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index e70019c..c55d0af 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -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 ( - - + 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() + 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 + }, + + }, + { + title: t('system.users.columns.dept_id', '所属部门'), dataIndex: 'dept_id', + render: (_, record) => { + return record.dept_name || '' + }, + renderFormItem: (item, config) => { + return + } + }, + { + title: t('system.users.columns.status', '状态'), dataIndex: 'status', valueType: 'switch', + render: (_, record) => { + return + }, + renderFormItem: (item, config) => { + return + }, + }, + { + 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) => [ + { + setCurrent(record) + setOpen(true) + form.setFieldsValue(record) + }} + > + {t('actions.edit', '编辑')} + , + { + resetPass(record.id) + }} + title={ + kk123456' + }) + }}> + }> + + {t('actions.resetPass', '重置密码')} + + , + { + deleteUser([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + , + ], + }, + ] as ProColumns[] + }, []) + + return ( + + + { + setSearch({ + dept_id: item.id, + }) + }} + /> + + + }> + { + refetch() + }, + }} + toolbar={{ + search: { + loading: isFetching && !!search.key, + onSearch: (value: string) => { + setSearch({ key: value }) + }, + placeholder: t('system.users.search.placeholder', '输入用户名') + }, + actions: [ + , + ] + }} + pagination={{ + total: data?.total, + current: page.page, + pageSize: page.pageSize, + onChange: (page) => { + setPage((prev) => { + return { ...prev, page } + }) + } + }} + /> + { + setOpen(open) + }} + loading={isSubmitting} + onFinish={async (values) => { + // console.log('values', values) + saveOrUpdate(values) + return true + }} + columns={columns as ProFormColumnsType[]}/> + ) } diff --git a/src/pages/system/users/style.ts b/src/pages/system/users/style.ts new file mode 100644 index 0000000..2b6faeb --- /dev/null +++ b/src/pages/system/users/style.ts @@ -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 + } +}) \ No newline at end of file diff --git a/src/service/system.ts b/src/service/system.ts index 7c3bfa0..0b9fcc9 100644 --- a/src/service/system.ts +++ b/src/service/system.ts @@ -8,32 +8,47 @@ import { IRole } from '@/types/roles' import { IDepartment } from '@/types/department' const systemServ = { - dept: { - ...createCURD('/sys/dept'), - tree: () => { - return request.get<{ tree: IDepartment }>('/sys/dept/tree') - } - }, - menus: { - ...createCURD('/sys/menu') - }, - login: (data: LoginRequest) => { - return request.post('/sys/login', data) - }, - user: { - ...createCURD('/sys/user'), - current: () => { - return request.get('/sys/user/info') + dept: { + ...createCURD('/sys/dept'), + tree: () => { + return request.get<{ tree: IDepartment }>('/sys/dept/tree') + } }, - menus: () => { - return request.get>('/sys/user/menus') + menus: { + ...createCURD('/sys/menu') }, - - - }, - role: { - ...createCURD('/sys/role') - } + login: (data: LoginRequest) => { + return request.post('/sys/login', data) + }, + logout:()=>{ + // + }, + user: { + ...createCURD('/sys/user'), + current: () => { + return request.get('/sys/user/info') + }, + menus: () => { + return request.get>('/sys/user/menus') + }, + resetPassword: (id: number) => { + return request.post(`/sys/user/reset/password`, { id }) + } + }, + role: { + ...createCURD('/sys/role') + }, + logs: { + login: { + ...createCURD('/sys/log/login'), + clear: (params: { + start: string, + end: string + }) => { + return request.post('/sys/log/login/clear', params) + } + } + } } diff --git a/src/store/logs.ts b/src/store/logs.ts new file mode 100644 index 0000000..ae40ea6 --- /dev/null +++ b/src/store/logs.ts @@ -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({ + page: 1, + pageSize: 10, +}) + +type LogSearch = { + key?: string, +} + +export const loginLogSearchAtom = atom({ + 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([]) + +export const deleteLoginLogAtom = atomWithMutation((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((get) => ({ + mutationKey: [ 'clearLoginLog' ], + mutationFn: async (params) => { + return await systemServ.logs.login.clear(params) + }, + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + get(loginLogsAtom).refetch() + } +})) \ No newline at end of file diff --git a/src/store/route.ts b/src/store/route.ts deleted file mode 100644 index 9bada25..0000000 --- a/src/store/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IRootContext } from '@/global' -import { atom } from 'jotai' - -export const routeContextAtom = atom({}) - -export const updateRouteContextAtom = atom(null, (set, get, update) => { - console.log(update) - set(routeContextAtom, { - ...get(routeContextAtom), - ...update, - }) -}) \ No newline at end of file diff --git a/src/store/system.ts b/src/store/system.ts index 676bb43..1695103 100644 --- a/src/store/system.ts +++ b/src/store/system.ts @@ -1,8 +1,7 @@ import { IAppData } from '@/global' -import { createStore } from 'jotai' +import { createStore } from 'jotai' import { atomWithStorage } from 'jotai/utils' import { changeLanguage as setLang } from 'i18next' -import { userMenuDataAtom } from '@/store/user.ts' /** * app全局状态 @@ -10,48 +9,45 @@ import { userMenuDataAtom } from '@/store/user.ts' export const appStore = createStore() export const appAtom = atomWithStorage>('app', { - name: 'Crazy Pro', - version: '1.0.0', - language: 'zh-CN', + name: 'Crazy Pro', + version: '1.0.0', + language: 'zh-CN', }) appStore.sub(appAtom, () => { - const token = appStore.get(appAtom).token - const { data = [], refetch } = appStore.get(userMenuDataAtom) - //如果没有menus数据,则请求 - if (token && data.length === 0) { - refetch() - } + // const token = appStore.get(appAtom).token + }) export const getAppData = () => { - return appStore.get(appAtom) + return appStore.get(appAtom) } export const changeLanguage = (lang: string, reload?: boolean) => { - if (appStore.get(appAtom).language !== lang) { - setLang(lang) - updateAppData({ language: lang }) - if (reload) { - window.location.reload() + if (appStore.get(appAtom).language !== lang) { + setLang(lang) + updateAppData({ language: lang }) + if (reload) { + window.location.reload() + } } - } } export const updateAppData = (app: Partial) => { - appStore.set(appAtom, (prev) => { - return { - ...prev, - ...app, - } - }) + appStore.set(appAtom, (prev) => { + return { + ...prev, + ...app, + } + }) } export const getToken = () => { - return appStore.get(appAtom).token + return appStore.get(appAtom).token } export const setToken = (token: string) => { - updateAppData({ token }) + console.log('settoken', token) + updateAppData({ token }) } diff --git a/src/store/user.ts b/src/store/user.ts index c7eefd7..0d9ca11 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,12 +1,14 @@ -import { appAtom } from '@/store/system.ts' +import { appAtom, setToken } from '@/store/system.ts' import { IMenu } from '@/types/menus' -import { IUserInfo } from '@/types/user' +import { IUser, IUserInfo } from '@/types/user' import { atom } from 'jotai/index' -import { IApiResult, IAuth, IPageResult, MenuItem } from '@/global' +import { IApiResult, IAuth, IPage, IPageResult, MenuItem } from '@/global' import { LoginRequest } from '@/types/login' -import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' +import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import systemServ from '@/service/system.ts' import { formatMenuData, isDev } from '@/utils' +import { message } from 'antd' +import { t } from 'i18next' export const authAtom = atom({ isLogin: false, @@ -22,17 +24,28 @@ export const loginFormAtom = atom({ ...(isDev ? devLogin : {}) } as LoginRequest) -export const loginAtom = atomWithMutation(() => ({ +export const loginAtom = atomWithMutation((get) => ({ mutationKey: [ 'login' ], mutationFn: async (params) => { return await systemServ.login(params) }, - onSuccess: () => { - // console.log('login success', data) + onSuccess: (res) => { + message.success(t('login.success')) + // console.log('login success', res) + get(userMenuDataAtom).refetch().then() + return res.data }, retry: false, })) +export const logoutAtom = atomWithMutation(() => ({ + mutationKey: [ 'logout' ], + mutationFn: async () => { + setToken('') + return true + }, +})) + export const currentUserAtom = atomWithQuery, any, IUserInfo>((get) => { return { queryKey: [ 'user_info', get(appAtom).token ], @@ -54,24 +67,100 @@ export const userMenuDataAtom = atomWithQuery>, select: (data) => { return formatMenuData(data.data.rows as any ?? []) }, - cacheTime: 1000 * 60, retry: false, })) -export const userSearchAtom = atom<{ - dept_id: number, - key: string -} | unknown>({}) +export type UserSearch = { + dept_id?: any, + key?: string +} + +export const userSearchAtom = atom({} as UserSearch) + +//=======user page store====== + +export const userPageAtom = atom({ + pageSize: 10, + page: 1 +}) -//user list +// user list export const userListAtom = atomWithQuery((get) => { return { - queryKey: [ 'user_list', get(userSearchAtom) ], - queryFn: async ({ queryKey: [ , params ] }) => { - return await systemServ.user.list(params) + queryKey: [ 'user_list', get(userSearchAtom), get(userPageAtom) ], + queryFn: async ({ queryKey: [ , params, page ] }) => { + return await systemServ.user.list({ + ...params as any, + ...page as any, + }) }, select: (data) => { return data.data }, + } }) + +// user selected +export const userSelectedAtom = atom({} as IUser) + +export const defaultUserData = { + id: 0, + dept_id: 0, + role_id: 0, +} as IUser + +//save or update user +export const saveOrUpdateUserAtom = atomWithMutation((get) => ({ + mutationKey: [ 'save_user' ], + mutationFn: async (params) => { + params.status = params.status ? '1' : '0' + const isAdd = 0 === params.id + if (isAdd) { + return await systemServ.user.add(params) + } + return await systemServ.user.update(params) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + //刷新userList + get(queryClientAtom).invalidateQueries({ queryKey: [ 'user_list' ] }) + + return res + }, +})) + +//delete user + +export const batchUserIdsAtom = atom([]) +export const deleteUserAtom = atomWithMutation((get) => ({ + mutationKey: [ 'delete_user' ], + mutationFn: async (params) => { + return await systemServ.user.batchDelete(params) + }, + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + //刷新userList + get(queryClientAtom).invalidateQueries({ queryKey: [ 'user_list' ] }) + return true + }, +})) + +//reset password +export const resetPasswordAtom = atomWithMutation(() => ({ + mutationKey: [ 'reset_password' ], + mutationFn: async (id) => { + return await systemServ.user.resetPassword(id) + }, + onSuccess: () => { + message.success(t('message.resetSuccess', '重置成功')) + //刷新userList + // get(queryClientAtom).invalidateQueries({ queryKey: [ 'user_list' ] }) + return true + }, + onError: () => { + message.error(t('message.resetError', '重置失败')) + }, +})) diff --git a/src/types/logs.d.ts b/src/types/logs.d.ts new file mode 100644 index 0000000..ed5285a --- /dev/null +++ b/src/types/logs.d.ts @@ -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; +} \ No newline at end of file diff --git a/src/types/user.d.ts b/src/types/user.d.ts index 5b4b53d..923c5ce 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -7,6 +7,7 @@ export interface IUser { updated_at: string, updated_by: number, username: string, + role_id: number, dept_id: number, dept_name: string, name: string, diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 5c9a097..17b2073 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -1,59 +1,96 @@ +import { FiledNames } from '@/global' + type TreeKey = string | number; type TreeNode = { - [key in keyof T]: T[keyof T]; + [key in keyof T]: T[keyof T]; } & { - key: TreeKey; - id?: TreeKey; - children?: TreeNode[]; + key: TreeKey; + id?: TreeKey; + children?: TreeNode[]; }; export function getTreeCheckedStatus(tree: TreeNode[], selectKeys: TreeKey[]): { - checked: TreeKey[], - halfChecked: TreeKey[] + checked: TreeKey[], + halfChecked: TreeKey[] } { - const checked: TreeKey[] = [] - const halfChecked: TreeKey[] = [] - - if (!tree || tree.length === 0) return { checked, halfChecked } - if (!selectKeys || selectKeys.length === 0) return { checked, halfChecked } - - // 辅助函数来递归地检查每个节点 - function checkNode(node: TreeNode, ancestors: TreeKey[]): void { - const key = node.key ?? node.id - const isLeaf = !node.children || node.children.length === 0 - const isSelected = selectKeys.includes(key) - - // 如果是叶节点并且被选中,则直接加入到checked数组 - if (isLeaf && isSelected) { - checked.push(key) - // 标记所有祖先为半选状态,除非它们已经被完全选中 - ancestors.forEach(ancestorKey => { - if (!halfChecked.includes(ancestorKey) && !checked.includes(ancestorKey)) { - halfChecked.push(ancestorKey) + const checked: TreeKey[] = [] + const halfChecked: TreeKey[] = [] + + if (!tree || tree.length === 0) return { checked, halfChecked } + if (!selectKeys || selectKeys.length === 0) return { checked, halfChecked } + + // 辅助函数来递归地检查每个节点 + function checkNode(node: TreeNode, ancestors: TreeKey[]): void { + const key = node.key ?? node.id + const isLeaf = !node.children || node.children.length === 0 + const isSelected = selectKeys.includes(key) + + // 如果是叶节点并且被选中,则直接加入到checked数组 + if (isLeaf && isSelected) { + checked.push(key) + // 标记所有祖先为半选状态,除非它们已经被完全选中 + ancestors.forEach(ancestorKey => { + if (!halfChecked.includes(ancestorKey) && !checked.includes(ancestorKey)) { + halfChecked.push(ancestorKey) + } + }) + return } - }) - return - } - // 非叶节点,递归检查其子节点 - if (node.children) { - const childAncestors = [ ...ancestors, key ] - node.children.forEach(child => checkNode(child, childAncestors)) - - // 检查当前节点的所有子节点是否全部或部分被选中 - const childSelectedCount = node.children.filter(child => checked.includes(child.key ?? child.id)).length - if (childSelectedCount === node.children.length) { - // 如果所有子节点都被选中,将当前节点标为全选 - checked.push(key) - } else if (childSelectedCount > 0) { - // 如果部分子节点被选中,将当前节点标为半选 - halfChecked.push(key) - } + // 非叶节点,递归检查其子节点 + if (node.children) { + const childAncestors = [ ...ancestors, key ] + node.children.forEach(child => checkNode(child, childAncestors)) + + // 检查当前节点的所有子节点是否全部或部分被选中 + const childSelectedCount = node.children.filter(child => checked.includes(child.key ?? child.id)).length + if (childSelectedCount === node.children.length) { + // 如果所有子节点都被选中,将当前节点标为全选 + checked.push(key) + } else if (childSelectedCount > 0) { + // 如果部分子节点被选中,将当前节点标为半选 + halfChecked.push(key) + } + } } - } - // 遍历每一个根节点 - tree.forEach(node => checkNode(node, [])) - return { checked, halfChecked } + // 遍历每一个根节点 + tree.forEach(node => checkNode(node, [])) + return { checked, halfChecked } } + +export function findValuePath(tree: TreeNode[], targetValue: string | number, filedNames?: FiledNames): (string | number)[] | null { + + const f = { + key: filedNames?.key ?? 'key', + title: filedNames?.title ?? 'title', + children: filedNames?.children ?? 'children', + } + + const findPathRecursive = (node: TreeNode, pathSoFar: (string | number)[]): (string | number)[] | null => { + if (node[f.key] === targetValue) { + return [ ...pathSoFar, node[f.key] ] + } + + if (node[f.children]) { + for (const child of node[f.children]) { + const result = findPathRecursive(child, [ ...pathSoFar, node[f.key] ]) + if (result !== null) { + return result + } + } + } + + return null + } + + for (const root of tree) { + const result = findPathRecursive(root, []) + if (result !== null) { + return result + } + } + + return null // 如果未找到目标值,则返回null +} \ No newline at end of file