Update: refactor proxy

This commit is contained in:
jas0ncn 2018-10-27 22:53:44 +08:00
parent 498e7c1f7c
commit 27af0a636b
18 changed files with 314 additions and 64 deletions

View File

@ -3,6 +3,6 @@
.card { .card {
padding: 15px; padding: 15px;
box-shadow: 0 0 20px rgba($color-primary-dark, 0.2); box-shadow: 0 0 20px rgba($color-primary-dark, 0.2);
background-color: #fff; background-color: $color-white;
border-radius: 4px; border-radius: 4px;
} }

View File

@ -44,7 +44,7 @@ $width: 32px;
height: $switch-radius; height: $switch-radius;
width: $switch-radius; width: $switch-radius;
border-radius: $switch-radius / 2; border-radius: $switch-radius / 2;
background-color: #fff; background-color: $color-white;
box-shadow: 0 0 8px rgba($color-primary-dark, 0.4); box-shadow: 0 0 8px rgba($color-primary-dark, 0.4);
transition: transform 0.3s ease; transition: transform 0.3s ease;
transform: translateX($width - $switch-radius + $switch-offset); transform: translateX($width - $switch-radius + $switch-offset);
@ -54,6 +54,6 @@ $width: 32px;
.switch-icon { .switch-icon {
position: absolute; position: absolute;
transform: translateX(13px) scale(0.4); transform: translateX(13px) scale(0.4);
color: #fff; color: $color-white;
line-height: $height; line-height: $height;
} }

View File

@ -0,0 +1,76 @@
import * as React from 'react'
import { translate } from 'react-i18next'
import classnames from 'classnames'
import { BaseComponentProps, Proxy as IProxy, I18nProps, TagColors } from '@models'
import { Modal, Row, Col } from '@components'
import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper'
import './style.scss'
interface ModifyProxyDialogProps extends BaseComponentProps, I18nProps {
config: IProxy
onOk?: (config: IProxy) => void
onCancel?: () => void
}
interface ModifyProxyDialogState {
config: IProxy
currentColor: string
}
class RawDialog extends React.Component<ModifyProxyDialogProps, ModifyProxyDialogState> {
constructor (props: ModifyProxyDialogProps) {
super(props)
this.state = {
config: props.config,
currentColor: getLocalStorageItem(props.config.name)
}
}
componentDidMount () {
console.log(this.props.config)
}
handleOk = () => {
const { onOk } = this.props
const { config, currentColor } = this.state
setLocalStorageItem(config.name, currentColor)
onOk(config)
}
render () {
const { onCancel, t } = this.props
const { currentColor } = this.state
return <Modal
className="proxy-editor"
title={t('editDialog.title')}
onOk={this.handleOk}
onClose={onCancel}
>
<Row gutter={24} style={{ padding: '12px 0' }}>
<Col span={6} style={{ paddingLeft: 0 }}>{t('editDialog.color')}</Col>
<Col span={18}>
<div className="proxy-editor-color-selector">
{
TagColors.map(color => (
<span
className={classnames('color-item', {
'color-item-active': currentColor === color
})}
key={color}
style={{ background: color }}
onClick={() => this.setState({ currentColor: color })}
/>
))
}
</div>
</Col>
</Row>
</Modal>
}
}
export const ModifyProxyDialog = translate(['Proxies'])(RawDialog)

View File

@ -0,0 +1,29 @@
@import '~@styles/variables';
.proxy-editor {
.proxy-editor-color-selector {
display: flex;
align-items: center;
.color-item {
position: relative;
margin-right: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
cursor: pointer;
}
.color-item-active::after {
position: absolute;
left: -3px;
top: -3px;
content: '';
display: block;
width: 22px;
height: 22px;
border-radius: 50%;
border: 1px solid $color-gray-dark;
}
}
}

View File

