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",
|
||||||
|
@ -32,4 +34,4 @@
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
"vite": "^5.0.2"
|
"vite": "^5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import {createBrowserRouter, RouterProvider} from 'react-router-dom'
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
import {ConfigProvider, App as AntdApp, theme} from 'antd'
|
import { ConfigProvider, App as AntdApp, theme } from 'antd'
|
||||||
import {IconContext} from 'react-icons'
|
import { IconContext } from 'react-icons'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
import enUS from 'antd/locale/en_US'
|
import enUS from 'antd/locale/en_US'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import routes from './routes.jsx'
|
import routes from './routes.jsx'
|
||||||
import {app, SContext} from '@/libs'
|
import { app, SContext } from '@/libs'
|
||||||
import {useImmer} from 'use-immer'
|
import { useImmer } from 'use-immer'
|
||||||
import './i18n.js'
|
import './i18n.js'
|
||||||
|
|
||||||
dayjs.locale(app.lang)
|
dayjs.locale(app.lang)
|
||||||
|
@ -14,10 +14,10 @@ dayjs.locale(app.lang)
|
||||||
const router = createBrowserRouter(routes)
|
const router = createBrowserRouter(routes)
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [S, updateS] = useImmer({theme: app.theme})
|
const [S, updateS] = useImmer({ theme: app.theme })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SContext.Provider value={{S, updateS}}>
|
<SContext.Provider value={{ S, updateS }}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
locale={app.lang === 'en' ? enUS : zhCN}
|
locale={app.lang === 'en' ? enUS : zhCN}
|
||||||
theme={{
|
theme={{
|
||||||
|
@ -37,9 +37,9 @@ function App() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<IconContext.Provider value={{className: 'anticon'}}>
|
<IconContext.Provider value={{ className: 'anticon' }}>
|
||||||
<AntdApp>
|
<AntdApp>
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router} />
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import {useState, useEffect} from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import {Button, Checkbox, Flex, Popover} from 'antd'
|
import { Button, Checkbox, Flex, Popover } from 'antd'
|
||||||
import {IoSettingsOutline} from 'react-icons/io5'
|
import { IoSettingsOutline } from 'react-icons/io5'
|
||||||
import {app, clsNames} from '@/libs'
|
import { app, clsNames } from '@/libs'
|
||||||
|
|
||||||
|
|
||||||
function Setting(props) {
|
function Setting(props) {
|
||||||
const {skey, columns, setCols} = props
|
const { skey, columns, setCols } = props
|
||||||
const [state, setState] = useState(app.getStable(skey))
|
const [state, setState] = useState(app.getStable(skey))
|
||||||
|
|
||||||
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
|
||||||
const newState = {...state, [value]: checked}
|
const newState = { ...state, [value]: checked }
|
||||||
setState(newState)
|
setState(newState)
|
||||||
app.updateStable(skey, newState)
|
app.updateStable(skey, newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setState({})
|
setState({})
|
||||||
app.updateStable(skey, {})
|
app.updateStable(skey, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -35,7 +35,7 @@ function Setting(props) {
|
||||||
title={(
|
title={(
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<div>{t('展示字段')}</div>
|
<div>{t('展示字段')}</div>
|
||||||
<Button type="link" style={{padding: 0}} onClick={handleReset}>{t('重置')}</Button>
|
<Button type="link" style={{ padding: 0 }} onClick={handleReset}>{t('重置')}</Button>
|
||||||
</Flex>)}
|
</Flex>)}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
|
@ -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>
|
||||||
|
@ -53,7 +53,7 @@ function Setting(props) {
|
||||||
</Flex>
|
</Flex>
|
||||||
)}>
|
)}>
|
||||||
<div className={clsNames('anticon', props.className)}>
|
<div className={clsNames('anticon', props.className)}>
|
||||||
<IoSettingsOutline/>
|
<IoSettingsOutline />
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,27 +94,47 @@ 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} />
|
||||||
) : (
|
) : (
|
||||||
<IoExpand className={css.icon} onClick={handleFullscreen}/>
|
<IoExpand className={css.icon} onClick={handleFullscreen} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Table loading={loading} columns={cols} dataSource={dataSource} pagination={pagination}/>
|
<Table loading={loading} columns={cols} dataSource={dataSource} pagination={pagination} />
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Stable.defaultProps = {
|
STable.defaultProps = {
|
||||||
sKey: null,
|
sKey: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
actions: [],
|
actions: [],
|
||||||
|
@ -53,11 +142,11 @@ Stable.defaultProps = {
|
||||||
pagination: {
|
pagination: {
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showLessItems: true,
|
showLessItems: true,
|
||||||
showTotal: total => t('page', {total}),
|
showTotal: total => t('page', { total }),
|
||||||
pageSizeOptions: ['10', '20', '50', '100']
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
},
|
},
|
||||||
onReload: () => {
|
onReload: () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {isSubArray, loadJSONStorage} from "@/libs/utils.js";
|
import { isSubArray, loadJSONStorage } from "@/libs/utils.js";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -17,7 +17,7 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
const {isSuper, permissions} = this.session;
|
const { isSuper, permissions } = this.session;
|
||||||
if (!code || isSuper) return true;
|
if (!code || isSuper) return true;
|
||||||
for (let item of code.split('|')) {
|
for (let item of code.split('|')) {
|
||||||
if (isSubArray(permissions, item.split('&'))) {
|
if (isSubArray(permissions, item.split('&'))) {
|
||||||
|
@ -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