ant migrate v4

pull/289/head
vapao 2020-11-24 22:51:00 +08:00
parent cbbcbb71fe
commit d97570b338
8 changed files with 316 additions and 262 deletions

View File

@ -9,24 +9,22 @@ import { CopyrightOutlined, GithubOutlined } from '@ant-design/icons';
import styles from './layout.module.less';
export default class extends React.Component {
render() {
return (
<Layout.Footer style={{padding: 0}}>
<div className={styles.footerZone}>
<div className={styles.linksZone}>
<a className={styles.links} title="官网" href="https://www.spug.dev" target="_blank"
rel="noopener noreferrer">官网</a>
<a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined/></a>
<a title="文档" href="https://www.spug.dev/docs/about-spug/" target="_blank"
rel="noopener noreferrer">文档</a>
</div>
<div style={{color: 'rgba(0, 0, 0, .45)'}}>
Copyright <CopyrightOutlined/> 2020 By OpenSpug
</div>
export default function () {
return (
<Layout.Footer style={{padding: 0}}>
<div className={styles.footer}>
<div className={styles.links}>
<a className={styles.item} title="官网" href="https://www.spug.dev" target="_blank"
rel="noopener noreferrer">官网</a>
<a className={styles.item} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined/></a>
<a title="文档" href="https://www.spug.dev/docs/about-spug/" target="_blank"
rel="noopener noreferrer">文档</a>
</div>
</Layout.Footer>
)
}
<div style={{color: 'rgba(0, 0, 0, .45)'}}>
Copyright <CopyrightOutlined/> 2020 By OpenSpug
</div>
</div>
</Layout.Footer>
)
}

View File

@ -5,68 +5,22 @@
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { Layout, Dropdown, Menu, List, Badge, Avatar } from 'antd';
import {
CheckOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
UserOutlined,
LogoutOutlined,
NotificationOutlined
} from '@ant-design/icons';
import { Layout, Dropdown, Menu, Avatar } from 'antd';
import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons';
import Notification from './Notification';
import styles from './layout.module.less';
import http from '../libs/http';
import history from '../libs/history';
import avatar from './avatar.png';
import moment from 'moment';
export default class extends React.Component {
constructor(props) {
super(props);
this.inerval = null;
this.state = {
loading: true,
notifies: [],
read: []
}
}
export default function (props) {
componentDidMount() {
this.fetch();
this.interval = setInterval(this.fetch, 60000)
}
componentWillUnmount() {
this.interval && clearInterval(this.interval)
}
fetch = () => {
this.setState({loading: true});
http.get('/api/notify/')
.then(res => this.setState({notifies: res, read: []}))
.finally(() => this.setState({loading: false}))
};
handleLogout = () => {
function handleLogout() {
history.push('/');
http.get('/api/account/logout/')
};
}
handleRead = (e, item) => {
e.stopPropagation();
if (this.state.read.indexOf(item.id) === -1) {
this.state.read.push(item.id);
this.setState({read: this.state.read});
http.patch('/api/notify/', {ids: [item.id]})
}
};
handleReadAll = () => {
const ids = this.state.notifies.map(x => x.id);
this.setState({read: ids});
http.patch('/api/notify/', {ids})
};
menu = (
const UserMenu = (
<Menu>
<Menu.Item>
<Link to="/welcome/info">
@ -74,66 +28,28 @@ export default class extends React.Component {
</Link>
</Menu.Item>
<Menu.Divider/>
<Menu.Item onClick={this.handleLogout}>
<Menu.Item onClick={handleLogout}>
<LogoutOutlined style={{marginRight: 10}}/>退出登录
</Menu.Item>
</Menu>
);
notify = () => (
<Menu className={styles.notify}>
<Menu.Item style={{padding: 0, whiteSpace: 'unset'}}>
<List
loading={this.state.loading}
style={{maxHeight: 500, overflow: 'scroll'}}
itemLayout="horizontal"
dataSource={this.state.notifies}
renderItem={item => (
<List.Item className={styles.notifyItem} onClick={e => this.handleRead(e, item)}>
<List.Item.Meta
style={{opacity: this.state.read.includes(item.id) ? 0.4 : 1}}
avatar={<CheckOutlined type={item.source} style={{fontSize: 24, color: '#1890ff'}}/>}
title={<span style={{fontWeight: 400, color: '#404040'}}>{item.title}</span>}
description={[
<div key="1" style={{fontSize: 12}}>{item.content}</div>,
<div key="2" style={{fontSize: 12}}>{moment(item['created_at']).fromNow()}</div>
]}/>
</List.Item>
)}/>
{this.state.notifies.length !== 0 && (
<div className={styles.notifyFooter} onClick={() => this.handleReadAll()}>全部 已读</div>
)}
</Menu.Item>
</Menu>
);
render() {
const {notifies, read} = this.state;
return (
<Layout.Header style={{padding: 0}}>
<div className={styles.header}>
<div className={styles.trigger} onClick={this.props.toggle}>
{this.props.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
</div>
<div className={styles.right}>
<Dropdown overlay={this.menu}>
<span className={styles.action}>
<Avatar size="small" src={avatar} style={{marginRight: 8}}/>
{localStorage.getItem('nickname')}
</span>
</Dropdown>
</div>
<div className={styles.right}>
<Dropdown overlay={this.notify} trigger={['click']}>
<span className={styles.trigger}>
<Badge count={notifies.length - read.length}>
<NotificationOutlined style={{fontSize: 16}}/>
</Badge>
</span>
</Dropdown>
</div>
return (
<Layout.Header className={styles.header}>
<div className={styles.left}>
<div className={styles.trigger} onClick={props.toggle}>
{props.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
</div>
</Layout.Header>
)
}
</div>
<Notification/>
<div className={styles.right}>
<Dropdown overlay={UserMenu} style={{background: '#000'}}>
<span className={styles.action}>
<Avatar size="small" src={avatar} style={{marginRight: 8}}/>
{localStorage.getItem('nickname')}
</span>
</Dropdown>
</div>
</Layout.Header>
)
}

View File

@ -0,0 +1,84 @@
import React, { useState, useEffect } from 'react';
import { Menu, List, Dropdown, Badge } from 'antd';
import { CheckOutlined, NotificationOutlined } from '@ant-design/icons';
import { http } from 'libs';
import moment from 'moment';
import styles from './layout.module.less';
let interval;
export default function () {
const [loading, setLoading] = useState(false);
const [notifies, setNotifies] = useState([]);
const [reads, setReads] = useState([]);
useEffect(() => {
fetch();
interval = setInterval(fetch, 60000);
return () => {
if (interval) clearInterval(interval)
}
}, [])
function fetch() {
setLoading(true);
http.get('/api/notify/')
.then(res => {
setNotifies(res);
setReads([])
})
.finally(() => setLoading(false))
}
function handleRead(e, item) {
e.stopPropagation();
if (reads.indexOf(item.id) === -1) {
reads.push(item.id);
setReads(reads)
http.patch('/api/notify/', {ids: [item.id]})
}
}
function handleReadAll() {
const ids = notifies.map(x => x.id);
setReads(ids);
http.patch('/api/notify/', {ids})
}
return (
<div className={styles.right}>
<Dropdown trigger={['click']} overlay={(
<Menu className={styles.notify}>
<Menu.Item style={{padding: 0, whiteSpace: 'unset'}}>
<List
loading={loading}
style={{maxHeight: 500, overflow: 'scroll'}}
itemLayout="horizontal"
dataSource={notifies}
renderItem={item => (
<List.Item className={styles.item} onClick={e => handleRead(e, item)}>
<List.Item.Meta
style={{opacity: reads.includes(item.id) ? 0.4 : 1}}
avatar={<CheckOutlined type={item.source} style={{fontSize: 24, color: '#1890ff'}}/>}
title={<span style={{fontWeight: 400, color: '#404040'}}>{item.title}</span>}
description={[
<div key="1" style={{fontSize: 12}}>{item.content}</div>,
<div key="2" style={{fontSize: 12}}>{moment(item['created_at']).fromNow()}</div>
]}/>
</List.Item>
)}/>
{notifies.length !== 0 && (
<div className={styles.footer} onClick={handleReadAll}>全部 已读</div>
)}
</Menu.Item>
</Menu>
)}>
<span className={styles.trigger}>
<Badge count={notifies.length - reads.length}>
<NotificationOutlined style={{fontSize: 16}}/>
</Badge>
</span>
</Dropdown>
</div>
)
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Layout, Menu } from 'antd';
import { hasPermission, history } from 'libs';
import styles from './layout.module.css';
import styles from './layout.module.less';
import menus from '../routes';
import logo from './logo-spug.png';
import logoText from './logo-text.png';

View File

@ -3,47 +3,64 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React from 'react';
import React, { useState } from 'react';
import { Switch, Route } from 'react-router-dom';
import { Layout } from 'antd';
import Sider from './Sider';
import Header from './Header';
import Footer from './Footer'
import { Router } from '../libs/router';
import { updatePermissions} from '../libs';
import styles from './layout.module.css';
import routes from '../routes';
import { updatePermissions, hasPermission } from 'libs';
import styles from './layout.module.less';
const Routes = [];
export default class extends React.Component {
constructor(props) {
super(props);
this.initPermissions();
this.state = {
collapsed: false
function initRoutes(routes) {
for (let route of routes) {
if (route.component) {
if (!route.auth || hasPermission(route.auth)) {
Routes.push(<Route exact key={route.path} path={route.path} component={route.component}/>)
}
} else if (route.child) {
initRoutes(route.child)
}
}
initPermissions() {
const data = localStorage.getItem('permissions');
const hostPerms = localStorage.getItem('host_perms');
const isSuper = localStorage.getItem('is_supper') === 'true';
data && updatePermissions(isSuper, JSON.parse(hostPerms), JSON.parse(data))
}
render() {
return (
<Layout>
<Sider collapsed={this.state.collapsed} />
<Layout>
<Header
collapsed={this.state.collapsed}
toggle={() => this.setState({collapsed: !this.state.collapsed})}
/>
<Layout.Content className={styles.content} style={{height: `${document.body.clientHeight - 64}px`}}>
<Router/>
<Footer/>
</Layout.Content>
</Layout>
</Layout>
)
}
}
initRoutes(routes)
updatePermissions()
// 404
function NotFound() {
return (
<div className={styles.router}>
<div className={styles.imgBlock}>
<div className={styles.img}/>
</div>
<div>
<h1 className={styles.title}>404</h1>
<div className={styles.desc}>抱歉你访问的页面不存在</div>
</div>
</div>
)
}
export default function () {
const [collapsed, setCollapsed] = useState(false)
return (
<Layout>
<Sider collapsed={collapsed}/>
<Layout style={{height: '100vh'}}>
<Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)}/>
<Layout.Content className={styles.content}>
<Switch>
{Routes}
<Route component={NotFound}/>
</Switch>
<Footer/>
</Layout.Content>
</Layout>
</Layout>
)
}

View File

@ -10,19 +10,6 @@
.left {
flex: 1;
.trigger {
font-size: 20px;
line-height: 48px;
cursor: pointer;
transition: all 0.3s, padding 0s;
padding: 0 24px;
float: left;
}
.trigger:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.right {
@ -38,9 +25,51 @@
background: rgba(0, 0, 0, 0.025);
}
}
.trigger {
font-size: 20px;
line-height: 48px;
cursor: pointer;
transition: all 0.3s, padding 0s;
padding: 0 24px;
float: left;
}
.trigger:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.notify {
width: 350px;
padding: 0;
.item {
align-items: center;
cursor: pointer;
padding: 12px 24px;
}
.item:hover {
background-color: rgb(233, 247, 254);
}
.footer {
line-height: 46px;
text-align: center;
cursor: pointer;
border-top: 1px solid #e8e8e8;
}
}
.notify :global(.ant-dropdown-menu-item:hover) {
background-color: #fff;
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 24px 24px 0;
overflow-y: scroll;
}
@ -60,32 +89,15 @@
.logo {
height: 64px;
line-height: 64px;
padding-left: 24px;
background-color: #002140;
padding-left: 15px;
overflow: hidden;
}
.logo h1 {
display: inline-block;
vertical-align: middle;
color: #ffffff;
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 20px;
margin: 0 0 0 12px;
font-weight: 600;
}
.logo img {
height: 32px;
width: 50px;
}
}
.footer {
margin: 48px 0 24px;
text-align: center;
color: rgba(0, 0, 0, .45);
}
.router {
display: flex;
height: 80%;
@ -121,4 +133,21 @@
line-height: 28px;
margin-bottom: 16px;
}
}
.footer {
width: 100%;
padding: 20px;
font-size: 14px;
text-align: center;
display: flex;
flex-direction: column;
.links {
margin-bottom: 7px;
.item {
margin-right: 40px;
}
}
}

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
export default [
{icon: 'desktop', title: '工作台', auth: 'home.home.view', path: '/home'},
{icon: 'cloud-server', title: '主机管理', auth: 'host.host.view', path: '/host'},
{
icon: 'code', title: '批量执行', auth: 'exec.task.do|exec.template.view', child: [
{title: '执行任务', auth: 'exec.task.do', path: '/exec/task'},
{title: '模板管理', auth: 'exec.template.view', path: '/exec/template'},
]
},
{
icon: 'flag', title: '应用发布', auth: 'deploy.app.view|deploy.request.view', child: [
{title: '应用管理', auth: 'deploy.app.view', path: '/deploy/app'},
{title: '发布申请', auth: 'deploy.request.view', path: '/deploy/request'},
]
},
{icon: 'schedule', title: '任务计划', auth: 'schedule.schedule.view', path: '/schedule'},
{
icon: 'deployment-unit', title: '配置中心', auth: 'config.env.view|config.src.view|config.app.view', child: [
{title: '环境管理', auth: 'config.env.view', path: '/config/environment'},
{title: '服务配置', auth: 'config.src.view', path: '/config/service'},
{title: '应用配置', auth: 'config.app.view', path: '/config/app'},
]
},
{icon: 'monitor', title: '监控中心', auth: 'monitor.monitor.view', path: '/monitor'},
{
icon: 'alert', title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
{title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm'},
{title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact'},
{title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group'},
]
},
{
icon: 'setting', title: '系统管理', auth: "system.account.view|system.role.view|system.setting.view", child: [
{title: '账户管理', auth: 'system.account.view', path: '/system/account'},
{title: '角色管理', auth: 'system.role.view', path: '/system/role'},
{title: '系统设置', auth: 'system.setting.view', path: '/system/setting'},
]
},
]

View File

@ -3,29 +3,83 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import { makeModuleRoute } from "./libs/router";
import welcomeRoues from './pages/welcome/routes';
import homeRoutes from './pages/home/routes';
import hostRoutes from './pages/host/routes';
import systemRoutes from './pages/system/routes';
import execRoutes from './pages/exec/routes';
import scheduleRoutes from './pages/schedule/routes';
import monitorRoutes from './pages/monitor/routes';
import alarmRoutes from './pages/alarm/routes';
import configRoutes from './pages/config/routes';
import deployRoutes from './pages/deploy/routes';
import React from 'react';
import {
DesktopOutlined,
CloudServerOutlined,
CodeOutlined,
FlagOutlined,
ScheduleOutlined,
DeploymentUnitOutlined,
MonitorOutlined,
AlertOutlined,
SettingOutlined
} from '@ant-design/icons';
import HomeIndex from './pages/home';
import HostIndex from './pages/host';
import ExecTask from './pages/exec/task';
import ExecTemplate from './pages/exec/template';
import DeployApp from './pages/deploy/app';
import DeployRequest from './pages/deploy/request';
import ScheduleIndex from './pages/schedule';
import ConfigEnvironment from './pages/config/environment';
import ConfigService from './pages/config/service';
import ConfigApp from './pages/config/app';
import MonitorIndex from './pages/monitor';
import AlarmIndex from './pages/alarm/alarm';
import AlarmGroup from './pages/alarm/group';
import AlarmContact from './pages/alarm/contact';
import SystemAccount from './pages/system/account';
import SystemRole from './pages/system/role';
import SystemSetting from './pages/system/setting';
import WelcomeIndex from './pages/welcome/index';
import WelcomeInfo from './pages/welcome/info';
export default [
makeModuleRoute('/welcome', welcomeRoues),
makeModuleRoute('/home', homeRoutes),
makeModuleRoute('/host', hostRoutes),
makeModuleRoute('/system', systemRoutes),
makeModuleRoute('/exec', execRoutes),
makeModuleRoute('/schedule', scheduleRoutes),
makeModuleRoute('/monitor', monitorRoutes),
makeModuleRoute('/alarm', alarmRoutes),
makeModuleRoute('/config', configRoutes),
makeModuleRoute('/deploy', deployRoutes),
{icon: <DesktopOutlined/>, title: '工作台', auth: 'home.home.view', path: '/home', component: HomeIndex},
{icon: <CloudServerOutlined/>, title: '主机管理', auth: 'host.host.view', path: '/host', component: HostIndex},
{
icon: <CodeOutlined/>, title: '批量执行', auth: 'exec.task.do|exec.template.view', child: [
{title: '执行任务', auth: 'exec.task.do', path: '/exec/task', component: ExecTask},
{title: '模板管理', auth: 'exec.template.view', path: '/exec/template', component: ExecTemplate},
]
},
{
icon: <FlagOutlined/>, title: '应用发布', auth: 'deploy.app.view|deploy.request.view', child: [
{title: '应用管理', auth: 'deploy.app.view', path: '/deploy/app', component: DeployApp},
{title: '发布申请', auth: 'deploy.request.view', path: '/deploy/request', component: DeployRequest},
]
},
{
icon: <ScheduleOutlined/>,
title: '任务计划',
auth: 'schedule.schedule.view',
path: '/schedule',
component: ScheduleIndex
},
{
icon: <DeploymentUnitOutlined/>, title: '配置中心', auth: 'config.env.view|config.src.view|config.app.view', child: [
{title: '环境管理', auth: 'config.env.view', path: '/config/environment', component: ConfigEnvironment},
{title: '服务配置', auth: 'config.src.view', path: '/config/service', component: ConfigService},
{title: '应用配置', auth: 'config.app.view', path: '/config/app', component: ConfigApp},
]
},
{icon: <MonitorOutlined/>, title: '监控中心', auth: 'monitor.monitor.view', path: '/monitor', component: MonitorIndex},
{
icon: <AlertOutlined/>, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
{title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex},
{title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact},
{title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup},
]
},
{
icon: <SettingOutlined/>, title: '系统管理', auth: "system.account.view|system.role.view|system.setting.view", child: [
{title: '账户管理', auth: 'system.account.view', path: '/system/account', component: SystemAccount},
{title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole},
{title: '系统设置', auth: 'system.setting.view', path: '/system/setting', component: SystemSetting},
]
},
{path: '/welcome/index', component: WelcomeIndex},
{path: '/welcome/info', component: WelcomeInfo},
]