From 3e45be5097f62f271a8603a08918efbdabab767c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91?= Date: Tue, 30 Apr 2024 15:57:36 +0800 Subject: [PATCH] update --- package.json | 1 + src/_authenticatedRoute.tsx | 18 + src/components/loading/index.tsx | 38 +- src/components/loading/style.ts | 4 +- src/components/user-picker/Item.tsx | 22 ++ src/components/user-picker/UserPicker.tsx | 61 ++++ src/components/user-picker/index.ts | 1 + src/components/user-picker/store.ts | 5 + src/components/user-picker/style.ts | 25 ++ src/layout/_authenticated.tsx | 18 - src/pages/list/list.tsx | 15 - src/pages/list/tree.tsx | 146 -------- .../departments/components/DepartmentTree.tsx | 115 +++--- .../departments/components/TreeNodeRender.tsx | 73 ++-- src/pages/system/departments/index.tsx | 255 ++++++------- src/pages/system/departments/store.ts | 84 ----- src/pages/system/menus/components/BatchButton.tsx | 45 +-- src/pages/system/menus/components/MenuTree.tsx | 121 +++---- .../system/menus/components/TreeNodeRender.tsx | 73 ++-- src/pages/system/menus/index.tsx | 11 +- src/pages/system/menus/store.ts | 95 ----- src/pages/system/roles/index.tsx | 19 +- src/pages/system/roles/store.ts | 88 ----- src/routes.tsx | 393 +++++++++++---------- src/store/department.ts | 110 ++++-- src/store/index.ts | 36 ++ src/store/menu.ts | 88 +++++ src/store/role.ts | 88 +++++ vite.config.ts | 7 +- 29 files changed, 1022 insertions(+), 1033 deletions(-) create mode 100644 src/_authenticatedRoute.tsx create mode 100644 src/components/user-picker/Item.tsx create mode 100644 src/components/user-picker/UserPicker.tsx create mode 100644 src/components/user-picker/index.ts create mode 100644 src/components/user-picker/store.ts create mode 100644 src/components/user-picker/style.ts delete mode 100644 src/layout/_authenticated.tsx delete mode 100644 src/pages/list/list.tsx delete mode 100644 src/pages/list/tree.tsx delete mode 100644 src/pages/system/departments/store.ts delete mode 100644 src/pages/system/menus/store.ts delete mode 100644 src/pages/system/roles/store.ts create mode 100644 src/store/index.ts create mode 100644 src/store/menu.ts create mode 100644 src/store/role.ts diff --git a/package.json b/package.json index 0fca715..6ff1fe5 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-layout-kit": "^1.9.0", "react-rnd": "^10.4.2-test2", "react-use": "^17.5.0", + "throttle-debounce": "^5.0.0", "wonka": "^6.3.4" }, "devDependencies": { diff --git a/src/_authenticatedRoute.tsx b/src/_authenticatedRoute.tsx new file mode 100644 index 0000000..7c72a4d --- /dev/null +++ b/src/_authenticatedRoute.tsx @@ -0,0 +1,18 @@ +import { isAuthenticated } from '@/utils/auth.ts' +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const AuthenticatedRoute = createFileRoute('/_authenticated')({ + beforeLoad: async ({ location }) => { + if (!isAuthenticated()) { + throw redirect({ + to: '/login', + search: { + // Use the current location to power a redirect after login + // (Do not use `router.state.resolvedLocation` as it can + // potentially lag behind the actual current location) + redirect: location.href, + }, + }) + } + }, +}) \ No newline at end of file diff --git a/src/components/loading/index.tsx b/src/components/loading/index.tsx index dae467b..b4ba7bd 100644 --- a/src/components/loading/index.tsx +++ b/src/components/loading/index.tsx @@ -1,17 +1,43 @@ -import React from 'react' +import { useEffect, useState } from 'react' import { useStyles } from './style.ts' +import { debounce } from 'throttle-debounce' interface ILoading { loading: boolean, className?: string + //延时加载 + delay?: number } -export const Loading = ({ loading, className }: ILoading) => { +function shouldDelay(loading?: boolean, delay?: number): boolean { + return !!loading && !!delay && !isNaN(Number(delay)) +} + +export const Loading = ({ loading, className, delay }: ILoading) => { const { styles, cx } = useStyles({ className }) - return ( -
+ const [ isLoading, setLoading ] = useState(() => { + return loading && !shouldDelay(loading, delay) + }) + + useEffect(() => { + if (loading) { + const loadingFunc = debounce(delay, () => { + setLoading(true) + }) + loadingFunc() + return () => { + loadingFunc?.cancel?.() + } + } + + setLoading(false) + + }, [ loading, delay ]) + + const render = () => { + return
@@ -22,7 +48,9 @@ export const Loading = ({ loading, className }: ILoading) => {
- ) + } + + return render() } export default Loading \ No newline at end of file diff --git a/src/components/loading/style.ts b/src/components/loading/style.ts index aa6f69a..3f0bfe7 100644 --- a/src/components/loading/style.ts +++ b/src/components/loading/style.ts @@ -1,12 +1,12 @@ import { createStyles } from '@/theme' // Define styles using createStyles -export const useStyles = createStyles(({ token, css, cx, prefixCls }, { className }) => { +export const useStyles = createStyles(({ token, css, cx, prefixCls }, props: any) => { const prefix = `${prefixCls}-${token.proPrefix}-loading` return { - container: cx(prefix, className), + container: cx(prefix, props.className), base: css` --tw-translate-x: 0; diff --git a/src/components/user-picker/Item.tsx b/src/components/user-picker/Item.tsx new file mode 100644 index 0000000..8bd15b5 --- /dev/null +++ b/src/components/user-picker/Item.tsx @@ -0,0 +1,22 @@ +import { IUser } from '@/types/user' +import { useAtom } from 'jotai' +import { useStyle } from './style.ts' +import { Checkbox } from 'antd' + +export const Item = ( props: { + value: IUser, + onChange:( value: IUser)=>void +} )=>{ + + const { styles } = useStyle() + + + return ( +
+ }> + {props.value.name} + +
+ ) + +} \ No newline at end of file diff --git a/src/components/user-picker/UserPicker.tsx b/src/components/user-picker/UserPicker.tsx new file mode 100644 index 0000000..d2c015a --- /dev/null +++ b/src/components/user-picker/UserPicker.tsx @@ -0,0 +1,61 @@ +import { Modal, ModalProps, Select, SelectProps } from 'antd' +import { memo } from 'react' +import { Flexbox } from 'react-layout-kit' +import { useStyle } from './style.ts' + +export interface UserSelectProps extends SelectProps { + +} + +export interface UserModelProps extends ModalProps { + +} + +export type UserPickerProps = + | { + type?: 'modal'; + /** Props for the modal component */ +} & UserModelProps + | { + type: 'select'; + /** Props for the select component */ +} & UserSelectProps + +const UserSelect = memo((props: UserSelectProps) => { + console.log(props) + return ( + + ) +}) + +const UserModel = memo(({ open, ...props }: UserModelProps) => { + const { styles } = useStyle() + + + return ( + + + + + + + + + + + + ) +}) + + +const UserPicker = memo(({ type, ...props }: UserPickerProps) => { + + return type === 'modal' ? : +}) + +export default UserPicker \ No newline at end of file diff --git a/src/components/user-picker/index.ts b/src/components/user-picker/index.ts new file mode 100644 index 0000000..f445245 --- /dev/null +++ b/src/components/user-picker/index.ts @@ -0,0 +1 @@ +export * from './UserPicker.tsx' \ No newline at end of file diff --git a/src/components/user-picker/store.ts b/src/components/user-picker/store.ts new file mode 100644 index 0000000..3ec8af6 --- /dev/null +++ b/src/components/user-picker/store.ts @@ -0,0 +1,5 @@ +import { IUser } from '@/types/user' +import { atom } from 'jotai' + +export const userSelectedAtom = atom([]) + diff --git a/src/components/user-picker/style.ts b/src/components/user-picker/style.ts new file mode 100644 index 0000000..fdc2942 --- /dev/null +++ b/src/components/user-picker/style.ts @@ -0,0 +1,25 @@ +import { createStyles } from '@/theme' + +export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { + const prefix = `${prefixCls}-${token?.proPrefix}-user-picker` + + const list = css`` + + const listItem = css` + + padding: 5px; + height: 40px; + background-color: ${token.colorBgContainer}; + + :hover { + background-color: ${token.controlItemBgActiveHover}; + } + + ` + + return { + container: cx(prefix), + list, + listItem, + } +}) \ No newline at end of file diff --git a/src/layout/_authenticated.tsx b/src/layout/_authenticated.tsx deleted file mode 100644 index aeec104..0000000 --- a/src/layout/_authenticated.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { isAuthenticated } from '@/utils/auth.ts' -import { createFileRoute,redirect } from '@tanstack/react-router' - -export const Route = createFileRoute('/_authenticated')({ - beforeLoad: async ({ location }) => { - if (!isAuthenticated()) { - throw redirect({ - to: '/login', - search: { - // Use the current location to power a redirect after login - // (Do not use `router.state.resolvedLocation` as it can - // potentially lag behind the actual current location) - redirect: location.href, - }, - }) - } - }, -}) \ No newline at end of file diff --git a/src/pages/list/list.tsx b/src/pages/list/list.tsx deleted file mode 100644 index 7335122..0000000 --- a/src/pages/list/list.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createLazyRoute } from '@tanstack/react-router' -import { ProCard } from '@ant-design/pro-components' -const List = () => { - return ( - - 列表页面 - - ) -} -export const Route = createLazyRoute('/list/index')({ - component: List, -}) - - -export default List \ No newline at end of file diff --git a/src/pages/list/tree.tsx b/src/pages/list/tree.tsx deleted file mode 100644 index 005dcbc..0000000 --- a/src/pages/list/tree.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { LightFilter, PageContainer, ProCard, ProColumns, ProTable } from '@ant-design/pro-components' -import { Tree, Input, Space, Button } from 'antd' -import { createLazyRoute } from '@tanstack/react-router' -import { departmentAtom } from '../../store/department.ts' -import { useAtomValue } from 'jotai' -import { getIcon } from '../../components/icon' -import dayjs from 'dayjs' - -//递归渲染树形结构,将name->title, id->key -const renderTree = (data: any[]) => { - return data?.map((item) => { - if (item.children) { - return { - title: item.name, - key: item.id, - children: renderTree(item.children), - } - } - return { - title: item.name, - key: item.id, - } - }) -} - - -const columns: ProColumns[] = [ - { - title: '姓名', - dataIndex: 'name', - render: (_) => {_}, - formItemProps: { - lightProps: { - labelFormatter: (value) => `app-${value}`, - }, - }, - }, - { - title: '帐号', - dataIndex: 'account', - }, - { - title: '创建者', - dataIndex: 'creator', - valueType: 'select', - search: false, - valueEnum: { - all: { text: '全部' }, - 付小小: { text: '付小小' }, - 曲丽丽: { text: '曲丽丽' }, - 林东东: { text: '林东东' }, - 陈帅帅: { text: '陈帅帅' }, - 兼某某: { text: '兼某某' }, - }, - }, - //操作 - { - title: '操作', - valueType: 'option', - render: (_, record) => { - return [ - { - alert('edit') - }}>编辑, - { - alert('delete') - }}>删除, - ] - } - }, -] - -const TreePage = () => { - - const { data, isError, isPending } = useAtomValue(departmentAtom) - - if (isError) { - return
Error
- } - // if (isPending){ - // return
Loading
- // } - - return ( - - - - , - ], - }} - - /> - -
- - - - ) -} - - -export default TreePage \ No newline at end of file diff --git a/src/pages/system/departments/components/DepartmentTree.tsx b/src/pages/system/departments/components/DepartmentTree.tsx index 2aeabb3..b51f828 100644 --- a/src/pages/system/departments/components/DepartmentTree.tsx +++ b/src/pages/system/departments/components/DepartmentTree.tsx @@ -1,10 +1,11 @@ +import { usePageStoreOptions } from '@/store' import { Empty, Spin, Tree } from 'antd' import { useStyle } from '../style.ts' import { useTranslation } from '@/i18n.ts' import { useSetAtom } from 'jotai' -import { selectedDepartAtom, departTreeAtom, batchIdsAtom } from '../store.ts' +import { selectedDepartAtom, departTreeAtom, batchIdsAtom } from '@/store/department.ts' import { FormInstance } from 'antd/lib' -import { useAtomValue } from 'jotai/index' +import { useAtomValue } from 'jotai' import { TreeNodeRender } from './TreeNodeRender.tsx' import { useEffect, useRef } from 'react' import { flattenTree } from '@/utils' @@ -13,69 +14,69 @@ import { IDepartment } from '@/types/department' export const DepartmentTree = ({ form }: { form: FormInstance }) => { - const { styles } = useStyle() - const { t } = useTranslation() - const setIds = useSetAtom(batchIdsAtom) - const setCurrent = useSetAtom(selectedDepartAtom) - const { data = [], isLoading } = useAtomValue(departTreeAtom) - const flattenMenusRef = useRef([]) + const { styles } = useStyle() + const { t } = useTranslation() + const setIds = useSetAtom(batchIdsAtom, usePageStoreOptions()) + const setCurrent = useSetAtom(selectedDepartAtom, usePageStoreOptions()) + const { data = [], isLoading } = useAtomValue(departTreeAtom, usePageStoreOptions()) + const flattenMenusRef = useRef([]) - useDeepCompareEffect(() => { + useDeepCompareEffect(() => { - if (isLoading) return + if (isLoading) return - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore array - if (data.length) { - // @ts-ignore flattenTree - flattenMenusRef.current = flattenTree(data as any) - } - - }, [ data, isLoading ]) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore array + if (data.length) { + // @ts-ignore flattenTree + flattenMenusRef.current = flattenTree(data as any) + } - useEffect(() => { + }, [ data, isLoading ]) - return () => { - setCurrent({} as IDepartment) - } - }, []) + useEffect(() => { - const renderEmpty = () => { - if ((data as any).length > 0 || isLoading) return null - return + return () => { + setCurrent({} as IDepartment) } + }, []) + + const renderEmpty = () => { + if ((data as any).length > 0 || isLoading) return null + return + } - return (<> - - { - renderEmpty() - } - { - return () - }} - fieldNames={{ - title: 'name', - key: 'id' - }} - onSelect={(item) => { - const current = flattenMenusRef.current?.find((menu) => menu.id === item[0]) - setCurrent(current as IDepartment) - form.setFieldsValue({ ...current }) - }} - onCheck={(item) => { - setIds(item as number[]) - }} - // checkable={true} - showIcon={false} - /> - - - ) + return (<> + + { + renderEmpty() + } + { + return () + }} + fieldNames={{ + title: 'name', + key: 'id' + }} + onSelect={(item) => { + const current = flattenMenusRef.current?.find((menu) => menu.id === item[0]) + setCurrent(current as IDepartment) + form.setFieldsValue({ ...current }) + }} + onCheck={(item) => { + setIds(item as number[]) + }} + // checkable={true} + showIcon={false} + /> + + + ) } export default DepartmentTree \ No newline at end of file diff --git a/src/pages/system/departments/components/TreeNodeRender.tsx b/src/pages/system/departments/components/TreeNodeRender.tsx index 4044f2b..89192c0 100644 --- a/src/pages/system/departments/components/TreeNodeRender.tsx +++ b/src/pages/system/departments/components/TreeNodeRender.tsx @@ -1,58 +1,59 @@ +import { usePageStoreOptions } from '@/store' import { memo } from 'react' import { MenuItem } from '@/types' import { Popconfirm, Space, TreeDataNode } from 'antd' import { FormInstance } from 'antd/lib' import { useTranslation } from '@/i18n.ts' import { useStyle } from '../style.ts' -import { useAtomValue, useSetAtom } from 'jotai/index' -import { selectedDepartAtom, deleteDepartAtom, defaultDepart } from '../store.ts' +import { useAtomValue, useSetAtom } from 'jotai' +import { selectedDepartAtom, deleteDepartAtom, defaultDepart } from '@/store/department.ts' import { PlusOutlined } from '@ant-design/icons' import ActionIcon, { DeleteAction } from '@/components/icon/action' export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => { - const { name } = node - const { t } = useTranslation() - const { styles } = useStyle() - const { mutate } = useAtomValue(deleteDepartAtom) + const { name } = node + const { t } = useTranslation() + const { styles } = useStyle() + const { mutate } = useAtomValue(deleteDepartAtom, usePageStoreOptions()) - const setCurrent = useSetAtom(selectedDepartAtom) + const setCurrent = useSetAtom(selectedDepartAtom, usePageStoreOptions()) - return ( -
- {name as any} - + return ( +
+ {name as any} + } - title={t('actions.add', '添加')} - onClick={(e) => { - // console.log('add') - e.stopPropagation() - e.preventDefault() - const data = { - ...defaultDepart, - parent_id: node.id, - } - setCurrent(data) - form.setFieldsValue(data) + size={12} + icon={} + title={t('actions.add', '添加')} + onClick={(e) => { + // console.log('add') + e.stopPropagation() + e.preventDefault() + const data = { + ...defaultDepart, + parent_id: node.id, + } + setCurrent(data) + form.setFieldsValue(data) - }}/> + }}/> { - mutate([ (node as any).id ]) - }} + title={t('message.deleteConfirm', '确定要删除吗?')} + onConfirm={() => { + mutate([ (node as any).id ]) + }} > { - e.stopPropagation() - e.stopPropagation() - }}/> + size={12} + onClick={(e) => { + e.stopPropagation() + e.stopPropagation() + }}/> -
- ) +
+ ) }) \ No newline at end of file diff --git a/src/pages/system/departments/index.tsx b/src/pages/system/departments/index.tsx index 88734f3..07016e7 100644 --- a/src/pages/system/departments/index.tsx +++ b/src/pages/system/departments/index.tsx @@ -1,3 +1,4 @@ +import { usePageStoreOptions } from '@/store' import { PageContainer, ProCard } from '@ant-design/pro-components' import { Flexbox } from 'react-layout-kit' import { DraggablePanel } from '@/components/draggable-panel' @@ -6,149 +7,149 @@ import { useStyle } from './style.ts' import DepartmentTree from './components/DepartmentTree.tsx' import { Alert, Button, Divider, Form, Input, InputNumber, InputRef, notification, TreeSelect } from 'antd' import { PlusOutlined } from '@ant-design/icons' -import { defaultDepart, selectedDepartAtom, departTreeAtom, saveOrUpdateDepartAtom } from './store.ts' +import { defaultDepart, selectedDepartAtom, departTreeAtom, saveOrUpdateDepartAtom } from '@/store/department.ts' import { useAtom, useAtomValue, } from 'jotai' import Glass from '@/components/glass' import { useEffect, useRef } from 'react' const Departments = () => { - const { t } = useTranslation() - const { styles, cx } = useStyle() - const [ form ] = Form.useForm() - const inputRef = useRef() - const { data } = useAtomValue(departTreeAtom) - const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateDepartAtom) - const [ current, setCurrent ] = useAtom(selectedDepartAtom) + const { t } = useTranslation() + const { styles, cx } = useStyle() + const [ form ] = Form.useForm() + const inputRef = useRef() + const { data } = useAtomValue(departTreeAtom, usePageStoreOptions()) + const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateDepartAtom, usePageStoreOptions()) + const [ current, setCurrent ] = useAtom(selectedDepartAtom, usePageStoreOptions()) - useEffect(() => { + useEffect(() => { - if (isError) { - notification.error({ - message: t('message.error', '错误'), - description: (error as any).message ?? t('message.saveFail', '保存失败'), - }) - } - }, [ isError ]) + if (isError) { + notification.error({ + message: t('message.error', '错误'), + description: (error as any).message ?? t('message.saveFail', '保存失败'), + }) + } + }, [ isError ]) - useEffect(() => { - if (current.id === 0 && inputRef.current) { - inputRef.current.focus() - } - }, [ current ]) + useEffect(() => { + if (current.id === 0 && inputRef.current) { + inputRef.current.focus() + } + }, [ current ]) - return ( - - - - - - -
- - -
-
- - - - } - > + return ( + + + + + + +
+ + +
+
+ + + + } + > - -
- - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - -
+ +
-
-
-
-
- ) +
+
+
+
+ ) } diff --git a/src/pages/system/departments/store.ts b/src/pages/system/departments/store.ts deleted file mode 100644 index 4a582a8..0000000 --- a/src/pages/system/departments/store.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' -import systemServ from '@/service/system.ts' -import { IApiResult, IPage } from '@/types' -import { IDepartment } from '@/types/department' -import { atom, createStore } from 'jotai' -import { t } from 'i18next' -import { message } from 'antd' - - -const store = createStore() - -export const departPageAtom = atom({}) - -export const defaultDepart = { - id: 0, - parent_id: 0, - name: '', - manager_user_id: 0, - phone: '', - sort: 0, -} as IDepartment - -export const batchIdsAtom = atom([]) - -export const selectedDepartAtom = atom({} as IDepartment) - -export const departTreeAtom = atomWithQuery(() => { - - return { - queryKey: [ 'departTree' ], - queryFn: async () => { - return await systemServ.dept.tree() - }, - select: (res) => { - return res.data.tree ?? [] - } - } -}) - -export const saveOrUpdateDepartAtom = atomWithMutation((get) => { - - return { - mutationKey: [ 'saveOrUpdateDepart' ], - mutationFn: async (data: IDepartment) => { - if (data.id) { - return await systemServ.dept.update(data) - } - return await systemServ.dept.add(data) - }, - onSuccess: (res) => { - const isAdd = !!res.data?.id - message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) - - if (isAdd) { - store.set(selectedDepartAtom, prev => ({ - ...prev, - id: res.data.id - })) - } - - get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree' ] }).then() - } - } - -}) - - -export const deleteDepartAtom = atomWithMutation((get) => { - - return { - mutationKey: [ 'deleteDepart', get(batchIdsAtom) ], - mutationFn: async (ids: number[]) => { - return await systemServ.dept.batchDelete(ids) - }, - onSuccess: () => { - message.success(t('message.deleteSuccess', '删除成功')) - store.set(batchIdsAtom, []) - get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree' ] }).then() - - } - } - -}) - diff --git a/src/pages/system/menus/components/BatchButton.tsx b/src/pages/system/menus/components/BatchButton.tsx index 8e5a45c..b6e938d 100644 --- a/src/pages/system/menus/components/BatchButton.tsx +++ b/src/pages/system/menus/components/BatchButton.tsx @@ -1,34 +1,35 @@ +import { usePageStoreOptions } from '@/store' import { Button, Popconfirm } from 'antd' import { useAtomValue } from 'jotai' -import { batchIdsAtom, deleteMenuAtom } from '../store.ts' +import { batchIdsAtom, deleteMenuAtom } from '@/store/menu.ts' import { useTranslation } from '@/i18n.ts' const BatchButton = () => { - const { t } = useTranslation() - const { isPending, mutate, } = useAtomValue(deleteMenuAtom) - const ids = useAtomValue(batchIdsAtom) + const { t } = useTranslation() + const { isPending, mutate, } = useAtomValue(deleteMenuAtom, usePageStoreOptions()) + const ids = useAtomValue(batchIdsAtom, usePageStoreOptions()) - if (ids.length === 0) { - return null - } + if (ids.length === 0) { + return null + } - return ( - { - mutate(ids as number[]) - }} - title={t('message.batchDelete', '确定要删除所选数据吗?')}> - - - ) + return ( + { + mutate(ids as number[]) + }} + title={t('message.batchDelete', '确定要删除所选数据吗?')}> + + + ) } export default BatchButton \ No newline at end of file diff --git a/src/pages/system/menus/components/MenuTree.tsx b/src/pages/system/menus/components/MenuTree.tsx index ae1ee6c..5f95081 100644 --- a/src/pages/system/menus/components/MenuTree.tsx +++ b/src/pages/system/menus/components/MenuTree.tsx @@ -1,11 +1,12 @@ +import { usePageStoreOptions } from '@/store' import { Empty, Spin, Tree } from 'antd' import { MenuItem } from '@/types' import { useStyle } from '../style.ts' import { useTranslation } from '@/i18n.ts' import { useSetAtom } from 'jotai' -import { batchIdsAtom, menuDataAtom, selectedMenuAtom } from '../store.ts' +import { batchIdsAtom, menuDataAtom, selectedMenuAtom } from '@/store/menu.ts' import { FormInstance } from 'antd/lib' -import { useAtomValue } from 'jotai/index' +import { useAtomValue } from 'jotai' import { TreeNodeRender } from './TreeNodeRender.tsx' import { useEffect, useRef } from 'react' import { flattenTree } from '@/utils' @@ -14,71 +15,71 @@ import { useDeepCompareEffect } from 'react-use' const MenuTree = ({ form }: { form: FormInstance }) => { - const { styles } = useStyle() - const { t } = useTranslation() - const setCurrentMenu = useSetAtom(selectedMenuAtom) - const setIds = useSetAtom(batchIdsAtom) - const { data = [], isLoading } = useAtomValue(menuDataAtom) - const flattenMenusRef = useRef([]) + const { styles } = useStyle() + const { t } = useTranslation() + const setCurrentMenu = useSetAtom(selectedMenuAtom, usePageStoreOptions()) + const setIds = useSetAtom(batchIdsAtom, usePageStoreOptions()) + const { data = [], isLoading } = useAtomValue(menuDataAtom, usePageStoreOptions()) + const flattenMenusRef = useRef([]) - useDeepCompareEffect(() => { + useDeepCompareEffect(() => { - if (isLoading) return + if (isLoading) return - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore array - if (data.length) { - // @ts-ignore flattenTree - flattenMenusRef.current = flattenTree(data as any) - // console.log(flattenMenusRef.current) - } - - }, [ data, isLoading ]) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore array + if (data.length) { + // @ts-ignore flattenTree + flattenMenusRef.current = flattenTree(data as any) + // console.log(flattenMenusRef.current) + } - useEffect(() => { - return () => { - setCurrentMenu({} as MenuItem) - setIds([]) - } - }, []) + }, [ data, isLoading ]) - const renderEmpty = () => { - if ((data as any).length > 0 || isLoading) return null - return + useEffect(() => { + return () => { + setCurrentMenu({} as MenuItem) + setIds([]) } + }, []) + + const renderEmpty = () => { + if ((data as any).length > 0 || isLoading) return null + return + } - return ( - <> - - { - renderEmpty() - } - { - return () - }} - fieldNames={{ - title: 'title', - key: 'id' - }} - onSelect={(item) => { - const current = flattenMenusRef.current?.find((menu) => menu.id === item[0]) - setCurrentMenu(current as MenuItem) - form.setFieldsValue({ ...current }) - }} - onCheck={(item) => { - setIds(item as number[]) - }} - checkable={true} - showIcon={false} - /> - - - ) + return ( + <> + + { + renderEmpty() + } + { + return () + }} + fieldNames={{ + title: 'title', + key: 'id' + }} + onSelect={(item) => { + const current = flattenMenusRef.current?.find((menu) => menu.id === item[0]) + setCurrentMenu(current as MenuItem) + form.setFieldsValue({ ...current }) + }} + onCheck={(item) => { + setIds(item as number[]) + }} + checkable={true} + showIcon={false} + /> + + + ) } export default MenuTree \ No newline at end of file diff --git a/src/pages/system/menus/components/TreeNodeRender.tsx b/src/pages/system/menus/components/TreeNodeRender.tsx index 4d479b6..c0193f7 100644 --- a/src/pages/system/menus/components/TreeNodeRender.tsx +++ b/src/pages/system/menus/components/TreeNodeRender.tsx @@ -1,58 +1,59 @@ +import { usePageStoreOptions } from '@/store' import { memo } from 'react' import { MenuItem } from '@/types' import { Popconfirm, Space, TreeDataNode } from 'antd' import { FormInstance } from 'antd/lib' import { useTranslation } from '@/i18n.ts' import { useStyle } from '../style.ts' -import { useAtomValue, useSetAtom } from 'jotai/index' -import { defaultMenu, deleteMenuAtom, selectedMenuAtom } from '../store.ts' +import { useAtomValue, useSetAtom } from 'jotai' +import { defaultMenu, deleteMenuAtom, selectedMenuAtom } from '@/store/menu.ts' import { PlusOutlined } from '@ant-design/icons' import ActionIcon, { DeleteAction } from '@/components/icon/action' export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => { - const { title } = node - const { t } = useTranslation() - const { styles } = useStyle() - const { mutate } = useAtomValue(deleteMenuAtom) + const { title } = node + const { t } = useTranslation() + const { styles } = useStyle() + const { mutate } = useAtomValue(deleteMenuAtom, usePageStoreOptions()) - const setMenuData = useSetAtom(selectedMenuAtom) + const setMenuData = useSetAtom(selectedMenuAtom, usePageStoreOptions()) - return ( -
- {title as any} - + return ( +
+ {title as any} + } - title={t('actions.add', '添加')} - onClick={(e) => { - // console.log('add') - e.stopPropagation() - e.preventDefault() - const menu = { - ...defaultMenu, - parent_id: node.id, - } - setMenuData(menu) - form.setFieldsValue(menu) + size={12} + icon={} + title={t('actions.add', '添加')} + onClick={(e) => { + // console.log('add') + e.stopPropagation() + e.preventDefault() + const menu = { + ...defaultMenu, + parent_id: node.id, + } + setMenuData(menu) + form.setFieldsValue(menu) - }}/> + }}/> { - mutate([ (node as any).id ]) - }} + title={t('message.deleteConfirm', '确定要删除吗?')} + onConfirm={() => { + mutate([ (node as any).id ]) + }} > { - e.stopPropagation() - e.stopPropagation() - }}/> + size={12} + onClick={(e) => { + e.stopPropagation() + e.stopPropagation() + }}/> -
- ) +
+ ) }) \ No newline at end of file diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx index 17e3694..846f229 100644 --- a/src/pages/system/menus/index.tsx +++ b/src/pages/system/menus/index.tsx @@ -1,10 +1,11 @@ import Glass from '@/components/glass' import { useTranslation } from '@/i18n.ts' +import { usePageStoreOptions } from '@/store' import { PlusOutlined } from '@ant-design/icons' import { PageContainer, 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.ts' +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' @@ -20,9 +21,9 @@ 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 { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom, usePageStoreOptions()) + const { data = [] } = useAtomValue(menuDataAtom, usePageStoreOptions()) + const [ currentMenu, setMenuData ] = useAtom(selectedMenuAtom, usePageStoreOptions()) ?? {} const menuInputRef = useRef(undefined) useEffect(() => { @@ -33,7 +34,7 @@ const Menus = () => { description: (error as any).message ?? t('message.saveFail', '保存失败'), }) } - }, [ isError ]) + }, [ isError ]) useEffect(() => { if (currentMenu.id === 0 && menuInputRef.current) { diff --git a/src/pages/system/menus/store.ts b/src/pages/system/menus/store.ts deleted file mode 100644 index 7c07cd7..0000000 --- a/src/pages/system/menus/store.ts +++ /dev/null @@ -1,95 +0,0 @@ -import systemServ from '@/service/system.ts' -import { IApiResult, IPage, MenuItem } from '@/types' -import { IMenu } from '@/types/menus' -import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' -import { atom, createStore } from 'jotai' -import { message } from 'antd' -import { t } from '@/i18n.ts' - - -export const defaultMenu = { - parent_id: 0, - type: 'menu', - name: '', - title: '', - icon: '', - path: '', - component: '', - sort: 0, - id: 0, - button: [], -} as unknown as MenuItem - -export const menuPageAtom = atom({}) - -const store = createStore() - -export const menuDataAtom = atomWithQuery((get) => { - - return { - queryKey: [ 'menus', get(menuPageAtom) ], - queryFn: async ({ queryKey: [ , page ] }) => { - return await systemServ.menus.list(page) - }, - select: (res) => { - return res.data.rows ?? [] - } - } -}) - - -export const selectedMenuIdAtom = atom(0) -export const selectedMenuAtom = atom({} as MenuItem) -export const byIdMenuAtom = atomWithQuery((get) => ({ - queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], - queryFn: async ({ queryKey: [ , id ] }) => { - return await systemServ.menus.info(id as number) - }, - select: data => data.data, -})) - - -export const saveOrUpdateMenuAtom = atomWithMutation((get) => { - - return { - mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], - mutationFn: async (data: IMenu) => { - if (data.id === 0) { - return await systemServ.menus.add(data) - } - return await systemServ.menus.update(data) - }, - onSuccess: (res) => { - const isAdd = !!res.data?.id - message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) - - if(isAdd) { - const menu = get(selectedMenuAtom) - store.set(selectedMenuAtom, { - ...menu, - id: res.data?.id - }) - } - //更新列表 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore fix - get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() - } - } -}) - -export const batchIdsAtom = atom([]) - -export const deleteMenuAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'deleteMenu', get(batchIdsAtom) ], - mutationFn: async (ids?: number[]) => { - return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) - }, - onSuccess: () => { - message.success(t('message.deleteSuccess', '删除成功')) - store.set(batchIdsAtom, []) - get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() - } - } -}) \ No newline at end of file diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 683552b..e1508f1 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -1,4 +1,5 @@ import Switch from '@/components/switch' +import { usePageStoreOptions } from '@/store' import { IMenu } from '@/types/menus' import { ActionType, @@ -18,11 +19,11 @@ import { rolesAtom, saveOrUpdateRoleAtom, searchAtom -} from './store.ts' +} from '@/store/role.ts' import { useTranslation } from '@/i18n.ts' import { Button, Form, Space, Spin, Table, Tree, Popconfirm } from 'antd' import { PlusOutlined } from '@ant-design/icons' -import { menuDataAtom } from '@/pages/system/menus/store.ts' +import { menuDataAtom } from '@/store/menu.ts' import { getTreeCheckedStatus } from '@/utils/tree.ts' const MenuTree = (props: any) => { @@ -53,13 +54,13 @@ const Roles = memo(() => { const { styles } = useStyle() const [ form ] = Form.useForm() const actionRef = useRef() - const [ page, setPage ] = useAtom(pageAtom) - const [ search, setSearch ] = useAtom(searchAtom) - const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom) - const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom) - const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom) - const { mutate: deleteRole, isPending: isDeleting } = useAtomValue(deleteRoleAtom) - const [ , setRole ] = useAtom(roleAtom) + const [ page, setPage ] = useAtom(pageAtom, usePageStoreOptions()) + const [ search, setSearch ] = useAtom(searchAtom, usePageStoreOptions()) + const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom, usePageStoreOptions()) + const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom, usePageStoreOptions()) + const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom, usePageStoreOptions()) + const { mutate: deleteRole, isPending: isDeleting } = useAtomValue(deleteRoleAtom, usePageStoreOptions()) + const [ , setRole ] = useAtom(roleAtom, usePageStoreOptions()) const [ open, setOpen ] = useState(false) const columns = useMemo(() => { diff --git a/src/pages/system/roles/store.ts b/src/pages/system/roles/store.ts deleted file mode 100644 index 25443dd..0000000 --- a/src/pages/system/roles/store.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { convertToBool } from '@/utils' -import { atom } from 'jotai/index' -import { IRole } from '@/types/roles' -import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' -import { IApiResult, IPage } from '@/types' -import systemServ from '@/service/system.ts' -import { message } from 'antd' -import { t } from '@/i18n.ts' - -type SearchParams = IPage & { - key?: string -} - -export const idAtom = atom(0) - -export const roleIdsAtom = atom([]) - -export const roleAtom = atom(undefined as unknown as IRole) - -export const searchAtom = atom({ - key: '' -} as SearchParams) - -export const pageAtom = atom({ - pageSize: 10, - page: 1, -}) - -export const rolesAtom = atomWithQuery((get) => { - return { - queryKey: [ 'roles', get(searchAtom) ], - queryFn: async ({ queryKey: [ , params ] }) => { - return await systemServ.role.list(params as SearchParams) - }, - select: res => { - const data = res.data - data.rows = data.rows?.map(row => { - return { - ...row, - status: convertToBool(row.status) - } - }) - return data - } - } -}) - -//saveOrUpdateRoleAtom - -export const saveOrUpdateRoleAtom = atomWithMutation((get) => { - - return { - mutationKey: [ 'updateMenu' ], - mutationFn: async (data) => { - data.status = data.status ? '1' : '0' - if (data.id === 0) { - return await systemServ.role.add(data) - } - return await systemServ.role.update(data) - }, - onSuccess: (res) => { - const isAdd = !!res.data?.id - message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) - - //更新列表 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore fix - get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) - - return res - } - } -}) - -export const deleteRoleAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'deleteMenu' ], - mutationFn: async (ids: number[]) => { - return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom)) - }, - onSuccess: (res) => { - message.success('message.deleteSuccess') - //更新列表 - get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) - return res - } - } -}) \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index 3078702..cec987d 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,9 +1,11 @@ import NotPermission from '@/components/error/403.tsx' import NotFound from '@/components/error/404.tsx' import ErrorPage from '@/components/error/error.tsx' +import Loading from '@/components/loading' import FetchLoading from '@/components/loading/FetchLoading.tsx' import PageLoading from '@/components/page-loading' -import { Route as AuthenticatedImport } from '@/layout/_authenticated.tsx' +import { PageStoreProvider } from '@/store' +import { AuthenticatedRoute as AuthenticatedImport } from './_authenticatedRoute.tsx' import EmptyLayout from '@/layout/EmptyLayout.tsx' // import ListPageLayout from '@/layout/ListPageLayout.tsx' // import { Route as DashboardImport } from '@/pages/dashboard' @@ -11,13 +13,13 @@ import { Route as LoginRouteImport } from '@/pages/login' import { generateUUID } from '@/utils/uuid.ts' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { - AnyRoute, - createRootRouteWithContext, - createRoute, - createRouter, lazyRouteComponent, - Outlet, - redirect, - RouterProvider, + AnyRoute, + createRootRouteWithContext, + createRoute, + createRouter, lazyRouteComponent, + Outlet, + redirect, + RouterProvider, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { memo } from 'react' @@ -25,66 +27,71 @@ import RootLayout from './layout/RootLayout' import { IRootContext, MenuItem } from './types' import { DevTools } from 'jotai-devtools' +const PageRootLayout = () => { + return + + +} export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - } + defaultOptions: { + queries: { + retry: false, } + } }) const rootRoute = createRootRouteWithContext()({ - component: () => ( - -
- - - - -
- ), - beforeLoad: ({ location }) => { - if (location.pathname === '/') { - return redirect({ to: '/dashboard' }) - } - }, - loader: () => { + component: () => ( + + <> + + + + + + ), + beforeLoad: ({ location }) => { + if (location.pathname === '/') { + return redirect({ to: '/dashboard' }) + } + }, + loader: () => { - }, - notFoundComponent: NotFound, - pendingComponent: PageLoading, - errorComponent: ({ error }) => , + }, + notFoundComponent: NotFound, + pendingComponent: PageLoading, + errorComponent: ({ error }) => , }) const emptyRoute = createRoute({ - getParentRoute: () => rootRoute, - id: '/_empty', - component: EmptyLayout, + getParentRoute: () => rootRoute, + id: '/_empty', + component: EmptyLayout, }) const authRoute = AuthenticatedImport.update({ - getParentRoute: () => rootRoute, - id: '/_authenticated', + getParentRoute: () => rootRoute, + id: '/_authenticated', } as any) const layoutNormalRoute = createRoute({ - getParentRoute: () => rootRoute, - id: '/_normal_layout', - component: RootLayout, + getParentRoute: () => rootRoute, + id: '/_normal_layout', + component: PageRootLayout, }) const layoutAuthRoute = createRoute({ - getParentRoute: () => authRoute, - id: '/_auth_layout', - component: RootLayout, + getParentRoute: () => authRoute, + id: '/_auth_layout', + component: PageRootLayout, }) const notAuthRoute = createRoute({ - getParentRoute: () => layoutNormalRoute, - path: '/not-auth', - component: NotPermission + getParentRoute: () => layoutNormalRoute, + path: '/not-auth', + component: NotPermission }) // const dashboardRoute = DashboardImport.update({ @@ -93,9 +100,10 @@ const notAuthRoute = createRoute({ // } as any) const loginRoute = LoginRouteImport.update({ - path: '/login', - getParentRoute: () => emptyRoute, + path: '/login', + getParentRoute: () => emptyRoute, } as any) + // // const menusRoute = createRoute({ // getParentRoute: () => layoutAuthRoute, @@ -119,169 +127,170 @@ const loginRoute = LoginRouteImport.update({ declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/_authenticated': { - preLoaderRoute: typeof AuthenticatedImport - parentRoute: typeof rootRoute - }, - '/_normal_layout': { - preLoaderRoute: typeof layoutNormalRoute - parentRoute: typeof rootRoute - }, - '/_layout': { - preLoaderRoute: typeof layoutAuthRoute - parentRoute: typeof rootRoute - }, - // '/': { - // preLoaderRoute: typeof DashboardImport - // parentRoute: typeof layoutAuthRoute - // }, - // '/dashboard': { - // preLoaderRoute: typeof DashboardImport - // parentRoute: typeof layoutAuthRoute - // }, - '/login': { - preLoaderRoute: typeof LoginRouteImport - parentRoute: typeof rootRoute - }, - // '/system/menus': { - // preLoaderRoute: typeof menusRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/departments': { - // preLoaderRoute: typeof departmentsRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/users': { - // preLoaderRoute: typeof usersRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/roles': { - // preLoaderRoute: typeof rolesRoute - // parentRoute: typeof layoutAuthRoute - // }, - '/welcome': { - preLoaderRoute: typeof rootRoute - parentRoute: typeof layoutAuthRoute - }, - } + interface FileRoutesByPath { + '/_authenticated': { + preLoaderRoute: typeof AuthenticatedImport + parentRoute: typeof rootRoute + }, + '/_normal_layout': { + preLoaderRoute: typeof layoutNormalRoute + parentRoute: typeof rootRoute + }, + '/_layout': { + preLoaderRoute: typeof layoutAuthRoute + parentRoute: typeof rootRoute + }, + // '/': { + // preLoaderRoute: typeof DashboardImport + // parentRoute: typeof layoutAuthRoute + // }, + // '/dashboard': { + // preLoaderRoute: typeof DashboardImport + // parentRoute: typeof layoutAuthRoute + // }, + '/login': { + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRoute + }, + // '/system/menus': { + // preLoaderRoute: typeof menusRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/departments': { + // preLoaderRoute: typeof departmentsRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/users': { + // preLoaderRoute: typeof usersRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/roles': { + // preLoaderRoute: typeof rolesRoute + // parentRoute: typeof layoutAuthRoute + // }, + '/welcome': { + preLoaderRoute: typeof rootRoute + parentRoute: typeof layoutAuthRoute + }, + } } -export const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => { - // 递归生成路由,如果有routes则递归生成子路由 +const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => { + // 递归生成路由,如果有routes则递归生成子路由 - const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { - const path = menu.path?.replace(parentRoute.options?.path, '') - const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu' + const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { + const path = menu.path?.replace(parentRoute.options?.path, '') + const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu' - if (isLayout && (!menu.path || !menu.component)) { - //没有component的layout,直接返回 - return createRoute({ - getParentRoute: () => layoutAuthRoute, - id: `/layout-no-path-${generateUUID()}`, - component: EmptyLayout, - }) - } - - // @ts-ignore 添加menu属性,方便后面获取 - const options = { - getParentRoute: () => parentRoute, - menu, - } as any - - if (isLayout) { - options.id = path ?? `/layout-${generateUUID()}` - } else { - if (!path) { - console.log(`${menu.name}没有设置视图`) - } else { - options.path = path - } - } - - let component = menu.component - // menu.type - // 1,组件(页面),2,IFrame,3,外链接,4,按钮 - if (menu.type === 'iframe') { - component = '@/components/Iframe' - } - - //处理component路径 - component = component.replace(/^\/pages/, '') - component = component.replace(/^\//, '') + if (isLayout && (!menu.path || !menu.component)) { + //没有component的layout,直接返回 + return createRoute({ + getParentRoute: () => layoutAuthRoute, + id: `/layout-no-path-${generateUUID()}`, + component: EmptyLayout, + }) + } - return createRoute({ - ...options, - component: lazyRouteComponent(() => import(`./pages/${component}`)), - notFoundComponent: NotFound, - }) + // @ts-ignore 添加menu属性,方便后面获取 + const options = { + getParentRoute: () => parentRoute, + menu, + } as any + + if (isLayout) { + options.id = path ?? `/layout-${generateUUID()}` + } else { + if (!path) { + console.log(`${menu.name}没有设置视图`) + } else { + options.path = path + } } - // 对menuData递归生成路由,只处理type =1 的菜单 - const did = (menus: MenuItem[], parentRoute: AnyRoute) => { - return menus.filter((item) => item.type === 'menu').map((item, index) => { - // 如果有children则递归生成子路由,同样只处理type =1 的菜单 - const route = generateRoutes(item, parentRoute) - - // console.log(route) - if (item.children && item.children.length > 0) { - const children = did(item.children, route) - if (children.length > 0) { - route.addChildren(children) - } - } - route.init({ originalIndex: index }) - return route - }) + let component = menu.component + // menu.type + // 1,组件(页面),2,IFrame,3,外链接,4,按钮 + if (menu.type === 'iframe') { + component = '@/components/Iframe' } - const routes = did(menuData, parentRoute) + //处理component路径 + component = component.replace(/^\/pages/, '') + component = component.replace(/^\//, '') - parentRoute.addChildren(routes) + return createRoute({ + ...options, + component: lazyRouteComponent(() => import(`./pages/${component}`)), + notFoundComponent: NotFound, + }) + } + + // 对menuData递归生成路由,只处理type =1 的菜单 + const did = (menus: MenuItem[], parentRoute: AnyRoute) => { + return menus.filter((item) => item.type === 'menu').map((item, index) => { + // 如果有children则递归生成子路由,同样只处理type =1 的菜单 + const route = generateRoutes(item, parentRoute) + + // console.log(route) + if (item.children && item.children.length > 0) { + const children = did(item.children, route) + if (children.length > 0) { + route.addChildren(children) + } + } + route.init({ originalIndex: index }) + return route + }) + } + + const routes = did(menuData, parentRoute) + + parentRoute.addChildren(routes) } const routeTree = rootRoute.addChildren( - [ - //非Layout - loginRoute, - emptyRoute, - - //不带权限Layout - layoutNormalRoute.addChildren([ - notAuthRoute, - ]), - - //带权限Layout - // dashboardRoute, - authRoute.addChildren( - [ - layoutAuthRoute - /*.addChildren( - [ - menusRoute, - departmentsRoute, - usersRoute, - rolesRoute, - ] - ),*/ - ]), - ] + [ + //非Layout + loginRoute, + emptyRoute, + + //不带权限Layout + layoutNormalRoute.addChildren([ + notAuthRoute, + ]), + + //带权限Layout + // dashboardRoute, + authRoute.addChildren( + [ + layoutAuthRoute + /*.addChildren( + [ + menusRoute, + departmentsRoute, + usersRoute, + rolesRoute, + ] + ),*/ + ]), + ] ) export const RootProvider = memo((props: { context: Partial }) => { - generateDynamicRoutes(props.context.menuData ?? [], layoutAuthRoute) + generateDynamicRoutes(props.context.menuData ?? [], layoutAuthRoute) - const router = createRouter({ - routeTree, - context: { queryClient, menuData: [] }, - defaultPreload: 'intent' - }) + const router = createRouter({ + routeTree, + context: { queryClient, menuData: [] }, + defaultPreload: 'intent', + defaultPendingComponent: () => + }) - return ( - - - - ) + return ( + + + + ) }) \ No newline at end of file diff --git a/src/store/department.ts b/src/store/department.ts index 51f9ccb..a210495 100644 --- a/src/store/department.ts +++ b/src/store/department.ts @@ -1,44 +1,84 @@ -import { atom } from 'jotai' -import { IDepartment } from '../types/department' -import { QueryClient } from '@tanstack/query-core' -import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' -import { IApiResult } from '../types' -import request from '../request.ts' +import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' +import systemServ from '@/service/system.ts' +import { IApiResult, IPage } from '@/types' +import { IDepartment } from '@/types/department' +import { atom, createStore } from 'jotai' +import { t } from 'i18next' +import { message } from 'antd' -export const departmentSearchAtom = atom>({}) +const store = createStore() -export const departmentAtom = atomWithQuery, any, IDepartment[]>((get) => ({ - queryKey: [ 'departments', get(departmentSearchAtom) ], - queryFn: async ({ queryKey: [ , departSearch ] }) => { - await new Promise(resolve => setTimeout(resolve, 5000)) +export const departPageAtom = atom({}) - return await request('/departments', { - data: departSearch, - }) - }, - select: data => data.data, -})) +export const defaultDepart = { + id: 0, + parent_id: 0, + name: '', + manager_user_id: 0, + phone: '', + sort: 0, +} as IDepartment + +export const batchIdsAtom = atom([]) + +export const selectedDepartAtom = atom({} as IDepartment) -export const departmentDetailAtom = atomWithQuery>((get) => ({ - queryKey: [ 'department', get(departmentSearchAtom) ], - queryFn: async ({ queryKey: [ , departSearch ] }) => { - return await request(`/departments/${(departSearch as IDepartment).id}`) +export const departTreeAtom = atomWithQuery(() => { + + return { + queryKey: [ 'departTree' ], + queryFn: async () => { + return await systemServ.dept.tree() }, - select: data => data, -})) - -//add use atomWithMutation -export const departmentAddAtom = (client: QueryClient) => atomWithMutation, any, IDepartment>(() => ({ - mutationKey: [ 'addDepartment', ], - mutationFn: async (depart: IDepartment) => { - return await request.post('/departments', depart) + select: (res) => { + return res.data.tree ?? [] + } + } +}) + +export const saveOrUpdateDepartAtom = atomWithMutation((get) => { + + return { + mutationKey: [ 'saveOrUpdateDepart' ], + mutationFn: async (data: IDepartment) => { + if (data.id) { + return await systemServ.dept.update(data) + } + return await systemServ.dept.add(data) }, - onSuccess: (result) => { - console.log(result) + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + if (isAdd) { + store.set(selectedDepartAtom, prev => ({ + ...prev, + id: res.data.id + })) + } + + get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree' ] }).then() + } + } + +}) + + +export const deleteDepartAtom = atomWithMutation((get) => { + + return { + mutationKey: [ 'deleteDepart', get(batchIdsAtom) ], + mutationFn: async (ids: number[]) => { + return await systemServ.dept.batchDelete(ids) }, - onSettled: () => { - //清空列表的缓存 - void client.invalidateQueries({ queryKey: [ 'departments' ] }) + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + store.set(batchIdsAtom, []) + get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree' ] }).then() + } -})) + } + +}) + diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..a697ae7 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,36 @@ +import { createStore } from 'jotai' +import React, { useContext, createContext } from 'react' + +export type Store = ReturnType + +export const InternalPageContext = createContext( + undefined, +) + +export const useInternalStore = (): Store => { + const store = useContext(InternalPageContext) + if (!store) { + throw new Error( + `Unable to find internal Page store, Did you wrap the component within PageStoreProvider?`, + ) + } + return store +} + +export const usePageStoreOptions = () => ({ + store: useInternalStore(), +}) + +export const pageStore = createStore() + +export const PageStoreProvider = ({ children }: React.PropsWithChildren) => { + const internalStoreRef = React.useRef() + + if (!internalStoreRef.current) { + internalStoreRef.current = pageStore + } + + return React.createElement(InternalPageContext.Provider, { + value: internalStoreRef.current + }, children) +} \ No newline at end of file diff --git a/src/store/menu.ts b/src/store/menu.ts new file mode 100644 index 0000000..f04e0b1 --- /dev/null +++ b/src/store/menu.ts @@ -0,0 +1,88 @@ +import systemServ from '@/service/system.ts' +import { IApiResult, IPage, MenuItem } from '@/types' +import { IMenu } from '@/types/menus' +import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' +import { atom, createStore } from 'jotai' +import { message } from 'antd' +import { t } from '@/i18n.ts' + + +export const defaultMenu = { + parent_id: 0, + type: 'menu', + name: '', + title: '', + icon: '', + path: '', + component: '', + sort: 0, + id: 0, + button: [], +} as unknown as MenuItem + +export const menuPageAtom = atom({}) + +const store = createStore() + +export const menuDataAtom = atomWithQuery((get) => { + + return { + queryKey: [ 'menus', get(menuPageAtom) ], + queryFn: async ({ queryKey: [ , page ] }) => { + return await systemServ.menus.list(page) + }, + select: (res) => { + return res.data.rows ?? [] + } + } +}) + + +export const selectedMenuIdAtom = atom(0) +export const selectedMenuAtom = atom({} as MenuItem) +export const byIdMenuAtom = atomWithQuery((get) => ({ + queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], + queryFn: async ({ queryKey: [ , id ] }) => { + return await systemServ.menus.info(id as number) + }, + select: data => data.data, +})) + + +export const saveOrUpdateMenuAtom = atomWithMutation((get) => { + + return { + mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], + mutationFn: async (data: IMenu) => { + if (data.id === 0) { + return await systemServ.menus.add(data) + } + return await systemServ.menus.update(data) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + //更新列表 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore fix + get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() + } + } +}) + +export const batchIdsAtom = atom([]) + +export const deleteMenuAtom = atomWithMutation((get) => { + return { + mutationKey: [ 'deleteMenu', get(batchIdsAtom) ], + mutationFn: async (ids?: number[]) => { + return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) + }, + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + store.set(batchIdsAtom, []) + get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() + } + } +}) \ No newline at end of file diff --git a/src/store/role.ts b/src/store/role.ts new file mode 100644 index 0000000..25443dd --- /dev/null +++ b/src/store/role.ts @@ -0,0 +1,88 @@ +import { convertToBool } from '@/utils' +import { atom } from 'jotai/index' +import { IRole } from '@/types/roles' +import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' +import { IApiResult, IPage } from '@/types' +import systemServ from '@/service/system.ts' +import { message } from 'antd' +import { t } from '@/i18n.ts' + +type SearchParams = IPage & { + key?: string +} + +export const idAtom = atom(0) + +export const roleIdsAtom = atom([]) + +export const roleAtom = atom(undefined as unknown as IRole) + +export const searchAtom = atom({ + key: '' +} as SearchParams) + +export const pageAtom = atom({ + pageSize: 10, + page: 1, +}) + +export const rolesAtom = atomWithQuery((get) => { + return { + queryKey: [ 'roles', get(searchAtom) ], + queryFn: async ({ queryKey: [ , params ] }) => { + return await systemServ.role.list(params as SearchParams) + }, + select: res => { + const data = res.data + data.rows = data.rows?.map(row => { + return { + ...row, + status: convertToBool(row.status) + } + }) + return data + } + } +}) + +//saveOrUpdateRoleAtom + +export const saveOrUpdateRoleAtom = atomWithMutation((get) => { + + return { + mutationKey: [ 'updateMenu' ], + mutationFn: async (data) => { + data.status = data.status ? '1' : '0' + if (data.id === 0) { + return await systemServ.role.add(data) + } + return await systemServ.role.update(data) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + + //更新列表 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore fix + get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) + + return res + } + } +}) + +export const deleteRoleAtom = atomWithMutation((get) => { + return { + mutationKey: [ 'deleteMenu' ], + mutationFn: async (ids: number[]) => { + return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom)) + }, + onSuccess: (res) => { + message.success('message.deleteSuccess') + //更新列表 + get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) + return res + } + } +}) \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 98a5ead..61fa264 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,9 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import { viteMockServe } from 'vite-plugin-mock' +import jotaiDebugLabel from 'jotai/babel/plugin-debug-label' +import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh' + //import { TanStackRouterVite } from '@tanstack/router-vite-plugin' @@ -10,6 +13,7 @@ export default defineConfig(({ mode }) => { // 根据当前工作目录中的 `mode` 加载 .env 文件 // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。 + // @ts-ignore fix process const env = loadEnv(mode, process.cwd(), '') return { //定义别名的路径 @@ -30,7 +34,8 @@ export default defineConfig(({ mode }) => { plugins: [ react({ babel: { - presets: ['jotai/babel/preset'], + presets: [ 'jotai/babel/preset' ], + plugins: [ jotaiDebugLabel, jotaiReactRefresh ] }, }), viteMockServe({