@ -1,24 +1,51 @@
import * as React from 'react' import * as React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { BaseComponentProps, Proxy as IProxy } from '@models' import { Icon } from '@components'
import { BaseComponentProps, Proxy as IProxy, TagColors } from '@models'
import { getProxyDelay } from '@lib/request' import { getProxyDelay } from '@lib/request'
import { to } from '@lib/helper' import { to, getLocalStorageItem, setLocalStorageItem, sample, noop } from '@lib/helper'
import './style.scss' import './style.scss'
interface ProxyProps extends BaseComponentProps { interface ProxyProps extends BaseComponentProps {
config: IProxy config: IProxy
onEdit?: (e: React.MouseEvent<HTMLElement>) => void
} }
interface ProxyState { interface ProxyState {
delay: number delay: number
hasError: boolean hasError: boolean
color: string
} }
export class Proxy extends React.Component<ProxyProps , ProxyState> { export class Proxy extends React.Component<ProxyProps , ProxyState> {
state = { constructor (props) {
delay: -1, super(props)
hasError: false
const { config } = props
const { name } = config
let color = getLocalStorageItem(name)
if (!color) {
color = sample(TagColors)
setLocalStorageItem(name, color)
}
this.state = {
delay: -1,
hasError: false,
color
}
}
componentWillUpdate () {
const { config: { name } } = this.props
const { color: rawColor } = this.state
const color = getLocalStorageItem(name)
if (rawColor !== color) {
this.setState({ color })
}
} }
async componentDidMount () { async componentDidMount () {
@ -34,13 +61,16 @@ export class Proxy extends React.Component<ProxyProps , ProxyState> {
} }
render () { render () {
const { config, className } = this.props const { config, className, onEdit = noop } = this.props
const { delay, hasError } = this.state const { delay, color, hasError } = this.state
const backgroundColor = hasError ? undefined : color
return ( return (
<div className={classnames('proxy-item', { 'proxy-error': hasError }, className)}> <div className={classnames('proxy-item', { 'proxy-error': hasError }, className)}>
<span className="proxy-name">{config.name}</span> <span className="proxy-type" style={{ backgroundColor }}>{config.type}</span>
<span className="proxy-delay">{delay === -1 ? '-' : `${delay}s`}</span> <p className="proxy-name">{config.name}</p>
<p className="proxy-delay">{delay === -1 ? '-' : `${delay}ms`}</p>
<Icon className="proxy-editor" type="setting" onClick={onEdit} />
</div> </div>
) )
} }

View File

@ -1,37 +1,65 @@
@import '~@styles/variables'; @import '~@styles/variables';
.proxy-item { .proxy-item {
display: flex; position: relative;
flex-direction: column; padding: 10px;
align-items: center; height: 110px;
justify-content: center; width: 110px;
height: 100px;
width: 100px;
box-shadow: 0 0 20px rgba($color-primary-dark, 0.2);
border-radius: 4px; border-radius: 4px;
background: $color-white;
user-select: none;
cursor: default;
box-shadow: 0 0 20px rgba($color-primary-dark, 0.2);
transition: all 300ms ease;
.proxy-icon { .proxy-type {
height: 40px; padding: 2px 5px;
width: 40px; font-size: 10px;
color: $color-white;
border-radius: 2px;
} }
.proxy-name { .proxy-name {
width: 80%; max-height: 30px;
margin-top: 8px; margin-top: 10px;
color: $color-primary-dark; color: $color-primary-darken;
font-size: 12px; font-size: 12px;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-align: center;
} }
.proxy-delay { .proxy-delay {
width: 80%; position: absolute;
margin-top: 8px; left: 10px;
color: $color-primary-dark; bottom: 13px;
font-size: 12px; font-size: 10px;
text-overflow: ellipsis; color: rgba($color: $color-primary-darken, $alpha: 0.8);
overflow: hidden; }
text-align: center;
.proxy-editor {
position: absolute;
right: 10px;
bottom: 10px;
cursor: pointer;
color: rgba($color: $color-primary-darken, $alpha: 0.8);
opacity: 0;
pointer-events: none;
transition: all 300ms ease;
}
&:hover {
box-shadow: 0 10px 20px rgba($color-primary-darken, 0.4);
.proxy-editor {
opacity: 1;
pointer-events: visible;
}
}
}
.proxy-error {
opacity: 0.5;
.proxy-type {
background-color: $color-gray-darken;
} }
} }

View File

@ -1 +1,2 @@
export * from './Proxy' export * from './Proxy'
export * from './ModifyProxyDialog'

View File

@ -3,16 +3,26 @@ import { translate } from 'react-i18next'
import { inject, observer } from 'mobx-react' import { inject, observer } from 'mobx-react'
import { storeKeys } from '@lib/createStore' import { storeKeys } from '@lib/createStore'
import { Header, Icon } from '@components' import { Header, Icon } from '@components'
import { I18nProps, BaseRouterProps } from '@models' import { I18nProps, BaseRouterProps, Proxy as IProxy } from '@models'
import { Proxy } from './components' import { Proxy, ModifyProxyDialog } from './components'
import './style.scss' import './style.scss'
interface ProxiesProps extends BaseRouterProps, I18nProps {} interface ProxiesProps extends BaseRouterProps, I18nProps {}
interface ProxiesState {
showModifyProxyDialog: boolean
activeConfig?: IProxy
}
@inject(...storeKeys) @inject(...storeKeys)
@observer @observer
class Proxies extends React.Component<ProxiesProps, {}> { class Proxies extends React.Component<ProxiesProps, ProxiesState> {
state = {
showModifyProxyDialog: false,
activeConfig: null
}
componentDidMount () { componentDidMount () {
this.props.config.fetchAndParseConfig() this.props.config.fetchAndParseConfig()
@ -20,31 +30,46 @@ class Proxies extends React.Component<ProxiesProps, {}> {
render () { render () {
const { t, config } = this.props const { t, config } = this.props
const { showModifyProxyDialog, activeConfig } = this.state
return ( return (
<div className="page"> <>
<div className="proxies-container"> <div className="page">
<Header title={t('title')} > <div className="proxies-container">
<Icon type="plus" size={20} style={{ fontWeight: 'bold' }} /> <Header title={t('title')} >
</Header> <Icon type="plus" size={20} style={{ fontWeight: 'bold' }} />
{ </Header>
config.state === 'ok' && <ul className="proxies-list"> {
{ config.state === 'ok' && <ul className="proxies-list">
config.config.proxy.map( {
(p, index) => ( config.config.proxy.map((p, index) => (
<li key={index}> <li key={index}>
<Proxy config={p} /> <Proxy config={p} onEdit={() => this.setState({
showModifyProxyDialog: true,
activeConfig: p
})} />
</li> </li>
) ))
) }
} </ul>
</ul> }
</div>
<div className="proxies-container">
<Header title={t('groupTitle')} />
</div>
{
showModifyProxyDialog && <ModifyProxyDialog
config={activeConfig}
onOk={config => {
console.log(config)
this.setState({ showModifyProxyDialog: false, activeConfig: null })
}}
onCancel={() => this.setState({ showModifyProxyDialog: false, activeConfig: null })}
/>
} }
</div> </div>
<div className="proxies-container"> </>
<Header title={t('groupTitle')} />
</div>
</div>
) )
} }
} }

View File

@ -7,6 +7,11 @@
list-style: none; list-style: none;
li { li {
margin: 20px 15px 20px 0; margin: 8px 0;
margin-right: 15px;
&:nth-child(6n) {
margin-right: 0;
}
} }
} }

View File

@ -52,7 +52,7 @@
> i { > i {
transform: scale(0.5); transform: scale(0.5);
color: #fff; color: $color-white;
font-weight: bold; font-weight: bold;
} }
} }

View File

@ -1,11 +1,15 @@
@import '~@styles/variables'; @import '~@styles/variables';
.sidebar { .sidebar {
position: fixed;
top: 0;
left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
width: 140px; width: 140px;
user-select: none;
} }
.sidebar-logo { .sidebar-logo {
@ -42,7 +46,7 @@
> a.active { > a.active {
background: linear-gradient(135deg, $color-primary, $color-primary-dark); background: linear-gradient(135deg, $color-primary, $color-primary-dark);
color: #fff; color: $color-white;
box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.5); box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.5);
} }
} }

View File

@ -43,6 +43,21 @@ export default {
}, },
Proxies: { Proxies: {
title: 'Proxies', title: 'Proxies',
editDialog: {
title: 'Edit Proxy',
color: 'Color',
name: 'Name',
type: 'Type',
server: 'Server',
port: 'Port',
password: 'Password',
cipher: 'Cipher',
obfs: 'Obfs',
'obfs-host': 'Obfs-host',
uuid: 'Uuid',
alterid: 'Alterid',
tls: 'TLS'
},
groupTitle: 'Policy Group' groupTitle: 'Policy Group'
} }
} }

View File

@ -43,6 +43,21 @@ export default {
}, },
Proxies: { Proxies: {
title: '代理', title: '代理',
editDialog: {
title: '编辑代理',
color: '颜色',
name: '名字',
type: '类型',
server: '服务器',
port: '端口',
password: '密码',
cipher: '加密方式',
obfs: 'Obfs',
'obfs-host': 'Obfs-host',
uuid: 'Uuid',
alterid: 'Alterid',
tls: 'TLS'
},
groupTitle: '策略组' groupTitle: '策略组'
} }
} }

View File

@ -10,6 +10,16 @@ export function removeLocalStorageItem (key: string) {
return window.localStorage.removeItem(key) return window.localStorage.removeItem(key)
} }
export function randomNumber (min: number, max: number) {
return (min + Math.random() * (max - min)) >> 0
}
export function sample<T> (arr: T[]) {
return arr[randomNumber(0, arr.length)]
}
export function noop () {}
/** /**
* to return Promise<[T, Error]> * to return Promise<[T, Error]>
* @param {Promise<T>} promise * @param {Promise<T>} promise

7
src/models/TagColors.ts Normal file
View File

@ -0,0 +1,7 @@
export const TagColors = [
'#ff3e5e',
'#686fff',
'#ff9a28',
'#b83fe6',
'#00c520'
]

View File

@ -3,3 +3,4 @@ export * from './Config'
export * from './Proxy' export * from './Proxy'
export * from './Rule' export * from './Rule'
export * from './I18n' export * from './I18n'
export * from './TagColors'

View File

@ -22,8 +22,8 @@ body {
} }
.app { .app {
display: flex;
min-height: 100vh; min-height: 100vh;
padding-left: 150px;
} }
.app.clash-x { .app.clash-x {
@ -32,13 +32,15 @@ body {
.page-container { .page-container {
width: 100%; width: 100%;
margin: 20px 0; height: 100vh;
} }
.page { .page {
padding: 10px 35px; padding: 20px 35px;
padding-left: 0;
padding-bottom: 30px;
width: 100%; width: 100%;
height: 100%; height: 100vh;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;

View File

@ -6,7 +6,7 @@
@font-face { @font-face {
font-family: "clash-iconfont"; font-family: "clash-iconfont";
src: url('//at.alicdn.com/t/font_841708_h6oakuryxxb.ttf') format('truetype'); src: url('//at.alicdn.com/t/font_841708_7ge2gqse7qv.ttf') format('truetype');
} }
.clash-iconfont { .clash-iconfont {
@ -37,3 +37,5 @@
.icon-info::before { content: "\e60b"; } .icon-info::before { content: "\e60b"; }
.icon-info-o::before { content: "\e60c"; } .icon-info-o::before { content: "\e60c"; }
.icon-setting::before { content: "\e60d"; }