Browse Source

no message

MS-CIAZDCOIXVRW\Administrator 3 years atrás
commit
a75b6f9eab
56 changed files with 26141 additions and 0 deletions
  1. 4 0
      .browserslistrc
  2. 5 0
      .editorconfig
  3. 5 0
      .env.development
  4. 5 0
      .env.production
  5. 16 0
      .eslintrc.js
  6. 23 0
      .gitignore
  7. 5 0
      .prettierrc
  8. 5 0
      babel.config.js
  9. 19 0
      jsconfig.json
  10. 3 0
      lint-staged.config.js
  11. 23671 0
      package-lock.json
  12. 44 0
      package.json
  13. BIN
      public/favicon.ico
  14. 17 0
      public/index.html
  15. 7 0
      src/App.vue
  16. 19 0
      src/api/project.js
  17. 10 0
      src/api/sys.js
  18. 46 0
      src/api/user.js
  19. 1 0
      src/assets/clothes.svg
  20. 1 0
      src/assets/language.svg
  21. BIN
      src/assets/login-bg.png
  22. BIN
      src/assets/login-group.png
  23. BIN
      src/assets/login-office.png
  24. BIN
      src/assets/login-people.png
  25. BIN
      src/assets/logo.png
  26. BIN
      src/assets/sidebar-logo.png
  27. 20 0
      src/constant/index.js
  28. 22 0
      src/layout/components/AppMain.vue
  29. 201 0
      src/layout/components/Navbar.vue
  30. 22 0
      src/layout/components/Sidebar/MenuItem.vue
  31. 33 0
      src/layout/components/Sidebar/SidebarItem.vue
  32. 48 0
      src/layout/components/Sidebar/SidebarMenu.vue
  33. 29 0
      src/layout/components/Sidebar/index.vue
  34. 49 0
      src/layout/index.vue
  35. 31 0
      src/main.js
  36. 24 0
      src/permission.js
  37. 59 0
      src/router/index.js
  38. 10 0
      src/store/getters.js
  39. 10 0
      src/store/index.js
  40. 61 0
      src/store/modules/user.js
  41. 3 0
      src/styles/element.scss
  42. 57 0
      src/styles/index.scss
  43. 28 0
      src/styles/mixin.scss
  44. 181 0
      src/styles/sidebar.scss
  45. 30 0
      src/styles/transition.scss
  46. 27 0
      src/styles/variables.module.scss
  47. 21 0
      src/utils/auth.js
  48. 15 0
      src/utils/jsencrypt.js
  49. 61 0
      src/utils/request.js
  50. 72 0
      src/utils/route.js
  51. 28 0
      src/utils/storage.js
  52. 480 0
      src/views/account-management/index.vue
  53. 212 0
      src/views/login/index.vue
  54. 9 0
      src/views/login/rules.js
  55. 370 0
      src/views/project-management/index.vue
  56. 22 0
      vue.config.js

+ 4 - 0
.browserslistrc

@@ -0,0 +1,4 @@
+> 1%
+last 2 versions
+not dead
+not ie 11

+ 5 - 0
.editorconfig

@@ -0,0 +1,5 @@
+[*.{js,jsx,ts,tsx,vue}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/reporting'

+ 5 - 0
.env.production

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'

+ 16 - 0
.eslintrc.js

@@ -0,0 +1,16 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: ['plugin:vue/vue3-essential', '@vue/standard'],
+  parserOptions: {
+    parser: '@babel/eslint-parser'
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'space-before-function-paren': 0,
+    'vue/multi-word-component-names': 'off'
+  }
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
.prettierrc

@@ -0,0 +1,5 @@
+{
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "none"
+}

+ 5 - 0
babel.config.js

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

+ 19 - 0
jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

+ 3 - 0
lint-staged.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  '*.{js,jsx,vue}': 'vue-cli-service lint'
+}

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


+ 44 - 0
package.json

@@ -0,0 +1,44 @@
+{
+  "name": "test_admin",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.0.10",
+    "axios": "^1.3.4",
+    "core-js": "^3.8.3",
+    "css-color-function": "^1.3.3",
+    "element-plus": "^2.2.32",
+    "fuse.js": "^6.6.2",
+    "jsencrypt": "^3.3.1",
+    "node-polyfill-webpack-plugin": "^2.0.1",
+    "vue": "^3.2.13",
+    "vue-router": "^4.0.3",
+    "vuex": "^4.0.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-plugin-router": "~5.0.0",
+    "@vue/cli-plugin-vuex": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "@vue/eslint-config-standard": "^6.1.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-import": "^2.25.3",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^5.1.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "lint-staged": "^11.1.2",
+    "sass": "^1.32.7",
+    "sass-loader": "^12.0.0"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  }
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>代理商登记管理系统</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but Agent registration management system doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 7 - 0
src/App.vue

@@ -0,0 +1,7 @@
+<template>
+  <router-view />
+</template>
+
+<script setup></script>
+
+<style lang="scss"></style>

+ 19 - 0
src/api/project.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// 获取项目管理列表
+export const getProjectList = (params) => {
+  return request({
+    url: '/informationReporting/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除项目管理列表
+export const deleteProjectList = (data) => {
+  return request({
+    url: '/informationReporting/delete',
+    method: 'delete',
+    data
+  })
+}

+ 10 - 0
src/api/sys.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 登录接口
+export const login = (data) => {
+  return request({
+    url: '/user/login',
+    method: 'post',
+    data
+  })
+}

+ 46 - 0
src/api/user.js

@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+// 获取账号管理列表
+export const getUserList = (params) => {
+  return request({
+    url: '/user/list',
+    method: 'get',
+    params
+  })
+}
+
+// 新增账号
+export const addUserList = (data) => {
+  return request({
+    url: '/user/add',
+    method: 'post',
+    data
+  })
+}
+
+// 编辑账号
+export const editUserList = (data) => {
+  return request({
+    url: '/user/update',
+    method: 'put',
+    data
+  })
+}
+
+// 修改当前用户密码
+export const editPassword = (data) => {
+  return request({
+    url: '/user/updatePassword',
+    method: 'put',
+    data
+  })
+}
+
+// 删除管理员
+export const deleteUserList = (data) => {
+  return request({
+    url: '/user/delete',
+    method: 'delete',
+    data
+  })
+}

File diff suppressed because it is too large
+ 1 - 0
src/assets/clothes.svg


File diff suppressed because it is too large
+ 1 - 0
src/assets/language.svg


BIN
src/assets/login-bg.png


BIN
src/assets/login-group.png


BIN
src/assets/login-office.png


BIN
src/assets/login-people.png


BIN
src/assets/logo.png


BIN
src/assets/sidebar-logo.png


+ 20 - 0
src/constant/index.js

@@ -0,0 +1,20 @@
+// token
+export const TOKEN = 'token'
+
+// // 用户姓名
+// export const realName = 'realName'
+
+// // 用户权限
+// export const AdminType = 'adminType'
+
+// // 用户id
+// export const AdminId = 'adminType'
+
+// 用户信息
+export const USERINFO = 'token'
+
+// token 时间戳
+export const TIME_STAMP = 'timeStamp'
+
+// 超时时长(毫秒) 两小时
+export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000

+ 22 - 0
src/layout/components/AppMain.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="app-main">
+    <router-view v-slot="{ Component }">
+      <transition name="fade-transform" mode="out-in">
+        <component :is="Component" />
+      </transition>
+    </router-view>
+  </div>
+</template>
+
+<script setup></script>
+
+<style lang="scss" scoped>
+.app-main {
+  min-height: calc(100vh - 50px - 43px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+  padding: 104px 20px 20px 20px;
+  box-sizing: border-box;
+}
+</style>

+ 201 - 0
src/layout/components/Navbar.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="navbar">
+    <!-- 左上角区域 -->
+    <div class="navbar-title">代理商登记管理系统</div>
+    <!-- 右上角区域 -->
+    <div class="right-menu">
+      <!-- 头像 -->
+      <el-dropdown class="avatar-container" trigger="click">
+        <div class="avatar-wrapper">
+          <el-avatar>{{ $store.getters.userInfo.name }}</el-avatar>
+          {{ $store.getters.userInfo.name }}
+        </div>
+        <template #dropdown>
+          <el-dropdown-menu class="user-dropdown">
+            <el-dropdown-item @click="handleEditPassword">
+              <el-icon><Lock /></el-icon>
+              修改密码
+            </el-dropdown-item>
+            <el-dropdown-item divided @click="logout">
+              <el-icon><SwitchButton /></el-icon>
+              退出登录
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+
+    <!-- 修改密码弹窗区域 -->
+    <el-dialog
+      v-model="dialogVisible"
+      title="修改密码"
+      width="25%"
+      top="20vh"
+      :close-on-click-modal="false"
+    >
+      <div class="dialog_box">
+        <div class="dialog_item">
+          <span>原密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请输入原密码"
+            v-model="password"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>新密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请输入新密码"
+            v-model="newPassword"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>确认密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请再次输入新密码"
+            v-model="newPassword2"
+          ></el-input>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleEditPasswordConfirm">
+            确定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { useStore } from 'vuex'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { RSAencrypt } from '@/utils/jsencrypt'
+import { editPassword } from '@/api/user'
+
+const dialogVisible = ref(false)
+
+const password = ref('')
+const newPassword = ref('')
+const newPassword2 = ref('')
+
+const store = useStore()
+const logout = () => {
+  store.dispatch('user/logout')
+}
+
+const handleEditPassword = () => {
+  dialogVisible.value = true
+}
+
+const handleEditPasswordConfirm = () => {
+  if (password.value === '') {
+    ElMessage.error('请输入原密码')
+    return
+  }
+  if (newPassword.value === '') {
+    ElMessage.error('请输入新密码')
+    return
+  }
+  if (newPassword2.value === '') {
+    ElMessage.error('请再次输入新密码')
+    return
+  }
+  if (newPassword.value !== newPassword2.value) {
+    ElMessage.error('两次输入的密码不一致')
+    return
+  }
+  ElMessageBox.confirm('确定修改密码吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const res = await editPassword({
+        id: store.getters.userInfo.id,
+        password: RSAencrypt(password.value),
+        newPassword: RSAencrypt(newPassword.value)
+      })
+      console.log(res)
+      if (res == null) {
+        dialogVisible.value = false
+        ElMessage.success('修改成功')
+        store.dispatch('user/logout')
+      }
+    })
+    .catch(() => {
+      ElMessage.info('已取消')
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  height: 80px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  .navbar-title {
+    float: left;
+    margin-left: 23px;
+    line-height: 80px;
+    font-size: 30px;
+    font-weight: bold;
+  }
+
+  .right-menu {
+    float: right;
+    padding-right: 46px;
+
+    :deep .avatar-container {
+      line-height: 80px;
+      cursor: pointer;
+      .avatar-wrapper {
+        position: relative;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        .el-avatar {
+          --el-avatar-background-color: none;
+          margin-right: 12px;
+        }
+      }
+    }
+  }
+
+  .dialog_box {
+    height: 150px;
+    .dialog_item {
+      height: 30px;
+      margin-bottom: 20px;
+      span {
+        display: inline-block;
+        width: 70px;
+      }
+    }
+  }
+}
+
+::v-deep .el-dialog__header {
+  margin: 0;
+  font-weight: bold;
+  background-color: #edf1f5;
+}
+
+::v-deep .el-button--primary {
+  background-color: #0061ff;
+}
+</style>

+ 22 - 0
src/layout/components/Sidebar/MenuItem.vue

@@ -0,0 +1,22 @@
+<template>
+  <el-icon>
+    <component class="fold-menu" :is="icon"></component>
+  </el-icon>
+  <span>{{ title }}</span>
+</template>
+
+<script setup>
+import { defineProps } from 'vue'
+defineProps({
+  title: {
+    type: String,
+    required: true
+  },
+  icon: {
+    type: String,
+    required: true
+  }
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 33 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,33 @@
+<template>
+  <!-- 支持渲染多级 menu 菜单 -->
+  <el-sub-menu v-if="route.children.length > 0" :index="route.path">
+    <template #title>
+      <menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
+    </template>
+    <!-- 循环渲染 -->
+    <sidebar-item
+      v-for="item in route.children"
+      :key="item.path"
+      :route="item"
+    ></sidebar-item>
+  </el-sub-menu>
+  <!-- 渲染 item 项 -->
+  <el-menu-item v-else :index="route.path">
+    <menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
+  </el-menu-item>
+</template>
+
+<script setup>
+import MenuItem from './MenuItem'
+import { defineProps } from 'vue'
+
+// 定义 props
+defineProps({
+  route: {
+    type: Object,
+    required: true
+  }
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 48 - 0
src/layout/components/Sidebar/SidebarMenu.vue

@@ -0,0 +1,48 @@
+<template>
+  <!-- 一级 menu 菜单 -->
+  <el-menu
+    background-color="#1E7DFB"
+    text-color="#fff"
+    active-text-color="#000000"
+    :default-active="activeMenu"
+    unique-opened
+    router
+  >
+    <template v-for="item in routes" :key="item.path">
+      <sidebar-item
+        :route="item"
+        v-if="item.meta.adminType >= $store.getters.userInfo.adminType"
+      ></sidebar-item>
+    </template>
+  </el-menu>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import SidebarItem from './SidebarItem'
+import { useRouter, useRoute } from 'vue-router'
+import { filterRouters, generateMenus } from '@/utils/route'
+
+// // 计算路由表结构
+const router = useRouter()
+const routes = computed(() => {
+  const filterRoutes = filterRouters(router.getRoutes())
+  return generateMenus(filterRoutes)
+})
+
+// // 计算高亮 menu 的方法
+const route = useRoute()
+const activeMenu = computed(() => {
+  const { meta, path } = route
+  if (meta.activeMenu) {
+    return meta.activeMenu
+  }
+  return path
+})
+</script>
+
+<style lang="scss" scoped>
+::v-deep .is-active {
+  background-color: #fff;
+}
+</style>

+ 29 - 0
src/layout/components/Sidebar/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div>
+    <!-- logo区域 -->
+    <div class="logo-container">
+      <img src="../../../assets/sidebar-logo.png" />
+    </div>
+    <!-- Menu 菜单区域 -->
+    <el-scrollbar>
+      <SidebarMenu />
+    </el-scrollbar>
+  </div>
+</template>
+
+<script setup>
+import SidebarMenu from './SidebarMenu.vue'
+</script>
+
+<style lang="scss" scoped>
+.logo-container {
+  height: 62px;
+  padding: 22px 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  img {
+    width: 100px;
+  }
+}
+</style>

+ 49 - 0
src/layout/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="app_wrapper">
+    <!-- 左侧 menu -->
+    <Sidebar id="guide-sidebar" class="sidebar-container" />
+    <!-- 右侧 -->
+    <div class="main-container">
+      <!-- 顶部的 navbar -->
+      <div class="fixed_header">
+        <Navbar />
+      </div>
+      <!-- 内容区 -->
+      <AppMain />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import Sidebar from './components/Sidebar/index.vue'
+import AppMain from './components/AppMain.vue'
+import Navbar from './components/Navbar.vue'
+</script>
+
+<style lang="scss" scoped>
+@import '~@/styles/mixin.scss';
+@import '~@/styles/variables.module.scss';
+.app_wrapper {
+  @include clearfix;
+  position: relative;
+  height: 100%;
+  width: 100%;
+}
+
+.fixed_header {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 9;
+  width: calc(100% - #{$sideBarWidth});
+  transition: width #{$sideBarDuration};
+}
+
+.hideSidebar .fixed_header {
+  width: calc(100% - #{$hideSideBarWidth});
+}
+
+.sidebar-container {
+  background-color: #1e7dfb;
+}
+</style>

+ 31 - 0
src/main.js

@@ -0,0 +1,31 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+// 导入权限控制模块
+// import './permission'
+
+// 引入初始化样式
+import '@/styles/index.scss'
+
+// 引入vue3 element 组件 和样式
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+
+// 引入vue3 element Icon图标
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+const app = createApp(App)
+
+// 全局注册element Icon图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app
+  .use(store)
+  .use(router)
+  .use(ElementPlus, {
+    locale: zhCn
+  })
+  .mount('#app')

+ 24 - 0
src/permission.js

@@ -0,0 +1,24 @@
+import router from './router'
+import store from './store'
+
+// 白名单
+const whiteList = ['/login']
+
+// 路由前置守卫
+router.beforeEach(async (to, from, next) => {
+  // 存在 token ,进入主页
+  if (store.getters.token) {
+    if (to.path === '/login') {
+      next('/')
+    } else {
+      next()
+    }
+  } else {
+    // 没有token的情况下,可以进入白名单
+    if (whiteList.indexOf(to.path) > -1) {
+      next()
+    } else {
+      next('/login')
+    }
+  }
+})

+ 59 - 0
src/router/index.js

@@ -0,0 +1,59 @@
+import {
+  createRouter,
+  createWebHistory,
+  createWebHashHistory
+} from 'vue-router'
+import layout from '@/layout'
+
+// 公开路由表
+const publicRoutes = [
+  {
+    path: '/login',
+    component: () =>
+      import(/* webpackChunkName: "login" */ '@/views/login/index.vue')
+  },
+  {
+    path: '/',
+    redirect: '/projectManagement',
+    component: layout,
+    children: [
+      {
+        path: '/projectManagement',
+        name: 'projectManagement',
+        component: () =>
+          import(
+            /* webpackChunkName: "projectManagement" */ '@/views/project-management/index'
+          ),
+        meta: {
+          title: '项目管理',
+          icon: 'Tickets',
+          adminType: 2
+        }
+      },
+      {
+        path: '/accountManagement',
+        name: 'accountManagement',
+        component: () =>
+          import(
+            /* webpackChunkName: "accountManagement" */ '@/views/account-management/index'
+          ),
+        meta: {
+          title: '账号管理',
+          icon: 'Avatar',
+          adminType: 1
+        }
+      }
+    ]
+  }
+]
+
+const router = createRouter({
+  // history: createWebHashHistory(),
+  history:
+    process.env.NODE_ENV === 'production'
+      ? createWebHistory()
+      : createWebHashHistory(),
+  routes: [...publicRoutes]
+})
+
+export default router

+ 10 - 0
src/store/getters.js

@@ -0,0 +1,10 @@
+const getters = {
+  token: (state) => state.user.token,
+  userInfo: (state) => state.user.userInfo,
+  // name: (state) => state.user.name,
+  // true 表示已存在用户信息
+  hasUserInfo: (state) => {
+    return JSON.stringify(state.user.userInfo) !== '{}'
+  }
+}
+export default getters

+ 10 - 0
src/store/index.js

@@ -0,0 +1,10 @@
+import { createStore } from 'vuex'
+import user from './modules/user'
+import getters from './getters'
+
+export default createStore({
+  getters,
+  modules: {
+    user
+  }
+})

+ 61 - 0
src/store/modules/user.js

@@ -0,0 +1,61 @@
+import { login } from '@/api/sys'
+import { setItem, getItem, removeAllItem } from '@/utils/storage'
+import { TOKEN, USERINFO } from '@/constant/index'
+import router from '@/router/index'
+import { RSAencrypt } from '@/utils/jsencrypt'
+import { setTimeStamp } from '@/utils/auth'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    token: getItem(TOKEN) || '',
+    userInfo: getItem(USERINFO) || {
+      name: '',
+      adminType: '',
+      id: ''
+    }
+  }),
+  mutations: {
+    setToken(state, token) {
+      state.token = token
+      setItem(TOKEN, token)
+    },
+    setInfo(state, data) {
+      state.userInfo.name = data.name
+      state.userInfo.adminType = data.adminType
+      state.userInfo.id = data.id
+      setItem(USERINFO, state.userInfo)
+    }
+  },
+  actions: {
+    // 登录请求
+    login(context, userInfo) {
+      const { userName, password } = userInfo
+      return new Promise((resolve, reject) => {
+        login({
+          userName,
+          password: RSAencrypt(password)
+        })
+          .then((data) => {
+            console.log(data)
+            this.commit('user/setToken', data.token)
+            this.commit('user/setInfo', data)
+            router.push('/')
+            // // 保存登录时间
+            setTimeStamp()
+          })
+          .catch((err) => {
+            reject(err)
+          })
+      })
+    },
+
+    // 退出登录
+    logout() {
+      this.commit('user/setToken', '')
+      this.commit('user/setInfo', {})
+      removeAllItem()
+      router.push('/login')
+    }
+  }
+}

+ 3 - 0
src/styles/element.scss

@@ -0,0 +1,3 @@
+.el-avatar {
+  --el-avatar-background-color: none !important;
+}

+ 57 - 0
src/styles/index.scss

@@ -0,0 +1,57 @@
+@import './variables.module.scss';
+@import './mixin.scss';
+@import './sidebar.scss';
+@import './element.scss';
+@import './transition.scss';
+
+html,
+body {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
+    Microsoft YaHei, Arial, sans-serif;
+}
+
+#app {
+  height: 100%;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+  margin: 0;
+  padding: 0;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: ' ';
+    clear: both;
+    height: 0;
+  }
+}

+ 28 - 0
src/styles/mixin.scss

@@ -0,0 +1,28 @@
+@mixin clearfix {
+  &:after {
+    content: '';
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}

+ 181 - 0
src/styles/sidebar.scss

@@ -0,0 +1,181 @@
+#app {
+  .main-container {
+    min-height: 100%;
+    transition: margin-left #{$sideBarDuration};
+    margin-left: $sideBarWidth;
+    position: relative;
+  }
+
+  .sidebar-container {
+    transition: width #{$sideBarDuration};
+    width: $sideBarWidth !important;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1001;
+    overflow: hidden;
+
+    // 重置 element-plus 的css
+    .horizontal-collapse-transition {
+      transition: 0s width ease-in-out, 0s padding-left ease-in-out,
+        0s padding-right ease-in-out;
+    }
+
+    .scrollbar-wrapper {
+      overflow-x: hidden !important;
+    }
+
+    .el-scrollbar__bar.is-vertical {
+      right: 0px;
+    }
+
+    .el-scrollbar {
+      height: 100%;
+    }
+
+    &.has-logo {
+      .el-scrollbar {
+        height: calc(100% - 50px);
+      }
+    }
+
+    .is-horizontal {
+      display: none;
+    }
+
+    a {
+      display: inline-block;
+      width: 100%;
+      overflow: hidden;
+    }
+
+    .svg-icon {
+      margin-right: 16px;
+    }
+
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+
+    .el-menu {
+      border: none;
+      height: 100%;
+      width: 100% !important;
+    }
+
+    .is-active > .el-submenu__title {
+      color: $subMenuActiveText !important;
+    }
+
+    & .nest-menu .el-submenu > .el-submenu__title,
+    & .el-submenu .el-menu-item {
+      min-width: $sideBarWidth !important;
+    }
+  }
+
+  .hideSidebar {
+    .sidebar-container {
+      width: 54px !important;
+    }
+
+    .main-container {
+      margin-left: 54px;
+    }
+
+    .submenu-title-noDropdown {
+      padding: 0 !important;
+      position: relative;
+
+      .el-tooltip {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+      }
+    }
+
+    .el-submenu {
+      overflow: hidden;
+
+      & > .el-submenu__title {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+      }
+    }
+
+    .el-menu--collapse {
+      .el-submenu {
+        & > .el-submenu__title {
+          & > span {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
+        }
+      }
+    }
+  }
+
+  .el-menu--collapse .el-menu .el-submenu {
+    min-width: $sideBarWidth !important;
+  }
+
+  .withoutAnimation {
+    .main-container,
+    .sidebar-container {
+      transition: none;
+    }
+  }
+}
+
+.el-menu--vertical {
+  & > .el-menu {
+    .svg-icon {
+      margin-right: 16px;
+    }
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+  }
+
+  // 菜单项过长时
+  > .el-menu--popup {
+    max-height: 100vh;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar-track-piece {
+      background: #d3dce6;
+    }
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #99a9bf;
+      border-radius: 20px;
+    }
+  }
+}

+ 30 - 0
src/styles/transition.scss

@@ -0,0 +1,30 @@
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all 0.5s;
+}
+
+.breadcrumb-enter-from,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all 0.5s;
+}
+
+.fade-transform-enter-from {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}

+ 27 - 0
src/styles/variables.module.scss

@@ -0,0 +1,27 @@
+// sidebar
+$menuText: #bfcbd9;
+$menuActiveText: #409eff;
+$subMenuActiveText: #f4f4f5;
+
+$menuBg: #304156;
+$menuHover: #263445;
+
+$subMenuBg: #1f2d3d;
+$subMenuHover: #001528;
+
+$sideBarWidth: 210px;
+$hideSideBarWidth: 54px;
+$sideBarDuration: 0.28s;
+
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+// JS 与 scss 共享变量,在 scss 中通过 :export 进行导出,在 js 中可通过 ESM 进行导入
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 21 - 0
src/utils/auth.js

@@ -0,0 +1,21 @@
+import { TIME_STAMP, TOKEN_TIMEOUT_VALUE } from '@/constant'
+import { setItem, getItem } from '@/utils/storage'
+
+// 获取本地缓存时间戳
+export function getTimeStamp() {
+  return getItem(TIME_STAMP)
+}
+
+// 保存当前登录时间戳
+export function setTimeStamp() {
+  setItem(TIME_STAMP, Date.now())
+}
+
+// 是否超时 true为超时
+export function isCheckTimeout() {
+  // 获取当前时间戳
+  const currentTime = Date.now()
+  // 获取缓存时间戳
+  const timeStamp = getTimeStamp()
+  return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE
+}

+ 15 - 0
src/utils/jsencrypt.js

@@ -0,0 +1,15 @@
+import { JSEncrypt } from 'jsencrypt'
+const publicKey =
+  'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMOcPB06u5yKyQsPjfVWiWgbEIrd14kiXNNihciaVKb6HnkQvq7zpQuZ80WEX94spnUMI3iOAl/GmIvHrpGwcbB4hJbznm+PajiwnUSPuCCXA68YJF640cJKb/8KeM7WVz69OFkIEPHhVxOy4FFF5QWe/kt6zOZ19HmE+ak+5x/QIDAQAB'
+
+// 加密方法
+export function RSAencrypt(pas) {
+  // 实例化jsEncrypt对象
+  const jse = new JSEncrypt()
+  // 设置公钥
+  jse.setPublicKey(publicKey)
+  // 加密后的密码
+  const res = jse.encrypt(pas)
+  // console.log('加密:' + res)
+  return res
+}

+ 61 - 0
src/utils/request.js

@@ -0,0 +1,61 @@
+import axios from 'axios'
+import store from '@/store'
+import { ElMessage } from 'element-plus'
+// import { isCheckTimeout } from '@/utils/auth'
+
+const service = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API,
+  timeout: 5000
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+  (config) => {
+    // 在这个位置需要统一的去注入token
+    if (store.getters.token) {
+      // // 判断token是否超时
+      // if (isCheckTimeout()) {
+      //   // 登出操作
+      //   store.dispatch('user/logout')
+      //   return Promise.reject(new Error('token 失效'))
+      // }
+
+      // 如果token存在 注入token
+      config.headers.admin_token = store.getters.token
+    }
+    return config // 必须返回配置
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+
+// // 响应拦截器
+service.interceptors.response.use(
+  (response) => {
+    const { success, message, data } = response.data
+    //   要根据success的成功与否决定下面的操作
+    if (success) {
+      return data
+    } else {
+      // 业务错误
+      ElMessage.error(message) // 提示错误消息
+      return Promise.reject(new Error(message))
+    }
+  },
+  (error) => {
+    // 处理 token 超时问题
+    if (
+      error.response &&
+      error.response.data &&
+      error.response.data.code === 401
+    ) {
+      // token超时
+      store.dispatch('user/logout')
+    }
+    ElMessage.error(error.message) // 提示错误信息
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 72 - 0
src/utils/route.js

@@ -0,0 +1,72 @@
+import path from 'path'
+
+// 返回所有子路由
+const getChildrenRoutes = (routes) => {
+  const result = []
+  routes.forEach((route) => {
+    if (route.children && route.children.length > 0) {
+      result.push(...route.children)
+    }
+  })
+  return result
+}
+
+/**
+ * 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级
+ * @param {*} routes router.getRoutes()
+ */
+export const filterRouters = (routes) => {
+  const childrenRoutes = getChildrenRoutes(routes)
+  // 把重复的路由过滤
+  return routes.filter((route) => {
+    return !childrenRoutes.find((childrenRoute) => {
+      return childrenRoute.path === route.path
+    })
+  })
+}
+
+// 判断数据是否为空值
+function isNull(data) {
+  if (!data) return true
+  if (JSON.stringify(data) === '{}') return true
+  if (JSON.stringify(data) === '[]') return true
+  return false
+}
+
+// 根据 routes 数据,返回对应 menu 规则数组
+export function generateMenus(routes, basePath = '') {
+  const result = []
+  // 遍历路由表
+  routes.forEach((item) => {
+    // 不存在 children && 不存在 meta 直接 return
+    if (isNull(item.meta) && isNull(item.children)) return
+    // 存在 children 不存在 meta,进入迭代
+    if (isNull(item.meta) && !isNull(item.children)) {
+      result.push(...generateMenus(item.children))
+      return
+    }
+    // 合并 path 作为跳转路径
+    const routePath = path.resolve(basePath, item.path)
+    // 路由分离之后,存在同名父路由的情况,需要单独处理
+    let route = result.find((item) => item.path === routePath)
+    if (!route) {
+      route = {
+        ...item,
+        path: routePath,
+        children: []
+      }
+
+      // icon 与 title 必须全部存在
+      if (route.meta.icon && route.meta.title) {
+        // meta 存在生成 route 对象,放入 arr
+        result.push(route)
+      }
+    }
+
+    // 存在 children 进入迭代到children
+    if (item.children) {
+      route.children.push(...generateMenus(item.children, route.path))
+    }
+  })
+  return result
+}

+ 28 - 0
src/utils/storage.js

@@ -0,0 +1,28 @@
+// 存储数据
+export const setItem = (key, value) => {
+  // 将数组、对象类型的数据转化为 JSON 字符串进行存储
+  if (typeof value === 'object') {
+    value = JSON.stringify(value)
+  }
+  window.localStorage.setItem(key, value)
+}
+
+// 获取数据
+export const getItem = key => {
+  const data = window.localStorage.getItem(key)
+  try {
+    return JSON.parse(data)
+  } catch (err) {
+    return data
+  }
+}
+
+// 删除某一条数据
+export const removeItem = key => {
+  window.localStorage.removeItem(key)
+}
+
+// 删除全部数据
+export const removeAllItem = () => {
+  window.localStorage.clear()
+}

+ 480 - 0
src/views/account-management/index.vue

@@ -0,0 +1,480 @@
+<template>
+  <div>
+    <el-card class="content">
+      <!-- 标题区域 -->
+      <el-row class="card_title">账号管理</el-row>
+
+      <!-- 筛选区域 -->
+      <el-row :gutter="35" class="card_search">
+        <el-col :span="5">
+          账号:
+          <el-input
+            style="width: 180px"
+            placeholder="请输入账号"
+            clearable
+            v-model="searchAccount"
+          ></el-input>
+        </el-col>
+
+        <el-col :span="5">
+          姓名:
+          <el-input
+            style="width: 180px"
+            placeholder="请输入姓名"
+            clearable
+            v-model="searchName"
+          ></el-input>
+        </el-col>
+
+        <el-col :span="5">
+          创建时间:
+          <el-date-picker
+            type="date"
+            placeholder="请选择创建时间"
+            value-format="YYYY-MM-DD"
+            v-model="searchTime"
+          />
+        </el-col>
+
+        <el-col :span="4">
+          <el-button type="primary" @click="handleSearch">
+            <el-icon><Search /></el-icon>
+            <span> 查询</span>
+          </el-button>
+        </el-col>
+      </el-row>
+
+      <!-- 新增账号按钮区域 -->
+      <el-row class="card_button">
+        <el-button type="primary" @click="handleAdd">
+          <el-icon><Plus /></el-icon>
+          <span> 新增账号</span>
+        </el-button>
+      </el-row>
+
+      <!-- 表格区域 -->
+      <el-table
+        stripe
+        height="481"
+        :data="tableData"
+        style="width: 100%"
+        :header-cell-style="{ backgroundColor: '#F0F3F7' }"
+      >
+        <el-table-column type="index" width="55" align="center" label="序号" />
+        <el-table-column
+          width="200"
+          align="center"
+          prop="userName"
+          label="账号"
+        />
+        <el-table-column
+          width="200"
+          align="center"
+          show-overflow-tooltip
+          prop="name"
+          label="姓名"
+        />
+        <el-table-column show-overflow-tooltip align="center" label="角色">
+          <template #default="{ row }">
+            {{ row.adminType == 1 ? '超级管理员' : '管理员' }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" prop="time" label="创建时间" />
+        <el-table-column width="200" align="center" label="操作">
+          <template #default="{ row }">
+            <el-button type="success" plain @click="handleEdit(row)"
+              >编辑
+            </el-button>
+            <el-button
+              v-if="row.adminType == 2"
+              type="danger"
+              plain
+              @click="handleDelete(row.id)"
+              >删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页器区域 -->
+      <el-row class="card_pagination">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[8, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </el-row>
+    </el-card>
+
+    <!-- 编辑弹窗区域 -->
+    <el-dialog
+      v-model="dialogVisible"
+      title="编辑账号"
+      width="25%"
+      top="20vh"
+      :close-on-click-modal="false"
+    >
+      <div class="dialog_box">
+        <div class="dialog_item">
+          <span>账号:</span>
+          <el-input
+            style="width: 180px"
+            placeholder="请输入账号"
+            clearable
+            v-model="editInfo.userName"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>姓名:</span>
+          <el-input
+            style="width: 180px"
+            placeholder="请输入姓名"
+            clearable
+            v-model="editInfo.name"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请输入密码"
+            clearable
+            v-model="editInfo.newPassword"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>确认密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请再次输入新密码"
+            clearable
+            v-model="editInfo.newPassword2"
+          ></el-input>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleEditConfirm">
+            确定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 新增账号弹窗区域 -->
+    <el-dialog
+      v-model="dialogVisibleAdd"
+      title="新增账号"
+      width="25%"
+      top="20vh"
+      :close-on-click-modal="false"
+    >
+      <div class="dialog_box">
+        <div class="dialog_item">
+          <span>账号:</span>
+          <el-input
+            style="width: 180px"
+            placeholder="请输入账号"
+            clearable
+            v-model="addInfo.userName"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>姓名:</span>
+          <el-input
+            style="width: 180px"
+            placeholder="请输入姓名"
+            clearable
+            v-model="addInfo.name"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请输入密码"
+            clearable
+            v-model="addInfo.newPassword"
+          ></el-input>
+        </div>
+        <div class="dialog_item">
+          <span>确认密码:</span>
+          <el-input
+            type="password"
+            :show-password="true"
+            style="width: 180px"
+            placeholder="请再次输入新密码"
+            clearable
+            v-model="addInfo.newPassword2"
+          ></el-input>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisibleAdd = false">取消</el-button>
+          <el-button type="primary" @click="handleEditConfirmAdd">
+            确定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  getUserList,
+  editUserList,
+  addUserList,
+  deleteUserList
+} from '@/api/user'
+import { RSAencrypt } from '@/utils/jsencrypt'
+
+const editInfo = ref({
+  userName: '',
+  name: '',
+  newPassword: '',
+  newPassword2: ''
+})
+
+const addInfo = ref({
+  userName: '',
+  name: '',
+  newPassword: '',
+  newPassword2: ''
+})
+
+const handleDelete = (id) => {
+  ElMessageBox.confirm('确定删除吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const res = await deleteUserList({
+        ids: [id]
+      })
+      // console.log(res)
+      if (res == null) {
+        ElMessage.success('删除成功')
+        getData()
+      }
+    })
+    .catch(() => {
+      ElMessage.info('已取消')
+    })
+}
+
+// 获取账号管理列表
+const getData = async () => {
+  const res = await getUserList({
+    userName: searchAccount.value,
+    name: searchName.value,
+    time: searchTime.value,
+    currPage: currentPage.value,
+    pageSize: pageSize.value
+  })
+  // console.log(res)
+  tableData.value = res.list
+  total.value = res.totalCount
+  currentPage.value = res.currPage
+}
+
+onMounted(() => {
+  getData()
+})
+
+// 筛选区域数据
+const searchAccount = ref('')
+const searchName = ref('')
+const searchTime = ref('')
+
+// 总条数
+const total = ref(0)
+// 当前页
+const currentPage = ref(1)
+// 每页多少条
+const pageSize = ref(8)
+
+// 编辑弹窗显示隐藏控制
+const dialogVisible = ref(false)
+
+const dialogVisibleAdd = ref(false)
+
+// 查询按钮回调
+const handleSearch = () => {
+  getData()
+}
+
+// 新增按钮回调
+const handleAdd = () => {
+  dialogVisibleAdd.value = true
+  addInfo.value = {
+    userName: '',
+    name: '',
+    newPassword: '',
+    newPassword2: ''
+  }
+}
+
+// 每页条数改变时的回调
+const handleSizeChange = (val) => {
+  // console.log(`${val} items per page`)
+  pageSize.value = val
+}
+// 当前页改变时的回调
+const handleCurrentChange = (val) => {
+  // console.log(`current page: ${val}`)
+  currentPage.value = val
+}
+
+// 编辑按钮回调
+const handleEdit = (row) => {
+  dialogVisible.value = true
+  editInfo.value = Object.assign(editInfo.value, row)
+  // editInfo.value.name = row.name
+  // editInfo.value.userName = row.userName
+  // console.log(editInfo.value)
+}
+
+const handleEditConfirm = async () => {
+  // console.log(editInfo.value)
+  if (editInfo.value.newPassword === '') {
+    ElMessage.error('请输入密码')
+    return
+  }
+  if (editInfo.value.newPassword2 === '') {
+    ElMessage.error('请输入确认新密码')
+    return
+  }
+  if (editInfo.value.newPassword !== editInfo.value.newPassword2) {
+    ElMessage.error('两次输入的密码不一致')
+    return
+  }
+  const res = await editUserList({
+    id: editInfo.value.id,
+    userName: editInfo.value.userName,
+    name: editInfo.value.name,
+    password: RSAencrypt(editInfo.value.newPassword)
+  })
+  // console.log(res)
+  if (res == null) {
+    dialogVisible.value = false
+    editInfo.value.newPassword = ''
+    editInfo.value.newPassword2 = ''
+    ElMessage.success('编辑账号成功')
+    getData()
+  }
+}
+
+const handleEditConfirmAdd = async () => {
+  if (addInfo.value.userName === '') {
+    ElMessage.error('请输入账号')
+    return
+  }
+  if (addInfo.value.name === '') {
+    ElMessage.error('请输入姓名')
+    return
+  }
+  if (addInfo.value.newPassword === '') {
+    ElMessage.error('请输入密码')
+    return
+  }
+  if (addInfo.value.newPassword2 === '') {
+    ElMessage.error('请输入确认新密码')
+    return
+  }
+  if (addInfo.value.newPassword !== addInfo.value.newPassword2) {
+    ElMessage.error('两次输入的密码不一致')
+    return
+  }
+  const res = await addUserList({
+    userName: addInfo.value.userName,
+    name: addInfo.value.name,
+    password: RSAencrypt(addInfo.value.newPassword)
+  })
+  // console.log(res)
+  if (res == null) {
+    ElMessage.success('添加成功')
+    dialogVisibleAdd.value = false
+    getData()
+  }
+}
+
+const tableData = ref([])
+</script>
+
+<style lang="scss" scoped>
+.content {
+  width: 1650px;
+  height: 782px;
+
+  ::v-deep .el-card__body {
+    padding: 0 27px;
+  }
+
+  .card_title {
+    line-height: 58px;
+    font-size: 17px;
+    font-weight: bold;
+    border-bottom: 2px solid #d9d9d9;
+  }
+  .card_search {
+    line-height: 85px;
+    font-size: 13px;
+    font-weight: bold;
+  }
+  .card_button {
+    height: 53px;
+  }
+
+  .card_pagination {
+    float: right;
+    line-height: 96px;
+  }
+}
+
+.dialog_box {
+  height: 150px;
+  .dialog_item {
+    height: 30px;
+    margin-bottom: 20px;
+    span {
+      display: inline-block;
+      width: 70px;
+    }
+  }
+}
+
+::v-deep .el-button--primary {
+  background-color: #0061ff;
+}
+
+::v-deep .el-dialog__header {
+  margin: 0;
+  font-weight: bold;
+  background-color: #edf1f5;
+}
+
+::v-deep
+  .el-table--striped
+  .el-table__body
+  tr.el-table__row--striped.el-table__row--striped.el-table__row--striped
+  td {
+  background-color: #f0f3f7;
+}
+</style>

+ 212 - 0
src/views/login/index.vue

@@ -0,0 +1,212 @@
+<template>
+  <div class="login_container">
+    <!-- 登录表单区域 -->
+    <el-form
+      class="login_form"
+      ref="ruleFormRef"
+      :model="loginForm"
+      :rules="loginRules"
+    >
+      <!-- logo标题区域 -->
+      <div class="form_logo">
+        <img src="../../assets/logo.png" />
+      </div>
+
+      <div class="form_title">代理商登记管理系统</div>
+
+      <!-- 用户名区域 -->
+      <el-form-item prop="userName">
+        <span class="form_svg">
+          <el-icon><Avatar /></el-icon>
+        </span>
+        <el-input
+          placeholder="请输入账户名"
+          type="text"
+          v-model="loginForm.userName"
+        ></el-input>
+      </el-form-item>
+
+      <!-- 密码区域 -->
+      <el-form-item prop="passWord">
+        <span class="form_svg">
+          <el-icon><Lock /></el-icon>
+        </span>
+        <el-input
+          placeholder="请输入密码"
+          type="password"
+          show-password
+          v-model="loginForm.password"
+        ></el-input>
+      </el-form-item>
+
+      <!-- 登录按钮区域 -->
+      <el-button
+        class="form_button"
+        type="primary"
+        :loading="loading"
+        @click="handleLogin"
+      >
+        登录
+      </el-button>
+    </el-form>
+
+    <!-- 图片模块 -->
+    <div class="img_group">
+      <img src="../../assets/login-group.png" />
+    </div>
+
+    <div class="img_people">
+      <img src="../../assets/login-people.png" />
+    </div>
+
+    <div class="img_office">
+      <img src="../../assets/login-office.png" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { useStore } from 'vuex'
+
+const store = useStore()
+
+const loading = ref(false)
+
+const loginForm = ref({
+  userName: '',
+  password: ''
+})
+
+const loginRules = computed(() => {
+  return {
+    userName: [
+      {
+        required: true,
+        trigger: 'change',
+        message: '请输入账户名'
+      }
+    ],
+    password: [
+      {
+        required: true,
+        trigger: 'change',
+        message: '请输入密码'
+      }
+    ]
+  }
+})
+
+const ruleFormRef = ref(null)
+
+// 登录按钮逻辑
+const handleLogin = () => {
+  ruleFormRef.value.validate((valid) => {
+    if (!valid) {
+      return
+    }
+    loading.value = true
+
+    store
+      .dispatch('user/login', loginForm.value)
+      .then(() => {
+        loading.value = false
+      })
+      .catch((err) => {
+        console.log(err)
+        loading.value = false
+      })
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+$bg: #409eff;
+$cursor: #fff;
+.login_container {
+  width: 100vw;
+  height: 100vh;
+  background-image: url(../../assets/login-bg.png);
+  background-size: 100% 100%;
+  .login_form {
+    position: absolute;
+    top: 227px;
+    left: 700px;
+    width: 446px;
+    .form_logo {
+      width: 140px;
+      height: 35px;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .form_title {
+      font-size: 48px;
+      margin: 20px 0 40px;
+      font-weight: bold;
+    }
+
+    .form_svg {
+      color: $bg;
+      margin: 0 10px;
+      cursor: pointer;
+    }
+
+    :deep .el-form-item {
+      height: 48px;
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      background: $cursor;
+      border-radius: 5px;
+      color: #454545;
+      margin-bottom: 50px;
+    }
+
+    :deep .el-input {
+      flex: 1;
+      height: 48px;
+    }
+
+    .form_button {
+      width: 100%;
+      height: 48px;
+      margin-bottom: 30px;
+      font-size: 16px;
+    }
+  }
+
+  .img_group {
+    position: absolute;
+    top: 364px;
+    left: 20px;
+    width: 403px;
+    height: 351px;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .img_people {
+    position: absolute;
+    top: 491px;
+    left: 78px;
+    width: 123px;
+    height: 440px;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .img_office {
+    position: absolute;
+    top: 582px;
+    right: 0;
+    width: 465px;
+    height: 265px;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 9 - 0
src/views/login/rules.js

@@ -0,0 +1,9 @@
+export const validatePassword = () => {
+  return (rule, value, callback) => {
+    if (value.length < 6) {
+      callback(new Error('密码不能少于6位'))
+    } else {
+      callback()
+    }
+  }
+}

+ 370 - 0
src/views/project-management/index.vue

@@ -0,0 +1,370 @@
+<template>
+  <div>
+    <el-card class="content">
+      <!-- 标题区域 -->
+      <el-row class="card_title">项目管理</el-row>
+
+      <!-- 筛选区域 -->
+      <el-row :gutter="35" class="card_search">
+        <el-col :span="5">
+          姓名:
+          <el-input
+            style="width: 180px"
+            placeholder="请输入姓名"
+            clearable
+            v-model="searchName"
+          ></el-input>
+        </el-col>
+
+        <el-col :span="5">
+          单位名称:
+          <el-input
+            style="width: 180px"
+            placeholder="请输入单位名称"
+            clearable
+            v-model="searchAddress"
+          ></el-input>
+        </el-col>
+
+        <el-col :span="5">
+          手机号码:
+          <el-input
+            style="width: 180px"
+            placeholder="请输入手机号码"
+            clearable
+            v-model="searchPhone"
+          ></el-input>
+        </el-col>
+
+        <el-col :span="5">
+          提交时间:
+          <el-date-picker
+            type="date"
+            placeholder="请选择提交时间"
+            value-format="YYYY-MM-DD"
+            v-model="searchTime"
+          />
+        </el-col>
+
+        <el-col :span="4">
+          <el-button type="primary" @click="handleSearch">
+            <el-icon><Search /></el-icon>
+            <span> 查询</span>
+          </el-button>
+        </el-col>
+      </el-row>
+
+      <!-- 导出表单按钮区域 -->
+      <el-row class="card_button">
+        <el-button type="primary" @click="hanleExportForm">
+          <el-icon><Download /></el-icon>
+          <span> 导出表单</span>
+        </el-button>
+      </el-row>
+
+      <!-- 表格区域 -->
+      <el-table
+        stripe
+        height="481"
+        :data="tableData"
+        style="width: 100%"
+        :header-cell-style="{ backgroundColor: '#F0F3F7' }"
+        @select="handleCurrentChangeTable"
+        @select-all="handleCurrentChangeAll"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column width="100" align="center" prop="name" label="姓名" />
+        <el-table-column
+          width="150"
+          align="center"
+          prop="phone"
+          label="手机号码"
+        />
+        <el-table-column
+          width="200"
+          align="center"
+          show-overflow-tooltip
+          prop="company"
+          label="单位名称"
+        />
+        <el-table-column
+          align="center"
+          show-overflow-tooltip
+          prop="content"
+          label="事件登记"
+        />
+        <el-table-column
+          width="200"
+          align="center"
+          prop="reportingTime"
+          label="提交时间"
+        />
+        <el-table-column width="200" align="center" label="操作">
+          <template #default="{ row }">
+            <el-button type="success" plain @click="handleCheckDetail(row)"
+              >详情</el-button
+            >
+            <el-button
+              v-if="store.getters.userInfo.adminType == 1"
+              type="danger"
+              plain
+              @click="handleDelete(row.id)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页器区域 -->
+      <el-row class="card_pagination">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[8, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </el-row>
+    </el-card>
+
+    <!-- 详情弹窗区域 -->
+    <el-dialog
+      v-model="dialogVisible"
+      title="详情"
+      width="40%"
+      top="20vh"
+      :close-on-click-modal="false"
+    >
+      <div class="dialog_box">
+        <div class="dialog_item">
+          <span>姓名:</span>
+          {{ dialogInfo.name }}
+        </div>
+        <div class="dialog_item">
+          <span>单位名称:</span>
+          {{ dialogInfo.company }}
+        </div>
+        <div class="dialog_item">
+          <span>手机号码:</span>
+          {{ dialogInfo.phone }}
+        </div>
+        <div class="dialog_item">
+          <span>提交时间:</span>
+          {{ dialogInfo.reportingTime }}
+        </div>
+        <div class="dialog_item">
+          <span>事件登记:</span>
+          <div>
+            {{ dialogInfo.content }}
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import { useStore } from 'vuex'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getProjectList, deleteProjectList } from '@/api/project'
+
+const store = useStore()
+
+const downIds = ref([])
+
+const handleCurrentChangeTable = (list) => {
+  downIds.value = []
+  if (list.length !== 0) {
+    list.forEach((item) => {
+      downIds.value.push(item.id)
+    })
+  }
+  console.log(downIds.value)
+}
+
+const handleCurrentChangeAll = (list) => {
+  downIds.value = []
+  if (list.length !== 0) {
+    list.forEach((item) => {
+      downIds.value.push(item.id)
+    })
+  }
+  console.log(downIds.value)
+}
+
+// 获取项目管理列表
+const getData = async () => {
+  const res = await getProjectList({
+    name: searchName.value,
+    phone: searchPhone.value,
+    company: searchAddress.value,
+    reportingTime: searchTime.value,
+    currPage: currentPage.value,
+    pageSize: pageSize.value
+  })
+  // console.log(res)
+  tableData.value = res.list
+  total.value = res.totalCount
+  currentPage.value = res.currPage
+}
+
+onMounted(() => {
+  getData()
+})
+
+// 筛选区域绑定数据
+const searchName = ref('')
+const searchAddress = ref('')
+const searchPhone = ref('')
+const searchTime = ref('')
+
+// 点击查询按钮回调
+const handleSearch = () => {
+  // ElMessage.success('查询')
+  getData()
+}
+
+// 导出表单按钮回调
+const hanleExportForm = () => {
+  ElMessageBox.confirm('确定导出表单吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(() => {
+      window.location.href = `/reporting/informationReporting/download?name=${searchName.value}&phone=${searchPhone.value}&company=${searchAddress.value}&reportingTime=${searchTime.value}&ids=${downIds.value}`
+    })
+    .catch(() => {
+      ElMessage.info('取消')
+    })
+}
+
+// 点击详情按钮回调
+const handleCheckDetail = (row) => {
+  dialogVisible.value = true
+  dialogInfo.value = row
+}
+
+// 点击删除按钮回调
+const handleDelete = (id) => {
+  ElMessageBox.confirm('确定删除吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const res = await deleteProjectList({
+        name: store.getters.userInfo.name,
+        id: store.getters.userInfo.id,
+        ids: [id]
+      })
+      // console.log(res)
+      if (res == null) {
+        ElMessage.success('删除成功')
+        getData()
+      }
+    })
+    .catch(() => {
+      ElMessage.info('已取消')
+    })
+}
+
+// 控制详情弹窗显示隐藏
+const dialogVisible = ref(false)
+
+const dialogInfo = ref({})
+
+// 一共多少条
+const total = ref(0)
+// 分页器当前页
+const currentPage = ref(1)
+// 每页条数
+const pageSize = ref(8)
+
+// 改变每页条数回调
+const handleSizeChange = (val) => {
+  // console.log(`每条${val}页`)
+  pageSize.value = val
+  getData()
+}
+// 改变当前页回调
+const handleCurrentChange = (val) => {
+  // console.log(`当前页: ${val}`)
+  currentPage.value = val
+  getData()
+}
+
+// 表格数据
+const tableData = ref([])
+</script>
+
+<style lang="scss" scoped>
+.content {
+  width: 1650px;
+  height: 782px;
+
+  ::v-deep .el-card__body {
+    padding: 0 27px;
+  }
+
+  .card_title {
+    line-height: 58px;
+    font-size: 17px;
+    font-weight: bold;
+    border-bottom: 2px solid #d9d9d9;
+  }
+  .card_search {
+    line-height: 85px;
+    font-size: 13px;
+    font-weight: bold;
+  }
+  .card_button {
+    height: 53px;
+  }
+
+  .card_pagination {
+    float: right;
+    line-height: 96px;
+  }
+}
+
+.dialog_box {
+  height: 430px;
+  .dialog_item {
+    margin-bottom: 10px;
+    height: 35px;
+    color: #4d4d4d;
+    font-size: 15px;
+    span {
+      font-weight: bold;
+    }
+
+    div {
+      height: 210px;
+      margin-top: 10px;
+      overflow-y: auto;
+    }
+  }
+}
+
+::v-deep .el-button--primary {
+  background-color: #0061ff;
+}
+
+::v-deep .el-dialog__header {
+  margin: 0;
+  font-weight: bold;
+  background-color: #edf1f5;
+}
+
+::v-deep
+  .el-table--striped
+  .el-table__body
+  tr.el-table__row--striped.el-table__row--striped.el-table__row--striped
+  td {
+  background-color: #f0f3f7;
+}
+</style>

+ 22 - 0
vue.config.js

@@ -0,0 +1,22 @@
+const { defineConfig } = require('@vue/cli-service')
+// node path 引用配置
+const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
+
+module.exports = defineConfig({
+  transpileDependencies: true,
+  devServer: {
+    // 配置反向代理
+    proxy: {
+      // 当地址中有/api的时候会触发代理机制
+      '/reporting': {
+        // 要代理的服务器地址  这里不用写 api
+        target: 'https://chtech.ncjti.edu.cn/gradiate-school',
+        changeOrigin: true // 是否跨域
+      }
+    }
+  },
+  // node path 引用配置
+  configureWebpack: {
+    plugins: [new NodePolyfillPlugin()]
+  }
+})