mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Add: rules page
This commit is contained in:
parent
56aef42c7e
commit
2be0eb448f
43
package-lock.json
generated
43
package-lock.json
generated
@ -931,6 +931,15 @@
|
|||||||
"@types/react-router": "4.0.31"
|
"@types/react-router": "4.0.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-sortable-hoc": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-sortable-hoc/-/react-sortable-hoc-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-SlUZ9Ek4UURWtpezoMlK2DdSR/XG0+8uTGkAao21pNWMYSoNUrRCOAtI6quohBAWIXMP1k2qfRVRsuizIYppXw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "16.4.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.7.6",
|
"version": "1.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.6.tgz",
|
||||||
@ -1543,6 +1552,15 @@
|
|||||||
"util.promisify": "1.0.0"
|
"util.promisify": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-runtime": {
|
||||||
|
"version": "6.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
|
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "2.5.7",
|
||||||
|
"regenerator-runtime": "0.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bail": {
|
"bail": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz",
|
||||||
@ -2580,6 +2598,11 @@
|
|||||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"core-js": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
||||||
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
@ -6097,6 +6120,11 @@
|
|||||||
"is-cwebp-readable": "2.0.1"
|
"is-cwebp-readable": "2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"immer": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-4Urocwu9+XLDJw4Tc6ZCg7APVjjLInCFvO4TwGsAYV5zT6YYSor14dsZR0+0tHlDIN92cFUOq+i7fC00G5vTxA=="
|
||||||
|
},
|
||||||
"import-cwd": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||||
@ -9777,6 +9805,16 @@
|
|||||||
"warning": "4.0.2"
|
"warning": "4.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-sortable-hoc": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-vt2qQ9DnPLjGZ9osM2jBULdi7WfFXtYVuHvjHX8o2em7Rcla9FXIG60aWFbvvpFC1iXyATw5PWZX0B57EUOYfQ==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"invariant": "2.2.4",
|
||||||
|
"prop-types": "15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-all-stream": {
|
"read-all-stream": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
|
||||||
@ -9881,6 +9919,11 @@
|
|||||||
"regenerate": "1.4.0"
|
"regenerate": "1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||||
|
},
|
||||||
"regenerator-transform": {
|
"regenerator-transform": {
|
||||||
"version": "0.13.3",
|
"version": "0.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz",
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
"@types/react-dom": "^16.0.7",
|
"@types/react-dom": "^16.0.7",
|
||||||
"@types/react-i18next": "^7.8.2",
|
"@types/react-i18next": "^7.8.2",
|
||||||
"@types/react-router-dom": "^4.3.1",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
|
"@types/react-sortable-hoc": "^0.6.4",
|
||||||
"autoprefixer": "^9.1.5",
|
"autoprefixer": "^9.1.5",
|
||||||
"awesome-typescript-loader": "^5.2.1",
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
"babel-loader": "^8.0.2",
|
"babel-loader": "^8.0.2",
|
||||||
@ -67,6 +68,7 @@
|
|||||||
"dayjs": "^1.7.5",
|
"dayjs": "^1.7.5",
|
||||||
"i18next": "^11.9.0",
|
"i18next": "^11.9.0",
|
||||||
"i18next-browser-languagedetector": "^2.2.3",
|
"i18next-browser-languagedetector": "^2.2.3",
|
||||||
|
"immer": "^1.7.2",
|
||||||
"ini": "^1.3.5",
|
"ini": "^1.3.5",
|
||||||
"mobx": "^5.1.2",
|
"mobx": "^5.1.2",
|
||||||
"mobx-react": "^5.2.8",
|
"mobx-react": "^5.2.8",
|
||||||
@ -76,6 +78,7 @@
|
|||||||
"react-dom": "^16.5.2",
|
"react-dom": "^16.5.2",
|
||||||
"react-i18next": "^7.12.0",
|
"react-i18next": "^7.12.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
"react-sortable-hoc": "^0.8.3",
|
||||||
"typescript": "^3.0.3"
|
"typescript": "^3.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,50 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { BaseComponentProps } from '@models/BaseProps'
|
import { BaseComponentProps } from '@models/BaseProps'
|
||||||
// import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
interface InputProps extends BaseComponentProps {
|
interface InputProps extends BaseComponentProps {
|
||||||
value?: string | number
|
value?: string | number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
align?: 'left' | 'center' | 'right'
|
||||||
|
inside?: boolean
|
||||||
|
autoFocus?: boolean
|
||||||
onChange?: (value: string, event?: React.ChangeEvent<HTMLInputElement>) => void
|
onChange?: (value: string, event?: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
onBlur?: (event?: React.FocusEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Input extends React.Component<InputProps, {}> {
|
export class Input extends React.Component<InputProps, {}> {
|
||||||
static defaultProps: InputProps = {
|
static defaultProps: InputProps = {
|
||||||
value: '',
|
value: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onChange: () => {}
|
align: 'center',
|
||||||
|
inside: false,
|
||||||
|
autoFocus: false,
|
||||||
|
onChange: () => {},
|
||||||
|
onBlur: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { onChange, value } = this.props
|
const {
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
align,
|
||||||
|
inside,
|
||||||
|
autoFocus,
|
||||||
|
onChange,
|
||||||
|
onBlur
|
||||||
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input className="input" onChange={(event) => {
|
<input
|
||||||
onChange(event.target.value, event)
|
className={classnames('input', `input-align-${align}`, { 'input-inside': inside }, className)}
|
||||||
}} value={value} ></input>
|
style={style}
|
||||||
|
value={value}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onChange={event => onChange(event.target.value, event)}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,41 @@
|
|||||||
@import '~@styles/variables';
|
@import '~@styles/variables';
|
||||||
|
|
||||||
|
$height: 30px;
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 30px;
|
height: $height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 3px;
|
padding: 0 10px;
|
||||||
border: 1px solid $color-primary-lightly;
|
|
||||||
padding: 4px 11px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $color-primary-darken;
|
color: $color-primary-darken;
|
||||||
text-align: center;
|
border-radius: 3px;
|
||||||
line-height: 1.5;
|
border: 1px solid $color-primary-lightly;
|
||||||
|
line-height: $height;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: $color-primary;
|
border-color: $color-primary;
|
||||||
|
color: $color-primary-dark;
|
||||||
box-shadow: 0 2px 5px rgba($color: $color-primary, $alpha: 0.5);
|
box-shadow: 0 2px 5px rgba($color: $color-primary, $alpha: 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-inside {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ $padding: 12px;
|
|||||||
.column {
|
.column {
|
||||||
padding: 0 $padding / 2;
|
padding: 0 $padding / 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $c from 1 through 24 {
|
@for $c from 1 through 24 {
|
||||||
|
166
src/components/Select/index.tsx
Normal file
166
src/components/Select/index.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { Icon } from '@components'
|
||||||
|
import { BaseComponentProps } from '@models'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
|
type OptionValue = string | number
|
||||||
|
|
||||||
|
interface SelectProps extends BaseComponentProps {
|
||||||
|
/**
|
||||||
|
* selected value
|
||||||
|
* must match one of options
|
||||||
|
*/
|
||||||
|
value: OptionValue
|
||||||
|
|
||||||
|
onSelect?: (value: OptionValue, e: React.MouseEvent<HTMLLIElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectState {
|
||||||
|
dropdownListStyles: React.CSSProperties
|
||||||
|
showDropDownList: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Select extends React.Component<SelectProps, SelectState> {
|
||||||
|
|
||||||
|
// portal container
|
||||||
|
$container: Element
|
||||||
|
|
||||||
|
// drop down list
|
||||||
|
$attachment = React.createRef<HTMLDivElement>()
|
||||||
|
|
||||||
|
// target position element
|
||||||
|
$target = React.createRef<HTMLDivElement>()
|
||||||
|
|
||||||
|
state = {
|
||||||
|
dropdownListStyles: {},
|
||||||
|
showDropDownList: false
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
// create container element
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
this.$container = container
|
||||||
|
|
||||||
|
document.addEventListener('click', this.handleGlobalClick, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.setState({ dropdownListStyles: this.calculateAttachmentPosition() })
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.body.removeChild(this.$container)
|
||||||
|
document.removeEventListener('click', this.handleGlobalClick, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGlobalClick = (e) => {
|
||||||
|
const el = this.$attachment.current
|
||||||
|
|
||||||
|
if (el && !el.contains(e.target)) {
|
||||||
|
this.setState({ showDropDownList: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateAttachmentPosition () {
|
||||||
|
const targetRectInfo = this.$target.current.getBoundingClientRect()
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: Math.floor(targetRectInfo.top) - 10,
|
||||||
|
left: Math.floor(targetRectInfo.left) - 10,
|
||||||
|
width: Math.floor(targetRectInfo.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectedOption = (value: OptionValue, children: React.ReactNode) => {
|
||||||
|
let matchChild: React.ReactElement<any> = null
|
||||||
|
|
||||||
|
React.Children.forEach(children, (child: React.ReactElement<any>) => {
|
||||||
|
if (child.props && child.props.value === value) {
|
||||||
|
matchChild = child
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return matchChild
|
||||||
|
}
|
||||||
|
|
||||||
|
private hookChildren = (
|
||||||
|
children: React.ReactNode,
|
||||||
|
value: OptionValue,
|
||||||
|
onSelect: SelectProps['onSelect']
|
||||||
|
) => {
|
||||||
|
return React.Children.map(children, (child: React.ReactElement<any>) => {
|
||||||
|
if (!child.props || !child.type) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
// add classname for selected option
|
||||||
|
const className = child.props.value === value
|
||||||
|
? classnames(child.props.className, 'selected')
|
||||||
|
: child.props.className
|
||||||
|
|
||||||
|
// hook element onclick event
|
||||||
|
const rawOnClickEvent = child.props.onClick
|
||||||
|
return React.cloneElement(child, Object.assign({}, child.props, {
|
||||||
|
onClick: (e: React.MouseEvent<HTMLLIElement>) => {
|
||||||
|
onSelect(child.props.value, e)
|
||||||
|
this.setState({ showDropDownList: false })
|
||||||
|
rawOnClickEvent && rawOnClickEvent(e)
|
||||||
|
},
|
||||||
|
className
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { value, onSelect, children, className: cn, style } = this.props
|
||||||
|
const { dropdownListStyles, showDropDownList } = this.state
|
||||||
|
const matchChild = this.getSelectedOption(value, children)
|
||||||
|
const dropDownList = (
|
||||||
|
<div
|
||||||
|
className={classnames('select-list', { 'select-list-show': showDropDownList })}
|
||||||
|
ref={this.$attachment}
|
||||||
|
style={dropdownListStyles}
|
||||||
|
>
|
||||||
|
<ul className="list">
|
||||||
|
{this.hookChildren(children, value, onSelect)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div
|
||||||
|
className={classnames('select', cn)}
|
||||||
|
style={style}
|
||||||
|
ref={this.$target}
|
||||||
|
onClick={() => this.setState({ showDropDownList: !showDropDownList })}
|
||||||
|
>
|
||||||
|
{matchChild.props.children}
|
||||||
|
<Icon type="triangle-down" />
|
||||||
|
</div>
|
||||||
|
{createPortal(dropDownList, this.$container)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionProps extends BaseComponentProps {
|
||||||
|
key: React.Key
|
||||||
|
value: OptionValue
|
||||||
|
disabled?: boolean
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLLIElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Option extends React.Component<OptionProps, {}> {
|
||||||
|
render () {
|
||||||
|
const { className: cn, style, key, disabled = false, children, onClick = () => {} } = this.props
|
||||||
|
const className = classnames('option', { disabled }, cn)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={className} style={style} key={key} onClick={onClick}>{children}</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
65
src/components/Select/style.scss
Normal file
65
src/components/Select/style.scss
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@import '~@styles/variables';
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-list {
|
||||||
|
position: fixed;
|
||||||
|
max-width: 170px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 5px rgba($color: $color-gray-dark, $alpha: 0.5);
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: scaleY(0);
|
||||||
|
transform-origin: top;
|
||||||
|
transition: all 200ms linear;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
background: $color-white;
|
||||||
|
padding: 5px 0;
|
||||||
|
transform: scaleY(2);
|
||||||
|
transform-origin: top;
|
||||||
|
transition: all 200ms linear;
|
||||||
|
|
||||||
|
> .option {
|
||||||
|
color: $color-primary-darken;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($color: $color-primary-lightly, $alpha: 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .selected {
|
||||||
|
background: rgba($color: $color-primary-lightly, $alpha: 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-list-show {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: visible;
|
||||||
|
transform: scaleY(1);
|
||||||
|
|
||||||
|
.list {
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
}
|
@ -24,10 +24,12 @@ $width: 32px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
background-color: $color-gray-dark;
|
background-color: $color-gray-dark;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
|
box-shadow: 0 0 8px rgba($color-gray-darken, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,3 +7,4 @@ export * from './Col'
|
|||||||
export * from './ButtonSelect'
|
export * from './ButtonSelect'
|
||||||
export * from './Tags'
|
export * from './Tags'
|
||||||
export * from './Input'
|
export * from './Input'
|
||||||
|
export * from './Select'
|
||||||
|
@ -1,15 +1,184 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Header } from '@components'
|
import produce from 'immer'
|
||||||
import { translate } from 'react-i18next'
|
import { translate } from 'react-i18next'
|
||||||
import { I18nProps } from '@models'
|
import { SortableContainer, SortableElement, SortableHandle, arrayMove } from 'react-sortable-hoc'
|
||||||
|
import { Header, Icon, Card, Row, Col, Select, Option, Input } from '@components'
|
||||||
|
import { I18nProps, RuleType } from '@models'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
|
interface Rule {
|
||||||
|
type: RuleType
|
||||||
|
payload: string
|
||||||
|
proxy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RulesState {
|
||||||
|
rules: Rule[]
|
||||||
|
proxies: { [key: string]: { type: string } }
|
||||||
|
modifiedIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rules extends React.Component<I18nProps, RulesState> {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
rules: [
|
||||||
|
{ type: RuleType['DOMAIN-SUFFIX'], payload: 'google.com.hk', proxy: 'HK' },
|
||||||
|
{ type: RuleType['DOMAIN-SUFFIX'], payload: 'twitter.com', proxy: 'HKG' },
|
||||||
|
{ type: RuleType['DOMAIN'], payload: 'pornhub.com', proxy: 'HKG' },
|
||||||
|
{ type: RuleType['FINAL'], payload: '', proxy: 'DIRECT' }
|
||||||
|
],
|
||||||
|
proxies: {
|
||||||
|
DIRECT: { type: 'Direct' },
|
||||||
|
GLOBAL: { type: 'Selector' },
|
||||||
|
HKG: { type: 'URLTest' },
|
||||||
|
HK: { type: 'Shadowsocks' },
|
||||||
|
SGGGGGGGGGG: { type: 'Vmess' },
|
||||||
|
REJECT: { type: 'Reject' }
|
||||||
|
},
|
||||||
|
modifiedIndex: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleModifyType = (index, type) => {
|
||||||
|
const { rules } = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
rules: produce(rules, draftState => {
|
||||||
|
draftState[index].type = type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleModifyPayload = (index, payload) => {
|
||||||
|
const { rules } = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
rules: produce(rules, draftState => {
|
||||||
|
draftState[index].payload = payload
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleModifyProxy = (index, proxy) => {
|
||||||
|
const { rules } = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
rules: produce(rules, draftState => {
|
||||||
|
draftState[index].proxy = proxy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||||
|
this.setState({
|
||||||
|
rules: arrayMove(this.state.rules, oldIndex, newIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRules = ({ rules }) => {
|
||||||
|
const SortableItem = SortableElement<{ rule: Rule, idx: number }>(itemProps => {
|
||||||
|
const { rule, idx } = itemProps
|
||||||
|
return this.renderRuleItem(rule, idx)
|
||||||
|
})
|
||||||
|
|
||||||
|
return <ul>
|
||||||
|
{
|
||||||
|
rules.map((rule: Rule, idx: number) => {
|
||||||
|
const isFinal = rule.type === 'FINAL'
|
||||||
|
return <SortableItem key={idx} index={idx} idx={idx} rule={rule} disabled={isFinal} />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRuleItem = (rule: Rule, index) => {
|
||||||
|
const { modifiedIndex, proxies } = this.state
|
||||||
|
const isFinal = rule.type === 'FINAL'
|
||||||
|
const DragHandle = SortableHandle(() => <Icon type="drag" size={16} />)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="rule-item" key={index}>
|
||||||
|
<Row className="rule-item-row" gutter={24} align="middle">
|
||||||
|
<Col className="drag-handler" span={1}>
|
||||||
|
{!isFinal && <DragHandle />}
|
||||||
|
</Col>
|
||||||
|
<Col className="rule-type" span={5}>
|
||||||
|
{
|
||||||
|
isFinal
|
||||||
|
? rule.type
|
||||||
|
: (
|
||||||
|
<Select value={rule.type} onSelect={type => this.handleModifyType(index, type)}>
|
||||||
|
{
|
||||||
|
Object.keys(RuleType)
|
||||||
|
.filter(type => type !== 'FINAL')
|
||||||
|
.map(typeName => {
|
||||||
|
const type = RuleType[typeName]
|
||||||
|
return (
|
||||||
|
<Option value={type} key={type}>{type}</Option>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
<Col className="payload" span={8}>
|
||||||
|
{
|
||||||
|
modifiedIndex === index
|
||||||
|
? (
|
||||||
|
<Input
|
||||||
|
value={rule.payload}
|
||||||
|
align="left"
|
||||||
|
inside={true}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={ value => this.handleModifyPayload(index, value) }
|
||||||
|
onBlur={() => this.setState({ modifiedIndex: -1 })}
|
||||||
|
style={{ maxWidth: 230 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: <span onClick={() => this.setState({
|
||||||
|
modifiedIndex: index
|
||||||
|
})}>{rule.payload}</span>
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
<Col className="rule-proxy" span={5}>
|
||||||
|
<Select className="rule-proxy-option" value={rule.proxy} onSelect={proxy => this.handleModifyProxy(index, proxy)}>
|
||||||
|
{
|
||||||
|
Object.keys(proxies).map(proxyName => {
|
||||||
|
const proxy = proxies[proxyName]
|
||||||
|
return (
|
||||||
|
<Option className="rule-proxy-option" value={proxyName} key={`${proxyName}-${proxy.type}`}>
|
||||||
|
<span className="label">{proxy.type}</span>
|
||||||
|
<span className="value">{proxyName}</span>
|
||||||
|
</Option>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col className="delete-btn" span={2} offset={3}>
|
||||||
|
{!isFinal && <span>删除</span>}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Rules extends React.Component<I18nProps, {}> {
|
|
||||||
render () {
|
render () {
|
||||||
const { t } = this.props
|
const { t } = this.props
|
||||||
|
const { rules } = this.state
|
||||||
|
const SortableList = SortableContainer<{ rules: Rule[] }>(this.renderRules)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<Header title={t('title')} />
|
<Header title={t('title')} >
|
||||||
|
<Icon type="plus" size={20} style={{ fontWeight: 'bold', cursor: 'pointer' }} />
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Card className="rules-card">
|
||||||
|
<div className="rules">
|
||||||
|
<SortableList rules={rules} onSortEnd={this.onSortEnd} useDragHandle={true} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
96
src/containers/Rules/style.scss
Normal file
96
src/containers/Rules/style.scss
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
@import '~@styles/variables';
|
||||||
|
|
||||||
|
.rules-card {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 5px 0;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
border-bottom: 1px solid rgba($color: $color-primary-lightly, $alpha: 0.5);
|
||||||
|
|
||||||
|
.rule-item-row {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handler {
|
||||||
|
cursor: row-resize;
|
||||||
|
margin: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-type {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payload {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-proxy {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-primary-darken;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 300ms ease;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-red;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-proxy-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: 5px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: $color-white;
|
||||||
|
background: $color-gray-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
line-height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,17 @@
|
|||||||
export interface Rule {
|
export interface Rule {
|
||||||
|
|
||||||
type?: 'DOMAIN' | 'DOMAIN-SUFFIX' | 'DOMAIN-KEYWORD' | 'DOMAIN-SUFFIX' | 'GEOIP' | 'FINAL'
|
type?: RuleType
|
||||||
|
|
||||||
value?: string
|
value?: string
|
||||||
|
|
||||||
use?: string // proxy or proxy group name
|
use?: string // proxy or proxy group name
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RuleType {
|
||||||
|
DOMAIN = 'DOMAIN',
|
||||||
|
'DOMAIN-SUFFIX' = 'DOMAIN-SUFFIX',
|
||||||
|
'DOMAIN-KEYWORD' = 'DOMAIN-KEYWORD',
|
||||||
|
'GEOIP' = 'GEOIP',
|
||||||
|
'FINAL' = 'FINAL'
|
||||||
|
}
|
||||||
|
@ -18,12 +18,12 @@ html {
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.clash-x {
|
.app.clash-x {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user