UNPKG

wodax-login-qrcode.react

Version:

login component with qrcode for WodaX applications

793 lines (694 loc) 21.1 kB
// @flow import React, { createElement, PureComponent } from 'react'; import QRCode from 'qrcode.react'; import PropTypes from 'prop-types'; const StateQrCodeLogin = { ready: `ready`, // 准备阶段,二维码未获取,界面准备阶段 loginFailed: `loginFailed`, // 登录反馈阶段:二维码扫描登录失败 loginSuccess: `loginSuccess`, // 登录反馈阶段:二维码扫描登录成功 scanComplete: `scanComplete`, // 登录反馈阶段: 二维码已经扫描,等待用户授权 codeOutWork: `codeOutWork`, // 登录反馈阶段: 二维码失效 networkLost: `networkLost`, // 登录反馈阶段:长连接丢失 qrCodeGetting: `qrCodeGetting`, // 信息获取:二维码获取... qrCodeGetSuccess: `qrCodeGetSuccess`, // 信息获取:二维码获取成功 qrCodeGetFailed: `qrCodeGetFailed`, // 信息获取:二维码获取失败 }; const ErrorType = { eventSourceError: 0, // eventSource error }; const noop = () => {}; let GlobalDebugEnable = false; const debugLog = function(type, ...args) { try { if (GlobalDebugEnable) { console[type || `log`].apply(console, [' [WODAX_DEBUG]: '].concat(args)); } else { noop(); } } catch (e) { console.error(e); } }; class WodaXLoginWithQrCode extends PureComponent { // 静态方法:暴露状态枚举 static getEnumStateQrCodeLogin = () => StateQrCodeLogin; state = { // 默认状态 currentQrLoginState: StateQrCodeLogin.ready, // 初始二维码值,二维码值来源于内部封装服务或者外部服务 qrcode: ``, // 接收到的事件存储起来 sseEvents: [], }; componentDidMount(): * { this.updateQrCode(); } componentWillUnmount(): * { this.uninstallEventSource(); } render() { const { className, style, titleRender, qrCodeRender, toolTipRender, additionLayerRender, refreshQrCodeRender, sseMessageRender, } = this.props; return ( <div className={className} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', ...style, }} > {/*标题渲染*/} {titleRender(this)} {/*显示二维码*/} {qrCodeRender(this)} {/*显示提示信息*/} {toolTipRender(this)} {/*附加层渲染处理*/} {additionLayerRender(this)} {/*刷新渲染*/} {refreshQrCodeRender(this)} {/*接收实时数据渲染*/} {sseMessageRender(this)} </div> ); } /** * 获取或更新二维码 */ updateQrCode = () => { const { options, fetchQrCodeHandler } = this.props; const { qrcode: oldQrCode } = this.state; const { useEventSourceForFetchQrCode } = options; this.setState( { currentQrLoginState: StateQrCodeLogin.qrCodeGetting, }, () => { if (!useEventSourceForFetchQrCode) { (async () => { if (fetchQrCodeHandler) { try { const response = await fetchQrCodeHandler(this); const { code, data } = response; if (code === 0) { if (oldQrCode === data) { this.setState({ currentQrLoginState: StateQrCodeLogin.qrCodeGetSuccess, }); } else { this.uninstallEventSource(); this.setState( { currentQrLoginState: StateQrCodeLogin.qrCodeGetSuccess, qrcode: data, }, () => { this.installEventSource({ qrcode: data, }); } ); } } else { this.uninstallEventSource(); this.setState({ currentQrLoginState: StateQrCodeLogin.qrCodeGetFailed, }); } } catch (e) { console.error(e); } } })(); } else { this.uninstallEventSource(); this.installEventSource(); } } ); }; installEventSource = (config = {}) => { const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options, onReceiveMessageHandler, debugEnable } = this.props; // 设置全局DebugEnable GlobalDebugEnable = debugEnable; // 获取默认设置的EventSource可以处理的消息类型 const { clientEventSource } = { ...defaultOptions, ...options, }; const { url, params, messageTypes, withCredentials, saveAllEvents, } = clientEventSource; const paramsString = Object.entries({ ...params, ...config }) .map(([key, value]) => `${key}=${value}`) .join(`&`); // 获取服务地址 const serviceUrl = encodeURI( `${url}${paramsString.length > 0 ? `?` : ``}${paramsString}` ); debugLog(`log`, `---> SSE url`, serviceUrl); if (EventSource) { // 判断当前存在的sse的url是否已经创建过 if (this.sse) { const { url } = this.sse; if (url === serviceUrl) { return; } else { this.uninstallEventSource(); } } // 创建 EventSource 实例 debugLog(`log`, `--> installEventSource`); this.sse = new EventSource(serviceUrl, { withCredentials: !!withCredentials, }); // 注册事件监听 let sseMsgTypes = messageTypes || []; sseMsgTypes = sseMsgTypes.includes(`message`) ? sseMsgTypes : sseMsgTypes.concat([`message`]); sseMsgTypes.forEach(type => { this.sse.addEventListener( type, message => { this.setState(prevState => { let events = (saveAllEvents ? prevState.sseEvents : []).concat( message ); return { sseEvents: events, }; }); onReceiveMessageHandler && onReceiveMessageHandler(message, this); }, false ); }); this.sse.onopen = e => { debugLog(`log`, `---> SSE Open`); onReceiveMessageHandler && onReceiveMessageHandler(e, this); }; // 连接异常时会触发 error 事件并自动重连 this.sse.onerror = e => { const { target } = e; const { readyState } = target; if (EventSource.CLOSED === readyState) { debugLog(`log`, `---> SSE Disconnected`); } else if (EventSource.CONNECTING === readyState) { debugLog(`log`, `---> SSE Connecting...`); } onReceiveMessageHandler && onReceiveMessageHandler(e, this); this.onErrorEventHandler({ type: ErrorType.eventSourceError, data: e, }); }; } else { console.error(`Your browser doesn't support SSE`); } }; uninstallEventSource = () => { const { debugEnable } = this.props; if (this.sse) { try { debugLog(`log`, `--> uninstallEventSource`); this.sse.close(); } catch (e) { debugLog(`error`, `--> uninstallEventSource [error]`, e); } } }; /** * error 处理器 * @param errorInfo */ onErrorEventHandler = errorInfo => { try { const { type, data } = errorInfo; if (type === ErrorType.eventSourceError) { } else { } } catch (e) { console.error(e); } }; } /** * 封装默认的渲染函数 * @private */ const _default = { /** * 最上部分标题渲染 * @param instance React Object * @returns {*} */ titleRender: instance => { const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options } = instance.props; const { title, titleStyle, titleClassName, appLogo, appLogoClassName, appLogoStyle, } = { ...defaultOptions, ...options }; return ( <div className={titleClassName} style={{ padding: '4px', ...titleStyle, }} wodaxuiname={`TitleContainer`} > <span>{title}</span> {appLogo !== `` ? ( <div className={appLogoClassName} style={appLogoStyle} wodaxuiname={`appLogoContainer`} > <img src={appLogo} alt={appLogo} /> </div> ) : null} </div> ); }, /** * 二维码显示渲染 * @param instance React Object * @returns {*} */ qrCodeRender: instance => { const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options } = instance.props; const { qrCodeContainerClassName, qrCodeContainerStyle, qrCodeContainerOpacity, qrCodeConfig, } = { ...defaultOptions, ...options, }; const { qrcode, currentQrLoginState } = instance.state; const opacity = [ StateQrCodeLogin.loginFailed, StateQrCodeLogin.loginSuccess, StateQrCodeLogin.scanComplete, StateQrCodeLogin.codeOutWork, StateQrCodeLogin.networkLost, StateQrCodeLogin.qrCodeGetting, StateQrCodeLogin.qrCodeGetFailed, ].includes(currentQrLoginState) ? qrCodeContainerOpacity[`min`] : qrCodeContainerOpacity[`max`]; return ( <div className={qrCodeContainerClassName} style={{ padding: '4px', ...qrCodeContainerStyle, opacity: opacity, }} wodaxuiname={`qrCodeContainer`} > {qrcode !== `` && ( <QRCode value={qrcode} renderAs={qrCodeConfig.renderAs} size={qrCodeConfig.size} bgColor={qrCodeConfig.bgColor} fgColor={qrCodeConfig.fgColor} level={qrCodeConfig.level} includeMargin={qrCodeConfig.includeMargin} /> )} </div> ); }, /** * 提示信息显示渲染 * @param instance React Object * @returns {*} */ toolTipRender: instance => { const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options } = instance.props; const { toolTipContainerClassName, toolTipContainerStyle, toolTipContainerProps, } = { ...defaultOptions, ...options, }; const { currentQrLoginState } = instance.state; const uiMapRender = () => { let uiMap = {}; // eslint-disable-next-line array-callback-return Object.entries(StateQrCodeLogin).map(([key, value]) => { [`Text`, `ClassName`, `Style`].forEach(attribute => { if ( { ...defaultOptions, ...options }.hasOwnProperty( `${key}${attribute}` ) ) { uiMap[key] = uiMap[key] || {}; uiMap[key] = { [`${attribute}`]: { ...defaultOptions, ...options }[ `${key}${attribute}` ], ...uiMap[key], }; } }); }); const eleProps = uiMap[currentQrLoginState]; return eleProps ? createElement( 'div', { className: toolTipContainerClassName, style: toolTipContainerStyle, wodaxuiname: `toolTipContainer`, ...toolTipContainerProps, }, <div className={eleProps[`ClassName`]} style={{ padding: '4px', ...eleProps[`Style`], }} > <span>{eleProps[`Text`]}</span> </div> ) : null; }; return uiMapRender(); }, /** * 附加层处理,完全由外部绘制 * @param instance * @returns {null} */ additionLayerRender: instance => { return null; }, /** * 刷新操作显示渲染 * @param instance React Object * @returns {*} */ refreshQrCodeRender: instance => { const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options } = instance.props; const { refreshContainerShowEveryTime, refreshContainerProps, refreshButtonCaption, refreshButtonElement, refreshButtonProps, } = { ...defaultOptions, ...options, }; const { currentQrLoginState } = instance.state; const enableRenderElement = [ // StateQrCodeLogin.ready, StateQrCodeLogin.loginFailed, StateQrCodeLogin.codeOutWork, // StateQrCodeLogin.qrCodeGetting, // StateQrCodeLogin.qrCodeGetSuccess, StateQrCodeLogin.qrCodeGetFailed, ].indexOf(currentQrLoginState) > 0 || refreshContainerShowEveryTime; return ( enableRenderElement && createElement( `div`, { wodaxuiname: `RefreshContainer`, ...refreshContainerProps, }, createElement( refreshButtonElement, { wodaxuiname: `RefreshButton`, onClick: () => { instance.updateQrCode(); }, ...refreshButtonProps, }, refreshButtonCaption ) ) ); }, /** * 实时信息接收处理 * @param instance React Object * @returns {null} */ sseMessageRender: instance => { const { sseEvents } = instance.state; const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps; const { options } = instance.props; const { showSSEMessage } = { ...defaultOptions, ...options, }; return instance.sse && sseEvents.length > 0 && showSSEMessage ? ( <div wodaxuiname={`sseMessageRender`} children={sseEvents.map(eventMsg => { // TODO: 根据事件的数据,来处理 return <p>{eventMsg.data}</p>; })} /> ) : null; }, /** * 获取二维码接口 * @param instance WodaXLoginWithQrCode 实例 * @returns {Promise<R>|Promise} */ fetchQrCodeHandler: instance => new Promise((resolve, reject) => { resolve({ code: 0, data: `http://ops.woda.ink/wodax/umi/${Date.now()}`, }); }), /** * 接收SSE发过来的数据 * @param data SSE发送的数据 * @param instance WodaXLoginWithQrCode 实例 */ onReceiveMessageHandler: (data, instance) => { // debugLog(`log`, `onReceiveMessageHandler -->`, data); }, }; WodaXLoginWithQrCode.propTypes = { className: PropTypes.string, Style: PropTypes.object, // 是否开启日志调试 debugEnable: PropTypes.bool, options: PropTypes.shape({ // 基础参数: {eventSource使用} // 是否使用EventSource来重新获取最新QrCode useEventSourceForFetchQrCode: PropTypes.bool, // EventSource 的配置 clientEventSource: PropTypes.shape({ // EventSource 主要服务url url: PropTypes.string, // EventSource 附带参数 params: PropTypes.object, // EventSource 可以处理的消息类型 messageTypes: PropTypes.array, // withCredentials withCredentials: PropTypes.bool, // EventSource 是否保存所有接收到的事件 saveAllEvents: PropTypes.bool, }), // 基础参数: {Title使用} // 显示的Title内容 title: PropTypes.string, titleStyle: PropTypes.object, titleClassName: PropTypes.string, // 基础参数: {AppLogo使用} // 显示App的Logo appLogo: PropTypes.string, appLogoClassName: PropTypes.string, appLogoStyle: PropTypes.object, // 基础参数: {二维码配置} // QrCode 二维码配置 // see: https://github.com/zpao/qrcode.react qrCodeConfig: PropTypes.shape({ renderAs: PropTypes.string, size: PropTypes.number, bgColor: PropTypes.string, fgColor: PropTypes.string, level: PropTypes.string, includeMargin: PropTypes.bool, }), qrCodeContainerClassName: PropTypes.string, qrCodeContainerStyle: PropTypes.object, qrCodeContainerOpacity: PropTypes.shape({ min: PropTypes.number, max: PropTypes.number, }), // 基础参数: {提示信息} toolTipContainerClassName: PropTypes.string, toolTipContainerStyle: PropTypes.object, toolTipContainerProps: PropTypes.object, codeOutWorkText: PropTypes.string, codeOutWorkClassName: PropTypes.string, codeOutWorkStyle: PropTypes.object, loginFailedText: PropTypes.string, loginFailedClassName: PropTypes.string, loginFailedStyle: PropTypes.object, loginSuccessText: PropTypes.string, loginSuccessClassName: PropTypes.string, loginSuccessStyle: PropTypes.object, qrCodeGetFailedText: PropTypes.string, qrCodeGetFailedClassName: PropTypes.string, qrCodeGetFailedStyle: PropTypes.object, scanCompleteText: PropTypes.string, scanCompleteClassName: PropTypes.string, scanCompleteStyle: PropTypes.object, // 基础参数: {刷新,重新获取二维码} // 是否一直显示刷新区域 refreshContainerShowEveryTime: PropTypes.bool, // 刷新最外层的div的Props refreshContainerProps: PropTypes.object, // 指定按钮的标题 refreshButtonCaption: PropTypes.string, // 指定按钮的选择的元素 refreshButtonElement: PropTypes.string | PropTypes.element | PropTypes.elementType | PropTypes.func, // 指定按钮的Props refreshButtonProps: PropTypes.object, // 基础参数: {实时信息接收处理} // 是否默认显示sse回显信息 showSSEMessage: PropTypes.bool, }), // 数据请求接口,可以覆盖 fetchQrCodeHandler: PropTypes.func.isRequired, // 数据反馈接口。外接应用,可以自己处理所有来自服务器的消息 onReceiveMessageHandler: PropTypes.func, // 渲染接口, 可以覆盖 titleRender: PropTypes.func.isRequired, qrCodeRender: PropTypes.func.isRequired, toolTipRender: PropTypes.func.isRequired, additionLayerRender: PropTypes.func, refreshQrCodeRender: PropTypes.func.isRequired, sseMessageRender: PropTypes.func.isRequired, }; WodaXLoginWithQrCode.defaultProps = { className: `wodax-login-with-qrcode`, Style: {}, // 是否开启日志调试 debugEnable: false, // 可选项 options: { // 基础参数: {eventSource使用} // 是否使用EventSource来重新获取最新QrCode useEventSourceForFetchQrCode: false, clientEventSource: { // EventSource 主要服务url url: `https://stream.wikimedia.org/v2/stream/recentchange`, // EventSource 附带参数 params: {}, // EventSource 可以处理的消息类型 messageTypes: [`message`], // withCredentials withCredentials: false, // EventSource 是否保存所有 saveAllEvents: false, }, // 基础参数: {Title使用} title: `打开App,扫码登录`, titleClassName: `qr-login-title`, appLogo: ``, // QrCode 二维码配置 qrCodeConfig: { renderAs: `canvas`, size: 128, bgColor: `#FFFFFF`, fgColor: `#000000`, level: `L`, includeMargin: false, }, qrCodeContainerOpacity: { min: 0.01, max: 1, }, // 提示信息 toolTipContainerClassName: `qr-login-tooltip`, toolTipContainerStyle: { padding: '4px', position: 'absolute', }, codeOutWorkText: `二维码已失效`, loginFailedText: `登录失败`, loginSuccessText: `登录成功`, qrCodeGetFailedText: `获取二维码失败`, scanCompleteText: `扫描成功! 请在手机上确认登录`, // 基础参数: {刷新区域,重新获取二维码} // 是否一直显示刷新区域 refreshContainerShowEveryTime: false, refreshContainerProps: { style: { padding: '4px', position: 'absolute', marginTop: '40px', }, }, // 点击刷新 refreshButtonCaption: `请点击刷新`, refreshButtonElement: `button`, refreshButtonProps: { style: { padding: '4px', }, }, // 基础参数: {实时信息接收处理} // 是否默认显示sse回显信息 showSSEMessage: false, }, // 数据请求接口,可以覆盖 fetchQrCodeHandler: _default.fetchQrCodeHandler, // 数据反馈接口。外接应用,可以自己处理所有来自服务器的消息 onReceiveMessageHandler: _default.onReceiveMessageHandler, // 渲染接口, 可以覆盖,重新写 titleRender: _default.titleRender, qrCodeRender: _default.qrCodeRender, toolTipRender: _default.toolTipRender, additionLayerRender: _default.additionLayerRender, refreshQrCodeRender: _default.refreshQrCodeRender, sseMessageRender: _default.sseMessageRender, }; export default WodaXLoginWithQrCode;