lrf402788946 4 年之前
父節點
當前提交
afb4c3c6e0

+ 1 - 1
.env

@@ -1,2 +1,2 @@
 VUE_APP_AXIOS_BASE_URL = ''
-VUE_APP_ROUTER="/cmszhwl"
+VUE_APP_ROUTER="/zhwl"

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "element-ui": "^2.13.2",
     "jsonwebtoken": "^8.5.1",
     "lodash": "^4.17.20",
+    "moment": "^2.29.1",
     "naf-core": "^0.1.2",
     "qrcode": "^1.4.4",
     "vue": "^2.6.11",

+ 9 - 0
src/assets/css/main.css

@@ -175,3 +175,12 @@ a {
 .v-note-wrapper .v-note-panel {
     min-height: 500px;
 }
+
+/* 自定义 */
+.btn_bar{
+  margin: 0.75rem 0;
+}
+
+.right {
+  text-align: right;
+}

+ 3 - 2
src/components/frame/filter-page-table.md

@@ -19,7 +19,7 @@
 |参数|类型|默认值|是否必填|说明|
 |:-:|:-:|:-:|:-:|:-:|
 |label|String|`-`|是|列名称|
-|prop|String|`-`|是|字段名称|
+|model|String|`-`|是|字段名称|
 |format|Function/String|`-`|否|Function类型:数据需要过滤则将过滤方法写在这;String类型:走toFormat方法,参数位(model=>字段名,value=>值)|
 |custom|Boolean|false|否|自定义输出|
 |options|Object|`-`|否|添加额外属性,比如说样式之类的|
@@ -29,6 +29,7 @@
 |filterReturn|Boolean|`-`|否|针对这个选项需要在选择后就做些逻辑处理时,改成true,然后再使用filterReturn方法处理,(例如二级联动的情况)|
 |notable|Boolean|false/undefined|否|不需要在表格中显示|
 |selected|Array|`-`|false|多选选项的数据|
+|type|String|`primary`|element的四大类型|
 
 >opera
 >>
@@ -47,4 +48,4 @@
 |:-:|:-:|:-:|
 |handleSelect|Array[object]|返回选择的内容|
 |query|{skip,limit,...info}|分页查询,及条件查询|
-|filterReturn|{data,prop}|查询条件栏过滤条件中filterReturn字段为true的回调方法|
+|filterReturn|{data,model}|查询条件栏过滤条件中filterReturn字段为true的回调方法|

+ 37 - 15
src/components/frame/filter-page-table.vue

@@ -4,12 +4,12 @@
       <el-form-item v-for="(item, index) in filterList" :key="index">
         <template v-if="item.filter === 'select'">
           <el-select
-            v-model="searchInfo[item.prop]"
+            v-model="searchInfo[item.model]"
             size="mini"
             clearable
             filterable
             :placeholder="`请选择${item.label}`"
-            @clear="toClear(item.prop)"
+            @clear="toClear(item.model)"
             @change="data => filterReturn(data, item)"
           >
             <slot name="options" v-bind="{ item }"></slot>
@@ -17,7 +17,7 @@
         </template>
         <template v-else-if="item.filter === 'date'">
           <el-date-picker
-            v-model="searchInfo[item.prop]"
+            v-model="searchInfo[item.model]"
             value-format="yyyy-MM-dd"
             format="yyyy-MM-dd"
             type="daterange"
@@ -29,7 +29,7 @@
           </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>
+          <el-input v-model="searchInfo[item.model]" clearable size="mini" :placeholder="`请输入${item.label}`" @clear="toClear(item.model)"></el-input>
         </template>
       </el-form-item>
       <el-form-item>
@@ -65,7 +65,7 @@
               :key="index"
               align="center"
               :label="item.label"
-              :prop="item.prop"
+              :prop="item.model"
               :formatter="toFormatter"
               sortable
               v-bind="item.options"
@@ -81,17 +81,39 @@
             <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
+                  <!-- <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-button> -->
+                  <el-link
+                    :key="index"
+                    :type="item.type || 'primary'"
+                    :icon="item.icon || ''"
+                    size="mini"
+                    style="padding-right:10px"
+                    :underline="false"
+                    @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                  >
+                  </el-link>
                 </el-tooltip>
-                <el-button v-else :key="index" type="text" size="mini" @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)">
+                <el-link
+                  v-else
+                  :key="index"
+                  :type="item.type || 'primary'"
+                  :icon="item.icon || ''"
+                  size="mini"
+                  style="padding-right:10px"
+                  :underline="false"
+                  @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                >
+                  {{ item.label }}
+                </el-link>
+                <!-- <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>
+                </el-button> -->
               </template>
             </template>
           </template>
