Browse Source

美化表格滚动条

main
dark 6 months ago
parent
commit
cc2c8f5111
  1. 95
      src/components/table/Table.tsx
  2. 1
      src/components/table/index.ts
  3. 23
      src/components/table/style.ts
  4. 4
      src/hooks/useScrollStyle.ts
  5. 3
      src/layout/ListPageLayout.tsx
  6. 53
      src/layout/TwoColPageLayout.tsx
  7. 36
      src/layout/style.ts
  8. 12
      src/pages/cms/collect/index.tsx
  9. 12
      src/pages/cms/video/index.tsx
  10. 12
      src/pages/cms/video_cloud/index.tsx
  11. 12
      src/pages/cms/video_magnet/index.tsx
  12. 258
      src/pages/system/logs/login/index.tsx
  13. 364
      src/pages/system/menus/index.tsx
  14. 15
      src/pages/system/menus/style.ts
  15. 7
      src/pages/system/roles/index.tsx
  16. 445
      src/pages/system/users/index.tsx
  17. 9
      src/pages/videos/list/index.tsx
  18. 11
      src/pages/websites/ssl/index.tsx
  19. 12
      src/utils/dom.ts

95
src/components/table/Table.tsx

@ -0,0 +1,95 @@
import { ProTable, ProTableProps, ProCard } from '@ant-design/pro-components'
import React, { useEffect, useRef, useState } from 'react'
import { useStyle } from './style'
export interface TableProps<T = any, D = any> extends ProTableProps<T, D> {
}
export const Table = <T extends Record<string, any> = any, D = any>(props: TableProps<T, D>) => {
const { styles } = useStyle()
const toolbarRef = useRef<HTMLDivElement | undefined | null>(undefined)
const alterRef = useRef<HTMLDivElement | undefined | null>(undefined)
const [ toolbarHeight, setHeight ] = useState<number>(65)
const [ alterHeight, setAlterHeight ] = useState<number>(0)
const scroll = props.scroll ? {
...props.scroll,
y: props.scroll.y ?? ` calc(100vh - ${toolbarHeight + 200}px)`
} : undefined
useEffect(() => {
if (!toolbarRef.current) return
setHeight(toolbarRef.current?.offsetHeight ?? 65)
//监听toolbarRef offsetHeight
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (entry.target === toolbarRef.current) {
setHeight(entry.contentRect.height)
}
}
})
observer.observe(toolbarRef.current!)
return () => {
observer.disconnect()
}
}, [ toolbarRef.current ])
useEffect(() => {
if (!alterRef.current) return
setHeight(alterRef.current?.offsetHeight ?? 65)
//监听toolbarRef offsetHeight
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (entry.target === alterRef.current && entry.contentRect.height > 0) {
setAlterHeight(entry.contentRect.height + 16)
}
}
})
observer.observe(alterRef.current!)
return () => {
observer.disconnect()
}
}, [ alterRef.current ])
const style = {
'--toolbar-height': `${toolbarHeight}px`,
'--alter-height': `${alterHeight}px`,
} as React.CSSProperties
// @ts-ignore fix dataItem
return <ProTable<T>
{...props}
className={styles.container}
style={style}
tableRender={(props, _dom, domList) => {
return <ProCard
ghost={props.ghost}
{...props.cardProps}
bodyStyle={{
paddingBlockStart: 0,
}}
>
<div ref={toolbarRef as any}>{domList.toolbar}</div>
<div ref={alterRef as any}>{domList.alert}</div>
<>{domList.table}</>
</ProCard>
}}
scroll={scroll}
></ProTable>
}
export default Table

1
src/components/table/index.ts

@ -0,0 +1 @@
export * from './Table.tsx'

23
src/components/table/style.ts

@ -0,0 +1,23 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-my-table`
const container = css`
--toolbar-height: 65px;
--alter-height: 0px;
--padding: 37px;
--table-body-height: calc(var(--toolbar-height, 65px) + var(--alter-height, 0px) + var(--header-height, 56px) + var(--padding, 20px) * 4);
.ant-table-body {
overflow: auto scroll;
max-height: calc(100vh - var(--table-body-height)) !important;
height: calc(100vh - var(--table-body-height)) !important;
}
`
return {
container: cx(prefix, props?.className, container),
}
})

4
src/hooks/useScrollStyle.ts

@ -64,11 +64,9 @@ export const useScrollStyle = () => {
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
background-attachment: local, local, scroll, scroll;
${scrollbar.toString()}
${scrollbar}
`
return {
scrollbarBackground,
scrollbar,

3
src/layout/ListPageLayout.tsx

@ -18,8 +18,9 @@ const ListPageLayout: React.FC<IListPageLayoutProps> = (
<>
<PageContainer
breadcrumbRender={false} title={false}
className={styles.container}
className={cx(styles.container, styles.pageCard)}
{...props}
>
<div className={cx({
[styles.authHeight]: authHeight

53
src/layout/TwoColPageLayout.tsx

@ -5,35 +5,36 @@ import { DraggablePanel, DraggablePanelProps } from '@/components/draggable-pane
import { useStyle } from './style'
interface ITreePageLayoutProps {
children?: React.ReactNode
draggableProps?: DraggablePanelProps
pageProps?: PageContainerProps
leftPanel?: React.ReactNode
children?: React.ReactNode
draggableProps?: DraggablePanelProps
pageProps?: PageContainerProps
leftPanel?: React.ReactNode
}
export const TwoColPageLayout: React.FC<ITreePageLayoutProps> = (props) => {
const { styles } = useStyle({ className: 'two-col' })
return (
<PageContainer
breadcrumbRender={false} title={false} className={styles.container}
{...props.pageProps}
>
<Flexbox horizontal className={styles.authHeight}>
<DraggablePanel expandable={false}
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
style={{ position: 'relative' }}
{...props.draggableProps}
>
{props.leftPanel}
</DraggablePanel>
<Flexbox className={styles.box}>
{props.children}
</Flexbox>
</Flexbox>
</PageContainer>
)
const { styles, cx } = useStyle({ className: 'two-col' })
return (
<PageContainer
breadcrumbRender={false} title={false}
className={cx(styles.container, styles.pageCard)}
{...props.pageProps}
>
<Flexbox horizontal className={styles.authHeight}>
<DraggablePanel expandable={false}
placement="left"
defaultSize={{ width: 300 }}
maxWidth={500}
style={{ position: 'relative' }}
{...props.draggableProps}
>
{props.leftPanel}
</DraggablePanel>
<Flexbox className={styles.box}>
{props.children}
</Flexbox>
</Flexbox>
</PageContainer>
)
}
export default TwoColPageLayout

36
src/layout/style.ts

@ -7,8 +7,6 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
const { scrollbar } = useScrollStyle()
console.log(scrollbar)
const container = {
[prefix]: css`
@ -33,9 +31,36 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
${scrollbar}
}
.ant-pro-layout-content{
padding: 20px;
}
.ant-pro-layout .ant-pro-layout-content-has-page-container{
padding: 0;
}
.ant-pro-page-container-children-container{
padding-inline: 20px;
padding-block-end: 20px;
}
.ant-page-header-no-children {
height: 20px;
}
.ant-pro-card{
height: calc(100vh - 100px)!important;
min-height: calc(100vh - 100px)!important;
}
`,
}
const pageCard = css`
`
const pageContext = css`
box-shadow: ${token.boxShadowSecondary};
`
@ -91,8 +116,12 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
background: ${token.colorBgContainer};
`
const authHeight = css`
min-height: calc(100vh - 122px);
background-color: ${token.colorBgContainer};
.ant-pro-draggable-panel-fixed{
height: calc(100vh - 100px)!important;
min-height: calc(100vh - 100px)!important;
}
`
return {
@ -103,6 +132,7 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
sideMenu,
mySider,
mySiderMenu,
pageCard,
}
})

12
src/pages/cms/collect/index.tsx

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import { useTranslation } from '@/i18n.ts'
import { useAtom, useAtomValue } from 'jotai'
import {
@ -14,6 +14,7 @@ import { Button, Form, Popconfirm } from 'antd'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import Switch from '@/components/switch'
import Action from '@/components/action/Action.tsx'
import { Table as ProTable } from '@/components/table'
const i18nPrefix = 'cms.collect'
@ -269,7 +270,7 @@ const Collect = () => {
]
}}
scroll={{
x: 2000,
x: 2000, y: 'calc(100vh - 265px)'
}}
loading={isLoading || isFetching}
dataSource={data?.rows ?? []}
@ -284,6 +285,13 @@ const Collect = () => {
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {

12
src/pages/cms/video/index.tsx

@ -8,7 +8,7 @@ import {
import { useEffect, useMemo, useState } from 'react'
import Switch from '@/components/switch'
import Action from '@/components/action/Action.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import TagPro from '@/components/tag-pro/TagPro.tsx'
import { categoriesAtom, categoryByIdAtom, categoryIdAtom } from '@/store/cms/category.ts'
@ -17,6 +17,7 @@ import { FilterOutlined } from '@ant-design/icons'
import TagValue from '@/components/tag-value/TagValue.tsx'
import dayjs from 'dayjs'
import { useStyle } from './style.ts'
import { Table as ProTable } from '@/components/table'
const i18nPrefix = 'cms.video'
@ -677,7 +678,7 @@ const Video = () => {
]
}}
scroll={{
x: 3800, y: 'calc(100vh - 290px)'
x: 3800, y: 'calc(100vh - 265px)'
}}
onRow={(record) => {
return {
@ -703,6 +704,13 @@ const Video = () => {
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {

12
src/pages/cms/video_cloud/index.tsx

@ -4,7 +4,7 @@ import { useAtom, useAtomValue } from 'jotai'
import { useEffect, useMemo, useState } from 'react'
import Switch from '@/components/switch'
import Action from '@/components/action/Action.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import {
deleteVideoCloudAtom,
@ -22,6 +22,7 @@ import { videoTypes } from '@/store/cms/video.ts'
import { useStyle} from './style'
import { getValueCount } from '@/utils'
import { FilterOutlined } from '@ant-design/icons'
import { Table as ProTable } from '@/components/table'
const i18nPrefix = 'cms.videoCloud'
@ -682,7 +683,7 @@ const VideoCloud = () => {
}}
scroll={{
x: 3800, y: 'calc(100vh - 290px)'
x: 3800, y: 'calc(100vh - 265px)'
}}
onRow={(record) => {
return {
@ -708,6 +709,13 @@ const VideoCloud = () => {
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {

12
src/pages/cms/video_magnet/index.tsx

@ -4,7 +4,7 @@ import { useAtom, useAtomValue } from 'jotai'
import { useEffect, useMemo, useState } from 'react'
import Switch from '@/components/switch'
import Action from '@/components/action/Action.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { videoTypes } from '@/store/cms/video.ts'
import {
@ -21,6 +21,7 @@ import TagValue from '@/components/tag-value/TagValue.tsx'
import dayjs from 'dayjs'
import TagPro from '@/components/tag-pro/TagPro.tsx'
import { useStyle } from './style'
import { Table as ProTable } from '@/components/table'
const i18nPrefix = 'cms.videoMagnet'
@ -681,7 +682,7 @@ const VideoMagnet = () => {
]
}}
scroll={{
x: 3800, y: 'calc(100vh - 290px)'
x: 3800, y: 'calc(100vh - 265px)'
}}
onRow={(record) => {
return {
@ -707,6 +708,13 @@ const VideoMagnet = () => {
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {

258
src/pages/system/logs/login/index.tsx

@ -1,144 +1,156 @@
import Switch from '@/components/switch'
import {
ActionType,
PageContainer,
ProColumns,
ProTable,
ActionType,
ProColumns,
} from '@ant-design/pro-components'
import { useStyle } from './style.ts'
import { memo, useMemo, useRef, useState } from 'react'
import { useAtom, useAtomValue } from 'jotai'
import { Table as ProTable } from '@/components/table'
import { useTranslation } from '@/i18n.ts'
import { Button, Space, Table, Popconfirm } from 'antd'
import { deleteLoginLogAtom, loginLogPageAtom, loginLogsAtom, loginLogSearchAtom } from '@/store/system/logs.ts'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
const LoginLog = memo(() => {
const { t } = useTranslation()
const { styles } = useStyle()
const actionRef = useRef<ActionType>()
const [ page, setPage ] = useAtom(loginLogPageAtom)
const [ search, setSearch ] = useAtom(loginLogSearchAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(loginLogsAtom)
const { mutate: deleteLog, isPending: isDeleting } = useAtomValue(deleteLoginLogAtom)
const [ ids, setIds ] = useState<number[]>([])
const { t } = useTranslation()
const { styles } = useStyle()
const actionRef = useRef<ActionType>()
const [ page, setPage ] = useAtom(loginLogPageAtom)
const [ search, setSearch ] = useAtom(loginLogSearchAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(loginLogsAtom)
const { mutate: deleteLog, isPending: isDeleting } = useAtomValue(deleteLoginLogAtom)
const [ ids, setIds ] = useState<number[]>([])
const columns = useMemo(() => {
const columns = useMemo(() => {
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.logs.login.columns.username', '登录帐号'), dataIndex: 'username', valueType: 'text',
},
{
title: t('system.logs.login.columns.ip', '登录IP'), dataIndex: 'ip', valueType: 'text',
},
{
title: t('system.logs.login.columns.user_agent', '浏览器'), dataIndex: 'user_agent', valueType: 'text',
},
{
title: t('system.logs.login.columns.status', '状态'), dataIndex: 'status', valueType: 'switch',
render: (_, record) => {
return <Switch value={record.status} size={'small'}/>
},
},
{
title: t('system.logs.login.columns.created_at', '登录时间'),
dataIndex: 'created_at',
valueType: 'dateTime',
},
{
title: t('system.logs.login.columns.option','操作'), valueType: 'option',
key: 'option',
render: (_, record) => [
<Popconfirm
key={'del_confirm'}
onConfirm={() => {
deleteLog([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.logs.login.columns.username', '登录帐号'), dataIndex: 'username', valueType: 'text',
},
{
title: t('system.logs.login.columns.ip', '登录IP'), dataIndex: 'ip', valueType: 'text',
},
{
title: t('system.logs.login.columns.user_agent', '浏览器'), dataIndex: 'user_agent', valueType: 'text',
width: 500,
ellipsis: true,
},
{
title: t('system.logs.login.columns.status', '状态'), dataIndex: 'status', valueType: 'switch',
width: 80,
render: (_, record) => {
return <Switch value={record.status} size={'small'}/>
},
},
{
title: t('system.logs.login.columns.created_at', '登录时间'),
dataIndex: 'created_at',
valueType: 'dateTime',
width: 180,
},
{
title: t('system.logs.login.columns.option', '操作'), valueType: 'option',
key: 'option',
render: (_, record) => [
<Popconfirm
key={'del_confirm'}
onConfirm={() => {
deleteLog([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}>
<div className={styles.authHeight}>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.logs.login.title', '登录日志')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
rowSelection={{
onChange: (selectedRowKeys) => {
setIds(selectedRowKeys as number[])
},
selectedRowKeys: ids,
selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ],
}}
tableAlertOptionRender={() => {
return (
<Space size={16}>
<Popconfirm
onConfirm={() => {
deleteLog(ids)
}}
title={t('message.batchDelete')}>
<Button type={'link'}
loading={isDeleting}>{t('actions.batchDel')}</Button>
</Popconfirm>
</Space>
)
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('system.logs.login.search.placeholder','请输入用户名查询')
},
actions: []
}}
pagination={{
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onChange: (page) => {
return (
<ListPageLayout className={styles.container}>
<>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.logs.login.title', '登录日志')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
rowSelection={{
onChange: (selectedRowKeys) => {
setIds(selectedRowKeys as number[])
},
selectedRowKeys: ids,
selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ],
}}
scroll={{
setPage((prev) => {
return { ...prev, page }
})
}
}}
/>
</div>
}}
tableAlertOptionRender={() => {
return (
<Space size={16}>
<Popconfirm
onConfirm={() => {
deleteLog(ids)
}}
title={t('message.batchDelete')}>
<Button type={'link'}
loading={isDeleting}>{t('actions.batchDel')}</Button>
</Popconfirm>
</Space>
)
}}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('system.logs.login.search.placeholder', '请输入用户名查询')
},
actions: []
}}
pagination={{
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onShowSizeChange: (current: number, size: number) => {
setPage({
...page,
pageSize: size,
page: current
})
},
onChange: (page) => {
</PageContainer>
)
setPage((prev) => {
return { ...prev, page }
})
}
}}
/>
</>
</ListPageLayout>
)
})
export default LoginLog

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

@ -16,192 +16,192 @@ import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx'
const Menus = () => {
const { styles, cx } = useStyle()
const { t } = useTranslation()
const [ form ] = Form.useForm()
const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom)
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined)
useEffect(() => {
if (isError) {
notification.error({
message: t('message.error', '错误'),
description: (error as any).message ?? t('message.saveFail', '保存失败'),
})
}
}, [ isError ])
useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) {
menuInputRef.current.focus()
}
}, [ currentMenu ])
return (
<TwoColPageLayout
leftPanel={<>
<ProCard title={t('system.menus.title', '菜单')}
extra={
<>
<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: currentMenu.id ?? 0,
}
setMenuData(menu)
form.setFieldsValue(menu)
}}
>{t('actions.news')}</Button>
</div>
const { styles, cx } = useStyle()
const { t } = useTranslation()
const [ form ] = Form.useForm()
const { mutate, isPending, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom)
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined)
useEffect(() => {
if (isError) {
notification.error({
message: t('message.error', '错误'),
description: (error as any).message ?? t('message.saveFail', '保存失败'),
})
}
}, [ isError ])
useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) {
menuInputRef.current.focus()
}
}, [ currentMenu ])
return (
<TwoColPageLayout
leftPanel={<>
<ProCard title={t('system.menus.title', '菜单')}
extra={
<>
<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: currentMenu.id ?? 0,
}
setMenuData(menu)
form.setFieldsValue(menu)
}}
>{t('actions.news')}</Button>
</div>
</>}
>
<Glass
enabled={currentMenu.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.menus.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
>
<Glass
enabled={currentMenu.id === undefined}
description={<>
<Alert
message={t('message.infoTitle', '提示')}
description={t('system.menus.form.empty', '请从左侧选择一行数据操作')}
type="info"
/>
</>}
<Form form={form}
initialValues={currentMenu!}
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
wrapperCol={{ flex: 1 }}
colon={false}
className={cx(styles.form, {
[styles.emptyForm]: currentMenu.id === undefined
})}
>
<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]: currentMenu.id === undefined
})}
<div className={'ant-pro-card-body-content'}>
<Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/>
</Form.Item>
<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
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
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'}
shouldUpdate={(prev: any, next: any) => {
return prev.icon !== next.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')}
>
<ProCard title={t('system.menus.setting', '配置')}
className={styles.formSetting}
>
<Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/>
</Form.Item>
<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
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
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'}
shouldUpdate={(prev: any, next: any) => {
return prev.icon !== next.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>
</TwoColPageLayout>
)
<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>
</div>
</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>
</TwoColPageLayout>
)
}
export default Menus

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

@ -1,7 +1,9 @@
import { createStyles } from '@/theme'
import { useScrollStyle } from '@/hooks/useScrollStyle'
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
const prefix = `${prefixCls}-${token?.proPrefix}-menu-page`
const { scrollbarBackground } = useScrollStyle()
const box = css`
flex: 1;
@ -18,12 +20,21 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
const formSetting = css`
flex: 1;
.ant-pro-card-body {
overflow: auto;
${scrollbarBackground}
.ant-pro-card-body-content {
padding-inline: 10px 0;
}
}
`
const formButtons = css`
width: 500px;
.ant-pro-card-body {
overflow: hidden;
}
`
const tree = css`

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

@ -203,6 +203,13 @@ const Roles = memo(() => {
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onShowSizeChange: (current: number, size: number) => {
setPage({
...page,
pageSize: size,
page: current
})
},
onChange: (page) => {
setPage((prev) => {

445
src/pages/system/users/index.tsx

@ -1,11 +1,11 @@
import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx'
import {
ActionType,
BetaSchemaForm,
ProCard,
ProColumns,
ProFormColumnsType,
ProTable
ActionType,
BetaSchemaForm,
ProCard,
ProColumns,
ProFormColumnsType,
ProTable
} from '@ant-design/pro-components'
import { Button, Form, Popconfirm } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
@ -13,12 +13,12 @@ import { useTranslation } from '@/i18n.ts'
import DepartmentTree from '@/components/department-tree/DepartmentTree.tsx'
import { useAtom, useAtomValue } from 'jotai'
import {
deleteUserAtom, resetPasswordAtom,
saveOrUpdateUserAtom,
userListAtom,
userPageAtom,
userSearchAtom,
userSelectedAtom
deleteUserAtom, resetPasswordAtom,
saveOrUpdateUserAtom,
userListAtom,
userPageAtom,
userSearchAtom,
userSelectedAtom
} from '@/store/system/user.ts'
import { useMemo, useRef, useState } from 'react'
import Switch from '@/components/switch'
@ -27,220 +27,227 @@ import RolePicker from '@/components/role-picker/RolePicker.tsx'
const Users = () => {
const { t } = useTranslation()
const [ page, setPage ] = useAtom(userPageAtom)
const [ search, setSearch ] = useAtom(userSearchAtom)
const [ , setCurrent ] = useAtom(userSelectedAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(userListAtom)
const { mutate: deleteUser, isPending, } = useAtomValue(deleteUserAtom)
const { mutate: resetPass, isPending: isResetting } = useAtomValue(resetPasswordAtom)
const [ form ] = Form.useForm()
const actionRef = useRef<ActionType>()
const [ open, setOpen ] = useState(false)
const { t } = useTranslation()
const [ page, setPage ] = useAtom(userPageAtom)
const [ search, setSearch ] = useAtom(userSearchAtom)
const [ , setCurrent ] = useAtom(userSelectedAtom)
const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateUserAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(userListAtom)
const { mutate: deleteUser, isPending, } = useAtomValue(deleteUserAtom)
const { mutate: resetPass, isPending: isResetting } = useAtomValue(resetPasswordAtom)
const [ form ] = Form.useForm()
const actionRef = useRef<ActionType>()
const [ open, setOpen ] = useState(false)
const columns = useMemo(() => {
const columns = useMemo(() => {
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.users.columns.name', '姓名'), dataIndex: 'name', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.users.columns.username', '用户名'), dataIndex: 'username', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.users.columns.roles', '角色'), dataIndex: 'roles', valueType: 'select',
render: (_, record) => {
return record.roles?.map(role => role.name).join(',') || ''
},
renderFormItem: (item, config) => {
const { mode, ...other } = config as any
return <RolePicker {...item.fieldProps as any}
view={mode !== 'edit'} {...other}/>
},
return [
{
title: 'id', dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: {
hidden: true
}
},
{
title: t('system.users.columns.name', '姓名'), dataIndex: 'name', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.users.columns.username', '用户名'), dataIndex: 'username', valueType: 'text',
formItemProps: {
rules: [ { required: true, message: t('message.required') } ]
}
},
{
title: t('system.users.columns.roles', '角色'), dataIndex: 'roles', valueType: 'select',
render: (_, record) => {
return record.roles?.map(role => role.name).join(',') || ''
},
renderFormItem: (item, config) => {
const { mode, ...other } = config as any
return <RolePicker {...item.fieldProps as any}
view={mode !== 'edit'} {...other}/>
},
},
{
title: t('system.users.columns.dept_id', '所属部门'), dataIndex: 'dept_id',
render: (_, record) => {
return record.dept_name || ''
},
renderFormItem: (item, config) => {
return <DepartmentCascader {...item.fieldProps as any} {...config} />
}
},
{
title: t('system.users.columns.status', '状态'), dataIndex: 'status', valueType: 'switch',
render: (_, record) => {
return <Switch value={record.status} size={'small'}/>
},
renderFormItem: (item, config) => {
return <Switch {...item.fieldProps as any} {...config}/>
},
},
{
title: t('system.users.columns.update_at', '更新时间'),
hideInTable: true,
hideInSearch: true,
hideInForm: true,
dataIndex: 'update_at',
valueType: 'dateTime',
},
{
title: t('system.users.columns.option', '操作'), valueType: 'option',
key: 'option',
render: (_, record) => [
<a key="editable"
onClick={() => {
setCurrent(record)
setOpen(true)
form.setFieldsValue(record)
}}
>
{t('actions.edit', '编辑')}
</a>,
<Popconfirm
disabled={isResetting}
key={'reset_password_confirm'}
onConfirm={() => {
resetPass(record.id)
}}
title={
<span dangerouslySetInnerHTML={{
__html: t('message.resetPassConfirm', '密码会重置为{{password}}操作不可回滚,是否继续?', {
password: '<span style="color:red;">kk123456</span>'
})
}}></span>
}>
<a key="del">
{t('actions.resetPass', '重置密码')}
</a>
</Popconfirm>,
<Popconfirm
key={'del_confirm'}
disabled={isPending}
onConfirm={() => {
deleteUser([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
},
{
title: t('system.users.columns.dept_id', '所属部门'), dataIndex: 'dept_id',
render: (_, record) => {
return record.dept_name || ''
},
renderFormItem: (item, config) => {
return <DepartmentCascader {...item.fieldProps as any} {...config} />
}
},
{
title: t('system.users.columns.status', '状态'), dataIndex: 'status', valueType: 'switch',
render: (_, record) => {
return <Switch value={record.status} size={'small'}/>
},
renderFormItem: (item, config) => {
return <Switch {...item.fieldProps as any} {...config}/>
},
},
{
title: t('system.users.columns.update_at', '更新时间'),
hideInTable: true,
hideInSearch: true,
hideInForm: true,
dataIndex: 'update_at',
valueType: 'dateTime',
},
{
title: t('system.users.columns.option', '操作'), valueType: 'option',
key: 'option',
render: (_, record) => [
<a key="editable"
onClick={() => {
setCurrent(record)
setOpen(true)
form.setFieldsValue(record)
}}
>
{t('actions.edit', '编辑')}
</a>,
<Popconfirm
disabled={isResetting}
key={'reset_password_confirm'}
onConfirm={() => {
resetPass(record.id)
}}
title={
<span dangerouslySetInnerHTML={{
__html: t('message.resetPassConfirm', '密码会重置为{{password}}操作不可回滚,是否继续?', {
password: '<span style="color:red;">kk123456</span>'
})
}}></span>
}>
<a key="del">
{t('actions.resetPass', '重置密码')}
</a>
</Popconfirm>,
<Popconfirm
key={'del_confirm'}
disabled={isPending}
onConfirm={() => {
deleteUser([ record.id ])
}}
title={t('message.deleteConfirm')}>
<a key="del">
{t('actions.delete', '删除')}
</a>
</Popconfirm>
,
],
},
] as ProColumns[]
}, [])
return (
<TwoColPageLayout leftPanel={
<>
<ProCard title={t('system.users.title', '部门')}>
<DepartmentTree
root={true}
fieldNames={{
title: 'name',
key: 'id',
}}
onItemClick={(item) => {
setSearch({
dept_id: item.id,
})
}}
/>
</ProCard>
</>
}>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.users.title', '用户管理')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
options={{
reload: () => {
refetch()
},
return (
<TwoColPageLayout leftPanel={
<>
<ProCard title={t('system.users.title', '部门')}>
<DepartmentTree
root={true}
fieldNames={{
title: 'name',
key: 'id',
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('system.users.search.placeholder', '输入用户名')
},
actions: [
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
dept_id: search.dept_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 }
})
}
onItemClick={(item) => {
setSearch({
dept_id: item.id,
})
}}
/>
<BetaSchemaForm
width={600}
form={form}
layout={'horizontal'}
title={t(`system.users.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '用户编辑' : '用户添加')}
colProps={{ span: 24 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
layoutType={'ModalForm'}
open={open}
modalProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</TwoColPageLayout>
)
</ProCard>
</>
}>
<ProTable
rowKey={'id'}
actionRef={actionRef}
headerTitle={t('system.users.title', '用户管理')}
columns={columns}
loading={isLoading || isFetching}
dataSource={data?.rows}
search={false}
options={{
reload: () => {
refetch()
},
}}
toolbar={{
search: {
loading: isFetching && !!search.key,
onSearch: (value: string) => {
setSearch({ key: value })
},
placeholder: t('system.users.search.placeholder', '输入用户名')
},
actions: [
<Button
key="button"
icon={<PlusOutlined/>}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
dept_id: search.dept_id ?? 0,
})
setOpen(true)
}}
type="primary"
>
{t('actions.add', '添加')}
</Button>,
]
}}
pagination={{
total: data?.total,
current: page.page,
pageSize: page.pageSize,
onShowSizeChange: (current: number, size: number) => {
setPage({
...page,
pageSize: size,
page: current
})
},
onChange: (page) => {
setPage((prev) => {
return { ...prev, page }
})
}
}}
/>
<BetaSchemaForm
width={600}
form={form}
layout={'horizontal'}
title={t(`system.users.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '用户编辑' : '用户添加')}
colProps={{ span: 24 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
layoutType={'ModalForm'}
open={open}
modalProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
// console.log('values', values)
saveOrUpdate(values)
return isSuccess
}}
columns={columns as ProFormColumnsType[]}/>
</TwoColPageLayout>
)
}

9
src/pages/videos/list/index.tsx

@ -14,7 +14,6 @@ import {
ProColumns,
ProFormColumnsType,
ProFormUploadButton,
ProTable
} from '@ant-design/pro-components'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { categoryByIdAtom, categoryIdAtom } from '@/store/videos/category.ts'
@ -23,6 +22,7 @@ import TagValue from '@/components/tag-value/TagValue.tsx'
import { useStyle } from './style'
import { ExportOutlined, FilterOutlined } from '@ant-design/icons'
import { getValueCount } from '@/utils'
import { Table as ProTable } from '@/components/table'
const i18nPrefix = 'videos.list'
@ -694,6 +694,13 @@ const Video = () => {
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {

11
src/pages/websites/ssl/index.tsx

@ -10,7 +10,7 @@ import {
sslSearchAtom, uploadSslAtom
} from '@/store/websites/ssl.ts'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm, Space } from 'antd'
@ -31,6 +31,7 @@ import { detailAtom } from './components/store.ts'
import Upload from './components/Upload.tsx'
import { FormInstance } from 'antd/lib'
import Download from '@/components/download/Download.tsx'
import { Table as ProTable } from '@/components/table'
const SSL = () => {
@ -371,10 +372,18 @@ const SSL = () => {
</Button>,
]
}}
scroll={{}}
pagination={{
pageSize: page?.pageSize ?? 10,
total: data?.total ?? 0,
current: page?.page ?? 1,
onShowSizeChange: (current: number, size: number) => {
setPage({
...page,
pageSize: size,
page: current
})
},
onChange: (page, pageSize) => {
setPage(prev => ({
...prev,

12
src/utils/dom.ts

@ -0,0 +1,12 @@
export function getElementRealHeight(el: Element) {
const style = window.getComputedStyle(el)
const paddingTop = parseInt(style.paddingTop)
const paddingBottom = parseInt(style.paddingBottom)
const marginTop = parseInt(style.marginTop)
const marginBottom = parseInt(style.marginBottom)
const clientHeight = el.clientHeight
return clientHeight + paddingTop + paddingBottom + marginTop + marginBottom
}
Loading…
Cancel
Save