Browse Source

1、完善菜单,角色模块

2、修正type类型提示
main
李金 5 months ago
parent
commit
0f855a931f
  1. 1
      package.json
  2. 12
      src/components/switch/index.tsx
  3. 5
      src/locales/lang/zh-CN.ts
  4. 54
      src/pages/system/menus/index.tsx
  5. 112
      src/pages/system/menus/store.ts
  6. 10
      src/pages/system/menus/style.ts
  7. 413
      src/pages/system/roles/index.tsx
  8. 90
      src/pages/system/roles/store.ts
  9. 177
      src/request.ts
  10. 8
      src/service/base.ts
  11. 35
      src/types/roles.d.ts
  12. 158
      src/utils/index.ts
  13. 8
      src/vite-env.d.ts
  14. 47
      vite.config.ts

1
package.json

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite --host --port 3000",
"dev:proxy": "vite --mode=proxy --host --port 3000",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"

12
src/components/switch/index.tsx

@ -0,0 +1,12 @@
import { convertToBool } from '@/utils'
import { Switch as AntSwitch, SwitchProps } from 'antd'
export const Switch = ({ value, ...props }: SwitchProps) => {
console.log(value, props)
return (
<AntSwitch {...props} value={convertToBool(value)}/>
)
}
export default Switch

5
src/locales/lang/zh-CN.ts

@ -45,7 +45,7 @@ export default {
roles
},
actions: {
news: '新增',
news: '新增',
add: '添加',
edit: '编辑',
cancel: '取消',
@ -72,6 +72,9 @@ export default {
emptyDataAdd: '暂无数据,点击添加',
required: '此项为必填项',
},
rules: {
required: '此项为必填项',
},
tabs: {
refresh: '刷新',
maximize: '最大化',

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

@ -1,9 +1,10 @@
import Glass from '@/components/glass'
import { useTranslation } from '@/i18n.ts'
import { PlusOutlined } from '@ant-design/icons'
import { PageContainer, ProCard } from '@ant-design/pro-components'
import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert } from 'antd'
import { useAtomValue } from 'jotai'
import { menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts'
import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert, InputRef, Divider } from 'antd'
import { useAtom, useAtomValue } from 'jotai'
import { defaultMenu, menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts'
import IconPicker from '@/components/icon/picker'
import ButtonTable from './components/ButtonTable.tsx'
import { Flexbox } from 'react-layout-kit'
@ -12,7 +13,7 @@ import { useStyle } from './style.ts'
import { MenuItem } from '@/types'
import MenuTree from './components/MenuTree.tsx'
import BatchButton from '@/pages/system/menus/components/BatchButton.tsx'
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { createLazyFileRoute } from '@tanstack/react-router'
@ -23,7 +24,8 @@ const Menus = () => {
const [ form ] = Form.useForm()
const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom)
const currentMenu = useAtomValue<MenuItem>(selectedMenuAtom) ?? {}
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined)
useEffect(() => {
@ -35,6 +37,12 @@ const Menus = () => {
}
}, [ isError, isSuccess ])
useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) {
menuInputRef.current.focus()
}
}, [ currentMenu ])
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
@ -43,15 +51,33 @@ const Menus = () => {
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
style={{ position: 'relative' }}
>
<ProCard title={t('system.menus.title', '菜单')}
extra={
<BatchButton/>
<>
<BatchButton/>
</>
}
>
<MenuTree 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 menu = {
...defaultMenu,
parent_id: 0,
}
setMenuData(menu)
form.setFieldsValue(menu)
}}
>{t('actions.news')}</Button>
</div>
</DraggablePanel>
<Flexbox className={styles.box}>
<Glass
@ -83,8 +109,12 @@ const Menus = () => {
<Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/>
</Form.Item>
<Form.Item label={t('system.menus.form.title', '菜单名称')} name={'title'}>
<Input/>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.menus.form.title', '菜单名称')} name={'title'}>
<Input ref={menuInputRef as any} placeholder={t('system.menus.form.title', '菜单名称')}/>
</Form.Item>
<Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}>
<TreeSelect
@ -121,8 +151,12 @@ const Menus = () => {
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t('system.menus.form.name', '别名')} name={'name'}>
<Input/>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.menus.form.name', '别名')} name={'name'}>
<Input placeholder={t('system.menus.form.name', '别名')}/>
</Form.Item>
<Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}>
<IconPicker placement={'left'}/>

112
src/pages/system/menus/store.ts

@ -1,5 +1,5 @@
import systemServ from '@/service/system.ts'
import { IApiResult, IPage, IPageResult, MenuItem } from '@/types'
import { IPage, IPageResult, MenuItem } from '@/types'
import { IMenu } from '@/types/menus'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { atom, createStore } from 'jotai'
@ -8,16 +8,16 @@ import { t } from '@/i18n.ts'
export const defaultMenu = {
parent_id: 0,
type: 'menu',
name: '',
title: '',
icon: '',
path: '',
component: '',
sort: 0,
id: 0,
button: [],
parent_id: 0,
type: 'menu',
name: '',
title: '',
icon: '',
path: '',
component: '',
sort: 0,
id: 0,
button: [],
} as unknown as MenuItem
export const menuPageAtom = atom<IPage>({})
@ -26,67 +26,67 @@ const store = createStore()
export const menuDataAtom = atomWithQuery<any, IPageResult<IMenu>>((get) => {
return {
queryKey: [ 'menus', get(menuPageAtom) ],
queryFn: async ({ queryKey: [ , page ] }) => {
return await systemServ.menus.list(page)
},
select: (data) => {
return data.rows ?? []
}
return {
queryKey: [ 'menus', get(menuPageAtom) ],
queryFn: async ({ queryKey: [ , page ] }) => {
return await systemServ.menus.list(page)
},
select: (data) => {
return data.rows ?? []
}
}
})
export const selectedMenuIdAtom = atom<number>(0)
export const selectedMenuAtom = atom<MenuItem>({} as MenuItem)
export const byIdMenuAtom = atomWithQuery((get) => ({
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ],
queryFn: async ({ queryKey: [ , id ] }) => {
return await systemServ.menus.info(id as number)
},
select: data => data.data,
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ],
queryFn: async ({ queryKey: [ , id ] }) => {
return await systemServ.menus.info(id as number)
},
select: data => data.data,
}))
export const saveOrUpdateMenuAtom = atomWithMutation<IApiResult<IMenu>>((get) => {
export const saveOrUpdateMenuAtom = atomWithMutation<any, IMenu>((get) => {
return {
mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ],
mutationFn: async (data: IMenu) => {
if (data.id === 0) {
return await systemServ.menus.add(data)
}
return await systemServ.menus.update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t( isAdd ? 'message.saveSuccess': 'message.editSuccess', '保存成功'))
const menu = get(selectedMenuAtom)
store.set(selectedMenuAtom, {
...menu,
id: res.data?.id
})
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ]}).then()
}
return {
mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ],
mutationFn: async (data: IMenu) => {
if (data.id === 0) {
return await systemServ.menus.add(data)
}
return await systemServ.menus.update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
const menu = get(selectedMenuAtom)
store.set(selectedMenuAtom, {
...menu,
id: res.data?.id
})
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then()
}
}
})
export const batchIdsAtom = atom<number[]>([])
export const deleteMenuAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteMenu', get(batchIdsAtom) ],
mutationFn: async (ids?: number[]) => {
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom))
},
onSuccess: () => {
message.success(t('message.deleteSuccess', '删除成功'))
store.set(batchIdsAtom, [])
get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ]}).then()
}
return {
mutationKey: [ 'deleteMenu', get(batchIdsAtom) ],
mutationFn: async (ids?: number[]) => {
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom))
},
onSuccess: () => {
message.success(t('message.deleteSuccess', '删除成功'))
store.set(batchIdsAtom, [])
get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then()
}
}
})

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

@ -26,8 +26,6 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
background: ${token.colorBgContainer};
`
const emptyForm = css`
backdrop-filter: ${token.backdropFilter};
color: red;
`
const form = css`
@ -62,7 +60,13 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
`
const treeActions = css`
padding: 0 10px 10px;
display: flex;
flex-direction: column;
position: sticky;
bottom: 0;
z-index: 10;
background: ${token.colorBgContainer};
`
return {

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

@ -1,22 +1,23 @@
import Switch from '@/components/switch'
import {
ActionType,
PageContainer,
ProColumns,
ProTable,
BetaSchemaForm,
ActionType,
PageContainer,
ProColumns,
ProTable,
BetaSchemaForm, ProFormColumnsType,
} from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
import { useStyle } from './style.ts'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {
deleteRoleAtom,
pageAtom,
roleAtom,
roleIdsAtom,
rolesAtom,
saveOrUpdateRoleAtom,
searchAtom
deleteRoleAtom,
pageAtom,
roleAtom,
roleIdsAtom,
rolesAtom,
saveOrUpdateRoleAtom,
searchAtom
} from './store.ts'
import { useTranslation } from '@/i18n.ts'
import { Button, Form, Space, Spin, Table, Tree, Popconfirm } from 'antd'
@ -25,214 +26,220 @@ import { menuDataAtom } from '@/pages/system/menus/store.ts'
import { getTreeCheckedStatus } from '@/utils/tree.ts'
const MenuTree = (props: any) => {
const { data: menuList, isLoading: menuLoading } = useAtomValue(menuDataAtom)
const { value, onChange, form, id, mode } = props
const { data: menuList, isLoading: menuLoading } = useAtomValue(menuDataAtom)
const { value, onChange, form, id, mode } = props
const onCheck = (checkedKeys: any, info: any) => {
if (onChange) {
onChange([ ...checkedKeys, ...info.halfCheckedKeys ])
} else {
form.setFieldsValue({ [id]: [ ...checkedKeys, ...info.halfCheckedKeys ] })
}
}
if (menuLoading) {
return <Spin spinning={true} size={'small'}/>
const onCheck = (checkedKeys: any, info: any) => {
if (onChange) {
onChange([ ...checkedKeys, ...info.halfCheckedKeys ])
} else {
form.setFieldsValue({ [id]: [ ...checkedKeys, ...info.halfCheckedKeys ] })
}
return <Tree treeData={menuList}
fieldNames={{ title: 'title', key: 'id' }}
disabled={mode !== 'edit'} checkable={true} onCheck={onCheck}
checkedKeys={getTreeCheckedStatus(menuList, value)}/>
}
if (menuLoading) {
return <Spin spinning={true} size={'small'}/>
}
return <Tree treeData={menuList}
fieldNames={{ title: 'title', key: 'id' }}
disabled={mode !== 'edit'} checkable={true} onCheck={onCheck}
checkedKeys={getTreeCheckedStatus(menuList, value)}/>
}
const Roles = memo(() => {
const { t } = useTranslation()
const { styles } = useStyle()
const [ form ] = Form.useForm()
const actionRef = useRef<ActionType>()
const [ page, setPage ] = useAtom(pageAtom)
const setSearch = useSetAtom(searchAtom)
const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom)
const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom)
const { mutate: deleteRole, isPending: isDeleteing } = useAtomValue(deleteRoleAtom)
const [ , setRole ] = useAtom(roleAtom)
const [ open, setOpen ] = useState(false)
const { t } = useTranslation()
const { styles } = useStyle()
const [ form ] = Form.useForm()
const actionRef = useRef<ActionType>()
const [ page, setPage ] = useAtom(pageAtom)
const setSearch = useSetAtom(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 [ open, setOpen ] = useState(false)
const columns = useMemo(() => {
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.roles.columns.name'), dataIndex: 'name', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.roles.columns.code'), dataIndex: 'code', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{ title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch', },
{
title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit',
},
{ title: t('system.roles.columns.description'), dataIndex: 'description', valueType: 'textarea' },
{
title: t('system.roles.columns.menu_ids'),
hideInTable: true,
hideInSearch: true,
dataIndex: 'menu_ids',
valueType: 'text',
renderFormItem: (item, config, form) => {
return <MenuTree {...config} form={form} {...item.fieldProps} />
}
},
{
title: t('system.roles.columns.option'), valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<a key="editable"
onClick={() => {
setRole(record)
setOpen(true)
form.setFieldsValue(record)
}}
>
{t('actions.edit', '编辑')}
</a>,
<Popconfirm
key={'del_confirm'}
onConfirm={()=>{
deleteRole([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del" >
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
const columns = useMemo(() => {
useEffect(() => {
if (isSuccess) {
setOpen(false)
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.roles.columns.name'), dataIndex: 'name', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.roles.columns.code'), dataIndex: 'code', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch',
render: (text) => {
return <Switch value={!!text} size={'small'}/>
}
}, [ isSuccess ])
},
{
title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit',
},
{ title: t('system.roles.columns.description'), dataIndex: 'description', valueType: 'textarea' },
{
title: t('system.roles.columns.menu_ids'),
hideInTable: true,
hideInSearch: true,
dataIndex: 'menu_ids',
valueType: 'text',
renderFormItem: (item, config, form) => {
return <MenuTree {...config} form={form} {...item.fieldProps} />
}
},
{
title: t('system.roles.columns.option'), valueType: 'option',
key: 'option',
render: (_, record) => [
<a key="editable"
onClick={() => {
setRole(record)
setOpen(true)
form.setFieldsValue(record)
}}
>
{t('actions.edit', '编辑')}
</a>,
<Popconfirm
key={'del_confirm'}
onConfirm={() => {
deleteRole([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.roles.title', '角色管理')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
rowSelection={{
onChange: (selectedRowKeys) => {
setRoleIds(selectedRowKeys as number[])
},
selectedRowKeys: roleIds,
selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ],
}}
tableAlertOptionRender={() => {
return (
<Space size={16}>
<Popconfirm
onConfirm={() => {
deleteRole(roleIds)
}}
title={t('message.batchDelete')}>
<Button type={'link'} loading={isDeleteing}>{t('actions.batchDel')}</Button>
</Popconfirm>
</Space>
)
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
onSearch: (value: string) => {
setSearch({ key: value })
},
},
actions: [
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type="primary"
>
{t('actions.add', '添加')}
</Button>,
]
}}
pagination={{
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onChange: (page) => {
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.roles.title', '角色管理')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
rowSelection={{
onChange: (selectedRowKeys) => {
setRoleIds(selectedRowKeys as number[])
},
selectedRowKeys: roleIds,
selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ],
}}
tableAlertOptionRender={() => {
return (
<Space size={16}>
<Popconfirm
onConfirm={() => {
deleteRole(roleIds)
}}
title={t('message.batchDelete')}>
<Button type={'link'} loading={isDeleting}>{t('actions.batchDel')}</Button>
</Popconfirm>
</Space>
)
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
onSearch: (value: string) => {
setSearch({ key: value })
},
},
actions: [
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type="primary"
>
{t('actions.add', '添加')}
</Button>,
]
}}
pagination={{
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onChange: (page) => {
setPage((prev) => {
return { ...prev, page }
})
}
}}
/>
<BetaSchemaForm
width={600}
form={form}
layout={'horizontal'}
title={t('system.roles.edit.title', '角色编辑')}
colProps={{ span: 24 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
layoutType={'ModalForm'}
open={open}
modalProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isPending}
onFinish={(values) => {
// console.log('values', values)
return mutate(values)
}}
columns={columns}/>
</PageContainer>
)
setPage((prev) => {
return { ...prev, page }
})
}
}}
/>
<BetaSchemaForm
width={600}
form={form}
layout={'horizontal'}
title={t('system.roles.edit.title', '角色编辑')}
colProps={{ span: 24 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
layoutType={'ModalForm'}
open={open}
modalProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isPending}
onFinish={async (values) => {
// console.log('values', values)
mutate(values)
return true
}}
columns={columns as ProFormColumnsType[]}/>
</PageContainer>
)
})
export const Route = createLazyFileRoute('/system/roles')({
component: Roles
component: Roles
})
export default Roles

90
src/pages/system/roles/store.ts

@ -1,13 +1,14 @@
import { convertToBool } from '@/utils'
import { atom } from 'jotai/index'
import { IRole } from '@/types/roles'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { IApiResult, IPage, IPageResult } from '@/types'
import { IPage } from '@/types'
import systemServ from '@/service/system.ts'
import { message } from 'antd'
import { t } from '@/i18n.ts'
type SearchParams = IPage & {
key?: string
key?: string
}
export const idAtom = atom(0)
@ -19,56 +20,67 @@ export const roleAtom = atom<IRole>(undefined as unknown as IRole)
export const searchAtom = atom<SearchParams>({} as SearchParams)
export const pageAtom = atom<IPage>({
pageSize: 10,
page: 1,
pageSize: 10,
page: 1,
})
export const rolesAtom = atomWithQuery<any, IPageResult<IRole>>((get) => {
return {
queryKey: [ 'roles', get(searchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
return await systemServ.role.list(params as SearchParams)
},
export const rolesAtom = atomWithQuery((get) => {
return {
queryKey: [ 'roles', get(searchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
return await systemServ.role.list(params as SearchParams)
},
select: res => {
const data = res.data
data.rows = data.rows?.map(row => {
return {
...row,
status: convertToBool(row.status)
}
})
return data
}
}
})
//saveOrUpdateRoleAtom
export const saveOrUpdateRoleAtom = atomWithMutation<IApiResult<IRole>>((get) => {
export const saveOrUpdateRoleAtom = atomWithMutation<any, IRole>((get) => {
return {
mutationKey: [ 'updateMenu' ],
mutationFn: async (data: IRole) => {
if (data.id === 0) {
return await systemServ.menus.add(data)
}
return await systemServ.menus.update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
return {
mutationKey: [ 'updateMenu' ],
mutationFn: async (data) => {
data.status = data.status ? '1' : '0'
if (data.id === 0) {
return await systemServ.role.add(data)
}
return await systemServ.role.update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] })
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] })
return res
}
return res
}
}
})
export const deleteRoleAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteMenu' ],
mutationFn: async (ids: number[]) => {
return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom))
},
onSuccess: (res) => {
message.success('删除成功')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] })
return res
}
return {
mutationKey: [ 'deleteMenu' ],
mutationFn: async (ids: number[]) => {
return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom))
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] })
return res
}
}
})

177
src/request.ts

@ -1,99 +1,140 @@
import { getToken, setToken } from '@/store/system.ts'
import { IApiResult } from '@/types'
import { Record } from '@icon-park/react'
import { message } from 'antd'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, {
AxiosRequestConfig,
AxiosInstance, AxiosResponse,
} from 'axios'
export type { AxiosRequestConfig }
type FetchMethod = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<IApiResult<T>>
interface RequestMethods extends Pick<AxiosInstance, 'get' | 'post' | 'put' | 'delete' | 'request' | 'postForm' | 'patch' | 'patchForm' | 'putForm' | 'options'> {
}
export const request = axios.create({
baseURL: '/api/v1',
// timeout: 1000,
headers: {
'Content-Type': 'application/json',
},
const axiosInstance = axios.create({
baseURL: '/api/v1',
// timeout: 1000,
headers: {
'Content-Type': 'application/json',
},
})
//拦截request,添加token
request.interceptors.request.use((config) => {
axiosInstance.interceptors.request.use((config) => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
return config
}, (error) => {
console.log('error', error)
return Promise.reject(error)
console.log('error', error)
return Promise.reject(error)
})
//拦截response,返回data
request.interceptors.response.use((response: AxiosResponse) => {
axiosInstance.interceptors.response.use(
(response) => {
// console.log('response', response.data)
message.destroy()
switch (response.data.code) {
case 200:
//login
if (response.config.url?.includes('/sys/login')) {
setToken(response.data.data.token)
}
return response.data
case 401:
setToken('')
if (window.location.pathname === '/login') {
return
}
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
default:
message.error(response.data.message)
return Promise.reject(response)
const result = response.data as IApiResult
switch (result.code) {
case 200:
//login
if (response.config.url?.includes('/sys/login')) {
setToken(result.data.token)
}
return response
case 401:
setToken('')
if (window.location.pathname === '/login') {
return Promise.reject(new Error('to login'))
}
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return Promise.reject(new Error('to login'))
default:
message.error(result.message ?? '请求失败')
return Promise.reject(response)
}
}, (error) => {
}, (error) => {
// console.log('error', error)
const { response } = error
if (response) {
switch (response.status) {
case 401:
if (window.location.pathname === '/login') {
return
}
setToken('')
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
default:
message.error(response.data.message)
return Promise.reject(response)
}
switch (response.status) {
case 401:
if (window.location.pathname === '/login') {
return
}
setToken('')
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
default:
message.error(response.data.message)
return Promise.reject(response)
}
}
return Promise.reject(error)
})
})
export const createFetchMethods = () => {
const methods = {}
for (const method of Object.keys(axiosInstance)) {
methods[method] = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => {
if (config && data) {
config = {
...config,
data,
}
}
return axiosInstance[method](url, config)
.then((response: AxiosResponse<IApiResult<T>>) => {
if (response.data.code !== 200) {
throw new Error(response.data.message)
}
return response.data as IApiResult<T>
})
.catch((err) => {
throw err
})
}
}
return methods as Record<keyof RequestMethods, FetchMethod>
}
export const request = createFetchMethods()
export default request

8
src/service/base.ts

@ -6,19 +6,19 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
return {
list: (params?: TParams & IPage) => {
return request.post<IPageResult<TResult>>(`${api}/list`, { ...options, ...params }).then(data => data.data)
return request.post<IPageResult<TResult>>(`${api}/list`, { ...options, ...params })
},
add: (data: TParams) => {
return request.post<TResult>(`${api}/add`, data, options)
},
update: (data: TParams) => {
return request.post(`${api}/edit`, data, options)
return request.post<TResult>(`${api}/edit`, data, options)
},
delete: (id: number) => {
return request.post(`${api}/delete`, { id }, options)
return request.post<TResult>(`${api}/delete`, { id }, options)
},
batchDelete: (ids: number[]) => {
return request.post(`${api}/deletes`, { ids }, options )
return request.post<TResult>(`${api}/deletes`, { ids }, options)
},
info: (id: number) => {
return request.get<TResult>(`${api}/${id}`, options)

35
src/types/roles.d.ts

@ -1,13 +1,12 @@
export interface IRole {
id: number,
sort: number,
code: string,
name: string,
description: string,
sequence: number,
status: string,
menu_ids: number[]
id: number,
sort: number,
code: string,
name: string,
description: string,
sequence: number,
status: string | boolean,
menu_ids: number[]
}
export interface RoleRequest extends IRole {
@ -15,17 +14,17 @@ export interface RoleRequest extends IRole {
}
export interface RoleListResponse {
key: string,
order: string,
prop: string,
page: number,
pageSize: number
key: string,
order: string,
prop: string,
page: number,
pageSize: number
}
export interface RoleListResponse {
page: number,
pageSize: number,
total: number,
rows: IRole[]
page: number,
pageSize: number,
total: number,
rows: IRole[]
}

158
src/utils/index.ts

@ -8,87 +8,119 @@ export const isDev = import.meta.env.MODE === 'development'
// 格式化菜单数据, 把children转换成routes
export const formatMenuData = (data: IMenu[]) => {
const result: MenuItem[] = []
for (const item of data) {
if (item.icon && typeof item.icon === 'string') {
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' })
}
if (!item.children || !item.children.length) {
result.push({
...item,
key: item.name,
name: item.title
})
} else {
const { children, name, ...other } = item
result.push({
...other,
key: name,
name: other.title,
children: formatMenuData(children),
routes: formatMenuData(children),
})
}
const result: MenuItem[] = []
for (const item of data) {
if (item.icon && typeof item.icon === 'string') {
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' })
}
return result
if (!item.children || !item.children.length) {
result.push({
...item,
key: item.name,
name: item.title
})
} else {
const { children, name, ...other } = item
result.push({
...other,
key: name,
name: other.title,
children: formatMenuData(children),
routes: formatMenuData(children),
})
}
}
return result
}
//把MenuItem[]转换成antd树形结构
export const formatterMenuData = (data: MenuItem[]): TreeDataNode[] => {
const result: TreeDataNode[] = []
for (const item of data) {
if (item.children && item.children.length) {
const { children, ...other } = item
result.push({
...other,
key: item.id!,
title: item.name!,
children: formatterMenuData(children),
})
} else {
result.push({
...item,
key: item.id!,
title: item.name!,
})
}
const result: TreeDataNode[] = []
for (const item of data) {
if (item.children && item.children.length) {
const { children, ...other } = item
result.push({
...other,
key: item.id!,
title: item.name!,
children: formatterMenuData(children),
})
} else {
result.push({
...item,
key: item.id!,
title: item.name!,
})
}
return result
}
return result
}
//把tree转成平铺数组
const defaultTreeFieldNames: FiledNames = {
key: 'id',
title: 'title',
children: 'children'
key: 'id',
title: 'title',
children: 'children'
}
export function flattenTree<T>(tree: TreeItem<T>[], fieldNames?: FiledNames) {
const result: FlattenData<T>[] = []
const result: FlattenData<T>[] = []
if (!fieldNames) {
fieldNames = defaultTreeFieldNames
}
if (!fieldNames) {
fieldNames = defaultTreeFieldNames
function flattenRecursive(item: TreeItem<T>, level: number, fieldNames: FiledNames) {
const data: FlattenData<T> = {
...item,
key: item[fieldNames.key!],
title: item[fieldNames.title!],
level,
}
const children = item[fieldNames.children!]
if (children) {
children.forEach((child) => flattenRecursive(child, level + 1, fieldNames))
data.children = children
}
result.push(data)
}
function flattenRecursive(item: TreeItem<T>, level: number, fieldNames: FiledNames) {
const data: FlattenData<T> = {
...item,
key: item[fieldNames.key!],
title: item[fieldNames.title!],
level,
}
const children = item[fieldNames.children!]
if (children) {
children.forEach((child) => flattenRecursive(child, level + 1, fieldNames))
data.children = children
}
result.push(data)
tree.forEach((item) => flattenRecursive(item, 0, fieldNames))
return result
}
export const convertToBool = (value: any): boolean => {
// 特殊处理字符串 '0'、'true' 和 'false'
if (typeof value === 'string') {
switch (value.toLowerCase()) {
case '0':
return false
case 'true':
return true
case 'false':
return false
default:
// 对于其他非空字符串,转换为 true
return Boolean(value)
}
}
// 处理常见 falsy 值
if (value === undefined || value === null ||
value === false || value === 0 || value === '' || Number.isNaN(value)) {
return false
}
tree.forEach((item) => flattenRecursive(item, 0, fieldNames))
// 对于对象或数组,我们通常认为非空即为 true
if (Array.isArray(value) || typeof value === 'object') {
return !!Object.keys(value).length
}
return result
// 其他情况,包括数字(非零)、字符串(已经被上述逻辑处理)和其他 truthy 值
return Boolean(value)
}

8
src/vite-env.d.ts

@ -1 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly API_URL: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

47
vite.config.ts

@ -1,36 +1,43 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
//import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
// https://vitejs.dev/config/
export default defineConfig({
export default defineConfig(({ mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
const env = loadEnv(mode, process.cwd(), '')
return {
//定义别名的路径
resolve: {
alias: {
'@': '/src',
},
alias: {
'@': '/src',
},
},
server: {
proxy: {
'/api': {
target: 'http://47.113.117.106:8000',
changeOrigin: true,
rewrite: (path) => path
}
proxy: {
'/api': {
target: env.API_URL,
changeOrigin: true,
rewrite: (path) => path
}
}
},
plugins: [
react(),
viteMockServe({
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
enable: false,
react(),
viteMockServe({
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
enable: false,
// mock 文件的根路径,默认值:'mocks'
mockPath: 'mock',
logger: true,
}),
//TanStackRouterVite(),
// mock 文件的根路径,默认值:'mocks'
mockPath: 'mock',
logger: true,
}),
//TanStackRouterVite(),
],
}
})
Loading…
Cancel
Save