@@ -149,7 +171,7 @@ export default {
   computed: {},
   methods: {
     toFormatter(row, column, cellValue, index) {
-      let this_fields = this.fields.filter(fil => fil.prop === column.property);
+      let this_fields = this.fields.filter(fil => fil.model === column.property);
       if (this_fields.length > 0) {
         let format = _.get(this_fields[0], `format`, false);
         if (format) {
@@ -249,7 +271,7 @@ export default {
       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, []);
+        if (i.filter === 'date' && this.searchInfo[i.model] === undefined) this.$set(this.searchInfo, i.model, []);
       });
       res = [...res, ...this.filter];
       this.$set(this, `filterList`, res);
@@ -261,12 +283,12 @@ export default {
     rowClick(row, column, event) {
       this.$emit(`rowClick`, row);
     },
-    toClear(prop) {
-      delete this.searchInfo[prop];
+    toClear(model) {
+      delete this.searchInfo[model];
     },
     filterReturn(data, item) {
-      let { prop, filterReturn } = item;
-      if (filterReturn) this.$emit('filterReturn', { data, prop });
+      let { model, filterReturn } = item;
+      if (filterReturn) this.$emit('filterReturn', { data, model });
     },
   },
   watch: {

+ 10 - 23
src/components/frame/form.md

@@ -20,24 +20,30 @@
 |参数|类型|默认值|是否必填|说明|
 |:-:|:-:|:-:|:-:|:-:|
 |label|String|`-`|是|显示的字段中文|
-|type|String|input|否|这个字段要用什么类型来输出 input的基本类型可选值:date,datetime,radio,checkbox,select,text(只显示值),editor(富文本编辑器),password|
+|type|String|input|否|这个字段要用什么类型来输出 input的基本类型可选值:date,datetime,radio,checkbox,select,text(只显示值),editor(富文本编辑器),password,number|
 |required|Boolean|`-`|否|是否必须输入|
 |model|String|`-`|是|字段名|
 |placeholder|String|`-`|否|占位,正常用,只是个透传|
 |options|object|`-`|否|标签的属性设置,例如:textarea 需要显示剩余字数,或者input限制长度,都往这里写,key-value形式(键值对,json的基本了解,不知道百度,具体属性看你具体用那个组件,那个组件有什么属性,瞎写不一定好使)|
 |custom|Boolean|`-`|否|是否使用自定义插槽|
-|tip|String|`-`|否|提示语,例如:请输入11位电话号码|
+|remark|String|`-`|否|提示语,例如:请输入11位电话号码|
 |labelWidth|String|`120px`|否|表单label宽度,element的,默认120px|
 |format|Function|`-`|否|当type = text 时需要将该字段内容转换,可以使用format|
+|filterReturn|Boolean|`-`|否|针对这个选项需要在选择后就做些逻辑处理时,改成true,然后再使用filterReturn方法处理,(例如二级联动的情况)|
 
 
+### methods
+|方法名|参数|说明|
+|:-:|:-:|:-:|
+|filterReturn|{data,prop}|查询条件栏过滤条件中filterReturn字段为true的回调方法|
+
 
 
 
 
 
 ### slot
->
+> 
 |插槽名|说明|
 |:-:|:-:|
 |options|fields中type为select的,选项都写在这个插槽中,多个select则需要区分options所属问题|
@@ -47,7 +53,7 @@
 |submit|提交按钮部分,当needSave为false时才可以使用|
 >>关于自定义的用法:
 >>在fields中,custom:true的情况即需要自定义,写法如下
-
+  
 >>`<template #custom="{ item, form, fieldChange }"> ... </template>`
 >>
 |参数名|说明|
@@ -61,22 +67,3 @@
 
 >> **如果有多处需要自定义,请区分开去写**
 
-
-***
-### 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}|上传成功返回

+ 32 - 10
src/components/frame/form.vue

@@ -16,7 +16,7 @@
           <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">
+                <el-tooltip class="item" effect="dark" :content="item.remark" placement="top-start" :disabled="!item.remark">
                   <template v-if="item.type === `date` || item.type === `datetime`">
                     <el-date-picker
                       v-model="form[item.model]"
@@ -39,30 +39,48 @@
                     >
                     </el-date-picker>
                   </template>
+                  <template v-else-if="item.type === 'number'">
+                    <el-input-number v-model="form[item.model]" placeholder=""></el-input-number>
+                  </template>
                   <template v-else-if="item.type === 'time'">
                     <el-time-picker v-model="form[item.model]" placeholder="请选择时间" format="HH:mm" value-format="HH:mm"></el-time-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>
+                      <template v-if="item.list">
+                        <el-radio v-for="(i, iIndex) in item.list" :key="`radio-${iIndex}`" :label="i.value">{{ i.label }}</el-radio>
+                      </template>
+                      <template v-else>
+                        <slot name="radios" v-bind="{ item, form, fieldChange }"></slot>
+                      </template>
                     </el-radio-group>
                   </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>
+                      <template v-if="item.list">
+                        <el-checkbox v-for="(i, iIndex) in item.list" :key="`checkbox-${iIndex}`" :label="i.value">{{ i.label }}</el-checkbox>
+                      </template>
+                      <template v-else>
+                        <slot name="checkboxs" v-bind="{ item, form, fieldChange }"></slot>
+                      </template>
                     </el-checkbox-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 v-model="form[item.model]" v-bind="item.options" filterable clearable @change="data => filterReturn(data, item)">
+                      <template v-if="item.list">
+                        <el-option v-for="(i, iIndex) in item.list" :key="`checkbox-${iIndex}`" :label="i.label" :value="i.value"></el-option>
+                      </template>
+                      <template v-else>
+                        <slot name="options" v-bind="{ item, form, fieldChange }"> </slot>
+                      </template>
                     </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'">
+                  <!-- <template v-else-if="item.type === 'editor'">
                     <wang-editor v-model="form[item.model]"></wang-editor>
-                  </template>
+                  </template> -->
                   <template v-else>
                     <el-input
                       clearable
@@ -106,7 +124,7 @@
 
 <script>
 import _ from 'lodash';
-import wangEditor from '@/components/frame/wang-editor.vue';
+// import wangEditor from '@frame/components/wang-editor';
 export default {
   name: 'add',
   props: {
@@ -123,7 +141,7 @@ export default {
     reset: { type: Boolean, default: true },
   },
   components: {
-    wangEditor,
+    // wangEditor,
   },
   data: () => ({
     form: {},
@@ -195,13 +213,17 @@ export default {
       if (!_.isFunction(dis)) return true;
       else return dis(field, this.form);
     },
+    filterReturn(data, item) {
+      let { model, filterReturn } = item;
+      if (filterReturn) this.$emit('filterReturn', { data, model });
+    },
   },
 };
 </script>
 
 <style lang="less" scoped>
 .form {
-  padding: 2rem 1rem;
+  padding: 1.5rem 1rem;
   background: #fff;
   border-radius: 20px;
 }

+ 0 - 11
src/components/frame/pagination.md

@@ -1,11 +0,0 @@
-# pagination.vue 分页组件
-#### props
-|参数|类型|默认值|是否必填|说明|
-|:-:|:-:|:-:|:-:|:-:|
-|position|String|'right'|否|组件的布局位置,默认为靠右侧|
-|total|Number|0|是|分页的总数据数,用来计算页码|
-|limit|Number|10|否|每页的数量|  
-#### methods
-|方法名|参数|说明|
-|:-:|:-:|:-:|
-|query|{skip,limit,...info}|分页查询|

+ 0 - 51
src/components/frame/pagination.vue

@@ -1,51 +0,0 @@
-<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 },
-    limit: { type: Number, default: 10 },
-  },
-  components: {},
-  data: () => {
-    return {
-      currentPage: 1,
-    };
-  },
-  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>

+ 0 - 106
src/components/frame/uploadone.vue

@@ -1,106 +0,0 @@
-<template>
-  <div id="upload">
-    <el-upload
-      v-if="url"
-      ref="upload"
-      :action="url"
-      :list-type="listType"
-      :file-list="fileList"
-      :limit="limit"
-      :on-exceed="outLimit"
-      :before-remove="handleRemove"
-      :on-success="onSuccess"
-      :before-upload="beforeUpload"
-      :show-file-list="showList"
-      :accept="accept"
-    >
-      <el-button size="small" type="primary" v-if="isBtn">点击上传</el-button>
-      <template v-else-if="uploadBtn">
-        <el-button type="primary">选择文件</el-button>
-      </template>
-      <template v-else>
-        <i class="el-icon-plus"></i>
-      </template>
-      <template #tip v-if="tip">
-        {{ tip }}
-      </template>
-    </el-upload>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'upload',
-  props: {
-    url: { type: null },
-    limit: { type: Number },
-    data: { type: null },
-    type: { type: String },
-    isBtn: { type: Boolean, default: false },
-    uploadBtn: { type: Boolean, default: false },
-    showList: { type: Boolean, default: true },
-    accept: { type: String, default: '' },
-    tip: { type: String, default: undefined },
-    listType: { type: String, default: 'picture-card' },
-    fileType: { type: String, default: '1' },
-  },
-  components: {},
-  data: () => ({
-    fileList: [],
-  }),
-  created() {
-    if (this.data) {
-      this.defalutProcess(this.data);
-    }
-  },
-  watch: {
-    data: {
-      handler(val) {
-        this.defalutProcess(val);
-      },
-    },
-  },
-  computed: {},
-  methods: {
-    handleRemove(file, fileList) {
-      if (this.fileType == '1') {
-        this.$emit('onedelete', file);
-        return true;
-      } else {
-        let index = fileList.findIndex(f => _.isEqual(f, file));
-        this.$emit('delete', index);
-      }
-    },
-    outLimit() {
-      this.$message.error(`只允许上传${this.limit}个文件`);
-    },
-    onSuccess(response, file, fileList) {
-      //将文件整理好传回父组件
-      this.$emit('upload', { type: this.type, data: { ...response, name: file.name } });
-    },
-    beforeUpload(file) {
-      const sizeLimit = file.size / 1024 / 1024 < 10;
-      if (sizeLimit) return true;
-      else {
-        this.$message.error('文件超出10M!');
-        return false;
-      }
-    },
-    defalutProcess(val) {
-      if (_.isArray(val)) {
-        let newArr = val.map(item => {
-          let object = {};
-          // object.name = item.name;
-          object.url = item ? item.url : null;
-          return object;
-        });
-        this.$set(this, `fileList`, newArr);
-      } else if (typeof val === 'string') {
-        this.$set(this, `fileList`, [{ name: '视频', url: val }]);
-      }
-    },
-  },
-};
-</script>
-
-<style lang="less" scoped></style>

+ 16 - 16
src/components/frame/qrcode.vue

@@ -10,7 +10,10 @@ import QRCode from 'qrcode';
 export default {
   name: 'qrcode',
   props: {
-    qrcode: null,
+    exchange: null,
+    qrcode: { type: String, required: true },
+    config: { type: Object, required: true },
+    uri: { type: String },
   },
   components: {},
   data: () => ({
@@ -25,27 +28,24 @@ export default {
   methods: {
     async initQrcode() {
       // 创建二维码
-      if (!this.qrcode) return;
-      let uri = `${Vue.config.weixin.baseUrl}/qrcode/${this.qrcode}/scan`;
-      if (uri.startsWith('/')) {
-        uri = `${location.protocol}//${location.host}${uri}`;
-      }
+      if (!this.exchange) return;
+      //二维码地址应该是变量,不过这里需要处理
+      let uri = `${this.config.weixin.baseUrl}${this.uri}`;
+      // console.log(uri);
+      // if (uri.startsWith('/')) {
+      //   uri = `${location.protocol}//${location.host}${uri}`;
+      // }
       this.dataUrl = await QRCode.toDataURL(uri);
+
       this.$stomp({
-        [`/exchange/qrcode.login/${this.qrcode}`]: this.onMessage,
+        [`/exchange/${this.exchange}/${this.qrcode}`]: this.onMessage,
       });
+      console.log(`/exchange/${this.exchange}/${this.qrcode}`);
+      // console.log(uri);
     },
     onMessage(message) {
       console.log('receive a message: ', message.body);
-      if (message.body == 'scaned') {
-        try {
-          this.$emit('toReturn', message);
-          console.log('扫码登录成功');
-        } catch (err) {
-          console.log('扫码登录失败');
-          console.error(err);
-        }
-      }
+      this.$emit('toReturn', message);
     },
   },
 };

+ 1 - 1
src/components/frame/wang-editor.vue

@@ -5,6 +5,7 @@
 import E from 'wangeditor';
 
 const menus = [
+  'code', // 插入代码
   'head', // 标题
   'bold', // 粗体
   'fontSize', // 字号
@@ -21,7 +22,6 @@ const menus = [
   // 'emoticon', // 表情
   'table', // 表格
   // 'video', // 插入视频
-  // 'code', // 插入代码
   'undo', // 撤销
   'redo', // 重复
 ];

+ 2 - 2
src/components/common/Header.vue

@@ -16,7 +16,7 @@
         </div>
         <!-- 用户头像 -->
         <div class="user-avator">
-          <img src="../../assets/img/img.jpg" />
+          <img src="../assets/img/img.jpg" />
         </div>
         <!-- 用户名下拉菜单 -->
         <el-dropdown class="user-name" trigger="click" @command="handleCommand">
@@ -33,7 +33,7 @@
   </div>
 </template>
 <script>
-import bus from '../common/bus';
+import bus from './bus';
 import { mapState, createNamespacedHelpers } from 'vuex';
 export default {
   data() {

+ 10 - 1
src/components/common/Home.vue

@@ -15,7 +15,14 @@
               <!-- <keep-alive :include="tagsList">
                 <router-view></router-view>
               </keep-alive> -->
-              <router-view></router-view>
+              <el-row>
+                <el-col :span="24" class="main">
+                  <breadcrumb :breadcrumbTitle="this.$route.meta.title"></breadcrumb>
+                  <el-col :span="24" class="container">
+                    <router-view></router-view>
+                  </el-col>
+                </el-col>
+              </el-row>
             </transition>
             <el-backtop target=".content"></el-backtop>
           </div>
@@ -26,6 +33,7 @@
 </template>
 
 <script>
+import breadcrumb from '@l/breadcrumb.vue';
 import vHead from './Header.vue';
 import vSidebar from './Sidebar.vue';
 import vTags from './Tags.vue';
@@ -41,6 +49,7 @@ export default {
     vHead,
     vSidebar,
     vTags,
+    breadcrumb,
   },
   created() {
     bus.$on('collapse-content', msg => {

+ 52 - 31
src/components/common/Sidebar.vue

@@ -8,44 +8,52 @@
       text-color="#bfcbd9"
       active-text-color="#20a0ff"
       unique-opened
-      router
     >
-      <template v-for="item in items">
-        <template v-if="item.subs">
-          <el-submenu class="second" :index="item.index" :key="item.index">
+      <!-- <template v-for="item in items">
+        <template v-if="item.children">
+          <el-submenu class="" :index="item.index" :key="item.index">
             <template slot="title">
               <i :class="item.icon"></i>
               <span slot="title">{{ item.title }}</span>
             </template>
-            <template v-for="subItem in item.subs">
-              <el-submenu v-if="subItem.subs" :index="subItem.index" :key="subItem.index">
+            <template v-for="subItem in item.children">
+              <el-submenu v-if="subItem.children" :index="subItem.index" :key="subItem.index">
                 <template slot="title">{{ subItem.title }}</template>
-                <el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">{{ threeItem.title }}</el-menu-item>
+                <el-menu-item v-for="(threeItem, i) in subItem.children" :key="i" :index="threeItem.index">{{ threeItem.title }}</el-menu-item>
               </el-submenu>
               <el-menu-item v-else :index="subItem.index" :key="subItem.index">{{ subItem.title }}</el-menu-item>
             </template>
           </el-submenu>
         </template>
         <template v-else>
-          <el-menu-item class="first" :index="item.index" :key="item.index">
+          <el-menu-item class="" :index="item.index" :key="item.index">
             <i :class="item.icon"></i>
             <span slot="title">{{ item.title }}</span>
           </el-menu-item>
         </template>
-      </template>
+      </template> -->
+      <menu-item :menus="menu" :level="0"></menu-item>
     </el-menu>
   </div>
 </template>
 
 <script>
 import _ from 'lodash';
+import menuItem from './menu-item.vue';
 import { mapState, createNamespacedHelpers } from 'vuex';
-import bus from '../common/bus';
+const { mapActions: userMenu } = createNamespacedHelpers('userMenu');
+import bus from './bus';
 export default {
+  components: { menuItem },
   data() {
     return {
       collapse: false,
       items: [
+        {
+          icon: 'el-icon-s-home',
+          index: '/menu',
+          title: '目录管理',
+        },
         {
           icon: 'el-icon-s-home',
           index: 'homeIndex',
@@ -56,27 +64,29 @@ export default {
           index: 'test',
           title: '测试菜单',
         },
-        // {
-        //   icon: 'el-icon-s-home',
-        //   index: '1',
-        //   title: '测试菜单',
-        //   subs: [
-        //     {
-        //       index: 'test',
-        //       title: '测试菜单',
-        //     },
-        //   ],
-        // },
+        {
+          icon: 'el-icon-s-home',
+          index: '1',
+          title: '测试菜单',
+          children: [
+            {
+              index: 'test',
+              title: '测试菜单',
+            },
+          ],
+        },
       ],
+      menu: [],
     };
   },
   computed: {
     ...mapState(['user']),
     onRoutes() {
-      return this.$route.path.replace('/', '');
+      return this.$route.path;
     },
   },
   created() {
+    this.requestMenu();
     // 通过 Event Bus 进行组件间通信,来折叠侧边栏
     bus.$on('collapse', msg => {
       this.collapse = msg;
@@ -84,16 +94,27 @@ export default {
     });
   },
   methods: {
-    // 分配用户彩带权限
-    getMenu() {
-      // 客户信息
-      let user = this.user;
-      // 复制列表
-      let list = _.cloneDeep(this.items);
-      let data = [];
-      list.push(...data);
-      this.$set(this, `items`, _.uniqBy(list, 'index'));
+    ...userMenu(['getMenu']),
+    async requestMenu() {
+      const { type, id: userid } = this.user;
+      const condition = { project: 'zhwl', type: 0, userid: 'dev' };
+      const res = await this.getMenu(condition);
+      if (this.$checkRes(res)) {
+        console.log(res);
+        this.$set(this, `menu`, res);
+        // sessionStorage.setItem('userMenu', JSON.stringify(res.data));
+      }
     },
+    // 分配用户彩带权限
+    // getMenu() {
+    //   // 客户信息
+    //   let user = this.user;
+    //   // 复制列表
+    //   let list = _.cloneDeep(this.items);
+    //   let data = [];
+    //   list.push(...data);
+    //   this.$set(this, `items`, _.uniqBy(list, 'index'));
+    // },
   },
   watch: {
     user: {

+ 2 - 2
src/components/common/Tags.vue

@@ -64,9 +64,9 @@ export default {
           this.tagsList.shift();
         }
         this.tagsList.push({
-          title: route.meta.title,
+          title: route.meta.title || '',
           path: route.fullPath,
-          name: route.matched[1].components.default.name,
+          name: route.matched[1].components.default.name || '',
         });
       }
       bus.$emit('tags', this.tagsList);

src/components/common/breadcrumb.vue → src/layouts/breadcrumb.vue


src/components/common/bus.js → src/layouts/bus.js


src/components/common/directives.js → src/layouts/directives.js


src/components/common/i18n.js → src/layouts/i18n.js


+ 72 - 0
src/layouts/menu-item.vue

@@ -0,0 +1,72 @@
+<template>
+  <div id="menu-item">
+    <template v-for="(item, index) in menus">
+      <!-- 没有子菜单情况 -->
+      <template v-if="item && item.children && item.children <= 0">
+        <el-menu-item :index="item.route" @click="selectMenu(item.route, item.module)" :key="index">
+          <i v-if="item.icon" :class="item.icon"></i>
+          <span v-if="item.title" slot="title">
+            <!-- <span :style="{ paddingLeft: `${level * 1}px` }">{{ item.title }}</span> -->
+            {{ item.title }}
+          </span>
+        </el-menu-item>
+      </template>
+      <template v-else>
+        <el-submenu :index="item.title || item.route" :key="item.title">
+          <template slot="title">
+            <i v-if="item && item.icon" :class="item.icon"></i>
+            <span v-if="item && item.title">
+              <!-- <span :style="{ paddingLeft: `${level * 1}px` }">{{ item.title }}</span> -->
+              {{ item.title }}
+            </span>
+          </template>
+          <menu-item :menus="item.children" :level="level + 1"></menu-item>
+        </el-submenu>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'menu-item',
+  props: {
+    level: { type: Number, default: 0 },
+    menus: { type: Array, require: true },
+  },
+  components: {},
+  data: function() {
+    return {};
+  },
+  created() {},
+  methods: {
+    selectMenu(path, modules) {
+      if (!_.isEqual(this.$route.path, path)) this.$router.push({ path });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.el-submenu .el-menu-item {
+  min-width: 16rem !important;
+  // padding-left: 3rem !important;
+  &:hover {
+    // color: #fff !important;
+  }
+}
+.el-submenu__title i {
+  font-size: 1rem;
+}
+.el-menu-item,
+.el-submenu .el-menu-item {
+  &.is-active {
+    // color: #fff !important;
+  }
+}
+</style>

+ 2 - 2
src/main.js

@@ -12,11 +12,11 @@ import '@/plugins/loading';
 import '@/plugins/var';
 import '@/plugins/methods';
 import '@/plugins/setting';
-import { messages } from './components/common/i18n';
+import { messages } from './layouts/i18n';
 import 'element-ui/lib/theme-chalk/index.css'; // 默认主题
 // import './assets/css/theme-green/index.css'; // 浅绿色主题
 import './assets/css/icon.css';
-import './components/common/directives';
+import './layouts/directives';
 import 'babel-polyfill';
 
 Vue.config.productionTip = false;

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

@@ -30,7 +30,6 @@ const Plugin = {
         return _errText();
       }
       Message.error(_errText || errmsg);
-      Message({ message: _errText || errmsg, duration: 2000 });
       return false;
     };
   },

+ 12 - 0
src/router/auth.js

@@ -0,0 +1,12 @@
+export default [
+  {
+    path: '/menu',
+    component: () => import('../views/auth/menu.vue'),
+    meta: { title: '目录管理' },
+  },
+  {
+    path: '/role',
+    component: () => import('../views/auth/role.vue'),
+    meta: { title: '角色管理' },
+  },
+];

+ 8 - 8
src/router/index.js

@@ -1,10 +1,14 @@
 import Vue from 'vue';
 import Router from 'vue-router';
+import test from './test';
+import auth from './auth';
 
 Vue.use(Router);
 export default new Router({
   mode: 'history',
-  base: process.env.NODE_ENV === 'development' ? '' : 'study',
+  // base: process.env.NODE_ENV === 'development' ? '' : 'zhwl',
+  base: 'zhwl',
+
   routes: [
     {
       path: '/',
@@ -12,19 +16,15 @@ export default new Router({
     },
     {
       path: '/',
-      component: () => import('../components/common/Home.vue'),
-      meta: { title: '自述文件' },
+      component: () => import('../layouts/Home.vue'),
       children: [
         {
           path: '/homeIndex',
           component: () => import('../views/homeIndex.vue'),
           meta: { title: '系统首页' },
         },
-        {
-          path: '/test',
-          component: () => import('../views/test/index.vue'),
-          meta: { title: '测试菜单' },
-        },
+        ...test,
+        ...auth,
       ],
     },
     {

+ 13 - 0
src/router/test.js

@@ -0,0 +1,13 @@
+// test
+export default [
+  {
+    path: '/test',
+    component: () => import('../views/test/index.vue'),
+    meta: { title: '测试菜单' },
+  },
+  {
+    path: '/testdir/test',
+    component: () => import('../views/test/testdir/test.vue'),
+    meta: { title: '测试菜单2' },
+  },
+];

+ 43 - 0
src/store/auth/menu.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import axios from 'axios';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/auth/menu`,
+};
+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 findProject({ commit }, { project, ...payload }) {
+    const res = await this.$axios.$get(`${api.interface}/project/${project || ''}`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 43 - 0
src/store/auth/role.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import axios from 'axios';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/auth/role`,
+};
+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 roleMenu({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/menu/tree`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 39 - 0
src/store/auth/user.js

@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import axios from 'axios';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/zhwl/user`,
+};
+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;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 42 - 0
src/store/auth/userMenu.js

@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  interface: `/api/auth/usermenu`,
+};
+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 getMenu({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/menu`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 10 - 2
src/store/index.js

@@ -2,15 +2,23 @@ import Vue from 'vue';
 import Vuex from 'vuex';
 import * as ustate from './user/state';
 import * as umutations from './user/mutations';
-import test from "./test";
+import test from './test';
+import menu from './auth/menu';
+import role from './auth/role';
+import userMenu from './auth/userMenu';
+import util from './util';
 
 Vue.use(Vuex);
 
 export default new Vuex.Store({
-  state: { ...ustate },
+  state: { ...ustate, project: 'zhwl' },
   mutations: { ...umutations },
   actions: {},
   modules: {
     test,
+    util,
+    menu,
+    role,
+    userMenu,
   },
 });

+ 23 - 0
src/store/util.js

@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import axios from 'axios';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  authModel: model => `/api/auth/model/${model}`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async authModel({}, payload) {
+    const res = await this.$axios.$get(`${api.authModel(payload)}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 40 - 22
src/util/axios-wrapper.js

@@ -1,11 +1,12 @@
+/* eslint-disable require-atomic-updates */
 /* eslint-disable no-console */
 /* eslint-disable no-param-reassign */
 
 import _ from 'lodash';
 import Axios from 'axios';
 import { Util, Error } from 'naf-core';
-// import { Indicator } from 'mint-ui';
-import util from './user-util';
+import { Loading } from 'element-ui';
+import UserUtil from './user-util';
 
 const { trimData, isNullOrUndefined } = Util;
 const { ErrorCode } = Error;
@@ -42,17 +43,21 @@ export default class AxiosWrapper {
     return this.$request(uri, null, query, options);
   }
 
+  $delete(uri, query, options = {}) {
+    return this.$request(uri, null, query, { ...options, method: 'delete' });
+  }
+
   $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
+    // 过滤key:''的情况
+    query = _.pickBy(query, val => val !== '' && val !== 'undefined' && val !== 'null');
+    if (!uri) console.error('uri不能为空');
     if (_.isObject(query) && _.isObject(options)) {
-      options = { ...options, params: query, method: 'get' };
+      const params = query.params ? query.params : query;
+      options = { ...options, params };
     } else if (_.isObject(query) && !query.params) {
       options = { params: query };
     } else if (_.isObject(query) && query.params) {
@@ -61,16 +66,18 @@ export default class AxiosWrapper {
     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',
-    // });
+    // const loadingInstance = Loading.service({ spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.5)' });
 
     try {
       const axios = Axios.create({
         baseURL: this.baseUrl,
       });
-      axios.defaults.headers.common.Authorization = util.token;
+      const user = localStorage.getItem('user');
+      if (user) {
+        axios.defaults.headers.common.Authorization = encodeURI(user);
+      }
       let res = await axios.request({
         method: isNullOrUndefined(data) ? 'get' : 'post',
         url,
@@ -78,15 +85,15 @@ export default class AxiosWrapper {
         responseType: 'json',
         ...options,
       });
-      res = res.data;
+      res = res.data || {};
       const { errcode, errmsg, details } = res;
       if (errcode) {
         console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
-        return res;
+        return undefined;
       }
       // unwrap data
       if (this.unwrap) {
-        res = _.omit(res, ['errmsg', 'details']);
+        res = _.omit(res, ['errcode', 'errmsg', 'details']);
         const keys = Object.keys(res);
         if (keys.length === 1 && keys.includes('data')) {
           res = res.data;
@@ -96,21 +103,32 @@ export default class AxiosWrapper {
     } catch (err) {
       let errmsg = '接口请求失败,请稍后重试';
       if (err.response) {
-        const { status } = err.response;
+        const { status, data = {} } = err.response;
+        console.log(err.response);
         if (status === 401) errmsg = '用户认证失败,请重新登录';
         if (status === 403) errmsg = '当前用户不允许执行该操作';
+        if (status === 400 && data.errcode) {
+          const { errcode, errmsg, details } = data;
+          console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+          return data;
+        }
+        if (data && data.error) {
+          const { status, error, message } = data;
+          console.warn(`[${uri}] fail: ${status}: ${error}-${message}`);
+          return {
+            errcode: status || ErrorCode.SERVICE_FAULT,
+            errmsg: error,
+            details: message,
+          };
+        }
       }
-      console.error(
-        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
-        ${err.message}`
-      );
+      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();
+        // loadingInstance.close();
       }
     }
   }

+ 1 - 0
src/views/404.vue

@@ -13,6 +13,7 @@
 
 <script>
 export default {
+  name: 'notFound',
   methods: {
     goBack() {
       this.$router.go(-1);

+ 162 - 0
src/views/auth/menu.vue

@@ -0,0 +1,162 @@
+<template>
+  <div id="menus">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="container info">
+          <el-col :span="24" class="top right btn_bar">
+            <el-button type="primary" size="mini" @click="toDialog()">添加主目录</el-button>
+          </el-col>
+          <el-col :span="24" class="list">
+            <el-table :data="list" row-key="title" border stripe :tree-props="defaultProps" :default-expand-all="true">
+              <el-table-column header-align="center" label="目录名称" prop="title"></el-table-column>
+              <el-table-column align="center" label="目录路由" prop="route"></el-table-column>
+              <el-table-column align="center" label="操作">
+                <template v-slot="{ row }">
+                  <el-row :gutter="10" type="flex">
+                    <el-col :span="8">
+                      <el-link align="center" size="mini" type="primary" @click="toDialog(row)">修改目录</el-link>
+                    </el-col>
+                    <el-col :span="8">
+                      <el-link align="center" size="mini" type="success" @click="toDialog(row, 'append')">添加子目录</el-link>
+                    </el-col>
+                    <el-col :span="8">
+                      <el-link align="center" size="mini" type="danger" @click="toRemove(row)">删除目录</el-link>
+                    </el-col>
+                  </el-row>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog title="添加目录" width="30%" center :visible.sync="dialog" @close="toClose" :destroy-on-close="true">
+      <data-form :data="form" :fields="fields" :rules="rules" @save="append" :isNew="true">
+        <template #custom="{item,form}">
+          <template v-if="item.model === 'pid'"> {{ getParent(form) }} </template>
+        </template>
+      </data-form>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import dataForm from '@/components/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: util } = createNamespacedHelpers('util');
+const { mapActions: menu } = createNamespacedHelpers('menu');
+export default {
+  name: 'menus',
+  props: {},
+  components: { dataForm },
+  data: function() {
+    return {
+      dialog: false,
+      list: [],
+      defaultProps: { children: 'children', hasChildren: 'hasChildren' },
+      rules: {
+        title: [{ required: true, message: '请输入目录名称' }],
+      },
+      form: {},
+      fields: [],
+      isNew: true,
+    };
+  },
+  created() {
+    this.getModel();
+    this.search();
+  },
+  methods: {
+    ...util(['authModel']),
+    ...menu(['findProject', 'create', 'update', 'delete', 'fetch']),
+    async search() {
+      const res = await this.findProject({ project: this.project });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res);
+      }
+    },
+    async getModel() {
+      let res = await this.authModel('menu');
+      if (res) {
+        res = res.map(i => {
+          let { options } = i;
+          i = { ..._.omit(i, ['options']), ...options };
+          return i;
+        });
+
+        this.$set(this, `fields`, res);
+      }
+    },
+    async append({ data }) {
+      let dup = _.cloneDeep(data);
+      const { _id } = dup;
+      let res;
+      if (_id) {
+        res = await this.update(dup);
+      } else {
+        dup.project = this.project;
+        dup.type = this.type || 0;
+        if (!dup.sort) dup.sort = 0;
+        res = await this.create(dup);
+      }
+      if (this.$checkRes(res, '操作成功', res.errmsg || '操作失败')) {
+        this.toClose();
+        this.search();
+      }
+    },
+    async toRemove(data) {
+      this.$confirm(`确定删除${data.title}?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }).then(async () => {
+        // 直接更新
+        const { _id } = data;
+        let res = await this.delete(_id);
+        if (this.$checkRes(res, '操作成功', res.errmsg || '操作失败')) {
+          this.search();
+        }
+      });
+    },
+    getParent(data) {
+      const { pname } = data;
+      return pname || '主菜单';
+    },
+    async toDialog(data, type) {
+      this.dialog = true;
+      if (_.isObject(data)) {
+        const dup = _.cloneDeep(data);
+        if (type !== 'append') {
+          const { pid } = dup;
+          if (pid) {
+            const pres = await this.fetch(pid);
+            if (this.$checkRes(pres)) {
+              const { title: pname } = pres;
+              dup.pname = pname;
+            }
+          }
+          this.$set(this, `form`, dup);
+        } else {
+          const { _id: pid, title: pname } = dup;
+          this.$set(this, `form`, { pid, pname });
+        }
+      }
+    },
+    toClose() {
+      this.dialog = false;
+      this.form = {};
+    },
+  },
+  computed: {
+    ...mapState(['user', 'project']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 217 - 0
src/views/auth/role.vue

@@ -0,0 +1,217 @@
+<template>
+  <div id="index">
+    <el-row type="flex" :gutter="20">
+      <el-col :span="10">
+        <el-card :body-style="cardStyle">
+          <template #header>
+            <el-row type="flex" justify="space-between" align="middle">
+              <el-col :span="4">角色</el-col>
+              <el-col :span="4">
+                <el-button type="primary" size="small" @click="toAdd">添加角色</el-button>
+              </el-col>
+            </el-row>
+          </template>
+          <el-table :data="roleList" style="width: 100%" border stripe size="small">
+            <el-table-column align="center" prop="name" label="名称">
+              <template v-slot="{ row }">
+                <div v-if="!row.toEdit">{{ row.name }}</div>
+                <div v-else>
+                  <el-input v-model="row.name" size="small"></el-input>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" prop="type" label="类型代码">
+              <template v-slot="{ row }">
+                <div v-if="!row.toEdit">{{ row.type }}</div>
+                <div v-else>
+                  <el-input v-model="row.type" size="small"></el-input>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" prop="name" label="操作">
+              <template v-slot="{ row }">
+                <el-row type="flex" justify="space-around" align="middle">
+                  <el-col :span="4">
+                    <el-button v-if="!row.toEdit" type="text" @click="toUpdate(row)">修改</el-button>
+                    <el-button v-else type="text" style="color:#67C23A" @click="toSave(row)">保存</el-button>
+                  </el-col>
+                  <template v-if="row.id">
+                    <el-col :span="4">
+                      <el-button type="text" style="color:#909399" @click="toSetRoleMenu(row)">权限</el-button>
+                    </el-col>
+                    <el-col :span="4">
+                      <el-button v-if="!row.disabled" type="text" style="color:#F56C6C" @click="changeDisable(row, true)">禁用</el-button>
+                      <el-button v-else type="text" style="color:#67C23A" @click="changeDisable(row, false)">启用</el-button>
+                    </el-col>
+                    <el-col :span="4">
+                      <el-button type="text" style="color:#F56C6C" @click="toDelete(row)">删除</el-button>
+                    </el-col>
+                  </template>
+                </el-row>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+      <el-col :span="14">
+        <el-card header="权限" :body-style="cardStyle">
+          <template #header>
+            <el-row type="flex" justify="space-between" align="middle">
+              <el-col :span="4">
+                权限
+                <span v-if="selectRole.id"> => {{ selectRole.name }} </span>
+              </el-col>
+              <el-col :span="4">
+                <el-button type="primary" size="small" v-if="selectRole.id" @click="toSaveMenu">保存 {{ selectRole.name }} 权限</el-button>
+                <el-button type="danger" size="small" v-else :disabled="true">未选择角色</el-button>
+              </el-col>
+            </el-row>
+          </template>
+          <el-tree
+            :data="menus"
+            ref="tree"
+            node-key="id"
+            :props="defaultProps"
+            :highlight-current="true"
+            show-checkbox
+            default-expand-all
+            :default-checked-keys="isSelect"
+          >
+          </el-tree>
+          <!-- @check-change="toPermission" -->
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: menu } = createNamespacedHelpers('menu');
+const { mapActions: role } = createNamespacedHelpers('role');
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      cardStyle: {
+        height: '600px',
+        overflowY: 'scroll',
+      },
+      menus: [],
+      roleList: [],
+      selectRole: {},
+      isSelect: [],
+      defaultProps: {
+        children: 'children',
+        label: 'title',
+      },
+    };
+  },
+  created() {
+    this.searchRole();
+    this.toFindMenuList();
+  },
+  methods: {
+    ...role(['create', 'update', 'query', 'delete']),
+    ...menu(['findProject']),
+    async searchRole() {
+      const res = await this.query();
+      if (this.$checkRes(res)) {
+        const { data = [] } = res;
+        this.$set(this, `roleList`, data);
+      }
+    },
+    async toFindMenuList() {
+      const res = await this.findProject({ project: this.project });
+      if (this.$checkRes(res)) {
+        this.$set(this, `menus`, res);
+      }
+    },
+    // 添加前置
+    toAdd() {
+      this.roleList.push({ toEdit: true });
+    },
+    // 修改前置
+    toUpdate(row) {
+      const index = _.findIndex(this.roleList, row);
+      if (index >= 0) {
+        this.$set(this.roleList, index, { ...row, toEdit: true });
+      }
+    },
+    // 添加/修改
+    async toSave(data) {
+      const dup = _.cloneDeep(data);
+      const { toEdit, ...info } = dup;
+      if (!toEdit) {
+        console.warn('不能保存未处于编辑状态的数据');
+        return;
+      }
+      const id = _.get(info, `id`);
+      let res;
+      if (id) {
+        res = await this.update(info);
+      } else {
+        info.project = this.project;
+        res = await this.create(info);
+      }
+      if (this.$checkRes(res, '保存成功', res.errmsg || '保存失败')) {
+        this.searchRole();
+      }
+    },
+    // 禁用/启用
+    async changeDisable(data, disabled) {
+      let duplicate = _.cloneDeep(data);
+      duplicate.disabled = disabled;
+      const res = await this.update(duplicate);
+      let word = disabled === true ? '禁用' : '启用';
+      if (this.$checkRes(res, `${word}成功`, res.errmsg || `${word}失败`)) {
+        this.searchRole();
+      }
+    },
+    // 删除
+    async toDelete(data) {
+      this.$confirm(`确定删除${data.name}?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }).then(async () => {
+        // 直接更新
+        const { _id } = data;
+        let res = await this.delete(_id);
+        if (this.$checkRes(res, '删除成功', res.errmsg || '删除失败')) {
+          this.searchRole();
+        }
+      });
+    },
+    // 设置该角色已选择的权限
+    toSetRoleMenu(data) {
+      const { menu = [] } = data;
+      this.$set(this, `selectRole`, _.cloneDeep(data));
+      this.$refs.tree.setCheckedKeys(menu);
+    },
+    async toSaveMenu() {
+      const selected = this.$refs.tree.getCheckedKeys();
+      let duplicate = _.cloneDeep(this.selectRole);
+      duplicate.menu = selected;
+      const res = await this.update(duplicate);
+      if (this.$checkRes(res, `权限设置成功`, res.errmsg || `权限设置失败`)) {
+        this.searchRole();
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user', 'project']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 3 - 13
src/views/homeIndex.vue

@@ -1,27 +1,17 @@
 <template>
-  <div id="Dashboard">
-    <el-row>
-      <el-col :span="24" class="main">
-        <breadcrumb :breadcrumbTitle="this.$route.meta.title"></breadcrumb>
-        <el-col :span="24" class="container"> </el-col>
-      </el-col>
-    </el-row>
-  </div>
+  <div id="Dashboard"></div>
 </template>
 
 <script>
-import breadcrumb from '@c/common/breadcrumb.vue';
 import { mapState, createNamespacedHelpers } from 'vuex';
-import bus from '@/components/common/bus';
+import bus from '@l/bus';
 export default {
   metaInfo() {
     return { title: this.$route.meta.title };
   },
   name: 'Dashboard',
   props: {},
-  components: {
-    breadcrumb,
-  },
+  components: {},
   data: function() {
     return {};
   },

+ 8 - 16
src/views/test/index.vue

@@ -2,28 +2,25 @@
   <div id="index">
     <el-row>
       <el-col :span="24" class="main">
-        <breadcrumb :breadcrumbTitle="this.$route.meta.title"></breadcrumb>
         <el-col :span="24" class="container info">
           <el-col :span="24" class="top">
             <el-button type="primary" size="mini" @click="dialog = true">添加</el-button>
           </el-col>
           <el-col :span="24" class="list">
-            <data-table :fields="fields" :opera="opera" :data="list" :total="total" @edit="toEdit" @delete="toDelete" @query="search"></data-table>
+            <!-- <data-table :fields="fields" :opera="opera" :data="list" :total="total" @edit="toEdit" @delete="toDelete" @query="search"></data-table> -->
           </el-col>
         </el-col>
       </el-col>
     </el-row>
     <el-dialog :visible.sync="dialog" title="增加菜单" @close="toClose" :destroy-on-close="true" width="50%">
-      <data-form :data="form" :fields="formFields" :rules="rules" @save="turnSave">
-      </data-form>
+      <!-- <data-form :data="form" :fields="formFields" :rules="rules" @save="turnSave"> </data-form> -->
     </el-dialog>
   </div>
 </template>
 
 <script>
-import breadcrumb from '@c/common/breadcrumb.vue';
-import dataTable from '@/components/frame/filter-page-table.vue';
-import dataForm from '@/components/frame/form.vue';
+// import dataTable from '@/components/frame/filter-page-table.vue';
+// import dataForm from '@/components/frame/form.vue';
 import { mapState, createNamespacedHelpers } from 'vuex';
 export default {
   metaInfo() {
@@ -32,9 +29,8 @@ export default {
   name: 'index',
   props: {},
   components: {
-    breadcrumb,
-    dataTable,
-    dataForm,
+    // dataTable,
+    // dataForm,
   },
   data: function() {
     return {
@@ -50,16 +46,12 @@ export default {
           method: 'delete',
         },
       ],
-      fields: [
-        { label: '菜单名称', prop: 'title' },
-      ],
+      fields: [{ label: '菜单名称', prop: 'title' }],
       list: [],
       total: 0,
       // 增加菜单
       dialog: false,
-      formFields: [
-        { label: '菜单名称', required: true, model: 'title' },
-      ],
+      formFields: [{ label: '菜单名称', required: true, model: 'title' }],
       form: {},
       rules: {
         title: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],

+ 0 - 0
src/views/test/testdir/test.vue


+ 21 - 14
vue.config.js

@@ -1,35 +1,42 @@
 const path = require('path');
 module.exports = {
-  publicPath:
-    process.env.NODE_ENV === "development" ? "/" : process.env.VUE_APP_ROUTER,
+  // publicPath: process.env.NODE_ENV === 'development' ? '/' : process.env.VUE_APP_ROUTER,
+  publicPath: process.env.VUE_APP_ROUTER,
+
   // 打包文件
-  outputDir: "cmszhwl",
-  configureWebpack: (config) => {
+  outputDir: 'zhwl',
+  configureWebpack: config => {
     Object.assign(config, {
       // 开发生产共同配置
       resolve: {
         alias: {
-          "@": path.resolve(__dirname, "./src"),
-          "@c": path.resolve(__dirname, "./src/components"),
-          "@a": path.resolve(__dirname, "./src/assets"),
+          '@': path.resolve(__dirname, './src'),
+          '@c': path.resolve(__dirname, './src/components'),
+          '@a': path.resolve(__dirname, './src/assets'),
+          '@l': path.resolve(__dirname, './src/layouts'),
         },
       },
     });
   },
   devServer: {
-    port: "7001",
+    port: '7001',
     //api地址前缀
     proxy: {
-      "/files": {
-        target: "http://free.liaoningdoupo.com",
+      '/api/auth': {
+        target: 'http://127.0.0.1:7006',
+        changeOrigin: true,
+        ws: true,
+      },
+      '/files': {
+        target: 'http://free.liaoningdoupo.com',
       },
-      "/api": {
-        target: "http://free.liaoningdoupo.com",
+      '/api': {
+        target: 'http://free.liaoningdoupo.com',
         changeOrigin: true,
         ws: true,
       },
-      "/ws": {
-        target: "http://free.liaoningdoupo.com",
+      '/ws': {
+        target: 'http://free.liaoningdoupo.com',
         ws: true,
       },
     },