diff --git a/package-lock.json b/package-lock.json index 6b9dd45..3d0341a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1762,7 +1762,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -1799,7 +1799,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2531,7 +2531,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -2544,7 +2544,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -3184,7 +3184,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -7761,7 +7761,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -8341,7 +8341,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { @@ -10275,7 +10275,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx new file mode 100644 index 0000000..c3286be --- /dev/null +++ b/src/components/Alert/index.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' +import classnames from 'classnames' +import { Icon } from '@components' +import { BaseComponentProps } from '@models' +import './style.scss' + +interface AlertProps extends BaseComponentProps { + message?: string + type?: 'success' | 'info' | 'warning' | 'error' + inside?: boolean +} + +export class Alert extends React.Component { + + static defaultProps: AlertProps = { + message: '', + type: 'info', + inside: false + } + + iconMap = { + success: 'check', + info: 'info', + warning: 'info', + error: 'close' + } + + render () { + const { message, type, inside, children, className, style } = this.props + + return ( +
+ + + + { + message + ?

{message}

+ :
{children}
+ } +
+ ) + } + +} diff --git a/src/components/Alert/style.scss b/src/components/Alert/style.scss new file mode 100644 index 0000000..aad9d40 --- /dev/null +++ b/src/components/Alert/style.scss @@ -0,0 +1,97 @@ +@import '~@styles/variables'; + +$iconSize: 20px; +$borderWidth: 4px; + +@mixin box ($color) { + background: linear-gradient(135deg, darken($color, 5%), $color); + box-shadow: 0 2px 8px rgba($color: darken($color, 5%), $alpha: 0.3); + + .alert-icon > i { + color: $color; + } +} + +@mixin note ($color) { + background: rgba($color: $color, $alpha: 0.05); + border-radius: 1px 4px 4px 1px; + border-left: 2px solid $color; + box-shadow: 0 2px 8px rgba($color: darken($color, 5%), $alpha: 0.3); + + .alert-icon { + background: $color; + + > i { + color: $color-white; + } + } + + .alert-message { + color: darken($color: $color, $amount: 20%); + } +} + +.alert { + padding: 15px; + background: $color-white; + border-radius: 4px; + box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.3); + font-size: 13px; + line-height: 1.6; + text-align: justify; + display: flex; + + .alert-icon { + margin-right: 10px; + width: $iconSize; + height: $iconSize; + border-radius: 50%; + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + background: $color-white; + + > i { + transform: scale(0.5); + font-weight: bold; + } + } + + .alert-message { + width: 100%; + color: $color-white; + } +} + +.alert-box-success { + @include box($color-green); +} + +.alert-box-info { + @include box($color-primary); +} + +.alert-box-warning { + @include box($color-orange); +} + +.alert-box-error { + @include box($color-red); +} + +.alert-note-success { + @include note($color-green); +} + +.alert-note-info { + @include note($color-primary); +} + +.alert-note-warning { + @include note($color-orange); +} + +.alert-note-error { + @include note($color-red); +} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx new file mode 100644 index 0000000..4de6df0 --- /dev/null +++ b/src/components/Button/index.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import classnames from 'classnames' +import { BaseComponentProps } from '@models' +import './style.scss' + +interface ButtonProps extends BaseComponentProps { + type?: 'primary' | 'normal' | 'danger' | 'success' | 'warning' + onClick?: React.MouseEventHandler +} + +export class Button extends React.Component { + + static defaultProps: ButtonProps = { + type: 'normal', + onClick: () => {} + } + + render () { + const { type, onClick, children, className, style } = this.props + + return ( + + ) + } + +} diff --git a/src/components/Button/style.scss b/src/components/Button/style.scss new file mode 100644 index 0000000..1192e91 --- /dev/null +++ b/src/components/Button/style.scss @@ -0,0 +1,88 @@ +@import '~@styles/variables'; + +.button { + outline: 0; + padding: 0 15px; + height: 32px; + line-height: 32px; + border-radius: 16px; + font-size: 14px; + cursor: pointer; + transition: all 150ms ease; +} + +.button-primary { + color: $color-white; + border: none; + background: linear-gradient(135deg, $color-primary, $color-primary-dark); + box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.5); + + &:hover { + border: none; + } + + &:active { + box-shadow: 0 0 2px rgba($color: $color-primary-dark, $alpha: 0.5); + } +} + +.button-normal { + color: $color-gray-darken; + background: $color-white; + border: 1px solid rgba($color: $color-black, $alpha: 0.1); + + &:hover { + border-color: $color-gray-dark; + color: $color-primary-darken; + } + + &:active { + background: darken($color-white, 2%); + color: $color-primary-darken; + } +} + +.button-danger { + color: $color-white; + border: none; + background: linear-gradient(135deg, $color-red, darken($color-red, 10%)); + box-shadow: 0 2px 8px rgba($color: darken($color-red, 10%), $alpha: 0.5); + + &:hover { + border: none; + } + + &:active { + box-shadow: 0 0 2px rgba($color: darken($color-red, 10%), $alpha: 0.5); + } +} + +.button-success { + color: $color-white; + border: none; + background: linear-gradient(135deg, $color-green, darken($color-green, 5%)); + box-shadow: 0 2px 8px rgba($color: darken($color-green, 5%), $alpha: 0.5); + + &:hover { + border: none; + } + + &:active { + box-shadow: 0 0 2px rgba($color: darken($color-green, 5%), $alpha: 0.5); + } +} + +.button-warning { + color: $color-white; + border: none; + background: linear-gradient(135deg, $color-orange, darken($color-orange, 5%)); + box-shadow: 0 2px 8px rgba($color: darken($color-orange, 5%), $alpha: 0.5); + + &:hover { + border: none; + } + + &:active { + box-shadow: 0 0 2px rgba($color: darken($color-orange, 5%), $alpha: 0.5); + } +} diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 0000000..d5f6d6b --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1,105 @@ +import * as React from 'react' +import classnames from 'classnames' +import { createPortal } from 'react-dom' +import { BaseComponentProps } from '@models' +import { Button } from '@components' +import './style.scss' + +const noop = () => {} + +interface ModalProps extends BaseComponentProps { + // show modal + show?: boolean + + // modal title + title: string + + // size + size?: 'small' | 'big' + + // body className + bodyClassName?: string + + // body style + bodyStyle?: React.CSSProperties + + // show footer + footer?: boolean + + // on click ok + onOk?: typeof noop + + // on click close + onClose?: typeof noop +} + +export class Modal extends React.Component { + + static defaultProps: ModalProps = { + show: true, + title: 'Modal', + size: 'small', + footer: true, + onOk: noop, + onClose: noop + } + + // portal container + $container: Element + + $modal = React.createRef() + + constructor (props) { + super(props) + + // create container element + const container = document.createElement('div') + document.body.appendChild(container) + this.$container = container + } + + componentWillUnmount () { + document.body.removeChild(this.$container) + } + + private handleMaskClick = (e) => { + const { onClose } = this.props + const el = this.$modal.current + + if (el && !el.contains(e.target)) { + onClose() + } + } + + render () { + const { show, size, title, footer, children, className, bodyClassName, style, bodyStyle, onOk, onClose } = this.props + const modal = ( +
+
+
{title}
+
{children}
+ { + footer && ( +
+ + +
+ ) + } +
+
+ ) + + return createPortal(modal, this.$container) + } +} diff --git a/src/components/Modal/style.scss b/src/components/Modal/style.scss new file mode 100644 index 0000000..ba68aa4 --- /dev/null +++ b/src/components/Modal/style.scss @@ -0,0 +1,78 @@ +@import '~@styles/variables'; + +$width: 400px; +$bigWidth: 600px; + +.modal-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba($color: $color-black, $alpha: 0.15); + opacity: 0; + pointer-events: none; + transition: all 500ms ease; + display: flex; + justify-content: center; + align-items: center; + + .modal { + margin-top: -50px; + padding: 20px 30px; + background: $color-white; + box-shadow: 0 2px 16px rgba($color: $color-primary-darken, $alpha: 0.2); + border-radius: 4px; + display: flex; + flex-direction: column; + transform: scale(0); + transition: all 300ms cubic-bezier(0.32, 0.26, 0.71, 1.29); + + .modal-title { + margin: 5px 0; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: bold; + font-size: 18px; + color: $color-primary-dark; + text-shadow: 0 2px 6px rgba($color: $color-primary-dark, $alpha: 0.4); + } + + .modal-body { + margin: 10px 0; + font-size: 14px; + color: $color-primary-darken; + } + + .footer { + width: 100%; + margin: 5px 0; + display: flex; + align-items: center; + justify-content: flex-end; + + .button { + margin-left: 10px; + } + } + } + + .modal-small { + width: $width; + } + + .modal-big { + width: $bigWidth; + } +} + +.modal-show { + opacity: 1; + pointer-events: visible; + + .modal { + transform: scale(1); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index ec567de..65dfbb2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,3 +8,6 @@ export * from './ButtonSelect' export * from './Tags' export * from './Input' export * from './Select' +export * from './Modal' +export * from './Alert' +export * from './Button' diff --git a/src/containers/Settings/components/ExternalControllerDrawer/index.tsx b/src/containers/Settings/components/ExternalControllerDrawer/index.tsx new file mode 100644 index 0000000..987def8 --- /dev/null +++ b/src/containers/Settings/components/ExternalControllerDrawer/index.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import { Modal, Input, Row, Col, Alert } from '@components' +import './style.scss' + +interface ExternalControllerDrawerProps { + show: boolean + host: string + port: string + onConfirm: (host: string, port: string) => void + onCancel: () => void +} + +interface ExternalControllerDrawerState { + host: string, + port: string +} + +export class ExternalControllerDrawer extends React.Component { + + state = { + host: this.props.host, + port: this.props.port + } + + private handleOk = () => { + const { onConfirm } = this.props + const { host, port } = this.state + onConfirm(host, port) + } + + render () { + const { show, onCancel } = this.props + const { host, port } = this.state + + return ( + + +

请注意,修改该配置项并不会修改你的 Clash 配置文件,请确认修改后的外部控制地址和 Clash 配置文件内的地址一致,否则会导致 Dashboard 无法连接。

+
+ + Host + + this.setState({ host })} + /> + + + + 端口 + + this.setState({ port })} + /> + + +
+ ) + } +} diff --git a/src/containers/Settings/components/ExternalControllerDrawer/style.scss b/src/containers/Settings/components/ExternalControllerDrawer/style.scss new file mode 100644 index 0000000..02424c9 --- /dev/null +++ b/src/containers/Settings/components/ExternalControllerDrawer/style.scss @@ -0,0 +1,22 @@ +@import '~@styles/variables'; + +.external-controller { + .row { + padding: 0; + } + + .alert { + margin: 10px 0; + } + + .title, + .form { + margin: 15px 0; + } + + .title { + margin-top: 15px; + font-size: 14px; + font-weight: bold; + } +} diff --git a/src/containers/Settings/components/index.ts b/src/containers/Settings/components/index.ts index e69de29..a77c889 100644 --- a/src/containers/Settings/components/index.ts +++ b/src/containers/Settings/components/index.ts @@ -0,0 +1 @@ +export * from './ExternalControllerDrawer' diff --git a/src/containers/Settings/index.tsx b/src/containers/Settings/index.tsx index 7749db4..a77a1f1 100644 --- a/src/containers/Settings/index.tsx +++ b/src/containers/Settings/index.tsx @@ -1,7 +1,8 @@ import * as React from 'react' -import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components' import { translate } from 'react-i18next' import { changeLanguage } from 'i18next' +import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components' +import { ExternalControllerDrawer } from './components' import { I18nProps } from '@models' import './style.scss' @@ -14,7 +15,9 @@ class Settings extends React.Component { proxyMode: 'Rule', socks5ProxyPort: 7891, httpProxyPort: 7890, - externalController: '127.0.0.1:7892' + externalControllerHost: '127.0.0.1', + externalControllerPort: '7892', + showEditDrawer: false } languageOptions: ButtonSelectOptions[] = [ @@ -35,7 +38,9 @@ class Settings extends React.Component { proxyMode, socks5ProxyPort, httpProxyPort, - externalController + externalControllerHost, + externalControllerPort, + showEditDrawer } = this.state const proxyModeOptions: ButtonSelectOptions[] = [ { label: t('values.global'), value: 'Global' }, @@ -116,11 +121,15 @@ class Settings extends React.Component { this.setState({ httpProxyPort })}> - + {t('labels.externalController')} - - + + {`${externalControllerHost}:${externalControllerPort}`} + this.setState({ showEditDrawer: true })} + >修改 @@ -132,6 +141,16 @@ class Settings extends React.Component {

{t('versionString', { version: 'unknown' })}

{t('checkUpdate')} + + console.log(host, port)} + onCancel={() => this.setState({ showEditDrawer: false })} + > +
666666
+
) } diff --git a/src/containers/Settings/style.scss b/src/containers/Settings/style.scss index cac555a..42e20df 100644 --- a/src/containers/Settings/style.scss +++ b/src/containers/Settings/style.scss @@ -17,6 +17,22 @@ font-size: 14px; color: $color-primary-darken; } + + .external-controller { + font-size: 14px; + color: $color-primary-darken; + display: flex; + justify-content: flex-end; + font-weight: normal; + line-height: 17px; + + .modify-btn { + margin-left: 5px; + font-size: 12px; + color: $color-primary-dark; + cursor: pointer; + } + } } .clash-version { diff --git a/src/styles/common.scss b/src/styles/common.scss index e45b5dc..f39c5fb 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -4,7 +4,7 @@ // styles initial html { box-sizing: border-box; - background: rgba($color: $color-white, $alpha: 0.9); + background: rgba($color: $color-white, $alpha: 0.8); } *, diff --git a/src/styles/iconfont.scss b/src/styles/iconfont.scss index badc20d..83b18ed 100644 --- a/src/styles/iconfont.scss +++ b/src/styles/iconfont.scss @@ -6,7 +6,7 @@ @font-face { font-family: "clash-iconfont"; - src: url('//at.alicdn.com/t/font_841708_2viqaiy9h37.ttf') format('truetype'); + src: url('//at.alicdn.com/t/font_841708_h6oakuryxxb.ttf') format('truetype'); } .clash-iconfont { @@ -20,8 +20,6 @@ .icon-close::before { content: "\e602"; } -.icon-info::before { content: "\e603"; } - .icon-drag::before { content: "\e604"; } .icon-down-arrow-o::before { content: "\e605"; } @@ -35,3 +33,7 @@ .icon-triangle-down::before { content: "\e609"; } .icon-up-arrow-o::before { content: "\e60a"; } + +.icon-info::before { content: "\e60b"; } + +.icon-info-o::before { content: "\e60c"; } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 4271d6e..0c5da65 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -18,3 +18,4 @@ $color-white: #fff; $color-green: #67c23a; $color-orange: #e6a23c; $color-red: #f56c6c; +$color-black: #000;