Browse Source

update

main
李金 7 months ago
parent
commit
3e45be5097
  1. 1
      package.json
  2. 4
      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. 115
      src/pages/system/departments/components/DepartmentTree.tsx
  13. 73
      src/pages/system/departments/components/TreeNodeRender.tsx
  14. 255
      src/pages/system/departments/index.tsx
  15. 84
      src/pages/system/departments/store.ts
  16. 45
      src/pages/system/menus/components/BatchButton.tsx
  17. 121
      src/pages/system/menus/components/MenuTree.tsx
  18. 73
      src/pages/system/menus/components/TreeNodeRender.tsx
  19. 11
      src/pages/system/menus/index.tsx
  20. 19
      src/pages/system/roles/index.tsx
  21. 393
      src/routes.tsx
  22. 110
      src/store/department.ts
  23. 36
      src/store/index.ts
  24. 7
      src/store/menu.ts
  25. 0
      src/store/role.ts
  26. 7
      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": {

4
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

115
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'
@ -13,69 +14,69 @@ import { IDepartment } from '@/types/department'
export const DepartmentTree = ({ form }: { form: FormInstance }) => { 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<IDepartment[]>([])
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<IDepartment[]>([])
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<IDepartment[]>(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<IDepartment[]>(data as any)
}
useEffect(() => {
}, [ data, isLoading ])
return () => {
setCurrent({} as IDepartment)
}
}, [])
useEffect(() => {
const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/>
return () => {
setCurrent({} as IDepartment)
} }
}, [])
const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/>
}
return (<>
<Spin spinning={isLoading} style={{ minHeight: 200 }}>
{
renderEmpty()
}
<Tree.DirectoryTree
className={styles.tree}
treeData={data as any}
defaultExpandAll={true}
// draggable={true}
titleRender={(node) => {
return (<TreeNodeRender node={node as any} form={form}/>)
}}
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}
/>
</Spin>
</>
)
return (<>
<Spin spinning={isLoading} style={{ minHeight: 200 }}>
{
renderEmpty()
}
<Tree.DirectoryTree
className={styles.tree}
treeData={data as any}
defaultExpandAll={true}
// draggable={true}
titleRender={(node) => {
return (<TreeNodeRender node={node as any} form={form}/>)
}}
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}
/>
</Spin>
</>
)
} }
export default DepartmentTree export default DepartmentTree

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

@ -1,58 +1,59 @@
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'
export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => { 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 (
<div className={styles.treeNode}>
<span>{name as any}</span>
<span className={'actions'}>
return (
<div className={styles.treeNode}>
<span>{name as any}</span>
<span className={'actions'}>
<Space size={'middle'}> <Space size={'middle'}>
<ActionIcon <ActionIcon
size={12}
icon={<PlusOutlined/>}
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={<PlusOutlined/>}
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)
}}/>
}}/>
<Popconfirm <Popconfirm
title={t('message.deleteConfirm', '确定要删除吗?')}
onConfirm={() => {
mutate([ (node as any).id ])
}}
title={t('message.deleteConfirm', '确定要删除吗?')}
onConfirm={() => {
mutate([ (node as any).id ])
}}
> >
<DeleteAction <DeleteAction
size={12}
onClick={(e) => {
e.stopPropagation()
e.stopPropagation()
}}/>
size={12}
onClick={(e) => {
e.stopPropagation()
e.stopPropagation()
}}/>
</Popconfirm> </Popconfirm>
</Space> </Space>
</span> </span>
</div>
)
</div>
)
}) })

255
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,149 +7,149 @@ 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'
const Departments = () => { const Departments = () => {
const { t } = useTranslation()
const { styles, cx } = useStyle()
const [ form ] = Form.useForm()
const inputRef = useRef<InputRef>()
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<InputRef>()
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 (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<Flexbox horizontal>
<DraggablePanel expandable={false}
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
style={{ position: 'relative' }}
>
<ProCard title={t('system.departments.title', '部门')}>
<DepartmentTree form={form}/>
</ProCard>
<div className={styles.treeActions}>
<Divider style={{ flex: 1, margin: '8px 0' }}/>
<Button style={{ flex: 1 }} size={'small'}
block={true} type={'dashed'}
icon={<PlusOutlined/>}
onClick={() => {
const data = {
...defaultDepart,
parent_id: current.id ?? 0,
}
setCurrent(data)
form.setFieldsValue(data)
}}
>{t('actions.news')}</Button>
</div>
</DraggablePanel>
<Flexbox className={styles.box}>
<Glass
enabled={current.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.departments.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
>
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<Flexbox horizontal>
<DraggablePanel expandable={false}
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
style={{ position: 'relative' }}
>
<ProCard title={t('system.departments.title', '部门')}>
<DepartmentTree form={form}/>
</ProCard>
<div className={styles.treeActions}>
<Divider style={{ flex: 1, margin: '8px 0' }}/>
<Button style={{ flex: 1 }} size={'small'}
block={true} type={'dashed'}
icon={<PlusOutlined/>}
onClick={() => {
const data = {
...defaultDepart,
parent_id: current.id ?? 0,
}
setCurrent(data)
form.setFieldsValue(data)
}}
>{t('actions.news')}</Button>
</div>
</DraggablePanel>
<Flexbox className={styles.box}>
<Glass
enabled={current.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.departments.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
>
<ProCard title={t('system.departments.setting', '编辑')}>
<Form form={form}
initialValues={current!}
labelAlign="left"
labelWrap
labelCol={{ span: 4 }}
wrapperCol={{ span: 12 }}
colon={false}
className={cx(styles.form, {
[styles.emptyForm]: current.id === undefined
})}
>
<Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/>
</Form.Item>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.departments.form.name', '部门名称')} name={'name'}>
<Input ref={inputRef as any}
placeholder={t('system.departments.form.name', '部门名称')}/>
</Form.Item>
<Form.Item label={t('system.departments.form.parent', '上级部门')}
name={'parent_id'}>
<TreeSelect
treeData={[
{ id: 0, name: '顶级', children: data as any },
]}
treeDefaultExpandAll={true}
fieldNames={{
label: 'name',
value: 'id'
}}/>
</Form.Item>
<ProCard title={t('system.departments.setting', '编辑')}>
<Form form={form}
initialValues={current!}
labelAlign="left"
labelWrap
labelCol={{ span: 4 }}
wrapperCol={{ span: 12 }}
colon={false}
className={cx(styles.form, {
[styles.emptyForm]: current.id === undefined
})}
>
<Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/>
</Form.Item>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.departments.form.name', '部门名称')} name={'name'}>
<Input ref={inputRef as any}
placeholder={t('system.departments.form.name', '部门名称')}/>
</Form.Item>
<Form.Item label={t('system.departments.form.parent', '上级部门')}
name={'parent_id'}>
<TreeSelect
treeData={[
{ id: 0, name: '顶级', children: data as any },
]}
treeDefaultExpandAll={true}
fieldNames={{
label: 'name',
value: 'id'
}}/>
</Form.Item>
<Form.Item label={t('system.departments.form.sort', '排序')} name={'sort'}>
<InputNumber/>
</Form.Item>
<Form.Item label={t('system.departments.form.manager_user_id', '负责人')}
name={'path'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.departments.form.sort', '排序')} name={'sort'}>
<InputNumber/>
</Form.Item>
<Form.Item label={t('system.departments.form.manager_user_id', '负责人')}
name={'path'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.departments.form.phone', '联系电话')}
name={'phone'}
>
<Input/>
</Form.Item>
<Form.Item label={' '}>
<Button type="primary"
htmlType={'submit'}
loading={isPending}
onClick={() => {
form.validateFields().then((values) => {
mutate(values)
})
}}
>
{t('system.departments.form.save', '保存')}
</Button>
</Form.Item>
<Form.Item label={t('system.departments.form.phone', '联系电话')}
name={'phone'}
>
<Input/>
</Form.Item>
<Form.Item label={' '}>
<Button type="primary"
htmlType={'submit'}
loading={isPending}
onClick={() => {
form.validateFields().then((values) => {
mutate(values)
})
}}
>
{t('system.departments.form.save', '保存')}
</Button>
</Form.Item>
</Form>
</ProCard>
</Form>
</ProCard>
</Glass>
</Flexbox>
</Flexbox>
</PageContainer>
)
</Glass>
</Flexbox>
</Flexbox>
</PageContainer>
)
} }

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()
}
}
})

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

