lrf402788946 5 years ago
parent
commit
a61e7d98d9

+ 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',
+  },
+};

+ 1 - 1
.gitignore

@@ -1,7 +1,7 @@
 .DS_Store
 node_modules
 /dist
-
+package-lock.json
 # local env files
 .env.local
 .env.*.local

File diff suppressed because it is too large
+ 0 - 11565
package-lock.json


+ 7 - 1
package.json

@@ -8,10 +8,16 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "axios": "^0.19.0",
     "core-js": "^3.4.3",
+    "element-ui": "^2.13.0",
+    "lodash": "^4.17.15",
+    "naf-core": "^0.1.2",
     "vue": "^2.6.10",
+    "vue-meta": "^2.3.1",
     "vue-router": "^3.1.3",
-    "vuex": "^3.1.2"
+    "vuex": "^3.1.2",
+    "wangeditor": "^3.1.1"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^4.1.0",

+ 0 - 130
src/components/HelloWorld.vue

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

+ 220 - 0
src/config/menu-config.js

@@ -0,0 +1,220 @@
+export const devMenu = [
+  {
+    path: '/test',
+    name: '测试',
+    children: [
+      {
+        path: '/list',
+        name: '测试列表',
+      },
+      {
+        path: '/detail',
+        name: '测试详情',
+      },
+    ],
+  },
+  // {
+  //   path: '/notice',
+  //   name: '通知管理(中心)',
+  //   children: [
+  //     {
+  //       path: '/add',
+  //       name: '通知发布',
+  //     },
+  //     {
+  //       path: '/list',
+  //       name: '通知查询',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/plan',
+  //   name: '计划管理(中心)',
+  //   children: [
+  //     {
+  //       path: '/add',
+  //       name: '制定计划',
+  //     },
+  //     {
+  //       path: '/index',
+  //       name: '计划列表',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/base',
+  //   name: '基础设置(中心)',
+  //   children: [
+  //     {
+  //       path: '/classesList',
+  //       name: '班级管理',
+  //     },
+  //     {
+  //       path: '/lessonList',
+  //       name: '课程管理',
+  //     },
+  //     {
+  //       path: '/teacherList',
+  //       name: '教师管理',
+  //     },
+  //     {
+  //       path: '/instructorList',
+  //       name: '班主任管理',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/arrangement',
+  //   name: '分班排课(中心)',
+  //   children: [
+  //     {
+  //       path: '/list',
+  //       name: '分班',
+  //     },
+  //     {
+  //       path: '/branchLess',
+  //       name: '排课',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/cer/index',
+  //   name: '证书管理(中心)',
+  // },
+  // {
+  //   path: '/leave',
+  //   name: '请假管理(中心)',
+  //   children: [
+  //     {
+  //       path: '/teacherLeaveList',
+  //       name: '教师申请',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/system',
+  //   name: '系统设置(中心)',
+  //   children: [
+  //     {
+  //       path: '/school',
+  //       name: '学校设置',
+  //     },
+  //     {
+  //       path: '/user',
+  //       name: '用户设置',
+  //     },
+  //     {
+  //       path: '/role',
+  //       name: '权限设置',
+  //     },
+  //     {
+  //       path: '/basics',
+  //       name: '基础设置',
+  //     },
+  //   ],
+  // },
+
+  // {
+  //   path: '/noticeSch',
+  //   name: '通知管理(学校)',
+  //   children: [
+  //     {
+  //       path: '/noticeList',
+  //       name: '通知管理',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/planSch',
+  //   name: '计划管理(学校)',
+  //   children: [
+  //     {
+  //       path: '/planList',
+  //       name: '计划管理',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/centerSch',
+  //   name: '个人中心(学校)',
+  //   children: [
+  //     {
+  //       path: '/schoolCenter',
+  //       name: '个人中心',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/student_list',
+  //   name: '学生管理(班主任)',
+  //   children: [
+  //     {
+  //       path: '/student',
+  //       name: '学生管理',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/work',
+  //   name: '考勤管理(班主任)',
+  //   children: [
+  //     {
+  //       path: '/workDetails',
+  //       name: '考勤详情',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/leaveDir',
+  //   name: '请假管理(班主任)',
+  //   children: [
+  //     {
+  //       path: '/studentLeaveList',
+  //       name: '学生申请',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/score_list',
+  //   name: '评分管理(班主任)',
+  //   children: [
+  //     {
+  //       path: '/score',
+  //       name: '评分管理',
+  //     },
+  //   ],
+  // },
+  // {
+  //   path: '/center',
+  //   name: '个人中心(班主任)',
+  //   children: [
+  //     {
+  //       path: '/myCenter',
+  //       name: '个人中心',
+  //     },
+  //   ],
+  // },
+
+  // {
+  //   path: '/teacher',
+  //   name: '教师管理(教师)',
+  //   children: [
+  //     {
+  //       path: '/timetable',
+  //       name: '课表管理',
+  //     },
+  //     {
+  //       path: '/leave',
+  //       name: '请假管理',
+  //     },
+  //     {
+  //       path: '/task',
+  //       name: '作业管理',
+  //     },
+  //     {
+  //       path: '/personal',
+  //       name: '个人中心',
+  //     },
+  //   ],
+  // },
+];

