Browse Source

完善部门页面

main
dark 5 months ago
parent
commit
bce3c5a86a
  1. 80
      src/pages/system/departments/components/DepartmentTree.tsx
  2. 59
      src/pages/system/departments/components/TreeNodeRender.tsx
  3. 152
      src/pages/system/departments/index.tsx
  4. 89
      src/pages/system/departments/store.ts
  5. 72
      src/pages/system/departments/style.ts
  6. 2
      src/pages/system/menus/components/MenuTree.tsx
  7. 15
      src/pages/system/menus/index.tsx
  8. 36
      src/pages/system/menus/style.ts
  9. 5
      src/pages/system/roles/index.tsx
  10. 4
      src/pages/system/users/index.tsx
  11. 6
      src/service/system.ts
  12. 2
      src/types/department.d.ts

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

@ -0,0 +1,80 @@
import { Empty, Spin, Tree } from 'antd'
import { useStyle } from '../style.ts'
import { useTranslation } from '@/i18n.ts'
import { useSetAtom } from 'jotai'
import { useDepartStore } from '../store.ts'
import { FormInstance } from 'antd/lib'
import { useAtomValue } from 'jotai/index'
import { TreeNodeRender } from './TreeNodeRender.tsx'
import { useRef } from 'react'
import { flattenTree } from '@/utils'
import { useDeepCompareEffect } from 'react-use'
import { IDepartment } from '@/types/department'
export const DepartmentTree = ({ form }: { form: FormInstance }) => {
const { selectedDepartAtom, departTreeAtom, batchIdsAtom } = useDepartStore()
const { styles } = useStyle()
const { t } = useTranslation()
const setIds = useSetAtom(batchIdsAtom)
const setCurrent = useSetAtom(selectedDepartAtom)
const { data = [], isLoading } = useAtomValue(departTreeAtom)
const flattenMenusRef = useRef<IDepartment[]>([])
useDeepCompareEffect(() => {
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)
// console.log(flattenMenusRef.current)
}
return () => {
setCurrent({} as IDepartment)
}
}, [ data, isLoading ])
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>
</>
)
}
export default DepartmentTree

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

@ -0,0 +1,59 @@
import { memo } from 'react'
import { MenuItem } from '@/types'
import { Popconfirm, Space, TreeDataNode } from 'antd'
import { FormInstance } from 'antd/lib'
import { useTranslation } from '@/i18n.ts'
import { useStyle } from '../style.ts'
import { useAtomValue, useSetAtom } from 'jotai/index'
import { useDepartStore } from '../store.ts'
import { PlusOutlined } from '@ant-design/icons'
import ActionIcon, { DeleteAction } from '@/components/icon/action'
export const TreeNodeRender = memo(({ node, form }: { node: MenuItem & TreeDataNode, form: FormInstance }) => {
const { name } = node
const { t } = useTranslation()
const { styles } = useStyle()
const { selectedDepartAtom, deleteDepartAtom, defaultDepart } = useDepartStore()
const { mutate } = useAtomValue(deleteDepartAtom)
const setCurrent = useSetAtom(selectedDepartAtom)
return (
<div className={styles.treeNode}>
<span>{name as any}</span>
<span className={'actions'}>
<Space size={'middle'}>
<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)
}}/>
<Popconfirm
title={t('message.deleteConfirm', '确定要删除吗?')}
onConfirm={() => {
mutate([ (node as any).id ])
}}
>
<DeleteAction
size={12}
onClick={(e) => {
e.stopPropagation()
e.stopPropagation()
}}/>
</Popconfirm>
</Space>
</span>
</div>
)
})

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

@ -1,16 +1,156 @@
import { PageContainer } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
import { PageContainer, ProCard } from '@ant-design/pro-components'
import { Flexbox } from 'react-layout-kit'
import { DraggablePanel } from '@/components/draggable-panel'
import { useTranslation } from '@/i18n.ts'
import { useStyle } from './style.ts'
import DepartmentTree from './components/DepartmentTree.tsx'
import { Alert, Button, Divider, Form, Input, InputNumber, InputRef, notification, TreeSelect } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { useDepartStore } from './store.ts'
import { useAtom, useAtomValue, } from 'jotai'
import Glass from '@/components/glass'
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 { defaultDepart, selectedDepartAtom, departTreeAtom, saveOrUpdateDepartAtom } = useDepartStore()
const { data } = useAtomValue(departTreeAtom)
const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateDepartAtom)
const [ current, setCurrent ] = useAtom(selectedDepartAtom)
useEffect(() => {
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 ])
return ( return (
<PageContainer breadcrumbRender={false}>
<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>
<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>
</ProCard>
</Glass>
</Flexbox>
</Flexbox>
</PageContainer> </PageContainer>
) )
} }
export const Route = createLazyFileRoute("/system/departments")({
component: Departments
})
export default Departments export default Departments

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

@ -0,0 +1,89 @@
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()
const departPageAtom = atom<IPage>({})
const defaultDepart = {
id: 0,
parent_id: 0,
name: '',
manager_user_id: 0,
phone: '',
sort: 0,
} as IDepartment
const batchIdsAtom = atom<number[]>([])
const selectedDepartAtom = atom<IDepartment>({} as IDepartment)
const departTreeAtom = atomWithQuery(() => {
return {
queryKey: [ 'departTree' ],
queryFn: async () => {
return await systemServ.dept.tree()
},
select: (res) => {
return res.data.tree ?? []
}
}
})
const saveOrUpdateDepartAtom = atomWithMutation<IApiResult, IDepartment>((get) => {
return {
mutationKey: [ 'saveOrUpdateDepart', get(selectedDepartAtom) ],
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', '保存成功'))
store.set(selectedDepartAtom, prev => ({
...prev,
...(isAdd ? res.data : {})
}))
get(queryClientAtom).invalidateQueries({ queryKey: [ 'departTree', get(departPageAtom) ] }).then()
}
}
})
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', get(departPageAtom) ] }).then()
}
}
})
export const useDepartStore = () => ({
defaultDepart,
departPageAtom,
selectedDepartAtom,
departTreeAtom,
deleteDepartAtom,
batchIdsAtom,
saveOrUpdateDepartAtom,
})

72
src/pages/system/departments/style.ts

@ -0,0 +1,72 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
const prefix = `${prefixCls}-${token?.proPrefix}-department-page`
const tree = css`
.ant-tree {
overflow: auto;
height: 100%;
border-right: 1px solid ${token.colorBorder};
background: ${token.colorBgContainer};
}
.ant-tree-directory .ant-tree-treenode-selected::before {
background: ${token.colorBgTextHover};
}
.ant-tree-treenode:before {
border-radius: ${token.borderRadius}px;
}
`
const treeNode = css`
display: flex;
justify-content: space-between;
align-items: center;
.actions {
display: none;
padding: 0 10px;
}
&:hover .actions { {
display: flex;
}
`
const treeActions = css`
padding: 0 24px 16px;
display: flex;
flex-direction: column;
position: sticky;
bottom: 0;
z-index: 10;
background: ${token.colorBgContainer};
`
const box = css`
flex: 1;
background: ${token.colorBgContainer};
`
const form = css`
//display: flex;
//flex-wrap: wrap;
min-width: 300px;
max-width: 800px;
`
const emptyForm = css`
`
return {
container: cx(prefix),
box,
form,
emptyForm,
tree,
treeNode,
treeActions
}
})

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

@ -6,7 +6,7 @@ import { useSetAtom } from 'jotai'
import { batchIdsAtom, menuDataAtom, selectedMenuAtom } from '../store.ts' import { batchIdsAtom, menuDataAtom, selectedMenuAtom } from '../store.ts'
import { FormInstance } from 'antd/lib' import { FormInstance } from 'antd/lib'
import { useAtomValue } from 'jotai/index' import { useAtomValue } from 'jotai/index'
import { TreeNodeRender } from '../components/TreeNodeRender.tsx'
import { TreeNodeRender } from './TreeNodeRender.tsx'
import { useRef } from 'react' import { useRef } from 'react'
import { flattenTree } from '@/utils' import { flattenTree } from '@/utils'
import { useDeepCompareEffect } from 'react-use' import { useDeepCompareEffect } from 'react-use'

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