@ -1,34 +1,35 @@
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 { 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 (
<Popconfirm
onConfirm={() => {
mutate(ids as number[])
}}
title={t('message.batchDelete', '确定要删除所选数据吗?')}>
<Button
type="primary"
danger={true}
size={'small'}
disabled={ids.length === 0}
loading={isPending}
>{t('actions.batchDel', '批量删除')}</Button>
</Popconfirm>
)
return (
<Popconfirm
onConfirm={() => {
mutate(ids as number[])
}}
title={t('message.batchDelete', '确定要删除所选数据吗?')}>
<Button
type="primary"
danger={true}
size={'small'}
disabled={ids.length === 0}
loading={isPending}
>{t('actions.batchDel', '批量删除')}</Button>
</Popconfirm>
)
} }
export default BatchButton export default BatchButton

121
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'
@ -14,71 +15,71 @@ import { useDeepCompareEffect } from 'react-use'
const MenuTree = ({ form }: { form: FormInstance }) => { 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<MenuItem[]>([])
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<MenuItem[]>([])
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<MenuItem[]>(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<MenuItem[]>(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 <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/>
useEffect(() => {
return () => {
setCurrentMenu({} as MenuItem)
setIds([])
} }
}, [])
const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/>
}
return (
<>
<Spin spinning={isLoading} style={{ minHeight: 200 }}>
{
renderEmpty()
}
<Tree.DirectoryTree
className={styles.tree}
treeData={data as any}
defaultExpandAll={true}
// draggable={true}
titleRender={(node) => {
return (<TreeNodeRender node={node as any} form={form}/>)
}}
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}
/>
</Spin>
</>
)
return (
<>
<Spin spinning={isLoading} style={{ minHeight: 200 }}>
{
renderEmpty()
}
<Tree.DirectoryTree
className={styles.tree}
treeData={data as any}
defaultExpandAll={true}
// draggable={true}
titleRender={(node) => {
return (<TreeNodeRender node={node as any} form={form}/>)
}}
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}
/>
</Spin>
</>
)
} }
export default MenuTree export default MenuTree

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

@ -1,58 +1,59 @@
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'
export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => { 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 (
<div className={styles.treeNode}>
<span>{title as any}</span>
<span className={'actions'}>
return (
<div className={styles.treeNode}>
<span>{title as any}</span>
<span className={'actions'}>
<Space size={'middle'}> <Space size={'middle'}>
<ActionIcon <ActionIcon
size={12}
icon={<PlusOutlined/>}
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={<PlusOutlined/>}
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)
}}/>
}}/>
<Popconfirm <Popconfirm
title={t('message.deleteConfirm', '确定要删除吗?')}
onConfirm={() => {
mutate([ (node as any).id ])
}}
title={t('message.deleteConfirm', '确定要删除吗?')}
onConfirm={() => {
mutate([ (node as any).id ])
}}
> >
<DeleteAction <DeleteAction
size={12}
onClick={(e) => {
e.stopPropagation()
e.stopPropagation()
}}/>
size={12}
onClick={(e) => {
e.stopPropagation()
e.stopPropagation()
}}/>
</Popconfirm> </Popconfirm>
</Space> </Space>
</span> </span>
</div>
)
</div>
)
}) })

11
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(() => {
@ -33,7 +34,7 @@ const Menus = () => {
description: (error as any).message ?? t('message.saveFail', '保存失败'), description: (error as any).message ?? t('message.saveFail', '保存失败'),
}) })
} }
}, [ isError ])
}, [ isError ])
useEffect(() => { useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) { if (currentMenu.id === 0 && menuInputRef.current) {

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

393
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'
@ -11,13 +13,13 @@ import { Route as LoginRouteImport } from '@/pages/login'
import { generateUUID } from '@/utils/uuid.ts' import { generateUUID } from '@/utils/uuid.ts'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { import {
AnyRoute,
createRootRouteWithContext,
createRoute,
createRouter, lazyRouteComponent,
Outlet,
redirect,
RouterProvider,
AnyRoute,
createRootRouteWithContext,
createRoute,
createRouter, lazyRouteComponent,
Outlet,
redirect,
RouterProvider,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo } from 'react' import { memo } from 'react'
@ -25,66 +27,71 @@ 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: {
queries: {
retry: false,
}
defaultOptions: {
queries: {
retry: false,
} }
}
}) })
const rootRoute = createRootRouteWithContext<IRootContext>()({ const rootRoute = createRootRouteWithContext<IRootContext>()({
component: () => (
<div>
<FetchLoading/>
<Outlet/>
<DevTools/>
<TanStackRouterDevtools position={'bottom-right'}/>
</div>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/dashboard' })
}
},
loader: () => {
component: () => (
<>
<FetchLoading/>
<Outlet/>
<DevTools/>
<TanStackRouterDevtools position={'bottom-right'}/>
</>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/dashboard' })
}
},
loader: () => {
},
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
},
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
}) })
const emptyRoute = createRoute({ const emptyRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_empty',
component: EmptyLayout,
getParentRoute: () => rootRoute,
id: '/_empty',
component: EmptyLayout,
}) })
const authRoute = AuthenticatedImport.update({ const authRoute = AuthenticatedImport.update({
getParentRoute: () => rootRoute,
id: '/_authenticated',
getParentRoute: () => rootRoute,
id: '/_authenticated',
} as any) } as any)
const layoutNormalRoute = createRoute({ const layoutNormalRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_normal_layout',
component: RootLayout,
getParentRoute: () => rootRoute,
id: '/_normal_layout',
component: PageRootLayout,
}) })
const layoutAuthRoute = createRoute({ const layoutAuthRoute = createRoute({
getParentRoute: () => authRoute,
id: '/_auth_layout',
component: RootLayout,
getParentRoute: () => authRoute,
id: '/_auth_layout',
component: PageRootLayout,
}) })
const notAuthRoute = createRoute({ const notAuthRoute = createRoute({
getParentRoute: () => layoutNormalRoute,
path: '/not-auth',
component: NotPermission
getParentRoute: () => layoutNormalRoute,
path: '/not-auth',
component: NotPermission
}) })
// const dashboardRoute = DashboardImport.update({ // const dashboardRoute = DashboardImport.update({
@ -93,9 +100,10 @@ const notAuthRoute = createRoute({
// } as any) // } as any)
const loginRoute = LoginRouteImport.update({ const loginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => emptyRoute,
path: '/login',
getParentRoute: () => emptyRoute,
} as any) } as any)
// //
// const menusRoute = createRoute({ // const menusRoute = createRoute({
// getParentRoute: () => layoutAuthRoute, // getParentRoute: () => layoutAuthRoute,
@ -119,169 +127,170 @@ const loginRoute = LoginRouteImport.update({
declare module '@tanstack/react-router' { 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( 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<IRootContext> }) => { export const RootProvider = memo((props: { context: Partial<IRootContext> }) => {
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: () => <Loading loading={true} delay={300}/>
})
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ ...props.context, queryClient }}/>
</QueryClientProvider>
)
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ ...props.context, queryClient }}/>
</QueryClientProvider>
)
}) })

110
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,
})
},
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<number[]>([])
export const selectedDepartAtom = atom<IDepartment>({} as IDepartment)
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 departTreeAtom = atomWithQuery(() => {
return {
queryKey: [ 'departTree' ],
queryFn: async () => {
return await systemServ.dept.tree()
}, },
select: data => data,
}))
//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)
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: (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<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

7
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 {
//定义别名的路径 //定义别名的路径
@ -30,7 +34,8 @@ export default defineConfig(({ mode }) => {
plugins: [ plugins: [
react({ react({
babel: { babel: {
presets: ['jotai/babel/preset'],
presets: [ 'jotai/babel/preset' ],
plugins: [ jotaiDebugLabel, jotaiReactRefresh ]
}, },
}), }),
viteMockServe({ viteMockServe({

Loading…
Cancel
Save