+ 125 - 0
src/layout/admin/admin-menu.vue

@@ -0,0 +1,125 @@
+<template>
+  <div id="admin-menu" style="background-color: rgb(0, 20, 42);">
+    <scroll-bar>
+      <div class="logo">
+        <img src="https://img.alicdn.com/tfs/TB13UQpnYGYBuNjy0FoXXciBFXa-242-134.png" width="40" />
+        <span class="site-name">ADMIN LITE</span>
+      </div>
+      <el-menu
+        mode="vertical"
+        :show-timeout="200"
+        background-color="#00142a"
+        text-color="hsla(0, 0%, 100%, .65)"
+        active-text-color="#409EFF"
+        @select="selectMenu"
+      >
+        <span v-for="(item, index) in devMenu" :key="index">
+          <!-- <span v-if="`${item.role}` === `${user.role}` || !item.role"> -->
+          <!--  v-if="`${item.role}` === `${user.role}`" -->
+          <span v-if="!item.children" :to="item.path" :key="item.name">
+            <el-menu-item :index="item.path">
+              <i v-if="item.icon" :class="item.icon"></i>
+              <span v-if="item.name" slot="title">{{ item.name }}</span>
+            </el-menu-item>
+          </span>
+
+          <el-submenu v-else :index="item.name || item.path" :key="item.name">
+            <template slot="title">
+              <i v-if="item && item.icon" :class="item.icon"></i>
+              <span v-if="item && item.name" slot="title">{{ item.name }}</span>
+            </template>
+            <template v-for="(child, childIndex) in item.children">
+              <div :key="childIndex" v-if="!child.hidden">
+                <el-menu-item :index="item.path + child.path">
+                  <span v-if="child && child.name" slot="title">{{ child.name }}</span>
+                </el-menu-item>
+              </div>
+            </template>
+          </el-submenu>
+        </span>
+      </el-menu>
+    </scroll-bar>
+  </div>
+</template>
+
+<script>
+import { devMenu } from '../../config/menu-config';
+import scrollBar from './scrollBar.vue';
+export default {
+  name: 'admin-menu',
+  props: {},
+  components: {
+    scrollBar,
+  },
+  data: () => ({
+    devMenu,
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    selectMenu(index) {
+      console.log(index);
+      this.$router.push({ path: index });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.logo {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 4rem;
+  line-height: 4rem;
+  background: #002140;
+  color: #fff;
+  text-align: center;
+  font-size: 1.1rem;
+  font-weight: 600;
+  overflow: hidden;
+}
+.site-name {
+  margin-left: 0.325rem;
+}
+.sidebar-container {
+  box-shadow: 0.125rem 0 0.375rem rgba(0, 21, 41, 0.35);
+  transition: width 0.28s;
+  width: 12rem !important;
+  height: 100%;
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1001;
+  overflow: hidden;
+  a {
+    display: inline-block;
+    width: 100%;
+  }
+  .el-menu {
+    padding-top: 1rem;
+    width: 100% !important;
+    border: none;
+  }
+  .el-submenu .el-menu-item {
+    min-width: 16rem !important;
+    padding-left: 3rem !important;
+    background-color: #000c17 !important;
+    &:hover {
+      color: #fff !important;
+    }
+  }
+  .el-menu-item,
+  .el-submenu .el-menu-item {
+    &.is-active {
+      background-color: #188fff !important;
+      color: #fff !important;
+    }
+  }
+  .el-submenu__title i {
+    font-size: 1rem;
+    color: rgba(255, 255, 255, 0.65);
+  }
+}
+</style>

+ 63 - 0
src/layout/admin/breadcrumb.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="breadcrumb">
+    <el-tag v-for="(item, index) in list" :key="index" type="primary" size="small" closable class="tags" @close="closeTag(index)">{{ item.name }}</el-tag>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'breadcrumb',
+  props: {},
+  components: {},
+  data: () => ({
+    list: [],
+  }),
+  created() {
+    this.getList();
+  },
+  computed: {},
+  methods: {
+    getList() {
+      console.log('bread getlist');
+      //将面包屑列表存到sessionStorage中,存为list,{name:xxx,path:xxx}形式
+      //1)是从sessionStorage中取出之前的列表
+      let o_list = sessionStorage.getItem('bread');
+      if (o_list !== null) {
+        //如果之前有浏览记录,那就将值取出来
+        this.$set(this, `list`, JSON.parse(o_list));
+      }
+      //2)查询当前路由是否已经存在
+      let { name, path } = this.$route;
+      if (path === '/admin' || path === '/admin/index') return;
+      let object = { name: name, path: path };
+      let res = [];
+      //列表有值,过滤看看有没有当前路由
+      if (this.list.length > 0) {
+        res = this.list.filter(fil => fil.path === object.path);
+        if (res.length <= 0) {
+          //没有当前路由,加上
+          this.list.push(object);
+          sessionStorage.setItem(`bread`, JSON.stringify(this.list));
+        }
+      } else {
+        //列表没有值,直接加上
+        this.list.push(object);
+        sessionStorage.setItem(`bread`, JSON.stringify(this.list));
+      }
+    },
+    closeTag(index) {
+      let elseList = this.list.filter((fil, filIndex) => filIndex !== index);
+      this.$set(this, `list`, elseList);
+      sessionStorage.setItem('bread', JSON.stringify(elseList));
+      if (this.list.length > 0) this.$router.push(this.list[this.list.length - 1].path);
+      else this.$router.push({ path: '/admin/index' });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.tags {
+  margin: 0 0.3rem;
+}
+</style>

+ 47 - 0
src/layout/admin/detail-frame.vue

@@ -0,0 +1,47 @@
+<template>
+  <div id="detail-frame">
+    <el-card style="background:rgb(231, 224, 235);border-radius: 60px;" shadow="hover">
+      <el-row>
+        <el-col :span="24" class="title">
+          <span v-if="returns">
+            <el-button
+              size="mini"
+              plan
+              circle
+              @click="$router.push({ path: returns })"
+              style="box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)"
+            >
+              <span class="el-icon-arrow-left" style="zoom:1.5;font-weight:700"></span>
+            </el-button>
+          </span>
+          <slot name="title"></slot>
+        </el-col>
+      </el-row>
+      <div style="padding:1.875rem;">
+        <slot></slot>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'detail-frame',
+  props: {
+    returns: { type: null, default: null },
+  },
+  components: {},
+  data: () => ({}),
+  created() {},
+  computed: {},
+  methods: {},
+};
+</script>
+
+<style lang="less" scoped>
+.title {
+  font-size: 1.25rem;
+  font-weight: 700;
+  padding: 1rem;
+}
+</style>

+ 23 - 0
src/layout/admin/fw-admin.vue

@@ -0,0 +1,23 @@
+<template>
+  <div id="fw-admin">
+    <section class="app-main">
+      <transition name="el-fade-in-linear" mode="out-in">
+        <router-view></router-view>
+      </transition>
+    </section>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'fw-admin',
+  props: {},
+  components: {},
+  data: () => ({}),
+  created() {},
+  computed: {},
+  methods: {},
+};
+</script>
+
+<style lang="less" scoped></style>

+ 70 - 0
src/layout/admin/list-frame.vue

@@ -0,0 +1,70 @@
+<template>
+  <div id="list-frame">
+    <el-card style="background:rgba(149, 14, 226, 0.21);border-radius: 60px;" shadow="hover">
+      <el-row>
+        <el-col :span="24" class="title">
+          <span v-if="returns">
+            <el-button
+              size="mini"
+              plan
+              circle
+              @click="$router.push({ path: returns })"
+              style="box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)"
+            >
+              <span class="el-icon-arrow-left" style="zoom:1.5;font-weight:700"></span>
+            </el-button>
+          </span>
+          <slot name="title"></slot>
+        </el-col>
+      </el-row>
+      <div style="padding:1.875rem;">
+        <slot></slot>
+      </div>
+      <el-row type="flex" align="middle" justify="end" v-if="needPag">
+        <el-col :span="24" style="text-align:right;">
+          <el-pagination
+            background
+            layout="total, prev, pager, next"
+            :total="totalRow"
+            :page-size="limit"
+            :current-page.sync="currentPage"
+            @current-change="changePage"
+          >
+          </el-pagination>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+export default {
+  name: 'list-frame',
+  props: {
+    totalRow: { type: Number, default: 0 },
+    needPag: { type: Boolean, default: true },
+    returns: { type: null, default: null },
+  },
+  components: {},
+  data: () => ({
+    limit: _.get(this, `$limit`, undefined) !== undefined ? this.$limit : 15,
+    currentPage: 1,
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    changePage(page) {
+      this.$emit('changePage', page);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.title {
+  font-size: 1.25rem;
+  font-weight: 700;
+  padding: 1rem;
+}
+</style>

+ 110 - 0
src/layout/admin/navBar.vue

@@ -0,0 +1,110 @@
+<template>
+  <div id="navBar">
+    <el-menu class="navbar" mode="horizontal">
+      <div class="user-profile-container">
+        <div class="user-profile-content">
+          <!-- <div class="menu-icons">
+          <span class="menu-icon"><i class="el-icon-search icon"></i></span>
+          <span class="menu-icon"><i class="el-icon-message icon"></i></span>
+          <span class="menu-icon">
+            <el-badge is-dot class="item">
+              <i class="el-icon-bell icon"></i>
+            </el-badge>
+          </span>
+        </div> -->
+          <el-dropdown>
+            <div class="user-profile-body">
+              <img class="user-avatar" src="https://img.alicdn.com/tfs/TB1ONhloamWBuNjy1XaXXXCbXXa-200-200.png" />
+              <span class="user-name" v-if="user && user.id">欢迎,{{ (user && user.user_name) || '' }}</span>
+              <span class="user-name" v-else @click="$router.push({ path: '/admin' })">请登录</span>
+            </div>
+            <el-dropdown-menu class="user-dropdown" slot="dropdown">
+              <!-- <router-link to="/updatePw" v-if="user && user.id">
+              <el-dropdown-item>
+                修改密码
+              </el-dropdown-item>
+            </router-link> -->
+              <el-dropdown-item v-if="user && user.id">
+                <span @click="toLogout()" style="display:block;">退出</span>
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </div>
+      </div>
+    </el-menu>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'navBar',
+  props: {},
+  components: {},
+  data: () => ({
+    user: {},
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    async toLogout() {
+      this.$router.push({ path: '/admin' });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.navbar {
+  height: 4rem;
+  box-shadow: 0 0.0625rem 0.25rem rgba(0, 21, 41, 0.08);
+  .user-profile-container {
+    position: absolute;
+    right: 1.25rem;
+    cursor: pointer;
+    .user-profile-content {
+      display: flex;
+      padding: 1.25rem 0;
+    }
+    .menu-icons {
+      display: flex;
+      align-items: center;
+      .menu-icon {
+        padding: 0 0.75rem;
+        .icon {
+          display: inline-block;
+          font-size: 1.125rem;
+          text-align: center;
+        }
+      }
+    }
+    .user-profile-body {
+      position: relative;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      text-align: center;
+      padding-right: 0.875rem;
+    }
+    .user-avatar {
+      width: 1.5rem;
+      height: 1.5rem;
+      margin: 0 0.5rem 0 0.75rem;
+      border-radius: 0.25rem;
+    }
+    .user-name {
+      color: rgba(0, 0, 0, 0.65);
+    }
+    .user-department {
+      font-size: 0.75rem;
+      color: rgba(102, 102, 102, 0.65);
+    }
+    .el-icon-caret-bottom {
+      position: absolute;
+      right: 0;
+      top: 0.8125rem;
+      font-size: 0.75rem;
+    }
+  }
+}
+</style>

+ 54 - 0
src/layout/admin/scrollBar.vue

@@ -0,0 +1,54 @@
+<template>
+  <div id="scrollBar">
+    <div class="scroll-container" ref="scrollContainer" @wheel.prevent="handleScroll">
+      <div class="scroll-wrapper" ref="scrollWrapper" :style="{ top: top + 'px' }">
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+const delta = 15;
+
+export default {
+  name: 'ScrollBar',
+  data() {
+    return {
+      top: 0,
+    };
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 3;
+      const $container = this.$refs.scrollContainer;
+      const $containerHeight = $container.offsetHeight;
+      const $wrapper = this.$refs.scrollWrapper;
+      const $wrapperHeight = $wrapper.offsetHeight;
+      if (eventDelta > 0) {
+        this.top = Math.min(0, this.top + eventDelta);
+      } else if ($containerHeight - delta < $wrapperHeight) {
+        if (this.top < -($wrapperHeight - $containerHeight + delta)) {
+          this.top = this.top;
+        } else {
+          this.top = Math.max(this.top + eventDelta, $containerHeight - $wrapperHeight - delta);
+        }
+      } else {
+        this.top = 0;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.scroll-container {
+  width: 100%;
+  height: 100%;
+  background-color: #00142a;
+  .scroll-wrapper {
+    position: absolute;
+    width: 100%;
+  }
+}
+</style>

+ 113 - 0
src/layout/mobile/framework.vue

@@ -0,0 +1,113 @@
+<template>
+  <div id="navBar">
+    <el-row type="flex" align="middle" justify="center" class="row">
+      <el-col :span="4" class="left" @click.native="$router.push({ path: returns ? returns : '/' })" v-if="returns">
+        <span class="el-icon-arrow-left" style="zoom:1.2;font-weight:700"></span>
+      </el-col>
+      <el-col :span="4" class="left" v-else>&nbsp;</el-col>
+      <el-col :span="16" style="text-align:center;font-size:1.25rem;font-weight:600">
+        <slot name="title">title</slot>
+      </el-col>
+      <el-col :span="4" style="text-align:center;" class="right">
+        <slot name="right">&nbsp;</slot>
+      </el-col>
+    </el-row>
+    <el-scrollbar tag="div" wrap-style="height:100vh;margin-bottom:5rem;">
+      <div class="content">
+        <slot>content</slot>
+      </div>
+    </el-scrollbar>
+    <el-row class="menu">
+      <el-col :span="8" :class="checkRouter('work')" @click.native="$router.push({ path: '/work/index' })">
+        <!-- 作业管理 -->
+        <div class="el-icon-s-claim"></div>
+        <br />
+        <font>作业管理</font>
+        <!-- <el-button type="primary" class="menuBtn">作业管理</!-->
+      </el-col>
+      <el-col :span="8" :class="checkRouter('attendance')" @click.native="$router.push({ path: '/attendance/index' })">
+        <div class="el-icon-date"></div>
+        <br />
+        <font>考勤管理</font>
+        <!-- 考勤管理中含有请假 -->
+      </el-col>
+      <el-col :span="8" :class="checkRouter('self')" @click.native="$router.push({ path: '/self/index' })">
+        <div class="el-icon-user"></div>
+        <br />
+        <font>个人中心</font>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'navBar',
+  props: {
+    returns: { type: null, default: null },
+  },
+  components: {},
+  data: () => ({}),
+  created() {},
+  computed: {},
+  methods: {
+    checkRouter(item) {
+      let router = this.$route.path;
+      return router.includes(item) ? `actives` : '';
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.row {
+  padding: 0.5rem 0;
+  border-bottom: 1px solid #c0c0c0;
+  position: fixed;
+  top: 0;
+  width: 100%;
+  height: 3rem;
+  z-index: 999;
+  background: #fff;
+}
+.left {
+  padding-left: 1rem;
+  border-right: 2px dashed #c0c0c0;
+}
+.right {
+  border-left: 2px dashed #c0c0c0;
+}
+.content {
+  // position: relative;
+  // top: 3rem;
+  margin: 3rem 0;
+}
+.menu {
+  width: 100%;
+  position: fixed;
+  bottom: 0;
+  background: #ebeef5;
+  .el-col {
+    padding-top: 0.25rem;
+    text-align: center;
+    color: #909399;
+    div {
+      zoom: 1.5;
+    }
+  }
+  .menuBtn {
+    width: 98%;
+    span {
+      zoom: 2;
+    }
+  }
+}
+.actives {
+  font {
+    color: #409eff;
+  }
+  div {
+    color: #409eff;
+  }
+}
+</style>

+ 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 });

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

@@ -0,0 +1,39 @@
+/* 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';
+
+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);
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 4 - 0
src/plugins/element.js

@@ -0,0 +1,4 @@
+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);

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+const Plugin = {
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = str => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if (!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function() {
+        if (this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      },
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function(subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = 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);
+};