Skip to content

Commit 99c52e7

Browse files
committed
feat: idframe [wip]
1 parent 26a91da commit 99c52e7

File tree

9 files changed

+295
-40
lines changed

9 files changed

+295
-40
lines changed

components/set-property.vue

+3-21
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,14 @@ export default {
4141
data () {
4242
return { submitting: false }
4343
},
44-
inject: [ 'snackbar' ],
44+
inject: [ 'snackbar', 'setIdframe' ],
4545
computed: {
4646
valid () {
4747
return this.validate ? this.validate() : true
4848
},
4949
},
50-
mounted () {
51-
const idFrameEl = document.createElement('span')
52-
window.idFrameEl = idFrameEl
53-
idFrameEl.style.cssText = 'top: 12px; right: 12px; position: fixed; --mdc-theme-primary: #f5fafd;'
54-
const scriptEl = document.createElement('script')
55-
scriptEl.src = 'https://idframe.keeer.net/js/appbar.js'
56-
document.head.appendChild(scriptEl)
57-
document.body.appendChild(idFrameEl)
58-
const intervalId = setInterval(function () {
59-
if ('idFrame' in window) {
60-
clearInterval(intervalId)
61-
// eslint-disable-next-line no-new, no-undef
62-
new idFrame.AppBarFrame({ container: idFrameEl, base: location.origin })
63-
}
64-
}, 100)
65-
},
66-
beforeDestroy () {
67-
document.body.removeChild(window.idFrameEl)
68-
delete window.idFrameEl
69-
},
50+
mounted () { this.setIdframe(true) },
51+
beforeDestroy () { this.setIdframe(false) },
7052
methods: {
7153
back () { this.$router.back() },
7254
async submit () {

idframe/appbar.js

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
{
2+
const data = __data__ // eslint-disable-line no-undef
3+
const defaultItems = [ 'nickname', 'myaccount', 'divider', 'kredit', 'recharge', 'divider', 'logout' ]
4+
const shortItems = {
5+
divider: () => ({ _isDivider: true, text: null }),
6+
nickname: () => ({ text: esc(data.nickname) }),
7+
myaccount: () => ({ text: '管理帐号信息', link: url('/') }),
8+
kredit: () => ({ text: 'Kredit 余额:' + data.kredit / 100 }),
9+
recharge: () => ({ text: '充值', link: url('/recharge') }),
10+
logout: () => ({ text: '退出登录', id: 'idframe-log-out', onclick: () => window.idFrame.logout() }),
11+
}
12+
13+
const url = path => data.base + path
14+
const esc = str => str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
15+
if (!('idFrame' in window)) window.idFrame = {}
16+
if (!('logout' in window.idFrame)) window.idFrame.logout = () => {
17+
const req = new XMLHttpRequest()
18+
req.onload = () => location.reload()
19+
req.onerror = () => alert('网络错误')
20+
req.open('DELETE', url('/api/token?set-cookie=true'))
21+
}
22+
23+
const waitMdc = new Promise((resolve, reject) => {
24+
const timeout = 300000
25+
setTimeout(reject, timeout)
26+
const intervalId = setInterval(() => {
27+
if (!('mdc' in window)) return
28+
if ('ripple' in window.mdc && 'menu' in window.mdc) return resolve()
29+
}, 200)
30+
setTimeout(() => clearInterval(intervalId), timeout)
31+
})
32+
33+
/**
34+
* Class representing a IDFrame object.
35+
* @constructor
36+
* @param {string|HTMLElement} config.container IDFrame container, string will be interpreted as selector
37+
* @param {string} [config.loginUrl] login button URL
38+
* @param {string} [config.signupUrl] signup button URL
39+
* @param {(string|object)[]} [config.items] items to show
40+
* @param {string} [config.serviceName] KAS service name to include in login and sign up URLs
41+
*/
42+
window.idFrame.AppBarFrame = class AppBarFrame {
43+
constructor (config) {
44+
this.config = config
45+
if (!(config.container instanceof HTMLElement)) {
46+
if (typeof config.container === 'string') config.container = document.querySelector(config.container)
47+
if (!(config.container instanceof HTMLElement)) throw new Error('Container not found.')
48+
}
49+
this.container = config.container
50+
if (!config.serviceName) {
51+
this.loginUrl = config.loginUrl || url('/login#login')
52+
this.signupUrl = config.signupUrl || url('/login#signup')
53+
} else {
54+
this.loginUrl = config.loginUrl || url(`/login?service=${config.serviceName}#login`)
55+
this.signupUrl = config.signupUrl || url(`/login?service=${config.serviceName}#signup`)
56+
}
57+
this.ready = this._init()
58+
}
59+
60+
/**
61+
* Initializes frame UI.
62+
* @private
63+
*/
64+
_init () {
65+
return waitMdc.then(() => {
66+
if (!data.loggedIn) {
67+
this.container.innerHTML = `
68+
<span class="idframe idframe--appbar idframe--not-logged-in">
69+
<a class="mdc-button" href="${this.loginUrl}"><div class="mdc-button__ripple"></div><span class="mdc-button__label">登录</span></a>&nbsp;
70+
<a class="mdc-button mdc-button--outlined" href="${this.signupUrl}"><div class="mdc-button__ripple"></div><span class="mdc-button__label">注册</span></a>
71+
</span>`
72+
const els = this.container.querySelectorAll('.mdc-button')
73+
for (let i = 0; i < els.length; i++) window.mdc.ripple.MDCRipple.attachTo(els[i])
74+
} else { // logged in
75+
this.container.innerHTML = `<span class="idframe idframe--appbar" role="button" title="KEEER 帐号">
76+
<span class="idframe--appbar__avatar" style="background-image: url('${data.avatar})"></span>
77+
<span class="idframe--appbar__nickname" dir="ltr">${esc(data.nickname)}</span>
78+
<span class="idframe--appbar__dropdown-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg></span>
79+
</span>`
80+
this.container.firstElementChild.onclick = () => this._menu.open = true
81+
this.updateItems(this.config.items || [].concat(defaultItems))
82+
}
83+
})
84+
}
85+
86+
/**
87+
* Gives layout of frame items.
88+
* @private
89+
*/
90+
_handleItems () {
91+
let html = '<ul class="mdc-list" role="menu" aria-hidden="true" aria-orientation="vertical" tabindex="-1">'
92+
const listeners = {}
93+
for (let i = 0; i < this.items.length; i++) {
94+
let item = this.items[i]
95+
if (typeof item === 'string') {
96+
if (!(item in shortItems)) throw new Error(`Item ${item} not found.`)
97+
item = shortItems[item](this)
98+
}
99+
if (!('text' in item)) throw new Error('Invalid item ' + item)
100+
if (item._isDivider) html += '<li role="separator" class="mdc-list-divider"></li>'
101+
else if ('link' in item) {
102+
html += `<a class="mdc-list-item" role="menuitem" href="${item.link}"><span class="mdc-list-item__text">${esc(item.text)}</span></a>`
103+
} else if ('onclick' in item) {
104+
if (!('id' in item)) throw new Error(`Item ${item} has an onclick listener without ID.`)
105+
const id = `${item.id}-${Math.floor(Math.random() * 100000)}`
106+
listeners[id] = item.onclick
107+
html += `<a id="${id}" class="mdc-list-item" role="menuitem" href="javascript:;"><span class="mdc-list-item__text">${esc(item.text)}</span></a>`
108+
} else {
109+
html += `<li class="mdc-list-item idframe-list-item--disabled" role="menuitem"><span class="mdc-list-item__text">${esc(item.text)}</span></li>`
110+
}
111+
}
112+
if (!this._menuEl) {
113+
const el = document.createElement('span')
114+
el.className = 'mdc-menu-surface--anchor'
115+
this._menuEl = document.createElement('div')
116+
this._menuEl.className = 'mdc-menu mdc-menu-surface'
117+
el.appendChild(this._menuEl)
118+
this.ready.then(() => this.container.firstElementChild.appendChild(el))
119+
}
120+
html += '</ul>'
121+
this._menuEl.innerHTML = html
122+
this._menuEl.querySelector('ul').addEventListener('click', function (e) { e.stopPropagation() })
123+
this._menu = new window.mdc.menu.MDCMenu(this._menuEl)
124+
const els = this._menu.list_.listElements
125+
for (let i = 0; i < els.length; i++) new window.mdc.ripple.MDCRipple(els[i]) // eslint-disable-line no-new
126+
setTimeout(() => {
127+
for (const id in listeners) document.getElementById(id).addEventListener('click', listeners[id])
128+
}, 100)
129+
}
130+
131+
/**
132+
* Updates menu items.
133+
* @param {(string|object)[]} items items to be displayed.
134+
*/
135+
updateItems (items) {
136+
if (!Array.isArray(items)) throw new TypeError('Items not an array')
137+
const oldItems = this.items
138+
this.items = items
139+
try {
140+
this._handleItems()
141+
} catch (e) {
142+
this.items = oldItems
143+
this._handleItems()
144+
throw e
145+
}
146+
}
147+
}
148+
149+
// initialize styles
150+
const styleEl = document.createElement('style')
151+
styleEl.innerHTML = `
152+
.idframe--appbar {
153+
cursor: pointer;
154+
user-select: none;
155+
display: flex;
156+
align-items: center;
157+
}
158+
.idframe--not-logged-in {
159+
margin-top: 2px;
160+
margin-right: 2px;
161+
}
162+
.idframe--appbar__nickname {
163+
font-size: 16px;
164+
margin: 0 4px 0 8px;
165+
color: var(--mdc-theme-primary, black);
166+
}
167+
@media(max-width: 599px) {
168+
.idframe--appbar__nickname {
169+
display: none;
170+
}
171+
}
172+
.idframe--appbar__avatar {
173+
width: 40px;
174+
height: 40px;
175+
background-size: contain;
176+
display: inline-block;
177+
border-radius: 4px;
178+
}
179+
.idframe--appbar .mdc-menu-surface--anchor {
180+
align-self: start;
181+
}
182+
.idframe--appbar .idframe-list-item--disabled .mdc-list-item__text {
183+
opacity: .5;
184+
}
185+
.idframe--appbar__dropdown-icon, .idframe--appbar__dropdown-icon svg {
186+
display: inline-block;
187+
width: 20px;
188+
height: 20px;
189+
opacity: .8;
190+
fill: var(--mdc-theme-primary, black);
191+
}`
192+
document.head.appendChild(styleEl)
193+
194+
// initialize MDC
195+
const used = {
196+
'mdc.button.': [ 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.button.min.css' ],
197+
'mdc.ripple.': [ 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.ripple.min.css', 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.ripple.min.js' ],
198+
'mdc.list.': [ 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.list.min.css' ],
199+
'mdc.menu-surface.': [ 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.menu-surface.min.css' ],
200+
'mdc.menu.': [ 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.menu.min.css', 'https://cdn.jsdelivr.net/npm/@material/[email protected]/dist/mdc.menu.min.js' ],
201+
}
202+
for (const i in used) {
203+
let has = !used[i][0]
204+
if (!('mdc' in window.idFrame) || !window.idFrame.mdc.style) {
205+
for (let j = 0; j < document.styleSheets.length; j++) {
206+
if ((document.styleSheets[j].href || '').includes(i)) has = true
207+
}
208+
if (!has) {
209+
const el = document.createElement('link')
210+
el.rel = 'stylesheet'
211+
el.href = used[i][0]
212+
document.head.appendChild(el)
213+
}
214+
}
215+
if (!('mdc' in window.idFrame) || !window.idFrame.mdc.script) {
216+
has = !used[i][1]
217+
for (let j = 0; j < document.scripts.length; j++) {
218+
if ((document.scripts[j].src || '').includes(i)) has = true
219+
}
220+
if (!has) {
221+
const el = document.createElement('script')
222+
el.src = used[i][1]
223+
document.head.appendChild(el)
224+
}
225+
}
226+
}
227+
}

idframe/serve.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { readFileSync } = require('fs')
2+
const path = require('path')
3+
const script = readFileSync(path.resolve(__dirname, 'appbar.js')).toString()
4+
5+
exports.applyIdframeRoutes = router => router.get('/api/idframe', ctx => {
6+
const info = ctx.getUserInformation()
7+
const data = info ? { ...info, loggedIn: true } : { loggedIn: false }
8+
const str = JSON.stringify(data).replace(/\//g, '\\u002F')
9+
ctx.set('Content-Type', 'application/javascript')
10+
ctx.body = script.replace('__data__', str)
11+
})

layouts/default.vue

+36-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
<template>
2-
<v-app>
3-
<!-- <v-app-bar v-if="hasAppBar" fixed app>
4-
<v-toolbar-title v-text="title" />
5-
</v-app-bar> -->
6-
<v-content>
7-
<nuxt />
8-
<v-snackbar v-model="snackbarModel" :timeout="3200">
9-
{{ snackbarText }}
10-
</v-snackbar>
11-
</v-content>
12-
</v-app>
2+
<div>
3+
<v-app>
4+
<v-content>
5+
<nuxt />
6+
<v-snackbar v-model="snackbarModel" :timeout="3200">
7+
{{ snackbarText }}
8+
</v-snackbar>
9+
</v-content>
10+
</v-app>
11+
<span v-show="showIdframe" id="idframe" ref="idframe" />
12+
</div>
1313
</template>
1414

1515
<script>
1616
export default {
1717
data () {
1818
return {
1919
title: 'KEEER Account Service',
20-
// hasAppBar: false,
20+
showIdframe: false,
2121
snackbarModel: false,
2222
snackbarText: '',
2323
snackbarQueue: [],
@@ -31,6 +31,8 @@ export default {
3131
this.snackbarText = text
3232
this.snackbarModel = true
3333
},
34+
setIdframe: val => this.showIdframe = val,
35+
reloadIdframe: () => this.reloadIdframe(),
3436
}
3537
},
3638
watch: {
@@ -43,6 +45,28 @@ export default {
4345
}
4446
},
4547
},
48+
mounted () { this.reloadIdframe() },
49+
methods: {
50+
reloadIdframe () {
51+
this.$refs.idframe.innerHTML = ''
52+
const scriptEl = document.createElement('script')
53+
scriptEl.onload = () => {
54+
// eslint-disable-next-line no-new
55+
new window.idFrame.AppBarFrame({ container: this.$refs.idframe })
56+
}
57+
scriptEl.src = '/api/idframe'
58+
document.head.appendChild(scriptEl)
59+
},
60+
},
4661
head () { return { title: 'KAS' } },
4762
}
4863
</script>
64+
65+
<style scoped>
66+
#idframe {
67+
top: 12px;
68+
right: 12px;
69+
position: fixed;
70+
--mdc-theme-primary: #f5fafd;
71+
}
72+
</style>

nuxt.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module.exports = {
4040
'default-src': [ '\'self\'', cdnOrigin, '\'report-sample\'' ],
4141
'img-src': [ '\'self\'', 'data:', jsdelivr, `https://*.${ALI_OSS_REGION}.aliyuncs.com`, 'https://keeer.net', 'https://*.keeer.net', 'https://www.google-analytics.com', 'https://payjs.cn' ],
4242
'script-src': [
43-
'\'self\'', cdnOrigin, jsdelivr, 'https://idframe.keeer.net', 'https://www.google-analytics.com', 'https://ssl.google-analytics.com', '\'report-sample\'',
43+
'\'self\'', cdnOrigin, jsdelivr, 'https://www.google-analytics.com', 'https://ssl.google-analytics.com', '\'report-sample\'',
4444
...(process.env.NODE_ENV === 'development' ? [ '\'unsafe-eval\'' ] : []),
4545
],
4646
'style-src': [ '\'self\'', jsdelivr, cdnOrigin, '\'unsafe-inline\'', '\'report-sample\'' ],

pages/login.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
import SmsVerify from '~/components/sms-verify'
8484
8585
export default {
86-
inject: [ 'snackbar' ],
86+
inject: [ 'snackbar', 'reloadIdframe' ],
8787
components: { SmsVerify },
8888
async asyncData ({ req }) {
8989
if (process.server) {
@@ -156,6 +156,7 @@ export default {
156156
},
157157
methods: {
158158
proceed () {
159+
this.reloadIdframe()
159160
if (this.useCustom && this.useCustom.redirectUrl) {
160161
const url = this.useCustom.redirectUrl
161162
if (/^https?:\/\//.test(url)) location = url

pages/sessions.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ export default {
4141
return { notLoggedIn: res.code === 'EUNAUTHORIZED', sessions: res.result || [] }
4242
}
4343
},
44-
inject: [ 'snackbar' ],
44+
inject: [ 'snackbar', 'setIdframe' ],
4545
created () {
4646
if (this.notLoggedIn) this.$router.push('/login')
4747
},
48+
mounted () { this.setIdframe(true) },
49+
beforeDestroy () { this.setIdframe(false) },
4850
methods: {
4951
timeString: time => new Date(time).toLocaleString(),
5052
async logout (session) {

pages/set-keeer-id.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<set-property
3-
title="设置您的KEEER ID"
3+
title="设置您的 KEEER ID"
44
subtitle="请慎重,您仅有 1 次设置机会"
55
put-path="/api/keeer-id"
66
track="keeer-id"

0 commit comments

Comments
 (0)