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. 26
      src/locales/lang/en-US.ts
  5. 31
      src/locales/lang/pages/system/menus/en-US.ts
  6. 12
      src/locales/lang/pages/system/menus/zh-CN.ts
  7. 19
      src/locales/lang/zh-CN.ts
  8. 21
      src/pages/system/menus/components/MenuTree.tsx
  9. 32
      src/pages/system/menus/index.tsx
  10. 39
      src/pages/system/menus/store.ts
  11. 17
      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,

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

@ -43,7 +43,31 @@ export default {
system: { system: {
menus, 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',
successTitle: 'Success',
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', errorTitle: 'Error',
successTitle: 'Success', successTitle: 'Success',
success: 'Submit Success', success: 'Submit Success',

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'
} }

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

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

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

@ -43,12 +43,31 @@ export default {
system: { system: {
menus, 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: '保存失败',
emptyData: '暂无数据',
emptyDataAdd: '暂无数据,点击添加',
required: '此项为必填项',
},
tabs: { tabs: {
refresh: '刷新', refresh: '刷新',
maximize: '最大化', maximize: '最大化',

21
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'
@ -42,26 +42,15 @@ const MenuTree = ({ form }: { form: FormInstance }) => {
const renderEmpty = () => { const renderEmpty = () => {
if ((data as any).length > 0 || isLoading) return null if ((data as any).length > 0 || isLoading) return null
return <Empty description={t('system.menus.empty', '暂无数据,点击添加')}>
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}>
<Button type="default" <Button type="default"
icon={<PlusOutlined/>} icon={<PlusOutlined/>}
onClick={() => { onClick={() => {
const menu = {
parent_id: 0,
type: 'menu',
name: '',
title: '',
icon: '',
path: '',
component: '',
sort: 0,
id: 0,
} as MenuItem
setCurrentMenu(menu)
form.setFieldsValue(menu)
setCurrentMenu(defaultMenu)
form.setFieldsValue(defaultMenu)
}} }}
> >
{t('system.menus.add', '添加')}
{t('actions.add', '添加')}
</Button> </Button>
</Empty> </Empty>
} }

32
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,7 +58,16 @@ const Menus = () => {
</ProCard> </ProCard>
</DraggablePanel> </DraggablePanel>
<Flexbox className={styles.box}> <Flexbox className={styles.box}>
<Glass
enabled={currentMenu.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.menus.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
>
<Form form={form} <Form form={form}
initialValues={currentMenu!} initialValues={currentMenu!}
labelCol={{ flex: '110px' }} labelCol={{ flex: '110px' }}
@ -65,7 +75,9 @@ const Menus = () => {
labelWrap labelWrap
wrapperCol={{ flex: 1 }} wrapperCol={{ flex: 1 }}
colon={false} colon={false}
className={styles.form}
className={cx(styles.form, styles.emptyForm, {
[styles.emptyForm]: currentMenu.id === undefined
})}
> >
<ProCard title={t('system.menus.setting', '配置')} <ProCard title={t('system.menus.setting', '配置')}
@ -128,7 +140,7 @@ const Menus = () => {
<Form.Item label={t('system.menus.form.component', '视图')} <Form.Item label={t('system.menus.form.component', '视图')}
name={'component'} name={'component'}
help={t('system.menus.form.component.componentHelp', '视图路径,相对于src/pages')}
help={t('system.menus.form.componentHelp', '视图路径,相对于src/pages')}
> >
<Input addonBefore={'pages/'}/> <Input addonBefore={'pages/'}/>
</Form.Item> </Form.Item>
@ -148,7 +160,7 @@ const Menus = () => {
</ProCard> </ProCard>
<ProCard title={t('system.menus.form.button', '按钮')}
<ProCard title={t('system.menus.button', '按钮')}
className={styles.formButtons} className={styles.formButtons}
colSpan={8}> colSpan={8}>
<Form.Item noStyle={true} name={'button'} <Form.Item noStyle={true} name={'button'}
@ -160,7 +172,7 @@ const Menus = () => {
</ProCard> </ProCard>
</Form> </Form>
</Glass>
</Flexbox> </Flexbox>
</Flexbox> </Flexbox>
</PageContainer> </PageContainer>

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

@ -1,11 +1,27 @@
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) => {
@ -32,7 +48,7 @@ export const byIdMenuAtom = atomWithQuery((get) => ({
})) }))
export const saveOrUpdateMenuAtom = atomWithMutation((get) => {
export const saveOrUpdateMenuAtom = atomWithMutation<IApiResult<IMenu>>((get) => {
return { return {
mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ],
@ -42,8 +58,16 @@ export const saveOrUpdateMenuAtom = atomWithMutation((get) => {
} }
return await systemServ.menus.update(data) return await systemServ.menus.update(data)
}, },
onSuccess: () => {
// console.log(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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix // @ts-ignore fix
@ -60,8 +84,9 @@ export const deleteMenuAtom = atomWithMutation((get) => {
mutationFn: async (ids?: number[]) => { mutationFn: async (ids?: number[]) => {
return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom))
}, },
onSuccess: (data) => {
console.log(data)
onSuccess: () => {
store.set(batchIdsAtom, [])
get(queryClientAtom).refetchQueries([ 'menus', get(menuPageAtom) ]).then()
} }
} }
}) })

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

@ -1,7 +1,7 @@
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 prefix = `${prefixCls}-${token?.proPrefix}-menu-page`
const tree = css` const tree = css`
.ant-tree { .ant-tree {
@ -11,10 +11,12 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
background: ${token.colorBgContainer}; background: ${token.colorBgContainer};
} }
.ant-tree-directory .ant-tree-treenode-selected::before{
.ant-tree-directory .ant-tree-treenode-selected::before {
background: ${token.colorBgTextHover}; background: ${token.colorBgTextHover};
} }
.ant-tree-treenode:before{
.ant-tree-treenode:before {
border-radius: ${token.borderRadius}px; border-radius: ${token.borderRadius}px;
} }
` `
@ -23,6 +25,10 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
flex: 1; flex: 1;
background: ${token.colorBgContainer}; background: ${token.colorBgContainer};
` `
const emptyForm = css`
backdrop-filter: ${token.backdropFilter};
color: red;
`
const form = css` const form = css`
display: flex; display: flex;
@ -45,12 +51,12 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.actions{
.actions {
display: none; display: none;
padding: 0 10px; padding: 0 10px;
} }
&:hover .actions{ {
&:hover .actions { {
display: flex; display: flex;
} }
@ -62,6 +68,7 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
return { return {
container: cx(prefix), container: cx(prefix),
box, box,
emptyForm,
tree, tree,
form, form,
treeNode, treeNode,

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