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-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": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.6.tgz",
|
||||
@ -1543,6 +1552,15 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz",
|
||||
@ -2580,6 +2598,11 @@
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.7.2.tgz",
|
||||
"integrity": "sha512-4Urocwu9+XLDJw4Tc6ZCg7APVjjLInCFvO4TwGsAYV5zT6YYSor14dsZR0+0tHlDIN92cFUOq+i7fC00G5vTxA=="
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||
@ -9777,6 +9805,16 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
|
||||
@ -9881,6 +9919,11 @@
|
||||
"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": {
|
||||
"version": "0.13.3",
|
||||
"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-i18next": "^7.8.2",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"@types/react-sortable-hoc": "^0.6.4",
|
||||
"autoprefixer": "^9.1.5",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-loader": "^8.0.2",
|
||||
@ -67,6 +68,7 @@
|
||||
"dayjs": "^1.7.5",
|
||||
"i18next": "^11.9.0",
|
||||
"i18next-browser-languagedetector": "^2.2.3",
|
||||
"immer": "^1.7.2",
|
||||
"ini": "^1.3.5",
|
||||
"mobx": "^5.1.2",
|
||||
"mobx-react": "^5.2.8",
|
||||
@ -76,6 +78,7 @@
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.12.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-sortable-hoc": "^0.8.3",
|
||||
"typescript": "^3.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,50 @@
|
||||
import * as React from 'react'
|
||||
import { BaseComponentProps } from '@models/BaseProps'
|
||||
// import classnames from 'classnames'
|
||||
import classnames from 'classnames'
|
||||
import './style.scss'
|
||||
|
||||
interface InputProps extends BaseComponentProps {
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
inside?: boolean
|
||||
autoFocus?: boolean
|
||||
onChange?: (value: string, event?: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onBlur?: (event?: React.FocusEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export class Input extends React.Component<InputProps, {}> {
|
||||
static defaultProps: InputProps = {
|
||||
value: '',
|
||||
disabled: false,
|
||||
onChange: () => {}
|
||||
align: 'center',
|
||||
inside: false,
|
||||
autoFocus: false,
|
||||
onChange: () => {},
|
||||
onBlur: () => {}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onChange, value } = this.props
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
value,
|
||||
align,
|
||||
inside,
|
||||
autoFocus,
|
||||
onChange,
|
||||
onBlur
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<input className="input" onChange={(event) => {
|
||||
onChange(event.target.value, event)
|
||||
}} value={value} ></input>
|
||||
<input
|
||||
className={classnames('input', `input-align-${align}`, { 'input-inside': inside }, className)}
|
||||
style={style}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
onChange={event => onChange(event.target.value, event)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,41 @@
|
||||
@import '~@styles/variables';
|
||||
|
||||
$height: 30px;
|
||||
|
||||
.input {
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
height: $height;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color-primary-lightly;
|
||||
padding: 4px 11px;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
color: $color-primary-darken;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color-primary-lightly;
|
||||
line-height: $height;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: $color-primary;
|
||||
color: $color-primary-dark;
|
||||
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 {
|
||||
padding: 0 $padding / 2;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@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 {
|
||||
cursor: not-allowed;
|
||||
background-color: $color-gray-dark;
|
||||
|
||||
&::after {
|
||||
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 './Tags'
|
||||
export * from './Input'
|
||||
export * from './Select'
|
||||
|
@ -1,15 +1,184 @@
|
||||
import * as React from 'react'
|
||||
import { Header } from '@components'
|
||||
import produce from 'immer'
|
||||
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 () {
|
||||
const { t } = this.props
|
||||
const { rules } = this.state
|
||||
const SortableList = SortableContainer<{ rules: Rule[] }>(this.renderRules)
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
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 {
|
||||
|
||||
type?: 'DOMAIN' | 'DOMAIN-SUFFIX' | 'DOMAIN-KEYWORD' | 'DOMAIN-SUFFIX' | 'GEOIP' | 'FINAL'
|
||||
type?: RuleType
|
||||
|
||||
value?: string
|
||||
|
||||
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 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user