Browse Source

add login page

KaysonCui 5 years ago
parent
commit
4773752093

+ 2 - 1
front/.eslintrc

@@ -54,6 +54,7 @@
     "class-methods-use-this": "off",
     "react/prop-types": "off",
     "global-require": "off",
-    "import/no-extraneous-dependencies": "off"
+    "import/no-extraneous-dependencies": "off",
+    "import/no-named-as-default": "off"
   }
 }

+ 17 - 14
front/project/www/app.js

@@ -3,29 +3,32 @@ import { LocaleProvider } from 'antd';
 import zhCN from 'antd/lib/locale-provider/zh_CN';
 import './app.less';
 import Header from './components/Header';
-import { User } from './stores/user';
+import Login from './components/Login';
 
 export default class extends Component {
   constructor(props) {
     super(props);
     const state = { routes: [] };
     this.state = state;
-    // 初始化登录
-    User.token().then(() => {
-      this.setState({ show: true });
-    });
   }
 
   render() {
     const { children, project, config } = this.props;
-    const { show } = this.state;
-    return (show ? <LocaleProvider locale={zhCN}>
-      {config.hideHeader ? (
-        <div id="full-page">{children}</div>
-      ) : (<div className={`${config.tab}`} id="page">
-        <Header tabs={project.tabs} active={config.tab} />
-        {children}
-      </div>)}
-    </LocaleProvider> : null);
+    return (
+      <LocaleProvider locale={zhCN}>
+        {config.hideHeader ? (
+          <div id="full-page">
+            {children}
+            <Login />
+          </div>
+        ) : (
+          <div className={`${config.tab}`} id="page">
+            <Header tabs={project.tabs} active={config.tab} />
+            {children}
+            <Login />
+          </div>
+        )}
+      </LocaleProvider>
+    );
   }
 }

+ 41 - 5
front/project/www/app.less

