mirror of https://github.com/openspug/spug
update
parent
3008fbfc86
commit
89d8f30570
|
@ -10,7 +10,6 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
|
||||||
"antd": "^5.12.4",
|
"antd": "^5.12.4",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"i18next": "^23.7.7",
|
"i18next": "^23.7.7",
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
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 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 {session, 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(session.lang)
|
dayjs.locale(app.lang)
|
||||||
|
|
||||||
const router = createBrowserRouter(routes)
|
const router = createBrowserRouter(routes)
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [S, updateS] = useImmer({theme: session.theme})
|
const [S, updateS] = useImmer({theme: app.theme})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SContext.Provider value={{S, updateS}}>
|
<SContext.Provider value={{S, updateS}}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
locale={session.lang === 'en' ? enUS : zhCN}
|
locale={app.lang === 'en' ? enUS : zhCN}
|
||||||
theme={{
|
theme={{
|
||||||
cssVar: true,
|
cssVar: true,
|
||||||
hashed: false,
|
hashed: false,
|
||||||
|
@ -36,9 +37,11 @@ function App() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
|
<IconContext.Provider value={{className: 'anticon'}}>
|
||||||
<AntdApp>
|
<AntdApp>
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router}/>
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
|
</IconContext.Provider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</SContext.Provider>
|
</SContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {useState, useEffect} from 'react'
|
||||||
|
import {Button, Checkbox, Flex, Popover} from 'antd'
|
||||||
|
import {IoSettingsOutline} from 'react-icons/io5'
|
||||||
|
import {app, clsNames} from '@/libs'
|
||||||
|
|
||||||
|
|
||||||
|
function Setting(props) {
|
||||||
|
const {skey, columns, setCols} = props
|
||||||
|
const [state, setState] = useState(app.getStable(skey))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newColumns = []
|
||||||
|
for (const item of columns) {
|
||||||
|
if (state[item.key] ?? !item.hidden) {
|
||||||
|
newColumns.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCols(newColumns)
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
function handleChange(e) {
|
||||||
|
const {value, checked} = e.target
|
||||||
|
const newState = {...state, [value]: checked}
|
||||||
|
setState(newState)
|
||||||
|
app.updateStable(skey, newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
setState({})
|
||||||
|
app.updateStable(skey, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
title={(
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<div>{t('展示字段')}</div>
|
||||||
|
<Button type="link" style={{padding: 0}} onClick={handleReset}>{t('重置')}</Button>
|
||||||
|
</Flex>)}
|
||||||
|
trigger="click"
|
||||||
|
placement="bottomRight"
|
||||||
|
content={(
|
||||||
|
<Flex vertical gap="small">
|
||||||
|
{columns.map((item, index) => (
|
||||||
|
<Checkbox
|
||||||
|
value={item.key}
|
||||||
|
key={index}
|
||||||
|
checked={state[item.key] ?? !item.hidden}
|
||||||
|
onChange={handleChange}>
|
||||||
|
{item.title}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)}>
|
||||||
|
<div className={clsNames('anticon', props.className)}>
|
||||||
|
<IoSettingsOutline/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setting
|
|
@ -0,0 +1,63 @@
|
||||||
|
import {useRef, useState} from 'react'
|
||||||
|
import {Card, Table, Flex, Divider} from 'antd'
|
||||||
|
import {IoExpand, IoContract, IoReloadOutline} from 'react-icons/io5'
|
||||||
|
import {clsNames} from '@/libs'
|
||||||
|
import Setting from './Setting.jsx'
|
||||||
|
import css from './index.module.scss'
|
||||||
|
|
||||||
|
function Stable(props) {
|
||||||
|
const {skey, loading, columns, dataSource, actions, pagination} = props
|
||||||
|
const ref = useRef();
|
||||||
|
const [cols, setCols] = useState([])
|
||||||
|
const [isFull, setIsFull] = useState(false)
|
||||||
|
|
||||||
|
if (!skey) throw new Error('skey is required')
|
||||||
|
|
||||||
|
function handleFullscreen() {
|
||||||
|
if (ref.current && document.fullscreenEnabled) {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
setIsFull(false)
|
||||||
|
} else {
|
||||||
|
ref.current.requestFullscreen()
|
||||||
|
setIsFull(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card ref={ref} className={clsNames(css.stable, props.className)} style={props.style}>
|
||||||
|
<Flex align="center" justify="flex-end" className={css.toolbar}>
|
||||||
|
<Flex gap="middle" align="center">
|
||||||
|
{actions}
|
||||||
|
{actions.length ? <Divider type="vertical"/> : null}
|
||||||
|
<IoReloadOutline className={css.icon} onClick={props.onReload}/>
|
||||||
|
<Setting className={css.icon} skey={skey} columns={columns} setCols={setCols}/>
|
||||||
|
{isFull ? (
|
||||||
|
<IoContract className={css.icon} onClick={handleFullscreen}/>
|
||||||
|
) : (
|
||||||
|
<IoExpand className={css.icon} onClick={handleFullscreen}/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Table loading={loading} columns={cols} dataSource={dataSource} pagination={pagination}/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Stable.defaultProps = {
|
||||||
|
sKey: null,
|
||||||
|
loading: false,
|
||||||
|
actions: [],
|
||||||
|
defaultFields: [],
|
||||||
|
pagination: {
|
||||||
|
showSizeChanger: true,
|
||||||
|
showLessItems: true,
|
||||||
|
showTotal: total => t('page', {total}),
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
},
|
||||||
|
onReload: () => {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stable
|
|
@ -0,0 +1,15 @@
|
||||||
|
.stable {
|
||||||
|
:global(.ant-pagination) {
|
||||||
|
margin: 16px 0 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import {initReactI18next} from 'react-i18next'
|
import {initReactI18next} from 'react-i18next'
|
||||||
import {session} from '@/libs'
|
import {app} from '@/libs'
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
lng: session.lang,
|
lng: app.lang,
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
||||||
translation: {
|
translation: {
|
||||||
|
@ -16,6 +16,8 @@ i18n.use(initReactI18next).init({
|
||||||
'重置': 'Reset',
|
'重置': 'Reset',
|
||||||
'展示字段': 'Columns Display',
|
'展示字段': 'Columns Display',
|
||||||
'年龄': 'Age',
|
'年龄': 'Age',
|
||||||
|
// buttons
|
||||||
|
'新建': 'Add',
|
||||||
'page': 'Total {{total}} items',
|
'page': 'Total {{total}} items',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {useContext, useEffect} from 'react'
|
import {useContext, useEffect} from 'react'
|
||||||
import {Dropdown, Flex, Layout, theme as antdTheme} from 'antd'
|
import {Dropdown, Flex, Layout, theme as antdTheme} from 'antd'
|
||||||
import {AiOutlineTranslation} from 'react-icons/ai'
|
import {IoMoon, IoSunny, IoLanguage} from 'react-icons/io5'
|
||||||
import {IoMoon, IoSunny} from 'react-icons/io5'
|
|
||||||
import {SContext} from '@/libs'
|
import {SContext} from '@/libs'
|
||||||
import css from './index.module.scss'
|
import css from './index.module.scss'
|
||||||
import i18n from '@/i18n.js'
|
import i18n from '@/i18n.js'
|
||||||
|
@ -44,7 +43,7 @@ function Header() {
|
||||||
<div className={css.item}>admin</div>
|
<div className={css.item}>admin</div>
|
||||||
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
|
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
|
||||||
<div className={css.item}>
|
<div className={css.item}>
|
||||||
<AiOutlineTranslation size={18}/>
|
<IoLanguage size={16}/>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<div className={css.item} onClick={handleThemeChange}>
|
<div className={css.item} onClick={handleThemeChange}>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {isSubArray, loadJSONStorage} from "@/libs/utils.js";
|
||||||
|
|
||||||
|
class App {
|
||||||
|
constructor() {
|
||||||
|
this.lang = localStorage.getItem('lang') || 'zh';
|
||||||
|
this.theme = localStorage.getItem('theme') || 'light';
|
||||||
|
this.stable = loadJSONStorage('stable', {});
|
||||||
|
this.session = loadJSONStorage('session', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
get access_token() {
|
||||||
|
return this.session['access_token'] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get nickname() {
|
||||||
|
return this.session['nickname'];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermission(code) {
|
||||||
|
const {isSuper, permissions} = this.session;
|
||||||
|
if (!code || isSuper) return true;
|
||||||
|
for (let item of code.split('|')) {
|
||||||
|
if (isSubArray(permissions, item.split('&'))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSession(data) {
|
||||||
|
Object.assign(this.session, data);
|
||||||
|
localStorage.setItem('session', JSON.stringify(this.session));
|
||||||
|
}
|
||||||
|
|
||||||
|
getStable(key) {
|
||||||
|
return this.stable[key] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStable(key, data) {
|
||||||
|
this.stable[key] = data;
|
||||||
|
localStorage.setItem('stable', JSON.stringify(this.stable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new App();
|
|
@ -1,6 +1,6 @@
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import {message} from 'antd'
|
import {message} from 'antd'
|
||||||
import session from '@/libs/session'
|
import app from '@/libs/app.js'
|
||||||
import {redirect} from 'react-router-dom'
|
import {redirect} from 'react-router-dom'
|
||||||
|
|
||||||
function fetcher(resource, init) {
|
function fetcher(resource, init) {
|
||||||
|
@ -33,7 +33,7 @@ function SWRGet(url, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function request(method, url, params) {
|
function request(method, url, params) {
|
||||||
const init = {method, headers: {'X-Token': session.access_token}}
|
const init = {method, headers: {'X-Token': app.accessToken}}
|
||||||
if (['GET', 'DELETE'].includes(method)) {
|
if (['GET', 'DELETE'].includes(method)) {
|
||||||
if (params) url = `${url}?${new URLSearchParams(params).toString()}`
|
if (params) url = `${url}?${new URLSearchParams(params).toString()}`
|
||||||
return fetcher(url, init)
|
return fetcher(url, init)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import http from './http'
|
import http from './http'
|
||||||
import session from './session'
|
import app from './app.js'
|
||||||
|
|
||||||
const SContext = React.createContext({})
|
const SContext = React.createContext({})
|
||||||
export * from './utils.js'
|
export * from './utils.js'
|
||||||
export {
|
export {
|
||||||
|
app,
|
||||||
http,
|
http,
|
||||||
session,
|
|
||||||
SContext,
|
SContext,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import {isSubArray} from "@/libs/utils.js";
|
|
||||||
|
|
||||||
class Session {
|
|
||||||
constructor() {
|
|
||||||
this._session = {};
|
|
||||||
this.lang = localStorage.getItem('lang') || 'zh';
|
|
||||||
this.theme = localStorage.getItem('theme') || 'light';
|
|
||||||
const tmp = localStorage.getItem('session');
|
|
||||||
if (tmp) {
|
|
||||||
try {
|
|
||||||
this._session = JSON.parse(tmp);
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.removeItem('session');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get access_token() {
|
|
||||||
return this._session['access_token'] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get nickname() {
|
|
||||||
return this._session['nickname'];
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPermission(code) {
|
|
||||||
const {isSuper, permissions} = this._session;
|
|
||||||
if (!code || isSuper) return true;
|
|
||||||
for (let item of code.split('|')) {
|
|
||||||
if (isSubArray(permissions, item.split('&'))) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data) {
|
|
||||||
Object.assign(this._session, data);
|
|
||||||
localStorage.setItem('session', JSON.stringify(this._session));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Session();
|
|
|
@ -43,3 +43,15 @@ export function includes(s, keys) {
|
||||||
return isInclude(s, keys)
|
return isInclude(s, keys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadJSONStorage(key, defaultValue = null) {
|
||||||
|
const tmp = localStorage.getItem(key)
|
||||||
|
if (tmp) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(tmp)
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {AiOutlineDesktop, AiOutlineCloudServer, AiOutlineCluster} from 'react-icons/ai'
|
import {FaDesktop, FaServer, FaSitemap} from 'react-icons/fa6'
|
||||||
import Layout from './layout/index.jsx'
|
import Layout from './layout/index.jsx'
|
||||||
import ErrorPage from './error-page.jsx'
|
import ErrorPage from './error-page.jsx'
|
||||||
import LoginIndex from './pages/login/index.jsx'
|
import LoginIndex from './pages/login/index.jsx'
|
||||||
|
@ -16,18 +16,18 @@ let routes = [
|
||||||
path: 'home',
|
path: 'home',
|
||||||
element: <HomeIndex/>,
|
element: <HomeIndex/>,
|
||||||
title: t('工作台'),
|
title: t('工作台'),
|
||||||
icon: <AiOutlineDesktop/>,
|
icon: <FaDesktop/>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'host',
|
path: 'host',
|
||||||
element: <HostIndex/>,
|
element: <HostIndex/>,
|
||||||
title: t('主机管理'),
|
title: t('主机管理'),
|
||||||
icon: <AiOutlineCloudServer/>
|
icon: <FaServer/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'exec',
|
path: 'exec',
|
||||||
title: t('批量执行'),
|
title: t('批量执行'),
|
||||||
icon: <AiOutlineCluster/>,
|
icon: <FaSitemap/>,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'task',
|
path: 'task',
|
||||||
|
|
Loading…
Reference in New Issue