李金
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 { |
|||
|
|||
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 { |
|||
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 { IPage, IPageResult, MenuItem } from '@/types' |
|||
import { IApiResult, IPage, IPageResult, MenuItem } from '@/types' |
|||
import { IMenu } from '@/types/menus' |
|||
import { AxiosResponse } from 'axios' |
|||
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>({}) |
|||
|
|||
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((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 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' |
|||
|
|||
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