diff --git a/components/mip-city-selection/example/data.json b/components/mip-city-selection/example/data.json new file mode 100644 index 00000000..3c976546 --- /dev/null +++ b/components/mip-city-selection/example/data.json @@ -0,0 +1,156 @@ +{ + "list": [{ + "key": "热门", + "cities": [ + { + "city": "北京", + "pinyin": "beijing", + "code": "1" + }, + { + "city": "上海", + "pinyin": "shanghai", + "code": "2" + }, + { + "city": "广州", + "pinyin": "guangzhou", + "code": "3" + }, + { + "city": "深圳", + "pinyin": "shenzhen", + "code": "4" + }, + { + "city": "重庆", + "pinyin": "chongqing", + "code": "5" + } + ] + }, { + "key": "A", + "cities": [ + { + "city": "澳门", + "pinyin": "aomen", + "code": "7" + }, + { + "city": "安庆", + "pinyin": "anqing", + "code": "8" + }, + { + "city": "安泽", + "pinyin": "anze", + "code": "9" + } + ] + }, { + "key": "B", + "cities": [ + { + "city": "宝清", + "pinyin": "baoqing", + "code": "10" + }, + { + "city": "宝鸡", + "pinyin": "baoji", + "code": "11" + }, + { + "city": "巴东", + "pinyin": "badong", + "code": "12" + } + ] + }, { + "key": "C", + "cities": [ + { + "city": "重庆", + "pinyin": "chongqing", + "code": "13" + }, + { + "city": "成都", + "pinyin": "chengdu", + "code": "14" + }, + { + "city": "苍山", + "pinyin": "cangshan", + "code": "15" + } + ] + }, { + "key": "D", + "cities": [ + { + "city": "大庆", + "pinyin": "daqing", + "code": "16" + }, + { + "city": "大理", + "pinyin": "dali", + "code": "17" + }, + { + "city": "东莞", + "pinyin": "dongguan", + "code": "18" + } + ] + }, { + "key": "E", + "cities": [ + { + "city": "鄂尔多斯", + "pinyin": "eerduosi", + "code": "19" + }, + { + "city": "峨眉山", + "pinyin": "emeishan", + "code": "20" + } + ] + }, { + "key": "F", + "cities": [ + { + "city": "阜阳", + "pinyin": "fuyang", + "code": "21" + }, + { + "city": "福州", + "pinyin": "fuzhou", + "code": "22" + }, + { + "city": "防城港", + "pinyin": "fangchenggang", + "code": "23" + } + ] + }, + { + "key": "G", + "cities": [ + { + "city": "广州", + "pinyin": "guangzhou", + "code": "24" + }, + { + "city": "贵阳", + "pinyin": "guiyang", + "code": "25" + } + ] + }] +} diff --git a/components/mip-city-selection/example/mip-city-selection.html b/components/mip-city-selection/example/mip-city-selection-inner-data.html similarity index 89% rename from components/mip-city-selection/example/mip-city-selection.html rename to components/mip-city-selection/example/mip-city-selection-inner-data.html index cdd7d48f..800c3732 100644 --- a/components/mip-city-selection/example/mip-city-selection.html +++ b/components/mip-city-selection/example/mip-city-selection-inner-data.html @@ -1,6 +1,5 @@ - @@ -9,19 +8,17 @@ - - - - + + + - diff --git a/components/mip-city-selection/example/mip-city-selection-src-data.html b/components/mip-city-selection/example/mip-city-selection-src-data.html new file mode 100644 index 00000000..5f6bb0dc --- /dev/null +++ b/components/mip-city-selection/example/mip-city-selection-src-data.html @@ -0,0 +1,29 @@ + + + + + + + MIP page + + + + + + + + + + + + + + + + diff --git a/components/mip-city-selection/example/mip-test.js b/components/mip-city-selection/example/mip-test.js new file mode 100644 index 00000000..a521059c --- /dev/null +++ b/components/mip-city-selection/example/mip-test.js @@ -0,0 +1,29 @@ +/** + * @file 测试组件 + * @author mj(zoumiaojiang@gmail.com) + */ + +/* globals MIP */ + +class MIPTest extends MIP.CustomElement { + build () { + this.addEventAction('print', data => { + this.print(data) + }) + } + + /** + * 打印的测试内容 + * + * @param {any} content 打印的内容 + */ + print (content) { + if (typeof content === 'object') { + content = JSON.stringify(content, null, 2) + } + + this.element.innerHTML = '
' + content + '
' + } +} + +MIP.registerElement('mip-test', MIPTest) diff --git a/components/mip-city-selection/mip-city-selection.js b/components/mip-city-selection/mip-city-selection.js new file mode 100644 index 00000000..1e2ec5ea --- /dev/null +++ b/components/mip-city-selection/mip-city-selection.js @@ -0,0 +1,312 @@ +/** + * @file 城市选择组件 + * @author mj(zoumiaojiang@gmail.com) + */ + +/* global MIP, fetch */ + +import './mip-city-selection.less' + +const { util, CustomElement, viewport, viewer } = MIP +const { rect, log } = util +const CustomStorage = util.customStorage +const logger = log('mip-city-selection') +const Storage = new CustomStorage(0) + +const SELECTION_CONTENT_CLS = 'mip-city-selection-content' + +export default class MIPCitySelection extends CustomElement { + constructor (...element) { + super(...element) + + /** + * 规定格式的城市选择组件的数据 + * + * @type {Array} + */ + this.list = [] + + /** + * 选中历史的最大缓存数 + * + * @type {number} + */ + this.maxHistory = 3 + + /** + * 异步请求数据源 URL 地址 + * + * @type {string} + */ + this.dataSrc = '' + + /** + * 缓存的数据 + * + * @type {Array} + */ + this.history = [] + + /** + * 滚动信息记录 + * + * @type {Array} + */ + this.offsetX = [] + + /** + * 标示是否有历史缓存信息 + * + * @type {boolean} + */ + this.hasHistory = false + } + + async build () { + await this.getData() + this.render() + viewport.on('scroll', () => { + this.getOffsetX() + }) + } + + /** + * 获取 + */ + getOffsetX () { + let scrollTop = viewport.getScrollTop() + let offsetX = [] + let currentCitys = [...this.element.querySelectorAll('.mip-city-selection-city')] + + for (let city of currentCitys) { + offsetX.push(rect.getElementOffset(city).top + scrollTop) + } + + this.offsetX = offsetX + } + + /** + * 获取组件数据 + */ + async getData () { + let ele = this.element + let dataset = ele.dataset + let dataSrc = this.dataSrc = (dataset && dataset.src) + let data = [] + + // 如果存在 data-src 属性的话,发起异步请求 + if (dataSrc) { + try { + let res = await fetch(dataSrc, {}) + data = await res.json() + this.list = data.list + } catch (e) { + logger.warn(ele, '数据请求错误:', e) + } + } else { + let dataScript = ele.querySelector('script[type="application/json"]') + try { + data = JSON.parse(dataScript.textContent) + this.list = data.list + } catch (e) { + logger.warn(ele, 'JSON 配置数据异常,请检查 JSON 格式') + } + } + + if (!data) { + logger.warn(ele, '请配置正确的 data-src 或提供正确的 JSON 配置数据!') + } + + this.getOffsetX() + + // 显示 locationStorage 数据 + let locationStorage = Storage.get('cityData', data) + this.hasHistory = !!locationStorage + + if (locationStorage) { + this.history = JSON.parse(locationStorage) + } + + return data + } + + /** + * 渲染组件内容 + */ + render () { + let wrapper = document.createElement('div') + let historyVisitedCityWrapper = document.createElement('div') + let cityListWrapper = document.createElement('div') + let cityNavWrapper = document.createElement('div') + + wrapper.className = 'mip-city-selection-wrapper' + historyVisitedCityWrapper.className = cityListWrapper.className = SELECTION_CONTENT_CLS + historyVisitedCityWrapper.classList.add('lasted-visted-hot') + + this.renderHistoryVisitCityList(historyVisitedCityWrapper) + this.renderCityList(cityListWrapper) + this.renderCityNav(cityNavWrapper) + this.bindEventForHistoryVisit(historyVisitedCityWrapper) + wrapper.appendChild(historyVisitedCityWrapper) + wrapper.appendChild(cityListWrapper) + wrapper.appendChild(cityNavWrapper) + this.element.appendChild(wrapper) + } + + /** + * 渲染最近访问城市列表 + * + * @param {HTMLElement} wrapper 最近访问城市列表的最外层 DOM + */ + renderHistoryVisitCityList (wrapper) { + let historyCityList = this.history + + if (this.hasHistory && historyCityList && historyCityList.length) { + let html = [ + '
', + '
最近访问的城市
' + ] + + historyCityList.forEach((item, index) => { + html.push(`

${item.city}

`) + }) + + html.push('
') + wrapper.innerHTML = html.join('') + } + } + + /** + * 渲染城市列表 + * + * @param {HTMLElement} wrapper 城市列表外层 DOM + */ + renderCityList (wrapper) { + let listData = this.list + let html = [] + + if (listData && listData.length) { + listData && listData.forEach((item, cityIndex) => { + html.push(` +
+
${item.key}
+ `) + + item.cities && item.cities.forEach((city, itemIndex) => { + html.push( + `

${city.city}

` + ) + }) + + html.push('
') + }) + + wrapper.innerHTML = html.join('') + + // 绑定点击城市 item 的事件 + wrapper.addEventListener('click', e => { + let target = e.target + if (target.classList.contains('mip-city-selection-item')) { + let cityIndex = +target.getAttribute('cityKey') + let itemIndex = +target.getAttribute('itemKey') + cityIndex && itemIndex && this.showInfo(listData[cityIndex - 1].cities[itemIndex - 1]) + } + }) + } + } + + /** + * 渲染城市字母导航 + * + * @param {HTMLElement} wrapper 导航外层容器 DOM + */ + renderCityNav (wrapper) { + let listData = this.list + let html = [` +
` + ] + + listData && listData.forEach((item, index) => { + html.push(``) + }) + + html.push('') + wrapper.innerHTML = html.join('') + wrapper.addEventListener('click', e => { + let target = e.target + if (target.classList.contains('mip-city-selection-link')) { + let index = +target.getAttribute('key') + index && this.scrollToCity(index - 1) + } + }) + } + + /** + * 选中城市后的逻辑 + * + * @param {Object} city 城市对象信息 + */ + showInfo (city) { + this.getOffsetX() + + // 对外暴露 citySelected 事件 + viewer.eventAction.execute('citySelected', this.element, city) + + let isExit = false + for (let i = 0; i < this.history.length; i++) { + if (this.history[i].city === city.city) { + let newCity = this.history[i] + this.history.splice(i, 1) + this.history.unshift(newCity) + isExit = true + } + } + + if (!isExit) { + this.history.unshift(city) + this.history = this.history.slice(0, this.maxHistory) + } + + let cityData = JSON.stringify(this.history) + Storage.set('cityData', cityData) + this.history = JSON.parse(Storage.get('cityData')) + + let locationStorage = Storage.get('cityData', cityData) + this.hasHistory = !!locationStorage + + if (locationStorage) { + this.history = JSON.parse(locationStorage) + this.renderHistoryVisitCityList(this.element.querySelector('.lasted-visted-hot')) + } + } + + /** + * 绑定最近访问的模块 + * + * @param {HTMLElement} wrapper 组件的最外层容器 DOM + */ + bindEventForHistoryVisit (wrapper) { + // 绑定点击城市 item 的事件 + wrapper.addEventListener('click', e => { + let target = e.target + if (target.classList.contains('mip-city-selection-item')) { + let index = +target.getAttribute('key') + index && this.showInfo(this.history[index - 1]) + } + }) + } + + /** + * 滚动到对应的城市块 + * + * @param {number} index 城市块下标 + */ + scrollToCity (index) { + this.getOffsetX() + let finalOffsetX = this.offsetX[index] + viewport.setScrollTop(finalOffsetX) + } +} diff --git a/components/mip-city-selection/mip-city-selection.less b/components/mip-city-selection/mip-city-selection.less new file mode 100644 index 00000000..3a1aac3c --- /dev/null +++ b/components/mip-city-selection/mip-city-selection.less @@ -0,0 +1,170 @@ +mip-city-selection { + .wrapper { + margin: 0 auto; + text-align: center; + } + + .content-wrapper { + padding-left: 8px; + padding-right: 8px; + } + + .city-json-content:first-child { + background: #f2f2f2; + padding-right: 5%; + } + + .lasted-visted-hot p { + border-bottom: 0; + } + + .lasted-visted-hot p, + .city-json-content:first-child p { + border-bottom: 0; + display: inline-block; + width: 20%; + max-height: 40px; + line-height: 40px; + text-align: center; + border-radius: 4px; + background-color: #fff; + font-size: 14px; + margin-bottom: 15px; + margin-right: 2%; + margin-left: 2%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .city-json-content:not(:first-child) { + background: #fff; + } + + .mip-city-selection-content { + overflow-x: hidden; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 0 0 800px 0; + color: #333; + } + + .lasted-visted-hot { + padding-bottom: 0; + background: #f2f2f2; + padding-right: 5%; + padding-top: 18px; + } + + .hotCity { + background: #666; + } + + .mip-city-selection-city { + padding-top: 18px; + + &:last-child { + margin-bottom: 800px; + } + } + + .mip-city-selection-title { + padding: 0 10px; + font-size: 12px; + color: #aaa; + margin-bottom: 12px; + margin-top: 0; + } + + .mip-city-selection-part-history { + display: none; + } + + .mip-city-selection-btn { + height: 38px; + line-height: 38px; + text-align: center; + background: #f8f8f8; + } + + .mip-city-selection-item { + height: 39px; + border-bottom: 1px solid #eee; + line-height: 39px; + padding: 0 10px; + display: block; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); + } + + .mip-city-selection-item.down { + background: rgba(0, 0, 0, 0.1); + } + + .mip-city-selection-sidebar-wrapper { + top: 0; + right: 10px; + bottom: 0; + margin: 10px 0; + } + + .mip-city-selection-sidebar { + z-index: 50; + right: 7px; + overflow: scroll; + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + max-height: 100%; + padding: 20px 0; + border: 1px solid rgba(0, 0, 0, 0.05); + text-align: center; + color: #666; + border-radius: 10px; + background: rgba(255, 255, 255, 0.5); + } + + .mip-city-selection-link { + display: block; + padding: 0 10px; + font-size: 13px; + font-weight: bold; + line-height: 3; + color: #38f; + border-radius: 50%; + } + + .mip-city-selection-letter-top { + position: absolute; + z-index: 40; + top: 44px; + left: 0; + display: none; + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + height: 50px; + padding-left: 16px; + line-height: 50px; + background: #fff; + } + + .mip-city-selection-large-char { + position: absolute; + top: 50%; + left: 50%; + display: none; + width: 78px; + height: 78px; + margin-top: -39px; + margin-left: -39px; + font-size: 36px; + line-height: 78px; + text-align: center; + color: #fff; + border-radius: 3px; + background: rgba(51, 51, 51, 0.4); + } +} diff --git a/components/mip-city-selection/mip-city-selection.md b/components/mip-city-selection/mip-city-selection.md index 99029c54..b491ef3a 100644 --- a/components/mip-city-selection/mip-city-selection.md +++ b/components/mip-city-selection/mip-city-selection.md @@ -7,24 +7,25 @@ mip-city-selection 分组选择组件,可用于城市分组,英文名分组 类型|通用 支持布局|responsive,fixed-height,fill,container,fixed 所需脚本|https://c.mipcdn.com/static/v2/mip-city-selection/mip-city-selection.js | + ## 示例 ### 基本用法 -1、本地数据 - -按照如下示例配置城市数据。 +1、本地数据 +按照如下示例配置城市数据(数据层级和属性值必须和示例保持一致)。 ```html - - - + + + - ``` - 2、异步传入数据 按照如下示例配置城市数据。 @@ -239,29 +248,26 @@ mip-city-selection 分组选择组件,可用于城市分组,英文名分组 [notice]`data-src`属于前后端交互请求。由于 MIP-Cache 为 HTTPS 环境,`data-src` 要求支持 HTTPS. ```html - - - + + + + - ``` - - ## 抛出事件 ### ready -每次触发抛事件后,抛出`mip-city-selection`的`citySelected`事件,并传json数据 - -格式 如 { "city": "鄂尔多斯", "pinyin": "eerduosi", "code": "19"} - - -组件间通信请看文档 https://www.mipengine.org/doc/3-widget/6-help/3-mip-normal.html +每次触发抛事件后,抛出 `citySelected` 事件,并通过回调透传 JSON 数据。 +格式如:`{"city": "鄂尔多斯", "pinyin": "eerduosi", "code": "19"}` +组件间通信机制请看 [MIP 事件通信文档](https://www.mipengine.org/doc/3-widget/6-help/3-mip-normal.html) ## 属性说明 + ### data-src -说明:用于指向远程数据地址,异步加载并渲染。指明`data-src`后,配置在` diff --git a/components/mip-confirm/mip-confirm.js b/components/mip-confirm/mip-confirm.js new file mode 100644 index 00000000..5bb5b2ee --- /dev/null +++ b/components/mip-confirm/mip-confirm.js @@ -0,0 +1,116 @@ +import './mip-confirm.less' + +const {CustomElement, viewer} = MIP + +export default class MIPConfirm extends CustomElement { + constructor (element) { + super(element) + this.confirm = false + this.dialog = false + this.myDialog = false + this.click = false + this.cancel = this.cancel.bind(this) + this.isOk = this.isOk.bind(this) + } + + build () { + const container = document.createElement('div') + + container.innerHTML = '' + + '' + + '
' + + '
' + + '

' + + '' + + '
' + + '
' + + this.container = container + this.element.appendChild(container) + + const {isDemo, pattern} = this.props + + if (isDemo) { + this.click = true + this.myDialog = false + } + + if (pattern === 'confirm') { + this.dialog = false + this.confirm = true + } else { + this.confirm = false + this.dialog = true + } + + this.addEventAction('show', () => { + this.myDialog = true + this.render() + }) + this.addEventAction('hidden', () => { + this.myDialog = false + this.render() + }) + + this.render() + } + + cancel () { + viewer.eventAction.execute('ready', this.element, false) + this.myDialog = false + if (this.isDemo) { + this.click = true + } + this.render() + } + + isOk () { + viewer.eventAction.execute('ready', this.element, true) + this.myDialog = false + if (this.isDemo) { + this.click = true + } + this.render() + } + + render () { + const {infoTitle, infoText} = this.props + + this.container.style.display = this.myDialog ? null : 'none' + this.element.querySelector('.confirm-title').innerHTML = `
${infoTitle}
` + this.element.querySelector('.confirm-content').innerText = infoText + + const footer = this.element.querySelector('.confirm-footer') + + if (this.confirm) { + footer.innerHTML = '' + + '' + footer.querySelector('.confirm-footer-left').addEventListener('click', this.cancel) + footer.querySelector('.confirm-footer-right').addEventListener('click', this.isOk) + } + + if (this.dialog) { + footer.innerHTML = '' + footer.querySelector('.confirm-footer-bottom').addEventListener('click', this.isOk) + } + } +} + +MIPConfirm.props = { + infoText: { + type: String, + default: '' + }, + infoTitle: { + type: String, + default: '' + }, + pattern: { + type: String, + default: 'alert' + }, + isDemo: { + type: Boolean, + default: true + } +} diff --git a/components/mip-confirm/mip-confirm.less b/components/mip-confirm/mip-confirm.less new file mode 100644 index 00000000..93151198 --- /dev/null +++ b/components/mip-confirm/mip-confirm.less @@ -0,0 +1,107 @@ +mip-confirm { + .wrapper { + z-index: 1000; + margin: 0 auto; + text-align: center; + top: 33%; + width: 90%; + line-height: 1.5; + background-color: rgba(255, 255, 255, 0.95); + } + + .mask { + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.5); + } + + .toast { + border: 1px solid rgba(0, 0, 0, 0.6); + margin: 0 auto; + background: white; + + /* border-radius: 10px; */ + color: black; + text-align: center; + white-space: nowrap; + font-size: 14px; + line-height: 100%; + height: auto; + } + + .confirm-title { + position: relative; + padding: 20px 20px 10px; + margin-bottom: -25px; + text-align: center; + } + + .confirm-title div { + margin: 0; + padding: 0; + font-weight: 400px; + font-size: 18px; + } + + .confirm-content { + margin: 25px 20px; + color: #666; + text-align: center; + font-size: 14px; + } + + .confirm-footer-btn { + flex: 1; + + /* display: block; */ + position: relative; + display: inline-block; + outline: none; + margin: 0; + padding: 0; + height: 44px; + line-height: 44px; + color: #007aff; + font-size: 17px; + font-weight: 400; + cursor: pointer; + text-decoration: none; + background-color: transparent; + } + + .mip-img { + margin-top: 10px; + } + + .toast p { + padding: 10px 0; + white-space: normal; + line-height: 1.5em; + } + + .confirm-footer { + display: flex; + position: relative; + font-size: 0; + border: 0; + } + + .confirm-footer-left { + border-right-width: 0; + border-left-width: 0; + border-bottom-width: 0; + } + + .confirm-footer-right { + border-right-width: 0; + border-bottom-width: 0; + } + + .confirm-footer-bottom { + border-right-width: 0; + border-left-width: 0; + border-bottom-width: 0; + } +} diff --git a/components/mip-confirm/mip-confirm.vue b/components/mip-confirm/mip-confirm.vue deleted file mode 100644 index 6dbf3f7e..00000000 --- a/components/mip-confirm/mip-confirm.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - - - diff --git a/components/mip-inservice-login/example/mip-inservice-login.html b/components/mip-inservice-login/example/mip-inservice-login.html deleted file mode 100644 index d81f518c..00000000 --- a/components/mip-inservice-login/example/mip-inservice-login.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - MIP page - - - - - -
- -
- - - - diff --git a/components/mip-inservice-login/mip-inservice-login.md b/components/mip-inservice-login/mip-inservice-login.md deleted file mode 100644 index 0967cb0f..00000000 --- a/components/mip-inservice-login/mip-inservice-login.md +++ /dev/null @@ -1,944 +0,0 @@ -[TOC] - -# `mip-inservice-login` - -## 说明 - -登录授权组件 - - -### 使用登录授权前的准备 - -登录授权能力基于熊掌号的网页授权机制,开发者进行登录授权开发前,需先成为熊掌号开发者: - -1. 若您是熊掌号主体,请开启开发者模式,参看[开发者接入说明](http://xiongzhang.baidu.com/open/wiki/chapter1/section1.0.html?t=1526461611082) -2. 若您是第三方平台, - * [接入熊掌号开发者平台](https://xiongzhang.baidu.com/open/wiki/chapter5/section5.0.html?t=1526461611082) - * [获得熊掌号授权](https://xiongzhang.baidu.com/open/wiki/chapter5/section5.3.html?t=1526461611082) - - -### 配置网页授权域名 - -为确保验证授权过程的安全,开发者必须在平台预先注册应用所在的域名或URL,作为OAuth2.0检验授权请求中的"redirect_uri"参数。以便保证OAuth2.0在回调过程中,会回调到安全域名。 - -在`熊掌号运营中心-开发-开发者设置`([地址](https://xiongzhang.baidu.com/site/devsetting))添加三个网页授权域名: - -1. 网站主域名 - 需要在登录组件的域名 -2. MIP-Cache 域名:`mipcache.bdstatic.com` -3. MIP-Cache 站点域名,规则:`域名(.换成-).mipcdn.com`,如: - - `www.mipengine.org` -> `www-mipengine-org.mipcdn.com` - - `demo.www.mipengine.org` -> `demo-www-mipengine-org.mipcdn.com` - -网页授权域名配置 - - -### 网页授权流程 - -1. 引导用户进入授权页面同意授权,获取code; -2. 通过code换取网页授权access_token; -3. 刷新接口调用凭据access_token,避免过期; -4. 获取用户基本信息。 - -若需了解百度账号授权流程的更多内容,请参看[熊掌号主体流程说明](https://xiongzhang.baidu.com/open/wiki/chapter2/section2.0.html?t=1526461611082)、[第三方平台流程说明](http://xiongzhang.baidu.com/open/wiki/chapter5/section5.4.html?t=1526461611082) - -## 组件 - -### 引入组件 - -标题|内容 -----|---- -类型|通用 -支持布局|responsive,fixed-height,fill,container,fixed -所需脚本|https://c.mipcdn.com/static/v2/mip-inservice-login/mip-inservice-login.js - -**注意:** 登录授权组件与`mip-bind`配合使用,且`无需`在页面额外引入`mip-bind`组件的脚本。 - - -### 名词解释 - -* 百度账号登录:用户在当前浏览器环境下登录了百度账号 -* 百度账号登出:用户在当前浏览器环境下登出了百度账号 -* 百度账号授权:用户在访问第三方站点时,第三方站点可以通过百度账号登录授权机制,来获取用户基本信息,进而实现自身业务功能。账号授权存在有效期,失效后需要重新授权。 -* 第三方站点登录:用户在当前浏览器环境下登录了第三方站点。当百度账号授权成功后,第三方站点可以使用百度用户的基本信息作为一次站点登录,按照业务逻辑对该登录用户进行管理 -* 第三方站点登出:用户在当前浏览器环境下登出了第三方站点,站点不再持有用户的登录信息 -* 后端:指使用登录授权组件的第三方站点的后端 -* 前端: 指使用登录授权组件的第三方站点的前端 - - -### 组件样式 - -当触发组件的`登录`功能时,根据用户的状态,有以下几种样式形态: - -* 登录授权弹窗:用户在当前浏览器下进行过`百度账号登录`,但还未在第三方站点进行`百度账号授权`,当触发组件的`登录`功能时,将在站点页面上出现`登录授权弹窗` - -授权弹窗 - -* 登录授权页面:用户在当前浏览器下`未`进行`百度账号登录`,且未在第三方站点进行`百度账号授权`,当触发组件的`登录`功能时,将跳转打开`登录授权页` - -授权登录 - -* 无样式: - * 已登录第三方站点 - * 用户曾经使用百度账号授权登录过第三方站点,后来登出了,但未登出百度账号,且授权关系未失效,这时触发组件的`登录`,将直接返回授权`code`,没有样式 - - - -### 组件流程 - -#### 流程简易说明 - -授权组件简易流程 - -#### 具体场景流程说明 - - -* 当用户`打开或刷新`第三方页面时,执行`同步登录` - -同步登录 - -* 当用户在第三方页面`点击按钮、链接`等交互元素触发登录行为时,执行`异步登录` - -异步登录 - -* 当开启了组件的自动登录(config属性的`autologin`为`true`),若访问第三方站点页面的用户未登录,则将执行自动登录逻辑 - -自动登录 - - -**注意:** `同步登录` 和 `异步登录` 是针对发生`向后端请求用户数据`这一操作的`时机`而言 - - -#### 渲染和触发事件逻辑 - -- 页面加载完成 - 因未登录,空的用户数据(`{}`)渲染页面 -- 页面请求用户信息 - - 有 `code` - 发送登录数据 - + 错误 - 触发 `error` 事件 - + 成功 - - 使用 `response.data` 重新渲染页面 - - 触发 `login` 事件 - - 无 `code` - - 未登录 - 忽略 - - 已登录 - - 使用 `response.data` 重新渲染页面 - - 触发 `login` 事件 -- 页面触发 `登录组件ID.login` 事件 - + 未登录 - 跳转登录授权页面 - + 已登录 - 忽略 -- 页面触发 `登录组件ID.logout` 事件 - - 未登录 - 忽略 - - 已登录 - - 后端返回 `response.data.url` - * 跳转到 `response.data.url` - - 后端没有返回 `response.data.url` - - 触发 `logout` 事件 - - 使用空数据(`{}`)渲染模板 - - -### 组件属性 - -#### config - -说明:组件初始化所必须的配置数据 - -必选项:是 - -类型: `Object` - -示例:通过`m-bind:config="配置数据"`将该属性传入组件,配置数据的示例说明如下, - -```json -{ - "appid": "12345678", // 熊掌号id,string, 必须 - "clientId": "R6HzvBSGAvkFMUrhELUZayfH2No86t1k", // 熊掌号开发者client_id, string,必须 - "id": "info", // 数据的键名(key),当登录信息发生变更时,将更新mip-data里以该值为键名(`key`)的对象数据 - "isGlobal": false, // 需要更新的mip-data里已id为键名的对象数据是否为 全局数据,默认值false - "autologin": false, // 页面打开后未登录状态下自动跳转登录,常用于必须登录状态下才可以访问的页面, boolean, 默认值false - "endpoint": "https://api.example.com/user/info.php", // 后端源站数据接口链接,需要使用 `https://` 或者 `//开头的源站地址,需要接口支持 HTTPS ,使用 POST 形式发送数据 , 必须 - "redirectUri": "https://example.com/xxx.html" // 登录成功后的重定向地址,不传默认跳回原页面 -} -``` - -其中,`endpoint`为第三方后端提供的数据接口,组件将通过该接口,查询登录状态、获取用户信息等。 - -### 组件方法和事件 - -#### 登录方法 - `
` - -在其他元素中绑定点击或其他动作时调起登录。 - -该方法接收两个参数: - -* `redirectUri`: string,非必须,登录成功后的跳转地址,要求是站内页面。当页面运行在不同环境时(搜索或者原站),跳转链接的形式会不一样,需要开发者自己处理。 -* `origin`: string,非必须,执行`登录`时,标识发起登录操作的动作来源,在接受到登录成功的事件里,需要判断触发登录操作的是不是指定动作来源。当同一个页面存在多个触发登录的元素时,设置origin是必要的,因为每个元素在登录后的业务逻辑很可能是不一样的。 - - -调用组件方法时的正确传递参数的方式是`
`。所有参数都会转成`字符串`传入组件的方法。 - - -注意: - -1. 该方法会根据当前用户`登录百度账号`的状态而打开登录弹层(已登录)或者 重新打开一个登录页面(未登录),在非搜索环境下,如果是打开登录页, 意味着这将导致当前页面未存储的数据丢失,如:表单用户填写内容。 -2. 在已经登录成功的情况下,再次触发login方法,该方法不会执行。 - - -#### 登出方法 - `
` - -在其他元素中绑定点击或其他动作时请求登出接口。 - -注意:该方法不会跳转页面,异步的调用 `endpoint` 接口去退出,并触发登录组件元素中的 `logout:其他组件id.其他组件行为` 事件。 - -#### 登录成功事件 - `` - -登录事件包含`同步登录成功`和`异步登录成功`两种状态: - -* 同步登录成功: 打开页面或者刷新页面时组件能够获取到用户数据 -* 异步登录成功: 由某个交互(如点击按钮)触发的登录操作,登录后当前页面不刷新 - -可以在登录成功时调用其他组件的组件行为。 - -#### 登录失败事件 - `` - -在登录请求后端返回值错误时触发。 - -#### 登出成功事件 - `` - -在退出登录时(由 `on="tap:组件id.logout"` 调用触发)调用其他组件的组件行为。 - -#### 取消自动登录事件 - `` - -在`自动登录`时若用户取消登录(指,在登录页点击mip-shelll上的`返回`按钮 回到触发登录的页面, 或者 是点击授权弹窗上的`取消`按钮)时触发。 - - -### 后端数据说明 - - -以下为后端需要处理的和用户登录操作相关的几个场景下的请求。 - -#### 页面打开时检查用户数据 - -执行时机: 每次打开页面时,如果url上没有和授权相关的查询参数(code,redirect_uri),都会发送`type=check`的请求,向后端查询当前用户的登录状态和数据。 - -请求: - -| 名称 | 说明 | -| ---- | ------------------------------------ | -| 请求链接 | `config.endpoint`,必须支持`https` | -| 请求类型 | POST | -| 请求参数 | `{type: 'check', sessionId: '会话凭证'}` | - -未登录返回值说明: - -```json -{ - "status": 0, - "sessionId": "会话凭证,必须返回", - "data": null -} -``` - -已登录返回值,整个返回值的 `data` 字段将认为是用户数据,并使用MIP.setData到`config`属性配置的存放用户数据的对应字段里,如`info.userInfo`,在页面渲染时使用该数据渲染: - -```json -{ - "status": 0, - "sessionId": "会话凭证,必须返回", - "data": { - "nickname": "mipengine", - "key2": "value2" - } -} -``` - -注意:上面 `data.nickname` 只是示例,具体什么数据请前、后端统一约定。 - -#### 百度账号登录 - -执行时机: 1)在当前页面的url地址的查询参数上能正确获取到`code`和`redirect_uri` 2)异步登录接口返回了`code`和`redirect_uri`的数据。只要满足以上条件,都会发起`type=login`的请求。 - -请求: - -| 名称 | 说明 | -| ---- | ---------------------------------------- | -| 请求链接 | `config.endpoint`,必须支持`https` | -| 请求类型 | POST | -| 请求参数 | `{type: 'login', code: '熊掌号授权code', redirect_uri: '回调链接', sessionid: '会话凭证'}` | - -源站后端服务需要使用 `code` 和 `redirect_uri` 参数去请求 [获取网页授权 access_token](http://xiongzhang.baidu.com/open/wiki/chapter2/section2.2.html?t=1522129995153) 、[获取授权用户信息](http://xiongzhang.baidu.com/open/wiki/chapter2/section2.4.html?t=1522129995153) 接口,并和源站的用户关联、记录用户登录状态。 - -其中,`sessionid`为可选字段,如果能从本地缓存里取到该数据都会带上,后端应结合sessionid和code逻辑 做登录处理,具体请参看`常见问题-后端相关`部分 - -处理成功,认为已登录,整个返回值的 `data` 字段将认为是用户数据,并使用MIP.setData到`config`属性配置的存放用户数据的对应字段里,如`info.userInfo`,在页面渲染时使用该数据渲染: - -```json -{ - "status": 0, - "sessionId": "会话凭证,必须返回", - "data": { - "nickname": "mipengine", - "key2": "value2" - } -} -``` - -如果 `status` 不为 `0` 触发 `error` 事件,如: -```json -{ - "status": 403 -} -``` - -#### 退出 - -请求: - -| 名称 | 说明 | -| ---- | ----------------------------- | -| 请求链接 | `config.endpoint`,必须支持`https` | -| 请求类型 | POST | -| 请求参数 | `{type: 'logout'}` | - -返回值说明: - -```json -{ - "status": 0, - "data": { - "url": "https://www.example.com 退出成功跳转的链接地址 可选", - "title": "主页 自定义标题 可选" - } -} -``` - - - - -#### 其他 - -##### 会话凭证 sessionId - -由于在 iOS 对跨域透传 `cooke` 的限制(),在前端组件请求后端接口时(`type=check` 和 `type=login`),由后端生成当前会话唯一凭证并记录到服务端,把凭证返回前端 `response.sessionId`,前端组件将在 `localStorage` 中缓存下来,在下次发后端接口请求时携带该凭证,后端就当优先使用 `cookie/session` 验证,不存在时获取 `POST` 参数中的 `sessionId` 去校验。 - -注意:本地 `localStorage` 是以 `config.endpoint` 为粒度去缓存。 - - -##### 后端需要支持 `CORS` + `withCredentials` - -- [CORS 文档](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS) -- [`withCredentials` 附带身份凭证的请求](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#%E9%99%84%E5%B8%A6%E8%BA%AB%E4%BB%BD%E5%87%AD%E8%AF%81%E7%9A%84%E8%AF%B7%E6%B1%82) - -登录组件(mip-inservice-login)已经在前端发送请求时处理了 `withCredentials` ,需要对应的接口服务响应头设置: - -- `Access-Control-Allow-Credentials: true` -- `Access-Control-Allow-Origin: 对应请求的 origin` - -**注意:** 出于安全考虑请对来源的 `origin` 进行判断,并正确的返回 `Access-Control-Allow-Origin` 字段,不能为 `*` 。 - - -### 使用组件示例 - -#### 基本用法 - -* 在`html`中使用登录组件 - -```html - - - - - - - - - - - - -``` - -`mip-my-example`组件代码示例: - -```html - - - - -``` - -* 在`vue组件`里使用登录组件 - -```html - - - - - - - - - - -``` - -`mip-my-example`组件代码示例: - -```html - - - - -``` - - -和`mip-bind`配合使用注意: - -1. 必须在`mip-inservice-login`组件的`config`属性里设置`id` 的值,该值与组件id的值可以不一样, 如示例中的`info`。 -2. 必须在 `` 配置数据中设置一个以`mip-inservice-login`的`config`属性里的`id` 为键名(`key`)的对象数据, 如示例中的`info`。 -3. 如果这个数据是全局共享数据,需要设置`mip-inservice-login`的`config`里的`isGlobal`为`true`。共享数据的用法,请参照文档[MIP 2.0 的数据和应用](https://github.com/mipengine/mip2/blob/master/docs/components/data-and-method.md)。 -4. 在请求登录(`type=login`)、检查是否登录(`type=check`)、退出(`type=logout`)成功时,会调用 `MIP.setData` 设置数据,数据结构为: - -```json -{ - "id": { - "isLogin": Boolean, - "userInfo": Response.data, - "sessionId": String - } -} -``` - - -#### 实现个人中心 - -个人中心需要自动登录的功能 - - -```html - - - - - - -
    -
  • - hi,,你上次登录地点为 。 -
  • -
  • - 订单中心 -
  • -
  • -
    退出
    -
  • -
- -``` - -说明: - -1. 将`mip-inservice-login`组件的`config`属性里,`autologin`设置为`true`。 - - - -#### 综合示例 - -定制登录成功后的行为 - -* 点击`用户名`,触发登录,登录后回到原页面 -* 点击`我的订单`,触发登录,登录后跳转到订单页面`order.html` -* 点击`确认下单`,触发登录,登录后执行自定义业务逻辑 - -代码如下: - -* html - -```html - - - - - - - - - - -``` - - -* `mip-example-container`代码 - -```html - - - - - -``` - - - -* `mip-example`代码 - -```html - - - - - - -``` - - -**注意:** - -1. 可以在调用登录组件的`login`方法时,直接传入重定向地址。本示例通过设置登录组件配置的方式,来修改重定向地址。 -2. 在`已登录`的情况下,业务方需要自己处理逻辑,登录组件不会再回调或者跳转到指定地址。 -3. 在`nextTick`里抛出自定义组件事件,是因为修改配置后,会触发一次dom渲染,但`登录组件`持有的数据没有更新,所以需要在dom渲染后的下个时间点再执行操作。 - - -## 常见问题 - -### 后端相关 - -1. 使用登录组件需要后端介入吗? - -答: 需要 - -2. 后端需要完成哪些工作? - -答: 后端需要 1)接入熊掌号`网页授权`,能够获取到百度用户的基本信息 ;2)提供一个供登录组件使用的、支持`CORS` + `withCredentials`的接口; 3)对用户在第三方站点的登录信息进行管理(用户名、登录状态)。 - -3. 在哪配置网页授权回调地址?支持配置多个回调地址吗? - -答: 熊掌号运营中心-开发-开发者设置-网页授权域名([地址](https://xiongzhang.baidu.com/site/devsetting))。支持。 - -4. endpoint的作用是什么?是必须的吗? - -答:endpoint是后端提供给组件的一个获取用户信息、登录状态的接口,是`必须`的。 - - -5. 用户数据来自哪里? - -答:当访客在第三方站点进行`百度账号登录授权`后,组件将使用授权code和redirect_uri,向第三方后端接口`endpoint`发起登录请求,后端使用code换取网页授权`access_token`,再使用`access_token`获取百度用户信息 - -6. 用户数据存在哪? - -答:用户数据存在第三方数据库,组件只负责向后端发起请求,获取用户数据和登录状态,组件本身不存储任何用户数据。 - -7. sessionid的作用是什么,为什么需要它? - -答:由于在 iOS 对跨域透传 `cooke` 的限制(),在组件请求后端接口时(`type=check` 和 `type=login`),由后端生成当前会话唯一凭证并记录到服务端,把凭证返回前端 `response.sessionId`,前端组件将在 `localStorage` 中缓存下来,在下次发后端接口请求时携带该凭证,后端就当优先使用 `cookie/session` 验证,不存在时获取 `POST` 参数中的 `sessionId` 去校验。 - -8. 为什么发起`type=login`的请求时,还携带有`sessionid`? - -答:发起`type=login`的条件是1)在当前页面的url地址的查询参数上能正确获取到`code`和`redirect_uri` 2)异步登录接口返回了`code`和`redirect_uri`的数据。只要满足以上条件,都会发起`type=login`的请求。所以,当用户从百度登录授权页返回到第三方页面后,将执行`type=login`,后端成功返回用户信息后,sessionid有值,此时刷新当前页面(因为url上依然有code信息),又会再次发起`type=login`,并携带`sessionid`。 - -所以,type=login时后端处理的逻辑应该是: - -``` -sessionid是否存在, - + 如果存在,sessionid是否已过期, - * 如果没过期,直接返回用户数据, - * 如果过期,使用code+redirect_uri发起换取access_token的操作,成功获取百度用户信息后,更新sessionid,并将最新数据返回给组件 - + 如果不存在,使用code+redirect_uri发起换取access_token的操作,成功获取百度用户信息后,将最新数据返回给组件 -``` - -9. 为什么`type=login`时,也有`code+redirect_uri`值,但还是返回`invalid redirecturi`? - -答:发起授权时生成`code`的重定向地址,与后端现在换取`accesstoken`使用的`redirect_uri`不一致 - - -10. 回调地址里有特殊字符怎么办? - -答:组件传给后端的`redirect_uri`,使用的是原始值(例如,在组件配置参数里设置的重定向地址),组件不会再做encode处理,如果含有特殊字符(中文、& 等),需要后端自己完成encode,再去换取access_token - - -### 前端相关 - -1. 前端怎么获取到用户数据? - -答: 1) 监听组件抛出的login事件,该事件会返回用户数据 2) 通过`mip-data`里设置的存储用户数据的字段获取,如示例中的`info.userInfo.nickname`和`info.isLogin`。 - -2. 打开页面就能获取到用户数据吗? - -答: 不能,组件每次都是异步向后端查询用户数据,所以页面本身应该做`有数据`和`没数据`时的显示效果兼容,已达到更好的用户体验。 - -3. 只有设置为自动登录才能获取到用户数据吗? - -答: 不是,组件每次初始化都会像后端查询用户数据,如果后端成功返回了登录态和数据,都会抛出`login`事件,并更新`mip-data -`里对应的用户数据。当设置为自动登录,但后端返回为`未登录`状态时,组件就会主动发起登录百度账号的操作。 - -4. 一个页面能够有多个登录组件吗? - -答:不可以,只允许一个登录组件。 - -5. 一个页面内允许多个操作触发登录吗? - -答:可以。 - -6. 能够指定登录之后的跳转地址吗? - -答:可以,1)可以在调起登录`login()`方法的时候传入`redirectUri`; 2)修改`config`配置里的`redirectUri`。 - -7. 能够指定登录之后除了跳转外的其他行为吗? - -答:可以,如果成功登录之后,会抛出`login`事件,前端可以监听该事件,然后执行自己的逻辑(需要设置`origin`参数,具体参加文档)。 - -8. 为什么控制台显示`invalid redirecturi`? - -答:当前配置的`授权回调`地址不合法,请核对当前的域名或者跳转地址是否已在`网页授权回调地址`的配置里。 - - -9. 能够定制登录之后的操作行为吗? - -答:可以,如果成功登录之后,会抛出`login`事件,前端可以监听该事件,然后执行自己的逻辑。 - - -10. 为什么发起登录操作时,没有任何显示效果,就跳转页面并且登录成功了呢? - -答:因为该用户曾经在第三方网站使用百度账号登录成功过,之后退出了第三方站点(比如sessiond被清除),但没有退出百度账号,授权关系也依然有效,则不需要用户再次操作的情况下,组件就能够获取到授权`code`,然后携带code信息跳转到 前端设置的`登录后的跳转地址`。 - - -11. 为什么已经成功登录的情况下(userInfo里有用户数据),再次刷新页面,有时有用户数据,有时候没有? - -答:组件只有进入视窗内才会被初始化,`强烈建议`把登录组件尽量靠近``标签放置。 - - -### 其他 - -1. 如何取消授权? - -答:访问该[地址](https://passport.baidu.com/accountbind?tpl=),解除应用绑定关系。 - - -2. 能够只登录不授权吗? - -答:暂时不支持该功能。 - -3. 支持在mip1页面下使用吗? - -答:不支持。 - -4. 关注之后在哪能看到? - -答: 百度app/简搜app -> 关注中心 - - -## changelog - -### 2018-08-23 - -增加熊掌号关注功能 - -* 授权弹窗 - -授权弹窗 - -* 登录授权页 - -授权登录 diff --git a/components/mip-inservice-login/mip-inservice-login.vue b/components/mip-inservice-login/mip-inservice-login.vue deleted file mode 100644 index e22357b5..00000000 --- a/components/mip-inservice-login/mip-inservice-login.vue +++ /dev/null @@ -1,493 +0,0 @@ - - - diff --git a/components/mip-inservice-login/util.js b/components/mip-inservice-login/util.js deleted file mode 100644 index d95967c2..00000000 --- a/components/mip-inservice-login/util.js +++ /dev/null @@ -1,266 +0,0 @@ -/** - * @file 常用方法 - * @author huangjing - */ - -/* globals location, fetch, localStorage */ - -const util = { - - loadJS (src, success = () => {}, fail = () => {}) { - let ref = document.getElementsByTagName('script')[0] - let script = document.createElement('script') - // src 为 百度pass账号服务 - script.src = src - script.async = true - ref.parentNode.insertBefore(script, ref) - script.onload = function () { - script = null - success() - } - script.onerror = function (e) { - script = null - fail() - } - }, - - /** - * 处理字符串query - * - * @type {Object} - */ - querystring: { - - /** - * 解析对象为 string - * - * @param {Object} data 一级对象数据 - * @returns {string} 结果 - */ - stringify (data) { - return Object.keys(data).map(function (key) { - return encodeURIComponent(key) + '=' + encodeURIComponent(data[key] || '') - }).join('&') - } - }, - - /** - * 获取当前原始页面链接 - * - * @description 会做如下处理: - * 1. 删除 hash 后面的字符,因为透传有问题 - * 2. 删除 code state 参数,防止多次重定向链接越来越长 - * @param {string=} url url地址 - * @returns {string} 结果 - */ - getSourceUrl (url) { - !url && (url = location.href) - - // 修复 MIP-Cache 环境识别,因为核心代码里只识别了 // 或 https:// - // https://github.com/mipengine/mip/blob/master/src/util.js#L58 - if (url.indexOf('.com/c/s/') > -1 && url.indexOf('http://') === 0) { - url = url.replace(/^http:/, '') - } - - return window.MIP.util.parseCacheUrl(url) - .replace(/#.*$/, '') - .replace(/([&?])((code|state)=[^&$]+)/g, function (matched, prefix) { - return prefix === '?' ? '?' : '' - }) - }, - - getSourceFormatUrl (url) { - return (location.protocol + '//' + location.host + location.pathname + location.search) - .replace(/([&?])((code|state)=[^&$]+)/g, function (matched, prefix) { - return prefix === '?' ? '?' : '' - }) - }, - - getRedirectUrl (url, query, hash) { - let result = url + (url.indexOf('?') >= 0 ? '&' : '?') + - 'code=' + query.code + '&state=' + query.state + hash - - return result - }, - - getFormatUrl (url) { - let a = document.createElement('a') - a.href = url - - let {protocol, host, pathname, search, hash} = a - - a = null - - return { - url: protocol + '//' + host + pathname + search, - hash: hash - } - }, - - getDomain (url) { - let str = url.replace(/^https?:\/\//, '').split('/') - str = str[0].replace(/:.*$/, '') - - let ext = [ - '.com', '.co', '.cn', '.info', '.net', '.org', '.me', - '.mobi', '.us', '.biz', '.xxx', '.ca', '.co.jp', - '.com.cn', '.net.cn', '.org.cn', '.gov.cn', '.mx', - '.tv', '.ws', '.ag', '.com.ag', '.net.ag', '.org.ag', - '.am', '.asia', '.at', '.be', '.com.br', '.net.br', - '.bz', '.com.bz', '.net.bz', '.cc', '.com.co', '.net.co', - '.nom.co', '.de', '.es', '.com.es', '.nom.es', '.org.es', - '.eu', '.fm', '.fr', '.gs', '.in', '.co.in', '.firm.in', - '.gen.in', '.ind.in', '.net.in', '.org.in', '.it', '.jobs', - '.jp', '.ms', '.com.mx', '.nl', '.nu', '.co.nz', '.net.nz', - '.org.nz', '.se', '.tc', '.tk', '.tw', '.com.tw', '.com.hk', - '.idv.tw', '.org.tw', '.hk', '.co.uk', '.me.uk', '.org.uk', '.vg', '.name' - ] - let res = '' - ext = ext.join('|').replace('.', '\\.') - let exps = new RegExp('\\.?([^.]+(' + ext + '))$') - - str.replace(exps, function ($0, $1) { - res = $1 - }) - - return res - }, - - /** - * 获取链接中的 query - * - * @param {string} name 参数名称 - * @returns {string} 结果 - */ - getQuery (name) { - let url = location.search.substr(1) - let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i') - let matched = url.match(reg) - - return matched ? decodeURIComponent(matched[2]) : '' - }, - - log (param) { - /* eslint-disable fecs-camelcase */ - let img = document.createElement('img') - let {action, xzhid, ext = {}} = param - - let data = { - rqt: 300, - click_token: window.MIP.util.customStorage(0).get('mip-click-token') || '', - url: location.href, - ext: JSON.stringify(ext), - action, - xzhid - } - - let queryArr = Object.keys(data).map(key => { - return `${key}=${encodeURIComponent(data[key])}` - }) - - img.src = 'https://rqs.baidu.com/service/api/rqs?' + queryArr.join('&') + '&_t=' + (new Date()).getTime() - /* eslint-enable fecs-camelcase */ - }, - - /** - * 小小的封装下 ls - * - * @type {Object} - */ - store: { - /** - * 存储 key 前缀 - * - * @type {string} - */ - prefix: 'mip-login-xzh:sessionId:', - - /** - * 获取 key - * - * @param {string} key 键值 - * - * @returns {string} 结果 - */ - getKey (key) { - return util.store.prefix + key - }, - - /** - * 检查是否支持 ls - * - * @type {boolean} - */ - support: (function () { - let support = true - try { - window.localStorage.setItem('lsExisted', '1') - window.localStorage.removeItem('lsExisted') - } catch (e) { - support = false - } - return support - })(), - - /** - * 获取缓存数据 - * - * @param {string} key 数据名称 - * @returns {string} 结果 - */ - get (key) { - if (util.store.support) { - return localStorage.getItem(util.store.getKey(key)) - } - }, - - /** - * 设置缓存数据 - * - * @param {string} key 数据名称 - * @param {string} value 数据值 - * @param {number} expires 过期时间UTC - */ - set (key, value, expires) { - if (util.store.support) { - localStorage.setItem(util.store.getKey(key), value) - } - }, - - /** - * 删除缓存数据 - * - * @param {string} key 数据名称 - */ - remove (key) { - if (util.store.support) { - return localStorage.removeItem(util.store.getKey(key)) - } - } - }, - - /** - * 发送 POST 请求 - * - * @param {string} url 接口链接 - * @param {Object} data 发送数据 - * - */ - post (url, data) { - return fetch(url, { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: util.querystring.stringify(Object.assign({}, data || {}, { - sessionId: util.store.get(url) - })), - credentials: 'include' - }).then(function (res) { - return res.json() - }) - } - -} - -export default util diff --git a/components/mip-inservice-pay/example/mip-inservice-pay.html b/components/mip-inservice-pay/example/mip-inservice-pay.html deleted file mode 100644 index 7712441c..00000000 --- a/components/mip-inservice-pay/example/mip-inservice-pay.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - MIP page - - - - - - - - - -
- - -
- - - - diff --git a/components/mip-inservice-pay/mip-inservice-pay.md b/components/mip-inservice-pay/mip-inservice-pay.md deleted file mode 100644 index ecf720c1..00000000 --- a/components/mip-inservice-pay/mip-inservice-pay.md +++ /dev/null @@ -1,174 +0,0 @@ -# `mip-inservice-pay` 极速服务 支付组件 - -为mip站长 提供支付调起服务组件,支持百付宝、支付宝、微信 -![MIP 支付流程图](https://user-images.githubusercontent.com/7043799/41702452-c470f1f8-7562-11e8-82a1-b9accf41f3ff.png) - -标题|内容 -----|---- -类型|通用 -支持布局|N/S -所需脚本|https://c.mipcdn.com/static/v2/mip-inservice-pay/mip-inservice-pay.js - -## 说明 -需在传入属性`pay-config`数据配置 - -## 示例 - -```html - - - - - -``` - -## 属性 `pay-config` -### payConfig.subject -说明:订单名称 -必选项:是 -类型:`string` -示例:"蓝犀牛订单" - -### payConfig.fee -说明:订单金额 -必选项:是 -类型:`number` -示例:300元 - - -### payConfig.sessionId -说明:会话凭证, 请求支付接口时传入后台进行校验 -必选项:是 -类型:`string` -示例:300元 - -### payConfig.redirectUrl -说明:微信内支付成功后跳转链接 -必选项:是 -类型:`string` -示例:"http://www.baidu.com/" - -### payConfig.endpoint {#endpoint} -说明:后端源站支付接口链接,需要使用 `https://` 或者 `//` 开头的源站地址,需要接口支持 HTTPS ,使用 POST 形式发送数据 -必选项:是 -类型:`object` -示例:{"baifubao":"https://api.example.com/pay.php", alipay:xxx, weixin:xxx} -说明:[后端跨域说明](#cors) 、[后端数据说明](#data) 、[会话凭证 sessionId](#sessionId) - - - -## 注意事项 - -### 1.怎样 动态配置更改`payConfig` -因数据配置在`mip-data`中以及数据通过 `props`传递给 支付组件,故可通过 `MIP.setData`动态设置`postData`、`sessionId`等数据 - - -### 2. 后端需要支持 CORS + `withCredentials` - -- [CORS 文档](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS) -- [`withCredentials` 附带身份凭证的请求](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#%E9%99%84%E5%B8%A6%E8%BA%AB%E4%BB%BD%E5%87%AD%E8%AF%81%E7%9A%84%E8%AF%B7%E6%B1%82) - -支付组件(mip-simple-pay)已经在前端发送请求时处理了 `withCredentials` ,需要对应的接口服务响应头设置: - -- `Access-Control-Allow-Credentials: true` -- `Access-Control-Allow-Origin: 对应请求的 origin` - -注意:出于安全考虑请对来源的 `origin` 进行判断,并正确的返回 `Access-Control-Allow-Origin` 字段,不能为 `*` 。 - - -### 3. 后端数据说明 -请求: - -名称 | 说明 ---- | --- -请求链接 | [payConfig.endpoint](#endpoint) -请求类型 | POST -请求参数 | `{sessionId: '会话凭证', state: '需要在支付完成后回传给 MIP oob 回调链接中', ...}` - -异常情况,`status` 非 `0` 时为失败: -```json -{ - "status": 403, - "msg":"支付错误信息" -} -``` - -成功: - -** 支付类型为 `nomal|alipay` 时** - -```json -{ - "status": 0, - "data": { - "url": "https://付款链接" - } -} -``` - -** 支付类型为 `weixin`时** - -- 微信外环境 -```json -{ - "status": 0, - "data": { - "url": "https://付款链接" - } -} -``` - -- 微信内环境 - -[微信内判断方法](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_5) -```javascript -{ - "status": 0, - "data": { - "appId": "wx3dxxxxxxxx", - "timeStamp": "1527508907", - "nonceStr": "ASDFWSACSDCDSGA", - "package": "prepay_id=wx3dxxxxxxxx", - "signType": "MD5", - "paySign": "SADF98S0A9D00A9S09A0SDCASD", - "timestamp": "1527508907" - } -} -``` - -注意:付款成功后回调链接应该为源站后端订单处理链接,如:`https://支付链接?callback=urlencode('https://api.mipengine.org/order?id=1')` ,回调链接(`https://api.mipengine.org/order?id=1`)在支付完成后处理完成订单数据后重定向到 `统一支付成功页面` - -格式如: -``` -https://xiongzhang.baidu.com/opensc/wps/payment?id=熊掌号ID&redirect=显示支付完成页面,必须是MIP页面 -``` - - - -### 4. 会话凭证 sessionId - -由于在 iOS 对跨域透传 `cooke` 的限制(),由登录组件统一记录会话标识,并透传给支付组件,在发送支付请求时携带,后端应该优先使用 `cookie > sessionId` 校验登录状态。 - -### 4. 百度搜索结果页降级处理 - -在百度搜索页打开使用该组件页面时,由于有些支付密码输入框在 `iframe` 框架下有问题,在调用[提交支付接口](#action-pay)时做了降级处理,处理方式为跳转源站。包括以下设备、浏览器: -- iOS设备下的手百App diff --git a/components/mip-inservice-pay/mip-inservice-pay.vue b/components/mip-inservice-pay/mip-inservice-pay.vue deleted file mode 100644 index 4247a8c5..00000000 --- a/components/mip-inservice-pay/mip-inservice-pay.vue +++ /dev/null @@ -1,861 +0,0 @@ - - - - - diff --git a/components/mip-inservice-pay/util.js b/components/mip-inservice-pay/util.js deleted file mode 100644 index d940111c..00000000 --- a/components/mip-inservice-pay/util.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @file 常用方法 - * @author zhuguoxi - */ -const encodeReserveRE = /[!'()*]/g -const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16) -const commaRE = /%2C/g - -// fixed encodeURIComponent which is more conformant to RFC3986: -// - escapes [!'()*] -// - preserve commas -const encode = str => encodeURIComponent(str) - .replace(encodeReserveRE, encodeReserveReplacer) - .replace(commaRE, ',') - -const decode = decodeURIComponent - -const util = { - - query: { - stringify (obj) { - let res = obj ? Object.keys(obj).map(key => { - let val = obj[key] - if (val === undefined) { - return '' - } - - if (val === null) { - return encode(key) - } - - if (Array.isArray(val)) { - let result = [] - val.forEach(val2 => { - if (val2 === undefined) { - return - } - - if (val2 === null) { - result.push(encode(key)) - } else { - result.push(encode(key) + '=' + encode(val2)) - } - }) - return result.join('&') - } - - return encode(key) + '=' + encode(val) - }).filter(x => x.length > 0).join('&') : null - return res ? `${res}` : '' - }, - resolveQuery (query) { - let parsedQuery - try { - parsedQuery = this.parseQuery(query || '') - } catch (e) { - parsedQuery = {} - } - return parsedQuery - }, - parse (query) { - const res = {} - - query = query.trim().replace(/^(\?|#|&)/, '') - - if (!query) { - return res - } - - query.split('&').forEach(param => { - const parts = param.replace(/\+/g, ' ').split('=') - const key = decode(parts.shift()) - const val = parts.length > 0 - ? decode(parts.join('=')) - : null - - if (res[key] === undefined) { - res[key] = val - } else if (Array.isArray(res[key])) { - res[key].push(val) - } else { - res[key] = [res[key], val] - } - }) - - return res - } - }, - getFormatUrl (url) { - let a = document.createElement('a') - a.href = url - - let {protocol, host, pathname, search, hash} = a - - a = null - - return { - url: protocol + '//' + host + pathname + search, - hash: hash - } - } -} - -export default util diff --git a/components/mip-mathml/example/mip-mathml.html b/components/mip-mathml/example/mip-mathml.html index b9a2c4b6..8708d010 100644 --- a/components/mip-mathml/example/mip-mathml.html +++ b/components/mip-mathml/example/mip-mathml.html @@ -11,13 +11,13 @@

二次公式

- +

柯西积分公式

- +

余弦的双角公式

- +

内敛公式

- 这个例子让这个公式内敛在文本当中 这个需要额外的 css 去设置 + 这个例子让这个公式内联在文本当中 这个需要额外的 css 去设置 diff --git a/components/mip-mathml/mip-mathml.js b/components/mip-mathml/mip-mathml.js new file mode 100644 index 00000000..f775fbd8 --- /dev/null +++ b/components/mip-mathml/mip-mathml.js @@ -0,0 +1,102 @@ +/** + * @file mathml 组件 + * @author mj(zoumiaojiang@gmail.com) + */ + +/* globals MIP */ + +import './mip-mathml.less' + +const { CustomElement } = MIP + +export default class MIPMathml extends CustomElement { + /** + * 指定在渲染 layout 的过程中是否出现 loading,默认 true + * + * @returns {boolean} 是否展示 loading + */ + isLoadingEnabled () { + return true + } + + async layoutCallback () { + await this.initialize() + } + + /** + * 初始化 + */ + initialize () { + let ele = this.element + let inline = ele.hasAttribute('inline') + let wrapper = document.createElement('div') + let iframe = document.createElement('iframe') + let iframeID = this.iframeID = `${Date.now()}_${Math.ceil(Math.random() * 100000)}` + + if (inline) { + // 如果是内联,要设置父级原生为 inline-block + ele.style.display = 'inline-block' + ele.style.verticalAlign = 'middle' + } + + wrapper.className = 'wrapper' + iframe.setAttribute('frameBorder', 0) + iframe.setAttribute('scrolling', 'no') + iframe.setAttribute('srcdoc', this.getIframeBody()) + wrapper.appendChild(iframe) + + this.applyFillContent(wrapper, true) + this.element.appendChild(wrapper) + + return new Promise((resolve, reject) => { + function messageHandler (event) { + let inline = ele.hasAttribute('inline') + + if (event.origin === window.location.origin && + event.data && + iframeID === event.data.iframeID + ) { + let { width, height } = event.data + + iframe.style.height = `${height}px` + + if (inline) { + iframe.style.width = `${width}px` + } + + iframe.style.visibility = 'visible' + resolve(true) + } + } + + window.addEventListener('message', e => messageHandler(e)) + }) + } + + getIframeBody () { + let ele = this.element + let mathjaxConfig = ele.getAttribute('mathjaxConfig') || 'TeX-MML-AM_CHTML' + let formula = ele.getAttribute('formula') || '' + const MATHJAX_CDN = `https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=${mathjaxConfig}` + + return ` + +
${formula}
+ + ` + } +} diff --git a/components/mip-mathml/mip-mathml.less b/components/mip-mathml/mip-mathml.less new file mode 100644 index 00000000..cb8c056a --- /dev/null +++ b/components/mip-mathml/mip-mathml.less @@ -0,0 +1,17 @@ +mip-mathml { + .wrapper { + margin: 0 auto; + text-align: center; + } + + iframe { + display: block; + visibility: hidden; + width: 0; + height: 0; + max-height: 100%; + min-height: 100%; + max-width: 100%; + min-width: 100%; + } +} diff --git a/components/mip-mathml/mip-mathml.vue b/components/mip-mathml/mip-mathml.vue deleted file mode 100644 index 382e0756..00000000 --- a/components/mip-mathml/mip-mathml.vue +++ /dev/null @@ -1,109 +0,0 @@ -