@@ -262,15 +262,51 @@ body,
 #root {}
 
 body {
-  .ant-tooltip {
-
-    .ant-tooltip-arrow {
-      border-top-color: #fff;
-    }
+  .ant-tooltip.white {
 
     .ant-tooltip-inner {
       background: #fff;
       color: @base_color;
     }
   }
+
+  .ant-tooltip.white.ant-tooltip-placement-left .ant-tooltip-arrow {
+    border-left-color: #fff;
+  }
+
+  .ant-tooltip.white.ant-tooltip-placement-right .ant-tooltip-arrow {
+    border-right-color: #fff;
+  }
+
+  .ant-tooltip.white.ant-tooltip-placement-top .ant-tooltip-arrow {
+    border-top-color: #fff;
+  }
+
+  .ant-tooltip.white.ant-tooltip-placement-bottom .ant-tooltip-arrow {
+    border-bottom-color: #fff;
+  }
+
+  .ant-tooltip.gray {
+
+    .ant-tooltip-inner {
+      background: rgba(0, 0, 0, 0.4);
+      color: #fff;
+    }
+  }
+
+  .ant-tooltip.gray.ant-tooltip-placement-left .ant-tooltip-arrow {
+    border-left-color: rgba(0, 0, 0, 0.4);
+  }
+
+  .ant-tooltip.gray.ant-tooltip-placement-right .ant-tooltip-arrow {
+    border-right-color: rgba(0, 0, 0, 0.4);
+  }
+
+  .ant-tooltip.gray.ant-tooltip-placement-top .ant-tooltip-arrow {
+    border-top-color: rgba(0, 0, 0, 0.4);
+  }
+
+  .ant-tooltip.gray.ant-tooltip-placement-bottom .ant-tooltip-arrow {
+    border-bottom-color: rgba(0, 0, 0, 0.4);
+  }
 }

BIN
front/project/www/assets/close.png


BIN
front/project/www/assets/close_hover.png


BIN
front/project/www/assets/code.png


BIN
front/project/www/assets/notice.png


BIN
front/project/www/assets/phone.png


BIN
front/project/www/assets/radio.png


BIN
front/project/www/assets/radio_blue.png


+ 4 - 3
front/project/www/components/Button/index.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 
-function Button(props) {
+function GButton(props) {
   const { children, className = '', theme = 'theme', size = 'basic', disabled, radius, width, onClick } = props;
   return (
     <div
@@ -13,5 +13,6 @@ function Button(props) {
     </div>
   );
 }
-Button.propTypes = {};
-export default Button;
+GButton.propTypes = {};
+export default GButton;
+export const Button = GButton;

+ 9 - 4
front/project/www/components/Icon/index.js

@@ -1,9 +1,14 @@
 import React from 'react';
 import './index.less';
 
-function Icon(props) {
+function GIcon(props) {
   const { active, name, onClick, children } = props;
-  return <div className={`icon ${name} ${active ? 'active' : ''}`} onClick={() => onClick && onClick()}>{children}</div>;
+  return (
+    <div className={`icon ${name} ${active ? 'active' : ''}`} onClick={() => onClick && onClick()}>
+      {children}
+    </div>
+  );
 }
-Icon.propTypes = {};
-export default Icon;
+GIcon.propTypes = {};
+export default GIcon;
+export const Icon = GIcon;

+ 11 - 0
front/project/www/components/Icon/index.less

@@ -136,4 +136,15 @@
 .icon.menu.active,
 .icon.menu:hover {
   background-image: url('/assets/meun_icon_hover.png');
+}
+
+.icon.close {
+  width: 20px;
+  height: 20px;
+  background: url('/assets/close.png') no-repeat center;
+}
+
+.icon.close.active,
+.icon.close:hover {
+  background-image: url('/assets/close_hover.png');
 }

+ 241 - 0
front/project/www/components/Login/index.js

@@ -0,0 +1,241 @@
+import React, { Component } from 'react';
+import './index.less';
+import { Modal, Icon, Button, Tooltip } from 'antd';
+import Assets from '@src/components/Assets';
+import { Icon as GIcon } from '../Icon';
+import { Button as GButton } from '../Button';
+
+const LOGIN_PHONE = 'LOGIN_PHONE';
+const REGISTER_PHONE = 'REGISTER_PHONE';
+const LOGIN_WX = 'LOGIN_WX';
+const BIND_PHONE = 'BIND_PHONE';
+const BIND_WX = 'BIND_WX';
+const BIND_WX_ERROR = 'BIND_WX_ERROR';
+
+export default class Login extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { show: false, type: BIND_WX_ERROR };
+  }
+
+  render() {
+    const { show, type } = this.state;
+    return (
+      <Modal wrapClassName={`login-modal ${type}`} visible={show} footer={null} closable={false} width={470}>
+        {this.renderBody(type)}
+        <GIcon name="close" />
+      </Modal>
+    );
+  }
+
+  renderBody(type) {
+    switch (type) {
+      case LOGIN_PHONE:
+        return this.renderLoginPhone();
+      case REGISTER_PHONE:
+        return this.renderRegisterPhone();
+      case LOGIN_WX:
+        return this.renderLoginWx();
+      case BIND_PHONE:
+        return this.renderBindPhone();
+      case BIND_WX:
+        return this.renderBindWx();
+      case BIND_WX_ERROR:
+        return this.renderBindWxError();
+      default:
+        return this.LOGIN_PHONE();
+    }
+  }
+
+  renderLoginPhone() {
+    return (
+      <div className="body">
+        <div className="title">手机号登录</div>
+        <SelectInput placeholder="请输入手机号" />
+        <VerificationInput placeholder="请输入验证码" />
+        <Button type="primary" size="large" block>
+          登录
+        </Button>
+        <Tooltip overlayClassName="gray" placement="left" title="微信扫码登录">
+          <a className="other">
+            <Assets name="code" />
+          </a>
+        </Tooltip>
+      </div>
+    );
+  }
+
+  renderRegisterPhone() {
+    return (
+      <div className="body">
+        <div className="title">手机号登录</div>
+        <div className="tip">
+          <Assets name="notice" />
+          该手机号尚未注册,将自动为您注册账户
+        </div>
+        <SelectInput placeholder="请输入手机号" />
+        <VerificationInput placeholder="请输入验证码" />
+        <Input placeholder="请输入邮箱" />
+        <Button type="primary" size="large" block>
+          登录
+        </Button>
+        <Tooltip overlayClassName="gray" placement="left" title="微信扫码登录">
+          <a className="other">
+            <Assets name="code" />
+          </a>
+        </Tooltip>
+      </div>
+    );
+  }
+
+  renderLoginWx() {
+    return (
+      <div className="body">
+        <div className="title">微信扫码登录</div>
+        <div className="qr-code">
+          <Assets name="qrcode" />
+          <div className="text">请使用微信扫描二维码登录</div>
+        </div>
+        <Tooltip overlayClassName="gray" placement="left" title="手机号登录">
+          <a className="other">
+            <Assets name="phone" />
+          </a>
+        </Tooltip>
+      </div>
+    );
+  }
+
+  renderBindPhone() {
+    return (
+      <div className="body">
+        <div className="title">绑定手机号</div>
+        <div className="tip">
+          <Assets name="notice" />
+          微信登录成功!为更好的使用服务,请您绑定手机号和邮箱。
+        </div>
+        <SelectInput placeholder="请输入手机号" />
+        <VerificationInput placeholder="请输入验证码" />
+        <Input placeholder="请输入邮箱" />
+        <Button type="primary" size="large" block>
+          绑定
+        </Button>
+      </div>
+    );
+  }
+
+  renderBindWx() {
+    return (
+      <div className="body">
+        <div className="title">绑定微信号</div>
+        <div className="tip">
+          <Assets name="notice" />
+          手机号注册成功!为更好的使用服务,建议您绑定微信号。
+        </div>
+        <div className="qr-code">
+          <Assets name="qrcode" />
+          <div className="text">请使用微信扫描二维码登录</div>
+          <div className="jump">跳过</div>
+        </div>
+      </div>
+    );
+  }
+
+  renderBindWxError() {
+    return (
+      <div className="body">
+        <div className="title">绑定失败</div>
+        <div className="text">该微信账户已绑定其他手机号,您可直接使用微信登入。</div>
+        <div className="btn">
+          <GButton radius>Ok</GButton>
+        </div>
+      </div>
+    );
+  }
+}
+
+class Input extends Component {
+  render() {
+    const { className = '', onChange, placeholder, error, left, right } = this.props;
+    return (
+      <div className={`g-input-container ${className}`}>
+        <div className={`g-input-wrapper ${error ? 'error' : ''}`}>
+          {left && <div className="g-input-left">{left}</div>}
+          <input className="g-input" placeholder={placeholder} onChange={data => onChange && onChange(data)} />
+          {right && <div className="g-input-right">{right}</div>}
+        </div>
+        <div hidden={!error} className="g-input-error">
+          {error}
+        </div>
+      </div>
+    );
+  }
+}
+
+class SelectInput extends Component {
+  render() {
+    const { className = '', onChange, placeholder, value, selectValue, onSelect } = this.props;
+    return (
+      <Input
+        className={className}
+        left={
+          <span className="g-input-left-select" onClick={() => onSelect && onSelect()}>
+            {selectValue}
+            <Icon type="down" />
+          </span>
+        }
+        value={value}
+        placeholder={placeholder}
+        onChange={data => onChange && onChange(data)}
+      />
+    );
+  }
+}
+
+class VerificationInput extends Component {
+  constructor(props) {
+    super(props);
+    this.timeKey = null;
+    this.state = { loading: 0 };
+  }
+
+  componentWillUnmount() {
+    if (this.timeKey) clearTimeout(this.timeKey);
+  }
+
+  onSend() {
+    const { onSend, time = 60 } = this.props;
+    if (onSend) {
+      onSend();
+    }
+    this.setTime(time);
+  }
+
+  setTime(time) {
+    this.setState({ loading: time });
+    this.timeKey = setTimeout(() => {
+      this.setTime(time - 1);
+    }, 1000);
+  }
+
+  render() {
+    const { loading } = this.state;
+    const { className = '', onChange, placeholder, value } = this.props;
+    return (
+      <Input
+        className={className}
+        right={
+          loading <= 0 ? (
+            <span className="g-input-right-verification" onClick={() => this.onSend()}>
+              获取验证码
+            </span>
+          ) : (
+            <span className="g-input-right-verification-loading">等待{loading}秒</span>
+          )
+        }
+        value={value}
+        placeholder={placeholder}
+        onChange={data => onChange && onChange(data)}
+      />
+    );
+  }
+}

+ 162 - 0
front/project/www/components/Login/index.less

@@ -0,0 +1,162 @@
+@import '../../app.less';
+
+.login-modal {
+  .title {
+    font-size: 26px;
+    color: #303036;
+    margin-bottom: 20px;
+    font-weight: 600;
+  }
+
+  .tip {
+    color: #686872;
+    font-size: 12px;
+    margin-bottom: 10px;
+    transform: translateY(-5px);
+
+    .assets {
+      margin-right: 3px;
+      transform: translateY(-1px);
+    }
+  }
+
+
+  .qr-code {
+    text-align: center;
+
+    .assets {
+      margin-bottom: 10px;
+    }
+
+    .text {
+      margin-bottom: 50px;
+    }
+
+    .jump {
+      display: inline-block;
+      color: #A7A7B7;
+      cursor: pointer;
+      margin-bottom: 10px;
+    }
+  }
+
+  .btn {
+    text-align: center;
+    padding-top: 60px;
+    padding-bottom: 10px;
+
+    .button {
+      width: 150px;
+      height: 45px;
+      line-height: 45px;
+      padding: 0;
+      border-radius: 25px;
+      font-size: 16px;
+    }
+  }
+
+  .icon.close {
+    position: absolute;
+    top: 15px;
+    right: 15px;
+  }
+
+  .other {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 40px;
+    height: 80px;
+    cursor: pointer;
+
+    .assets {
+      width: 80px;
+      height: 80px;
+      max-height: none;
+      max-width: none;
+      transform: translateX(-50%);
+    }
+  }
+
+  .g-input-container {
+
+    .g-input-wrapper {
+      display: flex;
+      background: #F7F7F7;
+      height: 44px;
+      padding: 8px 0;
+      line-height: 28px;
+      margin-bottom: 25px;
+
+      .g-input {
+        flex: 1;
+        background: transparent;
+        border: none;
+        padding: 10px 20px;
+        height: 28px;
+      }
+
+      .g-input-left {
+        cursor: pointer;
+
+        .g-input-left-select {
+          height: 28px;
+          border-right: 1px solid #eee;
+          padding-left: 10px;
+
+          i {
+            margin-left: 5px;
+            font-size: 10px;
+            margin-right: 10px;
+          }
+        }
+      }
+
+      .g-input-right {
+        padding: 0 20px;
+        cursor: pointer;
+
+        .g-input-right-verification {
+          color: #41A6F3;
+        }
+
+        .g-input-right-verification.loading {
+          color: #A7A7B7;
+        }
+      }
+    }
+
+    .g-input-wrapper.error {
+      margin-bottom: 0;
+    }
+
+    .g-input-error {
+      color: #FF562E;
+      font-size: 12px;
+      height: 25px;
+      line-height: 25px;
+    }
+  }
+}
+
+.LOGIN_PHONE .body {
+  padding-bottom: 120px;
+}
+
+.REGISTER_PHONE .body {
+  padding-bottom: 80px;
+}
+
+.LOGIN_WX .body {
+  .qr-code {
+    padding-top: 60px;
+    padding-bottom: 120px;
+  }
+}
+
+.BIND_WX .body {
+  .qr-code {
+    padding-top: 20px;
+    padding-bottom: 10px;
+  }
+}

+ 4 - 2
front/project/www/components/OtherAnswer/index.js

@@ -27,13 +27,15 @@ export default class OtherAnswer extends Component {
     const { show, more } = this.state;
     return (
       <div className={`other-answer ${more ? 'more' : ''} ${!show ? 'hide' : ''}`}>
-        <div className="title">Q: {data.content}</div>
+        <div className="small-tag">提问</div>
+        <div className="title">{data.content}</div>
+        <div className="small-tag">回答</div>
         <div
           ref={ref => {
             this.Text = ref;
           }}
           className="desc"
-          dangerouslySetInnerHTML={{ __html: `A: ${data.answer}` }}
+          dangerouslySetInnerHTML={{ __html: data.answer }}
         />
         {more && show && <Icon name="up" onClick={() => this.setState({ show: false })} />}
         {more && !show && <Icon name="down" onClick={() => this.setState({ show: true })} />}

+ 10 - 0
front/project/www/components/OtherAnswer/index.less

@@ -10,6 +10,16 @@
     margin-bottom: 10px;
   }
 
+  .small-tag {
+    display: inline-block;
+    background: rgb(172, 206, 251);
+    padding: 0 5px;
+    border-radius: 5px;
+    color: #fff;
+    margin-bottom: 5px;
+    font-size: 10px;
+  }
+
   .desc {
     color: #686872;
     font-size: 16px;

+ 4 - 8
front/project/www/local.json

@@ -7,20 +7,16 @@
     ],
     "proxy": [
       {
-        "target": "http://127.0.0.1:8888",
+        "target": "http://qianxing.nuliji.com",
         "from": "/api",
         "to": "/api"
       }
     ]
   },
   "test": {
-    "scripts": [
-      "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
-    ]
+    "scripts": ["http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"]
   },
   "production": {
-    "scripts": [
-      "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
-    ]
+    "scripts": ["http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"]
   }
-}
+}

