Browse Source

完善菜单模块

main
李金 7 months ago
parent
commit
e88b4c7566
  1. 38
      src/components/glass/index.tsx
  2. 29
      src/components/glass/style.ts
  3. 2
      src/i18n.ts
  4. 124
      src/locales/lang/en-US.ts
  5. 31
      src/locales/lang/pages/system/menus/en-US.ts
  6. 50
      src/locales/lang/pages/system/menus/zh-CN.ts
  7. 109
      src/locales/lang/zh-CN.ts
  8. 143
      src/pages/system/menus/components/MenuTree.tsx
  9. 220
      src/pages/system/menus/index.tsx
  10. 103
      src/pages/system/menus/store.ts
  11. 141
      src/pages/system/menus/style.ts
  12. 3
      src/theme/themes/token.ts

38
src/components/glass/index.tsx

@ -0,0 +1,38 @@
import { Flex } from 'antd'
import { useStyle } from './style.ts'
import React from 'react'
export interface IClassProps {
enabled: boolean
className?: string
description?: JSX.Element | React.ReactNode
children?: JSX.Element | React.ReactNode
}
const Glass = (props: IClassProps) => {
const { styles } = useStyle(props)
if (!props.enabled) {
return props.children
}
return (
<div className={styles.container}>
<Flex justify={'center'} align={'center'} className={styles.description}>
{props.description}
</Flex>
<div className={styles.glass}/>
{props.children}
</div>
)
}
export const withGlass = (Component: React.Component | React.FC | JSX.Element | React.ReactNode) => (props) => {
return (
<Glass enabled={props.enabled}>
<Component {...props} />
</Glass>
)
}
export default Glass

29
src/components/glass/style.ts

@ -0,0 +1,29 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-glass-wrap`
const container = css`
position: relative;
`
const glass = css`
background-color: transparent;
backdrop-filter: blur(6px);
z-index: 100;
position: absolute;
width: 100%;
height: 100%;
`
const description = css`
position: absolute;
top: 20%;
width: 100%;
z-index: 101;
`
return {
container: cx(prefix, props.className, container),
glass,
description,
}
})

2
src/i18n.ts

@ -35,7 +35,7 @@ export const initI18n = (options?: InitOptions) => {
}, },
}, },
fallbackLng: 'zh', fallbackLng: 'zh',
debug: false,
debug: true,
detection: detectionOptions, detection: detectionOptions,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,

124
src/locales/lang/en-US.ts

@ -3,61 +3,85 @@ import menus from './pages/system/menus/en-US'
export default { export default {
...antdEN,
...antdEN,
error: {
'404': {
title: 'not fund',
message: 'Sorry, not found this page.'
},
'403': {
title: 'not authorized',
message: 'Sorry, you are not authorized to access this page.'
},
'error': {
title: 'error info',
},
error: {
'404': {
title: 'not fund',
message: 'Sorry, not found this page.'
}, },
route: {
goBack: 'Go Back',
'403': {
title: 'not authorized',
message: 'Sorry, you are not authorized to access this page.'
}, },
app: {
header: {
logout: 'logout',
}
'error': {
title: 'error info',
}, },
login: {
title: 'Account Password Login',
username: 'Username',
usernameMsg: 'Please enter your username',
password: 'Password',
passwordMsg: 'Please enter your password',
code: 'Verification Code',
codeMsg: 'Please enter the verification code',
submit: 'Login',
success: 'Login success'
},
home: {
welcome: 'Welcome to'
},
system: {
menus,
},
},
route: {
goBack: 'Go Back',
},
app: {
header: {
logout: 'logout',
}
},
login: {
title: 'Account Password Login',
username: 'Username',
usernameMsg: 'Please enter your username',
password: 'Password',
passwordMsg: 'Please enter your password',
code: 'Verification Code',
codeMsg: 'Please enter the verification code',
submit: 'Login',
success: 'Login success'
},
home: {
welcome: 'Welcome to'
},
system: {
menus,
},
actions: {
news: 'Add newly',
add: 'Add',
cancel: 'Cancel',
delete: 'Delete',
batchDel: 'Batch Delete',
reset: 'Reset',
clear: 'Clear',
close: 'Close',
},
message: {
infoTitle: 'Hint',
errorTitle: 'Error', errorTitle: 'Error',
successTitle: 'Success', successTitle: 'Success',
success: 'Submit Success',
fail: 'Submit Fail',
saveSuccess: 'Save Success',
saveFail: 'Save Fail',
warningTitle: 'Warning',
batchDelete: 'Are you sure to delete the selected data?',
deleteConfirm: 'Are you sure to delete it?',
success: 'Submission successful',
fail: 'Submission failed',
saveSuccess: 'Save successfully',
saveFail: 'Save failed',
emptyData: 'No Data',
emptyDataAdd: 'No data at present, click to add',
required: 'This item is a required field',
},
errorTitle: 'Error',
successTitle: 'Success',
success: 'Submit Success',
fail: 'Submit Fail',
saveSuccess: 'Save Success',
saveFail: 'Save Fail',
tabs: {
refresh: 'Refresh',
maximize: 'Maximize',
closeCurrent: 'Close current',
closeLeft: 'Close Left',
closeRight: 'Close Right',
closeOther: 'Close other',
closeAll: 'Close All'
}
tabs: {
refresh: 'Refresh',
maximize: 'Maximize',
closeCurrent: 'Close current',
closeLeft: 'Close Left',
closeRight: 'Close Right',
closeOther: 'Close other',
closeAll: 'Close All'
}
} }

31
src/locales/lang/pages/system/menus/en-US.ts

@ -1,3 +1,32 @@
export default { export default {
title: 'Menu',
setting: 'Configuration',
saveSuccess: 'Save successfully',
form: {
title: 'Menu name',
parent: 'Upper-level menu',
type: 'Type',
typeOptions: {
menu: 'Menu',
iframe: 'Iframe',
link: 'External link',
button: 'Button',
},
name: 'Alias',
icon: 'Icon',
sort: 'Sorting',
path: 'Route',
component: 'View',
componentHelp: 'View path, relative to src/pages, menu groups can be left blank',
save: 'Save',
empty: 'Please select a row of data from the left for operation',
table: {
columns: {
name: 'Name',
code: 'Code',
option: 'Option',
}
}
},
button: 'Buttons'
} }

50
src/locales/lang/pages/system/menus/zh-CN.ts

@ -1,24 +1,32 @@
export default { export default {
title: '菜单',
setting: '配置',
saveSuccess: '保存成功',
form:{
title: '菜单名称',
parent: '上级菜单',
type:'类型',
typeOptions: {
menu: '菜单',
iframe: 'iframe',
link: '外链',
button: '按钮',
},
name: '别名',
icon: '图标',
sort: '排序',
path: '路由',
component: '视图',
componentHelp: '视图路径,相对于src/pages,菜单组可以不填',
save: '保存',
title: '菜单',
setting: '配置',
saveSuccess: '保存成功',
form: {
title: '菜单名称',
parent: '上级菜单',
type: '类型',
typeOptions: {
menu: '菜单',
iframe: 'iframe',
link: '外链',
button: '按钮',
}, },
button: '按钮'
name: '别名',
icon: '图标',
sort: '排序',
path: '路由',
component: '视图',
componentHelp: '视图路径,相对于src/pages,菜单组可以不填',
save: '保存',
empty: '请从左侧选择一行数据操作',
table: {
columns: {
name: '名称',
code: '标识',
option: '操作',
}
}
},
button: '按钮'
} }

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

@ -3,59 +3,78 @@ import menus from './pages/system/menus/zh-CN.ts'
export default { export default {
...antdZh,
error: {
'404': {
title: '无法找到',
message: '找不到此页面'
},
'403': {
title: '没有权限',
message: '对不起,您没有权限查看此页面。'
},
'error': {
title: '错误信息',
},
...antdZh,
error: {
'404': {
title: '无法找到',
message: '找不到此页面'
}, },
route: {
goBack: '返回',
'403': {
title: '没有权限',
message: '对不起,您没有权限查看此页面。'
}, },
app: {
header: {
logout: '退出登录',
}
},
login: {
title: '账户密码登录',
username: '用户名',
usernameMsg: '请输入用户名',
password: '密码',
passwordMsg: '请输入密码',
code: '验证码',
codeMsg: '请输入验证码',
submit: '登录',
success: '登录成功'
},
home: {
welcome: '欢迎使用'
'error': {
title: '错误信息',
}, },
},
route: {
goBack: '返回',
},
app: {
header: {
logout: '退出登录',
}
},
login: {
title: '账户密码登录',
username: '用户名',
usernameMsg: '请输入用户名',
password: '密码',
passwordMsg: '请输入密码',
code: '验证码',
codeMsg: '请输入验证码',
submit: '登录',
success: '登录成功'
},
home: {
welcome: '欢迎使用'
},
system: {
menus,
},
system: {
menus,
},
actions: {
news: '新增加',
add: '添加',
cancel: '取消',
delete: '删除',
batchDel: '批量删除',
reset: '重置',
clear: '清空',
close: '关闭',
},
message: {
infoTitle: '提示',
errorTitle: '错误', errorTitle: '错误',
successTitle: '成功', successTitle: '成功',
warningTitle: '警告',
batchDelete: '确定要删除所选数据吗?',
deleteConfirm: '确定要删除吗?',
success: '提交成功', success: '提交成功',
fail: '提交失败', fail: '提交失败',
saveSuccess: '保存成功', saveSuccess: '保存成功',
saveFail: '保存失败', saveFail: '保存失败',
tabs: {
refresh: '刷新',
maximize: '最大化',
closeCurrent: '关闭当前',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOther: '关闭其它',
closeAll: '关闭所有'
}
emptyData: '暂无数据',
emptyDataAdd: '暂无数据,点击添加',
required: '此项为必填项',
},
tabs: {
refresh: '刷新',
maximize: '最大化',
closeCurrent: '关闭当前',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOther: '关闭其它',
closeAll: '关闭所有'
}
} }

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

@ -4,7 +4,7 @@ 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, defaultMenu, 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 '../components/TreeNodeRender.tsx'
@ -15,89 +15,78 @@ 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)
const setIds = useSetAtom(batchIdsAtom)
const { data = [], isLoading } = useAtomValue(menuDataAtom)
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)
}
// 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)
}
return () => {
setCurrentMenu({} as MenuItem)
}
return () => {
setCurrentMenu({} as MenuItem)
}
}, [ data, isLoading ])
}, [ data, isLoading ])
const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('system.menus.empty', '暂无数据,点击添加')}>
<Button type="default"
icon={<PlusOutlined/>}
onClick={() => {
const menu = {
parent_id: 0,
type: 'menu',
name: '',
title: '',
icon: '',
path: '',
component: '',
sort: 0,
id: 0,
} as MenuItem
setCurrentMenu(menu)
form.setFieldsValue(menu)
}}
>
{t('system.menus.add', '添加')}
</Button>
</Empty>
}
const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}>
<Button type="default"
icon={<PlusOutlined/>}
onClick={() => {
setCurrentMenu(defaultMenu)
form.setFieldsValue(defaultMenu)
}}
>
{t('actions.add', '添加')}
</Button>
</Empty>
}
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

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

@ -1,6 +1,7 @@
import Glass from '@/components/glass'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { PageContainer, ProCard } from '@ant-design/pro-components' import { PageContainer, ProCard } from '@ant-design/pro-components'
import { Button, Form, Input, message, Radio, TreeSelect, InputNumber, notification } from 'antd'
import { Button, Form, Input, message, Radio, TreeSelect, InputNumber, notification, Alert } from 'antd'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts' import { menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts'
import IconPicker from '@/components/icon/picker' import IconPicker from '@/components/icon/picker'
@ -17,7 +18,7 @@ import { createLazyFileRoute } from '@tanstack/react-router'
const Menus = () => { const Menus = () => {
const { styles } = 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, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
@ -26,13 +27,13 @@ const Menus = () => {
useEffect(() => { useEffect(() => {
if (isSuccess) { if (isSuccess) {
message.success(t('saveSuccess', '保存成功'))
message.success(t('message.saveSuccess', '保存成功'))
} }
if (isError) { if (isError) {
notification.error({ notification.error({
message: t('errorTitle', '错误'),
description: (error as any).message ?? t('saveFail', '保存失败'),
message: t('message.error', '错误'),
description: (error as any).message ?? t('message.saveFail', '保存失败'),
}) })
} }
@ -57,110 +58,121 @@ const Menus = () => {
</ProCard> </ProCard>
</DraggablePanel> </DraggablePanel>
<Flexbox className={styles.box}> <Flexbox className={styles.box}>
<Form form={form}
initialValues={currentMenu!}
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
wrapperCol={{ flex: 1 }}
colon={false}
className={styles.form}
<Glass
enabled={currentMenu.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.menus.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
> >
<ProCard title={t('system.menus.setting', '配置')}
className={styles.formSetting}
<Form form={form}
initialValues={currentMenu!}
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
wrapperCol={{ flex: 1 }}
colon={false}
className={cx(styles.form, styles.emptyForm, {
[styles.emptyForm]: currentMenu.id === undefined
})}
> >
<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>
<Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}>
<TreeSelect
treeData={[
{ id: 0, title: '顶级菜单', children: data as any },
]}
treeDefaultExpandAll={true}
fieldNames={{
label: 'title',
value: 'id'
}}/>
</Form.Item>
<Form.Item label={t('system.menus.form.type', '类型')} name={'type'}>
<Radio.Group
options={[
{
label: t('system.menus.form.typeOptions.menu', '菜单'),
value: 'menu'
},
{
label: t('system.menus.form.typeOptions.iframe', 'iframe'),
value: 'iframe'
},
{
label: t('system.menus.form.typeOptions.link', '外链'),
value: 'link'
},
{
label: t('system.menus.form.typeOptions.button', '按钮'),
value: 'button'
},
]}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t('system.menus.form.name', '别名')} name={'name'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}>
<IconPicker placement={'left'}/>
</Form.Item>
<Form.Item label={t('system.menus.form.sort', '排序')} name={'sort'}>
<InputNumber/>
</Form.Item>
<Form.Item label={t('system.menus.form.path', '路由')} name={'path'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.menus.form.component', '视图')}
name={'component'}
help={t('system.menus.form.component.componentHelp', '视图路径,相对于src/pages')}
<ProCard title={t('system.menus.setting', '配置')}
className={styles.formSetting}
> >
<Input addonBefore={'pages/'}/>
</Form.Item>
<Form.Item label={' '}>
<Button type="primary"
htmlType={'submit'}
loading={isPending}
onClick={() => {
form.validateFields().then((values) => {
mutate(values)
})
}}
>
{t('system.menus.form.save', '保存')}
</Button>
</Form.Item>
</ProCard>
<ProCard title={t('system.menus.form.button', '按钮')}
className={styles.formButtons}
colSpan={8}>
<Form.Item noStyle={true} name={'button'}
shouldUpdate={(prevValues: MenuItem, curValues) => {
return prevValues.id !== curValues.id
}}>
<ButtonTable form={form} key={(currentMenu as any).id}/>
</Form.Item>
</ProCard>
</Form>
<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>
<Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}>
<TreeSelect
treeData={[
{ id: 0, title: '顶级菜单', children: data as any },
]}
treeDefaultExpandAll={true}
fieldNames={{
label: 'title',
value: 'id'
}}/>
</Form.Item>
<Form.Item label={t('system.menus.form.type', '类型')} name={'type'}>
<Radio.Group
options={[
{
label: t('system.menus.form.typeOptions.menu', '菜单'),
value: 'menu'
},
{
label: t('system.menus.form.typeOptions.iframe', 'iframe'),
value: 'iframe'
},
{
label: t('system.menus.form.typeOptions.link', '外链'),
value: 'link'
},
{
label: t('system.menus.form.typeOptions.button', '按钮'),
value: 'button'
},
]}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t('system.menus.form.name', '别名')} name={'name'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}>
<IconPicker placement={'left'}/>
</Form.Item>
<Form.Item label={t('system.menus.form.sort', '排序')} name={'sort'}>
<InputNumber/>
</Form.Item>
<Form.Item label={t('system.menus.form.path', '路由')} name={'path'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.menus.form.component', '视图')}
name={'component'}
help={t('system.menus.form.componentHelp', '视图路径,相对于src/pages')}
>
<Input addonBefore={'pages/'}/>
</Form.Item>
<Form.Item label={' '}>
<Button type="primary"
htmlType={'submit'}
loading={isPending}
onClick={() => {
form.validateFields().then((values) => {
mutate(values)
})
}}
>
{t('system.menus.form.save', '保存')}
</Button>
</Form.Item>
</ProCard>
<ProCard title={t('system.menus.button', '按钮')}
className={styles.formButtons}
colSpan={8}>
<Form.Item noStyle={true} name={'button'}
shouldUpdate={(prevValues: MenuItem, curValues) => {
return prevValues.id !== curValues.id
}}>
<ButtonTable form={form} key={(currentMenu as any).id}/>
</Form.Item>
</ProCard>
</Form>
</Glass>
</Flexbox> </Flexbox>
</Flexbox> </Flexbox>
</PageContainer> </PageContainer>

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

@ -1,67 +1,92 @@
import systemServ from '@/service/system.ts' import systemServ from '@/service/system.ts'
import { IPage, IPageResult, MenuItem } from '@/types'
import { IApiResult, IPage, IPageResult, MenuItem } from '@/types'
import { IMenu } from '@/types/menus' import { IMenu } from '@/types/menus'
import { AxiosResponse } from 'axios'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { atom } from 'jotai/index'
import { atom, createStore } from 'jotai'
export const defaultMenu = {
parent_id: 0,
type: 'menu',
name: '',
title: '',
icon: '',
path: '',
component: '',
sort: 0,
id: 0,
button: [],
} as MenuItem
export const menuPageAtom = atom<IPage>({}) export const menuPageAtom = atom<IPage>({})
const store = createStore()
export const menuDataAtom = atomWithQuery<any, IPageResult<IMenu>>((get) => { 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 selectedMenuIdAtom = atom<number>(0)
export const selectedMenuAtom = atom<MenuItem>({} as MenuItem) export const selectedMenuAtom = atom<MenuItem>({} as MenuItem)
export const byIdMenuAtom = atomWithQuery((get) => ({ 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((get) => {
export const saveOrUpdateMenuAtom = atomWithMutation<IApiResult<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: () => {
// console.log(data)
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).refetchQueries([ '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 menu = get(selectedMenuAtom)
console.log({
...menu,
id: res.data?.id
})
store.set(selectedMenuAtom, {
...menu,
id: res.data?.id
})
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).refetchQueries([ 'menus', get(menuPageAtom) ]).then()
} }
}
}) })
export const batchIdsAtom = atom<number[]>([]) export const batchIdsAtom = atom<number[]>([])
export const deleteMenuAtom = atomWithMutation((get) => { export const deleteMenuAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteMenu', get(batchIdsAtom) ],
mutationFn: async (ids?: number[]) => {
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom))
},
onSuccess: (data) => {
console.log(data)
}
return {
mutationKey: [ 'deleteMenu', get(batchIdsAtom) ],
mutationFn: async (ids?: number[]) => {
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom))
},
onSuccess: () => {
store.set(batchIdsAtom, [])
get(queryClientAtom).refetchQueries([ 'menus', get(menuPageAtom) ]).then()
} }
}
}) })

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

@ -1,72 +1,79 @@
import { createStyles } from '@/theme' 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 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`
flex: 1;
background: ${token.colorBgContainer};
`
const form = css`
display: flex;
flex-wrap: wrap;
min-width: 500px;
`
const formSetting = css`
flex: 1;
`
const formButtons = css`
width: 500px;
`
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`
`
return {
container: cx(prefix),
box,
tree,
form,
treeNode,
treeActions,
formSetting,
formButtons,
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`
flex: 1;
background: ${token.colorBgContainer};
`
const emptyForm = css`
backdrop-filter: ${token.backdropFilter};
color: red;
`
const form = css`
display: flex;
flex-wrap: wrap;
min-width: 500px;
`
const formSetting = css`
flex: 1;
`
const formButtons = css`
width: 500px;
`
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`
`
return {
container: cx(prefix),
box,
emptyForm,
tree,
form,
treeNode,
treeActions,
formSetting,
formButtons,
}
}) })

3
src/theme/themes/token.ts

@ -8,6 +8,7 @@ export interface ProThemeToken {
colorTypeBoolArray: string; colorTypeBoolArray: string;
colorTypeNumberArray: string; colorTypeNumberArray: string;
colorTypeStringArray: string; colorTypeStringArray: string;
filterBackdrop: string;
} }
export const getProToken: GetCustomToken<ProThemeToken> = () => ({ export const getProToken: GetCustomToken<ProThemeToken> = () => ({
@ -19,6 +20,8 @@ export const getProToken: GetCustomToken<ProThemeToken> = () => ({
colorTypeNumberArray: '#239BEF', colorTypeNumberArray: '#239BEF',
colorTypeStringArray: '#62AE8D', colorTypeStringArray: '#62AE8D',
filterBackdrop: 'blur(6px)'
}) })
export const themeToken = getProToken({} as any) export const themeToken = getProToken({} as any)
Loading…
Cancel
Save