Explorar o código

新增授权配置以及支持ip访问

zhangjie %!s(int64=3) %!d(string=hai) anos
pai
achega
4df594b507

+ 34 - 28
src/constants/adminNavs.js

@@ -1,28 +1,34 @@
-const navs = [
-  {
-    id: "1",
-    parentId: "-1",
-    name: "超管中心",
-    url: "admin"
-  },
-  {
-    id: "2",
-    parentId: "1",
-    name: "用户管理",
-    url: "AdminUserManage"
-  },
-  {
-    id: "3",
-    parentId: "1",
-    name: "系统角色管理",
-    url: "SystemRoleManage"
-  },
-  {
-    id: "4",
-    parentId: "1",
-    name: "菜单管理",
-    url: "SchoolMenuManage"
-  }
-];
-
-export default navs;
+const navs = [
+  {
+    id: "1",
+    parentId: "-1",
+    name: "超管中心",
+    url: "admin"
+  },
+  {
+    id: "2",
+    parentId: "1",
+    name: "用户管理",
+    url: "AdminUserManage"
+  },
+  {
+    id: "3",
+    parentId: "1",
+    name: "系统角色管理",
+    url: "SystemRoleManage"
+  },
+  {
+    id: "4",
+    parentId: "1",
+    name: "菜单管理",
+    url: "SchoolMenuManage"
+  },
+  {
+    id: "5",
+    parentId: "1",
+    name: "授权配置",
+    url: "AuthSet"
+  }
+];
+
+export default navs;

+ 30 - 20
src/constants/app.js

@@ -1,20 +1,30 @@
-const MD5 = require("js-md5");
-
-// domain
-let domain;
-if (process.env.VUE_APP_SELF_DEFINE_DOMAIN === "true") {
-  domain = window.localStorage.getItem("domain_in_url");
-}
-if (!domain) domain = window.location.hostname.split(".")[0];
-export const ORG_CODE = domain;
-
-const ADMIN_CODE = "admin";
-
-export const IS_ADMIN_SYSTEM = ADMIN_CODE === ORG_CODE;
-
-export const PLATFORM = "WEB";
-
-if (!localStorage.getItem("deviceId")) {
-  localStorage.setItem("deviceId", MD5(Math.random() + "-" + Date.now()));
-}
-export const DEVICE_ID = localStorage.getItem("deviceId");
+import { parseHrefParam } from "../plugins/utils";
+
+const MD5 = require("js-md5");
+
+// domain
+let domain;
+if (process.env.VUE_APP_SELF_DEFINE_DOMAIN === "true") {
+  domain = window.localStorage.getItem("domain_in_url");
+}
+if (!domain) {
+  const ipFormat = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
+  const hostname = window.location.hostname;
+  if (ipFormat.test(hostname)) {
+    domain = parseHrefParam(window.location.href, "code");
+  } else {
+    domain = hostname.split(".")[0];
+  }
+}
+export const ORG_CODE = domain;
+
+const ADMIN_CODE = "admin";
+
+export const IS_ADMIN_SYSTEM = ADMIN_CODE === ORG_CODE;
+
+export const PLATFORM = "WEB";
+
+if (!localStorage.getItem("deviceId")) {
+  localStorage.setItem("deviceId", MD5(Math.random() + "-" + Date.now()));
+}
+export const DEVICE_ID = localStorage.getItem("deviceId");

+ 59 - 42
src/modules/admin/api.js

@@ -1,42 +1,59 @@
-import { $postParam, $post } from "@/plugins/axios";
-
-// user-manage
-export const userListPage = datas => {
-  return $postParam("/api/admin/sys/user/list_customer", datas);
-};
-export const updateUser = datas => {
-  return $post("/api/admin/sys/user/save_customer", datas);
-};
-export const ableUser = ({ id, enable }) => {
-  return $post("/api/admin/sys/user/enable", { id, enable });
-};
-export const resetPwd = id => {
-  return $post("/api/admin/sys/user/reset_password", { id });
-};
-export const userRoleListPage = () => {
-  return $postParam("/api/admin/sys/role/list_to_user", {});
-};
-
-// menu-manage
-export const menuList = datas => {
-  return $postParam("/api/admin/sys/privilege/list", datas);
-};
-export const updateMenu = datas => {
-  return $post("/api/admin/sys/privilege/save", datas);
-};
-export const deleteMenu = id => {
-  return $post("/api/admin/sys/privilege/remove", { id });
-};
-
-// school-menu-manage
-export const schoolMenuTree = () => {
-  return $post("/api/admin/menu/custom/list", {});
-};
-export const schoolSelectedMenuTree = schoolId => {
-  return $postParam("/api/admin/menu/custom/get_school_custom_privileges", {
-    schoolId
-  });
-};
-export const updateSchoolMenu = ({ schoolId, privilegeIds }) => {
-  return $post("/api/admin/menu/custom/save", { schoolId, privilegeIds });
-};
+import { $postParam, $post } from "@/plugins/axios";
+
+// user-manage
+export const userListPage = datas => {
+  return $postParam("/api/admin/sys/user/list_customer", datas);
+};
+export const updateUser = datas => {
+  return $post("/api/admin/sys/user/save_customer", datas);
+};
+export const ableUser = ({ id, enable }) => {
+  return $post("/api/admin/sys/user/enable", { id, enable });
+};
+export const resetPwd = id => {
+  return $post("/api/admin/sys/user/reset_password", { id });
+};
+export const userRoleListPage = () => {
+  return $postParam("/api/admin/sys/role/list_to_user", {});
+};
+
+// menu-manage
+export const menuList = datas => {
+  return $postParam("/api/admin/sys/privilege/list", datas);
+};
+export const updateMenu = datas => {
+  return $post("/api/admin/sys/privilege/save", datas);
+};
+export const deleteMenu = id => {
+  return $post("/api/admin/sys/privilege/remove", { id });
+};
+
+// school-menu-manage
+export const schoolMenuTree = () => {
+  return $post("/api/admin/menu/custom/list", {});
+};
+export const schoolSelectedMenuTree = schoolId => {
+  return $postParam("/api/admin/menu/custom/get_school_custom_privileges", {
+    schoolId
+  });
+};
+export const updateSchoolMenu = ({ schoolId, privilegeIds }) => {
+  return $post("/api/admin/menu/custom/save", { schoolId, privilegeIds });
+};
+// auth-set
+// school-menu-manage
+export const authSelect = () => {
+  return $post("/api/admin/auth/select", {});
+};
+export const offlineActivation = datas => {
+  return $post("/api/admin/auth/offline/activation", datas);
+};
+export const exportDeviceInfo = () => {
+  return $post(
+    "/api/admin/auth/export/device/info",
+    {},
+    {
+      responseType: "blob"
+    }
+  );
+};

+ 39 - 33
src/modules/admin/router.js

@@ -1,33 +1,39 @@
-import AdminUserManage from "./views/AdminUserManage.vue";
-import PrivilegeManage from "./views/PrivilegeManage.vue";
-import SystemRoleManage from "./views/SystemRoleManage.vue";
-import SchoolMenuManage from "./views/SchoolMenuManage.vue";
-import Admin from "./views/Admin.vue";
-
-export default {
-  path: "/admin",
-  name: "Admin",
-  component: Admin,
-  children: [
-    {
-      path: "admin-user-manage",
-      name: "AdminUserManage",
-      component: AdminUserManage
-    },
-    {
-      path: "privilege-manage",
-      name: "PrivilegeManage",
-      component: PrivilegeManage
-    },
-    {
-      path: "system-role-manage",
-      name: "SystemRoleManage",
-      component: SystemRoleManage
-    },
-    {
-      path: "school-menu-manage",
-      name: "SchoolMenuManage",
-      component: SchoolMenuManage
-    }
-  ]
-};
+import AdminUserManage from "./views/AdminUserManage.vue";
+import PrivilegeManage from "./views/PrivilegeManage.vue";
+import SystemRoleManage from "./views/SystemRoleManage.vue";
+import SchoolMenuManage from "./views/SchoolMenuManage.vue";
+import AuthSet from "./views/AuthSet.vue";
+import Admin from "./views/Admin.vue";
+
+export default {
+  path: "/admin",
+  name: "Admin",
+  component: Admin,
+  children: [
+    {
+      path: "admin-user-manage",
+      name: "AdminUserManage",
+      component: AdminUserManage
+    },
+    {
+      path: "privilege-manage",
+      name: "PrivilegeManage",
+      component: PrivilegeManage
+    },
+    {
+      path: "system-role-manage",
+      name: "SystemRoleManage",
+      component: SystemRoleManage
+    },
+    {
+      path: "school-menu-manage",
+      name: "SchoolMenuManage",
+      component: SchoolMenuManage
+    },
+    {
+      path: "auth-set",
+      name: "AuthSet",
+      component: AuthSet
+    }
+  ]
+};

+ 222 - 222
src/modules/admin/views/Admin.vue

@@ -1,222 +1,222 @@
-<template>
-  <div class="admin">
-    <div class="home-header">
-      <div class="head-user menu-list">
-        <ul>
-          <li @click="toSelectSchool">
-            <i class="el-icon-s-home"></i>
-            <span>切换学校</span>
-          </li>
-          <li class="menu-item menu-item-account" @click="toResetPwd">
-            <i class="icon icon-account"></i>
-            <span :title="username">{{ username }}</span>
-          </li>
-          <li class="menu-item" @click="toLogout">
-            <i class="icon icon-logout" title="退出登录"></i>
-          </li>
-        </ul>
-      </div>
-    </div>
-
-    <div class="home-navs">
-      <div class="head-logo">
-        <div class="head-logo-content">
-          <h1>知学知考</h1>
-        </div>
-      </div>
-      <el-menu
-        class="el-menu-home"
-        active-text-color="#705eff"
-        text-color="#383b4a"
-        router
-        :default-active="curRouteName"
-        :default-openeds="curSubMenuNames"
-      >
-        <el-submenu
-          v-for="submenu in menus"
-          :key="submenu.url"
-          :index="submenu.url"
-        >
-          <template slot="title">
-            <span>{{ submenu.name }}</span>
-          </template>
-
-          <el-menu-item
-            v-for="nav in submenu.children"
-            :key="nav.url"
-            :index="nav.url"
-            :route="{ name: nav.url }"
-          >
-            <span>{{ nav.name }}</span>
-          </el-menu-item>
-        </el-submenu>
-      </el-menu>
-    </div>
-
-    <div class="home-body">
-      <div class="home-main">
-        <div class="home-breadcrumb" v-if="breadcrumbs.length">
-          <span class="breadcrumb-tips">
-            <i class="icon icon-location"></i>
-            <span>当前所在位置:</span>
-          </span>
-          <el-breadcrumb separator="/">
-            <el-breadcrumb-item
-              v-for="(bread, index) in breadcrumbs"
-              :key="index"
-              >{{ bread.name }}</el-breadcrumb-item
-            >
-          </el-breadcrumb>
-        </div>
-
-        <!-- home-view: page detail -->
-        <div class="home-view">
-          <router-view />
-        </div>
-      </div>
-    </div>
-
-    <!-- 修改密码 -->
-    <reset-pwd
-      ref="ResetPwd"
-      :user-info="userInfo"
-      @modified="resetPwdModified"
-    ></reset-pwd>
-  </div>
-</template>
-
-<script>
-import localNavs from "@/constants/adminNavs";
-import { SYS_ADMIN_NAME } from "@/constants/enumerate";
-import { logout } from "@/modules/login/api";
-import ResetPwd from "../../base/components/ResetPwd";
-
-export default {
-  name: "home",
-  components: { ResetPwd },
-  data() {
-    const user = this.$ls.get("user", { id: "", realName: "", roleList: [] });
-
-    return {
-      menus: [],
-      privileges: [],
-      curRouteName: "",
-      curSubMenuNames: [],
-      breadcrumbs: [],
-      username: user.realName,
-      userInfo: {
-        pwdCount: 0,
-        mobileNumber: 1,
-        userId: user.id
-      }
-    };
-  },
-  watch: {
-    $route(val) {
-      if (val.name === "Admin") return;
-      this.actCurNav();
-    }
-  },
-  created() {
-    const loginName = this.$ls.get("user", { loginName: "" }).loginName;
-    if (loginName !== SYS_ADMIN_NAME) {
-      this.$message.error("非法操作!");
-      this.$router.replace({ name: "Login" });
-      return;
-    }
-
-    if (this.$route.name === "Admin") {
-      this.$router.replace({
-        name: "AdminUserManage"
-      });
-    }
-    this.initData();
-  },
-  methods: {
-    initData() {
-      this.privileges = localNavs;
-      this.menus = this.getMenu();
-      this.curSubMenuNames = this.menus.map(item => item.url);
-
-      if (this.$route.name === "Admin") {
-        const firstRouteName = this.getFirstRouter();
-        this.$router.replace({
-          name: firstRouteName
-        });
-        return;
-      }
-
-      this.actCurNav();
-    },
-    getFirstRouter() {
-      let childNavs = this.privileges;
-      let firstRouteName = "";
-      while (childNavs.length) {
-        firstRouteName = childNavs[0].url;
-        childNavs = this.privileges.filter(
-          item => item.parentId === childNavs[0].id
-        );
-      }
-
-      return firstRouteName;
-    },
-    getMenu() {
-      let menus = this.privileges.filter(item => item.parentId === "-1");
-      const toTree = menus => {
-        menus.forEach(menu => {
-          const children = this.privileges.filter(
-            item => item.parentId === menu.id
-          );
-          if (children.length) {
-            menu.children = children;
-            toTree(menu.children);
-          }
-        });
-      };
-      toTree(menus);
-
-      return menus;
-    },
-    actCurNav() {
-      this.curRouteName = this.$route.name;
-      let breadcrumbs = [];
-      let curBreadcrumb = this.menus.find(
-        item => item.url === this.curRouteName
-      );
-      breadcrumbs.push({ ...curBreadcrumb });
-
-      while (curBreadcrumb && curBreadcrumb.parentId !== "-1") {
-        curBreadcrumb = this.privileges.find(
-          item => item.id === curBreadcrumb.parentId
-        );
-        if (curBreadcrumb) breadcrumbs.unshift({ ...curBreadcrumb });
-      }
-
-      this.breadcrumbs = breadcrumbs;
-    },
-    toLogout() {
-      this.$confirm("确定要退出登录吗?", "提示", {
-        type: "warning"
-      })
-        .then(() => {
-          this.logoutAction();
-        })
-        .catch(() => {});
-    },
-    async logoutAction() {
-      await logout();
-      this.$ls.clear();
-      this.$router.push({ name: "Login" });
-    },
-    toSelectSchool() {
-      this.$router.push({ name: "SelectSchool" });
-    },
-    toResetPwd() {
-      this.$refs.ResetPwd.open();
-    },
-    resetPwdModified() {
-      this.logoutAction();
-    }
-  }
-};
-</script>
+<template>
+  <div class="admin">
+    <div class="home-header">
+      <div class="head-user menu-list">
+        <ul>
+          <li @click="toSelectSchool">
+            <i class="el-icon-s-home"></i>
+            <span>切换学校</span>
+          </li>
+          <li class="menu-item menu-item-account" @click="toResetPwd">
+            <i class="icon icon-account"></i>
+            <span :title="username">{{ username }}</span>
+          </li>
+          <li class="menu-item" @click="toLogout">
+            <i class="icon icon-logout" title="退出登录"></i>
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <div class="home-navs">
+      <div class="head-logo">
+        <div class="head-logo-content">
+          <h1>知学知考</h1>
+        </div>
+      </div>
+      <el-menu
+        class="el-menu-home"
+        active-text-color="#705eff"
+        text-color="#383b4a"
+        router
+        :default-active="curRouteName"
+        :default-openeds="curSubMenuNames"
+      >
+        <el-submenu
+          v-for="submenu in menus"
+          :key="submenu.url"
+          :index="submenu.url"
+        >
+          <template slot="title">
+            <span>{{ submenu.name }}</span>
+          </template>
+
+          <el-menu-item
+            v-for="nav in submenu.children"
+            :key="nav.url"
+            :index="nav.url"
+            :route="{ name: nav.url }"
+          >
+            <span>{{ nav.name }}</span>
+          </el-menu-item>
+        </el-submenu>
+      </el-menu>
+    </div>
+
+    <div class="home-body">
+      <div class="home-main">
+        <div class="home-breadcrumb" v-if="breadcrumbs.length">
+          <span class="breadcrumb-tips">
+            <i class="icon icon-location"></i>
+            <span>当前所在位置:</span>
+          </span>
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item
+              v-for="(bread, index) in breadcrumbs"
+              :key="index"
+              >{{ bread.name }}</el-breadcrumb-item
+            >
+          </el-breadcrumb>
+        </div>
+
+        <!-- home-view: page detail -->
+        <div class="home-view">
+          <router-view />
+        </div>
+      </div>
+    </div>
+
+    <!-- 修改密码 -->
+    <reset-pwd
+      ref="ResetPwd"
+      :user-info="userInfo"
+      @modified="resetPwdModified"
+    ></reset-pwd>
+  </div>
+</template>
+
+<script>
+import localNavs from "@/constants/adminNavs";
+import { SYS_ADMIN_NAME } from "@/constants/enumerate";
+import { logout } from "@/modules/login/api";
+import ResetPwd from "../../base/components/ResetPwd";
+
+export default {
+  name: "home",
+  components: { ResetPwd },
+  data() {
+    const user = this.$ls.get("user", { id: "", realName: "", roleList: [] });
+
+    return {
+      menus: [],
+      privileges: [],
+      curRouteName: "",
+      curSubMenuNames: [],
+      breadcrumbs: [],
+      username: user.realName,
+      userInfo: {
+        pwdCount: 0,
+        mobileNumber: 1,
+        userId: user.id
+      }
+    };
+  },
+  watch: {
+    $route(val) {
+      if (val.name === "Admin") return;
+      this.actCurNav();
+    }
+  },
+  created() {
+    const loginName = this.$ls.get("user", { loginName: "" }).loginName;
+    if (loginName !== SYS_ADMIN_NAME) {
+      this.$message.error("非法操作!");
+      this.$router.replace({ name: "Login" });
+      return;
+    }
+
+    if (this.$route.name === "Admin") {
+      this.$router.replace({
+        name: "AdminUserManage"
+      });
+    }
+    this.initData();
+  },
+  methods: {
+    initData() {
+      this.privileges = localNavs;
+      this.menus = this.getMenu();
+      this.curSubMenuNames = this.menus.map(item => item.url);
+
+      if (this.$route.name === "Admin") {
+        const firstRouteName = this.getFirstRouter();
+        this.$router.replace({
+          name: firstRouteName
+        });
+        return;
+      }
+
+      this.actCurNav();
+    },
+    getFirstRouter() {
+      let childNavs = this.privileges;
+      let firstRouteName = "";
+      while (childNavs.length) {
+        firstRouteName = childNavs[0].url;
+        childNavs = this.privileges.filter(
+          item => item.parentId === childNavs[0].id
+        );
+      }
+
+      return firstRouteName;
+    },
+    getMenu() {
+      let menus = this.privileges.filter(item => item.parentId === "-1");
+      const toTree = menus => {
+        menus.forEach(menu => {
+          const children = this.privileges.filter(
+            item => item.parentId === menu.id
+          );
+          if (children.length) {
+            menu.children = children;
+            toTree(menu.children);
+          }
+        });
+      };
+      toTree(menus);
+
+      return menus;
+    },
+    actCurNav() {
+      this.curRouteName = this.$route.name;
+      let breadcrumbs = [];
+      let curBreadcrumb = this.privileges.find(
+        item => item.url === this.curRouteName
+      );
+      breadcrumbs.push({ ...curBreadcrumb });
+
+      while (curBreadcrumb && curBreadcrumb.parentId !== "-1") {
+        curBreadcrumb = this.privileges.find(
+          item => item.id === curBreadcrumb.parentId
+        );
+        if (curBreadcrumb) breadcrumbs.unshift({ ...curBreadcrumb });
+      }
+
+      this.breadcrumbs = breadcrumbs;
+    },
+    toLogout() {
+      this.$confirm("确定要退出登录吗?", "提示", {
+        type: "warning"
+      })
+        .then(() => {
+          this.logoutAction();
+        })
+        .catch(() => {});
+    },
+    async logoutAction() {
+      await logout();
+      this.$ls.clear();
+      this.$router.push({ name: "Login" });
+    },
+    toSelectSchool() {
+      this.$router.push({ name: "SelectSchool" });
+    },
+    toResetPwd() {
+      this.$refs.ResetPwd.open();
+    },
+    resetPwdModified() {
+      this.logoutAction();
+    }
+  }
+};
+</script>

+ 87 - 0
src/modules/admin/views/AuthSet.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="auth-set">
+    <div class="part-box part-box-pad">
+      <el-form ref="modalFormComp" :model="modalForm" label-width="120px">
+        <el-form-item prop="loginName" label="当前信息:">
+          {{ modalForm.expireTime === "-1" ? "未授权" : "已授权" }}
+        </el-form-item>
+        <el-form-item label="过期时间:">
+          <span v-if="modalForm.expireTime === -1">不过期</span>
+          <span v-else-if="modalForm.expireTime">
+            {{ modalForm.expireTime | timestampFilter }}</span
+          >
+          <span v-else>--</span>
+        </el-form-item>
+        <el-form-item prop="mobileNumber" label="导出:">
+          <el-button type="primary" :loading="downloading" @click="toExport">
+            硬件信息
+          </el-button>
+        </el-form-item>
+        <el-form-item prop="code" label="导入授权文件:">
+          <upload-button
+            btn-content="选择文件"
+            btn-type="primary"
+            :upload-url="uploadUrl"
+            :format="['lic']"
+            accept=".lic"
+            style="margin:0;"
+            @valid-error="validError"
+            @upload-success="uploadSuccess"
+          >
+          </upload-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { downloadByApi } from "@/plugins/download";
+import { exportDeviceInfo, authSelect } from "../api";
+import UploadButton from "@/components/UploadButton";
+
+export default {
+  name: "auth-set",
+  components: { UploadButton },
+  data() {
+    return {
+      modalForm: {
+        expireTime: "",
+        authFile: null
+      },
+      downloading: false,
+      // import
+      uploadUrl: "/api/admin/auth/offline/activation"
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      const res = await authSelect();
+      this.modalForm.expireTime = res;
+    },
+    async toExport() {
+      if (this.downloading) return;
+      this.downloading = true;
+
+      const res = await downloadByApi(() => {
+        return exportDeviceInfo();
+      }).catch(e => {
+        this.$message.error(e || "下载失败,请重新尝试!");
+      });
+      this.downloading = false;
+
+      if (!res) return;
+      this.$message.success("下载成功!");
+    },
+    validError(errorData) {
+      this.$message.error(errorData.message);
+    },
+    uploadSuccess() {
+      this.search();
+    }
+  }
+};
+</script>

