dark
6 months ago
19 changed files with 815 additions and 569 deletions
-
95src/components/table/Table.tsx
-
1src/components/table/index.ts
-
23src/components/table/style.ts
-
4src/hooks/useScrollStyle.ts
-
3src/layout/ListPageLayout.tsx
-
53src/layout/TwoColPageLayout.tsx
-
36src/layout/style.ts
-
12src/pages/cms/collect/index.tsx
-
12src/pages/cms/video/index.tsx
-
12src/pages/cms/video_cloud/index.tsx
-
12src/pages/cms/video_magnet/index.tsx
-
258src/pages/system/logs/login/index.tsx
-
364src/pages/system/menus/index.tsx
-
15src/pages/system/menus/style.ts
-
7src/pages/system/roles/index.tsx
-
445src/pages/system/users/index.tsx
-
9src/pages/videos/list/index.tsx
-
11src/pages/websites/ssl/index.tsx
-
12src/utils/dom.ts
@ -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 |
@ -0,0 +1 @@ |
|||||
|
export * from './Table.tsx' |
@ -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), |
||||
|
} |
||||
|
}) |
@ -1,144 +1,156 @@ |
|||||
import Switch from '@/components/switch' |
import Switch from '@/components/switch' |
||||
import { |
import { |
||||
ActionType, |
|
||||
PageContainer, |
|
||||
ProColumns, |
|
||||
ProTable, |
|
||||
|
ActionType, |
||||
|
ProColumns, |
||||
} from '@ant-design/pro-components' |
} from '@ant-design/pro-components' |
||||
import { useStyle } from './style.ts' |
import { useStyle } from './style.ts' |
||||
import { memo, useMemo, useRef, useState } from 'react' |
import { memo, useMemo, useRef, useState } from 'react' |
||||
import { useAtom, useAtomValue } from 'jotai' |
import { useAtom, useAtomValue } from 'jotai' |
||||
|
import { Table as ProTable } from '@/components/table' |
||||
|
|
||||
import { useTranslation } from '@/i18n.ts' |
import { useTranslation } from '@/i18n.ts' |
||||
import { Button, Space, Table, Popconfirm } from 'antd' |
import { Button, Space, Table, Popconfirm } from 'antd' |
||||
import { deleteLoginLogAtom, loginLogPageAtom, loginLogsAtom, loginLogSearchAtom } from '@/store/system/logs.ts' |
import { deleteLoginLogAtom, loginLogPageAtom, loginLogsAtom, loginLogSearchAtom } from '@/store/system/logs.ts' |
||||
|
import ListPageLayout from '@/layout/ListPageLayout.tsx' |
||||
|
|
||||
const LoginLog = memo(() => { |
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 |
export default LoginLog |
@ -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 |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue