index.js 15 KB

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