liuyu 4 vuotta sitten
vanhempi
commit
982442b6a1
100 muutettua tiedostoa jossa 21857 lisäystä ja 0 poistoa
  1. 3 0
      .env
  2. 33 0
      .eslintrc.js
  3. 3 0
      .vscode/settings.json
  4. 3 0
      babel.config.js
  5. 12859 0
      package-lock.json
  6. 68 0
      package.json
  7. BIN
      public/favicon.ico
  8. 57 0
      public/iconfont.css
  9. 18 0
      public/index.html
  10. 37 0
      src/App.vue
  11. BIN
      src/assets/beijing.jpg
  12. BIN
      src/assets/bg1.jpg
  13. BIN
      src/assets/bg2.jpg
  14. BIN
      src/assets/bg3.jpg
  15. BIN
      src/assets/login.png
  16. BIN
      src/assets/logo.png
  17. BIN
      src/assets/password.png
  18. BIN
      src/assets/roomid.png
  19. BIN
      src/assets/user.png
  20. 45 0
      src/components/data-table.md
  21. 297 0
      src/components/data-table.vue
  22. 81 0
      src/components/form.md
  23. 203 0
      src/components/form.vue
  24. 51 0
      src/components/pagination.vue
  25. 77 0
      src/components/upload.vue
  26. 77 0
      src/components/wang-editor.vue
  27. 341 0
      src/excel/Blob.js
  28. 279 0
      src/excel/Export2Excel.js
  29. 137 0
      src/iconfonts/iconfont.css
  30. BIN
      src/iconfonts/iconfont.eot
  31. 1 0
      src/iconfonts/iconfont.js
  32. 219 0
      src/iconfonts/iconfont.json
  33. 116 0
      src/iconfonts/iconfont.svg
  34. BIN
      src/iconfonts/iconfont.ttf
  35. BIN
      src/iconfonts/iconfont.woff
  36. BIN
      src/iconfonts/iconfont.woff2
  37. 168 0
      src/layout/layout-part/heads.vue
  38. 109 0
      src/layout/layout-part/menus.vue
  39. 39 0
      src/layout/layout-part/parts/bind.vue
  40. 63 0
      src/layout/layout-part/parts/passwdDia.vue
  41. 258 0
      src/layout/live/detailInfo copy.vue
  42. 801 0
      src/layout/live/detailInfo.vue
  43. 757 0
      src/layout/live/detailInfo_qili.vue
  44. 486 0
      src/layout/live/detailmetting.vue
  45. 121 0
      src/layout/live/liveList.vue
  46. 84 0
      src/layout/main-layout.vue
  47. 44 0
      src/layout/public/top.vue
  48. 205 0
      src/layout/room/detailInfo.vue
  49. 83 0
      src/layout/room/detailStatusInfo.vue
  50. 25 0
      src/main.js
  51. 19 0
      src/plugins/axios.js
  52. 42 0
      src/plugins/check-res.js
  53. 5 0
      src/plugins/element.js
  54. 6 0
      src/plugins/filters.js
  55. 27 0
      src/plugins/loading.js
  56. 4 0
      src/plugins/meta.js
  57. 33 0
      src/plugins/methods.js
  58. 20 0
      src/plugins/setting.js
  59. 65 0
      src/plugins/stomp.js
  60. 5 0
      src/plugins/vant.js
  61. 25 0
      src/plugins/var.js
  62. 24 0
      src/router/before.js
  63. 129 0
      src/router/index.js
  64. 46 0
      src/store/chat.js
  65. 46 0
      src/store/contact.js
  66. 24 0
      src/store/gensign.js
  67. 38 0
      src/store/index.js
  68. 102 0
      src/store/login.js
  69. 60 0
      src/store/lookuser.js
  70. 46 0
      src/store/news.js
  71. 64 0
      src/store/question.js
  72. 64 0
      src/store/questionnaire.js
  73. 46 0
      src/store/role.js
  74. 95 0
      src/store/room.js
  75. 44 0
      src/store/roomuser.js
  76. 68 0
      src/store/uploadquestion.js
  77. 20 0
      src/store/user/mutations.js
  78. 1 0
      src/store/user/state.js
  79. 118 0
      src/util/axios-wrapper.js
  80. 10 0
      src/util/filters.js
  81. 50 0
      src/util/methods-util.js
  82. 47 0
      src/util/optionTitles.js
  83. 50 0
      src/util/role_menu.js
  84. 69 0
      src/util/user-util.js
  85. 126 0
      src/views/anchor/detail.vue
  86. 98 0
      src/views/anchor/index.vue
  87. 109 0
      src/views/contact/index.vue
  88. 254 0
      src/views/index.vue
  89. 74 0
      src/views/live/detail.vue
  90. 59 0
      src/views/live/index.vue
  91. 74 0
      src/views/live/meetingDetail.vue
  92. 273 0
      src/views/liveIndex.vue
  93. 218 0
      src/views/livecheck.vue
  94. 105 0
      src/views/login copy.vue
  95. 161 0
      src/views/login.vue
  96. 143 0
      src/views/meetingBrief/detail.vue
  97. 162 0
      src/views/meetingBrief/index.vue
  98. 45 0
      src/views/question/index.vue
  99. 196 0
      src/views/question/part/tiku.vue
  100. 0 0
      src/views/question/part/tongji.vue

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_ROUTER="/liveadmin"
+VUE_APP_LIMIT = 10

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ['plugin:vue/essential', '@vue/prettier'],
+  plugins: ['vue'],
+  rules: {
+    'max-len': [
+      'warn',
+      {
+        code: 250,
+      },
+    ],
+    'no-unused-vars': 'off',
+    'no-console': 'off',
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        trailingComma: 'es5',
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160,
+      },
+    ],
+  },
+  parserOptions: {
+    parser: 'babel-eslint',
+  },
+};

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "eggHelper.serverPort": 64351
+}

+ 3 - 0
babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ["@vue/cli-plugin-babel/preset"]
+};

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 12859 - 0
package-lock.json


+ 68 - 0
package.json

@@ -0,0 +1,68 @@
+{
+  "name": "live-cms",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@antv/g2plot": "^1.1.7",
+    "@stomp/stompjs": "^5.4.4",
+    "axios": "^0.19.2",
+    "core-js": "^3.6.5",
+    "element-ui": "^2.13.2",
+    "file-saver": "^2.0.2",
+    "jsonwebtoken": "^8.5.1",
+    "loadsh": "0.0.4",
+    "naf-core": "^0.1.2",
+    "pili-rtc-web": "^2.2.8",
+    "qrcode": "^1.4.4",
+    "stomp": "^0.1.1",
+    "trtc-js-sdk": "^4.4.0",
+    "vue": "^2.6.11",
+    "vue-meta": "^2.4.0",
+    "vue-router": "^3.3.4",
+    "vuex": "^3.4.0",
+    "wangeditor": "^3.1.1",
+    "xlsx": "^0.16.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.4.0",
+    "@vue/cli-plugin-eslint": "~4.4.0",
+    "@vue/cli-plugin-router": "~4.4.0",
+    "@vue/cli-plugin-vuex": "~4.4.0",
+    "@vue/cli-service": "~4.4.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.4",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.11.3",
+    "less-loader": "^5.0.0",
+    "prettier": "^1.19.1",
+    "script-loader": "^0.7.2",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended",
+      "@vue/prettier"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 57 - 0
public/iconfont.css


+ 18 - 0
public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <!-- <link rel="stylesheet" type="text/css" href="iconfont.css" /> -->
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 37 - 0
src/App.vue

@@ -0,0 +1,37 @@
+<template>
+  <div id="app">
+    <main-layout></main-layout>
+  </div>
+</template>
+<script>
+import mainLayout from '@/layout/main-layout.vue';
+export default {
+  name: 'app',
+  components: {
+    mainLayout,
+  },
+  data: () => ({}),
+  created() {},
+  methods: {},
+};
+</script>
+
+<style lang="less">
+html {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+body {
+  margin: 0;
+  overflow-x: hidden;
+}
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+p {
+  padding: 0;
+  margin: 0;
+}
+</style>

BIN
src/assets/beijing.jpg


BIN
src/assets/bg1.jpg


BIN
src/assets/bg2.jpg


BIN
src/assets/bg3.jpg


BIN
src/assets/login.png


BIN
src/assets/logo.png


BIN
src/assets/password.png


BIN
src/assets/roomid.png


BIN
src/assets/user.png


+ 45 - 0
src/components/data-table.md

@@ -0,0 +1,45 @@
+# 组件文档说明
+## data-table.vue
+#### prop
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段列表(下文会说明如何使用)|
+|data|Array|`-`|是|数据列表|
+|opera|Array|[ ]|否|操作列的列表(下文会说明如何使用)|
+|toFormat|Function|`-`|否|如果fields中的format不是function类型,则会走toFormat的方法,需要自己写过滤规则,多个的情况需要区分|
+|select|Boolean|false|否|需要选择就变成true|
+|total|NUmber|0|否|分页的总数据|
+|usePage|Boolean|true|否|是否使用分页|
+|options|Object|null|否|加些属性,不知道能加啥,反正我把合计加上好使了|
+|useSum|Boolean|false|否|使用合计|
+|filter|Array|`[]`|否|额外查询|
+
+>fields
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|列名称|
+|prop|String|`-`|是|字段名称|
+|format|Function/String|`-`|否|Function类型:数据需要过滤则将过滤方法写在这;String类型:走toFormat方法,参数位(model=>字段名,value=>值)|
+|custom|Boolean|false|否|自定义输出|
+|options|Object|`-`|否|添加额外属性,比如说样式之类的|
+|filter|String|`-`|否|如果填写,则这个字段会查询,这里只填写类型,input/select,select的选项在options插槽中使用|
+|selected|Array|`-`|false|多选选项的数据|
+
+>opera
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|操作按钮提示文字|
+|icon|String|`-`|否|图标|
+|method|String|`-`|是|此按钮连接的父级方法($emit)|
+|confirm|Boolean|`-`|否|是否需要确认提示|
+|methodZh|String/Function|label|否|确认提示的操作文字,1,Function参数为这条数据,自己随意组合;2,String为纯自定义字符串,需要自己写整个提示语;3,默认,使用label字段提示|
+|display|Function|`-`|否|控制按钮是否显示(目前为简单版,只是根据此条数据中的内容判断,以后要是有需求会修改成toFormat的形式)|
+
+>methods
+>>
+|方法名|参数|说明|
+|:-:|:-:|:-:|
+|handleSelect|Array[object]|返回选择的内容|
+|query|{skip,limit,...info}|分页查询,及条件查询|

+ 297 - 0
src/components/data-table.vue

@@ -0,0 +1,297 @@
+<template>
+  <div id="data-table">
+    <el-form :model="searchInfo" :inline="true" size="mini" v-if="useFilter">
+      <el-form-item v-for="(item, index) in filterList" :key="index">
+        <template v-if="item.filter === 'select'">
+          <el-select v-model="searchInfo[item.prop]" size="mini" clearable filterable :placeholder="`请选择${item.label}`" @clear="toClear(item.prop)">
+            <slot name="options" v-bind="{ item }"></slot>
+          </el-select>
+        </template>
+        <template v-else-if="item.filter === 'date'">
+          <el-date-picker
+            v-model="searchInfo[item.prop]"
+            value-format="yyyy-MM-dd"
+            format="yyyy-MM-dd"
+            type="daterange"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            clearable
+          >
+          </el-date-picker>
+        </template>
+        <template v-else>
+          <el-input v-model="searchInfo[item.prop]" clearable size="mini" :placeholder="`请输入${item.label}`" @clear="toClear(item.prop)"></el-input>
+        </template>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" size="mini" @click="filterSearch">查询</el-button>
+      </el-form-item>
+    </el-form>
+    <el-table
+      ref="table"
+      row-key="id"
+      :data="data"
+      border
+      stripe
+      size="mini"
+      :max-height="height !== null ? height : ''"
+      @select="handleSelectionChange"
+      @select-all="handleSelectAll"
+      v-bind="options"
+      :show-summary="useSum"
+      @row-click="rowClick"
+    >
+      <el-table-column type="selection" width="55" v-if="select" prop="id" :reserve-selection="true" :show-overflow-tooltip="true"> </el-table-column>
+      <template v-for="(item, index) in fields">
+        <template v-if="item.custom">
+          <el-table-column :key="index" align="center" :label="item.label" v-bind="item.options" :show-overflow-tooltip="true">
+            <template v-slot="{ row }">
+              <slot name="custom" v-bind="{ item, row }"></slot>
+            </template>
+          </el-table-column>
+        </template>
+        <template v-else>
+          <el-table-column
+            :key="index"
+            align="center"
+            :label="item.label"
+            :prop="item.prop"
+            :formatter="toFormatter"
+            sortable
+            v-bind="item.options"
+            :show-overflow-tooltip="true"
+          >
+          </el-table-column>
+        </template>
+      </template>
+      <template v-if="opera.length > 0">
+        <el-table-column label="操作" align="center">
+          <template v-slot="{ row, $index }">
+            <template v-for="(item, index) in opera">
+              <template v-if="display(item, row)">
+                <el-tooltip v-if="item.icon" :key="index" effect="dark" :content="item.label" placement="bottom">
+                  <el-button
+                    :key="index"
+                    type="text"
+                    :icon="item.icon || ''"
+                    size="mini"
+                    @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                  ></el-button>
+                </el-tooltip>
+                <el-button v-else :key="index" type="text" size="mini" @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)">
+                  {{ item.label }}
+                </el-button>
+              </template>
+            </template>
+          </template>
+        </el-table-column>
+      </template>
+    </el-table>
+    <el-row type="flex" align="middle" justify="end" style="padding-top:1rem" v-if="usePage">
+      <el-col :span="24" style="text-align:right;">
+        <el-pagination
+          background
+          layout="sizes, total, prev, pager, next"
+          :page-sizes="[10, 15, 20, 50, 100]"
+          :total="total"
+          :page-size="limit"
+          :current-page.sync="currentPage"
+          @current-change="changePage"
+          @size-change="sizeChange"
+        >
+        </el-pagination>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataForm from '@/components/form.vue';
+export default {
+  name: 'data-table',
+  props: {
+    fields: { type: Array, required: true },
+    data: { type: Array, required: true },
+    opera: { type: Array, default: () => [] },
+    toFormat: null,
+    height: null,
+    select: { type: Boolean, default: false },
+    selected: { type: Array, default: () => [] },
+    usePage: { type: Boolean, default: true },
+    total: { type: Number, default: 0 },
+    options: null,
+    useSum: { type: Boolean, default: false },
+    filter: { type: Array, default: () => [] },
+  },
+  components: {},
+  data: () => ({
+    pageSelected: [],
+    currentPage: 1,
+    limit: _.get(this, `$limit`, undefined) !== undefined ? this.$limit : process.env.VUE_APP_LIMIT * 1,
+    searchInfo: {},
+    useFilter: true,
+    filterList: [],
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    toFormatter(row, column, cellValue, index) {
+      let this_fields = this.fields.filter(fil => fil.prop === column.property);
+      if (this_fields.length > 0) {
+        let format = _.get(this_fields[0], `format`, false);
+        if (format) {
+          let res;
+          if (_.isFunction(format)) {
+            res = format(cellValue);
+          } else {
+            res = this.toFormat({
+              model: this_fields[0].prop,
+              value: cellValue,
+            });
+          }
+          return res;
+        } else return cellValue;
+      }
+    },
+    handleOpera(data, method, confirm = false, methodZh, label, index) {
+      let self = true;
+      if (_.isFunction(methodZh)) {
+        methodZh = methodZh(data);
+      } else if (!_.isString(methodZh)) {
+        methodZh = label;
+        self = false;
+      }
+      if (confirm) {
+        this.$confirm(self ? methodZh : `您确认${methodZh}该数据?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+          .then(() => {
+            this.$emit(method, { data, index });
+          })
+          .catch(() => {});
+      } else {
+        this.$emit(method, { data, index });
+      }
+    },
+    handleSelectionChange(selection, row) {
+      // console.log(selection);
+      // console.log(row);
+      //根据row是否再pageSelected中,判断是添加还是删除
+      let res = [];
+      if (this.pageSelected.find(i => i.id === row.id)) {
+        res = this.pageSelected.filter(f => f.id !== row.id);
+      } else {
+        this.pageSelected.push(row);
+        res = this.pageSelected;
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, _.uniqBy(res, 'id'));
+    },
+    handleSelectAll(selection) {
+      //处于没全选状态,选择之后一定是全选,只有处于全选状态时,才会反选(全取消)
+      // console.log(selection);
+      let res = [];
+      if (selection.length > 0) {
+        //全选
+        res = _.uniqBy(this.pageSelected.concat(selection), 'id');
+      } else {
+        //全取消
+        res = _.differenceBy(this.pageSelected, this.data, 'id');
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, res);
+    },
+    initSelection() {
+      this.$nextTick(() => {
+        this.$refs.table.clearSelection();
+        this.selected.forEach(info => {
+          let d = this.data.filter(p => p.id === info.id);
+          if (d.length > 0) this.$refs.table.toggleRowSelection(d[0]);
+        });
+      });
+    },
+    selectReset() {
+      this.$refs.table.clearSelection();
+    },
+    display(item, row) {
+      let display = _.get(item, `display`, true);
+      if (display === true) return true;
+      else {
+        let res = display(row);
+        return res;
+      }
+    },
+    //
+    changePage(page) {
+      this.$emit('query', { skip: (page - 1) * this.limit, limit: this.limit, ...this.searchInfo });
+    },
+    sizeChange(limit) {
+      this.limit = limit;
+      this.currentPage = 1;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    getFilterList() {
+      let res = this.fields.filter(f => _.get(f, 'filter', false));
+      this.$set(this, `useFilter`, res.length > 0);
+      res.map(i => {
+        if (i.filter === 'date' && this.searchInfo[i.porp] === undefined) this.$set(this.searchInfo, i.prop, []);
+      });
+      res = [...res, ...this.filter];
+      this.$set(this, `filterList`, res);
+    },
+    filterSearch() {
+      this.currentPage = 1;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    rowClick(row, column, event) {
+      this.$emit(`rowClick`, row);
+    },
+    toClear(prop) {
+      delete this.searchInfo[prop];
+    },
+  },
+  watch: {
+    selected: {
+      handler(val) {
+        if (val.length > 0) {
+          this.pageSelected = val;
+          this.initSelection();
+        }
+      },
+      immediate: true,
+    },
+    data: {
+      handler(val, oval) {
+        if (this.select) {
+          this.initSelection();
+        }
+      },
+    },
+    fields: {
+      handler(val, oval) {
+        if (val) this.getFilterList();
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+/deep/ .el-table--mini th {
+  padding: 2px 0;
+}
+/deep/.el-table--mini td {
+  padding: 2px 0;
+}
+/deep/.el-table__empty-text {
+  line-height: 40px;
+}
+/deep/.el-table__empty-block {
+  min-height: 40px;
+}
+</style>

+ 81 - 0
src/components/form.md

@@ -0,0 +1,81 @@
+# 组件说明文档
+### form.vue
+### props
+
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段相关都在这里,用来自动输出,详情见下面|
+|submitText|String|`保存`|否|默认保存按钮的文字|
+|rules|Object|`-`|否|校验规则,不会找el-form的例子,不过使用的async-validator这个依赖为基础,会写这个也可以~~(那就厉害了,反正我是不行)~~|
+|isNew|Boolean|`-`|是|修改还是添加的提示|
+|data|Object|`-`|否|修改传来的数据|
+|needSave|Boolean|false|否|是否禁用保存按钮|
+|useEnter|Boolean|true|否|使用回车提交|
+|reset|Boolean|true|否|提交后是否重置表单|
+
+
+### fields
+>Array类型 必填
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|显示的字段中文|
+|type|String|input|否|这个字段要用什么类型来输出 input的基本类型可选值:date,datetime,radio,checkbox,select,text(只显示值),editor(富文本编辑器),password|
+|required|Boolean|`-`|否|是否必须输入|
+|model|String|`-`|是|字段名|
+|placeholder|String|`-`|否|占位,正常用,只是个透传|
+|options|object|`-`|否|标签的属性设置,例如:textarea 需要显示剩余字数,或者input限制长度,都往这里写,key-value形式(键值对,json的基本了解,不知道百度,具体属性看你具体用那个组件,那个组件有什么属性,瞎写不一定好使)|
+|custom|Boolean|`-`|否|是否使用自定义插槽|
+|tip|String|`-`|否|提示语,例如:请输入11位电话号码|
+|labelWidth|String|`120px`|否|表单label宽度,element的,默认120px|
+
+
+
+
+
+
+
+### slot
+>
+|插槽名|说明|
+|:-:|:-:|
+|options|fields中type为select的,选项都写在这个插槽中,多个select则需要区分options所属问题|
+|radios|fields中type为radio的,选项都写在这个插槽中,多个radio则需要区分radios所属问题|
+|checkboxs|fields中type为checkbox的,选项都写在这个插槽中,多个checkbox则需要区分checkboxs所属问题|
+|custom|自定义插槽,完全自己去写|
+|submit|提交按钮部分,当needSave为false时才可以使用|
+>>关于自定义的用法:
+>>在fields中,custom:true的情况即需要自定义,写法如下
+
+>>`<template #custom="{ item, form, fieldChange }"> ... </template>`
+>>
+|参数名|说明|
+|:-:|:-:|
+|item|fields循环出来的每一项|
+|form|组件内部的表单|
+|fieldChange|组件内部的修改方法,此方法不一定必须使用,看情况来;参数:{model:xxx,value:XXX}(model:字段名,value:值)|
+>>在使用时,此插槽内的v-model可以写成form[item.model],也可以写成form.字段名
+
+>>例如`<el-input v-model="form[item.model]">`或者`<el-input v-model="form.xxx">`
+
+>> **如果有多处需要自定义,请区分开去写**
+
+
+***
+### upload
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|url|String|`-`|是|上传地址|
+|limit|Number|`-`|是|限制上传数量|
+|data|any|`-`|否|上传数据|
+|type|String|`-`|否|上传返回的字段|
+|isBtn|Boolean|false|否|是否只显示按钮|
+|showList|Boolean|true|否|是否显示上传列表|
+|accept|String|`-`|否|可以上传的文件类型,不写就没限制|
+|tip|String|`-`|否|提示信息|
+|listType|String|picture-card|否|上传文件列表显示类型|
+
+>### method
+>|方法名|返回参数|说明|
+|:-:|:-:|:-:|
+|upload|{type,data}|上传成功返回

+ 203 - 0
src/components/form.vue

@@ -0,0 +1,203 @@
+<template>
+  <div id="add">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      :label-width="labelWidth"
+      class="form"
+      size="small"
+      @submit.native.prevent
+      :style="styles"
+      :inline="inline"
+    >
+      <template v-for="(item, index) in fields">
+        <template v-if="!loading">
+          <el-form-item v-if="display(item)" :key="'form-field-' + index" :label="getField('label', item)" :prop="item.model" :required="item.required">
+            <template v-if="!item.custom">
+              <template v-if="item.type !== 'text'">
+                <!-- <el-tooltip class="item" effect="dark" :content="item.tip" placement="top-start" :disabled="!item.tip"> -->
+                <template v-if="item.type === `date` || item.type === `datetime`">
+                  <el-date-picker
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    placeholder="选择择"
+                    format="yyyy-MM-dd"
+                    value-format="yyyy-MM-dd"
+                    v-bind="item.options"
+                  >
+                  </el-date-picker>
+                </template>
+                <template v-else-if="item.type === `year` || item.type === `week` || item.type === `day`">
+                  <el-date-picker
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    placeholder="选择择"
+                    :format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                    :value-format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                    v-bind="item.options"
+                  >
+                  </el-date-picker>
+                </template>
+                <template v-else-if="item.type === 'radio'">
+                  <el-radio-group v-model="form[item.model]" size="mini" v-bind="item.options">
+                    <slot name="radios" v-bind="{ item, form, fieldChange }"></slot>
+                  </el-radio-group>
+                </template>
+                <template v-else-if="item.type === 'select'">
+                  <el-select v-model="form[item.model]" v-bind="item.options" filterable clearable>
+                    <slot name="options" v-bind="{ item, form, fieldChange }"></slot>
+                  </el-select>
+                </template>
+                <template v-else-if="item.type === 'textarea'">
+                  <el-input clearable v-model="form[item.model]" type="textarea" :autosize="{ minRows: 3, maxRows: 5 }"></el-input>
+                </template>
+                <template v-else-if="item.type === 'editor'">
+                  <wang-editor v-model="form[item.model]"></wang-editor>
+                </template>
+                <template v-else-if="item.type === 'checkbox'">
+                  <el-checkbox-group v-model="form[item.model]" v-bind="item.options">
+                    <slot name="checkboxs" v-bind="{ item, form, fieldChange }"></slot>
+                  </el-checkbox-group>
+                </template>
+                <template v-else>
+                  <el-input
+                    clearable
+                    v-model="form[item.model]"
+                    :type="getField('type', item)"
+                    :placeholder="getField('placeholder', item)"
+                    :show-password="getField('type', item) === 'password'"
+                    v-bind="item.options"
+                  ></el-input>
+                </template>
+                <!-- </el-tooltip> -->
+              </template>
+              <template v-else>
+                {{ form[item.model] }}
+              </template>
+            </template>
+            <template v-else>
+              <slot name="custom" v-bind="{ item, form, fieldChange }"></slot>
+            </template>
+          </el-form-item>
+        </template>
+      </template>
+      <el-form-item v-if="needSave" class="btn">
+        <el-row type="flex" align="middle" justify="space-around">
+          <el-col :span="6">
+            <el-button type="primary" @click="save">{{ submitText }}</el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+      <el-form-item v-else>
+        <slot name="submit"></slot>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import wangEditor from './wang-editor.vue';
+export default {
+  name: 'add',
+  props: {
+    fields: { type: Array, default: () => [] },
+    rules: { type: Object, default: () => {} },
+    isNew: { type: Boolean, default: true },
+    data: null,
+    styles: { type: Object, default: () => {} },
+    needSave: { type: Boolean, default: true },
+    labelWidth: { type: String, default: '120px' },
+    useEnter: { type: Boolean, default: true },
+    submitText: { type: String, default: '保存' },
+    inline: { type: Boolean, default: false },
+    reset: { type: Boolean, default: true },
+  },
+  components: {
+    wangEditor,
+  },
+  data: () => ({
+    form: {},
+    show: false,
+    dateShow: false,
+    loading: true,
+  }),
+  created() {
+    if (this.useEnter) {
+      document.onkeydown = () => {
+        let key = window.event.keyCode;
+        if (key == 13) {
+          this.save();
+        }
+      };
+    }
+  },
+  computed: {},
+  mounted() {},
+  watch: {
+    fields: {
+      handler(val) {
+        this.checkType();
+      },
+      immediate: true,
+    },
+    data: {
+      handler(val) {
+        this.loading = true;
+        if (val) this.$set(this, `form`, this.data);
+        this.loading = false;
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  methods: {
+    getField(item, data) {
+      let res = _.get(data, item, null);
+      if (item === 'type') res = res === null ? `text` : res;
+      if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res;
+      if (item === 'required') res = res === null ? false : res;
+      if (item === `error`) res = res === null ? `${data.label}错误` : res;
+      return res;
+    },
+    save() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.$emit(`save`, { isNew: this.isNew, data: JSON.parse(JSON.stringify(this.form)) });
+          if (this.reset) this.$refs.form.resetFields();
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    fieldChange({ model, value }) {
+      this.$set(this.form, model, value);
+    },
+    checkType() {
+      let arr = this.fields.filter(fil => fil.type === 'checkbox');
+      if (arr.length > 0 && this.isNew) {
+        for (const item of arr) {
+          this.$set(this.form, `${item.model}`, []);
+        }
+      }
+    },
+    display(field) {
+      let dis = _.get(field, `display`);
+      if (!_.isFunction(dis)) return true;
+      else return dis(field, this.form);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.form {
+  padding: 2rem 1rem;
+  background: #fff;
+  border-radius: 20px;
+}
+/deep/.btn .el-form-item__content {
+  margin-left: 0 !important;
+}
+</style>

+ 51 - 0
src/components/pagination.vue

@@ -0,0 +1,51 @@
+<template>
+  <div id="pagination">
+    <el-row type="flex" align="middle" style="padding-top:1rem">
+      <el-col :span="24" :style="`text-align:${position};`">
+        <el-pagination
+          background
+          layout=" total, prev, pager, next"
+          :total="total"
+          :page-size="limit"
+          :current-page.sync="currentPage"
+          @current-change="changePage"
+        >
+        </el-pagination>
+        <!-- 
+          :page-sizes="[5, 10, 15, 20, 50, 100]"
+          @size-change="sizeChange"
+         -->
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+export default {
+  name: 'pagination',
+  props: {
+    position: { type: String, default: 'right' },
+    total: { type: Number, default: 0 },
+  },
+  components: {},
+  data: () => {
+    return {
+      currentPage: 1,
+      limit: 10,
+    };
+  },
+  created() {},
+  methods: {
+    changePage(page) {
+      this.$emit('query', { skip: (page - 1) * this.limit, limit: this.limit });
+    },
+    sizeChange(limit) {
+      this.limit = limit;
+      this.$emit('query', { skip: 0, limit: this.limit });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 77 - 0
src/components/upload.vue

@@ -0,0 +1,77 @@
+<template>
+  <div id="upload">
+    <el-upload
+      v-if="url"
+      ref="upload"
+      :action="url"
+      :list-type="listType"
+      :file-list="fileList"
+      :limit="limit"
+      :on-exceed="outLimit"
+      :on-preview="handlePictureCardPreview"
+      :before-remove="handleRemove"
+      :on-success="onSuccess"
+      accept=".jpg,.jpeg,.png,.bmp,.gif,.svg,.mp4"
+    >
+      <template>
+        <i class="el-icon-plus"></i>
+      </template>
+    </el-upload>
+    <el-dialog :visible.sync="dialogVisible">
+      <img width="100%" :src="dialogImageUrl" alt="" />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'upload',
+  props: {
+    url: { type: null },
+    limit: { type: Number },
+    data: { type: null },
+    type: { type: String },
+    listType: { type: String, default: 'picture-card' },
+  },
+  components: {},
+  data: () => ({
+    dialogVisible: false,
+    dialogImageUrl: '',
+    fileList: [],
+  }),
+  created() {
+    if (this.data) {
+      this.defalutProcess(this.data);
+    }
+  },
+  watch: {
+    data: {
+      handler(val) {
+        this.defalutProcess(val);
+      },
+    },
+  },
+  computed: {},
+  methods: {
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+    handleRemove(file) {
+      return true;
+    },
+    outLimit() {
+      this.$message.error('只允许上传1张图片');
+    },
+    onSuccess(response, file, fileList) {
+      //将文件整理好传回父组件
+      this.$emit('upload', { type: this.type, data: response });
+    },
+    defalutProcess(val) {
+      this.$set(this, `fileList`, [{ name: this.type, url: `${this.data}?${new Date().getTime()}` }]);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 77 - 0
src/components/wang-editor.vue

@@ -0,0 +1,77 @@
+<template>
+  <div ref="editor" style="text-align:left"></div>
+</template>
+<script>
+import E from 'wangeditor';
+
+const menus = [
+  'head', // 标题
+  'bold', // 粗体
+  'fontSize', // 字号
+  'fontName', // 字体
+  'italic', // 斜体
+  'underline', // 下划线
+  'strikeThrough', // 删除线
+  'foreColor', // 文字颜色
+  'backColor', // 背景颜色
+  'link', // 插入链接
+  'list', // 列表
+  'justify', // 对齐方式
+  'quote', // 引用
+  // 'emoticon', // 表情
+  'table', // 表格
+  // 'video', // 插入视频
+  // 'code', // 插入代码
+  'undo', // 撤销
+  'redo', // 重复
+];
+
+export default {
+  name: 'wang-editor',
+  model: {
+    prop: 'value',
+    event: 'change', // 默认为input时间,此处改为change
+  },
+  props: {
+    value: { type: String, required: false, default: '' },
+  },
+  data() {
+    return {
+      editorContent: this.value,
+    };
+  },
+  mounted() {
+    var editor = new E(this.$refs.editor);
+    editor.customConfig.onchange = html => {
+      this.editorContent = html;
+      this.$emit('change', html);
+    };
+    // 自定义菜单配置
+    editor.customConfig.menus = menus;
+    editor.customConfig.zIndex = 0;
+    editor.customConfig.uploadImgServer = '/files/cms/images/upload';
+    editor.customConfig.uploadImgMaxLength = 1;
+    editor.customConfig.uploadImgHooks = {
+      // 如果服务器端返回的不是 {errno:0, data: [...]} 这种格式,可使用该配置
+      // (但是,服务器端返回的必须是一个 JSON 格式字符串!!!否则会报错)
+      customInsert: function(insertImg, result, editor) {
+        // 图片上传并返回结果,自定义插入图片的事件(而不是编辑器自动插入图片!!!)
+        // insertImg 是插入图片的函数,editor 是编辑器对象,result 是服务器端返回的结果
+
+        // 举例:假如上传图片成功后,服务器端返回的是 {url:'....'} 这种格式,即可这样插入图片:
+        var url = result.uri;
+        insertImg(url);
+
+        // result 必须是一个 JSON 格式字符串!!!否则报错
+      },
+    };
+    editor.create();
+    editor.txt.html(this.value);
+  },
+  methods: {
+    getContent: function() {
+      return this.editorContent;
+    },
+  },
+};
+</script>

+ 341 - 0
src/excel/Blob.js

@@ -0,0 +1,341 @@
+/* eslint-disable */
+
+/* Blob.js
+
+* A Blob implementation.
+
+* 2014-05-27
+
+*
+
+* By Eli Grey, http://eligrey.com
+
+* By Devin Samarin, https://github.com/eboyjr
+
+* License: X11/MIT
+
+*  See LICENSE.md
+
+*/
+
+/*global self, unescape */
+
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+
+plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
+
+(function (view) {
+
+  "use strict";
+
+  view.URL = view.URL || view.webkitURL;
+
+  if (view.Blob && view.URL) {
+
+    try {
+
+      new Blob;
+
+      return;
+
+    } catch (e) { }
+
+  }
+
+  // Internally we use a BlobBuilder implementation to base Blob off of
+
+  // in order to support older browsers that only have BlobBuilder
+
+  var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function (view) {
+
+    var
+
+      get_class = function (object) {
+
+        return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
+
+      }
+
+      , FakeBlobBuilder = function BlobBuilder() {
+
+        this.data = [];
+
+      }
+
+      , FakeBlob = function Blob(data, type, encoding) {
+
+        this.data = data;
+
+        this.size = data.length;
+
+        this.type = type;
+
+        this.encoding = encoding;
+
+      }
+
+      , FBB_proto = FakeBlobBuilder.prototype
+
+      , FB_proto = FakeBlob.prototype
+
+      , FileReaderSync = view.FileReaderSync
+
+      , FileException = function (type) {
+
+        this.code = this[this.name = type];
+
+      }
+
+      , file_ex_codes = (
+
+        "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+
+        + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
+
+      ).split(" ")
+
+      , file_ex_code = file_ex_codes.length
+
+      , real_URL = view.URL || view.webkitURL || view
+
+      , real_create_object_URL = real_URL.createObjectURL
+
+      , real_revoke_object_URL = real_URL.revokeObjectURL
+
+      , URL = real_URL
+
+      , btoa = view.btoa
+
+      , atob = view.atob
+
+      , ArrayBuffer = view.ArrayBuffer
+
+      , Uint8Array = view.Uint8Array
+
+      ;
+
+    FakeBlob.fake = FB_proto.fake = true;
+
+    while (file_ex_code--) {
+
+      FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
+
+    }
+
+    if (!real_URL.createObjectURL) {
+
+      URL = view.URL = {};
+
+    }
+
+    URL.createObjectURL = function (blob) {
+
+      var
+
+        type = blob.type
+
+        , data_URI_header
+
+        ;
+
+      if (type === null) {
+
+        type = "application/octet-stream";
+
+      }
+
+      if (blob instanceof FakeBlob) {
+
+        data_URI_header = "data:" + type;
+
+        if (blob.encoding === "base64") {
+
+          return data_URI_header + ";base64," + blob.data;
+
+        } else if (blob.encoding === "URI") {
+
+          return data_URI_header + "," + decodeURIComponent(blob.data);
+
+        } if (btoa) {
+
+          return data_URI_header + ";base64," + btoa(blob.data);
+
+        } else {
+
+          return data_URI_header + "," + encodeURIComponent(blob.data);
+
+        }
+
+      } else if (real_create_object_URL) {
+
+        return real_create_object_URL.call(real_URL, blob);
+
+      }
+
+    };
+
+    URL.revokeObjectURL = function (object_URL) {
+
+      if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
+
+        real_revoke_object_URL.call(real_URL, object_URL);
+
+      }
+
+    };
+
+    FBB_proto.append = function (data/*, endings*/) {
+
+      var bb = this.data;
+
+      // decode data to a binary string
+
+      if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
+
+        var
+
+          str = ""
+
+          , buf = new Uint8Array(data)
+
+          , i = 0
+
+          , buf_len = buf.length
+
+          ;
+
+        for (; i < buf_len; i++) {
+
+          str += String.fromCharCode(buf[i]);
+
+        }
+
+        bb.push(str);
+
+      } else if (get_class(data) === "Blob" || get_class(data) === "File") {
+
+        if (FileReaderSync) {
+
+          var fr = new FileReaderSync;
+
+          bb.push(fr.readAsBinaryString(data));
+
+        } else {
+
+          // async FileReader won't work as BlobBuilder is sync
+
+          throw new FileException("NOT_READABLE_ERR");
+
+        }
+
+      } else if (data instanceof FakeBlob) {
+
+        if (data.encoding === "base64" && atob) {
+
+          bb.push(atob(data.data));
+
+        } else if (data.encoding === "URI") {
+
+          bb.push(decodeURIComponent(data.data));
+
+        } else if (data.encoding === "raw") {
+
+          bb.push(data.data);
+
+        }
+
+      } else {
+
+        if (typeof data !== "string") {
+
+          data += ""; // convert unsupported types to strings
+
+        }
+
+        // decode UTF-16 to binary string
+
+        bb.push(unescape(encodeURIComponent(data)));
+
+      }
+
+    };
+
+    FBB_proto.getBlob = function (type) {
+
+      if (!arguments.length) {
+
+        type = null;
+
+      }
+
+      return new FakeBlob(this.data.join(""), type, "raw");
+
+    };
+
+    FBB_proto.toString = function () {
+
+      return "[object BlobBuilder]";
+
+    };
+
+    FB_proto.slice = function (start, end, type) {
+
+      var args = arguments.length;
+
+      if (args < 3) {
+
+        type = null;
+
+      }
+
+      return new FakeBlob(
+
+        this.data.slice(start, args > 1 ? end : this.data.length)
+
+        , type
+
+        , this.encoding
+
+      );
+
+    };
+
+    FB_proto.toString = function () {
+
+      return "[object Blob]";
+
+    };
+
+    FB_proto.close = function () {
+
+      this.size = this.data.length = 0;
+
+    };
+
+    return FakeBlobBuilder;
+
+  }(view));
+
+  view.Blob = function Blob(blobParts, options) {
+
+    var type = options ? (options.type || "") : "";
+
+    var builder = new BlobBuilder();
+
+    if (blobParts) {
+
+      for (var i = 0, len = blobParts.length; i < len; i++) {
+
+        builder.append(blobParts[i]);
+
+      }
+
+    }
+
+    return builder.getBlob(type);
+
+  };
+
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

+ 279 - 0
src/excel/Export2Excel.js

@@ -0,0 +1,279 @@
+/* eslint-disable */
+
+require('script-loader!file-saver');
+
+require('./Blob');
+
+require('script-loader!xlsx/dist/xlsx.core.min');
+
+function generateArray(table) {
+
+  var out = [];
+
+  var rows = table.querySelectorAll('tr');
+
+  var ranges = [];
+
+  for (var R = 0; R < rows.length; ++R) {
+
+    var outRow = [];
+
+    var row = rows[R];
+
+    var columns = row.querySelectorAll('td');
+
+    for (var C = 0; C < columns.length; ++C) {
+
+      var cell = columns[C];
+
+      var colspan = cell.getAttribute('colspan');
+
+      var rowspan = cell.getAttribute('rowspan');
+
+      var cellValue = cell.innerText;
+
+      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
+
+      //Skip ranges
+
+      ranges.forEach(function (range) {
+
+        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
+
+          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
+
+        }
+
+      });
+
+      //Handle Row Span
+
+      if (rowspan || colspan) {
+
+        rowspan = rowspan || 1;
+
+        colspan = colspan || 1;
+
+        ranges.push({ s: { r: R, c: outRow.length }, e: { r: R + rowspan - 1, c: outRow.length + colspan - 1 } });
+
+      }
+
+      ;
+
+      //Handle Value
+
+      outRow.push(cellValue !== "" ? cellValue : null);
+
+      //Handle Colspan
+
+      if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
+
+    }
+
+    out.push(outRow);
+
+  }
+
+  return [out, ranges];
+
+};
+
+function datenum(v, date1904) {
+
+  if (date1904) v += 1462;
+
+  var epoch = Date.parse(v);
+
+  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
+
+}
+
+function sheet_from_array_of_arrays(data, opts) {
+
+  var ws = {};
+
+  var range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } };
+
+  for (var R = 0; R != data.length; ++R) {
+
+    for (var C = 0; C != data[R].length; ++C) {
+
+      if (range.s.r > R) range.s.r = R;
+
+      if (range.s.c > C) range.s.c = C;
+
+      if (range.e.r < R) range.e.r = R;
+
+      if (range.e.c < C) range.e.c = C;
+
+      var cell = { v: data[R][C] };
+
+      if (cell.v == null) continue;
+
+      var cell_ref = XLSX.utils.encode_cell({ c: C, r: R });
+
+      if (typeof cell.v === 'number') cell.t = 'n';
+
+      else if (typeof cell.v === 'boolean') cell.t = 'b';
+
+      else if (cell.v instanceof Date) {
+
+        cell.t = 'n';
+
+        cell.z = XLSX.SSF._table[14];
+
+        cell.v = datenum(cell.v);
+
+      }
+
+      else cell.t = 's';
+
+      ws[cell_ref] = cell;
+
+    }
+
+  }
+
+  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
+
+  return ws;
+
+}
+
+function Workbook() {
+
+  if (!(this instanceof Workbook)) return new Workbook();
+
+  this.SheetNames = [];
+
+  this.Sheets = {};
+
+}
+
+function s2ab(s) {
+
+  var buf = new ArrayBuffer(s.length);
+
+  var view = new Uint8Array(buf);
+
+  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+
+  return buf;
+
+}
+
+export function export_table_to_excel(id) {
+
+  var theTable = document.getElementById(id);
+
+  console.log('a')
+
+  var oo = generateArray(theTable);
+
+  var ranges = oo[1];
+
+  /* original data */
+
+  var data = oo[0];
+
+  var ws_name = "SheetJS";
+
+  console.log(data);
+
+  var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
+
+  /* add ranges to worksheet */
+
+  // ws['!cols'] = ['apple', 'banan'];
+
+  ws['!merges'] = ranges;
+
+  /* add worksheet to workbook */
+
+  wb.SheetNames.push(ws_name);
+
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' });
+
+  saveAs(new Blob([s2ab(wbout)], { type: "application/octet-stream" }), "test.xlsx")
+
+}
+
+function formatJson(jsonData) {
+
+  console.log(jsonData)
+
+}
+
+export function export_json_to_excel(th, jsonData, defaultTitle) {
+
+  /* original data */
+
+  var data = jsonData;
+
+  data.unshift(th);
+
+  var ws_name = "Sheet";
+
+  var wb = new Workbook(), ws = sheet_from_array_of_arrays(data);
+
+  /*设置worksheet每列的最大宽度*/
+
+  const colWidth = data.map(row => row.map(val => {
+
+    /*先判断是否为null/undefined*/
+
+    if (val == null) {
+
+      return { 'wch': 10 };
+
+    }
+
+    /*再判断是否为中文*/
+
+    else if (val.toString().charCodeAt(0) > 255) {
+
+      return { 'wch': val.toString().length * 2 + 5 };
+
+    } else {
+
+      return { 'wch': val.toString().length + 5 };
+
+    }
+
+  }))
+
+  /*以第一行为初始值*/
+
+  let result = colWidth[0];
+
+  for (let i = 1; i < colWidth.length; i++) {
+
+    for (let j = 0; j < colWidth[i].length; j++) {
+
+      if (result[j]['wch'] < colWidth[i][j]['wch']) {
+
+        result[j]['wch'] = colWidth[i][j]['wch'];
+
+      }
+
+    }
+
+  }
+
+  ws['!cols'] = result;
+
+  /* add worksheet to workbook */
+
+  wb.SheetNames.push(ws_name);
+
+  wb.Sheets[ws_name] = ws;
+
+  var wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' });
+
+  var title = defaultTitle || '列表'
+
+  saveAs(new Blob([s2ab(wbout)], { type: "application/octet-stream" }), title + ".xlsx")
+
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 137 - 0
src/iconfonts/iconfont.css


BIN
src/iconfonts/iconfont.eot


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 0
src/iconfonts/iconfont.js


+ 219 - 0
src/iconfonts/iconfont.json

@@ -0,0 +1,219 @@
+{
+  "id": "1865865",
+  "name": "直播平台",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon",
+  "description": "直播后台",
+  "glyphs": [
+    {
+      "icon_id": "1810267",
+      "name": "关闭",
+      "font_class": "icon-test1",
+      "unicode": "e6e0",
+      "unicode_decimal": 59104
+    },
+    {
+      "icon_id": "9696382",
+      "name": "信用信息管理、信息管理类",
+      "font_class": "icon_xinyong_xianxing_jijin-",
+      "unicode": "e60a",
+      "unicode_decimal": 58890
+    },
+    {
+      "icon_id": "1831603",
+      "name": "联系我们",
+      "font_class": "lianxiwomen",
+      "unicode": "e64a",
+      "unicode_decimal": 58954
+    },
+    {
+      "icon_id": "43407",
+      "name": "专家",
+      "font_class": "111",
+      "unicode": "e602",
+      "unicode_decimal": 58882
+    },
+    {
+      "icon_id": "250687",
+      "name": "温馨提示-",
+      "font_class": "wenxintishi",
+      "unicode": "e6cc",
+      "unicode_decimal": 59084
+    },
+    {
+      "icon_id": "551592",
+      "name": "客服",
+      "font_class": "kefu",
+      "unicode": "e61a",
+      "unicode_decimal": 58906
+    },
+    {
+      "icon_id": "3745064",
+      "name": "会议",
+      "font_class": "icon-test",
+      "unicode": "e627",
+      "unicode_decimal": 58919
+    },
+    {
+      "icon_id": "8027282",
+      "name": "教育",
+      "font_class": "jiaoyu",
+      "unicode": "e61b",
+      "unicode_decimal": 58907
+    },
+    {
+      "icon_id": "9550452",
+      "name": "简介",
+      "font_class": "jianjie",
+      "unicode": "e616",
+      "unicode_decimal": 58902
+    },
+    {
+      "icon_id": "14893766",
+      "name": "主办方认证",
+      "font_class": "zhubanfangrenzheng",
+      "unicode": "e603",
+      "unicode_decimal": 58883
+    },
+    {
+      "icon_id": "15253802",
+      "name": "主办方",
+      "font_class": "zhubanfang",
+      "unicode": "e60f",
+      "unicode_decimal": 58895
+    },
+    {
+      "icon_id": "6338139",
+      "name": "个人中心",
+      "font_class": "gerenzhongxin",
+      "unicode": "e638",
+      "unicode_decimal": 58936
+    },
+    {
+      "icon_id": "9606662",
+      "name": "大讲堂",
+      "font_class": "dajiangtang",
+      "unicode": "e609",
+      "unicode_decimal": 58889
+    },
+    {
+      "icon_id": "2570115",
+      "name": "麦克风",
+      "font_class": "maikefeng-tianchong",
+      "unicode": "e64c",
+      "unicode_decimal": 58956
+    },
+    {
+      "icon_id": "15220134",
+      "name": "一键分享",
+      "font_class": "yijianfenxiang",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "573775",
+      "name": "分享",
+      "font_class": "fenxiang",
+      "unicode": "e63c",
+      "unicode_decimal": 58940
+    },
+    {
+      "icon_id": "7335647",
+      "name": "麦克风",
+      "font_class": "maikefeng",
+      "unicode": "eb4a",
+      "unicode_decimal": 60234
+    },
+    {
+      "icon_id": "8157938",
+      "name": "摄像头",
+      "font_class": "shexiangtou",
+      "unicode": "e625",
+      "unicode_decimal": 58917
+    },
+    {
+      "icon_id": "5076572",
+      "name": "状态",
+      "font_class": "zhuangtai",
+      "unicode": "e655",
+      "unicode_decimal": 58965
+    },
+    {
+      "icon_id": "5017103",
+      "name": "房间",
+      "font_class": "fangjian1",
+      "unicode": "e617",
+      "unicode_decimal": 58903
+    },
+    {
+      "icon_id": "1638357",
+      "name": "首页",
+      "font_class": "shouye",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    },
+    {
+      "icon_id": "3710184",
+      "name": "房间",
+      "font_class": "fangjian",
+      "unicode": "e613",
+      "unicode_decimal": 58899
+    },
+    {
+      "icon_id": "7738015",
+      "name": "用户",
+      "font_class": "yonghu",
+      "unicode": "e608",
+      "unicode_decimal": 58888
+    },
+    {
+      "icon_id": "11800270",
+      "name": "直播",
+      "font_class": "zhibo",
+      "unicode": "e60b",
+      "unicode_decimal": 58891
+    },
+    {
+      "icon_id": "12837797",
+      "name": "统计1",
+      "font_class": "tongji",
+      "unicode": "e64b",
+      "unicode_decimal": 58955
+    },
+    {
+      "icon_id": "14033570",
+      "name": "权限",
+      "font_class": "quanxian",
+      "unicode": "e7ae",
+      "unicode_decimal": 59310
+    },
+    {
+      "icon_id": "712103",
+      "name": "姓名",
+      "font_class": "icon-person",
+      "unicode": "e607",
+      "unicode_decimal": 58887
+    },
+    {
+      "icon_id": "1356727",
+      "name": "绑定",
+      "font_class": "bangding",
+      "unicode": "e72b",
+      "unicode_decimal": 59179
+    },
+    {
+      "icon_id": "14683074",
+      "name": "修改密码",
+      "font_class": "mima_huaban1",
+      "unicode": "e605",
+      "unicode_decimal": 58885
+    },
+    {
+      "icon_id": "8098828",
+      "name": "退出",
+      "font_class": "iconfront-",
+      "unicode": "e621",
+      "unicode_decimal": 58913
+    }
+  ]
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 116 - 0
src/iconfonts/iconfont.svg


BIN
src/iconfonts/iconfont.ttf


BIN
src/iconfonts/iconfont.woff


BIN
src/iconfonts/iconfont.woff2


+ 168 - 0
src/layout/layout-part/heads.vue

@@ -0,0 +1,168 @@
+<template>
+  <div id="heads">
+    <el-row>
+      <el-col :span="24" class="heads">
+        <el-col :span="12" class="left">
+          <el-image :src="afterInfo.logo"></el-image>
+          <span>{{ afterInfo.title }}</span>
+        </el-col>
+        <el-col :span="12" class="right">
+          <span
+            ><router-link style="color:red;text-decoration:none;" target="_blank" :to="{ path: '/livecheck' }" :underline="false"
+              ><i class="iconfont iconbangding"></i>直播检测</router-link
+            ></span
+          >
+          <span @click="passwdBtn()"><i class="iconfont iconmima_huaban1"></i>修改密码</span>
+          <span
+            ><i class="iconfont iconicon-person"></i>
+            <span v-if="user.id">
+              {{ user.name }}
+            </span>
+            <span v-else>
+              <el-link href="/login" :underline="false">登录</el-link>
+            </span>
+          </span>
+          <span @click="logoutBtn()"><i class="iconfont iconiconfront-"></i>退出登录</span>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog title="绑定" :visible.sync="bindDia" width="30%" :before-close="handleClose">
+      <bind @bindDown="bindDown"></bind>
+    </el-dialog>
+    <el-dialog title="修改密码" :visible.sync="passwdDia" width="30%" :before-close="handleClose">
+      <passwdDias :form="form" @submitForm="submitForm"></passwdDias>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: login } = createNamespacedHelpers('login');
+import bind from './parts/bind.vue';
+import passwdDias from './parts/passwdDia.vue';
+export default {
+  name: 'heads',
+  props: {},
+  components: {
+    // 绑定微信
+    bind,
+    // 修改密码
+    passwdDias,
+  },
+  data: function() {
+    return {
+      afterInfo: {
+        logo: require('@/assets/logo.png'),
+        title: '管理后台',
+      },
+      // 绑定微信
+      bindDia: false,
+      // 修改密码
+      passwdDia: false,
+      form: {},
+    };
+  },
+  created() {},
+  methods: {
+    ...login({ loginUpdate: 'update', logout: 'logout' }),
+    // 绑定微信
+    bindBtn() {
+      this.bindDia = true;
+    },
+    // 绑定微信关闭
+    bindDown() {
+      this.bindDia = false;
+    },
+    // 修改密码
+    passwdBtn() {
+      this.passwdDia = true;
+    },
+    // 修改密码提交
+    submitForm({ data }) {
+      data.id = this.user.id;
+      let res = this.loginUpdate(data);
+      if (this.$checkRes(res)) {
+        this.passwdDia = false;
+        this.$message({
+          message: '修改密码成功',
+          type: 'success',
+        });
+        this.$router.push({ path: '/login' });
+      }
+    },
+    // 退出登录
+    logoutBtn() {
+      this.logout();
+      this.$router.push({ path: '/login' });
+    },
+
+    handleClose(done) {
+      done();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.left {
+  padding: 0 20px;
+  .el-image {
+    float: left;
+    width: 50px;
+    height: 50px;
+    margin: 5px 0 0 0;
+  }
+  span {
+    height: 64px;
+    line-height: 60px;
+    font-size: 30px;
+    color: #fff;
+    padding: 0 10px;
+    text-shadow: cornflowerblue 3px 3px 3px;
+    font-family: cursive;
+  }
+}
+.right {
+  float: right;
+  padding: 0 20px;
+  text-align: right;
+  height: 63px;
+  line-height: 63px;
+  span {
+    border-right: 1px solid #000;
+    padding: 0 15px;
+    color: #fff;
+    .iconfont {
+      margin: 0 5px 0 0;
+    }
+    .el-link.el-link--default {
+      color: #e6a23c;
+      font-size: 16px;
+      top: -2px;
+    }
+    .el-link.el-link--default:hover {
+      color: #409eff;
+    }
+  }
+  span:hover {
+    cursor: pointer;
+  }
+  span:last-child {
+    border-right: 0;
+  }
+}
+/deep/.el-link.el-link--default {
+  color: #000;
+  font-size: 16px;
+  top: -2px;
+}
+</style>

+ 109 - 0
src/layout/layout-part/menus.vue

@@ -0,0 +1,109 @@
+<template>
+  <div id="menus">
+    <el-row>
+      <el-col :span="24">
+        <!-- <el-menu :default-active="$route.path" background-color="#353852" text-color="#ffffff" active-text-color="#409eff" router>
+          <el-menu-item index="/">
+            <i class="iconfont iconshouye"></i>
+            <span slot="title">后台首页</span>
+          </el-menu-item>
+          <el-menu-item index="/user/index">
+            <i class="iconfont iconyonghu"></i>
+            <span slot="title">用户管理</span>
+          </el-menu-item>
+          <el-menu-item index="/role/index">
+            <i class="iconfont iconquanxian"></i>
+            <span slot="title">权限管理</span>
+          </el-menu-item>
+          <el-menu-item index="/live/index">
+            <i class="iconfont iconzhibo"></i>
+            <span slot="title">直播管理</span>
+          </el-menu-item>
+          <el-menu-item index="/room/index">
+            <i class="iconfont iconfangjian"></i>
+            <span slot="title">房间管理</span>
+          </el-menu-item>
+          <el-menu-item index="/stat/index">
+            <i class="iconfont icontongji"></i>
+            <span slot="title">统计管理</span>
+          </el-menu-item>
+        </el-menu> -->
+        <el-menu background-color="#353852" text-color="#ffffff" active-text-color="#409eff">
+          <template v-for="item in menu">
+            <template>
+              <el-menu-item :index="item.path" @click="selectMenu(item.path)" :key="item.name">
+                <i v-if="item.icon" :class="item.icon"></i>
+                <span v-if="item.name" slot="title">{{ item.name }}</span>
+              </el-menu-item>
+            </template>
+          </template>
+        </el-menu>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { index, user, role, live, room, question, stat, contact, meetingBrief, test } from '@/util/role_menu.js';
+import * as menus from '@/util/role_menu.js';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'menus',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      menu: [],
+    };
+  },
+  created() {},
+  methods: {
+    search() {
+      let menu = [];
+      if (this.user.role == '1') {
+        menu.push(index, user, role, room, question, contact, meetingBrief);
+      } else {
+        menu.push(index, live);
+      }
+      this.$set(this, `menu`, menu);
+    },
+    selectMenu(path) {
+      this.$router.push({ path: path });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    user: {
+      handler(val, oval) {
+        if (val && !_.isEqual(val, oval)) this.search();
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+// .el-menu-vertical-demo:not(.el-menu--collapse) {
+//   width: 200px;
+//   min-height: 400px;
+// }
+/deep/.el-menu {
+  border-right: none;
+}
+/deep/.el-menu-item {
+  font-size: 18px;
+}
+/deep/.el-menu-item i {
+  padding: 0 10px;
+  font-size: 18px;
+}
+</style>

+ 39 - 0
src/layout/layout-part/parts/bind.vue

@@ -0,0 +1,39 @@
+<template>
+  <div id="bind">
+    <el-row>
+      <el-col :span="24">
+        <p>绑定微信</p>
+        <el-button @click="bindDown()">关闭</el-button>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'bind',
+  props: {},
+  components: {},
+  data: function() {
+    return {};
+  },
+  created() {},
+  methods: {
+    bindDown() {
+      this.$emit('bindDown');
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 63 - 0
src/layout/layout-part/parts/passwdDia.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="passwdDia">
+    <el-row>
+      <el-col :span="24">
+        <el-form :model="form" :rules="rules" ref="form" label-width="100px" class="demo-ruleForm">
+          <el-form-item label="旧密码" prop="oldpasswd">
+            <el-input v-model="form.oldpasswd" placeholder="请输入旧密码" show-password></el-input>
+          </el-form-item>
+          <el-form-item label="新密码" prop="newpasswd">
+            <el-input v-model="form.newpasswd" placeholder="请输入新密码" show-password></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="submitForm('form')">保存</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: login } = createNamespacedHelpers('login');
+export default {
+  name: 'passwdDia',
+  props: {
+    form: null,
+  },
+  components: {},
+  data: function() {
+    return {
+      rules: {
+        oldpasswd: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
+        newpasswd: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {},
+  methods: {
+    submitForm(formName) {
+      this.$refs[formName].validate(async valid => {
+        if (valid) {
+          this.$emit('submitForm', { data: this.form });
+        } else {
+          console.log('error submit!!');
+          return false;
+        }
+      });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 258 - 0
src/layout/live/detailInfo copy.vue

@@ -0,0 +1,258 @@
+<template>
+  <div id="detailInfo">
+    <el-row>
+      <el-col :span="24" class="info">
+        <el-col :span="14" class="left">
+          <el-col :span="24" class="top">
+            <el-col :span="4" class="image">
+              <el-image :src="roomInfo.filedir"></el-image>
+            </el-col>
+            <el-col :span="20" class="title">
+              <p class="one">
+                <span>{{ roomInfo.title }}</span>
+                <span>房间号:{{ roomInfo.name }}</span>
+              </p>
+              <div class="two">
+                <el-col :span="16" class="select">
+                  <el-select v-model="cameraId" filterable placeholder="请选择摄像头">
+                    <el-option v-for="item in cameras" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+                  </el-select>
+                  <el-select v-model="microphoneId" filterable placeholder="请选择麦克风">
+                    <el-option v-for="item in microphones" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+                  </el-select>
+                </el-col>
+                <el-col :span="8" class="btn">
+                  <span @click="shareon"><i class="iconfont iconfenxiang"></i>分享</span>
+                  <span @click="liveon"><i class="iconfont iconshexiangtou"></i>摄像头</span>
+                  <span @click="liveclose">关闭</span>
+                </el-col>
+              </div>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="video">
+            <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+          </el-col>
+        </el-col>
+        <el-col :span="10" class="right">
+          <el-col :span="24" class="chat">
+            聊天页面
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: gensign } = createNamespacedHelpers('gensign');
+import TRTC from 'trtc-js-sdk';
+export default {
+  name: 'detailInfo',
+  props: {
+    roomInfo: null,
+  },
+  components: {},
+  data: function() {
+    return {
+      client_: '',
+      localStream_: '',
+      sdkAppId_: '1400380125',
+      cameras: [],
+      microphones: [],
+      cameraId: '',
+      microphoneId: '',
+      userId_: '1111',
+      open_: false,
+    };
+  },
+  created() {
+    this.initclient();
+    this.getDevices();
+  },
+  methods: {
+    ...gensign(['gensignFetch']),
+    async getDevices() {
+      this.cameras = await TRTC.getCameras();
+      this.microphones = await TRTC.getMicrophones();
+    },
+    async initclient() {
+      console.log(this.user.uid);
+      this.userId_ = this.user.uid;
+      const res = await this.gensignFetch({ userid: this.userId_ });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.client_ = TRTC.createClient({
+          mode: 'live',
+          sdkAppId: this.sdkAppId_,
+          userId: this.userId_,
+          userSig: res.data,
+        });
+      }
+    },
+    async liveon() {
+      this.open_ = true;
+      console.log('8888--' + this.userId_);
+      if (this.cameraId === '' || this.microphoneId === '') {
+        this.$message({
+          message: '请选择摄像头和麦克风',
+          type: 'warning',
+        });
+        return;
+      }
+      await this.client_.join({ roomId: this.name, role: 'anchor' });
+      this.localStream_ = await TRTC.createStream({
+        audio: true,
+        video: true,
+        cameraId: this.cameraId,
+        microphoneId: this.microphoneId,
+        userId: this.userId_,
+      });
+      await this.localStream_.initialize();
+      console.log('initialize local stream success');
+      // publish the local stream
+      await this.client_.publish(this.localStream_);
+      this.localStream_.play('main-video');
+      //$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
+    },
+    async shareon() {
+      const shareId = 'share-' + this.userId_;
+      const res = await this.gensignFetch({ userid: shareId });
+      if (this.$checkRes(res)) {
+        const shareClient = TRTC.createClient({
+          mode: 'videoCall',
+          sdkAppId: this.sdkAppId_,
+          userId: shareId,
+          userSig: res.data,
+        });
+        shareClient.setDefaultMuteRemoteStreams(true);
+        await shareClient.join({ roomId: this.name });
+        const localStream = TRTC.createStream({ audio: false, screen: true });
+        await localStream.initialize();
+        console.log('initialize share stream success');
+        await shareClient.publish(localStream);
+        this.client_.on('stream-added', event => {
+          const remoteStream = event.stream;
+          const remoteUserId = remoteStream.getUserId();
+          if (remoteUserId === shareId) {
+            // 取消订阅自己的屏幕分享流
+            this.client_.unsubscribe(remoteStream);
+          } else {
+            // 订阅其他一般远端流
+            this.client_.subscribe(remoteStream);
+          }
+        });
+      }
+    },
+    async liveclose() {
+      // 关闭视频通话
+      console.log(this.open_);
+      if (this.open_) {
+        const videoTrack = this.localStream_.getVideoTrack();
+        if (videoTrack) {
+          this.localStream_.removeTrack(videoTrack).then(() => {
+            console.log('remove video call success');
+            // 关闭摄像头
+            videoTrack.stop();
+            this.client_.unpublish(localStream).then(() => {
+              // 取消发布本地流成功
+            });
+          });
+        }
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.info {
+  border-style: double;
+  border-color: #ff0000 #00ff00 #0000ff rgb(250, 0, 255);
+  padding: 20px;
+  .left {
+    background-color: #f5f5f5;
+    min-height: 800px;
+    border: 1px solid blueviolet;
+    .top {
+      position: relative;
+      border: 1px solid red;
+      padding: 10px;
+      .image {
+        text-align: center;
+        .el-image {
+          width: 80px;
+          height: 80px;
+          border-radius: 90px;
+        }
+      }
+      .title {
+        .one {
+          padding: 0 0 10px 0;
+          span {
+            display: inline-block;
+            width: 50%;
+          }
+          span:last-child {
+            text-align: right;
+          }
+        }
+        .two {
+          position: absolute;
+          right: 10px;
+          bottom: 10px;
+          .select {
+            .el-select {
+              float: left;
+              width: 47%;
+              margin: 0 10px 0 0;
+            }
+          }
+          .btn {
+            height: 40px;
+            line-height: 40px;
+            text-align: right;
+            span {
+              margin: 0 10px 0 0;
+            }
+            span:last-child {
+              margin: 0;
+            }
+            span:hover {
+              cursor: pointer;
+            }
+          }
+        }
+      }
+    }
+  }
+  .right {
+    width: 39%;
+    min-height: 800px;
+    border: 1px solid cyan;
+    margin: 0 0 0 20px;
+  }
+  #main-video {
+    float: left;
+    width: 100%;
+    height: 600px;
+    min-height: 600px;
+    grid-area: 1/1/3/4;
+  }
+}
+</style>

+ 801 - 0
src/layout/live/detailInfo.vue

@@ -0,0 +1,801 @@
+<template>
+  <div id="detailInfo">
+    <el-row>
+      <el-col :span="24" class="info">
+        <el-col :span="4" class="left">
+          <el-col :span="24" class="leftTop">
+            <el-image :src="roomdetail.filedir"></el-image>
+            <p>{{ roomdetail.title }}</p>
+            <p>{{ roomdetail.content }}</p>
+          </el-col>
+          <el-col :span="24" class="leftDown">
+            <el-col :span="8" class="btn" @click.native="shexiangBtn()">
+              <i class="iconfont iconshexiangtou"></i>
+              <p>摄像头</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="tianchongBtn()">
+              <i class="iconfont iconmaikefeng-tianchong"></i>
+              <p>麦克风</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="chatBtn()">
+              <i class="el-icon-user"></i>
+              <p>聊天</p>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="leftDown">
+            <el-col :span="8" class="btn" @click.native="lookuserBtn()">
+              <i class="el-icon-user"></i>
+              <p>成员</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="queBtn()">
+              <i class="el-icon-question"></i>
+              <p>问卷</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="queCloseBtn()">
+              <i class="el-icon-circle-close"></i>
+              <p>停卷</p>
+            </el-col>
+          </el-col>
+        </el-col>
+        <el-col :span="20" class="right">
+          <el-col :span="24" class="rightTop">
+            <span @click="liveon"><i class="iconfont iconshexiangtou"></i>直播</span>
+            <span v-show="zjrshow" @click="livezhuchi"><i class="iconfont iconshexiangtou"></i>主持</span>
+            <span @click="liveclose"><i class="el-icon-switch-button"></i>关闭直播</span>
+            <span @click="shareon"><i class="iconfont iconfenxiang"></i>屏幕</span>
+            <span @click="shareclose"><i class="iconfont iconfenxiang"></i>关闭分享</span>
+            <span><el-switch @change="recordclick" v-model="isrecord" active-text="录制" inactive-text="停录"> </el-switch></span>
+            <span @click="dismissroomClick"><i class="iconfont iconfenxiang"></i>解散房间</span>
+          </el-col>
+          <el-col :span="2" class="noVideo"> </el-col>
+          <el-col :span="20" class="video">
+            <el-col :span="24" class="videoMeet">
+              <el-col :span="18" class="one">
+                <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="two">
+                <el-col v-show="zjrshow" :span="24" class="twoOne" v-for="(item, index) in zjrList" :key="index">
+                  <el-col :span="14">
+                    <div :id="forOtherId(item.zjrid)" class="video-box col-div othevideo" style="justify-content: flex-end"></div>
+                  </el-col>
+                  <el-col :span="10">
+                    <p>
+                      <span>{{ item.zjrname }}</span>
+                      <span>
+                        <el-button type="danger" size="mini" @click="zjrChange(item.zjrid)">主讲</el-button>
+                      </span>
+                    </p>
+                  </el-col>
+                </el-col>
+              </el-col>
+            </el-col>
+          </el-col>
+          <el-col :span="2" class="noVideo"></el-col>
+          <el-col :span="24" class="rightDown">
+            <!-- 开始直播 -->
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog title="摄像头" :visible.sync="shexiangDia" width="30%" :before-close="handleClose">
+      <el-select @change="cameraChange" v-model="cameraId" filterable placeholder="请选择摄像头">
+        <el-option v-for="item in cameras" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="麦克风" :visible.sync="tianchongDia" width="30%" :before-close="handleClose">
+      <el-select @change="micrChange" v-model="microphoneId" filterable placeholder="请选择麦克风">
+        <el-option v-for="item in microphones" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="讨论" :visible.sync="chatDia" width="50%" :before-close="handleClose">
+      <el-row>
+        <el-col :span="24" class="chatList">
+          <el-col :span="24" class="list" v-for="(item, index) in dataList" :key="index">
+            <p>
+              <span :class="item.sendname == user.name ? 'selfColor' : ''">{{ item.sendname }}</span>
+              <span>{{ item.content }}</span>
+            </p>
+          </el-col>
+        </el-col>
+        <el-col :span="24" class="chatInput">
+          <el-col :span="19" class="input">
+            <el-input type="textarea" maxlength="5000" show-word-limit v-model="content"></el-input>
+          </el-col>
+          <el-col :span="5" class="btn">
+            <el-button type="primary" size="mini" @click="chatCreate">发送</el-button>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+    <el-dialog title="问卷" :visible.sync="queDia" width="38%" :before-close="handleClose">
+      <el-row>
+        <el-col :span="24">
+          <el-col :span="12">
+            <el-select v-model="queid" filterable placeholder="请选择问卷">
+              <el-option v-for="item in questList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+            </el-select>
+          </el-col>
+          <el-col :span="12" class="btn">
+            <el-button type="primary" size="mini" @click="queCreate">发送</el-button>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+    <el-dialog title="成员" :visible.sync="lookuserDia" width="60%" height="450px" :before-close="handleClose" :close-on-click-modal="(clo = false)">
+      <el-row>
+        <el-col :span="24" class="sudoku_row">
+          <el-col :span="24" class="sudoku_item" v-for="(item, index) in userList" :key="index">
+            <div :id="forId(item.userid)" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+            <p>
+              <i class="el-icon-user"></i>
+              <span class="selfColor">{{ item.username }}({{ item.isonline === '1' ? '在线' : '离线' }})</span>
+              <span v-if="item.switchrole === 'anchor'">
+                <el-button type="danger" size="mini" @click="lookuserUpdate(item.id, 'audience')">移除</el-button>
+                <el-button type="primary" size="mini" @click="roomshangmai(item.userid)">主讲</el-button>
+              </span>
+              <span v-else><el-button type="primary" size="mini" @click="lookuserUpdate(item.id, 'anchor')">连麦</el-button></span>
+            </p>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: gensign } = createNamespacedHelpers('gensign');
+const { mapActions: chat } = createNamespacedHelpers('chat');
+const { mapActions: room } = createNamespacedHelpers('room');
+const { mapActions: lookuser } = createNamespacedHelpers('lookuser');
+const { mapActions: roomuser } = createNamespacedHelpers('roomuser');
+import TRTC from 'trtc-js-sdk';
+export default {
+  name: 'detailInfo',
+  props: {
+    roomInfo: null,
+  },
+  components: {},
+  data: function() {
+    return {
+      // 摄像头
+      shexiangDia: false,
+      cameraId: '',
+      cameras: [],
+      // 麦克风
+      tianchongDia: false,
+      chatDia: false,
+      queDia: false,
+      questList: [],
+      queid: '',
+      microphoneId: '',
+      microphones: [],
+      client_: '',
+      localStream_: '',
+      shareClient_: '',
+      shareStream_: '',
+      sdkAppId_: '1400414461',
+      userId_: '',
+      userMainId_: '',
+      open_: false,
+      content: '',
+      dataList: [],
+      isrecord: false,
+      shareid: '',
+      userList: [],
+      lookuserDia: false,
+      index_: 0,
+      ov1: '',
+      ov2: '',
+      ov3: '',
+      ov4: '',
+      ov5: '',
+      ov6: '',
+      ov7: '',
+      showbtn_: false,
+      roomdetail: {},
+      zjrList: [],
+      zjrshow: false,
+    };
+  },
+  created() {
+    this.getRoomInfo();
+    this.initclient();
+    this.getDevices();
+    this.chatSearch();
+  },
+  mounted() {
+    this.channel();
+  },
+  methods: {
+    ...gensign(['gensignFetch']),
+    ...chat(['query', 'create', 'fetch']),
+    ...room({
+      roomfetch: 'fetch',
+      startrecord: 'startrecord',
+      stoprecord: 'stoprecord',
+      roomquest: 'roomquest',
+      roomquestclose: 'roomquestclose',
+      questquery: 'questquery',
+      updateanchor: 'updateanchor',
+      updateshmai: 'updateshmai',
+      switchzjr: 'switchzjr',
+      switchzb: 'switchzb',
+      switchzp: 'switchzp',
+      dismissroom: 'dismissroom',
+    }),
+    ...lookuser(['lookquery', 'lookupdate']),
+    ...roomuser({ roomuserfetch: 'fetch' }),
+    // 解散房间
+    async dismissroomClick() {
+      const info = { roomid: this.id, roomname: this.name };
+      let res = await this.dismissroom({ ...info });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async livezp() {
+      const data = {};
+      data.id = this.id;
+      data.uid = this.user.uid;
+      const res = await this.switchzb(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async livezhuchi() {
+      const data = {};
+      data.id = this.id;
+      data.uid = this.user.uid;
+      const res = await this.switchzb(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.localStream_.unmuteVideo();
+        this.localStream_.unmuteAudio();
+      }
+    },
+    async zjrChange(zjrid) {
+      const data = {};
+      data.id = this.id;
+      data.switchzjr = zjrid;
+      const res = await this.switchzjr(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.localStream_.muteVideo();
+        this.localStream_.muteAudio();
+      }
+    },
+    async getRoomInfo() {
+      const res = await this.roomfetch(this.id);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$set(this, `roomdetail`, res.data);
+        if (res.data.anchorid === this.user.uid) {
+          this.zjrshow = true;
+        }
+        for (const elm of res.data.zjr) {
+          const ru = await this.roomuserfetch(elm);
+          if (this.$checkRes(ru)) {
+            const newdata = { zjrid: elm, zjrname: ru.data.name };
+            this.zjrList.push(newdata);
+          }
+        }
+      }
+    },
+    async roomshangmai(dataid) {
+      const data = {};
+      data.id = this.id;
+      data.shmaiid = dataid;
+      const res = await this.updateshmai(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async queCreate() {
+      const data = {};
+      data.roomid = this.id;
+      data.queid = this.queid;
+      const res = await this.roomquest(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async lookuserBtn() {
+      this.lookuserDia = true;
+      this.lookuserSearch();
+    },
+    async lookuserSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.lookquery({ skip, limit, ...info });
+      console.log(res.data);
+      this.$set(this, `userList`, res.data);
+    },
+    async lookuserUpdate(_id, _switchrole) {
+      console.log(_id);
+      let data = {};
+      data.id = _id;
+      data.switchrole = _switchrole;
+      const res = await this.lookupdate(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.lookuserSearch();
+      } else {
+        this.$message.error(res.errmsg);
+      }
+    },
+    async recordclick() {
+      console.log(this.isrecord);
+      if (this.isrecord) {
+        const info = { roomid: this.id, roomname: this.name, shareid: this.shareid };
+        let res = await this.startrecord({ ...info });
+      } else {
+        const info = { roomid: this.id, roomname: this.name };
+        let res = await this.stoprecord({ ...info });
+      }
+    },
+    async chatSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.query({ skip, limit, ...info });
+      this.$set(this, `dataList`, res.data);
+    },
+    async questSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { status: '1' };
+      let res = await this.questquery({ skip, limit, ...info });
+      console.log(res);
+      if (this.$checkRes(res)) {
+        this.$set(this, `questList`, res.data);
+      }
+    },
+    async chatCreate() {
+      let data = {};
+      data.roomid = this.id;
+      data.type = '0';
+      data.content = this.content;
+      data.sendid = this.user.uid;
+      data.sendname = this.user.name;
+      const res = await this.create(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.content = '';
+      }
+    },
+    channel() {
+      console.log('in function:');
+      this.$stomp({
+        [`/exchange/public_chat_` + this.id]: this.onMessage,
+      });
+    },
+    onMessage(message) {
+      // console.log('receive a message: ', message.body);
+      let body = _.get(message, 'body');
+      if (body) {
+        body = JSON.parse(body);
+        this.dataList.push(body);
+        this.content = '';
+      }
+      // const { content, contenttype, sendid, sendname, icon, groupid, sendtime, type } = message.headers;
+      // let object = { content, contenttype, sendid, sendname, icon, groupid, sendtime, type };
+      // this.list.push(object);
+    },
+    async getDevices() {
+      this.cameras = await TRTC.getCameras();
+      this.microphones = await TRTC.getMicrophones();
+    },
+    async initclient() {
+      console.log(this.user.uid);
+      this.userId_ = this.user.uid;
+      if (this.anchorid === this.user.uid) {
+        this.showbtn_ = true;
+        this.userMainId_ = 'mainr-' + this.user.uid;
+      } else {
+        this.userMainId_ = 'other-' + this.user.uid;
+      }
+      const res = await this.gensignFetch({ userid: this.userMainId_ });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.client_ = TRTC.createClient({
+          mode: 'live',
+          sdkAppId: this.sdkAppId_,
+          userId: this.userMainId_,
+          userSig: res.data,
+        });
+      }
+    },
+    async liveon() {
+      this.open_ = true;
+      console.log('8888--' + this.userId_);
+      if (this.cameraId === '' || this.microphoneId === '') {
+        this.$message({
+          message: '请选择摄像头和麦克风',
+          type: 'warning',
+        });
+        return;
+      }
+      await this.client_.join({ roomId: this.name, role: 'anchor' });
+      this.localStream_ = await TRTC.createStream({
+        audio: true,
+        video: true,
+        cameraId: this.cameraId,
+        microphoneId: this.microphoneId,
+        userId: this.userMainId_,
+      });
+      this.localStream_.setVideoProfile('480p');
+      await this.localStream_.initialize();
+      console.log('initialize local stream success');
+      // publish the local stream
+      await this.client_.publish(this.localStream_);
+      this.localStream_.play('main-video');
+      //$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
+
+      // 订阅其他用户音视频
+      this.client_.on('stream-subscribed', event => {
+        const remoteStream = event.stream;
+        // 远端流订阅成功,播放远端音视频流
+        const usertempid_ = remoteStream.getUserId();
+        console.log('111' + remoteStream.getUserId());
+        if (usertempid_) {
+          const usersplit_ = usertempid_.substring(0, 5);
+          if (usersplit_ === 'other') {
+            const id_ = 'othe-video-' + usertempid_;
+            remoteStream.play(id_);
+          } else if (usersplit_ === 'wxxcx') {
+            const id_ = 'look-video-' + usertempid_;
+            remoteStream.play(id_);
+          } else if (usersplit_ === 'meetu') {
+            const id_ = 'look-video-' + usertempid_;
+            remoteStream.play(id_);
+          }
+        }
+      });
+      // 监听远端流增加事件
+      this.client_.on('stream-added', event => {
+        const remoteStream = event.stream;
+        console.log('222' + remoteStream.getType());
+        // 订阅远端音频和视频流
+        this.client_.subscribe(remoteStream, { audio: true, video: true }).catch(e => {
+          console.error('failed to subscribe remoteStream');
+        });
+      });
+      this.client_.on('stream-removed', event => {
+        const remoteStream = event.stream;
+        console.log('stop----');
+        const usertempid_ = remoteStream.getUserId();
+        if (usertempid_) {
+          const usersplit_ = usertempid_.substring(0, 5);
+          if (usersplit_ === 'other') {
+            this.index_ = this.index_ - 1;
+          }
+        }
+        remoteStream.stop();
+      });
+      this.client_.on('mute-video', event => {
+        const remoteStream = event.stream;
+        // 订阅远端音频和视频流
+        const usertempid_ = remoteStream.getUserId();
+        if (usertempid_) {
+          const usersplit_ = usertempid_.substring(0, 4);
+          if (usersplit_ === 'othe') {
+            this.index_ = this.index_ - 1;
+          }
+        }
+      });
+    },
+    async shareon() {
+      const shareId = 'share-' + this.userId_;
+      this.shareid = shareId;
+      const res = await this.gensignFetch({ userid: shareId });
+      if (this.$checkRes(res)) {
+        const shareClient = TRTC.createClient({
+          mode: 'videoCall',
+          sdkAppId: this.sdkAppId_,
+          userId: shareId,
+          userSig: res.data,
+        });
+        this.shareClient_ = shareClient;
+        shareClient.setDefaultMuteRemoteStreams(true);
+        await shareClient.join({ roomId: this.name });
+        const localStream = TRTC.createStream({ audio: false, screen: true });
+        //localStream.setScreenProfile({ width: 200, height: 200, float: 'left', frameRate: 5, bitrate: 1600 /* kbps */ });
+        await localStream.initialize();
+        this.shareStream_ = localStream;
+        console.log('initialize share stream success');
+        await shareClient.publish(localStream);
+        this.client_.on('stream-added', event => {
+          const remoteStream = event.stream;
+          const remoteUserId = remoteStream.getUserId();
+          if (remoteUserId === shareId) {
+            // 取消订阅自己的屏幕分享流
+            this.client_.unsubscribe(remoteStream);
+          } else {
+            // 订阅其他一般远端流
+            this.client_.subscribe(remoteStream);
+          }
+        });
+      }
+    },
+    async liveclose() {
+      // 关闭视频通话
+      console.log(this.open_);
+      if (this.open_) {
+        const videoTrack = this.localStream_.getVideoTrack();
+        if (videoTrack) {
+          this.localStream_.removeTrack(videoTrack).then(() => {
+            console.log('remove video call success');
+            // 关闭摄像头
+            videoTrack.stop();
+            this.client_.unpublish(this.localStream_).then(() => {
+              // 取消发布本地流成功
+            });
+            this.localStream_.close();
+          });
+        }
+      }
+    },
+    async shareclose() {
+      this.shareClient_;
+      if (this.shareClient_) {
+        this.shareClient_.unpublish(this.shareStream_).then(() => {
+          // 关闭屏幕分享流
+          this.shareStream_.close();
+        });
+      }
+    },
+    async cameraChange() {
+      //await this.localStream_.switchDevice('video', this.cameraId);
+    },
+    async micrChange() {
+      //await this.localStream_.switchDevice('audio', this.microphoneId);
+    },
+    // 选择打开摄像头
+    shexiangBtn() {
+      this.shexiangDia = true;
+    },
+    // 选择打开麦克风
+    tianchongBtn() {
+      this.tianchongDia = true;
+    },
+    chatBtn() {
+      this.chatDia = true;
+    },
+    async queBtn() {
+      this.queDia = true;
+      this.questSearch();
+    },
+    async queCloseBtn() {
+      // 关闭问卷
+      const data = {};
+      data.roomid = this.id;
+      const res = await this.roomquestclose(data);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    forId(itemid) {
+      return 'look-video-wxxcx-' + itemid;
+    },
+    forOtherId(itemid) {
+      return 'othe-video-other-' + itemid;
+    },
+    // 关闭摄像头&麦克风
+    handleClose(done) {
+      done();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    anchorid() {
+      return this.$route.query.anchorid;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.info {
+  background-color: #2a2b30;
+  min-height: 840px;
+  .left {
+    .leftTop {
+      min-height: 540px;
+      padding: 0 15px;
+      margin: 20px 0;
+      p {
+        color: #ccc;
+      }
+      p:nth-child(2) {
+        padding: 10px 0;
+      }
+      p:nth-child(3) {
+        line-height: 25px;
+      }
+    }
+    .leftDown {
+      padding: 10px 0 0 0;
+      border-top: 1px solid #000;
+      .btn {
+        margin: 0 0 15px 0;
+        color: #cccccc;
+        text-align: center;
+      }
+      .btn:hover {
+        cursor: pointer;
+      }
+    }
+  }
+  .right {
+    .rightTop {
+      height: 100px;
+      background-color: #232428;
+      text-align: right;
+      color: #ccc;
+      line-height: 100px;
+      span {
+        margin: 0 10px 0 0;
+      }
+      span:hover {
+        cursor: pointer;
+        color: #409eff;
+      }
+    }
+    .video {
+      min-height: 640px;
+      background-color: #000;
+      overflow: hidden;
+      position: relative;
+      .videoMeet {
+        .one {
+          height: 480px;
+          overflow: hidden;
+        }
+        .two {
+          height: 480px;
+          overflow: hidden;
+          background-color: white;
+          .twoOne {
+            height: 80px;
+            width: 100%;
+            overflow: hidden;
+            span:first-child {
+              width: 100%;
+              text-align: center;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+              // font-weight: bold;
+            }
+            span:last-child {
+              width: 100%;
+            }
+          }
+        }
+        .three {
+          height: 160px;
+          overflow: hidden;
+        }
+      }
+    }
+    .noVideo {
+      min-height: 640px;
+      background-color: #151618;
+    }
+    .rightDown {
+      height: 100px;
+      background-color: #232428;
+    }
+  }
+}
+/deep/.el-dialog__body {
+  min-height: 100px;
+}
+#main-video {
+  float: left;
+  width: 100%;
+  height: 640px;
+  min-height: 600px;
+  grid-area: 1/1/3/4;
+}
+.chatList {
+  height: 400px;
+  padding: 5px 5px 5px 0px;
+  overflow-y: auto;
+  .list {
+    margin: 0 0 10px 0;
+    span:first-child {
+      float: left;
+      width: 15%;
+      text-align: center;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      // font-weight: bold;
+    }
+    span:last-child {
+      float: right;
+      width: 84%;
+    }
+  }
+}
+.chatInput {
+  position: absolute;
+  bottom: 0;
+  .el-button {
+    width: 100%;
+    padding: 20px 0;
+  }
+}
+.sudoku_row {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 430px;
+  flex-wrap: wrap;
+  overflow-y: auto;
+}
+.sudoku_item {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  width: 30%;
+  padding: 10px 10px 10px 10px;
+}
+.lookvideo {
+  width: 100%;
+  height: 160px;
+  min-height: 160px;
+  background-color: black;
+  grid-area: 1/1/3/4;
+}
+.othevideo {
+  margin: 10px 10px 10px 10px;
+  width: 80px;
+  height: 80px;
+  min-height: 80px;
+  background-color: black;
+  grid-area: 1/1/3/4;
+}
+</style>

+ 757 - 0
src/layout/live/detailInfo_qili.vue

@@ -0,0 +1,757 @@
+<template>
+  <div id="detailInfo">
+    <el-row>
+      <el-col :span="24" class="info">
+        <el-col :span="4" class="left">
+          <el-col :span="24" class="leftTop">
+            <el-image :src="roomdetail.filedir"></el-image>
+            <p>{{ roomdetail.title }}</p>
+            <p>{{ roomdetail.content }}</p>
+          </el-col>
+          <el-col :span="24" class="leftDown">
+            <el-col :span="8" class="btn" @click.native="shexiangBtn()">
+              <i class="iconfont iconshexiangtou"></i>
+              <p>摄像头</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="tianchongBtn()">
+              <i class="iconfont iconmaikefeng-tianchong"></i>
+              <p>麦克风</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="chatBtn()">
+              <i class="el-icon-user"></i>
+              <p>聊天</p>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="leftDown">
+            <el-col :span="8" class="btn" @click.native="lookuserBtn()">
+              <i class="el-icon-user"></i>
+              <p>成员</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="queBtn()">
+              <i class="el-icon-question"></i>
+              <p>问卷</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="queCloseBtn()">
+              <i class="el-icon-circle-close"></i>
+              <p>停卷</p>
+            </el-col>
+          </el-col>
+        </el-col>
+        <el-col :span="20" class="right">
+          <el-col :span="24" class="rightTop">
+            <span @click="liveon"><i class="iconfont iconshexiangtou"></i>直播</span>
+            <span v-show="zjrshow" @click="livezhuchi"><i class="iconfont iconshexiangtou"></i>主持</span>
+            <span @click="liveclose"><i class="el-icon-switch-button"></i>关闭直播</span>
+            <span @click="shareon"><i class="iconfont iconfenxiang"></i>屏幕</span>
+            <span @click="shareclose"><i class="iconfont iconfenxiang"></i>关闭分享</span>
+            <span><el-switch @change="recordclick" v-model="isrecord" active-text="录制" inactive-text="停录"> </el-switch></span>
+            <span @click="dismissroomClick"><i class="iconfont iconfenxiang"></i>解散房间</span>
+          </el-col>
+          <el-col :span="2" class="noVideo"> </el-col>
+          <el-col :span="20" class="video">
+            <el-col :span="24" class="videoMeet">
+              <el-col :span="18" class="one">
+                <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="two">
+                <el-col v-show="zjrshow" :span="24" class="twoOne" v-for="(item, index) in zjrList" :key="index">
+                  <el-col :span="14">
+                    <div :id="forOtherId(item.zjrid)" class="video-box col-div othevideo" style="justify-content: flex-end"></div>
+                  </el-col>
+                  <el-col :span="10">
+                    <p>
+                      <span>{{ item.zjrname }}</span>
+                      <span>
+                        <el-button type="danger" size="mini" @click="zjrChange(item.zjrid)">主讲</el-button>
+                      </span>
+                    </p>
+                  </el-col>
+                </el-col>
+              </el-col>
+            </el-col>
+          </el-col>
+          <el-col :span="2" class="noVideo"></el-col>
+          <el-col :span="24" class="rightDown">
+            <!-- 开始直播 -->
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog title="摄像头" :visible.sync="shexiangDia" width="30%" :before-close="handleClose">
+      <el-select @change="cameraChange" v-model="cameraId" filterable placeholder="请选择摄像头">
+        <el-option v-for="item in cameras" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="麦克风" :visible.sync="tianchongDia" width="30%" :before-close="handleClose">
+      <el-select @change="micrChange" v-model="microphoneId" filterable placeholder="请选择麦克风">
+        <el-option v-for="item in microphones" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="讨论" :visible.sync="chatDia" width="50%" :before-close="handleClose">
+      <el-row>
+        <el-col :span="24" class="chatList">
+          <el-col :span="24" class="list" v-for="(item, index) in dataList" :key="index">
+            <p>
+              <span :class="item.sendname == user.name ? 'selfColor' : ''">{{ item.sendname }}</span>
+              <span>{{ item.content }}</span>
+            </p>
+          </el-col>
+        </el-col>
+        <el-col :span="24" class="chatInput">
+          <el-col :span="19" class="input">
+            <el-input type="textarea" maxlength="5000" show-word-limit v-model="content"></el-input>
+          </el-col>
+          <el-col :span="5" class="btn">
+            <el-button type="primary" size="mini" @click="chatCreate">发送</el-button>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+    <el-dialog title="问卷" :visible.sync="queDia" width="38%" :before-close="handleClose">
+      <el-row>
+        <el-col :span="24">
+          <el-col :span="12">
+            <el-select v-model="queid" filterable placeholder="请选择问卷">
+              <el-option v-for="item in questList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+            </el-select>
+          </el-col>
+          <el-col :span="12" class="btn">
+            <el-button type="primary" size="mini" @click="queCreate">发送</el-button>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+    <el-dialog title="成员" :visible.sync="lookuserDia" width="60%" height="450px" :before-close="handleClose" :close-on-click-modal="(clo = false)">
+      <el-row>
+        <el-col :span="24" class="sudoku_row">
+          <el-col :span="24" class="sudoku_item" v-for="(item, index) in userList" :key="index">
+            <div :id="forId(item.userid)" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+            <p>
+              <i class="el-icon-user"></i>
+              <span class="selfColor">{{ item.username }}({{ item.isonline === '1' ? '在线' : '离线' }})</span>
+              <span v-if="item.switchrole === 'anchor'">
+                <el-button type="danger" size="mini" @click="lookuserUpdate(item.id, 'audience')">移除</el-button>
+                <el-button type="primary" size="mini" @click="roomshangmai(item.userid)">主讲</el-button>
+              </span>
+              <span v-else><el-button type="primary" size="mini" @click="lookuserUpdate(item.id, 'anchor')">连麦</el-button></span>
+            </p>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: gensign } = createNamespacedHelpers('gensign');
+const { mapActions: chat } = createNamespacedHelpers('chat');
+const { mapActions: room } = createNamespacedHelpers('room');
+const { mapActions: lookuser } = createNamespacedHelpers('lookuser');
+const { mapActions: roomuser } = createNamespacedHelpers('roomuser');
+import TRTC from 'trtc-js-sdk';
+import * as QNRTC from 'pili-rtc-web';
+export default {
+  name: 'detailInfo',
+  props: {
+    roomInfo: null,
+  },
+  components: {},
+  data: function() {
+    return {
+      // 摄像头
+      shexiangDia: false,
+      cameraId: '',
+      cameras: [],
+      // 麦克风
+      tianchongDia: false,
+      chatDia: false,
+      queDia: false,
+      questList: [],
+      queid: '',
+      microphoneId: '',
+      microphones: [],
+      client_: '',
+      localStream_: '',
+      shareClient_: '',
+      shareStream_: '',
+      sdkAppId_: '1400380125',
+      userId_: '',
+      userMainId_: '',
+      open_: false,
+      content: '',
+      dataList: [],
+      isrecord: false,
+      shareid: '',
+      userList: [],
+      lookuserDia: false,
+      index_: 0,
+      ov1: '',
+      ov2: '',
+      ov3: '',
+      ov4: '',
+      ov5: '',
+      ov6: '',
+      ov7: '',
+      showbtn_: false,
+      roomdetail: {},
+      zjrList: [],
+      zjrshow: false,
+      myRoom_: '',
+      tracks_: [],
+      shareTracks_: '',
+      trackInfoList_: [],
+    };
+  },
+  created() {
+    this.getRoomInfo();
+    this.initclient();
+    this.getDevices();
+    this.chatSearch();
+  },
+  mounted() {
+    this.channel();
+  },
+  methods: {
+    ...gensign(['gensignFetch']),
+    ...chat(['query', 'create', 'fetch']),
+    ...room({
+      roomfetch: 'fetch',
+      startrecord: 'startrecord',
+      stoprecord: 'stoprecord',
+      roomquest: 'roomquest',
+      roomquestclose: 'roomquestclose',
+      questquery: 'questquery',
+      updateanchor: 'updateanchor',
+      updateshmai: 'updateshmai',
+      switchzjr: 'switchzjr',
+      switchzb: 'switchzb',
+      switchzp: 'switchzp',
+      dismissroom: 'dismissroom',
+    }),
+    ...lookuser(['lookquery', 'lookupdate']),
+    ...roomuser({ roomuserfetch: 'fetch' }),
+    // 解散房间
+    async dismissroomClick() {
+      // 遍历 tracks,逐个销毁释放
+      for (const track of this.tracks_) {
+        track.release();
+      }
+    },
+    async livezp() {
+      const data = {};
+      data.id = this.id;
+      data.uid = this.user.uid;
+      const res = await this.switchzb(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async livezhuchi() {
+      const data = {};
+      data.id = this.id;
+      data.uid = this.user.uid;
+      const res = await this.switchzb(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.localStream_.unmuteVideo();
+        this.localStream_.unmuteAudio();
+      }
+    },
+    async zjrChange(zjrid) {
+      const data = {};
+      data.id = this.id;
+      data.switchzjr = zjrid;
+      const res = await this.switchzjr(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        // 将所有 tracks 的 trackId 取出当作参数,取消发布
+        await this.myRoom_.unpublish(this.tracks_.map(track => track.info.trackId));
+      }
+    },
+    async getRoomInfo() {
+      const res = await this.roomfetch(this.id);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$set(this, `roomdetail`, res.data);
+        if (res.data.anchorid === this.user.uid) {
+          this.zjrshow = true;
+        }
+        for (const elm of res.data.zjr) {
+          const ru = await this.roomuserfetch(elm);
+          if (this.$checkRes(ru)) {
+            const newdata = { zjrid: elm, zjrname: ru.data.name };
+            this.zjrList.push(newdata);
+          }
+        }
+      }
+    },
+    async roomshangmai(dataid) {
+      const data = {};
+      data.id = this.id;
+      data.shmaiid = dataid;
+      const res = await this.updateshmai(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async queCreate() {
+      const data = {};
+      data.roomid = this.id;
+      data.queid = this.queid;
+      const res = await this.roomquest(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    async lookuserBtn() {
+      this.lookuserDia = true;
+      this.lookuserSearch();
+    },
+    async lookuserSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.lookquery({ skip, limit, ...info });
+      console.log(res.data);
+      this.$set(this, `userList`, res.data);
+    },
+    async lookuserUpdate(_id, _switchrole) {
+      console.log(_id);
+      let data = {};
+      data.id = _id;
+      data.switchrole = _switchrole;
+      const res = await this.lookupdate(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.lookuserSearch();
+      } else {
+        this.$message.error(res.errmsg);
+      }
+    },
+    async recordclick() {
+      console.log(this.isrecord);
+      if (this.isrecord) {
+        const info = { roomid: this.id, roomname: this.name, shareid: this.shareid };
+        let res = await this.startrecord({ ...info });
+      } else {
+        const info = { roomid: this.id, roomname: this.name };
+        let res = await this.stoprecord({ ...info });
+      }
+    },
+    async chatSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.query({ skip, limit, ...info });
+      this.$set(this, `dataList`, res.data);
+    },
+    async questSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { status: '1' };
+      let res = await this.questquery({ skip, limit, ...info });
+      console.log(res);
+      if (this.$checkRes(res)) {
+        this.$set(this, `questList`, res.data);
+      }
+    },
+    async chatCreate() {
+      let data = {};
+      data.roomid = this.id;
+      data.type = '0';
+      data.content = this.content;
+      data.sendid = this.user.uid;
+      data.sendname = this.user.name;
+      const res = await this.create(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.content = '';
+      }
+    },
+    channel() {
+      console.log('in function:');
+      this.$stomp({
+        [`/exchange/public_chat_` + this.id]: this.onMessage,
+      });
+    },
+    onMessage(message) {
+      // console.log('receive a message: ', message.body);
+      let body = _.get(message, 'body');
+      if (body) {
+        body = JSON.parse(body);
+        this.dataList.push(body);
+        this.content = '';
+      }
+      // const { content, contenttype, sendid, sendname, icon, groupid, sendtime, type } = message.headers;
+      // let object = { content, contenttype, sendid, sendname, icon, groupid, sendtime, type };
+      // this.list.push(object);
+    },
+    async getDevices() {
+      this.cameras = await TRTC.getCameras();
+      this.microphones = await TRTC.getMicrophones();
+    },
+    async initclient() {
+      console.log(this.user.uid);
+      this.userId_ = this.user.uid;
+      if (this.anchorid === this.user.uid) {
+        this.showbtn_ = true;
+        this.userMainId_ = 'mainr-' + this.user.uid;
+      } else {
+        this.userMainId_ = 'other-' + this.user.uid;
+      }
+      console.log('current version', QNRTC.version);
+      // 生成房间token
+      const res = await this.gensignFetch({ userid: this.userMainId_, roomname: this.name });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.myRoom_ = await new QNRTC.TrackModeSession();
+        await this.myRoom_.joinRoomWithToken(res.data);
+      }
+    },
+    async liveon() {
+      this.open_ = true;
+      console.log('8888--' + this.userId_);
+      this.tracks_ = await QNRTC.deviceManager.getLocalTracks({
+        audio: { enabled: true, tag: 'audio', trackId: 'audio_' + this.userId_ },
+        video: { enabled: true, tag: 'video', trackId: 'video_' + this.userId_ },
+      });
+      console.log(this.tracks_);
+      const domElement = document.getElementById('main-video');
+      for (const track of this.tracks_) {
+        if (track.info.tag === 'audio') {
+          continue;
+        } else if (track.info.tag === 'screen') {
+          continue;
+        }
+        track.play(domElement, true);
+      }
+      await this.myRoom_.publish(this.tracks_);
+      console.log('查看视频流');
+      console.log(this.tracks_);
+      this.autoSubscribe(this.myRoom_);
+    },
+
+    autoSubscribe(myRoom) {
+      const trackInfoList = myRoom.trackInfoList;
+      console.log('room current trackInfo list', trackInfoList);
+
+      // 调用我们刚刚编写的 subscribe 方法
+      // 注意这里我们没有使用 async/await,而是使用了 Promise,大家可以思考一下为什么
+      this.subscribe(myRoom, trackInfoList)
+        .then(() => console.log('subscribe success!'))
+        .catch(e => console.error('subscribe error', e));
+
+      // 添加事件监听,当房间中出现新的 Track 时就会触发,参数是 trackInfo 列表
+      myRoom.on('track-add', trackInfoList => {
+        console.log('get track-add event!', trackInfoList);
+        this.subscribe(myRoom, trackInfoList)
+          .then(() => console.log('subscribe success!'))
+          .catch(e => console.error('subscribe error', e));
+      });
+      // 就是这样,就像监听 DOM 事件一样通过 on 方法监听相应的事件并给出处理函数即可
+    },
+
+    async subscribe(myRoom, trackInfoList) {
+      // 通过传入 trackId 调用订阅方法发起订阅,成功会返回相应的 Track 对象,也就是远端的 Track 列表了
+      const remoteTracks = await myRoom.subscribe(trackInfoList.map(info => info.trackId));
+
+      // 选择页面上的一个元素作为父元素,播放远端的音视频轨
+      const remoteElement = document.getElementById('remotetracks');
+      // 遍历返回的远端 Track,调用 play 方法完成在页面上的播放
+      for (const remoteTrack of remoteTracks) {
+        // 取得详细用户信息
+        const usertempid_ = remoteTrack.userId;
+        if (usertempid_) {
+          const usersplit_ = usertempid_.substring(0, 5);
+          if (usersplit_ === 'other') {
+            const id_ = 'othe-video-' + usertempid_;
+            const remoteElement = document.getElementById(id_);
+            remoteTrack.play(remoteElement);
+          } else if (usersplit_ === 'wxxcx') {
+            const id_ = 'look-video-' + usertempid_;
+            const remoteElement = document.getElementById(id_);
+            remoteTrack.play(remoteElement);
+          } else if (usersplit_ === 'meetu') {
+            const id_ = 'look-video-' + usertempid_;
+            const remoteElement = document.getElementById(id_);
+            remoteTrack.play(remoteElement);
+          }
+        }
+      }
+    },
+
+    async shareon() {
+      this.shareTracks_ = await QNRTC.deviceManager.getLocalTracks({
+        screen: { enabled: true, tag: 'screen' },
+      });
+      console.log('my local shareTracks', this.shareTracks_);
+      // 将刚刚的 Track 列表发布到房间中
+      await this.myRoom_.publish(this.shareTracks_);
+    },
+    async liveclose() {
+      // 关闭视频通话
+      console.log('进入关闭方法');
+      if (this.tracks_) {
+        await this.myRoom_.unpublish();
+        this.open_ = false;
+        // 遍历 tracks,逐个销毁释放
+        for (const track of this.tracks_) {
+          track.release();
+        }
+      }
+    },
+    async shareclose() {
+      if (this.shareTracks_) {
+        await this.myRoom_.unpublish(this.shareTracks_.map(track => track.info.trackId));
+      }
+    },
+    async cameraChange() {
+      //await this.localStream_.switchDevice('video', this.cameraId);
+    },
+    async micrChange() {
+      //await this.localStream_.switchDevice('audio', this.microphoneId);
+    },
+    // 选择打开摄像头
+    shexiangBtn() {
+      this.shexiangDia = true;
+    },
+    // 选择打开麦克风
+    tianchongBtn() {
+      this.tianchongDia = true;
+    },
+    chatBtn() {
+      this.chatDia = true;
+    },
+    async queBtn() {
+      this.queDia = true;
+      this.questSearch();
+    },
+    async queCloseBtn() {
+      // 关闭问卷
+      const data = {};
+      data.roomid = this.id;
+      const res = await this.roomquestclose(data);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+      }
+    },
+    forId(itemid) {
+      return 'look-video-wxxcx-' + itemid;
+    },
+    forOtherId(itemid) {
+      return 'othe-video-other-' + itemid;
+    },
+    // 关闭摄像头&麦克风
+    handleClose(done) {
+      done();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    anchorid() {
+      return this.$route.query.anchorid;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.info {
+  background-color: #2a2b30;
+  min-height: 840px;
+  .left {
+    .leftTop {
+      min-height: 540px;
+      padding: 0 15px;
+      margin: 20px 0;
+      p {
+        color: #ccc;
+      }
+      p:nth-child(2) {
+        padding: 10px 0;
+      }
+      p:nth-child(3) {
+        line-height: 25px;
+      }
+    }
+    .leftDown {
+      padding: 10px 0 0 0;
+      border-top: 1px solid #000;
+      .btn {
+        margin: 0 0 15px 0;
+        color: #cccccc;
+        text-align: center;
+      }
+      .btn:hover {
+        cursor: pointer;
+      }
+    }
+  }
+  .right {
+    .rightTop {
+      height: 100px;
+      background-color: #232428;
+      text-align: right;
+      color: #ccc;
+      line-height: 100px;
+      span {
+        margin: 0 10px 0 0;
+      }
+      span:hover {
+        cursor: pointer;
+        color: #409eff;
+      }
+    }
+    .video {
+      min-height: 640px;
+      background-color: #000;
+      overflow: hidden;
+      position: relative;
+      .videoMeet {
+        .one {
+          height: 480px;
+          overflow: hidden;
+        }
+        .two {
+          height: 480px;
+          overflow: hidden;
+          background-color: white;
+          .twoOne {
+            height: 80px;
+            width: 100%;
+            overflow: hidden;
+            span:first-child {
+              width: 100%;
+              text-align: center;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+              // font-weight: bold;
+            }
+            span:last-child {
+              width: 100%;
+            }
+          }
+        }
+        .three {
+          height: 160px;
+          overflow: hidden;
+        }
+      }
+    }
+    .noVideo {
+      min-height: 640px;
+      background-color: #151618;
+    }
+    .rightDown {
+      height: 100px;
+      background-color: #232428;
+    }
+  }
+}
+/deep/.el-dialog__body {
+  min-height: 100px;
+}
+#main-video {
+  float: left;
+  width: 100%;
+  height: 640px;
+  min-height: 600px;
+  grid-area: 1/1/3/4;
+}
+.chatList {
+  height: 400px;
+  padding: 5px 5px 5px 0px;
+  overflow-y: auto;
+  .list {
+    margin: 0 0 10px 0;
+    span:first-child {
+      float: left;
+      width: 15%;
+      text-align: center;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      // font-weight: bold;
+    }
+    span:last-child {
+      float: right;
+      width: 84%;
+    }
+  }
+}
+.chatInput {
+  position: absolute;
+  bottom: 0;
+  .el-button {
+    width: 100%;
+    padding: 20px 0;
+  }
+}
+.sudoku_row {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 430px;
+  flex-wrap: wrap;
+  overflow-y: auto;
+}
+.sudoku_item {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  width: 30%;
+  padding: 10px 10px 10px 10px;
+}
+.lookvideo {
+  width: 100%;
+  height: 160px;
+  min-height: 160px;
+  background-color: black;
+  grid-area: 1/1/3/4;
+}
+.othevideo {
+  margin: 10px 10px 10px 10px;
+  width: 80px;
+  height: 80px;
+  min-height: 80px;
+  background-color: black;
+  grid-area: 1/1/3/4;
+}
+</style>

+ 486 - 0
src/layout/live/detailmetting.vue

@@ -0,0 +1,486 @@
+<template>
+  <div id="detailmetting">
+    <el-row>
+      <el-col :span="24" class="info">
+        <el-col :span="4" class="left">
+          <el-col :span="24" class="leftTop">
+            <el-image :src="roomInfo.filedir"></el-image>
+            <p>{{ roomInfo.title }}</p>
+            <p>{{ roomInfo.content }}</p>
+          </el-col>
+          <el-col :span="24" class="leftDown">
+            <el-col :span="8" class="btn" @click.native="shexiangBtn()">
+              <i class="iconfont iconshexiangtou"></i>
+              <p>摄像头</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="tianchongBtn()">
+              <i class="iconfont iconmaikefeng-tianchong"></i>
+              <p>麦克风</p>
+            </el-col>
+            <el-col :span="8" class="btn" @click.native="lookuserBtn()">
+              <i class="el-icon-user"></i>
+              <p>成员</p>
+            </el-col>
+          </el-col>
+        </el-col>
+        <el-col :span="20" class="right">
+          <el-col :span="24" class="rightTop">
+            <span @click="liveon"><i class="iconfont iconshexiangtou"></i>直播</span>
+            <span @click="liveclose">关闭</span>
+          </el-col>
+          <el-col :span="18" class="video">
+            <el-col :span="24" class="videoMeet">
+              <el-col :span="18" class="one">
+                <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="two">
+                <el-col :span="24" class="twoOne">
+                  <div id="look-video-1" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+                </el-col>
+                <el-col :span="24" class="twoOne">
+                  <div id="look-video-2" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+                </el-col>
+                <el-col :span="24" class="twoOne">
+                  <div id="look-video-3" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+                </el-col>
+              </el-col>
+              <el-col :span="6" class="three">
+                <div id="look-video-4" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="three">
+                <div id="look-video-5" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="three">
+                <div id="look-video-6" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+              </el-col>
+              <el-col :span="6" class="three">
+                <div id="look-video-7" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
+              </el-col>
+            </el-col>
+          </el-col>
+          <el-col :span="6" class="noVideo">
+            <el-col :span="24" class="chatList">
+              <el-col :span="24" class="list" v-for="(item, index) in dataList" :key="index">
+                <p>
+                  <span :class="item.sendname == user.name ? 'selfColor' : ''">{{ item.sendname }}</span>
+                  <span>{{ item.content }}</span>
+                </p>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="chatInput">
+              <el-col :span="19" class="input">
+                <el-input type="textarea" maxlength="5000" show-word-limit v-model="content"></el-input>
+              </el-col>
+              <el-col :span="5" class="btn">
+                <el-button type="primary" size="mini" @click="chatCreate">发送</el-button>
+              </el-col>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="rightDown"> <!-- 开始直播 --> </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog title="摄像头" :visible.sync="shexiangDia" width="30%" :before-close="handleClose">
+      <el-select @change="cameraChange" v-model="cameraId" filterable placeholder="请选择摄像头">
+        <el-option v-for="item in cameras" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="麦克风" :visible.sync="tianchongDia" width="30%" :before-close="handleClose">
+      <el-select @change="micrChange" v-model="microphoneId" filterable placeholder="请选择麦克风">
+        <el-option v-for="item in microphones" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
+      </el-select>
+    </el-dialog>
+    <el-dialog title="成员" :visible.sync="lookuserDia" width="30%" :before-close="handleClose">
+      <el-row>
+        <el-col :span="24" class="chatList">
+          <el-col :span="24" class="list" v-for="(item, index) in userList" :key="index">
+            <p>
+              <i class="el-icon-user"></i>
+              <span class="selfColor">{{ item.username }}</span>
+              <span v-if="item.switchrole === 'anchor'"
+                ><el-button type="primary" size="mini" @click="lookuserUpdate(item.id, 'audience')">移除</el-button></span
+              >
+              <span v-else><el-button type="primary" size="mini" @click="lookuserUpdate(item.id, 'anchor')">连麦</el-button></span>
+            </p>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: gensign } = createNamespacedHelpers('gensign');
+const { mapActions: chat } = createNamespacedHelpers('chat');
+const { mapActions: lookuser } = createNamespacedHelpers('lookuser');
+import TRTC from 'trtc-js-sdk';
+export default {
+  name: 'detailmetting',
+  props: {
+    roomInfo: null,
+  },
+  components: {},
+  data: function() {
+    return {
+      // 摄像头
+      shexiangDia: false,
+      cameraId: '',
+      cameras: [],
+      // 麦克风
+      tianchongDia: false,
+      microphoneId: '',
+      microphones: [],
+      client_: '',
+      localStream_: '',
+      sdkAppId_: '1400414461',
+      content: '',
+      userId_: '',
+      open_: false,
+      dataList: [],
+      index: 0,
+      userList: [],
+      lookuserDia: false,
+      isanchor: true,
+    };
+  },
+  created() {
+    this.initclient();
+    this.getDevices();
+    this.chatSearch();
+  },
+  mounted() {
+    this.channel();
+  },
+  methods: {
+    ...gensign(['gensignFetch']),
+    ...chat(['query', 'create', 'fetch']),
+    ...lookuser(['lookquery', 'lookupdate']),
+    async lookuserBtn() {
+      this.lookuserDia = true;
+      this.lookuserSearch();
+    },
+    async lookuserSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.lookquery({ skip, limit, ...info });
+      this.$set(this, `userList`, res.data);
+    },
+    async lookuserUpdate(_id, _switchrole) {
+      console.log(_id);
+      let data = {};
+      data.id = _id;
+      data.switchrole = _switchrole;
+      const res = await this.lookupdate(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$message({
+          message: '操作成功',
+          type: 'success',
+        });
+        this.lookuserSearch();
+      } else {
+        this.$message.error(res.errmsg);
+      }
+    },
+    async chatSearch({ skip = 0, limit = 1000 } = {}) {
+      const info = { roomid: this.id };
+      let res = await this.query({ skip, limit, ...info });
+      this.$set(this, `dataList`, res.data);
+    },
+    async chatCreate() {
+      let data = {};
+      data.roomid = this.id;
+      data.type = '1';
+      data.content = this.content;
+      data.sendid = this.user.uid;
+      data.sendname = this.user.name;
+      const res = await this.create(data);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+      }
+    },
+    channel() {
+      console.log('in function:');
+      this.$stomp({
+        [`/exchange/public_chat_` + this.id]: this.onMessage,
+      });
+    },
+    onMessage(message) {
+      // console.log('receive a message: ', message.body);
+      let body = _.get(message, 'body');
+      if (body) {
+        body = JSON.parse(body);
+        this.dataList.push(body);
+        this.content = '';
+      }
+      // const { content, contenttype, sendid, sendname, icon, groupid, sendtime, type } = message.headers;
+      // let object = { content, contenttype, sendid, sendname, icon, groupid, sendtime, type };
+      // this.list.push(object);
+    },
+    async getDevices() {
+      this.cameras = await TRTC.getCameras();
+      this.microphones = await TRTC.getMicrophones();
+    },
+    async initclient() {
+      console.log(this.user.uid);
+      this.userId_ = this.user.uid;
+      const res = await this.gensignFetch({ userid: this.userId_ });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.client_ = TRTC.createClient({
+          useCloud: 1,
+          mode: 'live',
+          sdkAppId: this.sdkAppId_,
+          userId: this.userId_,
+          userSig: res.data,
+        });
+      }
+    },
+    async liveon() {
+      this.open_ = true;
+      console.log('8888--' + this.userId_);
+      if (this.cameraId === '' || this.microphoneId === '') {
+        this.$message({
+          message: '请选择摄像头和麦克风',
+          type: 'warning',
+        });
+        return;
+      }
+      await this.client_.join({ roomId: this.name, role: 'anchor' });
+      this.localStream_ = await TRTC.createStream({
+        audio: true,
+        video: true,
+        cameraId: this.cameraId,
+        microphoneId: this.microphoneId,
+        userId: this.userId_,
+      });
+      this.localStream_.setVideoProfile('480p');
+      await this.localStream_.initialize();
+      console.log('initialize local stream success');
+      // publish the local stream
+      await this.client_.publish(this.localStream_);
+      this.localStream_.play('main-video');
+      //$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
+      // 订阅其他用户音视频
+      this.client_.on('stream-subscribed', event => {
+        const remoteStream = event.stream;
+        // 远端流订阅成功,播放远端音视频流
+        console.log('111' + remoteStream.getType());
+        this.index = this.index + 1;
+        console.log('111--index--->' + this.index);
+        if (this.index < 8) {
+          const id_ = 'look-video-' + this.index;
+          remoteStream.play(id_);
+        }
+      });
+      // 监听远端流增加事件
+      this.client_.on('stream-added', event => {
+        const remoteStream = event.stream;
+        console.log('222' + remoteStream.getType());
+        // 订阅远端音频和视频流
+        this.client_.subscribe(remoteStream, { audio: true, video: true }).catch(e => {
+          console.error('failed to subscribe remoteStream');
+        });
+      });
+      this.client_.on('mute-video', event => {
+        const remoteStream = event.stream;
+        // 订阅远端音频和视频流
+        if (this.index > 0) {
+          this.index = this.index - 1;
+        }
+
+        console.log('333--index---->' + this.index);
+      });
+    },
+    async liveclose() {
+      // 关闭视频通话
+      console.log(this.open_);
+      if (this.open_) {
+        const videoTrack = this.localStream_.getVideoTrack();
+        if (videoTrack) {
+          this.localStream_.removeTrack(videoTrack).then(() => {
+            console.log('remove video call success');
+            // 关闭摄像头
+            videoTrack.stop();
+            this.client_.unpublish(this.localStream_).then(() => {
+              // 取消发布本地流成功
+            });
+          });
+        }
+      }
+    },
+    async cameraChange() {
+      //await this.localStream_.switchDevice('video', this.cameraId);
+    },
+    async micrChange() {
+      //await this.localStream_.switchDevice('audio', this.microphoneId);
+    },
+    // 选择打开摄像头
+    shexiangBtn() {
+      this.shexiangDia = true;
+    },
+    // 选择打开麦克风
+    tianchongBtn() {
+      this.tianchongDia = true;
+    },
+    // 关闭摄像头&麦克风
+    handleClose(done) {
+      done();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.info {
+  background-color: #2a2b30;
+  min-height: 840px;
+  .left {
+    .leftTop {
+      min-height: 540px;
+      padding: 0 15px;
+      margin: 20px 0;
+      p {
+        color: #ccc;
+      }
+      p:nth-child(2) {
+        padding: 10px 0;
+      }
+      p:nth-child(3) {
+        line-height: 25px;
+      }
+    }
+    .leftDown {
+      padding: 10px 0 0 0;
+      border-top: 1px solid #000;
+      .btn {
+        margin: 0 0 15px 0;
+        color: #cccccc;
+        text-align: center;
+      }
+      .btn:hover {
+        cursor: pointer;
+      }
+    }
+  }
+  .right {
+    .rightTop {
+      height: 100px;
+      background-color: #232428;
+      text-align: right;
+      color: #ccc;
+      line-height: 100px;
+      span {
+        margin: 0 10px 0 0;
+      }
+      span:hover {
+        cursor: pointer;
+        color: #409eff;
+      }
+    }
+    .video {
+      min-height: 640px;
+      background-color: #000;
+      overflow: hidden;
+      position: relative;
+      .videoMeet {
+        .one {
+          height: 480px;
+          overflow: hidden;
+        }
+        .two {
+          height: 480px;
+          overflow: hidden;
+          .twoOne {
+            height: 160px;
+            overflow: hidden;
+          }
+        }
+        .three {
+          height: 160px;
+          overflow: hidden;
+        }
+      }
+    }
+    .noVideo {
+      position: relative;
+      min-height: 640px;
+      background-color: #000;
+      border-left: 1px solid #fff;
+      color: #fff;
+      .chatList {
+        height: 598px;
+        padding: 5px 5px 5px 0px;
+        overflow-y: auto;
+        .list {
+          margin: 0 0 10px 0;
+          span:first-child {
+            float: left;
+            width: 17%;
+            text-align: center;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            // font-weight: bold;
+          }
+          span:last-child {
+            float: right;
+            width: 82%;
+          }
+        }
+      }
+      .chatInput {
+        position: absolute;
+        bottom: 0;
+        .el-button {
+          width: 100%;
+          padding: 13px 0;
+        }
+      }
+    }
+    .rightDown {
+      height: 100px;
+      background-color: #232428;
+    }
+  }
+}
+/deep/.el-dialog__body {
+  min-height: 100px;
+}
+#main-video {
+  width: 100%;
+  height: 480px;
+  min-height: 480px;
+  grid-area: 1/1/3/4;
+}
+.lookvideo {
+  width: 100%;
+  height: 160px;
+  min-height: 160px;
+  grid-area: 1/1/3/4;
+}
+/deep/.el-textarea__inner {
+  padding: 0 15px;
+  line-height: 20px;
+  border-radius: 0;
+}
+.selfColor {
+  color: #ff0000;
+}
+</style>

+ 121 - 0
src/layout/live/liveList.vue

@@ -0,0 +1,121 @@
+<template>
+  <div id="liveList">
+    <el-row>
+      <el-col :span="24">
+        <el-col :span="8" class="list" v-for="(item, index) in list" :key="index" @click.native="detaiBtn(item)">
+          <el-image class="image" :src="item.filedir"></el-image>
+          <el-col :span="24" class="info">
+            <p>
+              <span class="textOver">[房间号:{{ item.name }}]{{ item.title }}</span>
+              <span>直播类型:{{ item.type == '0' ? '直播' : '会议' }}</span>
+            </p>
+            <p>
+              <span><i class="iconfont iconicon-person"></i>{{ item.username }}</span>
+              <span><i class="iconfont iconzhuangtai"></i>{{ item.status == '0' ? '待开始' : '开始' }}</span>
+            </p>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'liveList',
+  props: {
+    list: null,
+  },
+  components: {},
+  data: function() {
+    return {};
+  },
+  created() {},
+  methods: {
+    detaiBtn(item) {
+      if (item.status == '1') {
+        if (item.type == '0') {
+          this.$router.push({ path: '/live/detail', query: { id: item.id, name: item.name, anchorid: item.anchorid } });
+        } else {
+          this.$router.push({ path: '/live/meetingDetail', query: { id: item.id, name: item.name, anchorid: item.anchorid } });
+        }
+      } else {
+        this.$message({
+          message: '直播尚未开始',
+          type: 'warning',
+        });
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.list {
+  width: 30%;
+  margin: 0 15px 15px 15px;
+  border-radius: 10px;
+  box-shadow: 0 0 4px #ccc;
+  .info {
+    padding: 0 10px;
+    p {
+      font-size: 14px;
+      color: #000;
+      span:first-child {
+        float: left;
+        width: 75%;
+        padding: 0 0 10px 0;
+      }
+      span:last-child {
+        float: right;
+        color: #888;
+      }
+    }
+    p:last-child {
+      color: #888;
+      span {
+        display: inline-block;
+        width: 50%;
+        i {
+          margin: 0 5px 0 0;
+        }
+      }
+      span:last-child {
+        text-align: right;
+      }
+    }
+  }
+}
+/deep/.el-image {
+  width: 100%;
+  height: 300px;
+  border-top-left-radius: 10px;
+  border-top-right-radius: 10px;
+}
+/deep/.el-image__inner {
+  -webkit-transition: all 0.3s linear;
+  -moz-transition: all 0.3s linear;
+  -o-transition: all 0.3s linear;
+  transition: all 0.3s linear;
+}
+/deep/.el-image:hover {
+  cursor: pointer;
+}
+/deep/.el-image:hover .el-image__inner {
+  transform: scale(1.09, 1.09);
+  -ms-transform: scale(1.09, 1.09);
+  -webkit-transform: scale(1.09, 1.09);
+  -o-transform: scale(1.09, 1.09);
+  -moz-transform: scale(1.09, 1.09);
+}
+</style>

+ 84 - 0
src/layout/main-layout.vue

@@ -0,0 +1,84 @@
+<template>
+  <div id="main-layout">
+    <el-row>
+      <div v-if="!toLogin()">
+        <el-container class="index">
+          <el-header height="4rem" class="heads">
+            <heads></heads>
+          </el-header>
+          <el-container>
+            <el-aside width="13rem" class="menus">
+              <menus></menus>
+            </el-aside>
+            <el-main class="main">
+              <router-view />
+            </el-main>
+          </el-container>
+        </el-container>
+      </div>
+      <div v-else>
+        <router-view />
+      </div>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import heads from '@/layout/layout-part/heads.vue';
+import menus from '@/layout/layout-part/menus.vue';
+import { mapActions, mapState } from 'vuex';
+export default {
+  name: 'main-layout',
+  props: {},
+  components: {
+    heads,
+    menus,
+  },
+  data: () => ({
+    loginBei: require('@/assets/beijing.jpg'),
+  }),
+  created() {},
+  computed: {
+    ...mapState(['user']),
+  },
+  methods: {
+    toLogin() {
+      let route = window.location.pathname;
+      console.log(route);
+      if (route == '/login') {
+        return route.includes('login');
+      } else if (route == '/livecheck') {
+        return route.includes('livecheck');
+      } else if (route == '/liveadmin/login') {
+        return route.includes('login');
+      } else if (route == '/liveadmin/livecheck') {
+        return route.includes('livecheck');
+      }
+    },
+  },
+  mounted() {},
+};
+</script>
+
+<style lang="less" scoped>
+.index {
+  background-image: url('../assets/beijing.jpg');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+}
+.heads {
+  padding: 0;
+  // background-color: #409eff8f;
+  background-color: #353852;
+  border-bottom: 1px solid #000;
+}
+.menus {
+  min-height: 905px;
+  background: rgb(53, 56, 82);
+}
+.main {
+  min-height: 905px;
+  background-color: #ffffff;
+  padding: 0 15px;
+}
+</style>

+ 44 - 0
src/layout/public/top.vue

@@ -0,0 +1,44 @@
+<template>
+  <div id="top">
+    <el-row>
+      <el-col class="info">
+        <el-col :span="1" class="home">
+          <i class="el-icon-s-home"></i>
+        </el-col>
+        <el-col :span="23">
+          <el-breadcrumb separator-class="el-icon-arrow-right">
+            <el-breadcrumb-item :to="{ path: '/' }"> 我的主页</el-breadcrumb-item>
+            <el-breadcrumb-item>{{ topTitle }}</el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'top',
+  props: {
+    topTitle: null,
+  },
+  components: {},
+  data: () => ({}),
+  created() {},
+  computed: {},
+  methods: {},
+};
+</script>
+
+<style lang="less" scoped>
+.info {
+  height: 40px;
+  line-height: 40px;
+}
+.home {
+  text-align: center;
+}
+/deep/.el-breadcrumb {
+  line-height: 40px;
+}
+</style>

+ 205 - 0
src/layout/room/detailInfo.vue

@@ -0,0 +1,205 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24">
+        <el-col :span="24" class="messgae">
+          <el-form ref="form" :model="form" label-width="120px" :rules="rules">
+            <el-form-item label="标题" prop="title">
+              <el-input v-model="form.title" placeholder="请输入房间标题"></el-input>
+            </el-form-item>
+            <el-form-item label="房间号" prop="name">
+              <el-input v-model.number="form.name" placeholder="房间号必须是数字"></el-input>
+            </el-form-item>
+            <el-form-item label="主持" prop="anchorid">
+              <el-select v-model="form.anchorid" placeholder="请选择主持人" filterable>
+                <el-option v-for="item in newlist" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="主讲人">
+              <el-checkbox-group v-model="form.zjr">
+                <el-checkbox v-for="item in newlist" :label="item.id" :key="item.id" :value="item.id">{{ item.name }}</el-checkbox>
+              </el-checkbox-group>
+            </el-form-item>
+            <el-form-item label="直播时间" required>
+              <el-col :span="9">
+                <el-form-item prop="starttime">
+                  <el-date-picker
+                    v-model="form.starttime"
+                    type="datetime"
+                    style="width: 60%;"
+                    placeholder="请选择直播开始时间"
+                    format="yyyy-MM-dd HH:mm"
+                    value-format="yyyy-MM-dd HH:mm"
+                  >
+                  </el-date-picker>
+                </el-form-item>
+              </el-col>
+              <el-col class="line" :span="2">-</el-col>
+              <el-col :span="9">
+                <el-form-item prop="endtime">
+                  <el-date-picker
+                    v-model="form.endtime"
+                    type="datetime"
+                    style="width: 60%;"
+                    placeholder="请选择直播结束时间"
+                    format="yyyy-MM-dd HH:mm"
+                    value-format="yyyy-MM-dd HH:mm"
+                  >
+                  </el-date-picker>
+                </el-form-item>
+              </el-col>
+              <el-col class="line" :span="4"></el-col>
+            </el-form-item>
+            <el-form-item label="类型" prop="type">
+              <el-radio v-model="form.type" label="0">直播</el-radio>
+              <el-radio v-model="form.type" label="1">会议</el-radio>
+            </el-form-item>
+            <el-form-item label="录播金额" prop="type">
+              <el-input v-model="form.price" placeholder="请输入金额"></el-input>
+            </el-form-item>
+            <el-form-item label="封面图片" prop="filedir">
+              <upload :limit="1" :data="form.filedir" type="filedir" :url="'/files/filedir/upload'" @upload="uploadSuccess"></upload>
+            </el-form-item>
+            <el-form-item label="简介" prop="content">
+              <el-input
+                type="textarea"
+                maxlength="300"
+                show-word-limit
+                :autosize="{ minRows: 2, maxRows: 4 }"
+                v-model="form.content"
+                placeholder="请输入简介"
+              ></el-input>
+            </el-form-item>
+            <el-form-item label="是否开启广告" prop="isadvert">
+              <el-switch v-model="form.isadvert" active-text="开启" inactive-text="关闭"> </el-switch>
+            </el-form-item>
+            <el-form-item label="广告位" v-if="form.isadvert == true">
+              <el-col :span="20">
+                <el-table :data="form.adverts" style="width: 100%" border>
+                  <el-table-column prop="title" label="标题" align="center"> </el-table-column>
+                  <el-table-column label="图片地址" align="center">
+                    <template slot-scope="scope">
+                      <el-image style="width: 30px; height: 30px" :src="scope.row.imgdir"></el-image>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="imgurl" label="链接地址" align="center"> </el-table-column>
+                  <el-table-column label="操作" align="center">
+                    <template slot-scope="scope">
+                      <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-col>
+              <el-col :span="4">
+                <el-button @click="drawer = true" type="primary" style="margin-left: 16px;">
+                  上传广告图片
+                </el-button>
+                <el-drawer title="广告位" class="drawer" :visible.sync="drawer" :direction="direction" :before-close="handleClose">
+                  <el-form-item label="标题">
+                    <el-input v-model="drawerform.title" placeholder="请输入标题"></el-input>
+                  </el-form-item>
+                  <el-form-item label="图片">
+                    <upload :limit="1" :data="drawerform.imgdir" type="imgdir" :url="'/files/advert/upload'" @upload="advertSuccess"></upload>
+                  </el-form-item>
+                  <el-form-item label="链接">
+                    <el-input v-model="drawerform.imgurl" placeholder="请输入链接地址"></el-input>
+                  </el-form-item>
+                  <el-col :span="24" style="text-align:center">
+                    <el-button type="primary" @click="submitDrawer">保存</el-button>
+                  </el-col>
+                </el-drawer>
+              </el-col>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="onSubmit('form')">提交</el-button>
+            </el-form-item>
+          </el-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import upload from '@/components/upload.vue';
+export default {
+  name: 'detail',
+  props: {
+    form: null,
+    newlist: null,
+  },
+  components: {
+    upload,
+  },
+  data: () => ({
+    adverts: [],
+    direction: 'rtl',
+    drawer: false,
+    imgurl: '',
+    imgdir: '',
+    title: '',
+    rules: {
+      title: [{ required: true, message: '请输入房间标题', trigger: 'blur' }],
+      name: [
+        { required: true, message: '请输入房间号', trigger: 'blur' },
+        // { type: 'number', message: '房间号必须为数字值' },
+      ],
+      anchorid: [{ required: true, message: '请选择主播', trigger: 'change' }],
+      filedir: [{ required: true, message: '请选择头像图片', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择类型', trigger: 'change' }],
+      content: [{ required: false, message: '请输入信息简介', trigger: 'blur' }],
+      starttime: [{ required: false, message: '请选择直播開始時間', trigger: 'change' }],
+      endtime: [{ required: false, message: '请选择直播結束時間', trigger: 'change' }],
+    },
+    drawerform: {},
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+    onSubmit() {
+      this.$emit('submitDate', { data: this.form });
+    },
+    advertSuccess({ type, data }) {
+      this.$set(this.drawerform, `${type}`, data.uri);
+    },
+    handleClose(done) {
+      done();
+    },
+    submitDrawer() {
+      if (this.drawerform.title != null && this.drawerform.imgdir != null && this.drawerform.imgurl != null) {
+        this.form.adverts.push(this.drawerform);
+      }
+      this.drawerform = {};
+      this.drawer = false;
+    },
+    handleDelete(index, row) {
+      this.form.adverts.splice(index, 1);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.top {
+  padding: 15px 0;
+}
+.top .topTitle {
+  text-align: left;
+}
+.top .topBtn {
+  text-align: right;
+  padding: 0 5px;
+}
+/deep/.el-table td {
+  padding: 5px 0;
+}
+/deep/ .el-table th {
+  padding: 5px 0;
+}
+/deep/.el-form-item .el-form-item {
+  margin-bottom: 20px;
+}
+</style>

+ 83 - 0
src/layout/room/detailStatusInfo.vue

@@ -0,0 +1,83 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24">
+        <el-col :span="24" class="messgae">
+          <el-form ref="form" :model="form" label-width="120px" :rules="rules">
+            <el-form-item label="房间标题" prop="title">
+              <el-input v-model="form.title" disabled></el-input>
+            </el-form-item>
+            <el-form-item label="房间号" prop="name">
+              <el-input v-model="form.name" disabled></el-input>
+            </el-form-item>
+            <el-form-item label="主播" prop="username">
+              <el-input v-model="form.username" disabled></el-input>
+            </el-form-item>
+            <el-form-item label="类型" prop="type">
+              <el-radio v-model="form.type" label="0" disabled>直播</el-radio>
+              <el-radio v-model="form.type" label="1" disabled>会议</el-radio>
+            </el-form-item>
+            <el-form-item label="封面图片" prop="filedir">
+              <upload :limit="1" :data="form.filedir" type="filedir" :url="'/files/filedir/upload'" @upload="uploadSuccess"></upload>
+            </el-form-item>
+            <el-form-item label="信息简介" prop="content">
+              <el-input type="textarea" v-model="form.content" disabled></el-input>
+            </el-form-item>
+            <el-form-item label="直播状态" prop="status">
+              <el-radio v-model="form.status" label="1">开启</el-radio>
+              <el-radio v-model="form.status" label="2">结束</el-radio>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="onSubmit()">提交</el-button>
+            </el-form-item>
+          </el-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import upload from '@/components/upload.vue';
+export default {
+  name: 'detail',
+  props: {
+    form: null,
+  },
+  components: {
+    upload,
+  },
+  data: () => ({
+    rules: {
+      name: [{ required: true, message: '请输入房间号', trigger: 'blur' }],
+      anchorid: [{ required: true, message: '请选择主播', trigger: 'change' }],
+      filedir: [{ required: true, message: '请选择头像图片', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择类型', trigger: 'change' }],
+      status: [{ required: true, message: '请选择直播状态', trigger: 'change' }],
+    },
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+    onSubmit() {
+      this.$emit('onSubmit', { data: this.form });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.top {
+  padding: 15px 0;
+}
+.top .topTitle {
+  text-align: left;
+}
+.top .topBtn {
+  text-align: right;
+  padding: 0 5px;
+}
+</style>

+ 25 - 0
src/main.js

@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import '@/plugins/element.js';
+// import '@/plugins/vant';
+import '@/plugins/axios';
+import '@/plugins/check-res';
+import '@/plugins/meta';
+import '@/plugins/filters';
+import '@/plugins/loading';
+import '@/plugins/var';
+import '@/plugins/methods';
+import '@/plugins/setting';
+import '@/iconfonts/iconfont.css';
+import InitStomp from '@/plugins/stomp';
+new Vue({
+  router,
+  store,
+  render: h => h(App),
+}).$mount('#app');
+InitStomp();
+window.vm = new Vue({
+  router,
+});

+ 19 - 0
src/plugins/axios.js

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import AxiosWrapper from '@/util/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 });

+ 42 - 0
src/plugins/check-res.js

@@ -0,0 +1,42 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-shadow */
+import Vue from 'vue';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+// import { Notify } from 'vant';
+
+const vm = new Vue({});
+const Plugin = {
+  install(Vue, options) {
+    // 4. 添加实例方法
+    Vue.prototype.$checkRes = (res, okText, errText) => {
+      let _okText = okText;
+      let _errText = errText;
+      if (!_.isFunction(okText) && _.isObject(okText) && okText != null) {
+        ({ okText: _okText, errText: _errText } = okText);
+      }
+      const { errcode = 0, errmsg } = res || {};
+      if (errcode === 0) {
+        if (_.isFunction(_okText)) {
+          return _okText();
+        }
+        if (_okText) {
+          Message.success(_okText);
+          // Notify({ type: 'success', message: _okText });
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Notify({ type: 'danger', message: _okText });
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 5 - 0
src/plugins/element.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Element from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.use(Element);

+ 6 - 0
src/plugins/filters.js

@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import filters from '@/util/filters';
+
+for (const method in filters) {
+  Vue.filter(method, filters[method]);
+}

+ 27 - 0
src/plugins/loading.js

@@ -0,0 +1,27 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+
+const Plugin = {
+  // eslint-disable-next-line no-unused-vars
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        // eslint-disable-next-line no-underscore-dangle
+        const isRoot = this.constructor === Vue;
+        // console.log(`rootId:${rootVue_uid}; thisId:${this._uid}`);
+        // if (rootVue_uid !== 3) {
+        //   console.log(this);
+        // }
+        if (isRoot) {
+          const el = document.getElementById('loading');
+          if (el) el.style.display = 'none';
+        }
+      },
+    });
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 4 - 0
src/plugins/meta.js

@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import Meta from 'vue-meta';
+
+Vue.use(Meta);

+ 33 - 0
src/plugins/methods.js

@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import _ from 'lodash';
+const Plugin = {
+  install(Vue, options) {
+    // 3. 注入组件
+    Vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$toUndefined) {
+          this.$store.$toUndefined = this.$toUndefined;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    Vue.prototype.$toUndefined = object => {
+      let keys = Object.keys(object);
+      keys.map(item => {
+        object[item] = object[item] === '' ? (object[item] = undefined) : object[item];
+      });
+      return object;
+    };
+    Vue.prototype.$turnTo = item => {
+      if (item.info_type == 1) {
+        window.open(item.url);
+      } else {
+        let router = window.vm.$router;
+        let route = window.vm.$route.path;
+        router.push({ path: `/info/detail?id=${item.id}` });
+      }
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 20 - 0
src/plugins/setting.js

@@ -0,0 +1,20 @@
+import Vue from 'vue';
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: `http://${location.host}/weixin`,
+};
+
+Vue.config.stomp = {
+  // brokerURL: 'ws://192.168.1.190:15674/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  connectHeaders: {
+    host: 'live',
+    login: 'live',
+    passcode: 'live',
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};

+ 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 = `wss://${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 = frame => {
+        // 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);
+};

+ 5 - 0
src/plugins/vant.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Vant from 'vant';
+import 'vant/lib/index.css';
+
+Vue.use(Vant);

+ 25 - 0
src/plugins/var.js

@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import _ from 'lodash';
+
+const getSiteId = () => {
+  let host = `${window.location.hostname}`; //`999991.smart.jilinjobswx.cn ${window.location.hostname}`
+  let schId;
+  host = host.replace('http://', '');
+  let arr = host.split('.');
+  if (arr.length > 0) {
+    schId = arr[0];
+    if (schId === 'smart') schId = 'master';
+    else `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? (schId = '99991') : '';
+    sessionStorage.setItem('schId', `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? '99991' : schId);
+  }
+  return schId;
+};
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$limit = 10;
+    vue.prototype.$site = getSiteId();
+  },
+};
+
+Vue.use(Plugin);

+ 24 - 0
src/router/before.js

@@ -0,0 +1,24 @@
+import store from '@/store/index';
+
+const checkLogin = (router, func) => {
+  router.beforeEach(async (to, form, next) => {
+    if (to.name == 'index') {
+      let token = localStorage.getItem('token');
+      if (token) {
+        next();
+      } else {
+        next({ name: 'login' });
+      }
+    } else if (to.name == 'liveIndex') {
+      let token = localStorage.getItem('token');
+      if (token) {
+        next();
+      } else {
+        next({ name: 'login' });
+      }
+    }
+    let res = await store.dispatch('login/toGetUser', func ? func : null);
+    next();
+  });
+};
+export default checkLogin;

+ 129 - 0
src/router/index.js

@@ -0,0 +1,129 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import checkLogin from './before';
+
+Vue.use(VueRouter);
+
+const routes = [
+  {
+    path: '/',
+    name: 'index',
+    meta: { title: '首页' },
+    component: () => import('../views/index.vue'),
+  },
+  {
+    path: '/liveIndex',
+    name: 'liveIndex',
+    meta: { title: '直播页' },
+    component: () => import('../views/liveIndex.vue'),
+  },
+  {
+    path: '/anchor/index',
+    meta: { title: '主播管理' },
+    component: () => import('../views/anchor/index.vue'),
+  },
+
+  {
+    path: '/anchor/detail',
+    meta: { title: '主播详情' },
+    component: () => import('../views/anchor/detail.vue'),
+  },
+  {
+    path: '/role/index',
+    meta: { title: '菜单管理' },
+    component: () => import('../views/role/index.vue'),
+  },
+
+  {
+    path: '/role/detail',
+    meta: { title: '菜单详情' },
+    component: () => import('../views/role/detail.vue'),
+  },
+  {
+    path: '/live/index',
+    meta: { title: '直播管理' },
+    component: () => import('../views/live/index.vue'),
+  },
+  {
+    path: '/live/detail',
+    meta: { title: '直播详情' },
+    component: () => import('../views/live/detail.vue'),
+  },
+  {
+    path: '/live/meetingDetail',
+    meta: { title: '会议直播详情' },
+    component: () => import('../views/live/meetingDetail.vue'),
+  },
+
+  {
+    path: '/room/index',
+    meta: { title: '房间管理' },
+    component: () => import('../views/room/index.vue'),
+  },
+  {
+    path: '/room/detail',
+    meta: { title: '房间详情' },
+    component: () => import('../views/room/detail.vue'),
+  },
+  {
+    path: '/room/detailStatus',
+    meta: { title: '房间状态审核' },
+    component: () => import('../views/room/detailStatus.vue'),
+  },
+  // 统计
+  {
+    path: '/room/statList',
+    meta: { title: '房间观看人数统计' },
+    component: () => import('../views/room/statList.vue'),
+  },
+
+  {
+    path: '/test/index',
+    meta: { title: '测试管理' },
+    component: () => import('../views/test/index.vue'),
+  },
+  {
+    path: '/test/detail',
+    meta: { title: '添加' },
+    component: () => import('../views/test/detail.vue'),
+  },
+  {
+    path: '/question/index',
+    meta: { title: '问卷管理' },
+    component: () => import('../views/question/index.vue'),
+  },
+  {
+    path: '/contact/index',
+    meta: { title: '联系我们' },
+    component: () => import('../views/contact/index.vue'),
+  },
+  {
+    path: '/meetingBrief/index',
+    meta: { title: '信息发布' },
+    component: () => import('../views/meetingBrief/index.vue'),
+  },
+  {
+    path: '/meetingBrief/detail',
+    meta: { title: '信息发布信息管理' },
+    component: () => import('../views/meetingBrief/detail.vue'),
+  },
+  {
+    path: '/login',
+    name: 'login',
+    meta: { title: '登录' },
+    component: () => import('../views/login.vue'),
+  },
+  {
+    path: '/livecheck',
+    name: 'livecheck',
+    meta: { title: '登录' },
+    component: () => import('../views/livecheck.vue'),
+  },
+];
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.NODE_ENV === 'development' ? '' : process.env.VUE_APP_ROUTER,
+  routes,
+});
+checkLogin(router);
+export default router;

+ 46 - 0
src/store/chat.js

@@ -0,0 +1,46 @@
+// 房间
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  chatInfo: `/api/onlive/chat`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 1000, ...info } = {}) {
+    const res = await this.$axios.$get(api.chatInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.chatInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.chatInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.chatInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.chatInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 46 - 0
src/store/contact.js

@@ -0,0 +1,46 @@
+// 权限
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  contactInfo: `/api/onlive/contact`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.contactInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.contactInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.contactInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.contactInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.contactInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 24 - 0
src/store/gensign.js

@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  gensignInfo: `/api/onlive/user/gensign`,
+  // gensignInfo: `/api/onlive/user/gensignqili`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async gensignFetch({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.gensignInfo}`, payload);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 38 - 0
src/store/index.js

@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import login from './login';
+import roomuser from './roomuser';
+import role from './role';
+import room from './room';
+import lookuser from './lookuser';
+import gensign from './gensign';
+import chat from './chat';
+import question from './question';
+import questionnaire from './questionnaire';
+import uploadquestion from './uploadquestion';
+import contact from './contact';
+import news from './news';
+import * as ustate from './user/state';
+import * as umutations from './user/mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: { ...ustate },
+  mutations: { ...umutations },
+  actions: {},
+  modules: {
+    login,
+    roomuser,
+    role,
+    room,
+    lookuser,
+    gensign,
+    chat,
+    question,
+    questionnaire,
+    uploadquestion,
+    contact,
+    news,
+  },
+});

+ 102 - 0
src/store/login.js

@@ -0,0 +1,102 @@
+// 登录login&退出logout&修改密码
+import Vue from 'vue';
+import Vuex from 'vuex';
+import axios from 'axios';
+import _ from 'lodash';
+import { Notification } from 'element-ui';
+const jwt = require('jsonwebtoken');
+Vue.use(Vuex);
+const api = {
+  loginInfo: `/api/onlive/login`,
+  getUser: `/api/onlive/token`,
+  logoutInfo: `/api/onlive/logout`,
+  uppasswdInfo: `/api/onlive/user/uppasswd`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  /**
+    user:Object required 登陆信息 
+    router:router 如果跳转就传
+    path:String 跳转到的路由位置
+    needReturn: Boolean 是否返回结果
+    typeCheck: Boolean 是否检查身份对应匹配的前端项目
+    isWx: Boolean 是否是微信登陆
+    needNotice:Boolean 是否需要提示
+   */
+  async login({ commit, dispatch }, { user, router, path = '/', needReturn = false, typeCheck = false, isWx = false, needNotice = true }) {
+    let res;
+    //wx登陆,openid存在,user中是openid和qrcode;正常登陆,user中是mobile和passwd
+    if (isWx) res = await this.$axios.$post(`${api.wxLogin}`, user);
+    else res = await this.$axios.$post(`${api.loginInfo}`, user);
+    const setUser = async (token, commit) => {
+      localStorage.setItem('token', token);
+      let userInfo = await dispatch('toGetUser');
+      return userInfo;
+    };
+    let userInfo = {};
+    if (res.errcode == '0') {
+      userInfo = await setUser(res.data.key, commit);
+      Notification({
+        title: '登录成功',
+        type: 'success',
+        duration: 2000,
+      });
+      return userInfo;
+    } else {
+      if (needReturn) return res;
+      else {
+        Notification({
+          title: '登录失败',
+          message: `失败原因:${res.errmsg || '登陆失败'}`,
+          type: 'error',
+        });
+      }
+    }
+  },
+  async toGetUser({ commit }, payload) {
+    let key = localStorage.getItem('token');
+    if (!key) {
+      if (_.isFunction(payload)) {
+        payload();
+        return;
+      }
+      let user = localStorage.getItem('user');
+      if (user) {
+        commit('setUser', JSON.parse(user), { root: true });
+      } else {
+        let stamp = new Date().getTime();
+        let name = `游客${stamp}`;
+        localStorage.setItem('user', JSON.stringify({ name }));
+        commit('setUser', { name }, { root: true });
+      }
+      return;
+    }
+    let res = await axios.post(api.getUser, { key: key });
+    let user = {};
+    if (res.data.errcode == '0') {
+      let token = _.get(res, `data.data.token`);
+      if (token) {
+        user = jwt.decode(token);
+        commit('setUser', user, { root: true });
+      }
+    }
+    return user;
+  },
+  async logout({ commit }, payload) {
+    let key = localStorage.removeItem('token');
+    const res = await this.$axios.$post(api.logoutInfo, { key: key });
+    commit('deleteUser');
+  },
+  async update({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.uppasswdInfo}`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 60 - 0
src/store/lookuser.js

@@ -0,0 +1,60 @@
+// 主播
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  roomuserInfo: `/api/onlive/lookuser`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.roomuserInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async lookquery({ commit }, { skip = 0, limit = 100, ...info } = {}) {
+    const res = await this.$axios.$get(api.roomuserInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.roomuserInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.roomuserInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.roomuserInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async lookupdate({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.roomuserInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.roomuserInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 46 - 0
src/store/news.js

@@ -0,0 +1,46 @@
+// 权限
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  newsInfo: `/api/onlive/news`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.newsInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.newsInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.newsInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.newsInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.newsInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 64 - 0
src/store/question.js

@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+import axios from 'axios';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/onlive/question`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}`, { skip, limit, ...info });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.interface}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.interface}/update/${id}`, data);
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.interface}/${payload}`);
+    return res;
+  },
+  async mergeRequest({ commit, dispatch }, { method, data }) {
+    let toRequest = () => {
+      let res = [];
+      for (const i of data) {
+        res.push(dispatch(method, i));
+      }
+      return res;
+    };
+    let result = await axios.all(toRequest());
+    let newFilter = data => {
+      let res = data.map(i => {
+        let type = _.isArray(i);
+        if (!type) {
+          //fetch的多个请求 是object 将errcode为0的data取出来
+          return _.get(i, `data`, i);
+        } else {
+          //query的多个请求 array 将此数据再次走这个方法
+          return newFilter(i);
+        }
+      });
+      return res;
+    };
+    let returns = _.flattenDeep(newFilter(result));
+    return returns;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 64 - 0
src/store/questionnaire.js

@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+import axios from 'axios';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/onlive/questionnaire`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}`, { skip, limit, ...info });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.interface}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/show/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.interface}/update/${id}`, data);
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.interface}/${payload}`);
+    return res;
+  },
+  async mergeRequest({ commit, dispatch }, { method, data }) {
+    let toRequest = () => {
+      let res = [];
+      for (const i of data) {
+        res.push(dispatch(method, i));
+      }
+      return res;
+    };
+    let result = await axios.all(toRequest());
+    let newFilter = data => {
+      let res = data.map(i => {
+        let type = _.isArray(i);
+        if (!type) {
+          //fetch的多个请求 是object 将errcode为0的data取出来
+          return _.get(i, `data`, i);
+        } else {
+          //query的多个请求 array 将此数据再次走这个方法
+          return newFilter(i);
+        }
+      });
+      return res;
+    };
+    let returns = _.flattenDeep(newFilter(result));
+    return returns;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 46 - 0
src/store/role.js

@@ -0,0 +1,46 @@
+// 权限
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  roleInfo: `/api/onlive/role`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.roleInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.roleInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.roleInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.roleInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.roleInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 95 - 0
src/store/room.js

@@ -0,0 +1,95 @@
+// 房间
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  roomInfo: `/api/onlive/room`,
+  interface: `/api/onlive/questionnaire`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async questquery({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}`, { skip, limit, ...info });
+    return res;
+  },
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.roomInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.roomInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.roomInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.roomInfo}/update/${id}`, {
+      ...info,
+    });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.roomInfo}/${payload}`);
+    return res;
+  },
+  async startrecord({ commit }, info) {
+    const res = await this.$axios.$get(`${api.roomInfo}/starttranscode`, info);
+    return res;
+  },
+  async stoprecord({ commit }, info) {
+    const res = await this.$axios.$get(`${api.roomInfo}/stoptranscode`, info);
+    return res;
+  },
+  async deletefile({ commit }, info) {
+    const res = await this.$axios.$get(`${api.roomInfo}/deletefile`, info);
+    return res;
+  },
+  async roomquest({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/roomquest`, info);
+    return res;
+  },
+  async updateanchor({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/updateanchor`, info);
+    return res;
+  },
+  async roomquestclose({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/roomquestclose`, info);
+    return res;
+  },
+  async updateshmai({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/updateshmai`, info);
+    return res;
+  },
+  async switchzjr({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/switchzjr`, info);
+    return res;
+  },
+  async switchzb({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/switchzb`, info);
+    return res;
+  },
+  async switchzp({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/switchzp`, info);
+    return res;
+  },
+  async dismissroom({ commit }, info) {
+    const res = await this.$axios.$post(`${api.roomInfo}/dismissroom`, info);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 44 - 0
src/store/roomuser.js

@@ -0,0 +1,44 @@
+// 主播
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  roomuserInfo: `/api/onlive/roomuser`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit = 10, ...info } = {}) {
+    const res = await this.$axios.$get(api.roomuserInfo, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.roomuserInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.roomuserInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...info } = {}) {
+    const res = await this.$axios.$post(`${api.roomuserInfo}/update/${id}`, { ...info });
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.roomuserInfo}/${payload}`);
+    return res;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 68 - 0
src/store/uploadquestion.js

@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+import axios from 'axios';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/onlive/uploadquestion`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async completion({ commit }, { ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}/completion`, { ...info });
+    return res;
+  },
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}`, { skip, limit, ...info });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.interface}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.interface}/update/${id}`, data);
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.interface}/${payload}`);
+    return res;
+  },
+  async mergeRequest({ commit, dispatch }, { method, data }) {
+    let toRequest = () => {
+      let res = [];
+      for (const i of data) {
+        res.push(dispatch(method, i));
+      }
+      return res;
+    };
+    let result = await axios.all(toRequest());
+    let newFilter = data => {
+      let res = data.map(i => {
+        let type = _.isArray(i);
+        if (!type) {
+          //fetch的多个请求 是object 将errcode为0的data取出来
+          return _.get(i, `data`, i);
+        } else {
+          //query的多个请求 array 将此数据再次走这个方法
+          return newFilter(i);
+        }
+      });
+      return res;
+    };
+    let returns = _.flattenDeep(newFilter(result));
+    return returns;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 20 - 0
src/store/user/mutations.js

@@ -0,0 +1,20 @@
+const jwt = require('jsonwebtoken');
+export const setUser = (state, payload) => {
+  let res = true;
+  //登陆时
+  if (payload) {
+    state.user = payload;
+  } else {
+    //已经登陆,切换路由时取出用户信息放在总store中
+    let token = localStorage.getItem('token');
+    if (token) {
+      state.user = jwt.decode(token);
+    }
+  }
+  return res;
+};
+
+export const deleteUser = (state, payload) => {
+  state.user = {};
+  localStorage.removeItem('token');
+};

+ 1 - 0
src/store/user/state.js

@@ -0,0 +1 @@
+export const user = {};

+ 118 - 0
src/util/axios-wrapper.js

@@ -0,0 +1,118 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import util 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);
+  }
+
+  $delete(uri, data = {}, router, query, options = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options, router);
+  }
+  async $request(uri, data, query, options) {
+    // 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;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      axios.defaults.headers.common.Authorization = util.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, ['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 {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 10 - 0
src/util/filters.js

@@ -0,0 +1,10 @@
+import _ from 'lodash';
+
+const filters = {
+  getName(object) {
+    const { data, searchItem } = object;
+    return _.get(data, searchItem) === undefined ? '' : _.get(data, searchItem);
+  },
+};
+
+export default filters;

+ 50 - 0
src/util/methods-util.js

@@ -0,0 +1,50 @@
+import { Util } from 'naf-core';
+
+const { isNullOrUndefined } = Util;
+
+export default {
+  //判断信息是否过期
+  isDateOff(dataDate) {
+    const now = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
+    dataDate = new Date(dataDate);
+    return now.getTime() <= dataDate.getTime();
+  },
+  //判断企业是否可以执行此动作/显示
+  checkCorp(data) {
+    const { role, unit, selfUnit, status, displayType, userid } = data;
+    if (!isNullOrUndefined(selfUnit) && !isNullOrUndefined(status)) {
+      return role === 'corp' && selfUnit === unit && status === '0';
+    } else if (!isNullOrUndefined(displayType)) {
+      if (role === 'corp') {
+        return role === displayType;
+      } else {
+        return role === displayType && !isNullOrUndefined(userid);
+      }
+    }
+  },
+  //获取url的参数params
+  getParams() {
+    let str = location.href;
+    let num = str.indexOf('?');
+    const param = {};
+    str = str.substr(num + 1);
+    let num2 = str.indexOf('#');
+    let str2 = '';
+    if (num2 > 0) {
+      str2 = str.substr(0, num2);
+    } else {
+      num2 = str.indexOf('/');
+      str2 = str.substr(0, num2);
+    }
+    const arr = str2.split('&');
+    for (let i = 0; i < arr.length; i++) {
+      num = arr[i].indexOf('=');
+      if (num > 0) {
+        const name = arr[i].substring(0, num);
+        const value = arr[i].substr(num + 1);
+        param[name] = decodeURI(value);
+      }
+    }
+    return param;
+  },
+};

+ 47 - 0
src/util/optionTitles.js

@@ -0,0 +1,47 @@
+export const JOBFAIR_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'address', label: '举办地址' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];
+
+export const CAMPUS_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'address', label: '举办地址' },
+  { prop: 'status', label: '审核状态' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];
+
+export const JOBINFO_TITLE = [
+  { prop: 'title', label: '' },
+  { prop: 'count', label: '需求人数' },
+  { prop: 'nature.name', label: '工作性质' },
+  { prop: 'salary.name', label: '薪资待遇' },
+  { prop: 'xlreqs.name', label: '最低学历' },
+  { prop: 'city.name', label: '所在城市' },
+  // { prop: 'expired', label: '状态' },
+];
+
+export const RESUME_TITLE = [{ prop: 'title', label: '' }];
+
+export const LETTER_TITLE = [
+  { prop: 'title', label: '' },
+  { prop: 'corpname', label: '企业名称' },
+  { prop: 'type', label: '类型' },
+  { prop: 'status', label: '状态' },
+];
+
+export const TICKET_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'type', label: '门票类型' },
+  { prop: 'origin', label: '' },
+  { prop: 'date', label: '举办日期' },
+];
+
+export const CORP_JOBFAIR = [
+  { prop: 'subject', label: '' },
+  { prop: 'time', label: '举办时间' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];

+ 50 - 0
src/util/role_menu.js

@@ -0,0 +1,50 @@
+export const index = {
+  name: '首页',
+  path: '/',
+  icon: 'iconfont iconshouye',
+};
+export const user = {
+  name: '主播管理',
+  path: '/anchor/index',
+  icon: 'iconfont iconyonghu',
+};
+export const role = {
+  name: '菜单管理',
+  path: '/role/index',
+  icon: 'iconfont iconquanxian',
+};
+export const live = {
+  name: '直播管理',
+  path: '/live/index',
+  icon: 'iconfont iconzhibo',
+};
+export const room = {
+  name: '房间管理',
+  path: '/room/index',
+  icon: 'iconfont iconfangjian',
+};
+export const stat = {
+  name: '统计管理',
+  path: '/stat/index',
+  icon: 'iconfont icontongji',
+};
+export const test = {
+  name: '测试管理',
+  path: '/test/index',
+  icon: 'iconfont icontongji',
+};
+export const question = {
+  name: '问卷管理',
+  path: '/question/index',
+  icon: 'iconfont icontongji',
+};
+export const contact = {
+  name: '联系我们',
+  path: '/contact/index',
+  icon: 'iconfont iconlianxiwomen',
+};
+export const meetingBrief = {
+  name: '信息发布',
+  path: '/meetingBrief/index',
+  icon: 'iconfont iconicon_xinyong_xianxing_jijin-',
+};

+ 69 - 0
src/util/user-util.js

@@ -0,0 +1,69 @@
+/* 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));
+  },
+  get token() {
+    return sessionStorage.getItem('token');
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get openid() {
+    return sessionStorage.getItem('openid');
+  },
+  set openid(openid) {
+    sessionStorage.setItem('openid', openid);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  save({ userinfo, token }) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    sessionStorage.setItem('token', token);
+  },
+
+  get corpInfo() {
+    const val = sessionStorage.getItem('corpInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set corpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+  saveCorpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+
+  get unit() {
+    const val = sessionStorage.getItem('unit');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set unit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  saveUnit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  get userInfo() {
+    const val = sessionStorage.getItem('userInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set userInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+  saveUserInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+};

+ 126 - 0
src/views/anchor/detail.vue

@@ -0,0 +1,126 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <data-form :fields="fields" :data="form" :rules="rules" @save="drawerSave" :isNew="drawerIsNew">
+            <template #radios="{item}">
+              <template v-if="item.model === 'gender'">
+                <el-radio label="男" value="男"></el-radio>
+                <el-radio label="女" value="女"></el-radio>
+              </template>
+              <template v-else>
+                <el-radio label="3">主播</el-radio>
+                <el-radio label="4">房间用户</el-radio>
+              </template>
+            </template>
+          </data-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import topInfo from '@/layout/public/top.vue';
+import dataForm from '@/components/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: roomuser } = createNamespacedHelpers('roomuser');
+export default {
+  name: 'detail',
+  props: {},
+  components: {
+    topInfo,
+    dataForm,
+  },
+  data: function() {
+    return {
+      drawerIsNew: false,
+      form: {},
+      fields: [
+        { label: '姓名', prop: 'name', model: 'name' },
+        { label: '电话', prop: 'phone', model: 'phone' },
+        { label: '密码', prop: 'passwd', model: 'passwd', type: 'paswword' },
+        { label: '机构名称', prop: 'deptname', model: 'deptname' },
+        { label: '职务', prop: 'level', model: 'level' },
+        { label: '个人简介', prop: 'title', model: 'title', type: 'textarea' },
+        { label: '备注', prop: 'remark', model: 'remark', type: 'textarea' },
+        { label: '用戶类型', required: true, model: 'role', type: 'radio' },
+      ],
+      rules: {
+        name: [{ required: true, message: '请输入姓名' }],
+        passwd: [{ required: false, message: '请输入密码' }],
+        phone: [{ required: true, message: '请输入电话' }],
+        deptname: [{ required: true, message: '请输入机构名称' }],
+        role: [{ required: true, message: '请选择用户类型' }],
+      },
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...roomuser(['query', 'delete', 'update', 'create', 'fetch']),
+    async search() {
+      if (this.id) {
+        let res = await this.fetch(this.id);
+        this.$set(this, `form`, res.data);
+      }
+    },
+    // 创建&修改
+    async drawerSave({ data, isNew }) {
+      let res;
+      let msg;
+      if (this.isNew) {
+        res = await this.update(data);
+        msg = `${this.keyWord}修改成功`;
+      } else {
+        res = await this.create(data);
+        msg = `${this.keyWord}添加成功`;
+      }
+      if (this.$checkRes(res, msg)) this.$router.push({ path: './index' });
+    },
+  },
+  computed: {
+    id() {
+      return this.$route.query.id;
+    },
+    isNew() {
+      return this.$route.query.id ? true : false;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    mainTitle() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      let sub = meta.sub || '';
+      return `${main}${sub}`;
+    },
+    keyWord() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      return main;
+    },
+  },
+
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    isNew: {
+      handler(val) {
+        if (!val) {
+          this.search();
+        }
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 98 - 0
src/views/anchor/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="add" style="text-align:right">
+          <el-button size="mini" type="primary" @click="$router.push({ path: './detail' })" icon="el-icon-plus">添加用户</el-button>
+        </el-col>
+      </el-col>
+      <el-col :span="24" class="info">
+        <data-table :fields="fields" @delete="toDelete" :data="list" :opera="opera" @edit="toEdit" :total="total" @query="search"></data-table>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+import topInfo from '@/layout/public/top.vue';
+import dataTable from '@/components/data-table.vue';
+import { mapActions, mapState, createNamespacedHelpers } from 'vuex';
+
+const { mapActions: roomuser } = createNamespacedHelpers('roomuser');
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    topInfo,
+    dataTable,
+  },
+  data: () => ({
+    opera: [
+      {
+        label: '编辑',
+        icon: 'el-icon-edit',
+        method: 'edit',
+      },
+      {
+        label: '删除',
+        icon: 'el-icon-delete',
+        method: 'delete',
+        confirm: true,
+      },
+    ],
+    fields: [
+      { label: '姓名', prop: 'name', filter: 'input' },
+      { label: '电话', prop: 'phone', filter: 'input' },
+      { label: '用戶类型', prop: 'role', format: i => (i == '3' ? '主播' : i == '4' ? '房间用户' : '临时用户') },
+      // { label: '状态', prop: 'status', format: i => (i == '0' ? '待审核' : i == '1' ? '审核成功' : i == '2' ? '审核拒绝' : '待认证') },
+      { label: '机构名称', prop: 'deptname' },
+      { label: '职务', prop: 'level' },
+      { label: '个人简介', prop: 'title' },
+      { label: '备注', prop: 'remark' },
+    ],
+    list: [],
+    total: 0,
+  }),
+  created() {
+    this.search();
+  },
+  methods: {
+    ...roomuser(['query', 'delete', 'update']),
+
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info.role = '3';
+      const res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    toEdit({ data }) {
+      this.$router.push({ path: './detail', query: { id: data.id } });
+    },
+    async toDelete({ data }) {
+      const res = await this.delete(data.id);
+      this.$checkRes(res, '删除成功', '删除失败');
+      this.search();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+<style lang="less" scoped>
+.add {
+  height: 40px;
+  line-height: 35px;
+  padding: 0 15px;
+}
+</style>

+ 109 - 0
src/views/contact/index.vue

@@ -0,0 +1,109 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <el-form :model="form" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" v-if="loading">
+            <el-form-item label="标题" prop="title">
+              <el-input v-model="form.title"></el-input>
+            </el-form-item>
+            <el-form-item label="内容" prop="content">
+              <wang-editor v-model="form.content"></wang-editor>
+            </el-form-item>
+            <el-form-item label="发布时间" prop="publish_time">
+              <el-date-picker
+                v-model="form.publish_time"
+                type="datetime"
+                style="width:100%"
+                placeholder="请选择发布时间"
+                format="yyyy-MM-dd HH:mm"
+                value-format="yyyy-MM-dd HH:mm"
+              >
+              </el-date-picker>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="submitForm()">保存</el-button>
+            </el-form-item>
+          </el-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import wangEditor from '@/components/wang-editor.vue';
+import topInfo from '@/layout/public/top.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: contact } = createNamespacedHelpers('contact');
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    topInfo,
+    wangEditor,
+  },
+  data: function() {
+    return {
+      form: {},
+      rules: {},
+      loading: true,
+    };
+  },
+  created() {
+    this.searchInfo();
+  },
+  methods: {
+    ...contact(['query', 'update', 'create']),
+    async searchInfo({ skip = 0, limit = 1, ...info } = {}) {
+      this.$set(this, `loading`, false);
+      let res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        if (res.data.length > 0) {
+          this.$set(this, `form`, res.data[0]);
+        }
+        this.$set(this, `loading`, true);
+      }
+    },
+    async submitForm() {
+      if (this.form.id) {
+        const res = await this.update(this.form);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '更新信息成功',
+            type: 'success',
+          });
+          this.searchInfo();
+        }
+      } else {
+        const res = await this.create(this.form);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '添加信息成功',
+            type: 'success',
+          });
+          this.searchInfo();
+        }
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+// .main {
+//   padding: 0 50% 0 0;
+// }
+</style>

+ 254 - 0
src/views/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24">
+        <el-col :span="24" class="top"> </el-col>
+        <el-col :span="24" class="main">
+          <div id="canvas"></div>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { Bar, StackedBar, Donut, Column, StateManager } from '@antv/g2plot';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: room } = createNamespacedHelpers('room');
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      roomInfo: [],
+    };
+  },
+  async created() {
+    await this.search();
+  },
+  // mounted() {
+  //   this.tongji();
+  // },
+  methods: {
+    ...room(['query']),
+    async search() {
+      const data = await this.query();
+      this.$set(this, `roomInfo`, data.data);
+      this.tongji();
+    },
+    tongji() {
+      const container = document.getElementById('canvas');
+      const chartsWraper = document.createElement('div');
+      chartsWraper.style.width = '100%';
+      chartsWraper.style.height = '900px';
+      container.appendChild(chartsWraper);
+      const canvasDiv1 = document.createElement('div');
+      canvasDiv1.style.width = '40%';
+      canvasDiv1.style.height = '300px';
+      canvasDiv1.style.marginLeft = '0px';
+      canvasDiv1.style.marginTop = '0px';
+      canvasDiv1.style.float = 'left';
+      canvasDiv1.id = 'canvas1';
+      chartsWraper.appendChild(canvasDiv1);
+
+      const canvasDiv2 = document.createElement('div');
+      canvasDiv2.style.width = '40%';
+      canvasDiv2.style.height = '300px';
+      canvasDiv2.style.marginLeft = '10px';
+      canvasDiv2.style.marginTop = '0px';
+      canvasDiv2.style.float = 'left';
+      canvasDiv2.id = 'canvas2';
+      chartsWraper.appendChild(canvasDiv2);
+
+      const canvasDiv3 = document.createElement('div');
+      canvasDiv3.style.width = '90%';
+      canvasDiv3.style.height = '300px';
+      canvasDiv3.style.marginLeft = '10px';
+      canvasDiv3.style.marginTop = '0px';
+      canvasDiv3.style.float = 'left';
+      canvasDiv3.id = 'canvas3';
+      chartsWraper.appendChild(canvasDiv3);
+
+      const data = [];
+      for (const room of this.roomInfo) {
+        data.push({ room: room.name, count: room.number });
+      }
+      // 初始化state manager
+      const stateManager = new StateManager();
+      //渲染图表
+      const bar = new StackedBar(canvasDiv1, {
+        title: {
+          visible: true,
+          text: '房间观看人数',
+        },
+        data,
+        xField: 'count',
+        yField: 'room',
+        tooltip: {
+          visible: false,
+        },
+        xAxis: {
+          formatter: v => v + '人',
+        },
+        color: ['#945fb9', '#1e9493', '#ff9845'],
+      });
+      bar.render();
+      bar.bindStateManager(stateManager, {
+        setState: [
+          {
+            event: 'bar:click',
+            state: e => {
+              const origin = e.target.get('origin').data;
+              const state = { name: 'room', exp: origin.room };
+              return state;
+            },
+          },
+        ],
+        onStateChange: [
+          {
+            name: 'room',
+            callback: (d, plot) => {
+              plot.setSelected(d, {
+                stroke: 'black',
+                lineWidth: 1,
+              });
+              plot.setDefault(
+                origin => {
+                  return origin[d.name] !== d.exp;
+                },
+                {
+                  stroke: null,
+                }
+              );
+            },
+          },
+        ],
+      });
+      const donut = new Donut(canvasDiv2, {
+        title: {
+          visible: true,
+          text: '房间观看总数',
+        },
+        data,
+        angleField: 'count',
+        colorField: 'room',
+        label: {
+          visible: false,
+        },
+        radius: 0.9,
+        annotation: [{ type: 'centralText', onActive: true }],
+      });
+      donut.render();
+      donut.bindStateManager(stateManager, {
+        setState: [
+          {
+            event: 'donut:click',
+            state: e => {
+              const origin = e.target.get('origin').data;
+              const state = { name: 'room', exp: origin.room };
+              return state;
+            },
+          },
+        ],
+        onStateChange: [
+          {
+            name: 'room',
+            callback: (d, plot) => {
+              plot.setSelected(d, {
+                strokeStyle: '#000000',
+                lineWidth: 1,
+              });
+              plot.setDefault(
+                origin => {
+                  return origin[d.name] !== d.exp;
+                },
+                {
+                  stroke: null,
+                }
+              );
+            },
+          },
+        ],
+      });
+
+      const area = new Column(canvasDiv3, {
+        title: {
+          visible: true,
+          text: '房间观看趋势',
+        },
+        forceFit: true,
+        data,
+        xField: 'room',
+        yField: 'count',
+        padding: 'auto',
+        meta: {
+          type: {
+            alias: '房间',
+          },
+          sales: {
+            alias: '人数',
+          },
+        },
+      });
+      area.render();
+
+      area.bindStateManager(stateManager, {
+        setState: [
+          {
+            event: 'area:click',
+            state: e => {
+              const origin = e.target.get('origin').data;
+              const state = { name: 'room', exp: origin.room };
+              return state;
+            },
+          },
+        ],
+        onStateChange: [
+          {
+            name: 'room',
+            callback: (d, plot) => {
+              plot.setSelected(d, {
+                strokeStyle: '#000000',
+                lineWidth: 1,
+              });
+              plot.setDefault(
+                origin => {
+                  return origin[d.name] !== d.exp;
+                },
+                {
+                  stroke: null,
+                }
+              );
+            },
+          },
+        ],
+      });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.transition-box {
+  margin-bottom: 10px;
+  width: 200px;
+  height: 100px;
+  border-radius: 4px;
+  background-color: #409eff;
+  text-align: center;
+  color: #fff;
+  padding: 40px 20px;
+  box-sizing: border-box;
+  margin-right: 20px;
+}
+</style>

+ 74 - 0
src/views/live/detail.vue

@@ -0,0 +1,74 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <detailInfo :roomInfo="roomInfo"></detailInfo>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import topInfo from '@/layout/public/top.vue';
+import detailInfo from '@/layout/live/detailInfo.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: room } = createNamespacedHelpers('room');
+export default {
+  name: 'detail',
+  props: {},
+  components: {
+    topInfo,
+    detailInfo,
+  },
+  data: function() {
+    return {
+      roomInfo: {},
+    };
+  },
+  created() {
+    this.searchInfo();
+  },
+  methods: {
+    ...room(['query', 'delete', 'update', 'fetch']),
+    async searchInfo() {
+      let res = await this.fetch(this.id);
+      if (this.$checkRes(res)) {
+        this.$set(this, `roomInfo`, res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    mainTitle() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      let sub = meta.sub || '';
+      return `${main}${sub}`;
+    },
+    keyWord() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      return main;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 59 - 0
src/views/live/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+      </el-col>
+      <el-col :span="24" class="info">
+        <liveList :list="list"></liveList>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import topInfo from '@/layout/public/top.vue';
+import liveList from '@/layout/live/liveList.vue';
+import { mapActions, mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: room } = createNamespacedHelpers('room');
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    topInfo,
+    liveList,
+  },
+  data: function() {
+    return {
+      list: [],
+    };
+  },
+  created() {
+    this.searchInfo();
+    console.log(this.user);
+  },
+  methods: {
+    ...room(['query', 'delete', 'update', 'fetch']),
+    async searchInfo({ skip = 0, limit = 10, ...info } = {}) {
+      let res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        var arr = res.data.filter(item => item.status != '2');
+        this.$set(this, `list`, arr);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 74 - 0
src/views/live/meetingDetail.vue

@@ -0,0 +1,74 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <detailMeetInfo :roomInfo="roomInfo"></detailMeetInfo>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import topInfo from '@/layout/public/top.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+import detailMeetInfo from '@/layout/live/detailmetting.vue';
+const { mapActions: room } = createNamespacedHelpers('room');
+export default {
+  name: 'detail',
+  props: {},
+  components: {
+    topInfo,
+    detailMeetInfo,
+  },
+  data: function() {
+    return {
+      roomInfo: {},
+    };
+  },
+  created() {
+    this.searchInfo();
+  },
+  methods: {
+    ...room(['query', 'delete', 'update', 'fetch']),
+    async searchInfo() {
+      let res = await this.fetch(this.id);
+      if (this.$checkRes(res)) {
+        this.$set(this, `roomInfo`, res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    name() {
+      return this.$route.query.name;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    mainTitle() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      let sub = meta.sub || '';
+      return `${main}${sub}`;
+    },
+    keyWord() {
+      let meta = this.$route.meta;
+      let main = meta.title || '';
+      return main;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 273 - 0
src/views/liveIndex.vue

@@ -0,0 +1,273 @@
+<template>
+  <div id="liveIndex">
+    <el-row>
+      <el-col :span="24" class="style">
+        <el-col :span="24" class="top">
+          <el-col :span="20" class="left">
+            <el-image :src="logo"></el-image>
+            <span>叁多健康平台</span>
+            <span>房间号: {{ roomname_ }}</span>
+            <span>用户: {{ username_ }}</span>
+          </el-col>
+          <el-col :span="4" class="right">
+            <el-button type="text"><i class="el-icon-close"></i>退出</el-button>
+          </el-col>
+        </el-col>
+        <el-col :span="24" class="down">
+          <el-col :span="18" class="left">
+            <el-col :span="24" class="video">
+              <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+            </el-col>
+            <el-col :span="1" class="videoBtn">
+              <p><i class="iconfont iconshexiangtou"></i></p>
+              <p><i class="iconfont iconmaikefeng-tianchong"></i></p>
+              <p><i class="iconfont iconicon-test1"></i></p>
+            </el-col>
+          </el-col>
+          <el-col :span="5" class="right">
+            <el-col :span="24" class="title">
+              学生进入10001教室即可听课
+            </el-col>
+            <el-col :span="24" class="info">
+              <el-tabs v-model="activeName">
+                <el-tab-pane label="信息列表" name="first">
+                  <el-col :span="24" class="firstInfo">
+                    <el-col :span="24" class="firstInfoList">
+                      <el-col :span="24" class="list" v-for="(item, index) in list" :key="index">
+                        <p>
+                          <span>[{{ item.date }}]</span>
+                          <span>{{ item.name }}:</span>
+                          <span>{{ item.content }}</span>
+                        </p>
+                      </el-col>
+                    </el-col>
+                    <el-col :span="24" class="firstInfoInout">
+                      <el-input placeholder="请输入内容" v-model="input" class="input-with-select">
+                        <el-button slot="append">发送</el-button>
+                      </el-input>
+                    </el-col>
+                  </el-col>
+                </el-tab-pane>
+                <el-tab-pane label="成员列表" name="second">
+                  <el-col :span="24" class="secondInfoList">
+                    <el-table :data="personalList" border style="width: 100%" :highlight-current-row="false">
+                      <el-table-column type="index" width="50" label="序号" align="center"> </el-table-column>
+                      <el-table-column label="姓名" align="center">
+                        <template slot-scope="scope">
+                          <el-image :src="scope.row.pic" style="width:20px;height:20px;top: 5px;margin: 0 10px 0 0;"></el-image>
+                          <span>{{ scope.row.name }}</span>
+                        </template>
+                      </el-table-column>
+                    </el-table>
+                  </el-col>
+                </el-tab-pane>
+              </el-tabs>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'liveIndex',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      logo: require('@a/logo.png'),
+      activeName: 'first',
+      // 聊天列表
+      list: [
+        {
+          date: '202-01-01',
+          name: '顾宏伟',
+          content: '信息内容信息内容信息内容信息内容信息内容信息内容信息内容信息内容信息内容信息内容信息内容信息内容',
+        },
+      ],
+      // 发言
+      input: '',
+      // 成员列表
+      personalList: [
+        {
+          pic: require('@a/logo.png'),
+          name: '顾红伟',
+        },
+      ],
+      username_: '',
+      roomname_: '',
+      client_: '',
+      localStream_: '',
+      shareClient_: '',
+      shareStream_: '',
+      sdkAppId_: '1400380125',
+      userId_: '',
+    };
+  },
+  created() {
+    this.getRoomInfo();
+  },
+  methods: {
+    async getRoomInfo() {
+      this.roomname_ = this.user.roomname;
+      this.username_ = this.user.name;
+      const res = await this.roomfetch(this.user.roomid);
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        this.$set(this, `roomdetail`, res.data);
+        if (res.data.anchorid === this.user.uid) {
+          this.zjrshow = true;
+        }
+        for (const elm of res.data.zjr) {
+          const ru = await this.roomuserfetch(elm);
+          if (this.$checkRes(ru)) {
+            const newdata = { zjrid: elm, zjrname: ru.data.name };
+            this.zjrList.push(newdata);
+          }
+        }
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.style {
+  background-color: #222124;
+  height: 100vh;
+  overflow: hidden;
+  .top {
+    height: 60px;
+    line-height: 60px;
+    background-color: #34363b;
+    margin: 0 0 15px 0;
+    .left {
+      .el-image {
+        width: 40px;
+        height: 40px;
+        width: 40px;
+        height: 40px;
+        top: 10px;
+        left: 10px;
+        margin: 0 10px 0 0;
+      }
+      span {
+        color: #ccc;
+        padding: 0 15px;
+      }
+    }
+    .right {
+      text-align: right;
+      padding: 0 15px 0px 0;
+    }
+  }
+  .down {
+    height: 100vh;
+    .left {
+      width: 78%;
+      height: 100%;
+      margin: 0 15px 0 0;
+      background-color: #2c2e32;
+      position: relative;
+      .videoBtn {
+        position: absolute;
+        top: 30%;
+        right: 10px;
+        background: #222124;
+        text-align: center;
+        min-height: 100px;
+        p {
+          margin: 15px 0;
+          .iconfont {
+            font-size: 30px;
+            color: #ccc;
+          }
+        }
+      }
+    }
+    .right {
+      height: 100vh;
+      background-color: #2c2e32;
+      .title {
+        height: 50px;
+        line-height: 50px;
+        color: #ccc;
+        text-align: center;
+        border-bottom: 1px solid #cccccc5f;
+      }
+      .info {
+        .firstInfo {
+          padding: 0 10px;
+          height: 100%;
+          .firstInfoList {
+            min-height: 435px;
+            max-height: 435px;
+            overflow-y: auto;
+            color: #ccc;
+            .list {
+              margin: 0 0 5px 0;
+            }
+          }
+          .firstInfoInout {
+            margin-bottom: 5px;
+          }
+        }
+        .secondInfoList {
+          padding: 0 10px;
+          height: 775px;
+          overflow-y: auto;
+          .personalList {
+          }
+        }
+      }
+    }
+  }
+}
+/deep/.el-tabs__item {
+  color: #ccc;
+}
+/deep/ .el-tabs--top .el-tabs__item.is-top:nth-child(2) {
+  padding-left: 20px;
+}
+/deep/.el-tabs--top .el-tabs__item.is-top:last-child {
+  padding-right: 20px;
+}
+/deep/.el-table td {
+  padding: 5px 0;
+  color: #ccc;
+}
+/deep/.el-table th {
+  padding: 5px 0;
+  background: #000;
+}
+/deep/.el-table tr {
+  background-color: transparent;
+}
+/deep/.el-table {
+  background-color: transparent;
+}
+.el-table {
+  /deep/tbody tr:hover > td {
+    background-color: #409eff;
+  }
+}
+#main-video {
+  float: left;
+  width: 100%;
+  height: 640px;
+  min-height: 600px;
+  grid-area: 1/1/3/4;
+}
+</style>

+ 218 - 0
src/views/livecheck.vue

@@ -0,0 +1,218 @@
+<template>
+  <div id="livecheck">
+    <el-row id="test-content" class="container">
+      <div class="header">
+        <h2 class="title">WEBRTC 能力测试</h2>
+        <el-button class="btn btn-info btn-circle btn-xl" type="danger" size="mini" @click="checkstart">开始</el-button>
+      </div>
+      <div class="header">
+        <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
+      </div>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: gensign } = createNamespacedHelpers('gensign');
+import TRTC from 'trtc-js-sdk';
+export default {
+  name: 'livecheck',
+  props: {},
+  components: {},
+  data: function() {
+    return {};
+  },
+  created() {},
+  methods: {
+    ...gensign(['gensignFetch']),
+    async checkstart() {
+      TRTC.checkSystemRequirements().then(result => {
+        if (!result) {
+          alert('Your browser is not compatible with TRTC');
+        }
+      });
+      await this.initclient();
+    },
+    async initclient() {
+      const userid_ = 'testlivecheck';
+      const res = await this.gensignFetch({ userid: userid_ });
+      if (this.$checkRes(res)) {
+        console.log(res.data);
+        const client_ = TRTC.createClient({
+          mode: 'live',
+          sdkAppId: '1400380125',
+          userId: userid_,
+          userSig: res.data,
+        });
+        await client_.join({ roomId: 9999, role: 'anchor' });
+        const localStream_ = await TRTC.createStream({
+          audio: true,
+          video: true,
+          userId: userid_,
+        });
+        localStream_.setVideoProfile('480p');
+        await localStream_.initialize();
+        await client_.publish(localStream_);
+        localStream_.play('main-video');
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+body {
+  /* padding: 50px; */
+  font: 14px 'Lucida Grande', Helvetica, Arial, sans-serif;
+}
+
+a {
+  color: #00b7ff;
+}
+
+.box {
+  width: 100%;
+  align-items: center;
+}
+.detail-box {
+  max-width: 640px;
+  width: 100%;
+  height: auto;
+  background: #e2e2e2;
+  margin-top: 2px;
+  font-family: Consolas;
+  font-size: 16px;
+  padding-top: 15px;
+  padding-left: 15px;
+  padding-bottom: 15px;
+  margin: auto;
+}
+#main-video {
+  width: 700px;
+  height: 500px;
+  grid-area: 1/1/3/4;
+  background-color: #000;
+}
+
+#test-content {
+  display: flex;
+  display: -webkit-flex;
+  align-items: center;
+  flex-direction: column;
+}
+
+.title {
+  width: 600px;
+  height: 70px;
+  color: white;
+  align-items: center;
+  display: flex;
+  padding-left: 20px;
+}
+.container {
+  padding: 0 !important;
+}
+.header {
+  display: flex;
+  flex-direction: row;
+  background: #4f7dc9;
+  max-width: 700px;
+  margin-bottom: 20px;
+  align-items: center;
+  width: 100%;
+  margin: auto;
+  padding: 0 10px;
+}
+.btn-circle {
+  width: 30px;
+  height: 30px;
+  text-align: center;
+  padding: 6px 0;
+  font-size: 12px;
+  line-height: 1.428571429;
+  border-radius: 15px;
+}
+.btn-circle.btn-lg {
+  width: 50px;
+  height: 50px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 25px;
+}
+.btn-circle.btn-xl {
+  width: 70px;
+  height: 70px;
+  padding: 10px 16px;
+  font-size: 24px;
+  line-height: 1.33;
+  border-radius: 35px;
+}
+
+#start-btn {
+  display: flex;
+  font-size: 20px;
+  color: black;
+  background: #99dd99;
+}
+
+#detail-info {
+  display: flex;
+  display: -webkit-flex;
+  -webkit-flex-direction: column;
+}
+
+.title {
+  color: black;
+  font-size: 20px;
+}
+
+#stunserver {
+}
+
+div#meters {
+  padding: 10px;
+}
+
+div#meters > div {
+  margin: 0 0 0 0;
+}
+
+div#meters div.label {
+  display: inline-block;
+  font-weight: 400;
+  margin: 0 0.5em 0 0;
+  width: 5em;
+  color: #000;
+}
+
+div#meters div.value {
+  display: inline-block;
+}
+
+meter {
+  width: 50%;
+}
+
+meter#clip {
+  color: #db4437;
+}
+
+meter#slow {
+  color: #f4b400;
+}
+
+meter#instant {
+  color: #0f9d58;
+}
+</style>

+ 105 - 0
src/views/login copy.vue

@@ -0,0 +1,105 @@
+<template>
+  <div id="login">
+    <el-row>
+      <el-col :span="24" class="style">
+        <el-col :span="24" class="login">
+          <el-col :span="24" class="top">
+            登录
+          </el-col>
+          <el-col :span="24" class="form">
+            <el-form :model="form" :rules="rules" ref="form" label-width="100px" class="demo-ruleForm">
+              <el-form-item prop="phone">
+                <span slot="label">手机号</span>
+                <el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11"></el-input>
+              </el-form-item>
+              <el-form-item prop="passwd">
+                <span slot="label">密码</span>
+                <el-input v-model="form.passwd" placeholder="请输入密码" show-password></el-input>
+              </el-form-item>
+              <el-col :span="24" style="text-align:center;">
+                <el-button type="primary" @click="resetForm('form')">重置</el-button>
+                <el-button type="success" @click="submitForm('form')">登录</el-button>
+              </el-col>
+            </el-form>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapActions, mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: login } = createNamespacedHelpers('login');
+export default {
+  name: 'login',
+  props: {},
+  components: {},
+  data: () => ({
+    form: {},
+    rules: {
+      phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+      passwd: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+    },
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    ...login({ toLogin: 'login' }),
+    submitForm(formName) {
+      this.$refs[formName].validate(async valid => {
+        if (valid) {
+          let res = await this.toLogin({ user: this.form });
+          if (res.id) {
+            this.$router.push('/');
+          }
+        } else {
+          console.log('error submit!!');
+          return false;
+        }
+      });
+    },
+    resetForm(formName) {
+      this.$refs[formName].resetFields();
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.style {
+  background-image: url('../assets/login.png');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+  width: 100%;
+  height: 100vh;
+  .login {
+    position: relative;
+    top: 32%;
+    left: 43%;
+    width: 450px;
+    height: 400px;
+    background-color: #ffffff5f;
+    border-radius: 10px;
+    .top {
+      text-align: center;
+      color: #fff;
+      font-size: 40px;
+      text-shadow: cornflowerblue 3px 3px 3px;
+      font-family: cursive;
+      padding: 35px 0;
+    }
+    .form {
+      padding: 25px 50px;
+    }
+  }
+}
+/deep/.el-form-item__label {
+  font-size: 24px;
+  font-family: 楷体;
+  color: #fff;
+}
+/deep/.el-form-item {
+  margin-bottom: 30px;
+}
+</style>

+ 161 - 0
src/views/login.vue

@@ -0,0 +1,161 @@
+<template>
+  <div id="login">
+    <el-row>
+      <el-col :span="24">
+        <div class="bg">
+          <div class="main">
+            <div class="left">
+              <span>直播管理平台</span>
+            </div>
+            <div class="right">
+              <el-col :span="24" class="title">
+                <span>用户登录</span>
+              </el-col>
+              <el-col :span="24" class="form">
+                <el-form :model="form" :rules="rules" ref="form" class="demo-ruleForm">
+                  <!-- <el-form-item prop="roomid" class="roomid">
+                    <span slot="label"><el-image :src="image.roomid"></el-image></span>
+                    <el-input v-model="form.roomid" placeholder="请输入房间号"></el-input>
+                  </el-form-item> -->
+                  <el-form-item prop="phone">
+                    <span slot="label"><el-image :src="image.phone"></el-image></span>
+                    <el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11"></el-input>
+                  </el-form-item>
+                  <el-form-item prop="passwd">
+                    <span slot="label"><el-image :src="image.password"></el-image></span>
+                    <el-input v-model="form.passwd" placeholder="请输入密码" show-password></el-input>
+                  </el-form-item>
+                  <el-col :span="24" style="text-align:center;">
+                    <el-button type="primary" @click="resetForm('form')">重置</el-button>
+                    <el-button type="success" @click="submitForm('form')">登录</el-button>
+                  </el-col>
+                </el-form>
+              </el-col>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapActions, mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: login } = createNamespacedHelpers('login');
+export default {
+  name: 'login',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      form: {},
+      rules: {
+        roomid: [{ required: false, message: '请输入房间号', trigger: 'blur' }],
+        phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+        passwd: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+      },
+      image: {
+        roomid: require('@/assets/roomid.png'),
+        phone: require('@/assets/user.png'),
+        password: require('@/assets/password.png'),
+      },
+    };
+  },
+  created() {},
+  methods: {
+    ...login({ toLogin: 'login' }),
+    submitForm(formName) {
+      this.$refs[formName].validate(async valid => {
+        if (valid) {
+          let res = await this.toLogin({ user: this.form });
+          if (res.id) {
+            this.$router.push('/');
+            // if (res.type == '0') {
+            //   this.$router.push('/');
+            // } else {
+            //   this.$router.push('/liveIndex');
+            // }
+          }
+        } else {
+          console.log('error submit!!');
+          return false;
+        }
+      });
+    },
+    resetForm(formName) {
+      this.$refs[formName].resetFields();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.bg {
+  width: 100%;
+  height: 100vh;
+  background-image: url('../assets/bg3.jpg');
+  background-size: 100% 100%;
+  background-position: center center;
+}
+.main {
+  position: absolute;
+  top: 30%;
+  left: 30%;
+  width: 660px;
+  height: 370px;
+}
+.main .left {
+  float: left;
+  width: 330px;
+  height: 370px;
+  text-align: center;
+  background: #0000004f;
+}
+.main .left span {
+  font-size: 25px;
+  color: #fff;
+  font-weight: bold;
+  font-family: cursive;
+  padding: 36% 0;
+  display: inline-block;
+  line-height: 40px;
+}
+.main .right {
+  float: left;
+  width: 330px;
+  height: 370px;
+  background: #ffffff5f;
+  .title {
+    text-align: center;
+    padding: 20px 0;
+    margin: 0 0 20px 0;
+    border-bottom: 1px solid #ccc;
+    span {
+      font-size: 20px;
+      color: #0635ab;
+      font-weight: bold;
+    }
+  }
+  .form {
+    padding: 0 10px;
+  }
+}
+/deep/.el-input {
+  width: 80%;
+}
+/deep/.el-image {
+  top: 5px;
+}
+/deep/.roomid .el-form-item__label {
+  padding: 0 13px 0 10px;
+}
+</style>

+ 143 - 0
src/views/meetingBrief/detail.vue

@@ -0,0 +1,143 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <el-col :span="24" class="btn">
+            <el-button type="primary" size="mini" @click="back()">返回</el-button>
+          </el-col>
+          <el-col :span="24">
+            <el-form :model="form" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" v-if="loading">
+              <el-form-item label="标题" prop="title">
+                <el-input v-model="form.title"></el-input>
+              </el-form-item>
+              <el-form-item label="信息图片" prop="filedir">
+                <upload :limit="1" :data="form.filedir" type="filedir" :url="'/files/filedir/upload'" @upload="uploadSuccess"></upload>
+              </el-form-item>
+              <el-form-item label="类型" prop="type">
+                <el-select v-model="form.type" placeholder="请选择信息类型">
+                  <el-option label="会议简介" value="0"></el-option>
+                  <el-option label="会议日程" value="1"></el-option>
+                  <el-option label="主办方介绍" value="2"></el-option>
+                  <el-option label="协办方介绍" value="3"></el-option>
+                  <el-option label="专家介绍" value="4"></el-option>
+                  <el-option label="继续再教育申请表" value="5"></el-option>
+                  <el-option label="温馨提示" value="6"></el-option>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="内容" prop="content">
+                <wang-editor v-model="form.content"></wang-editor>
+              </el-form-item>
+              <el-form-item label="发布时间" prop="publish_time">
+                <el-date-picker
+                  v-model="form.publish_time"
+                  type="datetime"
+                  style="width:100%"
+                  placeholder="请选择发布时间"
+                  format="yyyy-MM-dd HH:mm"
+                  value-format="yyyy-MM-dd HH:mm"
+                >
+                </el-date-picker>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" @click="submitForm()">保存</el-button>
+              </el-form-item>
+            </el-form>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import upload from '@/components/upload.vue';
+import wangEditor from '@/components/wang-editor.vue';
+import topInfo from '@/layout/public/top.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: news } = createNamespacedHelpers('news');
+export default {
+  name: 'detail',
+  props: {},
+  components: {
+    topInfo,
+    wangEditor,
+    upload,
+  },
+  data: function() {
+    return {
+      form: {},
+      rules: {},
+      loading: true,
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...news(['create', 'fetch', 'update']),
+    async search() {
+      if (this.id) {
+        this.$set(this, `loading`, false);
+        let res = await this.fetch(this.id);
+        if (this.$checkRes(res)) {
+          this.$set(this, `form`, res.data);
+        }
+        this.$set(this, `loading`, true);
+      }
+    },
+    async submitForm() {
+      if (this.id) {
+        let res = await this.update(this.form);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '修改信息成功',
+            type: 'success',
+          });
+          this.back();
+        }
+      } else {
+        let res = await this.create(this.form);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '添加信息成功',
+            type: 'success',
+          });
+          this.back();
+        }
+      }
+    },
+    // 返回
+    back() {
+      this.$router.push({ path: '/meetingBrief/index' });
+    },
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  .btn {
+    text-align: right;
+    padding: 10px 0;
+  }
+}
+</style>

+ 162 - 0
src/views/meetingBrief/index.vue

@@ -0,0 +1,162 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+        <el-col :span="24" class="main">
+          <el-col :span="24" class="btn">
+            <el-button type="primary" size="mini" @click="add()">增加</el-button>
+          </el-col>
+          <el-col :span="24">
+            <data-table :fields="fields" @delete="toDelete" :data="list" :opera="opera" @edit="toEdit" :total="total" @query="search">
+              <template #options="{item}">
+                <template v-if="item.prop === 'type'">
+                  <el-option v-for="(i, index) in typeList" :key="index" :label="i.label" :value="i.value"></el-option>
+                </template>
+              </template>
+            </data-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import topInfo from '@/layout/public/top.vue';
+import dataTable from '@/components/data-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: news } = createNamespacedHelpers('news');
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    topInfo,
+    dataTable,
+  },
+  data: function() {
+    return {
+      opera: [
+        {
+          label: '修改',
+          icon: 'el-icon-edit',
+          method: 'edit',
+        },
+        {
+          label: '删除',
+          icon: 'el-icon-delete',
+          method: 'delete',
+          confirm: true,
+        },
+      ],
+      fields: [
+        { label: '标题', prop: 'title', filter: 'input' },
+        {
+          label: '类型',
+          prop: 'type',
+          filter: 'select',
+          format: true,
+          format: i =>
+            i == '0'
+              ? '会议简介'
+              : i == '1'
+              ? '会议日程'
+              : i == '2'
+              ? '主办方介绍'
+              : i == '3'
+              ? '协办方介绍'
+              : i == '4'
+              ? '专家介绍'
+              : i == '5'
+              ? '继续再教育申请表'
+              : i == '6'
+              ? '温馨提示'
+              : '',
+        },
+        { label: '发布时间', prop: 'publish_time' },
+        { label: '状态', prop: 'status', format: i => (i == '0' ? '开启' : i == '1' ? '禁用' : '暂无') },
+      ],
+      list: [],
+      total: 0,
+      typeList: [
+        {
+          label: '会议介绍',
+          value: '0',
+        },
+        {
+          label: '会议日程',
+          value: '1',
+        },
+        {
+          label: '主办方介绍',
+          value: '2',
+        },
+        {
+          label: '协办方介绍',
+          value: '3',
+        },
+        {
+          label: '专家介绍',
+          value: '4',
+        },
+        {
+          label: '继续再教育申请表',
+          value: '5',
+        },
+        {
+          label: '温馨提示',
+          value: '6',
+        },
+      ],
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...news(['query', 'delete', 'update']),
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      let res = await this.query({ skip, limit, ...info });
+      this.$set(this, `list`, res.data);
+      this.$set(this, `total`, res.total);
+    },
+    toEdit({ data }) {
+      this.$router.push({ path: '/meetingBrief/detail', query: { id: data.id } });
+    },
+    async toDelete({ data }) {
+      let res = await this.delete(data.id);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '删除信息成功',
+          type: 'success',
+        });
+        this.search();
+      }
+    },
+    // 增加
+    add() {
+      this.$router.push({ path: '/meetingBrief/detail' });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  .btn {
+    text-align: right;
+    padding: 10px 0;
+  }
+}
+</style>

+ 45 - 0
src/views/question/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="index">
+        <el-col :span="24" class="top">
+          <topInfo :topTitle="pageTitle"></topInfo>
+        </el-col>
+      </el-col>
+      <el-col :span="24">
+        <el-tabs v-model="activeName" @tab-click="handleClick">
+          <el-tab-pane label="问卷统计" name="first"><tongji></tongji></el-tab-pane>
+          <el-tab-pane label="问卷管理" name="second"><wenjuan></wenjuan></el-tab-pane>
+          <el-tab-pane label="题库管理" name="third"><tiku></tiku></el-tab-pane>
+        </el-tabs>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+import topInfo from '@/layout/public/top.vue';
+import tongji from '@/views/question/part/tongji.vue';
+import wenjuan from '@/views/question/part/wenjuan.vue';
+import tiku from '@/views/question/part/tiku.vue';
+import { mapActions, mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: { topInfo, tongji, wenjuan, tiku },
+  data: () => ({
+    activeName: 'first',
+  }),
+  created() {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  methods: {
+    handleClick() {},
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 196 - 0
src/views/question/part/tiku.vue

@@ -0,0 +1,196 @@
+<template>
+  <div id="tiku">
+    <el-row>
+      <el-col class="add" :span="24">
+        <el-select v-model="type" placeholder="请选择题目类型">
+          <el-option v-for="item in typeoptions" :key="item.value" :label="item.label" :value="item.value"> </el-option>
+        </el-select>
+        <el-select v-model="status" placeholder="请选择题目状态">
+          <el-option v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value"> </el-option>
+        </el-select>
+        <el-button size="mini" type="primary" @click="search" style="margin-left:10px;">查询</el-button>
+        <el-button size="mini" type="primary" @click="add">添加</el-button>
+      </el-col>
+      <el-col :span="24"
+        ><el-table :data="list" border style="width: 100%">
+          <el-table-column prop="type" label="类型">
+            <template slot-scope="scope">
+              <span>{{ scope.row.type == '0' ? '单选' : scope.row.type == '1' ? '多选' : scope.row.type == '2' ? '问答' : '未识别' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="topic" label="题目" show-overflow-tooltip> </el-table-column>
+          <el-table-column prop="status" label="状态">
+            <template slot-scope="scope">
+              <span>{{ scope.row.status == '0' ? '弃用' : scope.row.status == '1' ? '正常' : '未识别' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作">
+            <template slot-scope="scope">
+              <el-button size="mini" @click="detailBtn(scope.row)">查看</el-button>
+              <el-button size="mini" @click="deleteBtn(scope.row.id)" type="danger">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-col>
+      <el-col :span="24" class="page">
+        <el-pagination background layout="prev, pager, next,total" :total="total" @current-change="handleCurrentChange"> </el-pagination>
+      </el-col>
+      <el-dialog title="添加题目" :visible.sync="dialogFormVisible">
+        <el-form size="mini" :model="form" ref="form" label-width="100px" class="demo-dynamic" :rules="rules">
+          <el-form-item prop="type" label="类型">
+            <el-radio-group v-model="form.type">
+              <el-radio :label="'0'">单选</el-radio>
+              <el-radio :label="'1'">多选</el-radio>
+              <el-radio :label="'2'">问答</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item prop="status" label="状态">
+            <el-radio-group v-model="form.status">
+              <el-radio :label="'0'">弃用</el-radio>
+              <el-radio :label="'1'">正常</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item prop="topic" label="题目">
+            <el-input v-model="form.topic"></el-input>
+          </el-form-item>
+          <el-col class="option" v-if="form.type != '2'">
+            <p>选项</p>
+            <el-form-item v-for="(item, index) in form.option" :label="sortNumber(index)" :key="index">
+              <el-input v-model="item.opname"></el-input><el-button @click.prevent="removeDomain(item)">删除</el-button>
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="addDomain">新增选项</el-button>
+            </el-form-item>
+          </el-col>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button @click="dialogFormVisible = false">取 消</el-button>
+          <el-button type="primary" @click="submitFrom">确 定</el-button>
+        </div>
+      </el-dialog>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { createNamespacedHelpers, mapState } from 'vuex';
+const { mapActions: question } = createNamespacedHelpers('question');
+export default {
+  name: 'tiku',
+  props: {},
+  components: {},
+  data: () => ({
+    list: [], //列表
+    total: 0, //总数
+    dialogFormVisible: false, //控制dialog是否显示
+    form: {
+      option: [],
+    }, //表单
+    type: null, //选择的类型
+    typeoptions: [
+      { value: '0', label: '单选' },
+      { value: '1', label: '多选' },
+      { value: '2', label: '问答' },
+    ], //类型选择数组
+    status: null, //选择的状态
+    statusoptions: [
+      { value: '0', label: '弃用' },
+      { value: '1', label: '正常' },
+    ], //状态选择数组
+    rules: {
+      type: { required: true, message: '类型不能为空', trigger: 'blur' },
+      topic: { required: true, message: '题目不能为空', trigger: 'blur' },
+      status: { required: true, message: '状态不能为空', trigger: 'blur' },
+    }, //表单验证规则
+  }),
+  created() {
+    this.search();
+  },
+  computed: {},
+  methods: {
+    ...question(['create', 'query', 'update', 'delete']),
+    //查询列表
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = { type: this.type, status: this.status };
+      const list = await this.query({ skip, limit, ...info });
+      this.$set(this, `list`, list.data);
+      this.$set(this, `total`, list.total);
+    },
+    //查看详情
+    detailBtn(item) {
+      this.$set(this, `form`, item);
+      this.dialogFormVisible = true;
+    },
+    //删除
+    async deleteBtn(id) {
+      let res = await this.delete(id);
+      if (res.errcode == 0) this.search();
+    },
+    //分页
+    handleCurrentChange(val) {
+      this.search({ skip: (val - 1) * 10, limit: val * 10 });
+    },
+    //提交表单
+    async submitFrom() {
+      let index = 0;
+      for (const option of this.form.option) {
+        option.number = this.sortNumber(index);
+        index++;
+      }
+      if (this.form.type == '2') {
+        delete this.form.option;
+      }
+      let res;
+      if (this.form.id) res = await this.update(this.form);
+      else res = await this.create(this.form);
+      if (res.errcode == 0) {
+        this.dialogFormVisible = false;
+        this.search();
+      }
+    },
+    //删除选项
+    removeDomain(item) {
+      var index = this.form.option.indexOf(item);
+      if (index !== -1) {
+        this.form.option.splice(index, 1);
+      }
+    },
+    //新增选项
+    addDomain() {
+      this.form.option.push({
+        opname: '',
+      });
+    },
+    //根据index生成选项number
+    sortNumber(index) {
+      return String.fromCharCode(65 + index);
+    },
+    //添加
+    add() {
+      this.dialogFormVisible = true;
+      this.form = { status: '1', option: [] };
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.add {
+  margin: 0 0 10px 0;
+}
+.add .el-button:last-child {
+  float: right;
+}
+/deep/.el-dialog {
+  width: 70%;
+}
+.option p {
+  width: 85px;
+  text-align: right;
+  padding: 0 20px 5px 0;
+}
+/deep/.option .el-input {
+  width: 70%;
+  margin: 0 5px 0 0;
+}
+</style>

+ 0 - 0
src/views/question/part/tongji.vue


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä