mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Add: setting page
This commit is contained in:
parent
c7ce6a1bca
commit
e8bde550a3
@ -2,7 +2,10 @@
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"font-family-no-missing-generic-family-keyword": null
|
||||
"font-family-no-missing-generic-family-keyword": null,
|
||||
"at-rule-no-unknown": [true, {
|
||||
"ignoreAtRules": ["for", "function", "if", "each", "include", "mixin"]
|
||||
}]
|
||||
},
|
||||
"ignoreFiles": [
|
||||
"**/*.ts",
|
||||
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -2888,6 +2888,11 @@
|
||||
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
|
||||
"dev": true
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.7.5.tgz",
|
||||
"integrity": "sha512-OzkAcosqOgWgQF+dQTXO/iaSGa3hMs/sSkfzkxwWpZXqJEbaA0V6O1V+Ew2tGBlTz1r7Rb7opU3w8ympWb9d2Q=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -63,6 +63,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"dayjs": "^1.7.5",
|
||||
"i18next": "^11.9.0",
|
||||
"i18next-browser-languagedetector": "^2.2.3",
|
||||
"ini": "^1.3.5",
|
||||
|
43
src/components/ButtonSelect/index.tsx
Normal file
43
src/components/ButtonSelect/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import * as React from 'react'
|
||||
import { BaseComponentProps } from '@models/BaseProps'
|
||||
import classnames from 'classnames'
|
||||
import './style.scss'
|
||||
|
||||
export interface ButtonSelectOptions {
|
||||
label: string,
|
||||
value: any
|
||||
}
|
||||
|
||||
export interface ButtonSelectProps extends BaseComponentProps {
|
||||
// options
|
||||
options: ButtonSelectOptions[]
|
||||
|
||||
// active value
|
||||
value: any
|
||||
|
||||
// select callback
|
||||
onSelect?: (value: any) => void
|
||||
}
|
||||
|
||||
export class ButtonSelect extends React.Component<ButtonSelectProps, {}> {
|
||||
|
||||
render () {
|
||||
const { options, value, onSelect } = this.props
|
||||
|
||||
return (
|
||||
<div className="button-select">
|
||||
{
|
||||
options.map(option => (
|
||||
<button
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
className={classnames('button-select-options', { actived: value === option.value })}
|
||||
onClick={() => onSelect(option.value)}
|
||||
>{option.label}</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
41
src/components/ButtonSelect/style.scss
Normal file
41
src/components/ButtonSelect/style.scss
Normal file
@ -0,0 +1,41 @@
|
||||
@import '~@styles/variables';
|
||||
|
||||
.button-select {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.button-select-options {
|
||||
height: 30px;
|
||||
padding: 0 15px;
|
||||
color: $color-primary-darken;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
background: $color-white;
|
||||
border: 1px solid $color-primary-lightly;
|
||||
border-right: none;
|
||||
transition: all 300ms ease;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button-select-options:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.button-select-options:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
border-right: 1px solid $color-primary-lightly;
|
||||
}
|
||||
|
||||
.button-select-options.actived {
|
||||
background: $color-primary;
|
||||
color: $color-white;
|
||||
border-color: $color-primary;
|
||||
box-shadow: 0 2px 5px rgba($color: $color-primary, $alpha: 0.5);
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
37
src/components/Col/index.tsx
Normal file
37
src/components/Col/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import * as React from 'react'
|
||||
import { BaseComponentProps } from '@models/BaseProps'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface ColProps extends BaseComponentProps {
|
||||
// left offset
|
||||
offset?: number
|
||||
|
||||
// flex order
|
||||
order?: number
|
||||
|
||||
span?: number
|
||||
}
|
||||
|
||||
export const Col: React.SFC<ColProps> = props => {
|
||||
const {
|
||||
offset = 0,
|
||||
order = 0,
|
||||
span = 1,
|
||||
className,
|
||||
style: s,
|
||||
children
|
||||
} = props
|
||||
|
||||
const style = Object.assign({}, { order }, s)
|
||||
|
||||
return (
|
||||
<div className={classnames(
|
||||
'column',
|
||||
`column-offset-${offset}`,
|
||||
`column-span-${span}`,
|
||||
className
|
||||
)} style={style}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
38
src/components/Row/index.tsx
Normal file
38
src/components/Row/index.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as React from 'react'
|
||||
import { BaseComponentProps } from '@models/BaseProps'
|
||||
import classnames from 'classnames'
|
||||
import './style.scss'
|
||||
|
||||
interface RowProps extends BaseComponentProps {
|
||||
// grid column
|
||||
gutter?: number
|
||||
|
||||
// row align
|
||||
align?: 'top' | 'middle' | 'bottom'
|
||||
|
||||
// column justify
|
||||
justify?: 'start' | 'end' | 'center' | 'space-around' | 'space-between'
|
||||
}
|
||||
|
||||
export const Row: React.SFC<RowProps> = props => {
|
||||
const {
|
||||
gutter = 24,
|
||||
align = 'top',
|
||||
justify = 'start',
|
||||
className,
|
||||
style,
|
||||
children
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className={classnames(
|
||||
'row',
|
||||
`row-gutter-${gutter}`,
|
||||
`row-align-${align}`,
|
||||
`row-justify-${justify}`,
|
||||
className
|
||||
)} style={style}>
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
65
src/components/Row/style.scss
Normal file
65
src/components/Row/style.scss
Normal file
@ -0,0 +1,65 @@
|
||||
@import '~@styles/variables';
|
||||
|
||||
$padding: 12px;
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// gutter
|
||||
@for $i from 1 through 24 {
|
||||
.row-gutter-#{$i} {
|
||||
padding: $padding $padding / 2;
|
||||
|
||||
.column {
|
||||
padding: 0 $padding / 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@for $c from 1 through 24 {
|
||||
.column-span-#{$c} {
|
||||
width: (100% / $i) * $c;
|
||||
}
|
||||
|
||||
.column-offset-#{$c} {
|
||||
margin-left: (100% / $i) * $c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// align
|
||||
.row-align-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.row-align-middle {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-align-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
// justify
|
||||
.row-justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.row-justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.row-justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row-justify-space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.row-justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
@ -28,7 +28,7 @@ export class Switch extends React.Component<SwitchProps, {}> {
|
||||
|
||||
return (
|
||||
<div className={classnames('switch', { checked, disabled }, className)} onClick={this.handleClick}>
|
||||
<Icon className="switch-icon" type="check" size={8} />
|
||||
<Icon className="switch-icon" type="check" size={8} style={{ fontWeight: 'bold' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
@import '~@styles/variables';
|
||||
|
||||
$height: 14px;
|
||||
$switch-radius: 16px;
|
||||
$height: 16px;
|
||||
$switch-radius: 18px;
|
||||
$switch-offset: 2px;
|
||||
$width: 28px;
|
||||
$width: 32px;
|
||||
|
||||
.switch {
|
||||
display: inline-block;
|
||||
@ -43,7 +43,7 @@ $width: 28px;
|
||||
width: $switch-radius;
|
||||
border-radius: $switch-radius / 2;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 8px rgba($color-primary-dark, 0.25);
|
||||
box-shadow: 0 0 8px rgba($color-primary-dark, 0.4);
|
||||
transition: transform 0.3s ease;
|
||||
transform: translateX($width - $switch-radius + $switch-offset);
|
||||
}
|
||||
|
@ -2,3 +2,6 @@ export * from './Header'
|
||||
export * from './Icon'
|
||||
export * from './Switch'
|
||||
export * from './Card'
|
||||
export * from './Row'
|
||||
export * from './Col'
|
||||
export * from './ButtonSelect'
|
||||
|
@ -1,21 +1,77 @@
|
||||
import * as React from 'react'
|
||||
import { Header, Card } from '@components'
|
||||
import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions } from '@components'
|
||||
import { translate } from 'react-i18next'
|
||||
import { I18nProps } from '@i18n'
|
||||
import './style.scss'
|
||||
|
||||
class Settings extends React.Component<I18nProps, {}> {
|
||||
|
||||
state = {
|
||||
startAtLogin: false
|
||||
startAtLogin: false,
|
||||
language: 'en',
|
||||
setAsSystemProxy: true,
|
||||
allowConnectFromLan: true
|
||||
}
|
||||
|
||||
languageOptions: ButtonSelectOptions[] = [
|
||||
{ label: '中文', value: 'zh' },
|
||||
{ label: 'English', value: 'en' }
|
||||
]
|
||||
|
||||
render () {
|
||||
const { t } = this.props
|
||||
const {
|
||||
startAtLogin,
|
||||
language,
|
||||
setAsSystemProxy,
|
||||
allowConnectFromLan
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Header title={t('title')} />
|
||||
<Card style={{ marginTop: 25 }}>
|
||||
<Card className="settings-card">
|
||||
<Row gutter={24} align="middle">
|
||||
<Col span={6} offset={1}>
|
||||
<span className="label">{t('labels.startAtLogin')}</span>
|
||||
</Col>
|
||||
<Col span={4} className="value-column">
|
||||
<Switch
|
||||
checked={startAtLogin}
|
||||
onChange={startAtLogin => this.setState({ startAtLogin })}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4} offset={1}>
|
||||
<span className="label">{t('labels.language')}</span>
|
||||
</Col>
|
||||
<Col span={7} className="value-column">
|
||||
<ButtonSelect
|
||||
options={this.languageOptions}
|
||||
value={language}
|
||||
onSelect={language => this.setState({ language })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={24} align="middle">
|
||||
<Col span={6} offset={1}>
|
||||
<span className="label">{t('labels.setAsSystemProxy')}</span>
|
||||
</Col>
|
||||
<Col span={4} className="value-column">
|
||||
<Switch
|
||||
checked={setAsSystemProxy}
|
||||
onChange={setAsSystemProxy => this.setState({ setAsSystemProxy })}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={7} offset={1}>
|
||||
<span className="label">{t('labels.allowConnectFromLan')}</span>
|
||||
</Col>
|
||||
<Col span={4} className="value-column">
|
||||
<Switch
|
||||
checked={allowConnectFromLan}
|
||||
onChange={allowConnectFromLan => this.setState({ allowConnectFromLan })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,12 +1,19 @@
|
||||
@import '~@styles/variables';
|
||||
|
||||
.proxies-list {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
.settings-card {
|
||||
margin-top: 25px;
|
||||
padding: 20px 0;
|
||||
|
||||
li {
|
||||
margin: 20px 15px 20px 0;
|
||||
.column {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value-column {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: $color-primary-darken;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ $color-primary: #57befc;
|
||||
$color-primary-dark: #2c8af8;
|
||||
$color-primary-darken: #54759a;
|
||||
$color-primary-light: #7fcae4;
|
||||
$color-primary-lightly: #b4ddf5;
|
||||
$color-primary-lightly: #e4eaef;
|
||||
|
||||
// common colors
|
||||
$color-gray: #d8dee2;
|
||||
|
Loading…
x
Reference in New Issue
Block a user