pull/661/head
vapao 2023-12-27 23:47:26 +08:00
parent 3008fbfc86
commit 89d8f30570
13 changed files with 221 additions and 64 deletions

View File

@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.4",
"dayjs": "^1.11.10",
"i18next": "^23.7.7",

View File

@ -1,24 +1,25 @@
import {createBrowserRouter, RouterProvider} from 'react-router-dom'
import {ConfigProvider, App as AntdApp, theme} from 'antd'
import {IconContext} from 'react-icons'
import zhCN from 'antd/locale/zh_CN'
import enUS from 'antd/locale/en_US'
import dayjs from 'dayjs'
import routes from './routes.jsx'
import {session, SContext} from '@/libs'
import {app, SContext} from '@/libs'
import {useImmer} from 'use-immer'
import './i18n.js'
dayjs.locale(session.lang)
dayjs.locale(app.lang)
const router = createBrowserRouter(routes)
function App() {
const [S, updateS] = useImmer({theme: session.theme})
const [S, updateS] = useImmer({theme: app.theme})
return (
<SContext.Provider value={{S, updateS}}>
<ConfigProvider
locale={session.lang === 'en' ? enUS : zhCN}
locale={app.lang === 'en' ? enUS : zhCN}
theme={{
cssVar: true,
hashed: false,
@ -36,9 +37,11 @@ function App() {
},
},
}}>
<IconContext.Provider value={{className: 'anticon'}}>
<AntdApp>
<RouterProvider router={router}/>
</AntdApp>
</IconContext.Provider>
</ConfigProvider>
</SContext.Provider>
)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,15 @@
.stable {
:global(.ant-pagination) {
margin: 16px 0 0 !important;
}
}
.toolbar {
margin-bottom: 12px;
.icon {
font-size: 18px;
cursor: pointer;
}
}

View File

@ -1,9 +1,9 @@
import i18n from 'i18next'
import {initReactI18next} from 'react-i18next'
import {session} from '@/libs'
import {app} from '@/libs'
i18n.use(initReactI18next).init({
lng: session.lang,
lng: app.lang,
resources: {
en: {
translation: {
@ -16,6 +16,8 @@ i18n.use(initReactI18next).init({
'重置': 'Reset',
'展示字段': 'Columns Display',
'年龄': 'Age',
// buttons
'新建': 'Add',
'page': 'Total {{total}} items',
}
},

View File

@ -1,7 +1,6 @@
import {useContext, useEffect} from 'react'
import {Dropdown, Flex, Layout, theme as antdTheme} from 'antd'
import {AiOutlineTranslation} from 'react-icons/ai'
import {IoMoon, IoSunny} from 'react-icons/io5'
import {IoMoon, IoSunny, IoLanguage} from 'react-icons/io5'
import {SContext} from '@/libs'
import css from './index.module.scss'
import i18n from '@/i18n.js'
@ -44,7 +43,7 @@ function Header() {
<div className={css.item}>admin</div>
<Dropdown menu={{items: locales, selectable: true, onClick: handleLangChange, selectedKeys: [i18n.language]}}>
<div className={css.item}>
<AiOutlineTranslation size={18}/>
<IoLanguage size={16}/>
</div>
</Dropdown>
<div className={css.item} onClick={handleThemeChange}>

45
spug_web2/src/libs/app.js Normal file
View File

@ -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();

View File

@ -1,6 +1,6 @@
import useSWR from 'swr'
import {message} from 'antd'
import session from '@/libs/session'
import app from '@/libs/app.js'
import {redirect} from 'react-router-dom'
function fetcher(resource, init) {
@ -33,7 +33,7 @@ function SWRGet(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 (params) url = `${url}?${new URLSearchParams(params).toString()}`
return fetcher(url, init)

View File

@ -1,11 +1,11 @@
import React from 'react'
import http from './http'
import session from './session'
import app from './app.js'
const SContext = React.createContext({})
export * from './utils.js'
export {
app,
http,
session,
SContext,
}

View File

@ -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();

View File

@ -43,3 +43,15 @@ export function includes(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
}

View File

@ -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 ErrorPage from './error-page.jsx'
import LoginIndex from './pages/login/index.jsx'
@ -16,18 +16,18 @@ let routes = [
path: 'home',
element: <HomeIndex/>,
title: t('工作台'),
icon: <AiOutlineDesktop/>,
icon: <FaDesktop/>,
},
{
path: 'host',
element: <HostIndex/>,
title: t('主机管理'),
icon: <AiOutlineCloudServer/>
icon: <FaServer/>
},
{
path: 'exec',
title: t('批量执行'),
icon: <AiOutlineCluster/>,
icon: <FaSitemap/>,
children: [
{
path: 'task',