李金
7 months ago
29 changed files with 1022 additions and 1033 deletions
-
1package.json
-
4src/_authenticatedRoute.tsx
-
38src/components/loading/index.tsx
-
4src/components/loading/style.ts
-
22src/components/user-picker/Item.tsx
-
61src/components/user-picker/UserPicker.tsx
-
1src/components/user-picker/index.ts
-
5src/components/user-picker/store.ts
-
25src/components/user-picker/style.ts
-
15src/pages/list/list.tsx
-
146src/pages/list/tree.tsx
-
11src/pages/system/departments/components/DepartmentTree.tsx
-
9src/pages/system/departments/components/TreeNodeRender.tsx
-
9src/pages/system/departments/index.tsx
-
84src/pages/system/departments/store.ts
-
7src/pages/system/menus/components/BatchButton.tsx
-
11src/pages/system/menus/components/MenuTree.tsx
-
9src/pages/system/menus/components/TreeNodeRender.tsx
-
9src/pages/system/menus/index.tsx
-
19src/pages/system/roles/index.tsx
-
23src/routes.tsx
-
110src/store/department.ts
-
36src/store/index.ts
-
7src/store/menu.ts
-
0src/store/role.ts
-
7vite.config.ts
@ -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({ |
@ -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> |
||||
|
) |
||||
|
|
||||
|
} |
@ -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 |
@ -0,0 +1 @@ |
|||||
|
export * from './UserPicker.tsx' |
@ -0,0 +1,5 @@ |
|||||
|
import { IUser } from '@/types/user' |
||||
|
import { atom } from 'jotai' |
||||
|
|
||||
|
export const userSelectedAtom = atom<IUser[]>([]) |
||||
|
|
@ -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, |
||||
|
} |
||||
|
}) |
@ -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 |
|
@ -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 |
|
@ -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() |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
}) |
|
||||
|
|
@ -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() |
||||
|
|
||||
|
} |
||||
} |
} |
||||
})) |
|
||||
|
|
||||
|
}) |
||||
|
|
@ -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) |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue