diff --git a/package-lock.json b/package-lock.json index fa6121b..df03e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3423,6 +3423,14 @@ } } }, + "async-validator": { + "version": "1.8.5", + "resolved": "https://registry.npmmirror.com/async-validator/download/async-validator-1.8.5.tgz", + "integrity": "sha1-3D4I7B/Q3dtn5ghC8CwM0c7G1/A=", + "requires": { + "babel-runtime": "6.x" + } + }, "asynckit": { "version": "0.4.0", "resolved": "http://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz", @@ -3479,6 +3487,14 @@ "resolved": "https://registry.npm.taobao.org/aws4/download/aws4-1.11.0.tgz?cache=0&sync_timestamp=1604101385256&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Faws4%2Fdownload%2Faws4-1.11.0.tgz", "integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=" }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmmirror.com/axios/download/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "http://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz", @@ -3538,6 +3554,11 @@ } } }, + "babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/download/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha1-Iq69OzOQIyjlEyk6jkmSs4T58bY=" + }, "babel-loader": { "version": "8.2.3", "resolved": "https://registry.npmmirror.com/babel-loader/download/babel-loader-8.2.3.tgz", @@ -6035,6 +6056,26 @@ "resolved": "https://registry.npmmirror.com/electron-to-chromium/download/electron-to-chromium-1.4.27.tgz", "integrity": "sha512-uZ95szi3zUbzRDx1zx/xnsCG+2xgZyy57pDOeaeO4r8zx5Dqe8Jv1ti8cunvBwJHVI5LzPuw8umKwZb3WKYxSQ==" }, + "element-ui": { + "version": "2.15.6", + "resolved": "https://registry.npmmirror.com/element-ui/download/element-ui-2.15.6.tgz", + "integrity": "sha512-rcYXEKd/j2G0AgficAOk1Zd1AsnHRkhmrK4yLHmNOiimU2JfsywgfKUjMoFuT6pQx0luhovj8lFjpE4Fnt58Iw==", + "requires": { + "async-validator": "~1.8.1", + "babel-helper-vue-jsx-merge-props": "^2.0.0", + "deepmerge": "^1.2.0", + "normalize-wheel": "^1.0.1", + "resize-observer-polyfill": "^1.5.0", + "throttle-debounce": "^1.0.1" + }, + "dependencies": { + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/download/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" + } + } + }, "elliptic": { "version": "6.5.4", "resolved": "https://registry.npm.taobao.org/elliptic/download/elliptic-6.5.4.tgz?cache=0&sync_timestamp=1612291311722&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felliptic%2Fdownload%2Felliptic-6.5.4.tgz", @@ -7901,8 +7942,7 @@ "follow-redirects": { "version": "1.14.6", "resolved": "https://registry.npmmirror.com/follow-redirects/download/follow-redirects-1.14.6.tgz", - "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", - "dev": true + "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==" }, "for-in": { "version": "1.0.2", @@ -11142,6 +11182,11 @@ "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=", "dev": true }, + "normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/normalize-wheel/download/normalize-wheel-1.0.1.tgz", + "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=" + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/npm-run-path/download/npm-run-path-2.0.2.tgz", @@ -13155,6 +13200,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha1-DpAg3T0hAkRY1OvSfiPkAmmBBGQ=" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.nlark.com/resolve/download/resolve-1.20.0.tgz", @@ -14812,6 +14862,11 @@ "neo-async": "^2.6.0" } }, + "throttle-debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/throttle-debounce/download/throttle-debounce-1.1.0.tgz", + "integrity": "sha1-UYU9o3vmihVctugns1FKPEIuic0=" + }, "through": { "version": "2.3.8", "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", diff --git a/package.json b/package.json index ca072ed..efc9a89 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ }, "dependencies": { "@walletconnect/web3-provider": "^1.7.1", + "axios": "^0.21.0", "core-js": "^3.6.5", + "element-ui": "^2.15.6", "js-cookie": "^2.2.1", "videojs-contrib-hls": "^5.15.0", "vue": "^2.6.11", diff --git a/src/App.vue b/src/App.vue index db0b058..d68b2df 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,14 +4,16 @@ diff --git a/src/main.ts b/src/main.ts index 88a2035..37e3e71 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,12 @@ import Vue from 'vue' import App from './App.vue' +import ElementUI from 'element-ui' +import 'element-ui/lib/theme-chalk/index.css' import router from './router' import store from './store' import 'video.js/dist/video-js.css' +Vue.use(ElementUI) Vue.config.productionTip = false new Vue({ diff --git a/src/store/index.ts b/src/store/index.ts index 97a23c5..83c726c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,11 +1,13 @@ import Vue from 'vue' import Vuex from 'vuex' import { IAppState } from './modules/app' +import { IUserState } from './modules/user' Vue.use(Vuex) export interface IRootState { app: IAppState + user: IUserState } // Declare empty store first, dynamically register all modules later. diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..02dddb1 --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,139 @@ +import { + Action, + getModule, + Module, + Mutation, + VuexModule +} from 'vuex-module-decorators' +import { getToken, removeToken, setToken } from '@/utils/cookies' +import { getUserInfo, login, logout } from '@/api/User' +import store from '@/store' + +export interface IUserState { + token: string + name: string + avatar: string + introduction: string + roles: string[] + email: string + permissions: string[][] + level: number + sex?: string +} + +@Module({ dynamic: true, store, name: 'user' }) +class User extends VuexModule implements IUserState { + public token = getToken() || '' + public name = '' + public avatar = '' + public introduction = '' + public roles: string[] = [] + public permissions: string[][] = [] + public email = '' + public level = 999 + public sex = '0' + + @Action + public async Login(userInfo: { username: string, password: string }) { + let { username, password } = userInfo + username = username.trim() + const { data } = await login({ username, password }) + setToken(data.token) + this.SET_TOKEN(data.token) + } + + @Action + public ResetToken() { + removeToken() + this.SET_TOKEN('') + this.SET_ROLES([]) + this.SET_PERMISSIONS([]) + } + + @Action + public async GetUserInfo() { + if (this.token === '') { + throw Error('GetUserInfo: token is undefined!') + } + const { data } = await getUserInfo({ /* Your params here */ }) + if (!data) { + throw Error('Verification failed, please Login again.') + } + const { showname, avatar, introduction, permissions, level } = data + + this.SET_NAME(showname) + this.SET_AVATAR(avatar) + this.SET_INTRODUCTION(introduction) + this.SET_PERMISSIONS(permissions) + this.SET_LEVEL(level) + } + + @Action + public async UpdateInfo(data: any) { + const { showname, avatar } = data + this.SET_NAME(showname) + this.SET_AVATAR(avatar) + } + + @Action + public async LogOut() { + if (this.token === '') { + throw Error('LogOut: token is undefined!') + } + await logout() + removeToken() + this.SET_TOKEN('') + this.SET_ROLES([]) + this.SET_PERMISSIONS([]) + } + + @Mutation + private SET_TOKEN(token: string) { + this.token = token + } + + @Action + public async updatePageToken(token: string) { + this.SET_TOKEN(token) + } + + @Mutation + private SET_NAME(name: string) { + this.name = name + } + + @Mutation + private SET_AVATAR(avatar: string) { + this.avatar = avatar + } + + @Mutation + private SET_INTRODUCTION(introduction: string) { + this.introduction = introduction + } + + @Mutation + private SET_ROLES(roles: string[]) { + this.roles = roles + } + + @Mutation + private SET_LEVEL(level: number) { + this.level = level + } + + @Mutation + private SET_PERMISSIONS(permissions: string[]) { + const results: string[][] = [] + for (const permission of permissions) { + if (permission === '*') { + results.push(['*', '*']) + } else { + results.push(permission.split(':')) + } + } + this.permissions = results + } +} + +export const UserModule = getModule(User) diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..9a278c9 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,72 @@ +import axios from 'axios' +import { Message, MessageBox } from 'element-ui' +import { UserModule } from '@/store/modules/user' + +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, + timeout: 5000 +}) + +// Request interceptors +service.interceptors.request.use( + (config) => { + // Add X-Access-Token header to every request, you can add other custom headers here + if (UserModule.token) { + config.headers.authorization = 'Bearer ' + UserModule.token + } + config.headers['Content-Type'] = 'application/json' + return config + }, + (error) => { + Promise.reject(error) + } +) + +// Response interceptors +service.interceptors.response.use( + (response) => { + // Some example codes here: + // code == 0: success + // code == 50001: invalid access token + // code == 50002: already login in other place + // code == 50003: access token expired + // code == 50004: invalid user (user not exist) + // code == 10: username or password is incorrect + // You can change this part for your own usage. + const res = response.data + if (res.code) { + Message({ + message: res.msg || 'Error', + type: 'error', + duration: 5 * 1000 + }) + if (res.code === 50008 || res.code === 50012 || res.code === 50014) { + MessageBox.confirm( + 'You have been logged out, try to login again.', + 'Log out', + { + confirmButtonText: 'Relogin', + cancelButtonText: 'Cancel', + type: 'warning' + } + ).then(() => { + UserModule.ResetToken() + location.reload() // To prevent bugs from vue-router + }) + } + return Promise.reject(new Error(res.msg || 'Error')) + } else { + return response.data + } + }, + (error) => { + Message({ + message: error.message, + type: 'error', + duration: 5 * 1000 + }) + return Promise.reject(error) + } +) + +export default service diff --git a/src/utils/resize.ts b/src/utils/resize.ts new file mode 100644 index 0000000..71c3d5d --- /dev/null +++ b/src/utils/resize.ts @@ -0,0 +1,47 @@ +import { Component, Vue, Watch } from 'vue-property-decorator' +import { AppModule, DeviceType } from '@/store/modules/app' + +const WIDTH = 992 // refer to Bootstrap's responsive design + +@Component({ + name: 'ResizeMixin' +}) +export default class extends Vue { + get device() { + return AppModule.device + } + + @Watch('$route') + private onRouteChange() { + console.log('route change: ', this.$route?.fullPath) + } + + beforeMount() { + window.addEventListener('resize', this.resizeHandler) + } + + mounted() { + const isMobile = this.isMobile() + if (isMobile) { + AppModule.ToggleDevice(DeviceType.Mobile) + } + } + + beforeDestroy() { + window.removeEventListener('resize', this.resizeHandler) + } + + private isMobile() { + const rect = document.body.getBoundingClientRect() + return rect.width - 1 < WIDTH + } + + private resizeHandler() { + if (!document.hidden) { + const isMobile = this.isMobile() + AppModule.ToggleDevice(isMobile ? DeviceType.Mobile : DeviceType.Desktop) + if (isMobile) { + } + } + } +}