add react版前端

This commit is contained in:
2021-06-11 14:48:04 +08:00
parent fe1f2fb821
commit bf2fc2b01a
137 changed files with 18445 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import React, { Component } from 'react'
export default class index extends Component {
render() {
return (
<div>
404
</div>
)
}
}

View File

@@ -0,0 +1,100 @@
import React, { Component } from 'react'
import { Button, Form, Input, message as Message } from 'antd'
import Container from 'components/container'
import { encryptByRSA } from 'util/rsa'
import { RSA_PUBLIC_KEY } from 'util/global'
import { api } from 'common/api'
import { token } from 'common/token'
import 'assets/style/login.less'
export default class index extends Component {
state = {
loading: false,
focusUser: false,
focusPassword: false
}
backgroundImage = require(`assets/image/login-bg-0${Math.floor(Math.random() * 4)}.jpg`)
focus = {
user: false,
password: false
}
form = React.createRef()
onLogin = (values) => {
this.setState({
loading: true
})
let { account, password } = values
password = encryptByRSA(password, RSA_PUBLIC_KEY)
api.login({ account, password }).then(({ success, data, message }) => {
if (success) {
token.value = data
Message.success('登录成功')
this.props.history.replace('/')
} else {
Message.error(message)
}
}).catch(({ message }) => {
if (typeof message === 'object' && message[0]) {
Message.error(message[0].messages[0])
}
})
}
render() {
return (
<div className="yo-login">
<img src={this.backgroundImage.default} alt="" />
<div className="yo-login--placeholder">
<Container mode="sm">
<Form ref={this.form} layout="vertical" onFinish={this.onLogin}>
<Form.Item
name="account"
className={!this.state.focusUser && 'yo-login--label'}
colon={false}
label="用户名"
>
<Input
onBlur={() => { this.setState({ focusUser: !!this.form.current.getFieldValue('account') }); }}
onFocus={() => { this.setState({ focusUser: true }) }}
size="large"
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="password"
className={!this.state.focusPassword && 'yo-login--label'}
colon={false}
label="密码"
>
<Input.Password
onBlur={() => { this.setState({ focusPassword: !!this.form.current.getFieldValue('password') }) }}
onFocus={() => { this.setState({ focusPassword: true }) }}
size="large"
autoComplete="off"
/>
</Form.Item>
<Form.Item className="mt-lg">
<Button
disabled={this.form.user === '' || this.form.password === ''}
loading={this.state.loading}
block
htmlType="submit"
size="large"
type="primary"
>登录</Button>
</Form.Item>
</Form>
</Container>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,138 @@
import React, { Component } from 'react'
import { Layout, Tabs, Menu, Dropdown } from 'antd'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import AntIcon from 'components/ant-icon'
NProgress.configure({ parent: '.ant-layout-content > .yo-tab-external-mount > .yo-tab-external-mount-content' });
class ComponentDynamic extends Component {
state = {
component: null
}
componentDidMount() {
NProgress.start()
setTimeout(async () => {
let c;
try {
c = await import(`../../../../pages${this.props.path}`)
}
catch {
c = await import(`views/error/404`)
}
this.setState({
component: c.default
}, () => {
NProgress.done()
})
})
}
render() {
return (<>
{
this.state.component && <this.state.component {...this.props} />
}
</>)
}
}
export default class index extends Component {
state = {
actived: ''
}
static getDerivedStateFromProps(props) {
return {
actived: props.actived
}
}
onChange = (activeKey) => {
this.props.parent.setState({
actived: activeKey
})
}
onClose = (targetKey, action) => {
if (action === 'remove') {
window.closeContentWindow(targetKey)
}
}
render() {
return (
<Layout.Content>
<div className="yo-tab-external-mount">
<Tabs
type="editable-card"
hideAdd
activeKey={this.state.actived}
onChange={this.onChange}
onEdit={this.onClose}
>
{
this.props.panes.map(pane => {
return (
<Tabs.TabPane
closable={pane.closable}
key={pane.key}
tab={
<Dropdown
trigger={['contextMenu']}
overlay={
<Menu>
<Menu.Item key="0">重新加载</Menu.Item>
<Menu.Divider />
<Menu.Item key="1" onClick={() => window.closeContentWindow(pane.key)}>关闭</Menu.Item>
<Menu.Item key="2" onClick={() => window.closeOtherContentWindow(pane.key)}>关闭其他标签页</Menu.Item>
<Menu.Item key="3" onClick={() => window.closeRightContentWindow(pane.key)}>关闭右侧标签页</Menu.Item>
</Menu>
}
>
<div>
{pane.icon && <AntIcon type={pane.icon} />}
{pane.title}
</div>
</Dropdown>
}
/>
)
})
}
</Tabs>
<div className="yo-tab-external-mount-content">
{
this.props.panes.map(pane => {
return (
<div
key={pane.key}
className={
(pane.key === this.state.actived ? 'yo-tab-external-tabpane-active' : 'yo-tab-external-tabpane-inactive') + ' yo-tab-external-tabpane'
}
>
<ComponentDynamic
path={pane.path}
id={pane.key}
key={pane.key}
param={pane.param}
/>
</div>
)
})
}
</div>
</div>
</Layout.Content>
)
}
}

View File

@@ -0,0 +1,36 @@
import React, { Component } from 'react'
import { Layout, Badge } from 'antd'
import Components from 'components'
import Logo from '../logo'
import User from './user'
const { AntIcon, Container } = Components
export default class index extends Component {
render() {
return (
<Layout.Header>
<Container mode="fluid">
<div className="header-actions">
<span className="header-action mr-md">
<AntIcon type="menu" />
</span>
<Logo />
{/* search */}
</div>
<div className="header-actions">
<span className="header-action">
<AntIcon type="reload" />
</span>
<span className="header-action">
<Badge count="5">
<AntIcon type="bell" />
</Badge>
</span>
<User />
</div>
</Container>
</Layout.Header>
)
}
}

View File

@@ -0,0 +1,121 @@
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { Modal, message as Message } from 'antd'
import { isEqual } from 'lodash'
import store from 'store'
import { api } from 'common/api'
import { token } from 'common/token'
import Components from 'components'
const { getState, subscribe } = store
const { AntIcon, Image } = Components
const storePath = 'user'
let userOpenTimer, userCloseTimer
let initDropdownHeight
class User extends Component {
state = {
dropdownHeight: 0,
user: getState(storePath)
}
constructor(props) {
super(props)
this.unsubscribe = subscribe(storePath, () => {
this.setState({
user: getState(storePath)
})
})
}
// shouldComponentUpdate(props, state) {
// return !isEqual(this.state, state)
// }
componentDidMount() {
initDropdownHeight = this.refs.dropdown.scrollHeight
}
componentWillUnmount() {
this.unsubscribe()
}
onOpen = (e) => {
clearTimeout(userCloseTimer)
this.refs.container.classList.add('open')
userOpenTimer = setTimeout(() => {
this.refs.container.classList.add('drop')
this.setState({
dropdownHeight: initDropdownHeight
})
}, 300)
}
onClose = (e) => {
clearTimeout(userOpenTimer)
this.refs.container.classList.remove('drop')
this.setState({
dropdownHeight: 0
})
userCloseTimer = setTimeout(() => {
this.refs.container.classList.remove('open')
}, 300)
}
onAccountSetting = () => { }
onLogout = () => {
Modal.confirm({
title: '提示',
content: '是否确定退出登录',
onOk: async () => {
const { success, message } = await api.logout()
if (success) {
token.value = ''
this.props.history.replace('/login')
} else {
Message.error(message)
}
}
})
}
render() {
return (
<div
className="user-container"
onMouseEnter={(e) => { this.onOpen(e) }}
onMouseLeave={(e) => { this.onClose(e) }}
ref="container"
>
<div className="user-container-inner">
<div className="user--base">
<Image width="32" type="avatar" className="user--avatar" icon={<AntIcon type="user" />} id={this.state.user.avatar} />
<span className="user--name">{this.state.user.nickName || this.state.user.name}</span>
</div>
<div className="user--dropdown" ref="dropdown" style={{ height: `${this.state.dropdownHeight}px` }}>
<ul className="ant-dropdown-menu ant-dropdown-menu-vertical">
<li className="ant-dropdown-menu-item" onClick={this.onAccountSetting}>
<AntIcon type="user" />
个人中心
</li>
<li className="ant-dropdown-menu-item-divider"></li>
<li className="ant-dropdown-menu-item" onClick={this.onLogout}>
<AntIcon type="logout" />
退出登录
</li>
</ul>
</div>
</div>
</div >
)
}
}
export default withRouter(User)

View File

@@ -0,0 +1,14 @@
import React, { Component } from 'react'
export default class index extends Component {
logo = require('assets/image/logo-w.png')
render() {
return (
<div className="logo">
<img src={this.logo.default} alt="" />
</div>
)
}
}

View File

@@ -0,0 +1,69 @@
import React, { Component } from 'react'
import { Layout } from 'antd'
import Swiper, { Mousewheel, Scrollbar } from 'swiper'
import 'swiper/swiper-bundle.css'
import Menu from './menu'
Swiper.use([Mousewheel, Scrollbar])
let timer,
swiper
const siderSwiperOptions = {
direction: 'vertical',
slidesPerView: 'auto',
freeMode: true,
scrollbar: {
el: '#layout--swiper-scrollbar',
},
mousewheel: true,
}
const UpdateSwiper = () => {
clearTimeout(timer)
timer = setTimeout(() => {
// 需要更新两次
swiper.update()
swiper.update()
}, 300)
}
export default class index extends Component {
state = {}
componentDidMount() {
swiper = new Swiper('#layout--swiper-container', siderSwiperOptions)
window.addEventListener('resize', () => {
UpdateSwiper()
})
}
updateSwiper() {
UpdateSwiper()
}
static getDerivedStateFromProps(props, state) {
UpdateSwiper()
return null
}
render() {
return (
<Layout.Sider className="yo-layout-sider" width="200" collapsed={false}>
<div className="yo-sider-nav">
<div className="swiper-container" id="layout--swiper-container">
<div className="swiper-wrapper">
<div className="swiper-slide">
<Menu parent={this} menuStyle={{ height: '100%', borderRight: 0 }} nav={this.props.nav} />
</div>
</div>
<div className="swiper-scrollbar" id="layout--swiper-scrollbar"></div>
</div>
</div>
</Layout.Sider>
)
}
}

View File

@@ -0,0 +1,71 @@
import React, { Component } from 'react'
import { Menu } from 'antd'
import AntIcon from 'components/ant-icon'
export default class index extends Component {
renderMenu = (menu) => {
return menu.map((p) => {
return p.children ? this.renderSubMenu(p) : this.renderMenuItem(p)
})
}
renderSubMenu = (menu) => {
return (
<Menu.SubMenu key={menu.id} title={menu.meta.title} icon={menu.meta.icon && <AntIcon type={menu.meta.icon} />}>
{this.renderMenu(menu.children)}
</Menu.SubMenu>
)
}
renderMenuItem = (menu) => {
return (
<Menu.Item key={menu.id} onClick={() => this.onOpenContentWindow(menu)}>
{menu.meta.icon && <AntIcon type={menu.meta.icon} />}
<span>{menu.meta.title}</span>
</Menu.Item>
)
}
onOpenContentWindow = (menu) => {
window.openContentWindow({
key: menu.id,
title: menu.meta.title,
icon: menu.meta.icon,
path: menu.component
})
}
onMenuOpenChange = () => {
this.props.parent.updateSwiper()
}
render() {
const props = {
mode: 'inline',
selectable: false,
style: this.props.menuStyle,
theme: 'light'
}
const on = {
onOpenChange: this.onMenuOpenChange,
}
return (
<>
{
this.props.nav.map((item, i) => {
return (
<section key={i}>
<div className="yo-sider-nav--app">{item.app.name}</div>
<Menu {...props} {...on}>{this.renderMenu(item.menu)}</Menu>
</section>
)
})
}
</>
)
}
}

View File

@@ -0,0 +1,245 @@
import React, { Component } from 'react'
import { Spin, Layout } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { api } from 'common/api'
import { cloneDeep, groupBy, findIndex, last } from 'lodash'
import store from 'store'
import { EMPTY_ID } from 'util/global'
import Header from './_layout/header'
import Sider from './_layout/sider'
import Content from './_layout/content'
const { dispatch } = store
const serializeMenu = (menus) => {
const menu = cloneDeep(menus)
const children = groupBy(menu, 'pid')
const serialize = (m) => {
m.forEach((p) => {
if (children[p.id]) {
p.children = serialize(children[p.id])
}
})
return m
}
return children[EMPTY_ID] ? serialize(children[EMPTY_ID]) : []
}
const setNav = async (nav) => {
const getNav = []
nav.apps.forEach((app) => {
getNav.push({
app,
})
})
const menus = await api
.$queue(getNav.map((p) => api.sysMenuChangeAwait({ application: p.app.code })))
menus.forEach((menu, i) => {
getNav[i].menu = serializeMenu(menu.data)
})
return getNav
}
const getNewID = () => {
return Math.random().toString(16).slice(2)
}
export default class index extends Component {
state = {
loading: true,
nav: [],
panes: [],
actived: '',
test: 0
}
componentDidMount() {
window.openContentWindow = this.onOpenContentWindow
window.closeContentWindow = this.onCloseContentWindow
window.closeOtherContentWindow = this.onCloseOtherContentWindow
window.closeRightContentWindow = this.onCloseRightContentWindow
api.getLoginUser().then(async ({ data }) => {
const nav = await setNav(data)
dispatch({
type: 'SET_USER_ACCOUNT',
user: data
})
this.setState({ loading: false, nav }, () => {
this.onOpenContentWindow({
title: '工作台',
path: '/home',
icon: 'home',
closable: false,
})
})
})
}
/**
* 打开窗口
* @param {*} settings
* @returns
*/
onOpenContentWindow = (settings) => {
if (settings.path) {
const key = settings.key || getNewID();
/**
* 如果当前标签页已打开,则只需要选中
*/
const pane = this.state.panes.find((p) => p.key === key);
if (pane) {
this.onChangeContentWindow(key);
return;
}
const path = settings.path.startsWith('/') ? settings.path : `/${settings.path}`;
/**
* 向标签页队列中添加一个新的标签页
*/
const newPane = {
key,
closable: settings.closable === undefined ? true : settings.closable,
icon: settings.icon,
title: settings.title || '新建窗口',
subTitle: settings.subTitle,
component: null,
path,
param: settings.param,
loaded: false,
}
this.setState({
panes: [...this.state.panes, newPane]
})
this.onChangeContentWindow(key);
//this.$refs.content.onLoadContentWindow(key);
} else {
console.warn('wrong component path');
}
}
/**
* 关闭窗口
* @param {*} key
*/
onCloseContentWindow = (key) => {
key = key || this.state.actived
const panes = this.state.panes
const i = findIndex(panes, (p) => p.key === key)
panes.splice(i, 1)
this.setState({
panes
}, () => {
if (panes.length) {
if (key === this.state.actived) {
const pane = panes[i]
if (pane) {
this.onChangeContentWindow(pane.key)
} else {
this.onChangeContentWindow(panes[i - 1].key)
}
}
}
})
}
onCloseOtherContentWindow = (key) => {
if (!key) return
const panes = this.state.panes
let flag = false
for (let i = panes.length - 1; i >= 0; i--) {
const p = panes[i]
if (p.key !== key && p.closable) {
if (p.key === this.state.actived) {
flag = true;
}
panes.splice(i, 1);
}
}
this.setState({
panes
}, () => {
if (flag) {
this.onChangeContentWindow(last(panes).key);
}
})
}
onCloseRightContentWindow = (key) => {
if (!key) return
const panes = this.state.panes
let flag = false
for (let i = panes.length - 1; i >= 0; i--) {
const p = panes[i]
if (p.key !== key && p.closable) {
if (p.key === this.tabActived) {
flag = true
}
panes.splice(i, 1)
} else if (p.key === key) {
break
}
}
this.setState({
panes
}, () => {
if (flag) {
this.onChangeContentWindow(last(panes).key);
}
})
}
onReloadContentWindow = (key) => {
if (!key) key = this.state.actived
//this.$refs.content.onLoadContentWindow(key);
}
/**
* 选中窗口
* @param {*} key
*/
onChangeContentWindow = (key) => {
this.setState({
actived: key
})
}
render() {
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />
return (
<section className="yo-layout--spin">
<Spin spinning={this.state.loading} indicator={antIcon}>
<Layout className="yo-layout--top-nav--container-fluid yo-layout--top-nav">
<Layout>
<Header />
<Layout className="yo-nav-theme--light">
<Sider nav={this.state.nav} />
<Content parent={this} panes={this.state.panes} actived={this.state.actived} />
</Layout>
</Layout>
</Layout>
</Spin>
</section>
)
}
}