+ 413 - 399
src/plugins/utils.js

@@ -1,399 +1,413 @@
-const deepmerge = require("deepmerge");
-
-/**
- * 判断对象类型
- * @param {*} obj 对象
- */
-export function objTypeOf(obj) {
-  const toString = Object.prototype.toString;
-  const map = {
-    "[object Boolean]": "boolean",
-    "[object Number]": "number",
-    "[object String]": "string",
-    "[object Function]": "function",
-    "[object Array]": "array",
-    "[object Date]": "date",
-    "[object RegExp]": "regExp",
-    "[object Undefined]": "undefined",
-    "[object Null]": "null",
-    "[object Object]": "object",
-    "[object Blob]": "blob"
-  };
-  return map[toString.call(obj)];
-}
-
-/**
- * 深拷贝
- * @param {Object/Array} data 需要拷贝的数据
- */
-export function deepCopy(data, options) {
-  const defObj = objTypeOf(data) === "array" ? [] : {};
-  return deepmerge(defObj, data, options || {});
-}
-
-/**
- * 将目标对象中有的属性值与源对象中的属性值合并
- * @param {Object} target 目标对象
- * @param {Object} sources 源对象
- */
-export function objAssign(target, sources) {
-  let targ = { ...target };
-  for (let k in targ) {
-    targ[k] = sources.hasOwnProperty(k) ? sources[k] : targ[k];
-  }
-  return targ;
-}
-
-/**
- * 文件流下载
- * @param {Object} option 文件下载设置
- */
-export function download(option) {
-  let defOpt = {
-    type: "get",
-    url: "",
-    data: "",
-    fileName: "",
-    header: ""
-  };
-  let opt = objAssign(defOpt, option);
-
-  return new Promise((resolve, reject) => {
-    let xhr = new XMLHttpRequest();
-    xhr.open(opt.type.toUpperCase(), opt.url, true);
-    xhr.responseType = "blob";
-
-    // header set
-    if (opt.header && objTypeOf(opt.header) === "object") {
-      for (let key in opt.header) {
-        xhr.setRequestHeader(key, opt.header[key]);
-      }
-    }
-
-    xhr.onload = function() {
-      if (this.readyState === 4 && this.status === 200) {
-        if (this.response.size < 1024) {
-          reject("文件不存在!");
-          return;
-        }
-
-        var blob = this.response;
-        let pdfUrl = "";
-        let uRl = window.URL || window.webkitURL;
-        if (uRl && uRl.createObjectURL) {
-          pdfUrl = uRl.createObjectURL(blob);
-        } else {
-          reject("浏览器不兼容!");
-        }
-        let a = document.createElement("a");
-        a.download = opt.fileName;
-        a.href = pdfUrl;
-        document.body.appendChild(a);
-        a.click();
-        a.parentNode.removeChild(a);
-        resolve(true);
-      } else {
-        reject("请求错误!");
-      }
-    };
-
-    if (opt.type.toUpperCase() === "POST") {
-      let fromData = new FormData();
-      for (let key in opt.data) {
-        fromData.append(key, opt.data[key]);
-      }
-      xhr.send(fromData);
-    } else {
-      xhr.send();
-    }
-  });
-}
-
-/**
- * 构建图表btn
- * @param {Function} h createElement
- * @param {Array} actions 操作分类数组
- */
-export function tableAction(h, actions) {
-  return actions.map(item => {
-    let attr = {
-      props: {
-        type: item.type || "primary",
-        size: "small",
-        disabled: !!item.disabled
-      },
-      style: {
-        marginRight: "5px"
-      },
-      on: {
-        click: () => {
-          item.action();
-        }
-      }
-    };
-    return h("el-button", attr, item.name);
-  });
-}
-
-/**
- * 获取随机code,默认获取16位
- * @param {Number} len 推荐8的倍数
- *
- */
-export function randomCode(len = 16) {
-  if (len <= 0) return;
-  let steps = Math.ceil(len / 8);
-  let stepNums = [];
-  for (let i = 0; i < steps; i++) {
-    let ranNum = Math.random()
-      .toString(32)
-      .slice(-8);
-    stepNums.push(ranNum);
-  }
-
-  return stepNums.join("");
-}
-
-/**
- * 序列化参数
- * @param {Object} params 参数对象
- */
-export function qsParams(params) {
-  return Object.entries(params)
-    .map(el => `${el[0]}=${el[1]}`)
-    .join("&");
-}
-
-/**
- *
- * @param {String} format 时间格式
- * @param {Date} date 需要格式化的时间对象
- */
-export function formatDate(format = "YYYY/MM/DD HH:mm:ss", date = new Date()) {
-  if (objTypeOf(date) !== "date") return;
-  const options = {
-    "Y+": date.getFullYear(),
-    "M+": date.getMonth() + 1,
-    "D+": date.getDate(),
-    "H+": date.getHours(),
-    "m+": date.getMinutes(),
-    "s+": date.getSeconds()
-  };
-  Object.entries(options).map(([key, val]) => {
-    if (new RegExp("(" + key + ")").test(format)) {
-      const zeros = key === "Y+" ? "0000" : "00";
-      const value = (zeros + val).substr(("" + val).length);
-      format = format.replace(RegExp.$1, value);
-    }
-  });
-  return format;
-}
-
-/**
- *  获取时间长度文字
- * @param {Number} timeNumber 时间数值,单位:毫秒
- */
-export function timeNumberToText(timeNumber) {
-  const DAY_TIME = 24 * 60 * 60 * 1000;
-  const HOUR_TIME = 60 * 60 * 1000;
-  const MINUTE_TIME = 60 * 1000;
-  const SECOND_TIME = 1000;
-  let [day, hour, minute, second] = [0, 0, 0, 0];
-  let residueTime = timeNumber;
-
-  if (residueTime >= DAY_TIME) {
-    day = Math.floor(residueTime / DAY_TIME);
-    residueTime -= day * DAY_TIME;
-    day += "天";
-  }
-  if (residueTime >= HOUR_TIME) {
-    hour = Math.floor(residueTime / HOUR_TIME);
-    residueTime -= hour * HOUR_TIME;
-    hour += "小时";
-  }
-  if (residueTime >= MINUTE_TIME) {
-    minute = Math.floor(residueTime / MINUTE_TIME);
-    residueTime -= minute * MINUTE_TIME;
-    minute += "分钟";
-  }
-  if (residueTime >= SECOND_TIME) {
-    second = Math.round(residueTime / SECOND_TIME);
-    second += "秒";
-  }
-
-  return [day, hour, minute, second].filter(item => !!item).join("");
-}
-
-/**
- *  警告时间
- * @param {Number} timeNumber 时间数值,单位:毫秒
- * @param {Number} wainingTime 最大剩余警告时间数值,单位:毫秒
- */
-export function residueFloorTime(timeNumber, wainingTime = 0) {
-  if (timeNumber < 0) {
-    return { status: "danger", title: "已过期" };
-  }
-
-  const DAY_TIME = 24 * 60 * 60 * 1000;
-  const HOUR_TIME = 60 * 60 * 1000;
-  const MINUTE_TIME = 60 * 1000;
-  const status = timeNumber < wainingTime ? "warning" : "primary";
-  let [day, hour, minute] = [0, 0, 0];
-  let residueTime = timeNumber;
-
-  if (residueTime >= DAY_TIME) {
-    day = Math.floor(residueTime / DAY_TIME);
-    residueTime -= day * DAY_TIME;
-    return {
-      status,
-      title: `剩余${day}天`
-    };
-  }
-  if (residueTime >= HOUR_TIME) {
-    hour = Math.floor(residueTime / HOUR_TIME);
-    residueTime -= hour * HOUR_TIME;
-    return {
-      status,
-      title: `剩余${hour}小时`
-    };
-  }
-  if (residueTime >= MINUTE_TIME) {
-    minute = Math.floor(residueTime / MINUTE_TIME);
-    return {
-      status,
-      title: `剩余${minute}分钟`
-    };
-  }
-
-  return {
-    status,
-    title: `不足1分钟`
-  };
-}
-
-export function parseTimeRangeDateAndTime(startTime, endTime) {
-  if (!startTime || !endTime)
-    return {
-      date: "",
-      time: ""
-    };
-
-  const st = formatDate("YYYY-MM-DD HH:mm", new Date(startTime)).split(" ");
-  const et = formatDate("YYYY-MM-DD HH:mm", new Date(endTime)).split(" ");
-
-  return {
-    date: st[0],
-    time: `${st[1]}-${et[1]}`
-  };
-}
-
-/**
- * 获取本地时间,格式:年月日时分秒
- */
-export function localNowDateTime() {
-  return formatDate("YYYY年MM月DD日HH时mm分ss秒");
-}
-
-/**
- *
- * @param {Number} time 时间戳
- */
-export function getTimeDatestamp(time) {
-  const date = formatDate("YYYY-MM-DD HH:mm", new Date(time)).split(" ")[0];
-  return new Date(`${date} 00:00:00`).getTime();
-}
-
-/**
- * 获取指定元素个数的数组
- * @param {Number} num
- */
-export function getNumList(num) {
-  return "#".repeat(num).split("");
-}
-
-/**
- * 清除html标签
- * @param {String} str html字符串
- */
-export function removeHtmlTag(str) {
-  return str.replace(/<[^>]+>/g, "");
-}
-
-/**
- * 计算总数
- * @param {Array} dataList 需要统计的数组
- */
-export function calcSum(dataList) {
-  if (!dataList.length) return 0;
-  return dataList.reduce(function(total, item) {
-    return total + item;
-  }, 0);
-}
-
-/** 获取数组最大数 */
-export function maxNum(dataList) {
-  return Math.max.apply(null, dataList);
-}
-
-export function isEmptyObject(obj) {
-  return !Object.keys(obj).length;
-}
-
-/**
- * 解决后台返回的数据中number位数超过16位的情况
- * @param {String} text json格式字符串
- */
-export function jsonBigNumberToString(text) {
-  return text
-    .replace(/\\":[0-9]{16,19}/g, function(match) {
-      return match.slice(0, 3) + '\\"' + match.slice(3) + '\\"';
-    })
-    .replace(/:[0-9]{16,19}/g, function(match) {
-      return match[0] + '"' + match.slice(1) + '"';
-    });
-}
-
-export function humpToLowLine(a) {
-  return a
-    .replace(/([A-Z])/g, "-$1")
-    .toLowerCase()
-    .slice(1);
-}
-
-export function pickByNotNull(params) {
-  let nData = {};
-  Object.entries(params).forEach(([key, val]) => {
-    if (val === null || val === "null" || val === "") return;
-    nData[key] = val;
-  });
-  return nData;
-}
-
-export function autoSubmitForm(url, params) {
-  const form = document.createElement("form");
-  form.action = url;
-  form.method = "post";
-
-  Object.entries(params).forEach(([key, val]) => {
-    const input = document.createElement("input");
-    input.type = "hidden";
-    input.name = key;
-    input.value = val;
-    form.appendChild(input);
-  });
-  document.body.appendChild(form);
-  form.submit();
-}
-
-export function blobToText(blob) {
-  return new Promise((resolve, reject) => {
-    const reader = new FileReader();
-    reader.readAsText(blob, "utf-8");
-    reader.onload = function() {
-      resolve(reader.result);
-    };
-    reader.onerror = function() {
-      reject();
-    };
-  });
-}
+const deepmerge = require("deepmerge");
+
+/**
+ * 判断对象类型
+ * @param {*} obj 对象
+ */
+export function objTypeOf(obj) {
+  const toString = Object.prototype.toString;
+  const map = {
+    "[object Boolean]": "boolean",
+    "[object Number]": "number",
+    "[object String]": "string",
+    "[object Function]": "function",
+    "[object Array]": "array",
+    "[object Date]": "date",
+    "[object RegExp]": "regExp",
+    "[object Undefined]": "undefined",
+    "[object Null]": "null",
+    "[object Object]": "object",
+    "[object Blob]": "blob"
+  };
+  return map[toString.call(obj)];
+}
+
+/**
+ * 深拷贝
+ * @param {Object/Array} data 需要拷贝的数据
+ */
+export function deepCopy(data, options) {
+  const defObj = objTypeOf(data) === "array" ? [] : {};
+  return deepmerge(defObj, data, options || {});
+}
+
+/**
+ * 将目标对象中有的属性值与源对象中的属性值合并
+ * @param {Object} target 目标对象
+ * @param {Object} sources 源对象
+ */
+export function objAssign(target, sources) {
+  let targ = { ...target };
+  for (let k in targ) {
+    targ[k] = sources.hasOwnProperty(k) ? sources[k] : targ[k];
+  }
+  return targ;
+}
+
+/**
+ * 文件流下载
+ * @param {Object} option 文件下载设置
+ */
+export function download(option) {
+  let defOpt = {
+    type: "get",
+    url: "",
+    data: "",
+    fileName: "",
+    header: ""
+  };
+  let opt = objAssign(defOpt, option);
+
+  return new Promise((resolve, reject) => {
+    let xhr = new XMLHttpRequest();
+    xhr.open(opt.type.toUpperCase(), opt.url, true);
+    xhr.responseType = "blob";
+
+    // header set
+    if (opt.header && objTypeOf(opt.header) === "object") {
+      for (let key in opt.header) {
+        xhr.setRequestHeader(key, opt.header[key]);
+      }
+    }
+
+    xhr.onload = function() {
+      if (this.readyState === 4 && this.status === 200) {
+        if (this.response.size < 1024) {
+          reject("文件不存在!");
+          return;
+        }
+
+        var blob = this.response;
+        let pdfUrl = "";
+        let uRl = window.URL || window.webkitURL;
+        if (uRl && uRl.createObjectURL) {
+          pdfUrl = uRl.createObjectURL(blob);
+        } else {
+          reject("浏览器不兼容!");
+        }
+        let a = document.createElement("a");
+        a.download = opt.fileName;
+        a.href = pdfUrl;
+        document.body.appendChild(a);
+        a.click();
+        a.parentNode.removeChild(a);
+        resolve(true);
+      } else {
+        reject("请求错误!");
+      }
+    };
+
+    if (opt.type.toUpperCase() === "POST") {
+      let fromData = new FormData();
+      for (let key in opt.data) {
+        fromData.append(key, opt.data[key]);
+      }
+      xhr.send(fromData);
+    } else {
+      xhr.send();
+    }
+  });
+}
+
+/**
+ * 构建图表btn
+ * @param {Function} h createElement
+ * @param {Array} actions 操作分类数组
+ */
+export function tableAction(h, actions) {
+  return actions.map(item => {
+    let attr = {
+      props: {
+        type: item.type || "primary",
+        size: "small",
+        disabled: !!item.disabled
+      },
+      style: {
+        marginRight: "5px"
+      },
+      on: {
+        click: () => {
+          item.action();
+        }
+      }
+    };
+    return h("el-button", attr, item.name);
+  });
+}
+
+/**
+ * 获取随机code,默认获取16位
+ * @param {Number} len 推荐8的倍数
+ *
+ */
+export function randomCode(len = 16) {
+  if (len <= 0) return;
+  let steps = Math.ceil(len / 8);
+  let stepNums = [];
+  for (let i = 0; i < steps; i++) {
+    let ranNum = Math.random()
+      .toString(32)
+      .slice(-8);
+    stepNums.push(ranNum);
+  }
+
+  return stepNums.join("");
+}
+
+/**
+ * 序列化参数
+ * @param {Object} params 参数对象
+ */
+export function qsParams(params) {
+  return Object.entries(params)
+    .map(el => `${el[0]}=${el[1]}`)
+    .join("&");
+}
+
+/**
+ *
+ * @param {String} format 时间格式
+ * @param {Date} date 需要格式化的时间对象
+ */
+export function formatDate(format = "YYYY/MM/DD HH:mm:ss", date = new Date()) {
+  if (objTypeOf(date) !== "date") return;
+  const options = {
+    "Y+": date.getFullYear(),
+    "M+": date.getMonth() + 1,
+    "D+": date.getDate(),
+    "H+": date.getHours(),
+    "m+": date.getMinutes(),
+    "s+": date.getSeconds()
+  };
+  Object.entries(options).map(([key, val]) => {
+    if (new RegExp("(" + key + ")").test(format)) {
+      const zeros = key === "Y+" ? "0000" : "00";
+      const value = (zeros + val).substr(("" + val).length);
+      format = format.replace(RegExp.$1, value);
+    }
+  });
+  return format;
+}
+
+/**
+ *  获取时间长度文字
+ * @param {Number} timeNumber 时间数值,单位:毫秒
+ */
+export function timeNumberToText(timeNumber) {
+  const DAY_TIME = 24 * 60 * 60 * 1000;
+  const HOUR_TIME = 60 * 60 * 1000;
+  const MINUTE_TIME = 60 * 1000;
+  const SECOND_TIME = 1000;
+  let [day, hour, minute, second] = [0, 0, 0, 0];
+  let residueTime = timeNumber;
+
+  if (residueTime >= DAY_TIME) {
+    day = Math.floor(residueTime / DAY_TIME);
+    residueTime -= day * DAY_TIME;
+    day += "天";
+  }
+  if (residueTime >= HOUR_TIME) {
+    hour = Math.floor(residueTime / HOUR_TIME);
+    residueTime -= hour * HOUR_TIME;
+    hour += "小时";
+  }
+  if (residueTime >= MINUTE_TIME) {
+    minute = Math.floor(residueTime / MINUTE_TIME);
+    residueTime -= minute * MINUTE_TIME;
+    minute += "分钟";
+  }
+  if (residueTime >= SECOND_TIME) {
+    second = Math.round(residueTime / SECOND_TIME);
+    second += "秒";
+  }
+
+  return [day, hour, minute, second].filter(item => !!item).join("");
+}
+
+/**
+ *  警告时间
+ * @param {Number} timeNumber 时间数值,单位:毫秒
+ * @param {Number} wainingTime 最大剩余警告时间数值,单位:毫秒
+ */
+export function residueFloorTime(timeNumber, wainingTime = 0) {
+  if (timeNumber < 0) {
+    return { status: "danger", title: "已过期" };
+  }
+
+  const DAY_TIME = 24 * 60 * 60 * 1000;
+  const HOUR_TIME = 60 * 60 * 1000;
+  const MINUTE_TIME = 60 * 1000;
+  const status = timeNumber < wainingTime ? "warning" : "primary";
+  let [day, hour, minute] = [0, 0, 0];
+  let residueTime = timeNumber;
+
+  if (residueTime >= DAY_TIME) {
+    day = Math.floor(residueTime / DAY_TIME);
+    residueTime -= day * DAY_TIME;
+    return {
+      status,
+      title: `剩余${day}天`
+    };
+  }
+  if (residueTime >= HOUR_TIME) {
+    hour = Math.floor(residueTime / HOUR_TIME);
+    residueTime -= hour * HOUR_TIME;
+    return {
+      status,
+      title: `剩余${hour}小时`
+    };
+  }
+  if (residueTime >= MINUTE_TIME) {
+    minute = Math.floor(residueTime / MINUTE_TIME);
+    return {
+      status,
+      title: `剩余${minute}分钟`
+    };
+  }
+
+  return {
+    status,
+    title: `不足1分钟`
+  };
+}
+
+export function parseTimeRangeDateAndTime(startTime, endTime) {
+  if (!startTime || !endTime)
+    return {
+      date: "",
+      time: ""
+    };
+
+  const st = formatDate("YYYY-MM-DD HH:mm", new Date(startTime)).split(" ");
+  const et = formatDate("YYYY-MM-DD HH:mm", new Date(endTime)).split(" ");
+
+  return {
+    date: st[0],
+    time: `${st[1]}-${et[1]}`
+  };
+}
+
+/**
+ * 获取本地时间,格式:年月日时分秒
+ */
+export function localNowDateTime() {
+  return formatDate("YYYY年MM月DD日HH时mm分ss秒");
+}
+
+/**
+ *
+ * @param {Number} time 时间戳
+ */
+export function getTimeDatestamp(time) {
+  const date = formatDate("YYYY-MM-DD HH:mm", new Date(time)).split(" ")[0];
+  return new Date(`${date} 00:00:00`).getTime();
+}
+
+/**
+ * 获取指定元素个数的数组
+ * @param {Number} num
+ */
+export function getNumList(num) {
+  return "#".repeat(num).split("");
+}
+
+/**
+ * 清除html标签
+ * @param {String} str html字符串
+ */
+export function removeHtmlTag(str) {
+  return str.replace(/<[^>]+>/g, "");
+}
+
+/**
+ * 计算总数
+ * @param {Array} dataList 需要统计的数组
+ */
+export function calcSum(dataList) {
+  if (!dataList.length) return 0;
+  return dataList.reduce(function(total, item) {
+    return total + item;
+  }, 0);
+}
+
+/** 获取数组最大数 */
+export function maxNum(dataList) {
+  return Math.max.apply(null, dataList);
+}
+
+export function isEmptyObject(obj) {
+  return !Object.keys(obj).length;
+}
+
+/**
+ * 解决后台返回的数据中number位数超过16位的情况
+ * @param {String} text json格式字符串
+ */
+export function jsonBigNumberToString(text) {
+  return text
+    .replace(/\\":[0-9]{16,19}/g, function(match) {
+      return match.slice(0, 3) + '\\"' + match.slice(3) + '\\"';
+    })
+    .replace(/:[0-9]{16,19}/g, function(match) {
+      return match[0] + '"' + match.slice(1) + '"';
+    });
+}
+
+export function humpToLowLine(a) {
+  return a
+    .replace(/([A-Z])/g, "-$1")
+    .toLowerCase()
+    .slice(1);
+}
+
+export function pickByNotNull(params) {
+  let nData = {};
+  Object.entries(params).forEach(([key, val]) => {
+    if (val === null || val === "null" || val === "") return;
+    nData[key] = val;
+  });
+  return nData;
+}
+
+export function autoSubmitForm(url, params) {
+  const form = document.createElement("form");
+  form.action = url;
+  form.method = "post";
+
+  Object.entries(params).forEach(([key, val]) => {
+    const input = document.createElement("input");
+    input.type = "hidden";
+    input.name = key;
+    input.value = val;
+    form.appendChild(input);
+  });
+  document.body.appendChild(form);
+  form.submit();
+}
+
+export function blobToText(blob) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.readAsText(blob, "utf-8");
+    reader.onload = function() {
+      resolve(reader.result);
+    };
+    reader.onerror = function() {
+      reject();
+    };
+  });
+}
+
+export function parseHrefParam(href, paramName = null) {
+  if (!href) return;
+  const paramStr = href.split("?")[1];
+  if (!paramStr) return;
+
+  let params = {};
+  paramStr.split("&").forEach(item => {
+    const con = item.split("=");
+    params[con[0]] = con[1];
+  });
+
+  return paramName ? params[paramName] : params;
+}