+ 140 - 97
front/project/www/routes/examination/main/page.js

@@ -133,7 +133,7 @@ export default class extends Page {
         title: '练习册',
         width: 250,
         align: 'left',
-        render: (row) => {
+        render: row => {
           return (
             <div className="table-row">
               <div className="night f-s-16">{row.title}</div>
@@ -228,7 +228,7 @@ export default class extends Page {
 
   init() {
     Main.getExercise().then(result => {
-      const list = result.map((row) => {
+      const list = result.map(row => {
         row.title = `${row.titleZh}${row.titleEn}`;
         row.key = row.extend;
         return row;
@@ -246,7 +246,7 @@ export default class extends Page {
     const { info = {} } = this.props.user;
     if (info.latestExercise) {
       // 获取最后一次做题记录
-      Question.baseReport(info.latestExercise).then((result) => {
+      Question.baseReport(info.latestExercise).then(result => {
         this.setState({ latest: result });
       });
     }
@@ -292,7 +292,7 @@ export default class extends Page {
     if (!articleMap) {
       Sentence.listArticle().then(result => {
         const map = {};
-        result.forEach((article) => {
+        result.forEach(article => {
           if (!map[article.chapter]) {
             map[article.chapter] = [];
           }
@@ -360,10 +360,10 @@ export default class extends Page {
       const exerciseChild = result;
       this.setState({ exerciseChild });
     });
-    Question.getExerciseProcess(type.id).then((r => {
+    Question.getExerciseProcess(type.id).then(r => {
       const exerciseProcess = getMap(r, 'id');
       this.setState({ exerciseProcess });
-    }));
+    });
   }
 
   onChangePreviewType(type) {
@@ -411,13 +411,12 @@ export default class extends Page {
   }
 
   activeSentence() {
-    Sentence.active(this.code)
-      .then(() => {
-        // 重新获取长难句信息
-        this.clearSentenceTrail();
-        this.setState({ sentence: null, articleMap: null, paperList: null });
-        this.refresh();
-      });
+    Sentence.active(this.code).then(() => {
+      // 重新获取长难句信息
+      this.clearSentenceTrail();
+      this.setState({ sentence: null, articleMap: null, paperList: null });
+      this.refresh();
+    });
   }
 
   trailSentence() {
@@ -447,27 +446,28 @@ export default class extends Page {
     const children = (map[tab1] || {}).children || [];
     return (
       <div>
-        {latest && <Continue
-          data={latest}
-          onClose={() => {
-            this.clearExercise();
-          }}
-          onContinue={() => {
-
-          }}
-          onRestart={() => {
-
-          }}
-          onNext={() => {
-
-          }} />}
+        {latest && (
+          <Continue
+            data={latest}
+            onClose={() => {
+              this.clearExercise();
+            }}
+            onContinue={() => {}}
+            onRestart={() => {}}
+            onNext={() => {}}
+          />
+        )}
         <div className="content">
           <Module className="m-t-2">
-            <Tabs type="card" active={tab1} tabs={tabs} onChange={key => {
-              this.onChangeTab(1, key);
-            }} />
+            <Tabs
+              type="card"
+              active={tab1}
+              tabs={tabs}
+              onChange={key => {
+                this.onChangeTab(1, key);
+              }}
+            />
             {children.length > 1 && <Tabs active={tab2} tabs={children} onChange={key => this.onChangeTab(2, key)} />}
-
           </Module>
           {tab1 !== SENTENCE && tab1 !== PREVIEW && this.renderExercise()}
           {tab1 === SENTENCE && this.renderSentence()}
@@ -549,7 +549,17 @@ export default class extends Page {
   }
 
   renderSentenceArticle() {
-    const { sentence = {}, chapters, chapter, exerciseChapter = {}, chapterMap = {}, articleMap = {}, paperFilterList = [], paperList = [], paperChecked } = this.state;
+    const {
+      sentence = {},
+      chapters,
+      chapter,
+      exerciseChapter = {},
+      chapterMap = {},
+      articleMap = {},
+      paperFilterList = [],
+      paperList = [],
+      paperChecked,
+    } = this.state;
     const { sentenceTrail } = this.props.user;
     let maxStep = 0;
     if (sentenceTrail) {
@@ -562,62 +572,83 @@ export default class extends Page {
     if (chapterInfo && chapterInfo.exercise) {
       isExercise = true;
     }
-    return <div>
-      {sentence.code && <div className='sentence-code'>CODE: {sentence.code}</div>}
-      {sentenceTrail && <div className='sentence-code'>CODE: <Link to=''>去获取</Link><a onClick={() => {
-        this.setState({ sentenceInput: true });
-      }}>输入</a></div>}
-      <Module>
-        <Step
-          list={chapters}
-          step={chapter}
-          onClick={(step) => {
-            this.setState({ chapter: step });
-          }}
-          message='请购买后访问'
-          maxStep={maxStep}
-        />
-      </Module>
-      {/* 正常文章 */}
-      {sentence.code && !isExercise && <List
-        title={`Chapter${chapter}`}
-        subTitle={chapterInfo.title}
-        list={articleMap[chapter]}
-        onClick={(part) => {
-          this.sentenceRead(part);
-        }}
-      />}
-      {/* 正常练习 */}
-      {sentence.code && isExercise && <ListTable
-        title={`Chapter${chapter}`}
-        subTitle={chapterInfo.title}
-        filters={[{
-          type: 'radio',
-          checked: paperChecked,
-          list: [{ key: 0, title: '未完成' }, { key: 1, title: '已完成' }],
-          onChange: (item) => {
-            console.log(item);
-            this.sentenceFilter(item);
-          },
-        }]}
-        data={paperFilterList}
-        columns={this.sentenceColums}
-      />}
-      {/* 试读文章 */}
-      {sentenceTrail && <List
-        list={[]}
-        onClick={(part) => {
-          this.sentenceRead(part);
-        }}
-      />}
-      {/* 试练 */}
-      {sentenceTrail && <ListTable
-        title={`Chapter${exerciseChapter.value}`}
-        subTitle={exerciseChapter.title}
-        data={paperList}
-        columns={this.sentenceColums}
-      />}
-    </div>;
+    return (
+      <div>
+        {sentence.code && <div className="sentence-code">CODE: {sentence.code}</div>}
+        {sentenceTrail && (
+          <div className="sentence-code">
+            CODE: <Link to="">去获取</Link>
+            <a
+              onClick={() => {
+                this.setState({ sentenceInput: true });
+              }}
+            >
+              输入
+            </a>
+          </div>
+        )}
+        <Module>
+          <Step
+            list={chapters}
+            step={chapter}
+            onClick={step => {
+              this.setState({ chapter: step });
+            }}
+            message="请购买后访问"
+            maxStep={maxStep}
+          />
+        </Module>
+        {/* 正常文章 */}
+        {sentence.code && !isExercise && (
+          <List
+            title={`Chapter${chapter}`}
+            subTitle={chapterInfo.title}
+            list={articleMap[chapter]}
+            onClick={part => {
+              this.sentenceRead(part);
+            }}
+          />
+        )}
+        {/* 正常练习 */}
+        {sentence.code && isExercise && (
+          <ListTable
+            title={`Chapter${chapter}`}
+            subTitle={chapterInfo.title}
+            filters={[
+              {
+                type: 'radio',
+                checked: paperChecked,
+                list: [{ key: 0, title: '未完成' }, { key: 1, title: '已完成' }],
+                onChange: item => {
+                  console.log(item);
+                  this.sentenceFilter(item);
+                },
+              },
+            ]}
+            data={paperFilterList}
+            columns={this.sentenceColums}
+          />
+        )}
+        {/* 试读文章 */}
+        {sentenceTrail && (
+          <List
+            list={[]}
+            onClick={part => {
+              this.sentenceRead(part);
+            }}
+          />
+        )}
+        {/* 试练 */}
+        {sentenceTrail && (
+          <ListTable
+            title={`Chapter${exerciseChapter.value}`}
+            subTitle={exerciseChapter.title}
+            data={paperList}
+            columns={this.sentenceColums}
+          />
+        )}
+      </div>
+    );
   }
 
   renderInputCode() {
@@ -625,12 +656,21 @@ export default class extends Page {
       <Module className="code-module">
         <div className="title">输入《千行GMAT长难句》专属 Code,解锁在线练习功能。</div>
         <div className="input-block">
-          <Input size="lager" placeholder="请输入CODE" onChange={(value) => {
-            this.code = value;
-          }} />
-          <Button size="lager" onClick={() => {
-            this.activeSentence();
-          }}>解锁</Button>
+          <Input
+            size="lager"
+            placeholder="请输入CODE"
+            onChange={value => {
+              this.code = value;
+            }}
+          />
+          <Button
+            size="lager"
+            onClick={() => {
+              this.activeSentence();
+            }}
+          >
+            解锁
+          </Button>
         </div>
         <div className="tip">
           <Link to="/" className="left link">
@@ -640,9 +680,12 @@ export default class extends Page {
           <Link to="/" className="link">
             去获取 >>
           </Link>
-          <a onClick={() => {
-            this.trailSentence();
-          }} className="right link">
+          <a
+            onClick={() => {
+              this.trailSentence();
+            }}
+            className="right link"
+          >
             试用 >>
           </a>
         </div>

+ 2 - 0
front/project/www/routes/page/demo/page.js

@@ -12,6 +12,7 @@ import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
 import Step from '../../../components/Step';
+import OtherAnswer from '../../../components/OtherAnswer';
 
 export default class extends Page {
   renderView() {
@@ -206,6 +207,7 @@ export default class extends Page {
               },
             ]}
           />
+          <OtherAnswer data={{ content: '123123', answer: '123123123' }} />
         </div>
       </div>
     );

+ 6 - 2
front/project/www/routes/paper/question/page.js

@@ -1,6 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import { Carousel } from 'antd';
+import { Carousel, Tooltip } from 'antd';
 import { Link } from 'react-router-dom';
 import Fullscreen from 'react-fullscreen-crossbrowser';
 import './index.less';
@@ -258,7 +258,11 @@ export default class extends Page {
       <div className="layout-body">{this.renderBody()}</div>
       <div className="layout-footer">
         <div className="left">
-          <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
+          <Tooltip overlayClassName='gray' placement='top' title='全屏'>
+            <a>
+              <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
+            </a>
+          </Tooltip>
         </div>
         <div className="center">
           <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>

+ 49 - 10
front/project/www/stores/user.js

@@ -1,23 +1,50 @@
 import BaseStore from '@src/stores/base';
 import * as querystring from 'querystring';
 
-const SENTENCE_TRAIL = 'SENTENCE_TRAIL';
-
 export default class UserStore extends BaseStore {
-  initState() {
+  constructor(props) {
+    super(props);
+    this.adminLogin = null;
     const { token } = querystring.parse(window.location.search.replace('?', ''));
     if (token) {
-      this.setToken(token);
+      this.adminLogin = token;
     }
-    const sentenceTrail = this.getLocal(SENTENCE_TRAIL);
-    return { login: !!token, sentenceTrail };
+  }
+
+  initState() {
+    if (this.adminLogin) this.setToken(this.adminLogin);
+    return { login: !!this.adminLogin };
+  }
+
+  initAfter() {
+    if (this.state.login || this.adminLogin) {
+      this.refreshToken().then(() => {
+        if (this.adminLogin) {
+          this.linkTo(window.location.href.substr(0, window.location.href.indexOf('?') + 1));
+        }
+      });
+    }
+  }
+
+  refreshToken() {
+    return this.apiPost('/auth/token')
+      .then(result => {
+        this.infoHandle(result);
+      })
+      .catch(() => {
+        this.logout(false);
+      });
+  }
+
+  infoHandle(result) {
+    this.setToken(result.token);
+    this.setState({ login: true, info: result, username: result.username });
   }
 
   /**
    * 设置长难句试用
    */
   sentenceTrail() {
-    this.saveLocal(SENTENCE_TRAIL, true);
     this.setState({ sentenceTrail: true });
   }
 
@@ -25,7 +52,6 @@ export default class UserStore extends BaseStore {
    * 清除长难句试用
    */
   clearSentenceTrail() {
-    this.removeLocal(SENTENCE_TRAIL);
     this.setState({ sentenceTrail: null });
   }
 
@@ -55,8 +81,21 @@ export default class UserStore extends BaseStore {
   /**
    * 登出
    */
-  logout() {
-    return this.apiPost('/auth/logout');
+
+  logout(login = true) {
+    return Promise.resolve()
+      .then(() => {
+        if (login) {
+          return this.apiPost('/auth/logout', {});
+        }
+        return true;
+      })
+      .then(() => {
+        this.setState({ login: false, info: {}, username: '' });
+      })
+      .then(() => {
+        linkTo(this.project.loginPath);
+      });
   }
 
   /**

+ 6 - 0
front/src/services/Store.js

@@ -11,12 +11,18 @@ function getReducers(stores) {
   return reducers;
 }
 function initReducers(stores, project, services) {
+  const map = {};
   stores.forEach(item => {
+    if (map[item.key]) return;
+    map[item.key] = true;
     item.init.bind(item)(project, services);
   });
 }
 function setReducersStore(store, stores) {
+  const map = {};
   stores.forEach(item => {
+    if (map[item.key]) return;
+    map[item.key] = true;
     item.setStore.bind(item)(store);
   });
 }