ソースを参照

first version

dygapp 5 年 前
コミット
f92909b041

+ 8 - 2
package.json

@@ -8,7 +8,11 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "core-js": "^3.2.1",
+    "@stomp/stompjs": "^5.4.2",
+    "axios": "^0.19.0",
+    "jsonwebtoken": "^8.5.1",
+    "naf-core": "^0.1.2",
+    "qrcode": "^1.4.1",
     "vue": "^2.6.10"
   },
   "devDependencies": {
@@ -16,8 +20,10 @@
     "@vue/cli-plugin-eslint": "^3.11.0",
     "@vue/cli-service": "^3.11.0",
     "babel-eslint": "^10.0.3",
-    "eslint": "^6.2.2",
+    "eslint": "^5.15.1",
     "eslint-plugin-vue": "^5.2.3",
+    "less": "^3.10.3",
+    "less-loader": "^4.1.0",
     "vue-template-compiler": "^2.6.10"
   },
   "eslintConfig": {

+ 3 - 4
src/App.vue

@@ -1,17 +1,16 @@
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <qrcode />
   </div>
 </template>
 
 <script>
-import HelloWorld from './components/HelloWorld.vue'
+import Qrcode from './components/qrcode.vue'
 
 export default {
   name: 'app',
   components: {
-    HelloWorld
+    Qrcode
   }
 }
 </script>

+ 0 - 58
src/components/HelloWorld.vue

@@ -1,58 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
-      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'HelloWorld',
-  props: {
-    msg: String
-  }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 95 - 0
src/components/qrcode.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="qrcode">
+    <img :src="dataUrl"/>
+    <div>微信扫码登录</div>
+    <p class="res" v-if="token">{{token}}</p>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import QRCode from 'qrcode';
+
+// import {login, getAdminInfo} from '@/api/getData'
+// import {mapActions, mapState} from 'vuex'
+export default {
+  metaInfo: {
+    title: '登录',
+  },
+  data() {
+    return {
+      qrcode: '',
+      dataUrl: null,
+      token: null,
+    };
+  },
+  async mounted() {
+    await this.initQrcode();
+  },
+  methods: {
+    async login({ qrcode }) {
+      const res = await this.$axios.$post(`/weixin/qrcode/${qrcode}/token`);
+      if (res.errcode != undefined && res.errcode === 0) {
+        console.log('qrcode login success', res);
+        return res.token;
+      }
+      console.error('create qrcode fail', res);
+    },
+    async createQrcode() {
+      const res = await this.$axios.$post('/weixin/qrcode/create');
+      if (res.errcode != undefined && res.errcode === 0) {
+        console.log('create qrcode success', res.data);
+        return res.data;
+      }
+      console.error('create qrcode fail', res);
+    },
+    async onMessage(message) {
+      console.log('receive a message: ', message.body);
+      if (message.body == 'scaned') {
+        try {
+          this.token = await this.login({
+            qrcode: this.qrcode,
+          });
+          console.log('扫码登录成功');
+        } catch (err) {
+          console.log('扫码登录失败');
+          console.error(err);
+        }
+      }
+    },
+    async initQrcode() {
+      // 创建二维码
+      this.qrcode = await this.createQrcode();
+      if (!this.qrcode) return;
+      let uri = `${Vue.config.weixin.baseUrl}/qrcode/${this.qrcode}/scan`;
+      if (uri.startsWith('/')) {
+        uri = `${location.protocol}//${location.host}${uri}`;
+      }
+      this.dataUrl = await QRCode.toDataURL(uri);
+      this.$stomp({
+        [`/exchange/qrcode.login/${this.qrcode}`]: this.onMessage,
+      });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.qrcode {
+  width: 160px;
+  font-size: 12px;
+  color: darkgray;
+  text-align: center;
+  background: white;
+  margin: 0 auto;
+  text-align: center;
+  padding-bottom: 5px;
+  img {
+    width: 140px;
+    height: 140px;
+  }
+  .res {
+    word-break: break-all;
+  }
+}
+</style>

+ 22 - 1
src/main.js

@@ -1,8 +1,29 @@
 import Vue from 'vue'
 import App from './App.vue'
+import '@/plugins/axios';
+import InitStomp from '@/plugins/stomp';
 
-Vue.config.productionTip = false
+Vue.config.productionTip = false;
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: 'http://smart.cc-lotus.info/weixin',
+};
+Vue.config.stomp = {
+  // brokerURL: 'ws://192.168.1.190:15674/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  connectHeaders: {
+    host: 'smart',
+    login: 'web',
+    passcode: 'web123',
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};
 
 new Vue({
   render: h => h(App),
 }).$mount('#app')
+
+InitStomp();

+ 22 - 0
src/plugins/axios.js

@@ -0,0 +1,22 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+import AxiosWrapper from '../utils/axios-wrapper';
+
+const Plugin = {
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$axios) {
+          this.$store.$axios = this.$axios;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    vue.prototype.$axios = new AxiosWrapper(options);
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL, unwrap: false });

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue'
+import _ from 'lodash'
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+
+const Plugin = {
+
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = (str) => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if(!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function () {
+        if(this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      }
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function (subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = () => {
+        // Do something, all subscribes must be done is this callback
+        // This is needed because this will be executed after a (re)connect
+        console.log('[stomp] connected');
+        Object.keys(subscribes).filter(p => _.isFunction(subscribes[p])).forEach(key => {
+          client.subscribe(key, subscribes[key]);
+        });
+      };
+
+      client.onStompError = (frame) => {
+        // Will be invoked in case of error encountered at Broker
+        // Bad login/passcode typically will cause an error
+        // Complaint brokers will set `message` header with a brief message. Body may contain details.
+        // Compliant brokers will terminate the connection after any error
+        console.log('Broker reported error: ' + frame.headers['message']);
+        console.log('Additional details: ' + frame.body);
+      };
+
+      client.activate();
+
+      this.$stompClient = client;
+    }
+  }
+}
+export default () => {
+  Vue.use(Plugin, Vue.config.stomp)
+}

+ 110 - 0
src/utils/axios-wrapper.js

@@ -0,0 +1,110 @@
+/* eslint-disable require-atomic-updates */
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+import UserUtil from './user-util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+export default class AxiosWrapper {
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach(key => {
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, null, query, options);
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+
+  async $request(uri, data, query, options) {
+    if (!uri) console.error('uri不能为空');
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params);
+    const url = AxiosWrapper.merge(uri, options.params);
+
+    currentRequests += 1;
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      if (UserUtil.token) {
+        axios.defaults.headers.common.Authorization = UserUtil.token;
+      }
+      let res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return res;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        res = _.omit(res, ['errcode', 'errmsg', 'details']);
+        const keys = Object.keys(res);
+        if (keys.length === 1 && keys.includes('data')) {
+          res = res.data;
+        }
+      }
+      return res;
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(`[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - ${err.message}`);
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+      }
+    }
+  }
+}

