Browse Source

update

main
李金 7 months ago
parent
commit
3e45be5097
  1. 1
      package.json
  2. 2
      src/_authenticatedRoute.tsx
  3. 38
      src/components/loading/index.tsx
  4. 4
      src/components/loading/style.ts
  5. 22
      src/components/user-picker/Item.tsx
  6. 61
      src/components/user-picker/UserPicker.tsx
  7. 1
      src/components/user-picker/index.ts
  8. 5
      src/components/user-picker/store.ts
  9. 25
      src/components/user-picker/style.ts
  10. 15
      src/pages/list/list.tsx
  11. 146
      src/pages/list/tree.tsx
  12. 11
      src/pages/system/departments/components/DepartmentTree.tsx
  13. 9
      src/pages/system/departments/components/TreeNodeRender.tsx
  14. 9
      src/pages/system/departments/index.tsx
  15. 84
      src/pages/system/departments/store.ts
  16. 7
      src/pages/system/menus/components/BatchButton.tsx
  17. 11
      src/pages/system/menus/components/MenuTree.tsx
  18. 9
      src/pages/system/menus/components/TreeNodeRender.tsx
  19. 9
      src/pages/system/menus/index.tsx
  20. 19
      src/pages/system/roles/index.tsx
  21. 23
      src/routes.tsx
  22. 106
      src/store/department.ts
  23. 36
      src/store/index.ts
  24. 7
      src/store/menu.ts
  25. 0
      src/store/role.ts
  26. 5
      vite.config.ts

1
package.json

