李金
7 months ago
12 changed files with 588 additions and 405 deletions
-
38src/components/glass/index.tsx
-
29src/components/glass/style.ts
-
2src/i18n.ts
-
124src/locales/lang/en-US.ts
-
31src/locales/lang/pages/system/menus/en-US.ts
-
50src/locales/lang/pages/system/menus/zh-CN.ts
-
109src/locales/lang/zh-CN.ts
-
143src/pages/system/menus/components/MenuTree.tsx
-
220src/pages/system/menus/index.tsx
-
103src/pages/system/menus/store.ts
-
141src/pages/system/menus/style.ts
-
3src/theme/themes/token.ts
@ -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 |
@ -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, |
||||
|
} |
||||
|
}) |
@ -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' |
||||
} |
} |
@ -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: '按钮' |
||||
} |
} |
@ -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() |
||||
} |
} |
||||
|
} |
||||
}) |
}) |
@ -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, |
||||
|
} |
||||
}) |
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue