index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import React, { Component } from 'react';
  2. import './index.less';
  3. import { Modal, Icon, Button, Tooltip } from 'antd';
  4. import Assets from '@src/components/Assets';
  5. import { asyncSMessage } from '@src/services/AsyncTools';
  6. import { Icon as GIcon } from '../Icon';
  7. import { Button as GButton } from '../Button';
  8. import RadioItem from '../RadioItem';
  9. import { User } from '../../stores/user';
  10. import { Common } from '../../stores/common';
  11. import { Main } from '../../stores/main';
  12. import { MobileArea, WechatPcAppId, PcUrl } from '../../../Constant';
  13. const LOGIN_PHONE = 'LOGIN_PHONE';
  14. const LOGIN_WX = 'LOGIN_WX';
  15. const BIND_PHONE = 'BIND_PHONE';
  16. const BIND_WX = 'BIND_WX';
  17. const BIND_WX_ERROR = 'BIND_WX_ERROR';
  18. export default class Login extends Component {
  19. constructor(props) {
  20. super(props);
  21. this.validNumber = 0;
  22. this.state = { type: LOGIN_WX, error: {}, data: { area: MobileArea[0].value } };
  23. this.validMobileNumber = 0;
  24. this.validEmailNumber = 0;
  25. window.addEventListener(
  26. 'message',
  27. event => {
  28. if (typeof event.data === 'string' && event.data.indexOf('code:') === 0) {
  29. const code = event.data.split(':')[1];
  30. if (this.state.type === LOGIN_WX) {
  31. this.scanLogin(code);
  32. } else if (this.state.type === BIND_WX) {
  33. this.scanBind(code);
  34. }
  35. }
  36. },
  37. false,
  38. );
  39. }
  40. componentWillReceiveProps(nextProps) {
  41. if (nextProps.user.needLogin && !this.init) {
  42. this.init = true;
  43. Main.getContract('register').then(result => {
  44. this.setState({ registerContract: result });
  45. });
  46. Main.getContract('privacy').then(result => {
  47. this.setState({ privacyContract: result });
  48. });
  49. }
  50. }
  51. close() {
  52. User.closeLogin();
  53. }
  54. login() {
  55. const { data, needEmail, mobileError, validError, emailError, error } = this.state;
  56. const { area, mobile, mobileVerifyCode, email } = data;
  57. if (mobileError || emailError || validError) return;
  58. if (!area || !mobile || !mobileVerifyCode || (needEmail && !email)) {
  59. error[LOGIN_PHONE] = { mobile: !mobile, mobileVerifyCode: !mobileVerifyCode, email: !email };
  60. this.setState({ error });
  61. return;
  62. }
  63. error[LOGIN_PHONE] = {};
  64. this.setState({ error });
  65. User.login(area, mobile, mobileVerifyCode, email, null, false)
  66. .then(result => {
  67. if (result.bindWechat) {
  68. this.close();
  69. } else {
  70. this.setState({ type: BIND_WX });
  71. }
  72. })
  73. .catch(err => {
  74. if (err.message.indexOf('验证码') >= 0) {
  75. this.setState({ validError: err.message });
  76. } else if (err.message.indexOf('邮箱') >= 0) {
  77. this.setState({ emailError: err.message });
  78. } else {
  79. this.setState({ mobileError: err.message });
  80. }
  81. });
  82. }
  83. bind() {
  84. const { data, needEmail, mobileError, emailError, validError, error } = this.state;
  85. const { area, mobile, mobileVerifyCode, email } = data;
  86. if (mobileError || emailError || validError) return;
  87. if (!area || !mobile || !mobileVerifyCode || (needEmail && !email)) {
  88. error[BIND_PHONE] = { mobile: !mobile, mobileVerifyCode: !mobileVerifyCode, email: !email };
  89. this.setState({ error });
  90. return;
  91. }
  92. error[BIND_PHONE] = {};
  93. this.setState({ error });
  94. User.bind(area, mobile, mobileVerifyCode, email)
  95. .then(() => {
  96. this.close();
  97. })
  98. .catch(err => {
  99. if (err.message.indexOf('验证码') >= 0) {
  100. this.setState({ validError: err.message });
  101. } else if (err.message.indexOf('邮箱') >= 0) {
  102. this.setState({ emailError: err.message });
  103. } else {
  104. this.setState({ mobileError: err.message });
  105. }
  106. });
  107. }
  108. scanLogin(code) {
  109. User.loginWechat(code, false)
  110. .then(result => {
  111. if (result.bindMobile) {
  112. this.close();
  113. } else {
  114. this.setState({ type: BIND_PHONE });
  115. }
  116. })
  117. .catch(err => {
  118. this.setState({ type: BIND_WX_ERROR, wechatError: err.message });
  119. });
  120. }
  121. scanBind(code) {
  122. User.loginWechat(code, false)
  123. .then(() => {
  124. this.close();
  125. })
  126. .catch(err => {
  127. this.setState({ type: BIND_WX_ERROR, wechatError: err.message });
  128. });
  129. }
  130. changeData(field, value) {
  131. let { data } = this.state;
  132. data = data || {};
  133. data[field] = value;
  134. this.setState({ data });
  135. }
  136. validMobile(login) {
  137. const { data } = this.state;
  138. const { area, mobile } = data;
  139. if (!area || !mobile) return;
  140. this.validMobileNumber += 1;
  141. const number = this.validMobileNumber;
  142. User.validWechat(area, mobile)
  143. .then(result => {
  144. if (result) {
  145. this.setState({ mobileError: '' });
  146. return User.validMobile(area, mobile).then(r => {
  147. if (number !== this.validMobileNumber) return;
  148. this.setState({ needEmail: r });
  149. });
  150. }
  151. this.setState({ needEmail: false });
  152. return login ? Promise.resolve() : Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
  153. })
  154. .catch(err => {
  155. this.setState({ mobileError: err.message });
  156. });
  157. }
  158. validEmail() {
  159. const { data } = this.state;
  160. const { email } = data;
  161. if (!email) return;
  162. this.validEmailNumber += 1;
  163. const number = this.validEmailNumber;
  164. User.validEmail(email)
  165. .then(result => {
  166. if (number !== this.validEmailNumber) return Promise.resolve();
  167. if (result) {
  168. this.setState({ emailError: '' });
  169. return Promise.resolve();
  170. }
  171. return Promise.reject(new Error('该邮箱已绑定其他账号,请更换邮箱地址'));
  172. })
  173. .catch(err => {
  174. this.setState({ emailError: err.message });
  175. });
  176. }
  177. sendValid() {
  178. const { data, mobileError } = this.state;
  179. const { area, mobile } = data;
  180. if (!area || !mobile || mobileError) return Promise.reject();
  181. return Common.sendSms(area, mobile)
  182. .then(result => {
  183. if (result) {
  184. asyncSMessage('发送成功');
  185. this.setState({ mobileError: '', validError: '' });
  186. } else {
  187. throw new Error('发送失败');
  188. }
  189. })
  190. .catch(err => {
  191. this.setState({ mobileError: err.message });
  192. throw err;
  193. });
  194. }
  195. render() {
  196. const { type } = this.state;
  197. const { user } = this.props;
  198. return (
  199. <Modal
  200. ref={ref => {
  201. this.modalR = ref;
  202. }}
  203. wrapClassName={`login-modal ${type}`}
  204. visible={user.needLogin}
  205. footer={null}
  206. closable={false}
  207. width={470}
  208. >
  209. {this.renderBody(type)}
  210. <GIcon
  211. name="close"
  212. onClick={() => {
  213. if (type === BIND_WX_ERROR) {
  214. // 绑定微信错误,返回重新绑定微信
  215. this.setState({ type: BIND_WX });
  216. } else {
  217. User.closeLogin(new Error('no login'));
  218. }
  219. }}
  220. />
  221. </Modal>
  222. );
  223. }
  224. renderBody(type) {
  225. switch (type) {
  226. case LOGIN_WX:
  227. return this.renderLoginWx();
  228. case BIND_PHONE:
  229. return this.renderBindPhone();
  230. case BIND_WX:
  231. return this.renderBindWx();
  232. case BIND_WX_ERROR:
  233. return this.renderBindWxError();
  234. case LOGIN_PHONE:
  235. default:
  236. return this.renderLoginPhone();
  237. }
  238. }
  239. renderLoginPhone() {
  240. const { needEmail = true, registerContract = {}, privacyContract, error = {} } = this.state;
  241. const emptyError = error[LOGIN_PHONE] || {};
  242. return (
  243. <div className="body">
  244. <div className="title">手机号登录</div>
  245. <div className="tip" hidden={!needEmail}>
  246. <Assets name="notice" />
  247. 该手机号尚未注册,将自动为您注册账户
  248. </div>
  249. <SelectInput
  250. placeholder="请输入手机号"
  251. selectValue={this.state.data.area}
  252. select={MobileArea}
  253. value={this.state.data.mobile}
  254. error={this.state.mobileError}
  255. empty={emptyError.mobile}
  256. onSelect={value => {
  257. this.changeData('area', value);
  258. this.validMobile(true);
  259. }}
  260. onChange={e => {
  261. this.changeData('mobile', e.target.value);
  262. this.validMobile(true);
  263. }}
  264. />
  265. <VerificationInput
  266. placeholder="请输入验证码"
  267. value={this.state.data.mobileVerifyCode}
  268. error={this.state.validError}
  269. empty={emptyError.mobileVerifyCode}
  270. onSend={() => {
  271. return this.sendValid();
  272. }}
  273. onChange={e => {
  274. this.changeData('mobileVerifyCode', e.target.value);
  275. }}
  276. />
  277. {needEmail && (
  278. <Input
  279. placeholder="请输入邮箱"
  280. value={this.state.data.email}
  281. empty={emptyError.email}
  282. error={this.state.emailError}
  283. onChange={e => {
  284. this.changeData('email', e.target.value);
  285. this.validEmail();
  286. }}
  287. />
  288. )}
  289. {needEmail && (
  290. <div>
  291. <RadioItem checked theme="white" className="m-r-5" />
  292. 我已同意{' '}
  293. <a href={`/contract/${registerContract.key}`} target="_blank">
  294. 《{registerContract.title}》
  295. </a>{' '}
  296. <a href={`/contract/${privacyContract.key}`} target="_blank">
  297. 《{privacyContract.title}》
  298. </a>
  299. </div>
  300. )}
  301. <Button
  302. type="primary"
  303. size="large"
  304. block
  305. onClick={() => {
  306. this.login();
  307. }}
  308. >
  309. 登录
  310. </Button>
  311. <Tooltip overlayClassName="gray" placement="left" title="微信扫码登录">
  312. <a
  313. className="other"
  314. onClick={() => {
  315. this.setState({ type: LOGIN_WX });
  316. }}
  317. >
  318. <Assets name="code" />
  319. </a>
  320. </Tooltip>
  321. </div>
  322. );
  323. }
  324. renderLoginWx() {
  325. return (
  326. <div className="body">
  327. <div className="title">微信扫码登录</div>
  328. <div className="qr-code">
  329. <iframe
  330. frameBorder="0"
  331. src={`/login.html?appid=${WechatPcAppId}&redirectUri=${encodeURIComponent(PcUrl)}`}
  332. width="300"
  333. height="300"
  334. />
  335. <div className="text">请使用微信扫描二维码登录</div>
  336. </div>
  337. <Tooltip overlayClassName="gray" placement="left" title="手机号登录">
  338. <a
  339. className="other"
  340. onClick={() => {
  341. this.setState({ type: LOGIN_PHONE });
  342. }}
  343. >
  344. <Assets name="phone" />
  345. </a>
  346. </Tooltip>
  347. </div>
  348. );
  349. }
  350. renderBindPhone() {
  351. const { needEmail = true, registerContract = {}, privacyContract = {}, error = {} } = this.state;
  352. const emptyError = error[BIND_PHONE] || {};
  353. return (
  354. <div className="body">
  355. <div className="title">绑定手机号</div>
  356. <div className="tip">
  357. <Assets name="notice" />
  358. 微信登录成功!为更好的使用服务,请您绑定手机号和邮箱。
  359. </div>
  360. <SelectInput
  361. placeholder="请输入手机号"
  362. selectValue={this.state.data.area}
  363. select={MobileArea}
  364. value={this.state.data.mobile}
  365. error={this.state.mobileError}
  366. empty={emptyError.mobile}
  367. onSelect={value => {
  368. this.changeData('area', value);
  369. this.validMobile(false);
  370. }}
  371. onChange={e => {
  372. this.changeData('mobile', e.target.value);
  373. this.validMobile(false);
  374. }}
  375. />
  376. <VerificationInput
  377. placeholder="请输入验证码"
  378. value={this.state.data.mobileVerifyCode}
  379. error={this.state.validError}
  380. empty={emptyError.mobileVerifyCode}
  381. onSend={() => {
  382. return this.sendValid();
  383. }}
  384. onChange={e => {
  385. this.changeData('mobileVerifyCode', e.target.value);
  386. }}
  387. />
  388. {needEmail && (
  389. <Input
  390. placeholder="请输入邮箱"
  391. value={this.state.data.email}
  392. empty={emptyError.email}
  393. error={this.state.emailError}
  394. onChange={e => {
  395. this.changeData('email', e.target.value);
  396. this.validEmail();
  397. }}
  398. />
  399. )}
  400. {needEmail && (
  401. <div className="m-b-2">
  402. <RadioItem checked theme="white" className="m-r-5" />
  403. 我已同意{' '}
  404. <a href={`/contract/${registerContract.key}`} target="_blank">
  405. 《{registerContract.title}》
  406. </a>{' '}
  407. <a href={`/contract/${privacyContract.key}`} target="_blank">
  408. 《{privacyContract.title}》
  409. </a>
  410. <a className="f-r" onClick={() => this.setState({ type: LOGIN_PHONE })}>
  411. 使用手机号码登录
  412. </a>
  413. </div>
  414. )}
  415. {!needEmail && (
  416. <div className="m-b-2">
  417. <a className="f-r" onClick={() => this.setState({ type: LOGIN_PHONE })}>
  418. 使用手机号码登录
  419. </a>
  420. </div>
  421. )}
  422. <Button
  423. type="primary"
  424. size="large"
  425. disabled={this.state.validError || this.state.mobileError}
  426. block
  427. onClick={() => {
  428. this.bind();
  429. }}
  430. >
  431. 绑定
  432. </Button>
  433. </div>
  434. );
  435. }
  436. renderBindWx() {
  437. return (
  438. <div className="body">
  439. <div className="title">绑定微信号</div>
  440. <div className="tip">
  441. <Assets name="notice" />
  442. 手机号注册成功!为更好的使用服务,建议您绑定微信号。
  443. </div>
  444. <div className="qr-code">
  445. <iframe
  446. frameBorder="0"
  447. src={`/login.html?appid=${WechatPcAppId}&redirectUri=${encodeURIComponent(PcUrl)}`}
  448. width="300"
  449. height="300"
  450. />
  451. <div className="text">请使用微信扫描二维码登录</div>
  452. <div
  453. className="jump"
  454. onClick={() => {
  455. this.close();
  456. }}
  457. >
  458. 跳过
  459. </div>
  460. </div>
  461. </div>
  462. );
  463. }
  464. renderBindWxError() {
  465. return (
  466. <div className="body">
  467. <div className="title">绑定失败</div>
  468. <div className="text">该微信账户已绑定其他手机号码,您可直接扫码登入。</div>
  469. <div className="btn">
  470. <GButton
  471. radius
  472. onClick={() => {
  473. this.setState({ type: LOGIN_WX });
  474. }}
  475. >
  476. 扫码登入
  477. </GButton>
  478. </div>
  479. </div>
  480. );
  481. }
  482. }
  483. export class Input extends Component {
  484. render() {
  485. const { className = '', onChange, placeholder, value, error, left, right, empty } = this.props;
  486. return (
  487. <div className={`g-input-container ${className}`}>
  488. <div className={`g-input-wrapper ${error ? 'error' : ''}`}>
  489. {left && <div className="g-input-left">{left}</div>}
  490. <input
  491. className={`g-input ${empty ? 'empty' : ''}`}
  492. placeholder={placeholder}
  493. value={value}
  494. onChange={data => onChange && onChange(data)}
  495. />
  496. {right && <div className="g-input-right">{right}</div>}
  497. </div>
  498. <div hidden={!error} className="g-input-error">
  499. {error}
  500. </div>
  501. </div>
  502. );
  503. }
  504. }
  505. export class SelectInput extends Component {
  506. constructor(props) {
  507. super(props);
  508. this.state = { showSelect: false };
  509. }
  510. render() {
  511. const { showSelect } = this.state;
  512. const { className = '', onChange, placeholder, value, error, empty, selectValue, select, onSelect } = this.props;
  513. return (
  514. <Input
  515. className={className}
  516. left={
  517. <span className="g-input-left-select" onClick={() => this.setState({ showSelect: !showSelect })}>
  518. {selectValue}
  519. <Icon type={showSelect ? 'up' : 'down'} />
  520. {showSelect && (
  521. <ul className="select-list">
  522. {select.map(row => {
  523. return (
  524. <li
  525. onClick={() => {
  526. this.setState({ showSelect: false });
  527. if (onSelect) onSelect(row.value);
  528. }}
  529. >
  530. {row.label}
  531. </li>
  532. );
  533. })}
  534. </ul>
  535. )}
  536. </span>
  537. }
  538. value={value}
  539. placeholder={placeholder}
  540. onChange={data => onChange && onChange(data)}
  541. error={error}
  542. empty={empty}
  543. />
  544. );
  545. }
  546. }
  547. export class VerificationInput extends Component {
  548. constructor(props) {
  549. super(props);
  550. this.timeKey = null;
  551. this.state = { loading: 0 };
  552. }
  553. componentWillUnmount() {
  554. if (this.timeKey) clearTimeout(this.timeKey);
  555. }
  556. onSend() {
  557. const { onSend, time = 60 } = this.props;
  558. if (onSend) {
  559. onSend().then(() => {
  560. if (this.timeKey) clearTimeout(this.timeKey);
  561. this.setTime(time);
  562. });
  563. }
  564. }
  565. setTime(time) {
  566. this.setState({ loading: time });
  567. this.timeKey = setTimeout(() => {
  568. this.setTime(time - 1);
  569. }, 1000);
  570. }
  571. render() {
  572. const { loading } = this.state;
  573. const { className = '', onChange, placeholder, value, error, empty } = this.props;
  574. return (
  575. <Input
  576. className={className}
  577. right={
  578. loading <= 0 ? (
  579. <span className="g-input-right-verification" onClick={() => this.onSend()}>
  580. 获取验证码
  581. </span>
  582. ) : (
  583. <span className="g-input-right-verification-loading">等待{loading}秒</span>
  584. )
  585. }
  586. value={value}
  587. error={error}
  588. empty={empty}
  589. placeholder={placeholder}
  590. onChange={data => onChange && onChange(data)}
  591. />
  592. );
  593. }
  594. }