@ -14,15 +14,13 @@ import { MenuItem } from '@/types'
import MenuTree from './components/MenuTree.tsx' import MenuTree from './components/MenuTree.tsx'
import BatchButton from '@/pages/system/menus/components/BatchButton.tsx' import BatchButton from '@/pages/system/menus/components/BatchButton.tsx'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { createLazyFileRoute } from '@tanstack/react-router'
const Menus = () => { 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, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom) const { data = [] } = useAtomValue(menuDataAtom)
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {} const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined) const menuInputRef = useRef<InputRef | undefined>(undefined)
@ -35,7 +33,7 @@ const Menus = () => {
description: (error as any).message ?? t('message.saveFail', '保存失败'), description: (error as any).message ?? t('message.saveFail', '保存失败'),
}) })
} }
}, [ isError, isSuccess ])
}, [ isError ])
useEffect(() => { useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) { if (currentMenu.id === 0 && menuInputRef.current) {
@ -71,7 +69,7 @@ const Menus = () => {
onClick={() => { onClick={() => {
const menu = { const menu = {
...defaultMenu, ...defaultMenu,
parent_id: 0,
parent_id: currentMenu.id ?? 0,
} }
setMenuData(menu) setMenuData(menu)
form.setFieldsValue(menu) form.setFieldsValue(menu)
@ -97,7 +95,7 @@ const Menus = () => {
labelWrap labelWrap
wrapperCol={{ flex: 1 }} wrapperCol={{ flex: 1 }}
colon={false} colon={false}
className={cx(styles.form, styles.emptyForm, {
className={cx(styles.form, {
[styles.emptyForm]: currentMenu.id === undefined [styles.emptyForm]: currentMenu.id === undefined
})} })}
> >
@ -209,9 +207,4 @@ const Menus = () => {
) )
} }
export const Route = createLazyFileRoute('/system/menus')({
component: Menus
})
export default Menus export default Menus

36
src/pages/system/menus/style.ts

@ -3,24 +3,6 @@ import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
const prefix = `${prefixCls}-${token?.proPrefix}-menu-page` const prefix = `${prefixCls}-${token?.proPrefix}-menu-page`
const tree = css`
.ant-tree {
overflow: auto;
height: 100%;
border-right: 1px solid ${token.colorBorder};
background: ${token.colorBgContainer};
}
.ant-tree-directory .ant-tree-treenode-selected::before {
background: ${token.colorBgTextHover};
}
.ant-tree-treenode:before {
border-radius: ${token.borderRadius}px;
}
`
const box = css` const box = css`
flex: 1; flex: 1;
background: ${token.colorBgContainer}; background: ${token.colorBgContainer};
@ -44,6 +26,24 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
` `
const tree = css`
.ant-tree {
overflow: auto;
height: 100%;
border-right: 1px solid ${token.colorBorder};
background: ${token.colorBgContainer};
}
.ant-tree-directory .ant-tree-treenode-selected::before {
background: ${token.colorBgTextHover};
}
.ant-tree-treenode:before {
border-radius: ${token.borderRadius}px;
}
`
const treeNode = css` const treeNode = css`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

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

@ -7,7 +7,6 @@ import {
ProTable, ProTable,
BetaSchemaForm, ProFormColumnsType, BetaSchemaForm, ProFormColumnsType,
} from '@ant-design/pro-components' } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
import { useStyle } from './style.ts' import { useStyle } from './style.ts'
import { memo, useEffect, useMemo, useRef, useState } from 'react' import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
@ -241,8 +240,4 @@ const Roles = memo(() => {
) )
}) })
export const Route = createLazyFileRoute('/system/roles')({
component: Roles
})
export default Roles export default Roles

4
src/pages/system/users/index.tsx

@ -1,5 +1,4 @@
import { PageContainer } from '@ant-design/pro-components' import { PageContainer } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
const Users = () => { const Users = () => {
return ( return (
@ -9,8 +8,5 @@ const Users = () => {
) )
} }
export const Route = createLazyFileRoute("/system/users")({
component: Users
})
export default Users export default Users

6
src/service/system.ts

@ -5,10 +5,14 @@ import { LoginRequest, LoginResponse } from '@/types/login'
import { createCURD } from '@/service/base.ts' import { createCURD } from '@/service/base.ts'
import { IMenu } from '@/types/menus' import { IMenu } from '@/types/menus'
import { IRole } from '@/types/roles' import { IRole } from '@/types/roles'
import { IDepartment } from '@/types/department'
const systemServ = { const systemServ = {
dept: { dept: {
...createCURD('/sys/dept')
...createCURD<any, IDepartment>('/sys/dept'),
tree: () => {
return request.get<{ tree: IDepartment }>('/sys/dept/tree')
}
}, },
menus: { menus: {
...createCURD<any, IMenu>('/sys/menu') ...createCURD<any, IMenu>('/sys/menu')

2
src/types/department.d.ts

@ -8,6 +8,8 @@ export interface IDepartment {
sort: number, sort: number,
status: string, status: string,
remark: string remark: string
children?: IDepartment[]
} }
export interface DepartmentRequest extends IDepartment { export interface DepartmentRequest extends IDepartment {

Loading…
Cancel
Save