mirror of https://github.com/openspug/spug
update
parent
89d8f30570
commit
9c0b41ba51
|
@ -1,8 +1,9 @@
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
Django >= 4.2.0, < 4.3.0
|
Django >= 4.2.0, < 4.3.0
|
||||||
paramiko==3.3.1
|
paramiko==3.4.0
|
||||||
channels >= 4.0.0, < 5.0.0
|
channels >= 4.0.0, < 5.0.0
|
||||||
channels-redis >= 4.1.0, < 5.0.0
|
channels-redis >= 4.1.0, < 5.0.0
|
||||||
|
django_redis >= 5.4.0, < 6.0.0
|
||||||
asgiref==3.7.2
|
asgiref==3.7.2
|
||||||
requests >= 2.31.0, < 3.0.0
|
requests >= 2.31.0, < 3.0.0
|
||||||
python-ldap==3.4.3
|
python-ldap==3.4.3
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
"antd": "^5.12.4",
|
"antd": "^5.12.4",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"i18next": "^23.7.7",
|
"i18next": "^23.7.7",
|
||||||
|
"mobx": "^6.12.0",
|
||||||
|
"mobx-react-lite": "^4.0.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^13.5.0",
|
"react-i18next": "^13.5.0",
|
||||||
|
|
|
@ -11,12 +11,12 @@ function Setting(props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newColumns = []
|
const newColumns = []
|
||||||
for (const item of columns) {
|
for (const item of columns) {
|
||||||
if (state[item.key] ?? !item.hidden) {
|
if (state[item.title] ?? !item.hidden) {
|
||||||
newColumns.push(item)
|
newColumns.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCols(newColumns)
|
setCols(newColumns)
|
||||||
}, [state]);
|
}, [columns, state]);
|
||||||
|
|
||||||
function handleChange(e) {
|
function handleChange(e) {
|
||||||
const { value, checked } = e.target
|
const { value, checked } = e.target
|
||||||
|
@ -27,7 +27,7 @@ function Setting(props) {
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setState({})
|
setState({})
|
||||||
app.updateStable(skey, {})
|
app.updateStable(skey, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -43,9 +43,9 @@ function Setting(props) {
|
||||||
<Flex vertical gap="small">
|
<Flex vertical gap="small">
|
||||||
{columns.map((item, index) => (
|
{columns.map((item, index) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={item.key}
|
value={item.title}
|
||||||
key={index}
|
key={index}
|
||||||
checked={state[item.key] ?? !item.hidden}
|
checked={state[item.title] ?? !item.hidden}
|
||||||
onChange={handleChange}>
|
onChange={handleChange}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
|
@ -1,18 +1,87 @@
|
||||||
import {useRef, useState} from 'react'
|
import { useRef, useState, useEffect } from 'react'
|
||||||
import {Card, Table, Flex, Divider} from 'antd'
|
import { Card, Table, Flex, Divider, Checkbox, Button, Input, Tag } from 'antd'
|
||||||
import { IoExpand, IoContract, IoReloadOutline } from 'react-icons/io5'
|
import { IoExpand, IoContract, IoReloadOutline } from 'react-icons/io5'
|
||||||
import {clsNames} from '@/libs'
|
import { useImmer } from 'use-immer'
|
||||||
|
import { clsNames, includes } from '@/libs'
|
||||||
import Setting from './Setting.jsx'
|
import Setting from './Setting.jsx'
|
||||||
import css from './index.module.scss'
|
import css from './index.module.scss'
|
||||||
|
|
||||||
function Stable(props) {
|
function STable(props) {
|
||||||
const { skey, loading, columns, dataSource, actions, pagination } = props
|
const { skey, loading, columns, dataSource, actions, pagination } = props
|
||||||
const ref = useRef();
|
const ref = useRef()
|
||||||
|
const sMap = useRef({})
|
||||||
|
const [sColumns, setSColumns] = useState([])
|
||||||
const [cols, setCols] = useState([])
|
const [cols, setCols] = useState([])
|
||||||
const [isFull, setIsFull] = useState(false)
|
const [isFull, setIsFull] = useState(false)
|
||||||
|
const [filters, updateFilters] = useImmer({})
|
||||||
|
|
||||||
if (!skey) throw new Error('skey is required')
|
if (!skey) throw new Error('skey is required')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newColumns = []
|
||||||
|
for (const item of columns) {
|
||||||
|
const key = item.dataIndex
|
||||||
|
if (item.filterKey) {
|
||||||
|
let inputRef = null
|
||||||
|
item.onFilter = (value, record) => includes(record[key], value)
|
||||||
|
item.filterDropdown = (x) => {
|
||||||
|
sMap.current[key] = x
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8, width: 200 }}>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
enterButton
|
||||||
|
placeholder="请输入"
|
||||||
|
value={x.selectedKeys[0] ?? ''}
|
||||||
|
ref={ref => inputRef = ref}
|
||||||
|
onChange={e => x.setSelectedKeys(e.target.value ? [e.target.value] : [])}
|
||||||
|
onSearch={v => handleSearch(key, v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item.onFilterDropdownOpenChange = (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
setTimeout(() => inputRef.focus(), 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (item.filterItems) {
|
||||||
|
item.onFilter = (value, record) => record[key] === value
|
||||||
|
item.filterDropdown = (x) => {
|
||||||
|
sMap.current[key] = x
|
||||||
|
return (
|
||||||
|
<div className={css.filterItems}>
|
||||||
|
<Checkbox.Group
|
||||||
|
options={item.filterItems}
|
||||||
|
value={x.selectedKeys}
|
||||||
|
onChange={x.setSelectedKeys} />
|
||||||
|
<Divider style={{ margin: '0' }} />
|
||||||
|
<Flex justify="space-between" className={css.action}>
|
||||||
|
<Button size="small" type="link" disabled={x.selectedKeys.length === 0} onClick={() => x.setSelectedKeys([])}>{t('重置')}</Button>
|
||||||
|
<Button size="small" type="primary" onClick={() => handleSearch(key, x.selectedKeys)}>{t('确定')}</Button>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newColumns.push(item)
|
||||||
|
}
|
||||||
|
setSColumns(newColumns)
|
||||||
|
}, [columns])
|
||||||
|
|
||||||
|
function handleSearch(key, v) {
|
||||||
|
const x = sMap.current[key]
|
||||||
|
updateFilters(draft => {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
v.length ? draft[key] = v : delete draft[key]
|
||||||
|
} else {
|
||||||
|
v ? draft[key] = v : delete draft[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!v) x.setSelectedKeys([])
|
||||||
|
x.confirm()
|
||||||
|
}
|
||||||
|
|
||||||
function handleFullscreen() {
|
function handleFullscreen() {
|
||||||
if (ref.current && document.fullscreenEnabled) {
|
if (ref.current && document.fullscreenEnabled) {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
|
@ -25,14 +94,34 @@ function Stable(props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SearchItem(props) {
|
||||||
|
const { cKey, value } = props
|
||||||
|
const column = columns.find(item => item.dataIndex === cKey)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag closable bordered={false} color="blue" onClose={() => handleSearch(cKey, '')} className={css.search}>
|
||||||
|
{column.title}: {Array.isArray(value) ? value.join(' | ') : value}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card ref={ref} className={clsNames(css.stable, props.className)} style={props.style}>
|
<Card ref={ref} className={clsNames(css.stable, props.className)} style={props.style}>
|
||||||
<Flex align="center" justify="flex-end" className={css.toolbar}>
|
<Flex align="center" justify="space-between" className={css.toolbar}>
|
||||||
|
{Object.keys(filters).length ? (
|
||||||
|
<Flex>
|
||||||
|
{Object.entries(filters).map(([key, value]) => (
|
||||||
|
<SearchItem key={key} cKey={key} value={value} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<div className={css.title}>{props.title}</div>
|
||||||
|
)}
|
||||||
<Flex gap="middle" align="center">
|
<Flex gap="middle" align="center">
|
||||||
{actions}
|
{actions}
|
||||||
{actions.length ? <Divider type="vertical" /> : null}
|
{actions.length ? <Divider type="vertical" /> : null}
|
||||||
<IoReloadOutline className={css.icon} onClick={props.onReload} />
|
<IoReloadOutline className={css.icon} onClick={props.onReload} />
|
||||||
<Setting className={css.icon} skey={skey} columns={columns} setCols={setCols}/>
|
<Setting className={css.icon} skey={skey} columns={sColumns} setCols={setCols} />
|
||||||
{isFull ? (
|
{isFull ? (
|
||||||
<IoContract className={css.icon} onClick={handleFullscreen} />
|
<IoContract className={css.icon} onClick={handleFullscreen} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -45,7 +134,7 @@ function Stable(props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Stable.defaultProps = {
|
STable.defaultProps = {
|
||||||
sKey: null,
|
sKey: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
actions: [],
|
actions: [],
|
||||||
|
@ -60,4 +149,4 @@ Stable.defaultProps = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Stable
|
export default STable
|
|
@ -4,12 +4,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterItems {
|
||||||
|
min-width: 150px;
|
||||||
|
|
||||||
|
:global(.ant-checkbox-group) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
padding: 8px 16px 8px 8px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ class App {
|
||||||
|
|
||||||
updateStable(key, data) {
|
updateStable(key, data) {
|
||||||
this.stable[key] = data;
|
this.stable[key] = data;
|
||||||
|
if (data === null) delete this.stable[key];
|
||||||
localStorage.setItem('stable', JSON.stringify(this.stable));
|
localStorage.setItem('stable', JSON.stringify(this.stable));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {} from 'react'
|
||||||
|
import {Card, Tree} from 'antd'
|
||||||
|
import css from './index.module.scss'
|
||||||
|
|
||||||
|
|
||||||
|
function Group() {
|
||||||
|
const dataSource = [
|
||||||
|
{
|
||||||
|
title: 'parent 1-0',
|
||||||
|
key: '0-0-0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-0-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-0-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-0-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'parent 1-1',
|
||||||
|
key: '0-0-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-1-0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'parent 1-2',
|
||||||
|
key: '0-0-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-2-0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'leaf',
|
||||||
|
key: '0-0-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="分组列表" className={css.group}>
|
||||||
|
<Tree.DirectoryTree
|
||||||
|
treeData={dataSource}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Group
|
|
@ -0,0 +1,14 @@
|
||||||
|
.group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-right: -1px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
flex: 3;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
Loading…
Reference in New Issue