Browse Source

初始化项目

zhangyuanyuan2022 1 year ago
commit
17bd505df4
84 changed files with 5313 additions and 0 deletions
  1. 4 0
      .env.development
  2. 4 0
      .env.production
  3. 4 0
      .env.testing
  4. 9 0
      .gitignore
  5. 35 0
      README.md
  6. 143 0
      VERSION.md
  7. 14 0
      index.html
  8. 41 0
      package.json
  9. BIN
      public/favicon.ico
  10. 30 0
      src/App.vue
  11. BIN
      src/assets/AYN.png
  12. 79 0
      src/assets/iconfont/iconfont.css
  13. 1 0
      src/assets/iconfont/iconfont.js
  14. 121 0
      src/assets/iconfont/iconfont.json
  15. BIN
      src/assets/iconfont/iconfont.ttf
  16. BIN
      src/assets/iconfont/iconfont.woff
  17. BIN
      src/assets/iconfont/iconfont.woff2
  18. BIN
      src/assets/images/401.gif
  19. BIN
      src/assets/images/404.png
  20. BIN
      src/assets/images/404_cloud.png
  21. BIN
      src/assets/images/default.png
  22. BIN
      src/assets/images/logo.jpg
  23. BIN
      src/assets/images/logo2.jpg
  24. BIN
      src/assets/images/tasklogo.png
  25. BIN
      src/assets/logo.png
  26. BIN
      src/assets/noclassify.png
  27. BIN
      src/assets/nodata.png
  28. 91 0
      src/assets/style/common.scss
  29. 37 0
      src/assets/style/transition.scss
  30. 12 0
      src/components/menu/index.vue
  31. 102 0
      src/components/table/index.vue
  32. 192 0
      src/components/wangEditor/BasicEditor.vue
  33. 5 0
      src/config/index.js
  34. 61 0
      src/layout/Header/Breadcrumb.vue
  35. 31 0
      src/layout/Header/functionList/fullscreen.vue
  36. 62 0
      src/layout/Header/functionList/sizeChange.vue
  37. 173 0
      src/layout/Header/functionList/theme.vue
  38. 77 0
      src/layout/Header/functionList/theme/theme-color.vue
  39. 130 0
      src/layout/Header/functionList/theme/theme-icon.vue
  40. 152 0
      src/layout/Header/index.vue
  41. 47 0
      src/layout/Logo/index.vue
  42. 41 0
      src/layout/Menu/Link.vue
  43. 100 0
      src/layout/Menu/MenuItem.vue
  44. 120 0
      src/layout/Menu/index.vue
  45. 265 0
      src/layout/Tabs/index.vue
  46. 100 0
      src/layout/Tabs/item.vue
  47. 9 0
      src/layout/Tabs/tabsHook.js
  48. 134 0
      src/layout/index.vue
  49. 21 0
      src/main.js
  50. 11 0
      src/request/api.js
  51. 31 0
      src/request/apiConfig.js
  52. 132 0
      src/request/modules/apiContent.js
  53. 49 0
      src/request/modules/apiCow.js
  54. 19 0
      src/request/modules/apiOrder.js
  55. 21 0
      src/request/modules/apiUser.js
  56. 46 0
      src/router/createNode.js
  57. 61 0
      src/router/index.js
  58. 32 0
      src/router/modules/dashboard.js
  59. 24 0
      src/router/modules/pages.js
  60. 43 0
      src/router/modules/system.js
  61. 116 0
      src/router/reload.vue
  62. 29 0
      src/store/index.js
  63. 44 0
      src/store/modules/app.js
  64. 41 0
      src/store/modules/keepAlive.js
  65. 79 0
      src/store/modules/user.js
  66. 9 0
      src/store/mutation-types.js
  67. 39 0
      src/store/plugins/persistent.js
  68. 99 0
      src/theme/index.js
  69. 51 0
      src/theme/index.scss
  70. 23 0
      src/theme/modules/dark.scss
  71. 9 0
      src/utils/system/filters.js
  72. 12 0
      src/utils/system/nprogress.js
  73. 98 0
      src/utils/system/request.js
  74. 39 0
      src/utils/system/time.js
  75. 6 0
      src/utils/system/title.js
  76. 32 0
      src/utils/system/uploadCompress.js
  77. 315 0
      src/views/classifyManage/classifyManageIndex.vue
  78. 474 0
      src/views/orderManage/orderDetail.vue
  79. 228 0
      src/views/orderManage/orderManageIndex.vue
  80. 223 0
      src/views/system/401.vue
  81. 228 0
      src/views/system/404.vue
  82. 152 0
      src/views/system/login.vue
  83. 21 0
      src/views/system/redirect.vue
  84. 30 0
      vue.config.js

+ 4 - 0
.env.development

@@ -0,0 +1,4 @@
+ENV = 'development'
+
+VITE_BASE_URL = '/dev-api'
+outputDir = 'building for developing please wait...'

+ 4 - 0
.env.production

@@ -0,0 +1,4 @@
+NODE_ENV = 'production'
+
+VITE_BASE_URL = '/pro-api'
+outputDir = 'building for production please wait...'

+ 4 - 0
.env.testing

@@ -0,0 +1,4 @@
+NODE_ENV = 'testing'
+
+VITE_BASE_URL = '/test-api'
+outputDir = 'building for testing please wait...'

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+cattletrading*
+node_modules
+.DS_Store
+purchasePriceTrend
+dist-ssr
+*.local
+yarn.lock
+yarn-error.log
+.vscode

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+
+#### 主要技术栈
+
+- MVVM框架:vue v3
+- 工程化管理:vite v2
+- UI框架:element-plus
+- 路由管理:vue-router v4
+- 状态管理:vuex v4
+- 数据请求:axios
+- 实用工具库:@vueuse/core
+
+
+2. 安装依赖,国内推荐使用cnpm或tyarn,国外推荐使用npm或yarn
+
+   ```
+   npm install
+   ```
+
+   
+
+3. 运行
+
+   ```
+   npm run dev 或 npm run start
+   ```
+
+   
+
+4. 打包
+
+   ```
+   npm run build
+   ```
+   
+

+ 143 - 0
VERSION.md

@@ -0,0 +1,143 @@
+# 版本更新日志
+
+## 0.41版本
+1. 【修改】菜单过长时,滚动条问题
+2. 【新增】主题配置功能,核心编辑代码编写
+
+## 0.40版本
+1. 【优化】路由keep-alive状态保存与删除
+2. 【新增】当前页刷新,缓存页刷新机制
+3. 【新增】关闭标签栏时的缓存页对应状态变化
+
+## 0.32版本
+1. 【优化】左侧菜单栏复杂情况下的示例应用
+2. 【新增】分类联动表格的实现
+3. 【新增】树联动表格的实现
+4. 【BUG】表格+开发BUG修复
+
+## 0.31版本
+1. 【新增】指令集模块完成
+2. 【新增】echarts图表模块完成
+
+## 0.24版本
+```
+1. 【新增】弹窗截图功能
+```
+
+## 0.23版本
+```
+1. 【新增】代码编辑器
+2. 【新增】JSON编辑器
+3. 【新增】可拖拽面板
+4. 【新增】地图组件
+```
+
+## 0.22版本
+```
+1. 【新增】MD编辑器
+```
+
+## 0.21版本
+```
+1. 【统计】统计大部分需要实现的功能页面,并展示于左侧菜单中
+2. 【新增】新增大部分需要实现的路由,并确保,大部分能于7月1日之前完全实现
+```
+
+## 0.11版本
+```
+1. 【新增】新增组件栏目,并补充按钮组进入
+2. 【优化】多级菜单跳转demo
+3. 【优化】侧边栏功能优化
+```
+
+## 0.10版本
+```
+1. 【新增】实现多级菜单Demo
+2. 【优化】当菜单数据量过高时,优化显示
+```
+
+## 0.9版本
+```
+1. 【优化】弹窗组件可拖拽
+2. 【优化】弹窗组件内部暴露逻辑,供外部使用
+```
+
+## 0.8版本
+```
+1. 【优化】axios提示配置
+2. 【优化】公用组件内部逻辑及外部调用方法
+3. 【补充】业务表格模块的数据调用方式
+```
+
+## 0.7版本
+```
+1. 【优化】element-ui国际化配置指南
+2. 【优化】国际化配置本地存储
+```
+
+## 0.6版本
+```
+1. 【新增】按钮尺寸调整功能,针对element-plus的全局尺寸调整
+2. 【新增】首页,图表类展示
+3. 【新增】页面模块,目前包含了业务表格,主要为较为完善的crud写法,正在完善中
+4. 【新增】基于echarts封装的业务chart组件,用户可快速利用此组件生成需要的图表
+```
+
+## 0.5.1版本
+```
+1. 【修复】mock地址,尽量不要使用较短的url,否则,打包上线后,较长的URL容易被冲突掉,示例:登录url和退出登录URL
+```
+
+## 0.5版本
+```
+1. 【优化】本地mock地址使用细节补充及细节优化
+2. 【新增】登录页鉴权
+3. 【新增】退出登录功能
+4. 【优化】本地封装的axios插件优化使用方案
+```
+
+## 0.4版本
+
+```
+1. 【新增】登录页面,及手机自适应处理
+2. 【新增】axios插件,请求处理机制建立
+3. 【优化】全局/@/替换为@/
+4. 【新增】本地mock地址模拟
+```
+
+## 0.3.1版本
+
+```
+1. 【实现】面包屑导航三种关闭功能实现
+```
+
+## 0.3版本
+
+```
+1. 【新增】github跳转链接
+2. 【新增】主题配置功能[页面制作实现]
+3. 【新增】vuex持久化插件,手写版
+```
+
+## 0.2版本
+
+```
+1. 【新增】框架国际化处理
+2. 【BUG】打包上线上路由跳转功能异常,已修复
+```
+
+
+
+## 0.1版本
+
+```
+1. 集成vue-router,vuex,@vueuse/core,element-plus等核心插件
+2. 全局状态管理方案实现
+3. 全局路由管理方案实现
+4. 核心组件库引入处理
+5. 全局layout制作,及自适应处理
+6. 菜单解决方案实现
+7. 面包屑导航实现
+8. 全屏功能实现
+```
+

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <link rel="stylesheet" href="//at.alicdn.com/t/font_2570680_2fgczr3435f.css">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title></title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 41 - 0
package.json

@@ -0,0 +1,41 @@
+{
+  "name": "vue-admin",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build --mode production",
+    "test": "vue-cli-service build --mode testing",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@vueuse/core": "^9.1.1",
+    "@wangeditor/editor": "^5.1.15",
+    "@wangeditor/editor-for-vue": "next",
+    "axios": "^0.21.1",
+    "element-plus": "^2.2.25",
+    "image-conversion": "^2.1.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "normalize.css": "^8.0.1",
+    "nprogress": "^0.2.0",
+    "qs": "^6.11.0",
+    "throttle-debounce": "^3.0.1",
+    "view-ui-plus": "^1.3.1",
+    "vue": "^3.1.2",
+    "vue-router": "4",
+    "vue3-count-to": "^1.1.2",
+    "vuex": "^4.0.0"
+  },
+  "devDependencies": {
+    "@types/node": "^15.0.3",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "^3.0.5",
+    "vue-tsc": "^0.0.24",
+    "sass-loader": "^8.0.2",
+    "sass": "^1.32.12"
+  }
+}

BIN
public/favicon.ico


+ 30 - 0
src/App.vue

@@ -0,0 +1,30 @@
+<template>
+  <el-config-provider :locale="locale">
+    <router-view></router-view>
+  </el-config-provider>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import locale from 'element-plus/lib/locale/lang/zh-cn'
+export default defineComponent({
+  name: 'App',
+  setup() {
+    return {
+      locale
+    }
+  }
+})
+</script>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  /* text-align: center; */
+  color: #2c3e50;
+  width: 100%;
+  height: 100vh;
+}
+</style>

BIN
src/assets/AYN.png


+ 79 - 0
src/assets/iconfont/iconfont.css

@@ -0,0 +1,79 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 3856542 */
+  src: url('iconfont.woff2?t=1673577592633') format('woff2'),
+       url('iconfont.woff?t=1673577592633') format('woff'),
+       url('iconfont.ttf?t=1673577592633') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-_guanggaochuangyi:before {
+  content: "\e606";
+}
+
+.icon-hezuo:before {
+  content: "\e60b";
+}
+
+.icon-dingdandingdanmingxishouzhimingxi:before {
+  content: "\e789";
+}
+
+.icon-yingyong:before {
+  content: "\e600";
+}
+
+.icon-xialazhankai:before {
+  content: "\e6b5";
+}
+
+.icon-xialajiantouxiao:before {
+  content: "\e87e";
+}
+
+.icon-shuaxin:before {
+  content: "\e613";
+}
+
+.icon-quanping1:before {
+  content: "\e62f";
+}
+
+.icon-shezhixitongshezhigongnengshezhishuxing:before {
+  content: "\e795";
+}
+
+.icon--nainiu:before {
+  content: "\e612";
+}
+
+.icon-caidanshouqi:before {
+  content: "\e611";
+}
+
+.icon-caidanzhankai:before {
+  content: "\e615";
+}
+
+.icon-tuichuquanping:before {
+  content: "\e792";
+}
+
+.icon-guanbi1:before {
+  content: "\e723";
+}
+
+.icon-fontsize:before {
+  content: "\e732";
+}
+
+.icon-shezhi:before {
+  content: "\e614";
+}
+

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


+ 121 - 0
src/assets/iconfont/iconfont.json

@@ -0,0 +1,121 @@
+{
+  "id": "3856542",
+  "name": "牛只交易项目",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "9058113",
+      "name": "7_11广告创意",
+      "font_class": "_guanggaochuangyi",
+      "unicode": "e606",
+      "unicode_decimal": 58886
+    },
+    {
+      "icon_id": "7819601",
+      "name": "招商管理",
+      "font_class": "hezuo",
+      "unicode": "e60b",
+      "unicode_decimal": 58891
+    },
+    {
+      "icon_id": "6129155",
+      "name": "114订单、订单明细、收支明细",
+      "font_class": "dingdandingdanmingxishouzhimingxi",
+      "unicode": "e789",
+      "unicode_decimal": 59273
+    },
+    {
+      "icon_id": "1301",
+      "name": "应用",
+      "font_class": "yingyong",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "1486990",
+      "name": "下拉展开",
+      "font_class": "xialazhankai",
+      "unicode": "e6b5",
+      "unicode_decimal": 59061
+    },
+    {
+      "icon_id": "2076220",
+      "name": " 下拉箭头小",
+      "font_class": "xialajiantouxiao",
+      "unicode": "e87e",
+      "unicode_decimal": 59518
+    },
+    {
+      "icon_id": "2152145",
+      "name": "刷新",
+      "font_class": "shuaxin",
+      "unicode": "e613",
+      "unicode_decimal": 58899
+    },
+    {
+      "icon_id": "5650911",
+      "name": "全屏",
+      "font_class": "quanping1",
+      "unicode": "e62f",
+      "unicode_decimal": 58927
+    },
+    {
+      "icon_id": "6129178",
+      "name": "138设置、系统设置、功能设置、属性",
+      "font_class": "shezhixitongshezhigongnengshezhishuxing",
+      "unicode": "e795",
+      "unicode_decimal": 59285
+    },
+    {
+      "icon_id": "9251668",
+      "name": "02-奶牛",
+      "font_class": "-nainiu",
+      "unicode": "e612",
+      "unicode_decimal": 58898
+    },
+    {
+      "icon_id": "11556685",
+      "name": "菜单收起",
+      "font_class": "caidanshouqi",
+      "unicode": "e611",
+      "unicode_decimal": 58897
+    },
+    {
+      "icon_id": "11556695",
+      "name": "菜单展开",
+      "font_class": "caidanzhankai",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    },
+    {
+      "icon_id": "22530095",
+      "name": "退出全屏",
+      "font_class": "tuichuquanping",
+      "unicode": "e792",
+      "unicode_decimal": 59282
+    },
+    {
+      "icon_id": "577309",
+      "name": "关闭1",
+      "font_class": "guanbi1",
+      "unicode": "e723",
+      "unicode_decimal": 59171
+    },
+    {
+      "icon_id": "586897",
+      "name": "font-size",
+      "font_class": "fontsize",
+      "unicode": "e732",
+      "unicode_decimal": 59186
+    },
+    {
+      "icon_id": "1106434",
+      "name": "设置",
+      "font_class": "shezhi",
+      "unicode": "e614",
+      "unicode_decimal": 58900
+    }
+  ]
+}

BIN
src/assets/iconfont/iconfont.ttf


BIN
src/assets/iconfont/iconfont.woff


BIN
src/assets/iconfont/iconfont.woff2


BIN
src/assets/images/401.gif


BIN
src/assets/images/404.png


BIN
src/assets/images/404_cloud.png


BIN
src/assets/images/default.png


BIN
src/assets/images/logo.jpg


BIN
src/assets/images/logo2.jpg


BIN
src/assets/images/tasklogo.png


BIN
src/assets/logo.png


BIN
src/assets/noclassify.png


BIN
src/assets/nodata.png


+ 91 - 0
src/assets/style/common.scss

@@ -0,0 +1,91 @@
+@import "./transition.scss";
+@import "@/theme/index.scss";
+.layout-container {
+  background-color: var(--system-container-main-background);
+  width: calc(100% - 30px);
+  height: calc(100% - 30px);
+  margin: 15px;
+  display: flex;
+  flex-direction: column;
+  overflow-y: auto;
+  &-form {
+    display: flex;
+    justify-content: space-between;
+    padding: 15px 15px 0;
+    &-handle {
+      display: flex;
+      justify-content: flex-start;
+    }
+    &-search {
+      display: flex;
+      justify-content: flex-end;
+      .search-btn {
+        margin-left: 15px;
+      }
+    }
+    .el-form-item {
+      margin-bottom: 0;
+    }
+  }
+  &-table {
+    flex: 1;
+    height: 100%;
+    padding: 15px;
+    overflow: auto;
+  }
+}
+.flex-box {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  padding: 15px;
+  box-sizing: border-box;
+}
+.flex {
+  display: flex;
+}
+.center {
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+}
+a {
+  text-decoration: none;
+}
+
+/** element-plus **/
+.el-icon{
+  text-align: center;
+}
+
+/** 用于提示信息 **/
+.my-tip {
+  background-color: #f1f1f1;
+  padding: 5px 10px;
+  text-align: left;
+  border-radius: 4px;
+}
+.system-scrollbar {
+  &::-webkit-scrollbar {
+    display: none;
+    width: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    background: rgba(144, 147, 153, 0.3);
+  }
+  &:hover {
+    &::-webkit-scrollbar {
+      display: block;
+    }
+    &::-webkit-scrollbar-thumb {
+      border-radius: 10px;
+      background: rgba(144, 147, 153, 0.3);
+      &:hover {
+        background: rgba(144, 147, 153, 0.5);
+      }
+    }
+  }
+ 
+}

+ 37 - 0
src/assets/style/transition.scss

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

+ 12 - 0
src/components/menu/index.vue

@@ -0,0 +1,12 @@
+<template>
+  <router-view></router-view>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  setup() {
+
+  }
+})
+</script>

+ 102 - 0
src/components/table/index.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="system-table-box">
+    <el-table
+      v-bind="$attrs"
+      class="system-table"
+      border
+      height="100%"
+      :data="data"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" align="center" width="50" v-if="showSelection" />
+      <el-table-column label="序号" width="60" align="center" v-if="showIndex">
+        <template #default="scope">
+          {{ (page.index - 1) * page.size + scope.$index + 1 }}
+        </template>
+      </el-table-column>
+      <slot></slot>
+    </el-table>
+    <el-pagination
+      v-if="showPage"
+      v-model:current-page="page.index"
+      class="system-page"
+      background
+      :layout="pageLayout"
+      :total="page.total"
+      :page-size="page.size"
+      :page-sizes="pageSizes"
+      @current-change="handleCurrentChange"
+      @size-change="handleSizeChange"
+    >
+    </el-pagination>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  props: {
+    data: { type: Array, default: () => [] }, // 数据源
+    select: { type: Array, default: () => [] }, // 已选择的数据,与selection结合使用
+    showIndex: { type: Boolean, default: false }, // 是否展示index选择,默认否
+    showSelection: { type: Boolean, default: false }, // 是否展示选择框,默认否
+    showPage: { type: Boolean, default: true }, // 是否展示页级组件,默认是
+    page: { // 分页参数
+      type: Object,
+      default: () => {
+        return { index: 1, size: 20, total: 0 }
+      }
+    },
+    pageLayout: { type: String, default: "total, sizes, prev, pager, next, jumper" }, // 分页需要显示的东西,默认全部
+    pageSizes: { type: Array, default: [10, 20, 50, 100] }
+  },
+  setup(props, context) {
+    let timer = null
+    // 分页相关:监听页码切换事件
+    const handleCurrentChange = (val) => {
+      if (timer) {
+        props.page.index = 1
+      } else {
+        props.page.index = val
+        context.emit("getTableData")
+      }
+    }
+    // 分页相关:监听单页显示数量切换事件
+    const handleSizeChange = (val) => {
+      timer = 'work'
+      setTimeout(() => {
+        timer = null
+      }, 100)
+      props.page.size = val
+      context.emit("getTableData", true)
+    }
+    // 选择监听器
+    const handleSelectionChange = (val) =>{
+      context.emit("selection-change", val)
+    }
+    return {
+      handleCurrentChange,
+      handleSizeChange,
+      handleSelectionChange
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .system-table-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: flex-start;
+    height: 100%;
+    .system-table {
+      flex: 1;
+      height: 100%;
+    }
+    
+    .system-page {
+      margin-top: 20px;
+    }
+  }
+</style>

+ 192 - 0
src/components/wangEditor/BasicEditor.vue

@@ -0,0 +1,192 @@
+<template>
+    <div>
+        <div style="border: 1px solid #ccc; margin-top: 10px;">
+            <Toolbar
+                :editor="editorRef"
+                :defaultConfig="toolbarConfig"
+                :mode="mode"
+                style="border-bottom: 1px solid #ccc"
+            />
+            <Editor
+                :defaultConfig="editorConfig"
+                :mode="mode"
+                v-model="valueHtml"
+                style="height: 400px; overflow-y: hidden;"
+                @onCreated="handleCreated"
+                @onChange="handleChange"
+                @onDestroyed="handleDestroyed"
+                @onFocus="handleFocus"
+                @onBlur="handleBlur"
+                @customAlert="customAlert"
+                @customPaste="customPaste"
+            />
+        </div>
+        <Button icon="md-add" size="small" @click="displayCodeState = !displayCodeState">显示代码视图</Button>
+        <div style="margin-top: 10px;" v-if="displayCodeState">
+            <textarea v-model="valueHtml" readonly style="width: 100%; height: 200px; outline: none;"></textarea>
+        </div>
+    </div>
+</template>
+
+<script>
+import '@wangeditor/editor/dist/css/style.css'
+import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import api from '@/request/apiConfig' //真实接口配置\
+import {compressFileM} from '@/utils/system/uploadCompress.js'
+
+export default {
+  components: { Editor, Toolbar },
+  setup() {
+    // 编辑器实例,必须用 shallowRef,重要!
+    const editorRef = shallowRef()
+
+    // 内容 HTML
+    const valueHtml = ref('')
+
+    // 显示代码视图开关
+    const displayCodeState = ref(false)
+
+    const toolbarConfig = {}
+    const editorConfig = { 
+        placeholder: '请输入内容...',
+        MENU_CONF: {
+            // 上传图片配置
+            uploadImage: {
+                server: api.apiManager + '/manager/images/upload', // 上传图片地址
+                timeout: 60 * 1000, // 60s
+                fieldName: 'file',
+                headers: {'X-AIYANGNIU-SIGNATURE':localStorage.aynUserToken},
+
+                onBeforeUpload(file) {
+                    return file
+                    // return false 会阻止上传
+                },
+                onSuccess(file, res) {
+                    // console.log('onSuccess', file, res)
+                },
+                // 编辑器自身原因,无法识别上传成功,放在failed里执行
+                onFailed(file, res) {
+
+                },
+                onError(file, err, res) {
+                    alert(err.message)
+                },
+
+                customInsert(res, insertFn) {
+                    insertFn(res.data, '图片', '')
+                },
+            },
+            // 上传视频配置
+            uploadVideo:{
+                onInsertedVideo(videoNode) {
+                    console.log('inserted video', videoNode)
+                },
+                server: api.apiManager + '/manager/images/upload', // 上传图片地址
+                timeout: 6000 * 1000, // 6000s
+                fieldName: 'file',
+                headers: {'X-AIYANGNIU-SIGNATURE':localStorage.aynUserToken},
+                maxFileSize: 5120 * 1024 * 1024, // 5G
+
+                onBeforeUpload(file) {
+                    console.log('onBeforeUpload', file)
+                    return file // will upload this file
+                },
+                onSuccess(file, res) {
+                    console.log('onSuccess', file, res)
+                },
+                onFailed(file, res) {
+                    console.log('onFailed', file, res)
+                },
+                onError(file, err, res) {
+                    alert(err.message)
+                    console.error('onError', file, err, res)
+                },
+                customInsert(res, insertFn) {
+                    insertFn(res.data, '')
+                },
+            }
+        }
+    }
+
+    // 组件销毁时,也及时销毁编辑器,重要!
+    onBeforeUnmount(() => {
+        const editor = editorRef.value
+        if (editor == null) return
+
+        editor.destroy()
+    })
+
+    // 编辑器回调函数
+    const handleCreated = (editor) => {
+      console.log('created', editor);
+      editorRef.value = editor // 记录 editor 实例,重要!
+    }
+    const handleChange = (editor) => {
+    //   console.log('change:', editor.getHtml());
+    }
+    const handleDestroyed = (editor) => {
+    //   console.log('destroyed', editor)
+    }
+    const handleFocus = (editor) => {
+        // console.log('focus', editor)
+    }
+    const handleBlur = (editor) => {
+        // console.log('blur', editor)
+    }
+    const customAlert = (info, type) => {
+        alert(`【自定义提示】${type} - ${info}`)
+    }
+
+    // 自动贴入内容
+    const customPaste = (editor, event, callback) => {
+        console.log('ClipboardEvent 粘贴事件对象', event)
+
+        // 自定义插入内容
+        editor.insertText('xxx')
+
+        // 返回值(注意,vue 事件的返回值,不能用 return)
+        callback(false) // 返回 false ,阻止默认粘贴行为
+        // callback(true) // 返回 true ,继续默认的粘贴行为
+    }
+
+    const insertText = () => {
+        const editor = editorRef.value
+        if (editor == null) return
+        editor.insertText('hello world')
+    }
+
+    const printHtml = () => {
+        const editor = editorRef.value
+        if (editor == null) return
+        console.log(editor.getHtml())
+    }
+    
+    // 禁用富文本
+    const disable = () => {
+        const editor = editorRef.value
+        if (editor == null) return
+        editor.disable()
+    }
+
+    return {
+      editorRef,
+      mode: 'default',
+      valueHtml,
+      toolbarConfig,
+      editorConfig,
+      handleCreated,
+      handleChange,
+      handleDestroyed,
+      handleFocus,
+      handleBlur,
+      customAlert,
+      customPaste,
+      insertText,
+      printHtml,
+      disable,
+      displayCodeState,
+    };
+  }
+}
+</script>

+ 5 - 0
src/config/index.js

@@ -0,0 +1,5 @@
+const showLogo = true; // 是否显示Logo顶部模块
+const systemTitle = '爱养牛竞价系统' // 系统名称,用于显示在左上角模块,以及浏览器标题上使用,使用配置项
+export {
+  systemTitle
+}

+ 61 - 0
src/layout/Header/Breadcrumb.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb hidden-sm-and-down" separator="/">
+    <transition-group appear name="breadcrumb">
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{  item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">
+          {{ item.meta.title }}
+        </a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import { defineComponent, ref, watch } from "vue";
+import { useRoute, useRouter } from "vue-router";
+export default defineComponent({
+  name: "BreadCrumb",
+  setup() {
+    const levelList = ref([]);
+    const route = useRoute();
+    const router = useRouter();
+    const getBreadcrumb = () => {
+      let matched = route.matched.filter(item => item.meta && item.meta.title);
+      const first = matched[0];
+      levelList.value = matched.filter(
+        item => item.meta && item.meta.title && item.meta.breadcrumb !== false
+      );
+    };
+    getBreadcrumb();
+    watch(
+      () => route.path,
+      () => getBreadcrumb()
+    );
+    const handleLink = (item) => {
+      const { redirect, path } = item;
+      if (redirect) {
+        router.push(redirect.toString());
+        return;
+      }
+      router.push(path);
+    };
+    return { levelList, handleLink };
+  }
+});
+</script>
+
+<style lang="scss" scoped >
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  .no-redirect {
+    color: var(--system-header-breadcrumb-text-color);
+    cursor: text;
+  }
+  a {
+    color: var(--system-header-text-color);
+  }
+}
+</style>

+ 31 - 0
src/layout/Header/functionList/fullscreen.vue

@@ -0,0 +1,31 @@
+<template>
+  <div :title="isFullscreen ? '退出全屏' : '全屏'">
+    <i class="iconfont" :class="isFullscreen ? 'icon-tuichuquanping':'icon-quanping1'" @click="toggle"></i>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { useFullscreen } from '@vueuse/core'
+
+export default defineComponent({
+  name: 'fullscreen',
+  setup() {
+    const { isFullscreen, toggle } = useFullscreen()
+    return {
+      isFullscreen,
+      toggle
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  i {
+    cursor: pointer;
+    font-size: 18px;
+    &:focus {
+      outline: none;
+    }
+  }
+</style>

+ 62 - 0
src/layout/Header/functionList/sizeChange.vue

@@ -0,0 +1,62 @@
+<template>
+  <el-dropdown @command="handleCommand">
+    <div class="el-dropdown-link">
+      <i class="iconfont icon-fontsize"></i>
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item
+          v-for="d in list"
+          :key="d.size"
+          :command="d.size"
+          :disabled=" elementSize === d.size "
+        >
+          {{ d.name }}
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>
+
+<script>
+import { defineComponent, computed, unref } from 'vue'
+import { useRoute } from 'vue-router'
+import { useStore } from 'vuex'
+export default defineComponent({
+  setup() {
+    const store = useStore()
+    const route = useRoute()
+    const elementSize = computed(() => store.state.app.elementSize)
+    const list = [
+      { size: 'default', name: '默认' },
+      { size: 'large ', name: '大' },
+      { size: 'small', name: '小' },
+    ]
+    const { fullPath } = unref(route)
+    return {
+      list,
+      elementSize,
+      fullPath
+    }
+  },
+  methods: {
+    handleCommand(command) {
+      this.$store.commit('app/stateChange', {
+        name: 'elementSize',
+        value: command
+      })
+      this.setElementSize()
+    },
+    setElementSize() {
+      this.$ELEMENT.size = this.elementSize
+      this.$router.replace({
+        path: "/redirect" + this.fullPath
+      })
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  
+</style>

+ 173 - 0
src/layout/Header/functionList/theme.vue

@@ -0,0 +1,173 @@
+<template>
+  <div title="系统设置" @click="drawerChange(true)">
+    <i class="iconfont icon-shezhi"></i>
+  </div>
+  <el-drawer
+    title="系统设置"
+    v-model="drawer"
+    size="300px"
+    :show-close="false"
+    direction="rtl">
+      <h3>整体风格设置</h3>
+      <div class="theme-box">
+        <theme-icon
+          v-model:active="state.style"
+          v-for="(row, index) in style"
+          :key="index"
+          :name="index"
+          :tip="row.name"
+          :logo="row.logo.background"
+          :menu="row.menu.background"
+          :header="row.header.background"
+          :main="row.container.background"
+          :activeColor="row.page.color"
+        ></theme-icon>
+      </div>
+      <h3>主题色</h3>
+      <div class="theme-box">
+        <theme-color v-for="(item, key) in themeColorArr"
+          v-model:active="state.primaryColor"
+          v-model:activeTextColor="state.primaryTextColor"
+          :key="key"
+          :color="item.color"
+          :textColor="item.textColor"
+          :tip="item.tip">
+        </theme-color>
+      </div>
+      <h3>其他设置</h3>
+      <div class="list">
+        <div class="list-item" v-for="option in options" :key="option.name">
+          <span>{{ option.name }}</span>
+          <el-switch
+            v-model="option.value"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+            @change="change(option)"
+          >
+          </el-switch>
+        </div>
+      </div>
+  </el-drawer>
+</template>
+
+<script>
+import { defineComponent, ref, reactive, computed, watch } from 'vue'
+import { useStore } from 'vuex'
+import themeIcon from './theme/theme-icon.vue'
+import themeColor from './theme/theme-color.vue'
+import { style } from '@/theme/index'
+
+export default defineComponent({
+  components: {
+    themeIcon,
+    themeColor
+  },
+  setup() {
+    const store = useStore()
+    // 只取值,不做computed
+    const state = reactive({
+      style: store.state.app.theme.state.style,
+      primaryColor: store.state.app.theme.state.primaryColor,
+      primaryTextColor: store.state.app.theme.state.primaryTextColor,
+      menuType: store.state.app.theme.state.menuType
+    })
+    const themeColorArr = [
+      { color: '#409eff', textColor: '#fff', tip: '默认蓝' },
+      { color: '#d60f20', textColor: '#fff', tip: '玫瑰红' },
+      { color: '#ac25e6', textColor: '#fff', tip: '优雅紫' },
+      { color: '#4dc86f', textColor: '#fff', tip: '故事绿' },
+      { color: '#13c2c2', textColor: '#fff', tip: '明青' },
+      { color: '#333', textColor: '#fff', tip: '极客黑' }
+    ]
+    const setTheme = () => {
+      const userTheme = style[state.style]
+      const body = document.getElementsByTagName('body')[0]
+      // 设置全局顶部body上的class名称,即为主题名称,便于自定义配置符合当前主题的样式统一入口
+      body.className = state.style
+      // 需要设置的颜色参照theme.scss,位置:assets/style/theme.scss
+      // 设置主题色
+      body.style.setProperty('--system-primary-color', state.primaryColor)
+      for (let i in userTheme) {
+        const item = userTheme[i]
+        for (let y in item) {
+          let cssVarName = '--system-' + i + '-' + y.replace(/([A-Z])/g, "-$1").toLowerCase()
+          body.style.setProperty(cssVarName, item[y])
+        }
+      }
+    }
+    // 监听数据的变化
+    watch(state, (newVal) => {
+      const theme = {
+        state: {
+          ...state
+        }
+      }
+      store.commit('app/stateChange', {
+        name: 'theme',
+        value: theme
+      })
+      setTheme()
+    })
+    let drawer = ref(false)
+    const options = reactive([
+      { name: '是否显示LOGO', value: store.state.app.showLogo, store: 'showLogo' },
+      { name: '显示面包屑导航', value: store.state.app.showTabs, store: 'showTabs' },
+      { name: '保持一个菜单展开', value: store.state.app.expandOneMenu, store: 'expandOneMenu' }
+    ])
+    const drawerChange = (value) => {
+      drawer.value = value
+    }
+    const change = (option) => {
+      store.commit(`app/stateChange`, { name: option.store, value: option.value })
+    }
+    setTheme()
+    return {
+      drawer,
+      options,
+      state,
+      style,
+      themeColorArr,
+      drawerChange,
+      change
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  i {
+    cursor: pointer;
+    &:focus {
+      outline: none;
+    }
+  }
+  
+  .list {
+    padding: 0 20px;
+    &-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 15px;
+      span {
+        font-size: 16px;
+      }
+    }
+  }
+  h3 {
+    margin-top: 40px;
+    margin-bottom: 20px;
+    color: rgba(0,0,0,.85);
+    font-size: 14px;
+    line-height: 22px;
+    text-align: left;
+    padding: 0 20px;
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+  .theme-box {
+    text-align: left;
+    padding-left: 20px;
+  }
+</style>

+ 77 - 0
src/layout/Header/functionList/theme/theme-color.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-tooltip class="item" effect="dark" :content="tip" placement="top">
+    <div class="theme-color" :style="{ 'background-color': color }" @click="handleClick">
+      <div class="active" v-if="active === color">
+        <i class="el-icon-check" :style="{'color': textColor}"></i>
+      </div>
+    </div>
+  </el-tooltip>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  props: {
+    active: {
+      type: String,
+      default: ''
+    },
+    activeTextColor: {
+      type: String,
+      default: ''
+    },
+    tip: {
+      type: String,
+      default: '默认蓝'
+    },
+    color: {
+      type: String,
+      default: '#409eff'
+    },
+    textColor: {
+      type: String,
+      default: '#fff'
+    }
+  },
+  setup(props, ctx) {
+    // 点击事件,触发v-model修改active值
+    const handleClick = () => {
+      ctx.emit('update:active', props.color)
+      ctx.emit('update:activeTextColor', props.textColor)
+    }
+    return {
+      handleClick
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .theme-color {
+    border-radius: 4px;
+    width: 20px;
+    height: 20px;
+    display: inline-block;
+    cursor: pointer;
+    outline: none;
+    position:relative;
+    .active {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      left: 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      i {
+        color: #fff;
+        font-weight: bold;
+        font-size: 12px;
+      }
+    }
+    &+.theme-color{
+      margin-left: 10px;
+    }
+  }
+</style>

+ 130 - 0
src/layout/Header/functionList/theme/theme-icon.vue

@@ -0,0 +1,130 @@
+<template>
+  <el-tooltip class="item" effect="dark" :content="tip" placement="top">
+    <div class="theme-icon" @click="handleClick">
+      <div class="theme-icon-sidebar">
+        <div class="theme-icon-sidebar-logo" :style="{ 'background-color': logo }"></div>
+        <div class="theme-icon-sidebar-menu" :style="{ 'background-color': menu }"></div>
+      </div>
+      <div class="theme-icon-content">
+        <div class="theme-icon-content-header" :style="{ 'background-color': header }"></div>
+        <div class="theme-icon-content-main" :style="{ 'background-color': main }">
+          <div class="active" v-if="active === name">
+            <i class="el-icon-check" :style="{'color': activeColor}"></i>
+          </div>
+        </div>
+      </div>
+      
+    </div>
+  </el-tooltip> 
+  
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  props: {
+    name: {
+      type: String,
+      default: 'default'
+    },
+    active: {
+      type: String,
+      default: ''
+    },
+    menu: {
+      type: String,
+      defualt: ''
+    },
+    logo: {
+      type: String,
+      defualt: ''
+    },
+    header: {
+      type: String,
+      defualt: ''
+    },
+    main: {
+      type: String,
+      defualt: ''
+    },
+    tip: {
+      type: String,
+      default: '默认菜单风格'
+    },
+    activeColor: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props, ctx) {
+    // 点击事件,触发v-model修改active值
+    const handleClick = () => {
+      ctx.emit('update:active', props.name)
+    }
+    return {
+      handleClick
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .theme-icon {
+    display: inline-flex;
+    width: 50px;
+    height: 50px;
+    box-shadow: 0 1px 2.5px 0 rgba(0,0,0,.18);
+    cursor: pointer;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.12),0 0 6px rgba(0,0,0,0.04);
+    outline: none;
+    border-radius: 4px;
+    overflow: hidden;
+    &-sidebar{
+      width: 18px;
+      display: flex;
+      flex-direction: column;
+      &-logo{
+        width: 20px;
+        height: 10px;
+        background-color: #263445;
+      }
+      &-menu{
+        flex: 1;
+        background-color: rgb(40, 65, 90);
+      }
+    }
+    &-content{
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      &-header{
+        height: 10px;
+        background-color: #fff;
+      }
+      &-main{
+        flex: 1;
+        background-color: #f8f8f8;
+        position: relative;
+        .active {
+          position: absolute;
+          width: 100%;
+          height: 100%;
+          top: 0;
+          left: 0;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          i {
+            color: #000;
+            font-weight: bold;
+            font-size: 22px;
+          }
+        }
+      }
+    }
+
+  }
+  .theme-icon+.theme-icon{
+    margin-left: 20px;
+  }
+</style>

+ 152 - 0
src/layout/Header/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <header>
+    <div class="left-box">
+      <!-- 收缩按钮 -->
+      <div class="menu-icon" @click="opendStateChange">
+        <i class="iconfont" :class="isCollapse ? 'icon-caidanzhankai':'icon-caidanshouqi'" style="font-size:24px;"></i>
+      </div>
+      <Breadcrumb />
+    </div>
+    <div class="right-box">
+      <!-- 快捷功能按钮 -->
+      <div class="function-list">
+        <div class="function-list-item hidden-sm-and-down">
+          <Full-screen />
+        </div>
+        <div class="function-list-item"><SizeChange /></div>
+        <div class="function-list-item hidden-sm-and-down"><Theme /></div>
+      </div>
+      <!-- 用户信息 -->
+      <div class="user-info">
+        <el-avatar src="https://i.loli.net/2017/08/21/599a521472424.jpg" style="margin-top:1px;" size="small"/>
+        <el-dropdown>
+          <div class="el-dropdown-link">
+            <span>{{userInfo.nickName || '请登录'}}</span> 
+            <i class="iconfont icon-xialazhankai" style="font-size:14px;margin-left: 3px;"></i>
+          </div>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item @click="loginOut">退出登录</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+  </header>
+</template>
+
+<script>
+import { defineComponent, computed, reactive } from 'vue'
+import { useStore } from 'vuex'
+import { useRouter, useRoute } from 'vue-router'
+import FullScreen from './functionList/fullscreen.vue'
+import SizeChange from './functionList/sizeChange.vue'
+import Theme from './functionList/theme.vue'
+import Breadcrumb from './Breadcrumb.vue'
+import { ElMessage } from 'element-plus'
+export default defineComponent({
+  components: {
+    FullScreen,
+    Breadcrumb,
+    SizeChange,
+    Theme,
+  },
+  setup() {
+    const store = useStore()
+    const router = useRouter()
+    const route = useRoute()
+    const layer = reactive({
+      show: false,
+      showButton: true
+    })
+
+    const userInfo = computed(()=>store.state.user.userInfo)
+    const isCollapse = computed(() => store.state.app.isCollapse)
+    // isCollapse change to hide/show the sidebar
+    const opendStateChange = () => {
+      store.commit('app/isCollapseChange', !isCollapse.value)
+    }
+
+    // login out the system
+    const loginOut = () => {
+      store.dispatch('user/loginOut').then(() => {
+        ElMessage.success({
+          message: '退出登录成功',
+          type: 'success',
+          showClose: true,
+          duration: 1000
+        })
+        router.push('/login')
+      })
+    }
+    
+  
+    return {
+      isCollapse,
+      layer,
+      opendStateChange,
+      loginOut,
+      userInfo,
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 60px;
+    background-color: var(--system-header-background);
+    padding-right: 22px;
+  }
+  .left-box {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    border-left: 1px solid var(--system-header-border-color);
+    .menu-icon {
+      width: 60px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 25px;
+      font-weight: 100;
+      cursor: pointer;
+      margin-right: 10px;
+      &:hover {
+        background-color: var(--system-header-item-hover-color);
+      }
+      i {
+        color: var(--system-header-text-color);
+      }
+    }
+  }
+  .right-box {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .function-list{
+      display: flex;
+      .function-list-item {
+        width: 30px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        :deep(i) {
+          color: var(--system-header-text-color);
+        }
+      }
+    }
+    .user-info {
+      margin-left: 20px;
+      .el-dropdown-link {
+        margin-top: 8px;
+        margin-left: 5px;
+        color: var(--system-header-breadcrumb-text-color);
+      }
+    }
+  }
+</style>

+ 47 - 0
src/layout/Logo/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="logo-container">
+    <Row>
+      <Col :span="isCollapse?24:5" class="login-posi">
+        <img src="@/assets/AYN.png" style="width:70%;" alt="">
+      </Col>
+      <Col span="19" v-if="!isCollapse">
+        <h1>{{ systemTitle }}</h1>
+      </Col>
+    </Row>
+  </div>
+</template>
+
+<script>
+import { defineComponent, computed } from 'vue'
+import { useStore } from 'vuex'
+import { systemTitle } from '@/config'
+export default defineComponent({
+  setup() {
+    const store = useStore()
+    const isCollapse = computed(() => store.state.app.isCollapse)
+    return {
+      isCollapse,
+      systemTitle
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .logo-container {
+    height: 60px;
+    width: 100%;
+    background-color: var(--system-logo-background);
+    .login-posi{
+      height: 60px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    h1 {
+      font-size: 20px;
+      white-space: nowrap;
+      color: var(--system-logo-color);
+    }
+  }
+</style>

+ 41 - 0
src/layout/Menu/Link.vue

@@ -0,0 +1,41 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)" @click="hideMenu" >
+    <slot>
+    </slot>
+  </component>
+</template>
+<script>
+import { defineComponent, computed } from 'vue'
+import { useStore } from "vuex";
+export default defineComponent({
+  name: 'appLink',
+  props: {
+    to: {
+      type: String,
+      required: true
+    }
+  },
+  setup(props) {
+    const store = useStore();
+    const isCollapse = computed(() => store.state.app.isCollapse);
+    const linkProps = (to) => {
+     return {
+       to: to
+     } 
+    }
+    const hideMenu = () => {
+      if (document.body.clientWidth <= 1000 && !isCollapse.value) {
+        store.commit("app/isCollapseChange", true);
+      }
+    };
+    return {
+      type: "router-link",
+      linkProps,
+      hideMenu
+    }
+  }
+})
+</script>
+<style lang="">
+  
+</style>

+ 100 - 0
src/layout/Menu/MenuItem.vue

@@ -0,0 +1,100 @@
+<template>
+  <template v-if="!menu.hideMenu">
+    <el-sub-menu v-if="showMenuType === 2" :index="pathResolve">
+      <template #title v-if="!menu.meta.hideMenuItem">
+        <i :class="'iconfont'+' '+menu.meta.icon" v-if="menu.meta.icon"></i>
+        <span class="marginLeft5">{{ menu.meta.title }}</span>
+      </template>
+      <menu-item v-for="(item, key) in menu.children" :key="key" :menu="item" :basePath="pathResolve" />
+    </el-sub-menu>
+    <app-link v-else-if="showMenuType === 1" :to="pathResolve">
+      <el-menu-item :index="pathResolve" v-if="(!menu.children[0].children || menu.children[0].children.length === 0)&&!menu.meta.hideMenuItem">
+        <i :class="'iconfont'+' '+menu.children[0].meta.icon || menu.meta.icon" v-if="menu.children[0].meta.icon || menu.meta.icon"></i>
+        <template #title><span class="marginLeft5">{{ menu.children[0].meta.title }}</span></template>
+      </el-menu-item>
+      <el-sub-menu v-else :index="pathResolve">
+        <template #title v-if="!menu.meta.hideMenuItem">
+          <i :class="'iconfont'+' '+menu.children[0].meta.icon || menu.meta.icon" v-if="menu.children[0].meta.icon || menu.meta.icon"></i>
+          <span class="marginLeft5">{{ menu.children[0].meta.title }}</span>
+        </template>
+        <menu-item v-for="(item, key) in menu.children[0].children" :key="key" :menu="item" :basePath="pathResolve" />
+      </el-sub-menu>
+    </app-link>
+    <app-link v-else :to="pathResolve">
+      <el-menu-item :index="pathResolve" v-if="!menu.meta.hideMenuItem">
+        <i :class="'iconfont'+' '+menu.meta.icon" v-if="menu.meta.icon"></i>
+        <template #title><span class="marginLeft5">{{ menu.meta.title }}</span></template>
+      </el-menu-item>
+    </app-link>
+  </template>
+</template>
+
+<script>
+import { defineComponent, computed } from 'vue'
+import appLink from './Link.vue'
+export default defineComponent({
+  name: 'menu-item',
+  props: {
+    menu: {
+      type: Object,
+      required: true
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  components: {
+    appLink
+  },
+
+  setup(props) {
+    const menu = props.menu
+    // todo: 优化if结构
+    const showMenuType = computed(() => { // 0: 无子菜单, 1:有1个子菜单, 2:显示上下级子菜单
+      if (menu.children && (menu.children.length > 1 || (menu.children.length === 1 && menu.alwayShow))) {
+        return 2
+      } else if (menu.children && menu.children.length === 1 && !menu.alwayShow) {
+        return 1
+      } else {
+        return 0
+      }
+    })
+    // todo: 优化多层if
+    const pathResolve = computed(() => {
+      let path = ''
+      if (showMenuType.value === 1) {
+        if (menu.children[0].path.charAt(0) === '/') {
+          path = menu.children[0].path
+        } else {
+          let char = '/'
+          if (menu.path.charAt(menu.path.length - 1) === '/') {
+            char = ''
+          }
+          path = menu.path + char + menu.children[0].path
+        }
+      } else {
+        path = menu.path
+      }
+      path = props.basePath ? props.basePath + '/' + path : path
+      return path
+    })
+    return {
+      showMenuType,
+      pathResolve
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .el-sub-menu {
+    text-align: left;
+  }
+  .el-menu-item {
+    text-align: left;
+  }
+  .marginLeft5{
+    margin-left: 5px;
+  }
+</style>

+ 120 - 0
src/layout/Menu/index.vue

@@ -0,0 +1,120 @@
+<template>
+  <el-scrollbar>
+    <el-menu
+      class="layout-menu system-scrollbar"
+      background-color="var(--system-menu-background)"
+      text-color="var(--system-menu-text-color)"
+      active-text-color="var(--system-primary-color)"
+      :default-active="activeMenu"
+      :class="isCollapse? 'collapse': ''"
+      :collapse="isCollapse"
+      :collapse-transition="false"
+      :unique-opened="expandOneMenu"
+    >
+      <menu-item v-for="(menu, key) in allRoutes" :key="key" :menu="menu" />
+    </el-menu>
+  </el-scrollbar>
+</template>
+
+<script>
+import { defineComponent, computed, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from 'vuex'
+import MenuItem from './MenuItem.vue'
+export default defineComponent({
+  components: {
+    MenuItem
+  },
+
+  setup() {
+    const store = useStore()
+    const isCollapse = computed(() => store.state.app.isCollapse)
+    const expandOneMenu = computed(() => store.state.app.expandOneMenu)
+    const allRoutes = useRouter().options.routes
+    const route = useRoute()
+    const activeMenu = computed(() => {
+      const { meta, path } = route;
+      if (meta.activeMenu) {
+        return meta.activeMenu;
+      }
+      return path;
+    });
+    
+    onMounted(() => {
+
+    })
+    return {
+      isCollapse,
+      expandOneMenu,
+      allRoutes,
+      activeMenu,
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .el-scrollbar {
+    background-color: var(--system-menu-background);
+  }
+  .layout-menu {
+    border-right:0px solid;
+    width: 100%;
+    &.collapse {
+      margin-left: 0px;
+    }
+    :deep() {
+      .el-menu-item, .el-submenu {
+        background-color: var(--system-menu-background) !important;
+      }
+      .el-menu-item i, .el-menu-item-group__title, .el-submenu__title i {
+        color: var(--system-menu-text-color);
+      }
+      .el-menu-item, .el-submenu__title{
+        &.is-active {
+          background-color: var(--system-primary-color) !important;
+          color: var(--system-primary-text-color) !important;
+          i {
+            color: var(--system-primary-text-color) !important;
+          }
+          &:hover {
+            background-color: var(--system-primary-color) !important;
+            color: var(--system-primary-text-color) !important;
+          }
+        }
+        &:hover {
+          background-color: var(--system-menu-hover-background) !important;
+        }
+      }
+      .el-submenu {
+        &.is-active {
+          >.el-submenu__title, >.el-submenu__title i {
+            color: var(--system-menu-submenu-active-color) !important;
+          }
+        }
+        .el-menu-item {
+          background-color: var(--system-menu-children-background) !important;
+          &.is-active {
+            background-color: var(--system-primary-color) !important;
+            color: var(--system-primary-text-color) !important;
+            &:hover {
+              background-color: var(--system-primary-color) !important;
+              color: var(--system-primary-text-color) !important;
+            }
+          }
+          &:hover {
+            background-color: var(--system-menu-hover-background) !important;
+          }
+        }
+      }
+      .el-submenu {
+        .el-submenu__title {
+          background-color: var(--system-menu-children-background) !important;
+          &:hover {
+            background-color: var(--system-menu-hover-background) !important;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 265 - 0
src/layout/Tabs/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="tabs">
+    <el-scrollbar class="scroll-container tags-view-container" ref="scrollbarDom">
+      <Item
+        v-for="menu in menuList"
+        :key="menu.meta.title"
+        :menu="menu"
+        :active="activeMenu.path === menu.path"
+        @close="delMenu(menu)"
+        @reload="pageReload"
+      />
+    </el-scrollbar>
+    <div class="handle">
+      <el-dropdown placement="bottom">
+        <div class="el-dropdown-link">
+          <i class="iconfont icon-xialajiantouxiao"></i>
+        </div>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item @click="pageReload">重新加载</el-dropdown-item>
+            <el-dropdown-item :disabled="currentDisabled" @click="closeCurrentRoute">关闭当前标签</el-dropdown-item>
+            <el-dropdown-item :disabled="menuList.length < 3" @click="closeOtherRoute">关闭其他标签</el-dropdown-item>
+            <el-dropdown-item :disabled="menuList.length <= 1" @click="closeAllRoute">关闭所有标签</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+      <i class="iconfont" :class="contentFullScreen?'icon-tuichuquanping':'icon-quanping1'" @click="onFullscreen"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent, computed, unref, watch, reactive, ref, nextTick } from 'vue'
+import Item from './item.vue'
+import { useStore } from 'vuex'
+import { useRoute, useRouter } from 'vue-router'
+import tabsHook from './tabsHook'
+export default defineComponent({
+  components: {
+    Item
+  },
+  setup() {
+    const store = useStore()
+    const route = useRoute()
+    const router = useRouter()
+    const scrollbarDom = ref(null)
+    const allRoutes = router.options.routes
+    const defaultMenu = {
+      path: '/dashboard',
+      meta: { title: '首页', hideClose: true }
+    }
+    const contentFullScreen = computed(() => store.state.app.contentFullScreen)
+    const currentDisabled = computed(() => route.path === defaultMenu.path)
+
+    let activeMenu = reactive({ path: '' })
+    let menuList = ref(tabsHook.getItem())
+    if (menuList.value.length === 0) { // 判断之前有没有调用过
+      addMenu(defaultMenu)
+    } 
+    watch(menuList.value, (newVal) => {
+      tabsHook.setItem(newVal)
+    })
+    watch(menuList, (newVal) => {
+      tabsHook.setItem(newVal)
+    })
+    router.afterEach(() => {
+      addMenu(route)
+      initMenu(route)
+    })
+
+    // 全屏
+    function onFullscreen() {
+      store.commit('app/contentFullScreenChange', !contentFullScreen.value)
+    }
+    // 当前页面组件重新加载
+    function pageReload() {
+      const self = route.matched[route.matched.length-1].instances.default
+      // console.log(route.matched);
+      
+      self.handleReload();
+      // const { fullPath, meta, name } = unref(route);
+      // if (meta.cache && name) {
+      //   store.commit('keepAlive/delKeepAliveComponentsName', name)
+      // }
+      // router.replace({
+      //   path: "/redirect" + fullPath
+      // });
+    }
+
+    // 关闭当前标签,首页不关闭
+    function closeCurrentRoute() {
+      if (route.path !== defaultMenu.path) {
+        delMenu(route)
+      }
+    }
+    // 关闭除了当前标签之外的所有标签
+    function closeOtherRoute() {
+      menuList.value = [defaultMenu]
+      if (route.path !== defaultMenu.path) {
+        addMenu(route)
+      }
+      setKeepAliveData()
+    }
+
+    // 关闭所有的标签,除了首页
+    function closeAllRoute() {
+      menuList.value = [defaultMenu]
+      setKeepAliveData()
+      router.push(defaultMenu.path)
+    }
+
+    // 添加新的菜单项
+    function addMenu(menu) {
+      let { path, meta, name } = menu
+      if (meta.hideTabs) {
+        return
+      }
+      let hasMenu = menuList.value.some((obj) => {
+        return obj.path === path
+      })
+      if (!hasMenu) {
+        menuList.value.push({
+          path,
+          meta,
+          name
+        })
+      }
+    }
+
+    // 删除菜单项
+    function delMenu(menu) {
+      let index = 0
+      if (!menu.meta.hideClose) {
+        if (menu.meta.cache && menu.name) {
+          store.commit('keepAlive/delKeepAliveComponentsName', menu.name)
+        }
+        index = menuList.value.findIndex((item) => item.path === menu.path)
+        menuList.value.splice(index, 1)
+      }
+      if (menu.path === activeMenu.path) {
+        index - 1 > 0 ? router.push(menuList.value[index - 1].path) : router.push(defaultMenu.path)
+      }
+    }
+
+    // 初始化activeMenu
+    function initMenu(menu) {
+      activeMenu = menu
+      nextTick(() => {
+        // setPosition()
+      })
+    }
+    // 设置当前滚动条应该在的位置
+    function setPosition() {
+      console.log('查看当前什么值',scrollbarDom);
+      if (scrollbarDom.value) {
+        const domBox = {
+          scrollbar: scrollbarDom.value.scrollbar.querySelector('.el-scrollbar__wrap '),
+          activeDom: scrollbarDom.value.scrollbar.querySelector('.active'),
+          activeFather: scrollbarDom.value.scrollbar.querySelector('.el-scrollbar__view')
+        }
+        for (let i in domBox) {
+          if (!domBox[i]) {
+            return
+          }
+        }
+        const domData = {
+          scrollbar: domBox.scrollbar.getBoundingClientRect(),
+          activeDom: domBox.activeDom.getBoundingClientRect(),
+          activeFather: domBox.activeFather.getBoundingClientRect()
+        }
+        const num = domData.activeDom.x - domData.activeFather.x + 1/2 * domData.activeDom.width - 1/2 * domData.scrollbar.width
+        domBox.scrollbar.scrollLeft = num
+      }
+    }
+
+    // 配置需要缓存的数据
+    function setKeepAliveData() {
+      let keepAliveNames = []
+      menuList.value.forEach((menu) => {
+        menu.meta && menu.meta.cache && menu.name && keepAliveNames.push(menu.name)
+      })
+      store.commit('keepAlive/setKeepAliveComponentsName', keepAliveNames)
+    }
+    
+    // 初始化时调用:1. 新增菜单 2. 初始化activeMenu
+    addMenu(route)
+    initMenu(route)
+    return {
+      contentFullScreen,
+      onFullscreen,
+      pageReload,
+      scrollbarDom,
+      // 菜单相关
+      menuList,
+      activeMenu,
+      delMenu,
+      closeCurrentRoute,
+      closeOtherRoute,
+      closeAllRoute,
+      currentDisabled
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .tabs {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 40px;
+    background: var(--system-header-background);
+    border-bottom: 1px solid var(--system-header-border-color);
+    border-top: 1px solid var(--system-header-border-color);
+    border-left: 1px solid var(--system-header-border-color);
+    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .1);
+    .handle {
+      min-width: 95px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      .el-dropdown-link {
+        margin-top: 5px;
+        border-left: 1px solid var(--system-header-border-color);
+        height: 25px;
+        width: 40px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+      i {
+        color: var(--system-header-text-color);
+      }
+    }
+  }
+  .scroll-container {
+    white-space: nowrap;
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    :deep {
+      .el-scrollbar__bar {
+        bottom: 0px;
+      }
+      .el-scrollbar__wrap {
+        height: 49px;
+      }
+    }
+  }
+  .tags-view-container {
+    height: 34px;
+    flex: 1;
+    width: 100%;
+    display: flex;
+  }
+  .el-icon-full-screen {
+    cursor: pointer;
+    &:hover {
+      background: rgba(0,0,0,.025);
+    }
+    &:focus {
+      outline: none;
+    }
+  }
+</style>

+ 100 - 0
src/layout/Tabs/item.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="tags-view-item" :class="active? 'active' : ''">
+    <router-link :to="menu.path" v-if="menu.meta.title">
+      {{ menu.meta.title }}
+    </router-link>
+    <i class="iconfont icon-shuaxin"  @click.stop="reload" v-if="active" style="font-size:14px;"></i>
+    <i class="iconfont icon-guanbi1"  @click.stop="closeTab" v-if="!menu.meta.hideClose" alt="关闭当前标签页" style="font-size:14px;"></i>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  props: {
+    menu: {
+      type: Object,
+      default: () => {
+        return {
+          path: '',
+          meta: {
+            label: '',
+            hideClose: false
+          }
+        }
+      }
+    },
+    active: {
+      type: Boolean,
+      default: false
+    }
+  },
+  setup(props, { emit }) {
+    // 关闭按钮
+    function closeTab() {
+      emit('close')
+    }
+    // 刷新按钮
+    function reload() {
+      emit('reload')
+    }
+    return {
+      closeTab,
+      reload
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .tags-view-item {
+    display: inline-block;
+    position: relative;
+    cursor: pointer;
+    height: 24px;
+    line-height: 24px;
+    border: 1px solid var(--system-header-border-color);
+    color: var(--system-header-text-color);
+    background: var(--system-header-tab-background);
+    padding: 0 8px;
+    font-size: 12px;
+    margin-left: 5px;
+    margin-top: 4px;
+    border-radius: 2px;
+    a {
+      color: var(--system-header-text-color);
+      height: 26px;
+      display: inline-block;
+      padding-left: 8px;
+      padding-right: 8px;
+    }
+    .el-icon-refresh-right {
+      display: inline-block;
+      margin-right: 5px;
+    }
+    .el-icon-close {
+      display: inline-block;
+      height: 26px;
+    }
+    &:first-of-type {
+      margin-left: 15px;
+    }
+    &:last-of-type {
+      margin-right: 15px;
+    }
+    &.active {
+      background: var(--system-primary-color);
+      border-color: var(--system-primary-color);
+      color: var(--system-primary-text-color);
+      a {
+        color: var(--system-primary-text-color);
+      }
+      &:hover {
+        background: var(--system-primary-color);
+      }
+    }
+    &:hover {
+      background-color: var(--system-header-item-hover-color);
+    }
+  }
+</style>

+ 9 - 0
src/layout/Tabs/tabsHook.js

@@ -0,0 +1,9 @@
+const tabsHook = {
+  setItem: function(arr) {
+    localStorage.setItem('tabs', JSON.stringify(arr))
+  },
+  getItem: function() {
+    return JSON.parse(localStorage.getItem('tabs') || '[]')
+  }
+}
+export default tabsHook

+ 134 - 0
src/layout/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <el-container style="height: 100vh">
+    <div class="mask" v-show="!isCollapse && !contentFullScreen" @click="hideMenu"></div>
+    <el-aside
+      :width="isCollapse ? '60px' : '250px'"
+      :class="isCollapse ? 'hide-aside' : 'show-side'"
+      v-show="!contentFullScreen"
+    >
+      <Logo v-if="showLogo" />
+      <Menu />
+    </el-aside>
+    <el-container>
+      <el-header v-show="!contentFullScreen">
+        <Header />
+      </el-header>
+      <Tabs v-show="showTabs" />
+      <el-main>
+        <router-view v-slot="{ Component, route }">
+          <transition :name="route.meta.transition || 'fade-transform'" mode="out-in">
+            <keep-alive v-if="keepAliveComponentsName" :include="keepAliveComponentsName">
+              <component :is="Component" :key="route.fullPath" />
+            </keep-alive>
+            <component v-else :is="Component" :key="route.fullPath" />
+          </transition>
+        </router-view>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import { defineComponent, computed, onBeforeMount } from "vue";
+import { useStore } from "vuex";
+import { useRouter } from "vue-router";
+import { useEventListener } from "@vueuse/core";
+import Menu from "./Menu/index.vue";
+import Logo from "./Logo/index.vue";
+import Header from "./Header/index.vue";
+import Tabs from "./Tabs/index.vue";
+export default defineComponent({
+  components: {
+    Menu,
+    Logo,
+    Header,
+    Tabs,
+  },
+  setup() {
+    const store = useStore();
+    // computed
+    const isCollapse = computed(() => store.state.app.isCollapse);
+    const contentFullScreen = computed(() => store.state.app.contentFullScreen);
+    const showLogo = computed(() => store.state.app.showLogo);
+    const showTabs = computed(() => store.state.app.showTabs);
+    const keepAliveComponentsName = computed(() => store.getters['keepAlive/keepAliveComponentsName']);
+    // 页面宽度变化监听后执行的方法
+    const resizeHandler = () => {
+      if (document.body.clientWidth <= 1000 && !isCollapse.value) {
+        store.commit("app/isCollapseChange", true);
+      } else if (document.body.clientWidth > 1000 && isCollapse.value) {
+        store.commit("app/isCollapseChange", false);
+      }
+    };
+    // 初始化调用
+    resizeHandler();
+    // beforeMount
+    onBeforeMount(() => {
+      // 监听页面变化
+      useEventListener("resize", resizeHandler);
+    });
+    // methods
+    // 隐藏菜单
+    const hideMenu = () => {
+      store.commit("app/isCollapseChange", true);
+    };
+    return {
+      isCollapse,
+      showLogo,
+      showTabs,
+      contentFullScreen,
+      keepAliveComponentsName,
+      hideMenu,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.el-header {
+  padding-left: 0;
+  padding-right: 0;
+}
+.el-aside {
+  display: flex;
+  flex-direction: column;
+  transition: 0.2s;
+  overflow-x: hidden;
+  transition: 0.3s;
+  &::-webkit-scrollbar {
+    width: 0 !important;
+  }
+}
+.el-main {
+  background-color: var(--system-container-background);
+  height: 100%;
+  padding: 0;
+}
+.el-main-box {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  box-sizing: border-box;
+}
+@media screen and (max-width: 1000px) {
+  .el-aside {
+    position: fixed;
+    top: 0;
+    left: 0;
+    height: 100vh;
+    z-index: 1000;
+    &.hide-aside {
+      left: -250px;
+    }
+  }
+  .mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    z-index: 999;
+    background: rgba(0, 0, 0, 0.5);
+  }
+}
+</style>

+ 21 - 0
src/main.js

@@ -0,0 +1,21 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import store from './store'
+import router from './router'
+// 引入iview
+import ViewUIPlus from 'view-ui-plus'
+import 'view-ui-plus/dist/styles/viewuiplus.css'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import 'normalize.css' // css初始化
+import './assets/style/common.scss' // 公共css
+import '@/assets/iconfont/iconfont.css'
+
+
+const app = createApp(App)
+app.use(store)
+// app.config.performance = true
+app.use(ViewUIPlus)
+app.use(ElementPlus, { size: store.state.app.elementSize })
+
+app.use(router).mount('#app')

+ 11 - 0
src/request/api.js

@@ -0,0 +1,11 @@
+
+import * as apiUser from './modules/apiUser'    //用户服务系统
+import * as apiContent  from './modules/apiContent' //内容服务系统
+import * as apiCow  from './modules/apiCow' //牛只管理
+import * as apiOrder  from './modules/apiOrder' //牛只订单
+
+
+export const user = apiUser
+export const content  = apiContent
+export const cow  = apiCow
+export const order  = apiOrder

+ 31 - 0
src/request/apiConfig.js

@@ -0,0 +1,31 @@
+// 接口配置文件
+let env = process.env.NODE_ENV
+let api
+if (env === 'development') {  //开发环境
+  api = {
+    apiUser: 'http://account2.aiyangniu.net',
+    apiContent: 'http://contents2.aiyangniu.net',
+    apiManager: 'http://manager2.aiyangniu.net',
+    apiCow: 'http://cow.aiyangniu.net',
+    apiOrder: 'http://orders2.aiyangniu.net',
+  }
+} else if (env === 'testing') {
+  api = {
+    apiUser: 'http://account2.aiyangniu.net',
+    apiContent: 'http://contents2.aiyangniu.net',
+    apiManager: 'http://manager2.aiyangniu.net', 
+    apiCow: 'http://cow.aiyangniu.net',
+    apiOrder: 'http://orders2.aiyangniu.net',
+  }
+} else { //生产环境
+  api = {
+    apiUser: 'http://account.aiyangniu.cn',
+    apiContent: 'http://contents.aiyangniu.cn',
+    apiManager: 'http://manager.aiyangniu.cn',
+    apiCow: 'http://cow.aiyangniu.cn',
+    apiOrder: 'http://orders.aiyangniu.cn',
+  }
+}
+
+
+export default api

+ 132 - 0
src/request/modules/apiContent.js

@@ -0,0 +1,132 @@
+/**
+ * 内容服务系统
+ * apiContent:'http://10.162.16.5:8070'  //内容服务系统
+ */
+ import res from '@/utils/system/request.js'
+
+/* 获取内容列表 */
+export const GetContent = (params) => res('get', 'apiContent', '/contents/{contentTypeId}/list', {...params,params})
+
+/* 获取楼层广告数据 */
+export const GetAdvertList = (params) => res('get','apiContent', '/manager/publicitys/onLineList', {params:params})
+
+/* M003获取楼层数据 */
+export const GetFloorList = (params) => res('get','apiContent', '/manager/floors/list/'+params.status, {params:params})
+/* M011根据楼层获取楼层数据 */
+export const GetFloorListById = (params) => res('get','apiContent', '/manager/floors/'+params.id, {params:params})
+
+//根据ID获取广告详情
+export const GetAdvertDetail = (params) => res('get', 'apiContent', '/manager/publicitys/'+params.id, {params:params});
+
+//M002新增广告
+export const AddNewAdvert = (params) => res('post', 'apiContent', '/manager/publicitys/add', params);
+
+//M003更新广告信息
+export const UpdateAdvert = (params) => res('put', 'apiContent', '/manager/publicitys/update',params);
+
+//M001新增未上线的楼层广告
+export const AddFloor = (params) => res('post', 'apiContent', '/manager/floors/add', params);
+
+//M002删除未上线的楼层广告
+export const DeleteFloor = (params) => res('delete', 'apiContent', '/manager/floors/delete/'+params.id, {params});
+
+//M004楼层上线接口
+export const OnLine = (params) => res('put', 'apiContent', '/manager/floors/onLine/'+params.id,params, {form:true});
+
+//M005楼层下线接口
+export const OffLine = (params) => res('put', 'apiContent', '/manager/floors/offLine/'+params.id,params, {form:true});
+
+//M006更新楼层信息
+export const UpdateFloor = (params) => res('put', 'apiContent', '/manager/floors/update',params);
+
+//M008楼层下移接口
+export const DownMove = (params) => res('put', 'apiContent', '/manager/floors/moveDown/'+params.id,params, {form:true});
+
+//M009楼层上移接口
+export const TopMove = (params) => res('put', 'apiContent', '/manager/floors/moveUp/'+params.id,params, {form:true});
+
+//M005广告下移接口
+export const MoveDownAdvert = (params) => res('put', 'apiContent', '/manager/publicitys/moveDown/'+params.id,params, {form:true});
+
+//M005广告上移接口
+export const MoveUpAdvert = (params) => res('put', 'apiContent', '/manager/publicitys/moveUp/'+params.id,params, {form:true});
+
+//M007获取楼层数接口
+export const GetFloorNum = (params) => res('get', 'apiContent', '/manager/floors/getOnLineCount',params);
+
+//M010全部楼层接口
+export const GetFloorAll = (params) => res('get', 'apiContent', '/manager/floors/list/all',params);
+
+//Moo4广告库查询接口
+export const AdvertsList = (params) => res('get','apiContent','/manager/publicitys/library',{params});
+
+//Moo1删除楼层广告接口
+export const DeleteAdverts= (params) => res('delete','apiContent','/manager/publicitys/delete/'+params.id,{params});
+
+/**
+  * 内容管理
+  * contents : Content Controller
+  */
+/* 获取内容列表 */
+export const GetContentList = (params) => res('get','apiContent', '/manager/contents/list', {params:params})
+
+//根据ID获取内容详情
+export const GetContentDetail = (params) => res('get', 'apiContent', '/manager/contents/list/'+params.id, {params:params});
+
+//删除内容接口
+export const DeleteContent= (params) => res('delete','apiContent','/manager/contents/delete/'+params.id,{params});
+
+//删除内容类别接口
+export const DeleteConType= (params) => res('delete','apiContent','/manager/contentTypes/delete',{params});
+
+//审核内容接口
+export const AuditContent = (params) => res('put', 'apiContent', '/manager/contents/audit/'+params.id,params, {form:true});
+
+/* 获取内容类型列表 */
+export const GetContentTypeList = (params) => res('get','apiContent', '/manager/contentTypes/list', {params:params})
+
+//根据ID获取内容类型详情
+export const GetContentTypeDetail = (params) => res('get', 'apiContent', '/manager/contentTypes/list/{id}', {params:params});
+
+//新增内容
+export const AddNewContent = (params) => res('post', 'apiContent', '/manager/contents/add', params);
+
+//更新内容信息
+export const UpdateContent = (params) => res('put', 'apiContent', '/manager/contents/update',params);
+
+//新增内容类别
+export const AddNewConType = (params) => res('post', 'apiContent', '/manager/contentTypes/add', params);
+
+//更新内容类别信息
+export const UpdateConType = (params) => res('put', 'apiContent', '/manager/contentTypes/update',params);
+
+//获取APP启动页图片
+export const getBootStartImage = (params) => res('get', 'apiContent', '/bootStart/getBootStartImage', {params:params});
+//获取APP启动页图片列表
+export const getBootStartImageList = (params) => res('get', 'apiContent', '/bootStart/getBootStartImageList', {params:params});
+
+//保存APP启动页图片
+export const saveBootStartImage = (params) => res('post', 'apiContent', '/bootStart/saveBootStartImage', params);
+
+//获取文章分类列表
+export const articleClassifyList = (params) => res('get', 'apiContent', '/article/classify/list', {params:params});
+//新增文章分类
+export const addArticleClassify = (params) => res('post', 'apiContent', '/article/classify/add',params);
+//修改文章分类
+export const updateArticleClassify = (params) => res('post', 'apiContent', '/article/classify/update',params);
+//删除文章分类
+export const deleteArticleClassify = (params) => res('post', 'apiContent', '/article/classify/delete',params, {form:true});
+//获取一级文章分类
+export const getClassifyoneList = (params) => res('get', 'apiContent', '/article/classify/getClassifyoneList', {params:params});
+//获取三级文章分类
+export const getClassifyList = (params) => res('get', 'apiContent', '/article/classify/getClassifyList', {params:params});
+//文章信息列表
+export const articleList = (params) => res('get', 'apiContent', '/article/list', {params:params});
+//新增文章内容
+export const addArticle = (params) => res('post', 'apiContent', '/article/add',params);
+//更新文章信息
+export const updateArticle = (params) => res('post', 'apiContent', '/article/update',params);
+//删除文章信息
+export const deleteArticle = (params) => res('post', 'apiContent', '/article/delete',params, {form:true});
+//获取文章详情
+export const getArticleDetail = (params) => res('get', 'apiContent', '/article/list/'+params.id,{params:params});

+ 49 - 0
src/request/modules/apiCow.js

@@ -0,0 +1,49 @@
+import res from '@/utils/system/request.js'
+
+// 获取全部分类接口
+export const getCattleClassify = (params) => res('get', 'apiCow', '/manager/cattleClassify/listTree')
+// 新增牛只分类信息
+export const addCowClassify = (params) => res('post', 'apiCow', '/manager/cattleClassify/add',params)
+// 删除牛只分类信息
+export const deleteCowClassify = (params) => res('delete', 'apiCow', '/manager/cattleClassify/delete',{params:params})
+// 编辑牛只分类信息
+export const updateCowClassify = (params) => res('post', 'apiCow', '/manager/cattleClassify/update',params)
+// 新增牛只接口
+export const saveCattleItem = (params) => res('post', 'apiCow', '/manager/cattleItem/saveCattleItem',params)
+// 获取牛只重量范围列表
+export const getCattleWeightScopes = (params) => res('get', 'apiCow', '/manager/cattleItem/getCattleWeightScopes',{params:params})
+
+// 牛只商品分页查询
+export const getCattleItemPage = (params) => res('get', 'apiCow', '/manager/cattleItem/getCattleItemPage',{params:params})
+// 牛只商品下架 
+export const setOffSale = (params) => res('put', 'apiCow', '/manager/cattleItem/sku/offSale',params);
+// 牛只商品上架
+export const setToSale = (params) => res('put', 'apiCow', '/manager/cattleItem/sku/toSale',params);
+// 获取牛只详情 
+export const getCattleSpu = (params) => res('get', 'apiCow', '/manager/cattleItem/getCattleSpu',{params:params})
+// 编辑牛只商品 
+export const updateCattleSpu = (params) => res('post', 'apiCow', '/manager/cattleItem/updateCattleSpu',params)
+// 删除牛只商品 
+export const updateCattleSku = (params) => res('post', 'apiCow', '/manager/cattleItem/updateCattleSku',params)
+
+// 订单
+// 查询全部订单
+export const getCattleOrderPage = (params) => res('get', 'apiCow', '/manager/cattle/order/getCattleOrderPage',{params:params})
+
+//意向用户列表 
+export const potentialUserListPage = (params) => res('get','apiCow', '/cattle/potential/potentialUserListPage', {params:params})
+// 修改意向客户联系状态
+export const updatePotentialStatus = (params) => res('post', 'apiCow', '/cattle/potential/update', params);
+
+//查询订单详情 
+export const getCattleOrderInfo = (params) => res('get','apiCow', '/manager/cattle/order/getCattleOrderInfo', {params:params})
+// 发起改价 
+export const UpdatePrice = (params) => res('post', 'apiCow', '/manager/cattle/order/cattleOrderUpdatePrice', params);
+// 确认已看牛 
+export const confirmAppointCattle = (params) => res('post', 'apiCow', '/manager/cattle/order/confirmAppointCattle', params,{form:true});
+// 确认支付尾款 
+export const confirmLastPay = (params) => res('post', 'apiCow', '/manager/cattle/order/confirmLastPay', params,{form:true});
+
+
+
+

+ 19 - 0
src/request/modules/apiOrder.js

@@ -0,0 +1,19 @@
+import res from '@/utils/system/request.js'
+
+// 审核售后订单
+export const cattleAudit = (params) => res('post', 'apiOrder', '/cattle/afterSale/audit', params)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 21 - 0
src/request/modules/apiUser.js

@@ -0,0 +1,21 @@
+import res from '@/utils/system/request.js'
+
+// 获取token
+export const GetUserToken = (params) => res('post', 'apiManager', '/manager/login', params, {form: true})
+
+// 用户信息读取接口
+export const GetUserInfo = () => res('get', 'apiManager', '/manager/managerUser',{})
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 46 - 0
src/router/createNode.js

@@ -0,0 +1,46 @@
+// 1. 用于解决keep-alive需要name的问题,动态生成随机name供keep-alive使用
+// 2. 用于解决transition动画内部结点只能为根元素的问题,单文件可写多结点
+import { defineComponent, h, createVNode, ref, nextTick } from 'vue'
+import reload from './reload.vue'
+import NProgress from '@/utils/system/nprogress'
+
+export function createNameComponent(component) {
+  return () => {
+    return new Promise((res) => {
+      component().then((comm) => {
+        const name = (comm.default.name || 'vueAdminBox') + '$' + Date.now();
+        const tempComm = defineComponent({
+          name,
+          setup() {
+            const isReload = ref(false);
+            let timeOut = null;
+            const handleReload = () => {
+              isReload.value = true;
+              timeOut && clearTimeout(timeOut);
+              NProgress.start();
+              timeOut = setTimeout(() => {
+                nextTick(() => {
+                  NProgress.done();
+                  isReload.value = false;
+                });
+              }, 260);
+            };
+            return {
+              isReload,
+              handleReload
+            };
+          },
+          render: function () {
+            if (this.isReload) {
+              return h('div', { class: 'el-main-box' }, [h(reload)]);
+            } else {
+              return h('div', { class: 'el-main-box' }, [createVNode(comm.default)]);
+            }
+          }
+        });
+        res(tempComm);
+      });
+    });
+  };
+}
+

+ 61 - 0
src/router/index.js

@@ -0,0 +1,61 @@
+/**
+ * @description 所有人可使用的参数配置列表
+ * @params hideMenu: 是否隐藏当前路由结点不在导航中展示
+ * @params alwayShow: 只有一个子路由时是否总是展示菜单,默认false
+ */
+import { createRouter, createWebHashHistory } from 'vue-router'
+import store from '@/store'
+import NProgress from '@/utils/system/nprogress'
+import { changeTitle } from '@/utils/system/title'
+
+// 动态路由相关引入数据
+import Layout from '@/layout/index.vue'
+import MenuBox from '@/components/menu/index.vue'
+import { createNameComponent } from './createNode'
+
+// 引入modules
+import Dashboard from './modules/dashboard'
+import Pages from './modules/pages'
+import System from './modules/system'
+
+let modules = [
+  ...System,
+  ...Dashboard,
+  ...Pages,
+]
+
+const routes = modules
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+const whiteList = ['/login']
+
+router.beforeEach((to, _from, next) => {
+  NProgress.start();
+  if (store.state.user.userToken || whiteList.indexOf(to.path) !== -1) {
+    to.meta.title ? (changeTitle(to.meta.title)) : ""; // 动态title
+    next()
+  } else {
+    next("/login"); // 全部重定向到登录页
+    to.meta.title ? (changeTitle(to.meta.title)) : ""; // 动态title
+  }
+});
+
+// 路由进入后出发守卫
+router.afterEach((to, _from) => {
+  const keepAliveComponentsName = store.getters['keepAlive/keepAliveComponentsName'] || []
+  const name = to.matched[to.matched.length - 1].components.default.name
+  if (to.meta && to.meta.cache && name && !keepAliveComponentsName.includes(name)) {
+    store.commit('keepAlive/addKeepAliveComponentsName', name)
+  }
+  NProgress.done();
+});
+
+export {
+  modules
+}
+
+export default router

+ 32 - 0
src/router/modules/dashboard.js

@@ -0,0 +1,32 @@
+import Layout from '@/layout/index.vue'
+import { createNameComponent } from '../createNode'
+const route = [
+  // {
+  //   path: '/',
+  //   component: Layout,
+  //   redirect: '/dashboard',
+  //   meta: { title: '', icon: 'icon-bingtutongji' },
+  //   children: [
+  //     {
+  //       path: 'dashboard',
+  //       component: createNameComponent(() => import('@/views/dashboard/index.vue')),
+  //       meta: { title: '数据看板', icon: 'icon-bingtutongji', hideClose: true }
+  //     }
+  //   ]
+  // }
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/classifyManageIndex',
+    meta: { title: '', icon: 'icon-bingtutongji' },
+    children: [
+      {
+        path: 'classifyManageIndex',
+        component: createNameComponent(() => import('@/views/classifyManage/classifyManageIndex.vue')),
+        meta: { title: '牛只分类', icon: 'icon-yingyong', hideClose: true }
+      }
+    ]
+  }
+]
+
+export default route

+ 24 - 0
src/router/modules/pages.js

@@ -0,0 +1,24 @@
+import Layout from '@/layout/index.vue'
+import { createNameComponent } from '../createNode'
+const route = [
+  {
+    path: '/orderManage',
+    component: Layout,
+    redirect: 'orderManage/orderManageIndex',
+    meta: { title: '牛只订单', icon: 'icon-shu' },
+    children: [
+      {
+        path: 'orderManage/orderManageIndex',
+        component: createNameComponent(() => import('@/views/orderManage/orderManageIndex.vue')),
+        meta: { title: '牛只订单管理',icon: 'icon-dingdandingdanmingxishouzhimingxi', cache: false},
+      },
+      {
+        path: 'orderManage/orderDetail',
+        component: createNameComponent(() => import('@/views/orderManage/orderDetail.vue')),
+        meta: { title: '牛只订单详情',hideMenuItem: true, cache: false,},
+      }
+    ]
+  },
+]
+
+export default route

+ 43 - 0
src/router/modules/system.js

@@ -0,0 +1,43 @@
+import Layout from '@/layout/index.vue'
+import { createNameComponent } from '../createNode'
+const route = [
+  {
+    path: '/system',
+    component: Layout,
+    redirect: '/404',
+    hideMenu: true,
+    meta: { title: '系统目录' },
+    children: [
+      {
+        path: '/404',
+        component: createNameComponent(() => import('@/views/system/404.vue')),
+        meta: { title: '404', hideTabs: true }
+      },
+      {
+        path: '/401',
+        component: createNameComponent(() => import('@/views/system/401.vue')),
+        meta: { title: '401', hideTabs: true }
+      },
+      {
+        path: '/redirect/:path(.*)',
+        component: createNameComponent(() => import('@/views/system/redirect.vue')),
+        meta: { title: 'redirect', hideTabs: true }
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: createNameComponent(() => import('@/views/system/login.vue')),
+    hideMenu: true,
+    meta: { title: '登录', hideTabs: true }
+  },
+  {
+    // 找不到路由重定向到404页面
+    path: "/:pathMatch(.*)",
+    component: Layout,
+    redirect: "/404",
+    hideMenu: true
+  },
+]
+
+export default route

+ 116 - 0
src/router/reload.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="ok-loading">
+    <div class="ball-loader"> <span></span><span></span><span></span><span></span> </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+export default defineComponent({
+  name: 'routerReload',
+  setup() {}
+});
+</script>
+
+<style lang="scss">
+.ok-loading {
+  width: calc(100% - 30px);
+  height: calc(100% - 30px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color:  var(--system-page-background);
+  margin: 15px;
+}
+.ok-loading.close {
+  animation: close 1s;
+  -webkit-animation: close 1s;
+  animation-fill-mode: forwards;
+}
+.ball-loader {
+
+}
+.ball-loader > span,
+.signal-loader > span {
+  background-color: var(--system-primary-color);
+  display: inline-block;
+}
+
+.ball-loader > span:nth-child(1),
+.ball-loader.sm > span:nth-child(1),
+.signal-loader > span:nth-child(1),
+.signal-loader.sm > span:nth-child(1) {
+  -webkit-animation-delay: 0s;
+  animation-delay: 0s;
+}
+.ball-loader > span:nth-child(2),
+.ball-loader.sm > span:nth-child(2),
+.signal-loader > span:nth-child(2),
+.signal-loader.sm > span:nth-child(2) {
+  -webkit-animation-delay: 0.1s;
+  animation-delay: 0.1s;
+}
+.ball-loader > span:nth-child(3),
+.ball-loader.sm > span:nth-child(3),
+.signal-loader > span:nth-child(3),
+.signal-loader.sm > span:nth-child(3) {
+  -webkit-animation-delay: 0.15s;
+  animation-delay: 0.15s;
+}
+.ball-loader > span:nth-child(4),
+.ball-loader.sm > span:nth-child(4),
+.signal-loader > span:nth-child(4),
+.signal-loader.sm > span:nth-child(4) {
+  -webkit-animation-delay: 0.2s;
+  animation-delay: 0.2s;
+}
+.ball-loader > span {
+  width: 20px;
+  height: 20px;
+  margin: 0 3px;
+  border-radius: 50%;
+  transform: scale(0);
+  -ms-transform: scale(0);
+  -webkit-transform: scale(0);
+  animation: ball-load 1s ease-in-out infinite;
+  -webkit-animation: 1s ball-load ease-in-out infinite;
+}
+@-webkit-keyframes ball-load {
+  0% {
+    transform: scale(0);
+    -webkit-transform: scale(0);
+  }
+  50% {
+    transform: scale(1);
+    -webkit-transform: scale(1);
+  }
+  100% {
+    transform: scale(0);
+    -webkit-transform: scale(0);
+  }
+}
+@keyframes ball-load {
+  0% {
+    transform: scale(0);
+    -webkit-transform: scale(0);
+  }
+  50% {
+    transform: scale(1);
+    -webkit-transform: scale(1);
+  }
+  100% {
+    transform: scale(0);
+    -webkit-transform: scale(0);
+  }
+}
+@keyframes close {
+  0% {
+    opacity: 1;
+    /*display: block;*/
+  }
+  100% {
+    opacity: 0;
+    /*display: none;*/
+  }
+}
+</style>

+ 29 - 0
src/store/index.js

@@ -0,0 +1,29 @@
+import { createStore, createLogger } from 'vuex'
+import Presistent from './plugins/persistent'
+const debug = process.env.NODE_ENV !== 'production'
+
+const files = require.context("./modules/", true, /\.js$/)
+const map = {}
+for (const key of files.keys()) {
+  map[key] = files(key)
+}
+
+let modules = {}
+Object.keys(map).forEach((c) => {
+  const module = map[c].default
+  const moduleName = ('./modules/'+c.replace('./','')).replace(/^\.\/(.*)\/(.*)\.\w+$/, '$2')
+  modules[moduleName] = module
+})
+
+const presistent = Presistent({ key: 'vuex', modules, modulesKeys: {
+  local: Object.keys(modules),
+  session: []
+} })
+
+export default createStore({
+  modules: {
+    ...modules
+  },
+  strict: debug,
+  plugins: debug ? [createLogger(), presistent] : [presistent]
+})

+ 44 - 0
src/store/modules/app.js

@@ -0,0 +1,44 @@
+const state = () => ({
+  isCollapse: false, // 侧边栏是否收缩展示
+  contentFullScreen: false, // 内容是否可全屏展示
+  showLogo: true, // 是否显示Logo
+  fixedTop: false, // 是否固定顶部, todo,暂未使用
+  showTabs: true, // 是否显示导航历史
+  expandOneMenu: true, // 一次是否只能展开一个菜单
+  elementSize: 'default', // element默认尺寸,支持官网四个大小参数
+  theme: {
+    state: {
+      style: 'default',
+      primaryColor: '#409eff',
+      menuType: 'side'
+    }
+  }
+})
+
+// mutations
+const mutations = {
+  isCollapseChange(state, type) {
+    state.isCollapse = type
+  },
+  contentFullScreenChange(state, type) {
+    state.contentFullScreen = type
+  },
+  menuListChange(state, arr) {
+    state.menuList = arr
+  },
+  stateChange(state, option) {
+    state[option.name] = option.value
+  }
+}
+
+// actions
+const actions = {
+
+}
+
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 41 - 0
src/store/modules/keepAlive.js

@@ -0,0 +1,41 @@
+
+const state = () => ({
+  keepAliveComponentsName: [] // 需要缓存的组件名称
+})
+
+// mutations
+const mutations = {
+  // 重置,Push, splice keep-alive对象
+  setKeepAliveComponentsName(state, nameArr) {
+    state.keepAliveComponentsName = nameArr
+  },
+  addKeepAliveComponentsName(state, name) {
+    state.keepAliveComponentsName.push(name)
+  },
+  delKeepAliveComponentsName(state, name) {
+    const key = state.keepAliveComponentsName.indexOf(name)
+    if (key !== -1) {
+      state.keepAliveComponentsName.splice(key, 1)
+      console.log(state.keepAliveComponentsName)
+    }
+  }
+}
+
+const getters = {
+  keepAliveComponentsName(state) {
+    return state.keepAliveComponentsName
+  }
+}
+
+// actions
+const actions = {
+
+}
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations
+}

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

@@ -0,0 +1,79 @@
+import * as types from '../mutation-types'
+import {user} from '@/request/api'
+
+const state = () => ({
+  token: '', // 登录token
+  userInfo: {},  // 用户信息
+})
+
+// mutations
+const mutations = {
+
+  /* 用户登录,获取token */
+  [types.USER_GET_TOKEN](state, info) {
+    if (info.code == 101) {
+      state.userToken = info.data //token
+      localStorage.aynUserToken = info.data //把token放入本地存储
+      state.isGetToken = true
+    } else {
+      state.userToken = ''
+      localStorage.aynUserToken = ''
+      state.isGetToken = false
+    }
+    state.loginMessage = info.message //登录的提示信息
+  },
+  /* 用户登录,获取用户信息 */
+  [types.USER_GET_USERINFO](state, info) {
+    if (info.code == 101) {            
+      state.userInfo = info.data || {} //用户信息
+      console.log(state.userInfo);
+      state.loginStatus = true //登录状态
+    } else {
+      state.loginMessage = info.message //登录失败提示信息
+    }
+  },
+  /* 用户退出登录 */
+  [types.USER_LOGIN_EXIT](state) {
+    state.isGetToken = false
+    state.loginStatus = false
+    state.userToken = ''
+    state.userInfo = ''
+    localStorage.aynUserToken = ''
+  },
+
+}
+
+// actions
+const actions = {
+  /* 用户登录,获取token */
+  getUserToken: async({commit}, params) => {
+    let token = await user.GetUserToken(params) //获取token
+    await commit(types.USER_GET_TOKEN, token)
+  },
+
+  /* 获取用户信息 */
+  getUserInfo: async({commit }) => {
+    let result = await user.GetUserInfo() //获取用户信息
+    await commit(types.USER_GET_USERINFO, result)
+  },
+
+   /* 退出登录 */
+  loginOut: ({
+    commit
+  }) => {
+    commit(types.USER_LOGIN_EXIT)
+  }
+}
+
+const getters = {
+  userToken: state => state.userToken,
+  userInfo: state => state.userInfo,
+}
+
+export default {
+  namespaced: true,
+  state,
+  actions,
+  getters,
+  mutations
+}

+ 9 - 0
src/store/mutation-types.js

@@ -0,0 +1,9 @@
+
+
+// 登录注册
+
+export const USER_GET_TOKEN = 'USER_GET_TOKEN'
+export const USER_GET_USERINFO = 'USER_GET_USERINFO'
+export const USER_LOGIN_EXIT = 'USER_LOGIN_EXIT'
+export const USER_GET_LOGIN_BG = 'USER_GET_LOGIN_BG'
+export const USER_GET_USERTYPE = 'USER_GET_USERTYPE'

+ 39 - 0
src/store/plugins/persistent.js

@@ -0,0 +1,39 @@
+const exclude = ['actions', 'getters', 'mutations', 'namespaced']
+export default function Presistent({ key, modules, modulesKeys }) {
+  return (store) => {
+    const localOldState = JSON.parse(localStorage.getItem(key) || '{}')
+    const sessionOldState = JSON.parse(sessionStorage.getItem(key) || '{}')
+    let oldState = {}
+    Object.assign(oldState, localOldState, sessionOldState)
+    if (Object.keys(oldState).length > 0) {
+      for (const oldKey in oldState) {
+        modules[oldKey] = oldState[oldKey]
+      }
+      store.replaceState(modules)
+    }
+    store.subscribe((mutation, state) => {
+      // 判断是否需要缓存数据至localStorage
+      if (modulesKeys.local.length > 0) {
+        const localData = setData(store.state, modulesKeys.local)
+        localStorage.setItem(key, JSON.stringify(localData))
+      } else {
+        localStorage.removeItem(key)
+      }
+      // 判断是否需要缓存数据至sessionStorage
+      if (modulesKeys.session.length > 0) {
+        const sessionData = setData(store.state, modulesKeys.session)
+        sessionStorage.setItem(key, JSON.stringify(sessionData))
+      } else {
+        sessionStorage.removeItem(key)
+      }
+    })
+  }
+}
+
+function setData(state, module) {
+  let data = {}
+  for (const i of module) {
+    data[i] = state[i]
+  }
+  return data
+}

+ 99 - 0
src/theme/index.js

@@ -0,0 +1,99 @@
+
+export const style = {
+  'default': {
+    name: '默认菜单风格',
+    menu: {
+      textColor: '#bfcbd9',
+      background: '#515a6e',
+      childrenBackground: '#1f2d3d',
+      hoverBackground: '#203448',
+      submenuActiveColor: '#fff'
+    },
+    logo: {
+      color: '#f1f1f1',
+      background: '#263445'
+    },
+    header: {
+      background: '#fff',
+      textColor: '#303133',
+      itemHoverColor: 'rgba(0,0,0,.025)',
+      breadcrumbTextColor: '#606266',
+      borderColor: '#d8dce5',
+      tabBackground: '#fff'
+    },
+    container: {
+      background: '#f8f8f8',
+      mainBackground: '#fff'
+    },
+    page: {
+      background: '#fff',
+      color: '#303133',
+      tipColor: 'rgba(0, 0, 0, 0.45)',
+      borderColor: '#ebeef5'
+    }
+  },
+  'light': {
+    name: '亮色菜单风格',
+    menu: {
+      textColor: '#272727',
+      background: '#fff',
+      childrenBackground: '#fff',
+      hoverBackground: '#f1f1f1',
+      submenuActiveColor: 'var(--system-primary-color)'
+    },
+    logo: {
+      color: '#000',
+      background: '#fff'
+    },
+    header: {
+      background: '#fff',
+      textColor: '#303133',
+      itemHoverColor: 'rgba(0,0,0,.025)',
+      breadcrumbTextColor: '#606266',
+      borderColor: '#d8dce5',
+      tabBackground: '#fff'
+    },
+    container: {
+      background: '#f8f8f8',
+      mainBackground: '#fff'
+    },
+    page: {
+      background: '#fff',
+      color: '#303133',
+      tipColor: 'rgba(0, 0, 0, 0.45)',
+      borderColor: '#ebeef5'
+    }
+  },
+  'dark': {
+    name: '暗色菜单风格',
+    menu: {
+      textColor: '#bbb',
+      background: '#18181c',
+      childrenBackground: '#18181c',
+      hoverBackground: '#000',
+      submenuActiveColor: '#fff'
+    },
+    logo: {
+      color: '#fff',
+      background: '#18181c'
+    },
+    header: {
+      background: '#18181c',
+      textColor: '#e3e3e4',
+      itemHoverColor: '#000',
+      breadcrumbTextColor: '#fff',
+      borderColor: '#3e3e3e',
+      tabBackground: '#1b1b1b'
+    },
+    container: {
+      background: '#000',
+      mainBackground: '#18181c'
+    },
+    page: {
+      background: '#18181c',
+      color: '#c7c7c7',
+      tipColor: 'rgba(255, 255, 255, 0.45)',
+      borderColor: '#3e3e3e'
+    }
+  }
+}

+ 51 - 0
src/theme/index.scss

@@ -0,0 +1,51 @@
+:root {
+  // 主题色
+  --system-primary-color: #409eff; // 可做背景色和文本色,用做背景色时,需要和--system-primary-text-color配合使用,避免文件颜色和主题色冲突
+  --system-primary-text-color: #fff; // 主题色作为背景色时使用
+
+  // logo颜色相关
+  --system-logo-color: #f1f1f1;
+  --system-logo-background: #263445;
+
+  // 菜单颜色相关
+  --system-menu-text-color: #bfcbd9;
+  --system-menu-background: #28415a;
+  --system-menu-children-background: #1f2d3d;
+  --system-menu-submenu-active-color: #fff; 
+  --system-menu-hover-background: #203448;
+
+  // header区域
+  --system-header-background: #fff;
+  --system-header-text-color: #bbb;
+  --system-header-breadcrumb-text-color: #97a8be;
+  --system-header-item-hover-color: #000;
+  --system-header-border-color: #d8dce5;
+  --system-header-tab-background: #fff;
+
+  // contaier区域,父框架
+  --system-container-background: #f8f8f8;
+  --system-container-main-background: #fff;
+
+  // 页面区域, 这一块是你在自己写的文件中使用主题,核心需要关注的地方
+  --system-page-background: #fff; // 主背景
+  --system-page-color: #303133; // 主要的文本颜色
+  --system-page-tip-color: rgba(0, 0, 0, 0.45); // 协助展示的文本颜色
+  --system-page-border-color: #000; // 通用的边框配置色,便于主题扩展
+  
+  // element主题色修改
+  --el-color-primary: var(--system-primary-color);
+}
+
+// 进度条颜色修改为主题色
+body #nprogress .bar {
+  background-color: var(--system-primary-color);
+}
+body #nprogress .peg {
+  box-shadow: 0 0 10px var(--system-primary-color), 0 0 5px var(--system-primary-color);
+}
+body #nprogress .spinner-icon {
+  border-top-color: var(--system-primary-color);
+  border-left-color: var(--system-primary-color);
+}
+
+@import './modules/dark.scss';

+ 23 - 0
src/theme/modules/dark.scss

@@ -0,0 +1,23 @@
+.dark {
+  // 通用
+  p, h1, h2, h3, h4, h5, h6, article {
+    color: var(--system-page-color);
+  }
+  .el-tree {
+    background-color: var(--system-page-background);
+    .el-tree-node__content:hover {
+      background-color: #272727;
+    }
+    --el-color-primary-light-9: #272727;
+  }
+  .el-card {
+    background-color: var(--system-page-background);
+    color: var(--system-page-color);
+    border-color: var(--system-page-border-color);
+    .el-card__header {
+      border-color: var(--system-page-border-color);
+    }
+  }
+  // 页面内部样式修改
+  
+}

+ 9 - 0
src/utils/system/filters.js

@@ -0,0 +1,9 @@
+/**
+ * 自定义过滤器
+ */
+
+
+export{
+
+}
+

+ 12 - 0
src/utils/system/nprogress.js

@@ -0,0 +1,12 @@
+import NProgress from "nprogress"
+import "nprogress/nprogress.css"
+
+NProgress.configure({
+  easing: 'ease', // 动画方式    
+  speed: 500, // 递增进度条的速度    
+  showSpinner: true, // 是否显示加载ico    
+  trickleSpeed: 200, // 自动递增间隔    
+  minimum: 0.3 // 初始化时的最小百分比
+})
+
+export default NProgress

+ 98 - 0
src/utils/system/request.js

@@ -0,0 +1,98 @@
+import axios from 'axios'
+import store from '@/store'
+import api from '@/request/apiConfig' //真实接口配置
+import { ElMessage } from 'element-plus'
+import qs from 'qs'
+import router from '@/router/index.js'
+
+
+/**  axios基础配置 */
+axios.defaults.timeout = 50000;
+axios.defaults.headers['Content-Type'] = 'application/json;charset=UTF-8'
+
+// 请求拦截器
+axios.interceptors.request.use(
+  config => {
+    if (localStorage.aynUserToken) {
+      config.headers.common['X-AIYANGNIU-SIGNATURE'] = localStorage.aynUserToken;
+    }
+    return config;
+  },
+  err => {
+    return Promise.reject(err);
+  }
+);
+
+// 响应拦截器
+axios.interceptors.response.use(
+  response => {
+    if (response.data) {
+      let code = response.data.code
+      switch (code) {
+        case '1002': // 109 清除token信息并跳转到登录页面
+          localStorage.aynUserToken = ''
+          router.push('/login')
+          break;
+        case 110:
+          break;
+      }
+    }else{
+      showError({ code, message: response.data.code })
+    }
+    return response;
+  },
+  error => {
+    if (error.response) {
+      let status = error.response.status
+      switch (status) {
+        case 401: // 109 清除token信息并跳转到登录页面
+          localStorage.aynUserToken = ''
+          router.push('/login')
+          break;
+        case 403: // 109 清除token信息并跳转到登录页面
+          location.hash = "#/page403"
+          break;
+      }
+    }
+    let err = error.response ? (error.response.data || error.response) : (error.message || error)
+    return Promise.reject(err)
+  }
+);
+
+
+function showError(error) {
+  if (error.code === 403) {
+    // to re-login
+    store.dispatch('user/loginOut')
+  } else {
+    ElMessage({
+      message: error.msg || error.message || '服务异常',
+      type: 'error',
+      duration: 3 * 1000
+    })
+  }
+  
+}
+
+/**
+ * 二次封装axios请求
+ */
+ export default async(type, apiName, url, data, options) => {
+
+  let result = {}
+  let path = (apiName != '')? api[apiName] + url:url
+  
+  //post/put data转form
+  if(options && options.form){
+    let opt = {headers:{'Content-Type':'application/x-www-form-urlencoded'}}
+    data = qs.stringify(data)
+    options === {}?options.headers = opt.headers:options = opt
+  }
+
+  //axios
+  await axios[type](path,data,options).then((res) => {
+    result = res.data
+  })
+  
+  return result
+}

+ 39 - 0
src/utils/system/time.js

@@ -0,0 +1,39 @@
+// 用于格式化时间
+function formatDate(value, format) {
+    //value: 需要格式化的数据
+    //format: 指定格式 yyyy-MM-dd hh:mm:ss
+    let date = new Date(value);
+    // 获取年份
+    let year = date.getFullYear();
+  
+    if (/(y+)/.test(format)) {
+      // 获取匹配组的内容
+      let content = RegExp.$1;
+      format = format.replace(content, year.toString().slice(4 - content.length));
+    }
+  
+    let o = {
+      // y: date.getFullYear(),  // 用这一句也行,但只适用于四位数显示时候用
+      M: date.getMonth() + 1,
+      d: date.getDate(),
+      h: date.getHours(),
+      m: date.getMinutes(),
+      s: date.getSeconds()
+    };
+  
+    for (let key in o) {
+      // 构造动态正则
+      let reg = new RegExp(`(${key}+)`);
+  
+      if (reg.test(format)) {
+        // 获取匹配组的内容
+        let content = RegExp.$1;
+        let k = o[key] >= 10 ? o[key] : content.length == 2 ? '0' + o[key] : o[key];
+        format = format.replace(content, k);
+      }
+    }
+    return format;
+  }
+  
+  export default formatDate
+  

+ 6 - 0
src/utils/system/title.js

@@ -0,0 +1,6 @@
+// import { systemTitle } from '@/config'
+
+export function changeTitle(name) {
+  // document.title = `${name}-${systemTitle}`
+  document.title = '爱养牛-点点爱养牛-轻松养好牛'
+}

+ 32 - 0
src/utils/system/uploadCompress.js

@@ -0,0 +1,32 @@
+/*  time:20022.08.29
+**  classify:AYN Define Component
+**  author:zhangb
+**  description: 用于资源图片上传压缩,以提升终端性能,提升用户良性体验,该组件基于‘image-conversion’基础进行封装延展,视频文件不进行压缩,PNG由于无法改变分辨率,所以压缩后原型输出
+**  params: file -- 上传文件   ctimes -- 压缩比例值
+*/
+
+import * as imageConversion from 'image-conversion'; 
+
+export function compressFileM(file,ctimes) {
+  console.log('上传前的尺寸',file);
+    // 如果是视频不做压缩处理
+    if(file.type === 'video/mp4'){
+      return true
+    }
+    const isLt1M = (file.size / 1024 / 1024) * 10 < 1;
+    if (!isLt1M) {
+      let myImg = new Promise((resolve) => {
+        // 压缩值自定义,当前定义为原图的  1/<ctimes>
+        imageConversion.compressAccurately(file,{
+          size:file.size/1024/ctimes,  //指定压缩后的大小
+          type:file.type                 //指定压缩的后文件类型
+        }).then((res) => {
+          res = new File([res], file.name, { type: res.type, lastModified: Date.now() })
+          resolve(res);
+        });
+      });
+      console.log('今经过压缩处理的文件',myImg);
+      return myImg;
+    }
+  return isLt1M;
+}

+ 315 - 0
src/views/classifyManage/classifyManageIndex.vue

@@ -0,0 +1,315 @@
+/**
+  // author:zhangb
+  // time:2022-11-07
+  // desc:牛只分类管理
+*/
+<template>
+  <div class="classifyManage">
+    <!-- top -->
+    <div>
+      <Row>
+        <Col span="24" style="text-align:right;">
+          <Button type="primary" @click="addNewClassify">添加一级分类</Button>
+        </Col>
+      </Row>
+    </div>
+
+    <!-- tree content -->
+    <div class="tree_content">
+      <el-tree :data="dataSource" show-checkbox node-key="typeId" :props="classifyTreePropsSet" :expand-on-click-node="false" style="width:90%;">
+        <template #default="{ node, data }">
+          <div class="custom-tree-node">
+            <span>{{ node.data.name }}</span>
+            <span>输出顺序:{{node.data.sort || '暂无'}}</span>
+            <span style="text-align:right !important;">
+              <a v-show="node.level < 3" @click="addNextClassify(node,data)">添加下级分类</a>
+              <a v-show="node.level === 3" @click="optClassifyImg(node,data)">分类图片</a>
+              <a style="margin-left: 8px" @click="editTreeClassify(node,data)">编辑</a>
+              <a style="margin-left: 8px" @click="delClassidy(node,data)">删除</a>
+            </span>
+          </div>
+        </template>
+      </el-tree>
+    </div>
+
+    <!-- 新增分类信息 -->
+    <Modal v-model="optClassifyModal" :title="optClassifyModalTitle" width="30%">
+      <Tag v-show="optClassifyModalTitle == '添加下级分类'" color="primary" size="medium" style="margin-bottom:1em;">
+        当前上级:{{recordClassifyData.name}}
+      </Tag>
+      <Form ref="optClassifyFormRef" :model="optClassifyForm" :rules="optClassifyRule" :label-width="100">
+        <FormItem prop="name" label="分类名称:">
+          <Input v-model="optClassifyForm.name" placeholder="新增分类名称" style="width:80%;"></Input>
+        </FormItem>
+        <FormItem prop="sort" label="输出排序:">
+          <InputNumber :min="1" v-model="optClassifyForm.sort" placeholder="请输入输出顺序" style="width:80%;" />
+        </FormItem>
+      </Form>
+      <template #footer>
+        <div style="text-align:center;">
+          <Button style="margin-right:5em;" @click="optClassifyModal = false">取消操作</Button>
+          <Button type="primary" @click="submitOptClassify">确定提交</Button>
+        </div>
+      </template>
+    </Modal>
+
+    <!-- 三级分类显示图片 -->
+    <Modal v-model="optClassifyImgModal" :title="optClassifyModalTitle" width="30%">
+      <Tag color="primary" size="medium" style="margin-bottom:1em;">
+        当前分类:{{recordClassifyData.name}}
+      </Tag>
+
+      <div style="margin-left:1rem;">
+        <div style="margin:.5rem 0;">分类图片</div>
+        <Upload
+          type="drag"
+          :action="uploadUrl"
+          :headers="{'X-AIYANGNIU-SIGNATURE':token}"
+          :show-upload-list="false"
+          :on-success="handleSuccess"
+          :before-upload="handleBeforeUpload"
+          :format="['jpg','jpeg','png']"
+          :max-size="2048"
+          :on-format-error="handleFormatError"
+          style="width:100px; height:100px;">
+          <div v-if="!recordClassifyData.picUrl" style="width:100px; height:100px; line-height:100px; font-size:30px; color:#aaa;">
+            <span>+</span>
+          </div>
+          <div v-else style="width:100px; height:100px;"><img :src="recordClassifyData.picUrl" style="width:100%; height:100%;" alt=""></div>
+        </Upload>
+      </div>
+      
+      <template #footer>
+        <div style="text-align:center;">
+          <Button style="margin-right:5em;" @click="optClassifyImgModal = false">取消操作</Button>
+          <Button type="primary" @click="confirmSubClassifyImg">确定提交</Button>
+        </div>
+      </template>
+    </Modal>
+
+  </div>
+</template>
+
+<script>
+import { Message,Modal } from 'view-ui-plus'
+import { defineComponent,reactive,ref,getCurrentInstance, onMounted} from 'vue'
+import { useRouter,useRoute} from 'vue-router'
+import {compressFileM} from '@/utils/system/uploadCompress.js'
+import {cow} from '@/request/api'
+import api from '@/request/apiConfig' //真实接口配置
+export default defineComponent({
+  components:{
+    
+  },
+  setup() {
+    let {proxy} = getCurrentInstance();
+    const route = useRoute()
+    const router = useRouter()
+
+    const token = localStorage.aynUserToken
+    const uploadUrl = api.apiManager + '/manager/images/upload'
+
+    let dataSource = ref([])
+    const classifyTreePropsSet = {label:'name'}
+
+    let optClassifyModal = ref(false)
+    let optClassifyModalTitle = ref('新增分类')
+    let optClassifyForm = reactive({name:'',sort:null,reqRandom:''})
+    let optClassifyRule = ref({
+      name: [{ required: true, message: '请填写分类名称', trigger: 'blur' }],
+      sort: [{ required: true, type:'number', message: '请填写分类输出顺序', trigger: 'blur' }],
+    })
+
+    // 获取全部分类内容
+    async function getAllClassifyData(){
+      await cow.getCattleClassify({parentId:0}).then(res=>{
+        if(res.code === 101){
+          dataSource.value = res.data || []
+        }
+      })
+    }
+
+    // 新增一级分类
+    function addNewClassify(){
+      optClassifyForm.name = '',optClassifyForm.sort = ''
+      optClassifyModal.value = true
+      optClassifyModalTitle.value = '新增分类'
+    }
+
+    let recordClassifyData = ref({})  //当前操作分类
+    // 编辑数节点
+    function editTreeClassify(n,d){
+      recordClassifyData.value = JSON.parse(JSON.stringify(d))
+      optClassifyForm.name = d.name,optClassifyForm.sort = d.sort
+      optClassifyModal.value = true
+      optClassifyModalTitle.value = '编辑分类'
+    }
+
+    // 添加下级分类
+    function addNextClassify(n,d){
+      optClassifyForm.name = '',optClassifyForm.sort = ''
+      recordClassifyData.value = JSON.parse(JSON.stringify(d))
+      optClassifyModal.value = true
+      optClassifyModalTitle.value = '添加下级分类'
+      console.log('数据',recordClassifyData);
+    }
+
+    // 提交操作分类
+    function submitOptClassify(){
+      proxy.$refs['optClassifyFormRef'].validate(async (valid) => {
+        if (valid) {
+          let sendParams = JSON.parse(JSON.stringify(optClassifyForm))
+          let repFun = ''
+          // 新增一级分类
+          if(optClassifyModalTitle.value === '新增分类'){
+            sendParams.parentId = "0"
+            repFun = cow.addCowClassify
+          }
+          // 添加下级分类
+          else if(optClassifyModalTitle.value === '添加下级分类'){
+            sendParams.parentId = recordClassifyData.value.id
+            repFun = cow.addCowClassify
+          }
+          // 编辑分类
+          else if(optClassifyModalTitle.value === '编辑分类'){
+            console.log(recordClassifyData.value);
+            sendParams = JSON.parse(JSON.stringify(recordClassifyData.value))
+            sendParams.name = optClassifyForm.name
+            sendParams.sort = optClassifyForm.sort
+            repFun = cow.updateCowClassify
+          }
+          await repFun(sendParams).then(res=>{
+            if(res.code === 101){
+              optClassifyModal.value = false
+              Message.success({ content: res.retMsg || '操作成功'})
+              getAllClassifyData()
+            }else{
+              Message.error({content: res.retMsg || '操作失败'})
+            }
+          }).catch(e=>{
+            Message.error({content: e.retMsg || '操作失败'})
+          })
+        } else {
+          Message.warning({content: '请填写完整内容'})
+        }
+      })
+    }
+
+    // 删除分类
+    function delClassidy(n,d){
+      Modal.confirm({
+        title: '温馨提示',
+        content: `<p>您确定要删除 <b>${d.name}</b> 分类吗?</p>`,
+        onOk:async () => {
+          cow.deleteCowClassify({ids:d.id}).then(res=>{
+            if(res.code === 101){
+              Message.success(res.retMsg || '操作成功');
+              getAllClassifyData()
+            }else{
+              Message.error({content: res.msg || '操作失败'})
+            }
+          }).catch(e=>{
+            Message.error({content: e.msg || '操作失败'})
+          })
+        },
+        onCancel: () => {
+          Message.info('您已取消操作');
+        }
+      });
+    }
+
+    let optClassifyImgModal = ref(false)
+    // 操作分类图片
+    function optClassifyImg(n,d){
+      recordClassifyData.value = JSON.parse(JSON.stringify(d))
+      optClassifyImgModal.value = true
+    }
+
+    function handleBeforeUpload(file){
+      // 进行2.5倍系数压缩
+      return (compressFileM(file,2.5))
+    }
+
+    function handleSuccess (res, file) {
+      if(res.code === 101){
+        recordClassifyData.value.picUrl = res.data
+      }
+      console.log('结果输出',recordClassifyData.value);
+    }
+
+    function handleFormatError (file) {
+      this.$Notice.warning({
+          title: '温馨提示',
+          desc: '上传失败!'
+      });
+    }
+
+    async function confirmSubClassifyImg(){
+      let sendParams = JSON.parse(JSON.stringify(recordClassifyData.value))
+      await cow.updateCowClassify(sendParams).then(res=>{
+        if(res.code === 101){
+          Message.success(res.retMsg || '操作成功');
+          optClassifyImgModal.value = false
+          getAllClassifyData()
+        }else{
+          Message.error({content: res.msg || '操作失败'})
+        }
+      }).catch(e=>{
+        Message.error({content: e.msg || '操作失败'})
+      })
+    }
+    
+    // 初始化挂载
+    onMounted(async ()=>{
+      getAllClassifyData() //获取分类树
+    })
+
+    return {
+      dataSource,optClassifyModal,optClassifyModalTitle,optClassifyForm,optClassifyRule,recordClassifyData,
+      classifyTreePropsSet,optClassifyImgModal,uploadUrl,token,
+      editTreeClassify,addNewClassify,submitOptClassify,addNextClassify,delClassidy,optClassifyImg,handleSuccess,
+      handleFormatError,handleBeforeUpload,confirmSubClassifyImg
+    }
+  },
+  methods: {
+    
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.classifyManage{
+  padding: 2em;
+  .tree_content{
+    background-color: #fff;
+    padding: 2em; margin: 2em 0;
+  }  
+  .upload_classify_img{
+    .upload_border{
+      width: 100%; height: 100%;
+    }
+  }
+}
+</style>
+<style lang="scss">
+.classifyManage{
+  .el-tree-node__label{
+    border: 1px solid #000;
+    width: 100%;
+  }
+  .custom-tree-node {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    padding-right: 8px;
+    span{
+      width: 33%;
+    }
+  }
+  .ivu-upload-drag{
+    border: 0px;
+  }
+}
+</style>

+ 474 - 0
src/views/orderManage/orderDetail.vue

@@ -0,0 +1,474 @@
+/**
+  // author:zhangyy
+  // time:2023-02-09
+  // desc:订单详情
+*/
+<template>
+  <div class="orderDetail">
+    <div style="text-align:right;margin:1em 0;"><el-button @click="$router.back()">返回</el-button></div>
+    <div class="detail_order_num"><Icon type="ios-paper-outline" style="margin:0 0.5rem"/>
+      <b>订单编号:{{orderDataList.orderNum}}</b>
+      <b style="float:right;color:#ed4014;">{{orderStateDisplay(orderDataList.status)}}</b>
+    </div>
+    <div class="order_detail_box">
+      <!-- 卖家信息 -->
+      <div class="detail_buyer_box">
+        <h3>买家信息</h3>
+        <div>买家账户:{{orderDataList.userName}}</div>
+        <div>收货人:{{orderDataList.cattleOrderAddress.contacts}}</div>
+        <div>联系方式:{{orderDataList.cattleOrderAddress.phone}}</div>
+        <div>
+          收货地址:
+          {{orderDataList.cattleOrderAddress.province}}
+          {{orderDataList.cattleOrderAddress.city}}
+          {{orderDataList.cattleOrderAddress.area}}
+          {{orderDataList.cattleOrderAddress.addressInfo}}
+        </div>
+      </div>
+      <!-- 订单信息 -->
+      <div class="detail_order_box">
+        <h3>订单信息</h3>
+        <div>下单时间:{{moment(orderDataList.addTime).format('yyyy-MM-DD hh:mm:ss') || '暂无'}}</div>
+        <div>下单数量:{{orderDataList.count || 0}} 头</div>
+        <div>
+          订单总金额:
+          <Numeral :value="orderDataList.totalMoney || 0" format="0,0.00">
+            <template #prefix>
+              <strong>¥</strong>
+            </template>
+          </Numeral>
+        </div>
+        <div>
+          订金总金额:
+          <Numeral :value="orderDataList.firstMoney || 0" format="0,0.00">
+            <template #prefix>
+              <strong>¥</strong>
+            </template>
+          </Numeral>
+        </div>
+        <div>
+          尾款金额:
+          <Numeral :value="orderDataList.lastMoney || 0" format="0,0.00">
+            <template #prefix>
+              <strong>¥</strong>
+            </template>
+          </Numeral>
+        </div>
+        <div>备注信息:{{orderDataList.remark || '无'}}</div>
+      </div>
+      <!-- 支付状态 -->
+      <div class="detail_pay_box">
+        <h3>支付信息</h3>
+        <div>订金支付:{{payStateDisplay(orderDataList.payStatus)}}</div>
+        <div>尾款支付:{{orderStateDisplay(orderDataList.status)}}</div>
+      </div>
+      <!-- 开票信息  -->
+      <div class="detail_invoice_box">
+        <h3>开票信息</h3>
+        <span v-if="orderDataList.cattleOrderInvoice && orderDataList.cattleOrderInvoice.id">
+          <div>发票抬头:{{orderDataList.cattleOrderInvoice.companyName}}</div>
+          <div>统一社会信用代码:{{orderDataList.cattleOrderInvoice.taxNum}}</div>
+          <div>注册地址:{{orderDataList.cattleOrderInvoice.companyAddress}}</div>
+          <div>电话:{{orderDataList.cattleOrderInvoice.contactCell}}</div>
+          <div>开户银行:{{orderDataList.cattleOrderInvoice.bankName}}</div>
+          <div>银行账号:{{orderDataList.cattleOrderInvoice.bankNum}}</div>
+        </span>
+        <div v-else>买家不需开票</div>
+      </div>
+      <!-- 申请售后  -->
+      <div class="detail_invoice_box" v-if="orderDataList.cattleAfterSale && orderDataList.cattleAfterSale.id">
+        <h3>申请售后</h3>
+        <span>
+          <div>售后状态:
+            <span v-if="orderDataList.cattleAfterSale.status == 1" style="color:#ff9900;font-weight:bold;">申请退款待审核</span>
+            <span v-if="orderDataList.cattleAfterSale.status == 2" style="color:#19be6b;font-weight:bold;">审核通过已退款</span>
+            <span v-if="orderDataList.cattleAfterSale.status == 3" style="color:#ed4014;font-weight:bold;">审核已拒绝</span>
+          </div>
+          <div>申请时间:{{moment(orderDataList.cattleAfterSale.addTime).format('yyyy-MM-DD hh:mm:ss') || '暂无'}}</div>
+          <div>退款理由:{{orderDataList.cattleAfterSale.reason}}</div>
+          <div>退款说明:{{orderDataList.cattleAfterSale.content}}</div>
+          <div v-if="orderDataList.cattleAfterSale.refuseReason">拒绝原因:{{orderDataList.cattleAfterSale.refuseReason}}</div>
+          <div v-if="orderDataList.cattleAfterSale.status == 1">
+            <Button size="small" type="error" @click="auditRefundM(3)">拒绝申请</Button>
+            <Button size="small" type="success" style="margin-left:1em;" @click="auditRefundM(2)">同意退款</Button>
+          </div>
+        </span>
+      </div>
+    </div>
+
+    <div style="margin:1em 0;">
+      <el-table :data="[orderDataList]" style="width: 100%" border size="small">
+        <el-table-column label="商品图片" width="160" >
+          <template #default="scope">
+            <img :src="scope.row.imgUrl" style="width:100%;" alt="">
+          </template>
+        </el-table-column>
+        <el-table-column prop="itemName" label="商品名称" width="180" />
+        <el-table-column prop="weight" label="规格" width="180" />
+        <el-table-column label="单价(/头)" width="120">
+          <template #default="scope">
+            <Numeral :value="scope.row.price || 0" format="0,0.00">
+              <template #prefix>
+                <strong>¥</strong>
+              </template>
+            </Numeral>
+          </template>
+        </el-table-column>
+        <el-table-column label="订金(/头)" width="120" >
+          <template #default="scope">
+            <Numeral :value="scope.row.previewMoney || 0" format="0,0.00">
+              <template #prefix>
+                <strong>¥</strong>
+              </template>
+            </Numeral>
+          </template>
+        </el-table-column>
+        <el-table-column prop="count" label="下单数量(头)" width="120" align="center" />
+        <el-table-column label="订单总额" min-width="130">
+          <template #default="scope">
+            <Numeral :value="scope.row.totalMoney || 0" format="0,0.00">
+              <template #prefix>
+                <strong>¥</strong>
+              </template>
+            </Numeral>
+          </template>
+        </el-table-column>
+        <el-table-column label="订金总额" min-width="130">
+          <template #default="scope">
+            <Numeral :value="scope.row.firstMoney || 0" format="0,0.00">
+              <template #prefix>
+                <strong>¥</strong>
+              </template>
+            </Numeral>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="130" fixed="right">
+          <template #default="scope">
+            <el-button  type="warning" size="small" v-if="orderDataList.status == '3' && !orderDataList.updateLogId" @click="InitiatePrice('发起改价')">发起改价</el-button>
+            <el-button  type="primary" size="small" v-if="orderDataList.status == '3' &&  orderDataList.updateLogId" @click="lookPrice('查看改价')">
+              <span v-if="orderDataList.cattleOrderUpdateLog.status == 1">改价待确认</span>
+              <span v-if="orderDataList.cattleOrderUpdateLog.status == 2">查看改价</span>
+            </el-button>
+            <el-button  type="success" size="small" v-if="orderDataList.status == '2' && orderDataList.contactCell" @click="confirmlookCow">确定已看牛</el-button>
+            <el-button  type="success" size="small" v-if="(orderDataList.status == '3' && !orderDataList.updateLogId) || (orderDataList.status == '3' && orderDataList.cattleOrderUpdateLog.status == 2) " @click="confirmPayLast" style="margin:1rem 0">确认支付尾款</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <Modal v-model="lookModal" @on-ok="confirmPrice">
+        <template #header>
+          <p style="text-align:center">
+              <span>{{ modalTitle}}</span>
+          </p>
+        </template>
+        <div v-if="modalTitle == '发起改价'">
+          <Row style="padding:1rem">
+              <Col span="11">
+                <div style="color:#409EFF;margin-bottom:0.6rem">原始订单信息</div>
+                <div>定金: ¥{{orderDataList.firstMoney}}</div>
+                <div>尾款: ¥{{orderDataList.lastMoney}}</div>
+                <div>总计: ¥{{orderDataList.totalMoney}}</div>
+              </Col>
+              <Col span="2">
+                <Divider type="vertical" style="height:6rem"/>
+              </Col>
+              <Col span="11">
+                <div style="color:#409EFF;margin-bottom:0.6rem">更改订单信息</div>
+                <div>定金: ¥{{orderDataList.firstMoney}}</div>
+                <div>尾款: ¥{{orderDataList.lastMoney}}</div>
+                <div>合同总款: <Input v-model="totalPrice" size="small" placeholder="请输入总牛款金额" style="width: 130px" /></div>
+              </Col>
+          </Row>
+          <Divider />
+          <div style="padding:1rem" >
+            备注信息:<Input v-model="updateComment" type="textarea" placeholder="请输入备注信息" />
+          </div>
+        </div>
+        <template #footer v-if="modalTitle == '发起改价'">
+          <div style="text-align:center;">
+            <Button  @click="lookModal = false" style="margin-right:4em;">取消操作</Button>
+            <Button type="primary" @click="confirmPrice">确定提交</Button>
+          </div>
+        </template>
+        <div v-if="modalTitle == '查看改价'">
+          <Row style="padding:1rem">
+              <Col span="11" style="text-align:center">
+                <div style="color:#409EFF;margin-bottom:0.6rem">原始订单信息</div>
+                <div>定金: ¥{{orderDataList.firstMoney}}</div>
+                <div>尾款: ¥{{orderDataList.cattleOrderUpdateLog.oldLastMoney}}</div>
+                <div>总计: ¥{{orderDataList.cattleOrderUpdateLog.oldTotalMoney}}</div>
+              </Col>
+              <Col span="2">
+                <Divider type="vertical" style="height:6rem"/>
+              </Col>
+              <Col span="11" style="text-align:center">
+                <div style="color:#409EFF;margin-bottom:0.6rem">更改订单信息</div>
+                <div>合同总款: ¥{{orderDataList.cattleOrderUpdateLog.totalMoney}}</div>
+                <div>定金: ¥{{orderDataList.firstMoney}}</div>
+                <div>尾款: ¥{{orderDataList.cattleOrderUpdateLog.lastMoney}}</div>
+              </Col>
+          </Row>
+          <Divider />
+          <div style="padding:1rem" >
+            备注信息:<span >{{orderDataList.cattleOrderUpdateLog.updateComment }}</span>
+          </div>
+        </div>
+        <template #footer v-if="modalTitle == '查看改价'">
+          <Button  type="primary" @click="lookModal = false">确定</Button>
+        </template>
+    </Modal>
+
+    <Modal v-model="auditRefundModal"  title="审核售后" width="25%">
+      <div v-if="auditNum == 3">
+        <Input v-model="auditRefuseReason" :border="false" placeholder="请填写您得审核拒绝理由" />
+      </div>
+      <div v-if="auditNum == 2">
+        您确定同意该笔退款申请吗?
+      </div>
+      <template #footer>
+        <Button  @click="auditRefundModal = false">取消操作</Button>
+        <Button  type="primary" @click="confirmSubAudit">确定提交</Button>
+      </template>
+    </Modal>
+  </div>
+</template>
+
+<script>
+import { Message,Modal,Spin } from 'view-ui-plus'
+import { defineComponent, reactive,onMounted,ref} from 'vue'
+import { ElMessage,ElMessageBox} from 'element-plus'
+import { useRouter } from 'vue-router';
+import {cow,order} from '@/request/api'
+import moment from 'moment'
+
+export default defineComponent({
+  components:{
+  },
+  setup() {
+    let orderDataList = ref({cattleOrderAddress:{},cattleOrderInvoice:{},cattleAfterSale:{}})
+    let lookModal = ref(false)
+    let modalTitle = ref('发起改价')
+    let updateComment = ref()
+    let totalPrice = ref()
+    let orderStatusList = ref([
+      {label:'待支付',val:1},
+      {label:'待看牛',val:2},
+      {label:'待付尾款',val:3},
+      {label:'待交割',val:4},
+      {label:'已完成',val:5},
+      {label:'申请售后',val:6},
+      {label:'已取消',val:7},
+    ])
+
+    const $router = useRouter();
+    // 查询订单详情
+    async function getCattleOrderInfo(id){
+      await cow.getCattleOrderInfo({orderNum:id}).then(res=>{
+        if(res.code == 101){
+          orderDataList.value = res.data
+        }else{
+          Message.warning({content: res.message || '操作失败'})
+        }
+      })
+    }
+
+    // 查看改价
+    function lookPrice(title){
+      modalTitle.value = title
+      lookModal.value = true
+    }
+
+    // 发起改价
+    function InitiatePrice(title){
+      modalTitle.value = title
+      lookModal.value = true
+    }
+
+    //确定
+    async function confirmPrice(){
+      const params={
+        orderNum:orderDataList.value.orderNum,
+        status:1,
+        updateComment:updateComment.value,
+        totalMoney:totalPrice.value,
+      } 
+      await cow.UpdatePrice(params).then(res=>{
+        if(res.code == 101){
+          lookModal.value = false
+          getCattleOrderInfo($router.currentRoute.value.query.id)
+          Message.success({content: res.message || '操作成功'})
+        }else{
+          Message.warning({content: res.message || '操作失败'})
+        }
+      })
+    }
+
+    // 订单状态回执
+    function payStateDisplay(val){
+      switch(val){
+        case 2:
+          return '待付款'
+        break;
+        case 3:
+          return '已付订金'
+        break;
+        case 5:
+          return '已退款'
+        break;
+      }
+    }
+
+    // 订单状态回执
+    function orderStateDisplay(val){
+      switch(val){
+        case 1:
+          return '待付订金'; break;
+        case 2:
+          return '待看牛'; break;
+        case 3:
+          return '待付尾款'; break;
+        case 4:
+          return '待交割'; break;
+        case 5:
+          return '已取消'; break;
+        case 6:
+          return '已完成'; break;
+        case 7:
+          return '申请售后'; break;
+        case 8:
+          return '已退款'; break;
+      }
+    }
+
+    // 确认已经看牛
+    function confirmlookCow(){
+      ElMessageBox.confirm(
+        '请确认:买家是否已完成看牛?',
+        '确定已看牛',
+        {
+          confirmButtonText: '是的,已看',
+          cancelButtonText: '还未看牛',
+          type: 'warning',
+          closeOnClickModal:false
+        }
+      )
+        .then(async() => {
+          const params ={
+            orderNum:orderDataList.value.orderNum,
+          }
+          await cow.confirmAppointCattle(params).then(res=>{
+            if(res.code == 101){
+              getCattleOrderInfo($router.currentRoute.value.query.id)
+              Message.success({content: res.message || '确认已看牛'})
+            }else{
+              Message.warning({content: res.message || '操作失败'})
+            }
+          })
+        })
+        .catch(() => {
+          Message.warning({content: '还未看牛'})
+        })
+    }
+
+    // 确认支付尾款
+    function confirmPayLast(){
+      ElMessageBox.confirm(
+        '请确认:买家是否已支付尾款?',
+        '确定支付尾款',
+        {
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+          type: 'warning',
+          closeOnClickModal:false
+        }
+      ).then(async() => {
+          const params ={
+            orderNum:orderDataList.value.orderNum,
+          }
+          await cow.confirmLastPay(params).then(res=>{
+            if(res.code == 101){
+              getCattleOrderInfo($router.currentRoute.value.query.id)
+              Message.success({content: res.message || '确认成功'})
+            }else{
+              Message.warning({content: res.message || '确认失败'})
+            }
+          })
+        })
+    }
+    
+    let auditRefundModal = ref(false) //审核售后modal
+    let auditRefuseReason = ref('') //拒绝理由
+    let auditNum = ref('')
+    // 审核退款申请
+    function auditRefundM(val){
+      auditNum.value = val
+      auditRefundModal.value = true
+    }
+    // 确定提交审核
+    async function confirmSubAudit(){
+      if(auditNum.value == 3 && !auditRefuseReason.value){
+        Message.warning({content:'请填写拒绝的理由'})
+        return
+      }
+      let sendParams = {}
+      sendParams.id =  orderDataList.value.cattleAfterSale.id
+      sendParams.status = auditNum.value
+      sendParams.refuseReason = auditRefuseReason.value
+      await order.cattleAudit(sendParams).then(res=>{
+        if(res.code == 101){
+          auditRefundModal.value = false
+          Message.success({content: res.message || '操作成功'})
+          getCattleOrderInfo($router.currentRoute.value.query.id)  //获取订单详情
+        }else{
+          Message.warning({content: res.message || '操作失败'})
+        }
+      }).catch(e=>{
+        Message.warning({content: e.message || '确认失败'})
+      })
+    }
+
+    onMounted(async()=>{
+      getCattleOrderInfo($router.currentRoute.value.query.id)  //获取订单详情
+    })
+
+    
+    return {
+      orderDataList,lookModal,modalTitle,modalTitle,updateComment,totalPrice,orderStatusList,moment,auditRefundModal,auditNum,auditRefuseReason,
+      getCattleOrderInfo,InitiatePrice,lookPrice,confirmPrice,confirmlookCow,payStateDisplay,orderStateDisplay,confirmPayLast,auditRefundM,confirmSubAudit
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.orderDetail{
+  padding: 1em 2em;
+  .detail_order_num{padding: 1em .5em; background-color: #fff;}
+  .order_detail_box{
+    border-top: 3px solid #19be6b; background-color: #fff;
+    display: flex; flex-wrap:wrap;
+    .detail_buyer_box{
+      width: 33%; height: 250px; border-right: 1px dotted #ccc; padding: 1em 2em; margin:1em 0;
+      div{margin-top:10px;}
+    }
+    .detail_order_box{
+      width: 33%; height: 250px; border-right: 1px dotted #ccc; padding: 1em 2em;margin:1em 0;
+      div{margin-top:10px;}
+    }
+    .detail_pay_box{
+      width: 33%; height: 250px; padding: 1em 2em;margin:1em 0;
+      div{margin-top:10px;}
+    }
+    .detail_invoice_box{
+      width: 33%; height: 250px; padding: 1em 2em; border-right: 1px dotted #ccc; margin:1em 0;
+      div{margin-top:10px;}
+    }
+  }
+}
+</style>
+<style lang="scss">
+.orderDetail{
+
+}
+</style>

+ 228 - 0
src/views/orderManage/orderManageIndex.vue

@@ -0,0 +1,228 @@
+/**
+  // author:zhangb
+  // time:2022-12-01
+  // desc:订单管理
+*/
+<template>
+  <div class="orderManageIndex">
+    <!-- 筛选 -->
+    <Row :gutter="8">
+      <Col span="5">
+        <Input placeholder="请输入订单编号" v-model="filtInfoData.orderNum" style="width: 100%" clearable>
+          <template #suffix>
+            <Icon type="ios-search" />
+          </template>
+        </Input>
+      </Col>
+      <Col span="4">
+        <Input placeholder="请输入商品名称" v-model="filtInfoData.itemName" style="width: 100%" clearable></Input>
+      </Col>
+      <Col span="4">
+        <Select v-model="filtInfoData.status" placeholder="全部订单" style="width: 100%" @on-change="getAllOrderList">
+          <Option v-for="item in orderStatusList" :value="item.val" :key="item.val">{{ item.label }}</Option>
+        </Select>
+      </Col>
+      <Col span="5">
+        <DatePicker type="datetimerange" format="yyyy-MM-dd" placeholder="请选择订单时间范围" style="width:100%" @on-change="changeOrderTimeRage" />
+      </Col>
+      <Col span="3">
+        <Button type="primary" @click="getAllOrderList">确定搜索</Button>
+      </Col>
+    </Row>
+    <!-- 表格部分 -->
+    <el-table :data="orderDataList" border style="width: 100%; margin-top:20px;">
+      <el-table-column type="index" label="序号" width="55" align="center" fixed="left" />
+      <el-table-column prop="orderNum" label="订单编号" min-width="220" fixed="left" />
+      <el-table-column label="订单状态" width="100" align="center" >
+        <template #default="scope">
+          {{orderStatus(scope.row.status)}}
+        </template>
+      </el-table-column>
+      <el-table-column prop="itemName" label="商品名称" min-width="130" />
+      <el-table-column prop="pastureName" label="商品单价" width="120" align="center">
+        <template #default="scope">
+          <Numeral :value="scope.row.price || 0" format="0,0.00">
+            <template #prefix>
+                <strong>¥</strong>
+            </template>
+            <template #suffix>
+                <span>/头</span>
+            </template>
+          </Numeral>
+        </template>
+      </el-table-column>
+      <el-table-column label="商品订金" width="120" align="center">
+        <template #default="scope">
+          <Numeral :value="scope.row.firstMoney || 0" format="0,0.00">
+            <template #prefix>
+                <strong>¥</strong>
+            </template>
+            <template #suffix>
+                <span>/头</span>
+            </template>
+          </Numeral>
+        </template>
+      </el-table-column>
+      <el-table-column prop="payStatus" label="下单数量(头)" width="110" align="center" >
+        <template #default="scope">
+          {{scope.row.count}} 
+        </template>
+      </el-table-column>
+      <!-- <el-table-column prop="payStatus" label="支付状态" width="100" align="center" >
+        <template #default="scope">
+          <el-tag :type="scope.row.payStatus == 1?'success':'warning'">{{ payOrderState(scope.row.payStatus) }}</el-tag>
+        </template>
+      </el-table-column> -->
+      <el-table-column prop="sum" label="订单总额" width="120" align="center">
+        <template #default="scope">
+          <Numeral :value="scope.row.totalMoney" format="0,0.00">
+            <template #prefix>
+                <strong>¥</strong>
+            </template>
+          </Numeral>
+        </template>
+      </el-table-column>
+      <el-table-column prop="price2" label="订金总额" width="150" align="center" >
+        <template #default="scope">
+          <Numeral :value="scope.row.firstMoney * scope.row.count" format="0,0.00">
+            <template #prefix>
+                <strong>¥</strong>
+            </template>
+          </Numeral>
+          <span v-if="scope.row.payStatus == 3" style="background-color:#19be6b;color:#fff; font-size:12px;padding:3px 5px;border-radius:2px;margin-left:5px;margin-top:-2px;">已付</span>
+          <span v-if="scope.row.payStatus == 2" style="background-color:#ff9900;color:#fff; font-size:12px;padding:3px 5px;border-radius:2px;margin-left:5px;margin-top:-2px;">待付</span>
+          <span v-if="scope.row.payStatus == 5" style="background-color:#ed4014;color:#fff; font-size:12px;padding:3px 5px;border-radius:2px;margin-left:5px;margin-top:-2px;">已退款</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="price3" label="待付尾款" width="100" align="center" >
+        <template #default="scope">
+          {{ scope.row.waitPayLast || '—' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" min-width="220"  fixed="right">
+        <template  #default="scope">
+          <el-button  size="small" @click="$router.push('orderDetail?id=' + scope.row.orderNum)">订单详情</el-button>&nbsp;
+          <el-button  size="small" type="primary" v-if="scope.row.status == '2' && scope.row.contactCell" @click="lookCow(scope.row,'预约记录')">查看预约</el-button>
+          <!-- <el-button  size="small" type="success" v-if="scope.row.status == '3'">联系买家</el-button> -->
+          <el-button  size="small" type="warning" v-if="scope.row.status == '7'" @click="$router.push('orderDetail?id=' + scope.row.orderNum)">申请售后</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <div class="page_style">
+      <Page :total="orderTotalNum" :model-value="currentPage" :page-size="filtInfoData.limit" show-elevator show-total @on-change="changePage" />
+    </div>
+    <!-- 查看预约 -->
+    <Modal v-model="lookModal" width="25%">
+      <template #header>
+        <p style="text-align:center">
+            <span>{{ modalTitle}}</span>
+        </p>
+      </template>
+      <div v-if="modalTitle == '预约记录'" style="padding:1rem 5rem;">
+        <p style="margin-bottom:0.5rem">联系人: {{cattleMsg.contactName || '无'}}</p>
+        <p>联系电话: {{cattleMsg.contactCell || '无'}}</p>
+      </div>
+      <template #footer >
+        <div style="text-align:center;">
+          <Button @click="lookModal=false">关闭页面</Button>
+        </div>
+      </template>
+    </Modal>
+  </div>
+</template>
+
+<script>
+import { defineComponent, onMounted, ref ,reactive} from 'vue'
+import {cow} from '@/request/api'
+import {orderStatus,payOrderState} from '@/utils/system/filters'
+import { ElMessage } from 'element-plus'
+
+export default defineComponent({
+  components:{
+    
+  },
+  setup() {
+    let filtInfoData = ref({
+      limit:20,
+      offset:0,
+      orderNum:'',
+      status:'',
+      startDate:'',
+      endDate:'',
+      itemName:'',
+    })
+
+    let orderStatusList = ref([
+      {label:'全部订单',val:''},
+      {label:'待支付',val:'1'},
+      {label:'待看牛',val:'2'},
+      {label:'待付尾款',val:'3'},
+      {label:'待交割',val:'4'},
+      {label:'已取消',val:'5'},
+      {label:'已完成',val:'6'},
+      {label:'申请售后',val:'7'},
+      {label:'已退款',val:'8'},
+    ])
+
+    const lookModal = ref(false)
+    let modalTitle = ref('预约记录')
+    let cattleMsg = ref({})  
+
+    // 改变订单的时间范围
+    function changeOrderTimeRage(val){
+      console.log('选择的时间',val);
+      filtInfoData.value.startDate = val[0]
+      filtInfoData.value.endDate = val[1]
+      getAllOrderList()
+    }
+
+    let orderDataList = ref([])
+    let orderTotalNum = ref(0)
+    // 获取全部订单
+    async function getAllOrderList(){
+      await cow.getCattleOrderPage(filtInfoData.value).then(res=>{
+        if(res.code === 101){
+          orderDataList.value = res.data
+          orderTotalNum.value = res.total
+        }
+      })
+    }
+  
+    let currentPage = ref(1)
+    //更改页码
+    function changePage (page) {
+      filtInfoData.value.offset = (page -1) *  filtInfoData.value.limit  //更新偏移量
+      currentPage.val = page  //切换当前页码
+      getAllOrderList()
+    }
+
+    // 查看预约
+    function lookCow(row,title){
+      modalTitle.value = title
+      lookModal.value = true
+      cattleMsg.value = row
+    }
+
+    onMounted(()=>{
+      getAllOrderList() //获取全部订单
+    })
+
+    return {
+      orderDataList,orderTotalNum,orderStatus,payOrderState,
+      filtInfoData,orderStatusList,currentPage,lookModal,modalTitle,cattleMsg,
+      changePage,changeOrderTimeRage,lookCow,getAllOrderList,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.orderManageIndex{
+  padding: 1em;
+  .page_style{
+    text-align: right; margin-top: 1em;
+    background-color: var(--system-container-background);
+  }
+}
+</style>

+ 223 - 0
src/views/system/401.vue

@@ -0,0 +1,223 @@
+<template>
+  <div class="wscn-http404-container layout-container flex center">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" :src="first" alt="404" />
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">无权限访问!</div>
+        <div class="bullshit__info">
+          您没有访问权限,如需权限,请联系上级授权!
+        </div>
+        <el-button @click="go" type="primary" round>跳转首页</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import first from "@/assets/images/401.gif"
+export default defineComponent({
+  setup() {
+    return {
+      first
+    }
+  },
+  methods: {
+    go() {
+      this.$router.push('/')
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .wscn-http404-container {
+    
+  }
+  .wscn-http404 {
+    position: relative;
+    overflow: hidden;
+    .pic-404 {
+      position: relative;
+      float: left;
+      width: 600px;
+      overflow: hidden;
+      &__parent {
+        width: 313px;
+      }
+      &__child {
+        position: absolute;
+        &.left {
+          width: 80px;
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+          animation-name: cloudLeft;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1s;
+        }
+        &.mid {
+          width: 46px;
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+          animation-name: cloudMid;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1.2s;
+        }
+        &.right {
+          width: 62px;
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+          animation-name: cloudRight;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1s;
+        }
+        @keyframes cloudLeft {
+          0% {
+            top: 17px;
+            left: 220px;
+            opacity: 0;
+          }
+          20% {
+            top: 33px;
+            left: 188px;
+            opacity: 1;
+          }
+          80% {
+            top: 81px;
+            left: 92px;
+            opacity: 1;
+          }
+          100% {
+            top: 97px;
+            left: 60px;
+            opacity: 0;
+          }
+        }
+        @keyframes cloudMid {
+          0% {
+            top: 10px;
+            left: 420px;
+            opacity: 0;
+          }
+          20% {
+            top: 40px;
+            left: 360px;
+            opacity: 1;
+          }
+          70% {
+            top: 130px;
+            left: 180px;
+            opacity: 1;
+          }
+          100% {
+            top: 160px;
+            left: 120px;
+            opacity: 0;
+          }
+        }
+        @keyframes cloudRight {
+          0% {
+            top: 100px;
+            left: 500px;
+            opacity: 0;
+          }
+          20% {
+            top: 120px;
+            left: 460px;
+            opacity: 1;
+          }
+          80% {
+            top: 180px;
+            left: 340px;
+            opacity: 1;
+          }
+          100% {
+            top: 200px;
+            left: 300px;
+            opacity: 0;
+          }
+        }
+      }
+    }
+    .bullshit {
+      position: relative;
+      float: left;
+      width: 190px;
+      padding: 30px 0;
+      overflow: hidden;
+      &__oops {
+        font-size: 32px;
+        font-weight: bold;
+        line-height: 40px;
+        color: #1482f0;
+        opacity: 0;
+        margin-bottom: 20px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-fill-mode: forwards;
+      }
+      &__headline {
+        font-size: 20px;
+        line-height: 24px;
+        color: #222;
+        font-weight: bold;
+        opacity: 0;
+        margin-bottom: 10px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.1s;
+        animation-fill-mode: forwards;
+      }
+      &__info {
+        font-size: 13px;
+        line-height: 21px;
+        color: grey;
+        opacity: 0;
+        margin-bottom: 30px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.2s;
+        animation-fill-mode: forwards;
+      }
+      &__return-home {
+        display: block;
+        float: left;
+        width: 110px;
+        height: 36px;
+        background: #1482f0;
+        border-radius: 100px;
+        text-align: center;
+        color: #ffffff;
+        opacity: 0;
+        font-size: 14px;
+        line-height: 36px;
+        cursor: pointer;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.3s;
+        animation-fill-mode: forwards;
+      }
+      @keyframes slideUp {
+        0% {
+          transform: translateY(60px);
+          opacity: 0;
+        }
+        100% {
+          transform: translateY(0);
+          opacity: 1;
+        }
+      }
+    }
+  }
+</style>

+ 228 - 0
src/views/system/404.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="wscn-http404-container layout-container flex center">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" :src="four" alt="404" />
+        <img class="pic-404__child left" :src="four_cloud" alt="404" />
+        <img class="pic-404__child mid" :src="four_cloud" alt="404" />
+        <img class="pic-404__child right" :src="four_cloud" alt="404" />
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">页面不存在!</div>
+        <div class="bullshit__info">
+          请检查您输入的网址是否正确,或单击下面的按钮返回主页
+        </div>
+        <el-button @click="go" type="primary" round>跳转首页</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import four from "@/assets/images/404.png"
+import four_cloud from "@/assets/images/404_cloud.png"
+export default defineComponent({
+  setup() {
+    return {
+      four,
+      four_cloud
+    }
+  },
+  methods: {
+    go() {
+      this.$router.push('/')
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .wscn-http404-container {
+    
+  }
+  .wscn-http404 {
+    position: relative;
+    overflow: hidden;
+    .pic-404 {
+      position: relative;
+      float: left;
+      width: 600px;
+      overflow: hidden;
+      &__parent {
+        width: 100%;
+      }
+      &__child {
+        position: absolute;
+        &.left {
+          width: 80px;
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+          animation-name: cloudLeft;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1s;
+        }
+        &.mid {
+          width: 46px;
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+          animation-name: cloudMid;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1.2s;
+        }
+        &.right {
+          width: 62px;
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+          animation-name: cloudRight;
+          animation-duration: 2s;
+          animation-timing-function: linear;
+          animation-fill-mode: forwards;
+          animation-delay: 1s;
+        }
+        @keyframes cloudLeft {
+          0% {
+            top: 17px;
+            left: 220px;
+            opacity: 0;
+          }
+          20% {
+            top: 33px;
+            left: 188px;
+            opacity: 1;
+          }
+          80% {
+            top: 81px;
+            left: 92px;
+            opacity: 1;
+          }
+          100% {
+            top: 97px;
+            left: 60px;
+            opacity: 0;
+          }
+        }
+        @keyframes cloudMid {
+          0% {
+            top: 10px;
+            left: 420px;
+            opacity: 0;
+          }
+          20% {
+            top: 40px;
+            left: 360px;
+            opacity: 1;
+          }
+          70% {
+            top: 130px;
+            left: 180px;
+            opacity: 1;
+          }
+          100% {
+            top: 160px;
+            left: 120px;
+            opacity: 0;
+          }
+        }
+        @keyframes cloudRight {
+          0% {
+            top: 100px;
+            left: 500px;
+            opacity: 0;
+          }
+          20% {
+            top: 120px;
+            left: 460px;
+            opacity: 1;
+          }
+          80% {
+            top: 180px;
+            left: 340px;
+            opacity: 1;
+          }
+          100% {
+            top: 200px;
+            left: 300px;
+            opacity: 0;
+          }
+        }
+      }
+    }
+    .bullshit {
+      position: relative;
+      float: left;
+      width: 190px;
+      padding: 30px 0;
+      overflow: hidden;
+      &__oops {
+        font-size: 32px;
+        font-weight: bold;
+        line-height: 40px;
+        color: #1482f0;
+        opacity: 0;
+        margin-bottom: 20px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-fill-mode: forwards;
+      }
+      &__headline {
+        font-size: 20px;
+        line-height: 24px;
+        color: #222;
+        font-weight: bold;
+        opacity: 0;
+        margin-bottom: 10px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.1s;
+        animation-fill-mode: forwards;
+      }
+      &__info {
+        font-size: 13px;
+        line-height: 21px;
+        color: grey;
+        opacity: 0;
+        margin-bottom: 30px;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.2s;
+        animation-fill-mode: forwards;
+      }
+      &__return-home {
+        display: block;
+        float: left;
+        width: 110px;
+        height: 36px;
+        background: #1482f0;
+        border-radius: 100px;
+        text-align: center;
+        color: #ffffff;
+        opacity: 0;
+        font-size: 14px;
+        line-height: 36px;
+        cursor: pointer;
+        animation-name: slideUp;
+        animation-duration: 0.5s;
+        animation-delay: 0.3s;
+        animation-fill-mode: forwards;
+      }
+      @keyframes slideUp {
+        0% {
+          transform: translateY(60px);
+          opacity: 0;
+        }
+        100% {
+          transform: translateY(0);
+          opacity: 1;
+        }
+      }
+    }
+  }
+</style>

+ 152 - 0
src/views/system/login.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="container">
+    <div class="box">
+      <img src="../../assets/images/logo2.jpg" alt="" class="logoImg">
+      <h1>{{ systemTitle }}</h1>
+      <el-form class="form">
+        <el-input size="large" v-model="form.userName"  placeholder="用户名" type="text" maxlength="50">
+          <template #prepend>
+            <i class="sfont system-xingmingyonghumingnicheng"></i>
+          </template>
+        </el-input>
+
+        <el-input size="large" ref="password"  v-model="form.password" :type="passwordType" placeholder="密码" name="password" maxlength="50">
+          <template #prepend>
+            <i class="sfont system-mima"></i>
+          </template>
+          <template #append>
+            <i class="sfont password-icon" :class="passwordType ? 'system-yanjing-guan': 'system-yanjing'" @click="passwordTypeChange"></i>
+          </template>
+        </el-input>
+        
+        <el-button type="primary" @click="submit" style="width: 100%;" size="large">登录</el-button>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { systemTitle } from '@/config'
+import { defineComponent, ref, reactive } from 'vue'
+import { useStore } from 'vuex'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+export default defineComponent({
+  setup() {
+    const store = useStore()
+    const router = useRouter()
+    const route = useRoute()
+    const form = reactive({
+      userName: '',
+      password: ''
+    })
+    const passwordType = ref('password')
+    const passwordTypeChange = () => {
+      passwordType.value === '' ? passwordType.value = 'password' : passwordType.value = ''
+    }
+    const checkForm = () => {
+      return new Promise((resolve, reject) => {
+        if (form.userName === '') {
+          ElMessage.warning({
+            message: '用户名不能为空',
+            type: 'warning'
+          });
+          return;
+        }
+        if (form.password === '') {
+          ElMessage.warning({
+            message: '密码不能为空',
+            type: 'warning'
+          })
+          return;
+        }
+        resolve(true)
+      })
+    }
+    const submit = () => {
+      checkForm().then(() => {
+        store.dispatch('user/getUserToken', form).then((res) => {
+          ElMessage.success({
+            message: '登录成功',
+            type: 'success',
+            showClose: true,
+            duration: 1000
+          })
+          store.dispatch('user/getUserInfo') //获取登录用户信息
+          router.push(route.query.redirect || '/')
+        })
+
+      })
+    }
+    return {
+      systemTitle,
+      form,
+      passwordType,
+      passwordTypeChange,
+      submit
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+  .container {
+    position: relative;
+    width: 100vw;
+    height: 100vh;
+    background-color: #eef0f3;
+    .box {
+      width: 500px;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      background: white;
+      border-radius: 8px;
+      transform: translate(-50%, -50%);
+      height: 440px;
+      overflow: hidden;
+      box-shadow: 0 6px 20px 5px rgba(152, 152, 152, 0.1), 0 16px 24px 2px rgba(117, 117, 117, 0.14);
+      h1 {
+        margin-top: 20px;
+        text-align: center;
+      }
+      .logoImg{margin-top: 15px;margin-left: 15px;}
+      .form {
+        width: 80%;
+        margin: 40px auto 20px;
+        .el-input {
+          margin-bottom: 20px;
+        }
+        .password-icon {
+          cursor: pointer;
+          color: #409EFF;
+        }
+      }
+      .fixed-top-right {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+      }
+    }
+  }
+  @media screen and ( max-width: 750px ) {
+    .container .box {
+      width: 100vw;
+      height: 100vh;
+      box-shadow: none;
+      left: 0;
+      top: 0;
+      transform: none;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      h1 {
+        margin-top: 0;
+      }
+      .form {
+        
+      }
+    }
+  }
+</style>

+ 21 - 0
src/views/system/redirect.vue

@@ -0,0 +1,21 @@
+<template>
+  <div></div>
+</template>
+<script>
+import { defineComponent, unref } from "vue";
+import { useRouter } from "vue-router";
+export default defineComponent({
+  name: "Redirect",
+  setup() {
+    const { currentRoute, replace } = useRouter();
+    const { params, query } = unref(currentRoute);
+    const { path } = params;
+    const _path = Array.isArray(path) ? path.join("/") : path;
+    replace({
+      path: "/" + _path,
+      query
+    });
+    return {};
+  }
+});
+</script>

+ 30 - 0
vue.config.js

@@ -0,0 +1,30 @@
+const path = require("path");
+
+function resolve(dir) {
+  return path.join(__dirname, dir);
+}
+
+module.exports = {
+  publicPath: "./",
+  outputDir: "cattletrading",
+  lintOnSave: false,
+  productionSourceMap: false,
+  devServer: {
+    inline: true,
+    port: 8999,
+    open: true,
+  },
+  configureWebpack: {
+    resolve: {
+      alias: {
+        "@": resolve("src"),
+      },
+    },
+    performance:{
+      // 入口起点的最大体积
+      maxEntrypointSize: 50000000,
+      // 生成文件的最大体积
+      maxAssetSize: 30000000,
+    }
+  },
+};