@ -39,6 +39,7 @@
"react-layout-kit": "^1.9.0", "react-layout-kit": "^1.9.0",
"react-rnd": "^10.4.2-test2", "react-rnd": "^10.4.2-test2",
"react-use": "^17.5.0", "react-use": "^17.5.0",
"throttle-debounce": "^5.0.0",
"wonka": "^6.3.4" "wonka": "^6.3.4"
}, },
"devDependencies": { "devDependencies": {

2
src/layout/_authenticated.tsx → src/_authenticatedRoute.tsx

@ -1,7 +1,7 @@
import { isAuthenticated } from '@/utils/auth.ts' import { isAuthenticated } from '@/utils/auth.ts'
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
export const AuthenticatedRoute = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => { beforeLoad: async ({ location }) => {
if (!isAuthenticated()) { if (!isAuthenticated()) {
throw redirect({ throw redirect({

38
src/components/loading/index.tsx

@ -1,17 +1,43 @@
import React from 'react'
import { useEffect, useState } from 'react'
import { useStyles } from './style.ts' import { useStyles } from './style.ts'
import { debounce } from 'throttle-debounce'
interface ILoading { interface ILoading {
loading: boolean, loading: boolean,
className?: string 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 }) const { styles, cx } = useStyles({ className })
return (
<div className={cx(styles.container, styles.base, loading ? styles.visible : styles.hidden)}>
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 <div className={cx(styles.container, styles.base, isLoading ? styles.visible : styles.hidden)}>
<div className={styles.centeredElement}> <div className={styles.centeredElement}>
<svg stroke="currentColor" fill="none" strokeWidth="0" viewBox="0 0 24 24" className={styles.svgIcon} <svg stroke="currentColor" fill="none" strokeWidth="0" viewBox="0 0 24 24" className={styles.svgIcon}
height="30px" width="30px" xmlns="http://www.w3.org/2000/svg"> height="30px" width="30px" xmlns="http://www.w3.org/2000/svg">
@ -22,7 +48,9 @@ export const Loading = ({ loading, className }: ILoading) => {
</svg> </svg>
</div> </div>
</div> </div>
)
}
return render()
} }
export default Loading export default Loading

4
src/components/loading/style.ts

@ -1,12 +1,12 @@
import { createStyles } from '@/theme' import { createStyles } from '@/theme'
// Define styles using createStyles // 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` const prefix = `${prefixCls}-${token.proPrefix}-loading`
return { return {
container: cx(prefix, className),
container: cx(prefix, props.className),
base: css` base: css`
--tw-translate-x: 0; --tw-translate-x: 0;

22
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 (
<div className={styles.listItem}>
<Checkbox value={props.value} onChange={e=>}>
<span>{props.value.name}</span>
</Checkbox>
</div>
)
}

61
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 (
<Select {...props}>
</Select>
)
})
const UserModel = memo(({ open, ...props }: UserModelProps) => {
const { styles } = useStyle()
return (
<Modal
open={open}
{...props}
>
<Flexbox horizontal={true} className={styles.container}>
<Flexbox>
</Flexbox>
<Flexbox>
</Flexbox>
</Flexbox>
</Modal>
)
})
const UserPicker = memo(({ type, ...props }: UserPickerProps) => {
return type === 'modal' ? <UserModel {...props} /> : <UserSelect {...props as UserSelectProps} />
})
export default UserPicker

1
src/components/user-picker/index.ts

@ -0,0 +1 @@
export * from './UserPicker.tsx'

5
src/components/user-picker/store.ts

@ -0,0 +1,5 @@
import { IUser } from '@/types/user'
import { atom } from 'jotai'
export const userSelectedAtom = atom<IUser[]>([])

25
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,
}
})

15
src/pages/list/list.tsx

@ -1,15 +0,0 @@
import { createLazyRoute } from '@tanstack/react-router'
import { ProCard } from '@ant-design/pro-components'
const List = () => {
return (
<ProCard>
</ProCard>
)
}
export const Route = createLazyRoute('/list/index')({
component: List,
})
export default List

146
src/pages/list/tree.tsx

@ -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: (_) => <a>{_}</a>,
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 [
<a key="editable" onClick={() => {
alert('edit')
}}></a>,
<a key="delete" onClick={() => {
alert('delete')
}}></a>,
]
}
},
]
const TreePage = () => {
const { data, isError, isPending } = useAtomValue(departmentAtom)
if (isError) {
return <div>Error</div>
}
// if (isPending){
// return <div>Loading</div>
// }
return (
<PageContainer breadcrumbRender={false}>
<ProCard split="vertical">
<ProCard title="部门"
colSpan="25%"
loading={isPending}
extra={<>
<Button size={'small'} icon={getIcon('Plus')} shape={'circle'}/>
</>}
>
<Tree showLine={true} treeData={renderTree(data)}/>
</ProCard>
<ProCard headerBordered>
<div style={{ height: 360 }}>
<ProTable
rowKey="account"
headerTitle={'帐号列表'}
columns={columns}
dataSource={[
{
name: '张三',
account: 'zhangsan',
},
{
name: '李四',
account: 'lisi',
},
]}
// pagination={false}
options={{
search: true,
}}
search={false}
toolbar={{
search: {
onSearch: (value: string) => {
alert(value)
},
},
actions: [
<Button
key="primary"
type="primary"
onClick={() => {
alert('add')
}}
>
</Button>,
],
}}
/>
</div>
</ProCard>
</ProCard>
</PageContainer>
)
}
export default TreePage

11
src/pages/system/departments/components/DepartmentTree.tsx

@ -1,10 +1,11 @@
import { usePageStoreOptions } from '@/store'
import { Empty, Spin, Tree } from 'antd' import { Empty, Spin, Tree } from 'antd'
import { useStyle } from '../style.ts' import { useStyle } from '../style.ts'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { useSetAtom } from 'jotai' 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 { FormInstance } from 'antd/lib'
import { useAtomValue } from 'jotai/index'
import { useAtomValue } from 'jotai'
import { TreeNodeRender } from './TreeNodeRender.tsx' import { TreeNodeRender } from './TreeNodeRender.tsx'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { flattenTree } from '@/utils' import { flattenTree } from '@/utils'
@ -15,9 +16,9 @@ export const DepartmentTree = ({ form }: { form: FormInstance }) => {
const { styles } = useStyle() const { styles } = useStyle()
const { t } = useTranslation() const { t } = useTranslation()
const setIds = useSetAtom(batchIdsAtom)
const setCurrent = useSetAtom(selectedDepartAtom)
const { data = [], isLoading } = useAtomValue(departTreeAtom)
const setIds = useSetAtom(batchIdsAtom, usePageStoreOptions())
const setCurrent = useSetAtom(selectedDepartAtom, usePageStoreOptions())
const { data = [], isLoading } = useAtomValue(departTreeAtom, usePageStoreOptions())
const flattenMenusRef = useRef<IDepartment[]>([]) const flattenMenusRef = useRef<IDepartment[]>([])
useDeepCompareEffect(() => { useDeepCompareEffect(() => {

9
src/pages/system/departments/components/TreeNodeRender.tsx

@ -1,11 +1,12 @@
import { usePageStoreOptions } from '@/store'
import { memo } from 'react' import { memo } from 'react'
import { MenuItem } from '@/types' import { MenuItem } from '@/types'
import { Popconfirm, Space, TreeDataNode } from 'antd' import { Popconfirm, Space, TreeDataNode } from 'antd'
import { FormInstance } from 'antd/lib' import { FormInstance } from 'antd/lib'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { useStyle } from '../style.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 { PlusOutlined } from '@ant-design/icons'
import ActionIcon, { DeleteAction } from '@/components/icon/action' import ActionIcon, { DeleteAction } from '@/components/icon/action'
@ -13,9 +14,9 @@ export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataN
const { name } = node const { name } = node
const { t } = useTranslation() const { t } = useTranslation()
const { styles } = useStyle() const { styles } = useStyle()
const { mutate } = useAtomValue(deleteDepartAtom)
const { mutate } = useAtomValue(deleteDepartAtom, usePageStoreOptions())
const setCurrent = useSetAtom(selectedDepartAtom)
const setCurrent = useSetAtom(selectedDepartAtom, usePageStoreOptions())
return ( return (
<div className={styles.treeNode}> <div className={styles.treeNode}>

9
src/pages/system/departments/index.tsx

@ -1,3 +1,4 @@
import { usePageStoreOptions } from '@/store'
import { PageContainer, ProCard } from '@ant-design/pro-components' import { PageContainer, ProCard } from '@ant-design/pro-components'
import { Flexbox } from 'react-layout-kit' import { Flexbox } from 'react-layout-kit'
import { DraggablePanel } from '@/components/draggable-panel' import { DraggablePanel } from '@/components/draggable-panel'
@ -6,7 +7,7 @@ import { useStyle } from './style.ts'
import DepartmentTree from './components/DepartmentTree.tsx' import DepartmentTree from './components/DepartmentTree.tsx'
import { Alert, Button, Divider, Form, Input, InputNumber, InputRef, notification, TreeSelect } from 'antd' import { Alert, Button, Divider, Form, Input, InputNumber, InputRef, notification, TreeSelect } from 'antd'
import { PlusOutlined } from '@ant-design/icons' 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 { useAtom, useAtomValue, } from 'jotai'
import Glass from '@/components/glass' import Glass from '@/components/glass'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
@ -17,9 +18,9 @@ const Departments = () => {
const { styles, cx } = useStyle() const { styles, cx } = useStyle()
const [ form ] = Form.useForm() const [ form ] = Form.useForm()
const inputRef = useRef<InputRef>() const inputRef = useRef<InputRef>()
const { data } = useAtomValue(departTreeAtom)
const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateDepartAtom)
const [ current, setCurrent ] = useAtom(selectedDepartAtom)
const { data } = useAtomValue(departTreeAtom, usePageStoreOptions())
const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateDepartAtom, usePageStoreOptions())
const [ current, setCurrent ] = useAtom(selectedDepartAtom, usePageStoreOptions())
useEffect(() => { useEffect(() => {

84
src/pages/system/departments/store.ts

@ -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<IPage>({})
export const defaultDepart = {
id: 0,
parent_id: 0,
name: '',
manager_user_id: 0,
phone: '',
sort: 0,
} as IDepartment
export const batchIdsAtom = atom<number[]>([])
export const selectedDepartAtom = atom<IDepartment>({} 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<IApiResult, IDepartment>((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<IApiResult, number[]>((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()
}
}
})

7
src/pages/system/menus/components/BatchButton.tsx

@ -1,13 +1,14 @@
import { usePageStoreOptions } from '@/store'
import { Button, Popconfirm } from 'antd' import { Button, Popconfirm } from 'antd'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { batchIdsAtom, deleteMenuAtom } from '../store.ts'
import { batchIdsAtom, deleteMenuAtom } from '@/store/menu.ts'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
const BatchButton = () => { const BatchButton = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { isPending, mutate, } = useAtomValue(deleteMenuAtom)
const ids = useAtomValue(batchIdsAtom)
const { isPending, mutate, } = useAtomValue(deleteMenuAtom, usePageStoreOptions())
const ids = useAtomValue(batchIdsAtom, usePageStoreOptions())
if (ids.length === 0) { if (ids.length === 0) {

11
src/pages/system/menus/components/MenuTree.tsx

@ -1,11 +1,12 @@
import { usePageStoreOptions } from '@/store'
import { Empty, Spin, Tree } from 'antd' import { Empty, Spin, Tree } from 'antd'
import { MenuItem } from '@/types' import { MenuItem } from '@/types'
import { useStyle } from '../style.ts' import { useStyle } from '../style.ts'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { useSetAtom } from 'jotai' 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 { FormInstance } from 'antd/lib'
import { useAtomValue } from 'jotai/index'
import { useAtomValue } from 'jotai'
import { TreeNodeRender } from './TreeNodeRender.tsx' import { TreeNodeRender } from './TreeNodeRender.tsx'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { flattenTree } from '@/utils' import { flattenTree } from '@/utils'
@ -16,9 +17,9 @@ const MenuTree = ({ form }: { form: FormInstance }) => {
const { styles } = useStyle() const { styles } = useStyle()
const { t } = useTranslation() const { t } = useTranslation()
const setCurrentMenu = useSetAtom(selectedMenuAtom)
const setIds = useSetAtom(batchIdsAtom)
const { data = [], isLoading } = useAtomValue(menuDataAtom)
const setCurrentMenu = useSetAtom(selectedMenuAtom, usePageStoreOptions())
const setIds = useSetAtom(batchIdsAtom, usePageStoreOptions())
const { data = [], isLoading } = useAtomValue(menuDataAtom, usePageStoreOptions())
const flattenMenusRef = useRef<MenuItem[]>([]) const flattenMenusRef = useRef<MenuItem[]>([])
useDeepCompareEffect(() => { useDeepCompareEffect(() => {

9
src/pages/system/menus/components/TreeNodeRender.tsx

@ -1,11 +1,12 @@
import { usePageStoreOptions } from '@/store'
import { memo } from 'react' import { memo } from 'react'
import { MenuItem } from '@/types' import { MenuItem } from '@/types'
import { Popconfirm, Space, TreeDataNode } from 'antd' import { Popconfirm, Space, TreeDataNode } from 'antd'
import { FormInstance } from 'antd/lib' import { FormInstance } from 'antd/lib'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { useStyle } from '../style.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 { PlusOutlined } from '@ant-design/icons'
import ActionIcon, { DeleteAction } from '@/components/icon/action' import ActionIcon, { DeleteAction } from '@/components/icon/action'
@ -13,9 +14,9 @@ export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataN
const { title } = node const { title } = node
const { t } = useTranslation() const { t } = useTranslation()
const { styles } = useStyle() const { styles } = useStyle()
const { mutate } = useAtomValue(deleteMenuAtom)
const { mutate } = useAtomValue(deleteMenuAtom, usePageStoreOptions())
const setMenuData = useSetAtom(selectedMenuAtom)
const setMenuData = useSetAtom(selectedMenuAtom, usePageStoreOptions())
return ( return (
<div className={styles.treeNode}> <div className={styles.treeNode}>

9
src/pages/system/menus/index.tsx

@ -1,10 +1,11 @@
import Glass from '@/components/glass' import Glass from '@/components/glass'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { usePageStoreOptions } from '@/store'
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { PageContainer, ProCard } from '@ant-design/pro-components' import { PageContainer, ProCard } from '@ant-design/pro-components'
import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert, InputRef, Divider } from 'antd' import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert, InputRef, Divider } from 'antd'
import { useAtom, useAtomValue } from 'jotai' 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 IconPicker from '@/components/icon/picker'
import ButtonTable from './components/ButtonTable.tsx' import ButtonTable from './components/ButtonTable.tsx'
import { Flexbox } from 'react-layout-kit' import { Flexbox } from 'react-layout-kit'
@ -20,9 +21,9 @@ const Menus = () => {
const { styles, cx } = useStyle() const { styles, cx } = useStyle()
const { t } = useTranslation() const { t } = useTranslation()
const [ form ] = Form.useForm() const [ form ] = Form.useForm()
const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom)
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom, usePageStoreOptions())
const { data = [] } = useAtomValue(menuDataAtom, usePageStoreOptions())
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom, usePageStoreOptions()) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined) const menuInputRef = useRef<InputRef | undefined>(undefined)
useEffect(() => { useEffect(() => {

19
src/pages/system/roles/index.tsx

@ -1,4 +1,5 @@
import Switch from '@/components/switch' import Switch from '@/components/switch'
import { usePageStoreOptions } from '@/store'
import { IMenu } from '@/types/menus' import { IMenu } from '@/types/menus'
import { import {
ActionType, ActionType,
@ -18,11 +19,11 @@ import {
rolesAtom, rolesAtom,
saveOrUpdateRoleAtom, saveOrUpdateRoleAtom,
searchAtom searchAtom
} from './store.ts'
} from '@/store/role.ts'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { Button, Form, Space, Spin, Table, Tree, Popconfirm } from 'antd' import { Button, Form, Space, Spin, Table, Tree, Popconfirm } from 'antd'
import { PlusOutlined } from '@ant-design/icons' 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' import { getTreeCheckedStatus } from '@/utils/tree.ts'
const MenuTree = (props: any) => { const MenuTree = (props: any) => {
@ -53,13 +54,13 @@ const Roles = memo(() => {
const { styles } = useStyle() const { styles } = useStyle()
const [ form ] = Form.useForm() const [ form ] = Form.useForm()
const actionRef = useRef<ActionType>() const actionRef = useRef<ActionType>()
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 [ open, setOpen ] = useState(false)
const columns = useMemo(() => { const columns = useMemo(() => {

23
src/routes.tsx

@ -1,9 +1,11 @@
import NotPermission from '@/components/error/403.tsx' import NotPermission from '@/components/error/403.tsx'
import NotFound from '@/components/error/404.tsx' import NotFound from '@/components/error/404.tsx'
import ErrorPage from '@/components/error/error.tsx' import ErrorPage from '@/components/error/error.tsx'
import Loading from '@/components/loading'
import FetchLoading from '@/components/loading/FetchLoading.tsx' import FetchLoading from '@/components/loading/FetchLoading.tsx'
import PageLoading from '@/components/page-loading' 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 EmptyLayout from '@/layout/EmptyLayout.tsx'
// import ListPageLayout from '@/layout/ListPageLayout.tsx' // import ListPageLayout from '@/layout/ListPageLayout.tsx'
// import { Route as DashboardImport } from '@/pages/dashboard' // import { Route as DashboardImport } from '@/pages/dashboard'
@ -25,6 +27,11 @@ import RootLayout from './layout/RootLayout'
import { IRootContext, MenuItem } from './types' import { IRootContext, MenuItem } from './types'
import { DevTools } from 'jotai-devtools' import { DevTools } from 'jotai-devtools'
const PageRootLayout = () => {
return <PageStoreProvider>
<RootLayout/>
</PageStoreProvider>
}
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -38,12 +45,12 @@ export const queryClient = new QueryClient({
const rootRoute = createRootRouteWithContext<IRootContext>()({ const rootRoute = createRootRouteWithContext<IRootContext>()({
component: () => ( component: () => (
<div>
<>
<FetchLoading/> <FetchLoading/>
<Outlet/> <Outlet/>
<DevTools/> <DevTools/>
<TanStackRouterDevtools position={'bottom-right'}/> <TanStackRouterDevtools position={'bottom-right'}/>
</div>
</>
), ),
beforeLoad: ({ location }) => { beforeLoad: ({ location }) => {
if (location.pathname === '/') { if (location.pathname === '/') {
@ -72,13 +79,13 @@ const authRoute = AuthenticatedImport.update({
const layoutNormalRoute = createRoute({ const layoutNormalRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
id: '/_normal_layout', id: '/_normal_layout',
component: RootLayout,
component: PageRootLayout,
}) })
const layoutAuthRoute = createRoute({ const layoutAuthRoute = createRoute({
getParentRoute: () => authRoute, getParentRoute: () => authRoute,
id: '/_auth_layout', id: '/_auth_layout',
component: RootLayout,
component: PageRootLayout,
}) })
const notAuthRoute = createRoute({ const notAuthRoute = createRoute({
@ -96,6 +103,7 @@ const loginRoute = LoginRouteImport.update({
path: '/login', path: '/login',
getParentRoute: () => emptyRoute, getParentRoute: () => emptyRoute,
} as any) } as any)
// //
// const menusRoute = createRoute({ // const menusRoute = createRoute({
// getParentRoute: () => layoutAuthRoute, // getParentRoute: () => layoutAuthRoute,
@ -167,7 +175,7 @@ declare module '@tanstack/react-router' {
} }
} }
export const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
// 递归生成路由,如果有routes则递归生成子路由 // 递归生成路由,如果有routes则递归生成子路由
const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => {
@ -276,7 +284,8 @@ export const RootProvider = memo((props: { context: Partial<IRootContext> }) =>
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: { queryClient, menuData: [] }, context: { queryClient, menuData: [] },
defaultPreload: 'intent'
defaultPreload: 'intent',
defaultPendingComponent: () => <Loading loading={true} delay={300}/>
}) })
return ( return (

106
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<Partial<IDepartment>>({})
const store = createStore()
export const departmentAtom = atomWithQuery<IApiResult<IDepartment[]>, any, IDepartment[]>((get) => ({
queryKey: [ 'departments', get(departmentSearchAtom) ],
queryFn: async ({ queryKey: [ , departSearch ] }) => {
await new Promise(resolve => setTimeout(resolve, 5000))
export const departPageAtom = atom<IPage>({})
return await request('/departments', {
data: departSearch,
})
export const defaultDepart = {
id: 0,
parent_id: 0,
name: '',
manager_user_id: 0,
phone: '',
sort: 0,
} as IDepartment
export const batchIdsAtom = atom<number[]>([])
export const selectedDepartAtom = atom<IDepartment>({} as IDepartment)
export const departTreeAtom = atomWithQuery(() => {
return {
queryKey: [ 'departTree' ],
queryFn: async () => {
return await systemServ.dept.tree()
}, },
select: data => data.data,
}))
select: (res) => {
return res.data.tree ?? []
}
}
})
export const departmentDetailAtom = atomWithQuery<IApiResult<IDepartment>>((get) => ({
queryKey: [ 'department', get(departmentSearchAtom) ],
queryFn: async ({ queryKey: [ , departSearch ] }) => {
return await request(`/departments/${(departSearch as IDepartment).id}`)
export const saveOrUpdateDepartAtom = atomWithMutation<IApiResult, IDepartment>((get) => {
return {
mutationKey: [ 'saveOrUpdateDepart' ],
mutationFn: async (data: IDepartment) => {
if (data.id) {
return await systemServ.dept.update(data)
}
return await systemServ.dept.add(data)
}, },
select: data => 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
})) }))
}
//add use atomWithMutation
export const departmentAddAtom = (client: QueryClient) => atomWithMutation<IApiResult<IDepartment>, any, IDepartment>(() => ({
mutationKey: [ 'addDepartment', ],
mutationFn: async (depart: IDepartment) => {
return await request.post('/departments', depart)
},
onSuccess: (result) => {
console.log(result)
get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree' ] }).then()
}
}
})
export const deleteDepartAtom = atomWithMutation<IApiResult, number[]>((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()
} }
}))
}
})

36
src/store/index.ts

@ -0,0 +1,36 @@
import { createStore } from 'jotai'
import React, { useContext, createContext } from 'react'
export type Store = ReturnType<typeof createStore>
export const InternalPageContext = createContext<Store | undefined>(
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<Store>()
if (!internalStoreRef.current) {
internalStoreRef.current = pageStore
}
return React.createElement(InternalPageContext.Provider, {
value: internalStoreRef.current
}, children)
}

7
src/pages/system/menus/store.ts → src/store/menu.ts

@ -63,13 +63,6 @@ export const saveOrUpdateMenuAtom = atomWithMutation<IApiResult, IMenu>((get) =>
const isAdd = !!res.data?.id const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix // @ts-ignore fix

0
src/pages/system/roles/store.ts → src/store/role.ts

5
vite.config.ts

@ -2,6 +2,9 @@ import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock' 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' //import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
@ -10,6 +13,7 @@ export default defineConfig(({ mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件 // 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。 // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
// @ts-ignore fix process
const env = loadEnv(mode, process.cwd(), '') const env = loadEnv(mode, process.cwd(), '')
return { return {
//定义别名的路径 //定义别名的路径
@ -31,6 +35,7 @@ export default defineConfig(({ mode }) => {
react({ react({
babel: { babel: {
presets: [ 'jotai/babel/preset' ], presets: [ 'jotai/babel/preset' ],
plugins: [ jotaiDebugLabel, jotaiReactRefresh ]
}, },
}), }),
viteMockServe({ viteMockServe({

Loading…
Cancel
Save