+ 46 - 0
src/utils/user-util.js

@@ -0,0 +1,46 @@
+/* eslint-disable no-console */
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    if (this.unit) {
+      this.lastUnit = this.unit;
+    }
+  },
+  get token() {
+    return sessionStorage.getItem('token') || '';
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  get unit() {
+    if (!this.user || this.user.iss !== 'platform') return undefined;
+    const unit = this.user.sub.split('@', 2)[1] || 'master';
+    return unit;
+  },
+  get platform() {
+    const unit = this.unit || this.lastUnit;
+    return unit === 'master' ? 'master' : 'school';
+  },
+  set lastUnit(value) {
+    localStorage.setItem('unit', value);
+  },
+  get lastUnit() {
+    return localStorage.getItem('unit');
+  },
+  save({ userinfo, token }) {
+    this.user = userinfo;
+    this.token = token;
+  },
+};

+ 24 - 0
vue.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  publicPath: '/demo',
+
+  // transpileDependencies: ['naf-core'],
+
+  // configureWebpack: {
+  //   externals: {
+  //     'element-ui': 'Element',
+  //     vue: 'Vue',
+  //   },
+  // },
+  devServer: {
+    proxy: {
+      '/ws': {
+        // target: 'http://smart.chinahuian.cn',
+        target: 'http://172.17.116.100:15674',
+        ws: true,
+      },
+      '/weixin': {
+        target: 'http://smart.cc-lotus.info',
+      },
+    },
+  },
+};