ljw 1 year ago
commit
364064e5ce
62 changed files with 8448 additions and 0 deletions
  1. 6 0
      .env.development
  2. 5 0
      .env.production
  3. 20 0
      .gitignore
  4. 21 0
      LICENSE
  5. 41 0
      README.md
  6. 13 0
      index.html
  7. 13 0
      jsconfig.json
  8. 28 0
      package.json
  9. BIN
      public/favicon.ico
  10. 19 0
      src/App.vue
  11. 46 0
      src/api/address_book.js
  12. 7 0
      src/api/file.js
  13. 46 0
      src/api/group.js
  14. 46 0
      src/api/peer.js
  15. 8 0
      src/api/rustdesk.js
  16. 46 0
      src/api/tag.js
  17. 69 0
      src/api/user.js
  18. BIN
      src/assets/logo.png
  19. 99 0
      src/components/form/address.vue
  20. 198 0
      src/components/form/upload/imageUpload.vue
  21. 272 0
      src/components/form/upload/imagesUpload.vue
  22. 21 0
      src/components/form/upload/local.js
  23. 52 0
      src/components/form/upload/oss.js
  24. 19 0
      src/global.js
  25. 20 0
      src/layout/components/aside.vue
  26. 72 0
      src/layout/components/header.vue
  27. 56 0
      src/layout/components/menu/index.vue
  28. 52 0
      src/layout/components/menu/item.vue
  29. 140 0
      src/layout/components/setting/index.vue
  30. 80 0
      src/layout/components/tags/index.vue
  31. 85 0
      src/layout/index.vue
  32. 20 0
      src/main.js
  33. 50 0
      src/permission.js
  34. 118 0
      src/router/index.js
  35. 23 0
      src/store/app.js
  36. 3 0
      src/store/index.js
  37. 64 0
      src/store/router.js
  38. 73 0
      src/store/tags.js
  39. 62 0
      src/store/user.js
  40. 20 0
      src/styles/style.scss
  41. 13 0
      src/utils/auth.js
  42. 4 0
      src/utils/common_options.js
  43. 34 0
      src/utils/file.js
  44. 4272 0
      src/utils/pca.json
  45. 80 0
      src/utils/request.js
  46. 26 0
      src/utils/webclient.js
  47. 132 0
      src/views/address_book/index.js
  48. 212 0
      src/views/address_book/index.vue
  49. 13 0
      src/views/error-page/404.vue
  50. 149 0
      src/views/group/index.vue
  51. 75 0
      src/views/index/index.vue
  52. 91 0
      src/views/login/login.vue
  53. 204 0
      src/views/my/address_book/index.vue
  54. 133 0
      src/views/my/tag/index.vue
  55. 211 0
      src/views/peer/index.vue
  56. 155 0
      src/views/tag/index.js
  57. 165 0
      src/views/tag/index.vue
  58. 86 0
      src/views/user/composables/edit.js
  59. 124 0
      src/views/user/composables/index.js
  60. 76 0
      src/views/user/edit.vue
  61. 78 0
      src/views/user/index.vue
  62. 82 0
      vite.config.js

+ 6 - 0
.env.development

@@ -0,0 +1,6 @@
1
+ENV = 'development'
2
+
3
+VITE_DEV_PORT = 8888
4
+VITE_SERVER_API = /api/admin
5
+VITE_SERVER_PATH = http://127.0.0.1:21114
6
+

+ 5 - 0
.env.production

@@ -0,0 +1,5 @@
1
+ENV = 'production'
2
+
3
+VITE_DEV_PORT = 5000
4
+VITE_SERVER_API =/api/admin
5
+VITE_SERVER_PATH = http://127.0.0.1:5000

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
1
+.DS_Store
2
+node_modules/
3
+dist/
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+**/*.log
8
+
9
+
10
+# Editor directories and files
11
+.idea
12
+.vscode
13
+*.suo
14
+*.ntvs*
15
+*.njsproj
16
+*.sln
17
+*.local
18
+
19
+package-lock.json
20
+yarn.lock

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2016-2021 vue-manage-system
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 41 - 0
README.md

@@ -0,0 +1,41 @@
1
+# Gwen-Admin
2
+# 基于 Vue3 + Element Plus 的后台管理系统
3
+
4
+<a href="https://github.com/vuejs/vue-next">
5
+    <img src="https://img.shields.io/badge/vue-^3.2.16-brightgreen.svg" alt="vue3">
6
+  </a>
7
+  <a href="https://github.com/element-plus/element-plus">
8
+    <img src="https://img.shields.io/badge/element--plus-^1.2.0--beta.1-brightgreen.svg" alt="element-plus">
9
+  </a>
10
+  <a href="https://github.com/lejianwen/Gwen-admin/blob/master/LICENSE">
11
+    <img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
12
+  </a>
13
+
14
+# 安装步骤
15
+
16
+~~~shell script
17
+git clone https://github.com/lejianwen/Gwen-admin.git  
18
+cd Gwen-admin   
19
+npm install
20
+
21
+// 本地开发
22
+npm run dev
23
+
24
+// 打包
25
+npm run build
26
+
27
+// 本地预览
28
+npm run server
29
+~~~
30
+
31
+## 功能
32
+
33
+-   [x] Element Plus
34
+-   [x] 登录/注销
35
+-   [x] 路由权限
36
+-   [x] Dashboard
37
+-   [x] 表格
38
+-   [x] 表单
39
+-   [x] 图片本地/oss上传
40
+-   [x] 404
41
+-   [x] 多级菜单

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
1
+<!DOCTYPE html>
2
+<html lang="zh">
3
+  <head>
4
+    <meta charset="UTF-8" />
5
+    <link rel="icon" href="/favicon.ico" />
6
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+    <title>Gwen-Admin</title>
8
+  </head>
9
+  <body>
10
+    <div id="app"></div>
11
+    <script type="module" src="/src/main.js"></script>
12
+  </body>
13
+</html>

+ 13 - 0
jsconfig.json

@@ -0,0 +1,13 @@
1
+{
2
+  "compilerOptions": {
3
+    "baseUrl": "./",
4
+    "paths": {
5
+      "@/*": [
6
+        "src/*"
7
+      ]
8
+    }
9
+  },
10
+  "exclude": [
11
+    "node_modules"
12
+  ]
13
+}

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
1
+{
2
+  "name": "hello-vue3",
3
+  "version": "0.0.0",
4
+  "scripts": {
5
+    "dev": "vite --host",
6
+    "build": "vite build",
7
+    "serve": "vite preview"
8
+  },
9
+  "dependencies": {
10
+    "axios": "1.6.0",
11
+    "element-plus": "^2.8.2",
12
+    "js-cookie": "^3.0.1",
13
+    "normalize.css": "^8.0.1",
14
+    "nprogress": "^0.2.0",
15
+    "pinia": "2.0.3",
16
+    "vue": "3.2.37",
17
+    "vue-router": "^4.0.12"
18
+  },
19
+  "devDependencies": {
20
+    "@element-plus/icons": "0.0.11",
21
+    "@vitejs/plugin-vue": "^1.9.3",
22
+    "dotenv": "^10.0.0",
23
+    "qs": "^6.10.2",
24
+    "sass-loader": "^12.3.0",
25
+    "sass": "^1.43.4",
26
+    "vite": "^2.9.18"
27
+  }
28
+}

BIN
public/favicon.ico


+ 19 - 0
src/App.vue

@@ -0,0 +1,19 @@
1
+<template>
2
+  <router-view/>
3
+</template>
4
+<script>
5
+  import { defineComponent, ref, onMounted } from 'vue'
6
+
7
+  export default defineComponent({
8
+    props: {},
9
+    setup (props) {
10
+    },
11
+    created () {
12
+
13
+    },
14
+  })
15
+
16
+
17
+</script>
18
+<style>
19
+</style>

+ 46 - 0
src/api/address_book.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+export function list (params) {
4
+  return request({
5
+    url: '/address_book/list',
6
+    params,
7
+  })
8
+}
9
+
10
+export function detail (id) {
11
+  return request({
12
+    url: `/address_book/detail/${id}`,
13
+  })
14
+}
15
+
16
+export function create (data) {
17
+  return request({
18
+    url: '/address_book/create',
19
+    method: 'post',
20
+    data,
21
+  })
22
+}
23
+
24
+export function update (data) {
25
+  return request({
26
+    url: '/address_book/update',
27
+    method: 'post',
28
+    data,
29
+  })
30
+}
31
+
32
+export function remove (data) {
33
+  return request({
34
+    url: '/address_book/delete',
35
+    method: 'post',
36
+    data,
37
+  })
38
+}
39
+
40
+export function changePwd (data) {
41
+  return request({
42
+    url: '/address_book/changePwd',
43
+    method: 'post',
44
+    data,
45
+  })
46
+}

+ 7 - 0
src/api/file.js

@@ -0,0 +1,7 @@
1
+import request from '@/utils/request'
2
+
3
+export function ossToken () {
4
+  return request({
5
+    url: '/file/oss_token',
6
+  })
7
+}

+ 46 - 0
src/api/group.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+export function list (params) {
4
+  return request({
5
+    url: '/group/list',
6
+    params,
7
+  })
8
+}
9
+
10
+export function detail (id) {
11
+  return request({
12
+    url: `/group/detail/${id}`,
13
+  })
14
+}
15
+
16
+export function create (data) {
17
+  return request({
18
+    url: '/group/create',
19
+    method: 'post',
20
+    data,
21
+  })
22
+}
23
+
24
+export function update (data) {
25
+  return request({
26
+    url: '/group/update',
27
+    method: 'post',
28
+    data,
29
+  })
30
+}
31
+
32
+export function remove (data) {
33
+  return request({
34
+    url: '/group/delete',
35
+    method: 'post',
36
+    data,
37
+  })
38
+}
39
+
40
+export function changePwd (data) {
41
+  return request({
42
+    url: '/group/changePwd',
43
+    method: 'post',
44
+    data,
45
+  })
46
+}

+ 46 - 0
src/api/peer.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+export function list (params) {
4
+  return request({
5
+    url: '/peer/list',
6
+    params,
7
+  })
8
+}
9
+
10
+export function detail (id) {
11
+  return request({
12
+    url: `/peer/detail/${id}`,
13
+  })
14
+}
15
+
16
+export function create (data) {
17
+  return request({
18
+    url: '/peer/create',
19
+    method: 'post',
20
+    data,
21
+  })
22
+}
23
+
24
+export function update (data) {
25
+  return request({
26
+    url: '/peer/update',
27
+    method: 'post',
28
+    data,
29
+  })
30
+}
31
+
32
+export function remove (data) {
33
+  return request({
34
+    url: '/peer/delete',
35
+    method: 'post',
36
+    data,
37
+  })
38
+}
39
+
40
+export function changePwd (data) {
41
+  return request({
42
+    url: '/peer/changePwd',
43
+    method: 'post',
44
+    data,
45
+  })
46
+}

+ 8 - 0
src/api/rustdesk.js

@@ -0,0 +1,8 @@
1
+import request from '@/utils/request'
2
+
3
+export function config () {
4
+  return request({
5
+    url: '/server-config',
6
+    method: 'get',
7
+  })
8
+}

+ 46 - 0
src/api/tag.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+export function list (params) {
4
+  return request({
5
+    url: '/tag/list',
6
+    params,
7
+  })
8
+}
9
+
10
+export function detail (id) {
11
+  return request({
12
+    url: `/tag/detail/${id}`,
13
+  })
14
+}
15
+
16
+export function create (data) {
17
+  return request({
18
+    url: '/tag/create',
19
+    method: 'post',
20
+    data,
21
+  })
22
+}
23
+
24
+export function update (data) {
25
+  return request({
26
+    url: '/tag/update',
27
+    method: 'post',
28
+    data,
29
+  })
30
+}
31
+
32
+export function remove (data) {
33
+  return request({
34
+    url: '/tag/delete',
35
+    method: 'post',
36
+    data,
37
+  })
38
+}
39
+
40
+export function changePwd (data) {
41
+  return request({
42
+    url: '/tag/changePwd',
43
+    method: 'post',
44
+    data,
45
+  })
46
+}

+ 69 - 0
src/api/user.js

@@ -0,0 +1,69 @@
1
+import request from '@/utils/request'
2
+
3
+export function login (data) {
4
+  return request({
5
+    url: '/login',
6
+    method: 'post',
7
+    data,
8
+  })
9
+}
10
+
11
+export function current () {
12
+  return request({
13
+    url: '/user/current',
14
+    method: 'get',
15
+  })
16
+}
17
+
18
+export function list (params) {
19
+  return request({
20
+    url: '/user/list',
21
+    params,
22
+  })
23
+}
24
+
25
+export function detail (id) {
26
+  return request({
27
+    url: `/user/detail/${id}`,
28
+  })
29
+}
30
+
31
+export function create (data) {
32
+  return request({
33
+    url: '/user/create',
34
+    method: 'post',
35
+    data,
36
+  })
37
+}
38
+
39
+export function update (data) {
40
+  return request({
41
+    url: '/user/update',
42
+    method: 'post',
43
+    data,
44
+  })
45
+}
46
+
47
+export function remove (data) {
48
+  return request({
49
+    url: '/user/delete',
50
+    method: 'post',
51
+    data,
52
+  })
53
+}
54
+
55
+export function changePwd (data) {
56
+  return request({
57
+    url: '/user/changePwd',
58
+    method: 'post',
59
+    data,
60
+  })
61
+}
62
+
63
+export function changeCurPwd (data) {
64
+  return request({
65
+    url: '/user/changeCurPwd',
66
+    method: 'post',
67
+    data,
68
+  })
69
+}

BIN
src/assets/logo.png


+ 99 - 0
src/components/form/address.vue

@@ -0,0 +1,99 @@
1
+<template>
2
+  <el-form-item ref="formAddress" :label="label" :prop="prop">
3
+    <el-select v-model="currentProvince" clearable placeholder="省" @change="changeProvince">
4
+      <el-option v-for="(_, name) in pca" :key="name" :label="name" :value="name"/>
5
+    </el-select>
6
+    <el-select v-model="currentCity" clearable placeholder="市" @change="changeCity">
7
+      <el-option v-for="(_, name) in cities" :key="name" :label="name" :value="name"/>
8
+    </el-select>
9
+    <el-select v-model="currentCounty" clearable placeholder="区" @change="changeCounty">
10
+      <el-option v-for="item in counties" :key="item" :label="item" :value="item"/>
11
+    </el-select>
12
+  </el-form-item>
13
+</template>
14
+
15
+<script>
16
+  import { defineComponent, ref, computed } from 'vue'
17
+  import pca from '@/utils/pca.json'
18
+
19
+  export default defineComponent({
20
+    name: 'FormAddress',
21
+    props: {
22
+      prop: {
23
+        type: String,
24
+        default: '',
25
+      },
26
+      label: {
27
+        type: String,
28
+        default: '省/市/区',
29
+      },
30
+      province: {
31
+        type: String,
32
+        default: '',
33
+      },
34
+      city: {
35
+        type: String,
36
+        default: '',
37
+      },
38
+      county: {
39
+        type: String,
40
+        default: '',
41
+      },
42
+    },
43
+    setup (props, context) {
44
+      const cities = computed(() => pca[props.province] || [])
45
+      const counties = computed(() => pca[props.province] && pca[props.province][props.city] ? pca[props.province][props.city] : [])
46
+
47
+      let currentProvince = computed({
48
+        get: () => props.province,
49
+        set: (val) => {
50
+          context.emit('update:province', val)
51
+        },
52
+      })
53
+      let currentCity = computed({
54
+        get: () => props.city,
55
+        set: (val) => {
56
+          context.emit('update:city', val)
57
+        },
58
+      })
59
+      let currentCounty = computed({
60
+        get: () => props.county,
61
+        set: (val) => {
62
+          context.emit('update:county', val)
63
+        },
64
+      })
65
+
66
+      const changeProvince = (val) => {
67
+        currentCity = ''
68
+        currentCounty = ''
69
+        context.emit('changeProvince', val)
70
+      }
71
+      const changeCity = (val) => {
72
+        currentCounty = ''
73
+        context.emit('changeCity', val)
74
+      }
75
+      const changeCounty = (val) => {
76
+        context.emit('changeCounty', val)
77
+      }
78
+
79
+      return {
80
+        pca,
81
+        cities,
82
+        counties,
83
+
84
+        currentProvince,
85
+        currentCity,
86
+        currentCounty,
87
+
88
+        changeProvince,
89
+        changeCity,
90
+        changeCounty,
91
+      }
92
+    },
93
+
94
+  })
95
+</script>
96
+
97
+<style scoped>
98
+
99
+</style>

+ 198 - 0
src/components/form/upload/imageUpload.vue

@@ -0,0 +1,198 @@
1
+<template>
2
+  <div class="upload-order-file">
3
+    <el-upload
4
+            size="mini"
5
+            ref="upload"
6
+            :on-success="fileUploadSuccess"
7
+            :before-upload="beforeFileUpload"
8
+            :on-preview="onPreview"
9
+            :on-remove="fileRemove"
10
+            :on-error="onError"
11
+            name="file"
12
+            :file-list="fileList"
13
+            :action="fileUploadHost"
14
+            :data="fileUploadData"
15
+            :headers="headers"
16
+            list-type="picture-card"
17
+            :limit="0"
18
+            accept="image/*"
19
+    >
20
+      <template #default>
21
+        <div class="default-slot">
22
+          <slot name="default">
23
+            <el-icon class="default-icon">
24
+              <plus/>
25
+            </el-icon>
26
+          </slot>
27
+        </div>
28
+      </template>
29
+    </el-upload>
30
+    <el-dialog v-model="showPreview" top="5vh">
31
+      <el-image :src="showImage" class="preview-image" fit="contain"></el-image>
32
+    </el-dialog>
33
+  </div>
34
+</template>
35
+<script>
36
+  import { defineComponent, ref, computed, reactive, unref, readonly, toRefs } from 'vue'
37
+  import { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check } from '@element-plus/icons'
38
+  import { useOss } from '@/components/form/upload/oss'
39
+  import { ElMessage } from 'element-plus'
40
+  import { useLocal } from '@/components/form/upload/local'
41
+
42
+  export default defineComponent({
43
+    name: 'imageUpload',
44
+    props: {
45
+      limit: {
46
+        type: Number,
47
+        default: 0,
48
+      },
49
+      beforeUpload: {
50
+        type: Function,
51
+        default: function () {
52
+          return true
53
+        },
54
+      },
55
+      host: {
56
+        type: String,
57
+        default: import.meta.env.VITE_BASE_API + '/file/upload',
58
+      },
59
+      modelValue: {
60
+        type: String,
61
+        default: '',
62
+      },
63
+      type: {
64
+        type: String,
65
+        default: 'local', //local oss
66
+      },
67
+      width: {
68
+        type: String,
69
+        default: '148px',
70
+      },
71
+    },
72
+    components: { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check },
73
+    setup (props, context) {
74
+      const showPreview = ref(false)
75
+      const showImage = ref('')
76
+
77
+      let fileList = computed(() => props.modelValue ? [{ url: props.modelValue, status: 'success' }] : [])
78
+
79
+      let fileUpload = reactive({
80
+        fileUploadHost: '',
81
+        fileUploadData: {},
82
+        beforeFileUpload: null,
83
+        headers: {},
84
+      })
85
+
86
+      if (props.type === 'oss') {
87
+        fileUpload = useOss(props.beforeUpload, props.multiple)
88
+      } else {
89
+        fileUpload = useLocal(props.beforeUpload, props.host)
90
+      }
91
+
92
+      function removeImage (file) {
93
+        let fList = unref(fileList)
94
+        const index = fList.findIndex(f => f.url === file.url)
95
+        fList.splice(index, 1)
96
+        updateValue(fList)
97
+      }
98
+
99
+      function updateValue (_fileList) {
100
+        let fList = unref(_fileList)
101
+        context.emit(
102
+          'update:modelValue',
103
+          fList.length ? fList[0].url : '',
104
+        )
105
+      }
106
+
107
+      function fileRemove (file, _fileList) {
108
+        updateValue(_fileList)
109
+      }
110
+
111
+      function onError () {
112
+
113
+      }
114
+
115
+      function fileUploadSuccess (response, file, _fileList) {
116
+        file.url = response?.data?.url || file.url
117
+        if (_fileList.length > 1) {
118
+          _fileList.splice(0, 1)
119
+        }
120
+        if (_fileList.every(f => f.status === 'success')) {
121
+          updateValue(_fileList)
122
+        }
123
+      }
124
+
125
+      function onPreview (file) {
126
+        showImage.value = file.url
127
+        showPreview.value = true
128
+      }
129
+
130
+      return {
131
+        fileList,
132
+
133
+        ...toRefs(fileUpload),
134
+
135
+        fileRemove,
136
+        onError,
137
+        fileUploadSuccess,
138
+
139
+        onPreview,
140
+        removeImage,
141
+
142
+        showPreview,
143
+        showImage,
144
+      }
145
+    },
146
+  })
147
+</script>
148
+
149
+<style scoped lang="scss">
150
+  .upload-order-file {
151
+
152
+    ::v-deep(.el-upload-list__item-thumbnail) {
153
+      object-fit: contain;
154
+
155
+    }
156
+
157
+    ::v-deep(.el-upload--picture-card) {
158
+      width: v-bind(width);
159
+      height: v-bind(width);
160
+    }
161
+
162
+    ::v-deep(.el-upload-list__item) {
163
+      width: v-bind(width);
164
+      height: v-bind(width);
165
+    }
166
+
167
+    ::v-deep(.el-progress) {
168
+      width: v-bind(width) !important;
169
+      height: v-bind(width) !important;
170
+    }
171
+
172
+    ::v-deep(.el-progress-circle) {
173
+      width: v-bind(width) !important;
174
+      height: v-bind(width) !important;
175
+    }
176
+
177
+
178
+    .default-slot {
179
+      height: 100%;
180
+      width: 100%;
181
+      display: flex;
182
+      justify-content: center;
183
+      align-items: center;
184
+
185
+      .default-icon {
186
+        margin-top: 0;
187
+      }
188
+    }
189
+  }
190
+
191
+  .preview-image {
192
+    width: 100%;
193
+
194
+    ::v-deep(img) {
195
+      max-height: 700px;
196
+    }
197
+  }
198
+</style>

+ 272 - 0
src/components/form/upload/imagesUpload.vue

@@ -0,0 +1,272 @@
1
+<template>
2
+  <div class="upload-order-file">
3
+    <el-upload
4
+            ref="upload"
5
+            :on-success="fileUploadSuccess"
6
+            :before-upload="beforeFileUpload"
7
+            :on-remove="fileRemove"
8
+            :on-exceed="onExceed"
9
+            :on-error="onError"
10
+            name="file"
11
+            :multiple="multiple"
12
+            :file-list="fileList"
13
+            :action="fileUploadHost"
14
+            :data="fileUploadData"
15
+            :headers="headers"
16
+            list-type="picture-card"
17
+            :limit="limit"
18
+            accept="image/*"
19
+            :drag="drag"
20
+    >
21
+      <template #default>
22
+        <div class="default-slot">
23
+          <slot name="default">
24
+            <div>
25
+              <el-icon class="default-icon">
26
+                <plus/>
27
+              </el-icon>
28
+              <div class="drag-tips">点击上传<span v-if="drag">或直接拖入文件</span></div>
29
+            </div>
30
+          </slot>
31
+        </div>
32
+      </template>
33
+      <template #file="{file}">
34
+        <img
35
+                v-if="file.status === 'success'"
36
+                class="el-upload-list__item-thumbnail"
37
+                :src="file.url"
38
+                alt=""
39
+        >
40
+        <label class="el-upload-list__item-status-label">
41
+          <el-icon color="white">
42
+            <check/>
43
+          </el-icon>
44
+        </label>
45
+        <el-progress
46
+                v-if="file.status === 'uploading'"
47
+                type="circle"
48
+                :stroke-width="6"
49
+                :percentage="parseInt(file.percentage)"
50
+        />
51
+        <span v-else-if="file.status === 'success'" class="el-upload-list__item-actions">
52
+          <el-icon class="el-upload-list__item-icon" @click="leftImage(file)"><arrow-left/></el-icon>
53
+          <el-icon class="el-upload-list__item-icon" @click="removeImage(file)"><Delete/></el-icon>
54
+          <el-icon class="el-upload-list__item-icon" @click="rightImage(file)"><arrow-right/></el-icon>
55
+        </span>
56
+      </template>
57
+    </el-upload>
58
+  </div>
59
+</template>
60
+<script>
61
+  import { defineComponent, ref, computed, reactive, unref, readonly, toRefs } from 'vue'
62
+  import { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check } from '@element-plus/icons'
63
+  import { useOss } from '@/components/form/upload/oss'
64
+  import { ElMessage } from 'element-plus'
65
+  import { useLocal } from '@/components/form/upload/local'
66
+
67
+  export default defineComponent({
68
+    name: 'imagesUpload',
69
+    props: {
70
+      drag: {
71
+        type: Boolean,
72
+        default: false,
73
+      },
74
+      limit: {
75
+        type: Number,
76
+        default: 0,
77
+      },
78
+      beforeUpload: {
79
+        type: Function,
80
+        default: function () {
81
+          return true
82
+        },
83
+      },
84
+      host: {
85
+        type: String,
86
+        default: import.meta.env.VITE_BASE_API + '/file/upload',
87
+      },
88
+      modelValue: {
89
+        type: Array,
90
+        default: function () {
91
+          return []
92
+        },
93
+      },
94
+      type: {
95
+        type: String,
96
+        default: 'local', //local oss
97
+      },
98
+      multiple: {
99
+        type: Boolean,
100
+        default: false,
101
+      },
102
+      width: {
103
+        type: String,
104
+        default: '148px',
105
+      },
106
+    },
107
+    components: { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check },
108
+    setup (props, context) {
109
+
110
+      let fileList = computed(() => props.modelValue.map(url => { return { url, status: 'success' } }))
111
+
112
+      let fileUpload = reactive({
113
+        fileUploadHost: '',
114
+        fileUploadData: {},
115
+        beforeFileUpload: null,
116
+        headers: {},
117
+      })
118
+
119
+      if (props.type === 'oss') {
120
+        fileUpload = useOss(props.beforeUpload, props.multiple)
121
+      } else {
122
+        fileUpload = useLocal(props.beforeUpload, props.host)
123
+      }
124
+
125
+      function leftImage (file) {
126
+        let fList = unref(fileList)
127
+        const index = fList.findIndex(f => f.url === file.url)
128
+        if (index === 0 || index === -1) {
129
+          return
130
+        }
131
+        fList[index] = fList.splice(index - 1, 1, fList[index])[0]
132
+        updateValue(fList)
133
+      }
134
+
135
+      function rightImage (file) {
136
+        let fList = unref(fileList)
137
+        const index = fList.findIndex(f => f.url === file.url)
138
+        if (index === fList.length - 1 || index === -1) {
139
+          return
140
+        }
141
+        fList[index] = fList.splice(index + 1, 1, fList[index])[0]
142
+        updateValue(fList)
143
+      }
144
+
145
+      function removeImage (file) {
146
+        let fList = unref(fileList)
147
+        const index = fList.findIndex(f => f.url === file.url)
148
+        fList.splice(index, 1)
149
+        updateValue(fList)
150
+      }
151
+
152
+      function updateValue (_fileList) {
153
+        let fList = unref(_fileList)
154
+        context.emit(
155
+          'update:modelValue',
156
+          fList.filter(f => f.status === 'success').map(file => file.url),
157
+        )
158
+      }
159
+
160
+      function fileRemove (file, _fileList) {
161
+        updateValue(_fileList)
162
+      }
163
+
164
+      function onError () {
165
+
166
+      }
167
+
168
+      function fileUploadSuccess (response, file, _fileList) {
169
+        file.url = response?.data?.url || file.url
170
+        if (_fileList.every(f => f.status === 'success')) {
171
+          updateValue(_fileList)
172
+        }
173
+      }
174
+
175
+      function onExceed () {
176
+        ElMessage.error('超出数量限制')
177
+      }
178
+
179
+      return {
180
+        fileList,
181
+
182
+        ...toRefs(fileUpload),
183
+
184
+        onExceed,
185
+        fileRemove,
186
+        onError,
187
+        fileUploadSuccess,
188
+
189
+        leftImage,
190
+        rightImage,
191
+        removeImage,
192
+      }
193
+    },
194
+  })
195
+</script>
196
+
197
+<style scoped lang="scss">
198
+  .upload-order-file {
199
+    ::v-deep(.el-upload-dragger) {
200
+      border: none;
201
+      width: 100%;
202
+      height: 100%;
203
+    }
204
+
205
+    ::v-deep(.el-upload--picture-card) {
206
+      width: v-bind(width);
207
+      height: v-bind(width);
208
+    }
209
+
210
+    ::v-deep(.el-upload-list__item) {
211
+      width: v-bind(width);
212
+      height: v-bind(width);
213
+    }
214
+
215
+    ::v-deep(.el-progress) {
216
+      width: v-bind(width) !important;
217
+      height: v-bind(width) !important;
218
+    }
219
+
220
+    ::v-deep(.el-progress-circle) {
221
+      width: v-bind(width) !important;
222
+      height: v-bind(width) !important;
223
+    }
224
+
225
+    .drag-tips {
226
+      font-size: 12px;
227
+      color: #999;
228
+    }
229
+
230
+
231
+    ::v-deep(.el-upload-list__item) {
232
+      transition: none !important;
233
+    }
234
+
235
+    ::v-deep(.el-upload-list) {
236
+      transition: none !important;
237
+    }
238
+
239
+    .el-upload-list__item-thumbnail {
240
+      object-fit: contain;
241
+    }
242
+
243
+    .el-upload-list__item-actions {
244
+      display: flex;
245
+      justify-content: space-around;
246
+      align-items: center;
247
+
248
+      &:after {
249
+        display: none;
250
+      }
251
+
252
+      .el-upload-list__item-icon {
253
+        cursor: pointer;
254
+        font-size: 20px;
255
+        color: #fff;
256
+      }
257
+    }
258
+
259
+    .default-slot {
260
+      height: 100%;
261
+      width: 100%;
262
+      display: flex;
263
+      justify-content: center;
264
+      align-items: center;
265
+
266
+      .default-icon {
267
+        margin-top: 0;
268
+      }
269
+    }
270
+  }
271
+
272
+</style>

+ 21 - 0
src/components/form/upload/local.js

@@ -0,0 +1,21 @@
1
+import { getToken } from '@/utils/auth'
2
+
3
+export function useLocal (beforeUp, host) {
4
+  const fileUploadData = {}
5
+  const fileUploadHost = host
6
+  const headers = { 'api-token': getToken() }
7
+  const beforeFileUpload = async (file) => {
8
+    if (beforeUp) {
9
+      const br = await beforeUp(file)
10
+      if (!br) { return Promise.reject() }
11
+    }
12
+    return Promise.resolve()
13
+  }
14
+
15
+  return {
16
+    fileUploadData,
17
+    fileUploadHost,
18
+    beforeFileUpload,
19
+    headers,
20
+  }
21
+}

+ 52 - 0
src/components/form/upload/oss.js

@@ -0,0 +1,52 @@
1
+import { ossToken } from '@/api/file'
2
+import { random_filename } from '@/utils/file'
3
+import { reactive, ref } from 'vue'
4
+
5
+export function useOss (beforeUp, multiple) {
6
+  let fileUploadData = reactive({
7
+    policy: '',
8
+    OSSAccessKeyId: '',
9
+    success_action_status: '200', // 让服务端返回200,不然,默认会返回204
10
+    callback: '',
11
+    signature: '',
12
+    'x:dir': '',
13
+  })
14
+  const fileExpire = ref(0)
15
+  const fileUploadHost = ref('')
16
+
17
+  const beforeFileUpload = async (file) => {
18
+    if (beforeUp) {
19
+      const br = await beforeUp(file)
20
+      if (!br) { return Promise.reject() }
21
+    }
22
+
23
+    const now = Date.parse(new Date()) / 1000
24
+    if (fileExpire.value < now) {
25
+      const res = await ossToken()
26
+      const obj = JSON.parse(res.data)
27
+      fileExpire.value = parseInt(obj['expire'])
28
+      fileUploadData.policy = obj['policy']
29
+      fileUploadData.OSSAccessKeyId = obj['accessid']
30
+      fileUploadData.callback = obj['callback']
31
+      fileUploadData.signature = obj['signature']
32
+      fileUploadData['x:dir'] = obj['dir']
33
+      fileUploadHost.value = obj['host']
34
+    }
35
+    //多选文件时需要这个,不然每个文件上传的都是一样的data
36
+    if (multiple) {
37
+      await new Promise(resolve => {
38
+        setTimeout(() => { resolve() }, 50)
39
+      })
40
+    }
41
+    fileUploadData['x:origin_filename'] = file.name
42
+    fileUploadData.key = fileUploadData['x:dir'] + random_filename(file.name)
43
+    return Promise.resolve()
44
+  }
45
+
46
+  return {
47
+    fileUploadHost,
48
+    fileUploadData,
49
+    beforeFileUpload,
50
+    headers: {},
51
+  }
52
+}

+ 19 - 0
src/global.js

@@ -0,0 +1,19 @@
1
+import { ref, reactive, watch } from 'vue'
2
+import { list as fetchUsers } from '@/api/user'
3
+
4
+export function loadAllUsers () {
5
+  const allUsers = ref([])
6
+  const getAllUsers = async () => {
7
+    const res = await fetchUsers({ page_size: 9999 }).catch(_ => false)
8
+    if (res) {
9
+      allUsers.value = res.data.list
10
+    }
11
+  }
12
+
13
+  return {
14
+    allUsers,
15
+    getAllUsers,
16
+  }
17
+
18
+}
19
+

+ 20 - 0
src/layout/components/aside.vue

@@ -0,0 +1,20 @@
1
+<template>
2
+  <el-scrollbar class="scroll-sidebar" height="100vh">
3
+    <menus></menus>
4
+  </el-scrollbar>
5
+</template>
6
+<script>
7
+  import Menus from '@/layout/components/menu/index.vue'
8
+  import { defineComponent, ref, onMounted } from 'vue'
9
+
10
+  export default defineComponent({
11
+    name: 'GAside',
12
+    components: { Menus },
13
+  })
14
+</script>
15
+
16
+<style scoped>
17
+  .scroll-sidebar{
18
+    position: fixed;
19
+  }
20
+</style>

+ 72 - 0
src/layout/components/header.vue

@@ -0,0 +1,72 @@
1
+<template>
2
+  <el-icon class="ex-icon" @click="expandOrFoldSlider">
3
+    <el-icon-expand v-if="setting.sideIsCollapse"></el-icon-expand>
4
+    <el-icon-fold v-else></el-icon-fold>
5
+  </el-icon>
6
+  <div class="header-logo">
7
+    <img :src="setting.logo" alt="" class="logo">
8
+    <div class="title">{{setting.title}}</div>
9
+  </div>
10
+  <Setting></Setting>
11
+</template>
12
+
13
+<script>
14
+  import { defineComponent, computed } from 'vue'
15
+  import HeaderMenu from '@/layout/components/menu/index.vue'
16
+  import Setting from '@/layout/components/setting/index.vue'
17
+  import { useAppStore } from '@/store/app'
18
+  import GTags from '@/layout/components/tags/index.vue'
19
+
20
+  export default defineComponent({
21
+    name: 'LayerHeader',
22
+    created () {
23
+    },
24
+    components: { HeaderMenu, Setting, GTags },
25
+    watch: {},
26
+    setup (props) {
27
+      const appStore = useAppStore()
28
+      const setting = computed(() => appStore.setting)
29
+      const expandOrFoldSlider = () => {
30
+        appStore.sideCollapse()
31
+      }
32
+      return {
33
+        setting,
34
+        expandOrFoldSlider,
35
+      }
36
+    },
37
+
38
+  })
39
+</script>
40
+
41
+<style scoped lang="scss">
42
+  .ex-icon {
43
+    height: 100%;
44
+    display: flex;
45
+    align-items: center;
46
+    margin-right: 10px;
47
+    font-size: 16px;
48
+    cursor: pointer;
49
+  }
50
+
51
+  .header-logo {
52
+    display: flex;
53
+    height: 100%;
54
+    align-items: center;
55
+
56
+    .title {
57
+      display: block;
58
+      margin-left: 10px;
59
+    }
60
+
61
+    .logo {
62
+      display: block;
63
+      width: 30px;
64
+      height: 30px;
65
+    }
66
+  }
67
+
68
+
69
+</style>
70
+<style lang="scss">
71
+
72
+</style>

+ 56 - 0
src/layout/components/menu/index.vue

@@ -0,0 +1,56 @@
1
+<template>
2
+  <el-menu
3
+          class="menus"
4
+          :collapse="isCollapse"
5
+          :default-active="activeIndex"
6
+          background-color="#2d3a4b"
7
+          text-color="#fff"
8
+          active-text-color="#409eff"
9
+          router
10
+  >
11
+    <menu-item v-for="(route,index) in routes" :key="route.name" :route="route"></menu-item>
12
+  </el-menu>
13
+</template>
14
+
15
+<script>
16
+  import { defineComponent, ref, onMounted, watch, computed } from 'vue'
17
+  import { useRouteStore } from '@/store/router'
18
+  import MenuItem from '@/layout/components/menu/item.vue'
19
+  import { useRoute } from 'vue-router'
20
+  import { useAppStore } from '@/store/app'
21
+
22
+  export default defineComponent({
23
+    name: 'Menu',
24
+    created () {
25
+    },
26
+    components: { MenuItem },
27
+    setup () {
28
+      const routes = ref([])
29
+      const route = useRoute()
30
+      const app = useAppStore()
31
+      const isCollapse = computed(() => app.setting.sideIsCollapse)
32
+      const activeIndex = computed(() => route.name)
33
+
34
+      routes.value = useRouteStore().routes
35
+      return {
36
+        routes,
37
+        activeIndex,
38
+        isCollapse,
39
+      }
40
+    },
41
+
42
+  })
43
+</script>
44
+
45
+<style lang="scss" scoped>
46
+  .menus {
47
+    min-height: 100vh;
48
+    border-right: none;
49
+    &:not(.el-menu--collapse) {
50
+      width: 210px;
51
+    }
52
+
53
+  }
54
+</style>
55
+<style>
56
+</style>

+ 52 - 0
src/layout/components/menu/item.vue

@@ -0,0 +1,52 @@
1
+<template>
2
+  <el-sub-menu v-if="route.children&&route.children.filter(c=>!c.meta?.hide).length>1&&route.children.some(r => !r.meta?.hide)"
3
+               :key="route.name"
4
+               :index="route.name"
5
+  >
6
+    <template #title>
7
+      <el-icon v-if="route.meta?.icon">
8
+        <component :is="`el-icon-${route.meta.icon}`"></component>
9
+      </el-icon>
10
+      <span>{{route.meta?.title||route.name}}</span>
11
+    </template>
12
+    <menu-item v-for="(_route,_index) in route.children"
13
+               :route="_route"
14
+               :key="_route.name">
15
+    </menu-item>
16
+  </el-sub-menu>
17
+  <el-menu-item v-else-if="!parseRoute(route).meta?.hide" :route="parseRoute(route)" :index="parseRoute(route).name">
18
+    <el-icon v-if="parseRoute(route).meta?.icon">
19
+      <component :is="`el-icon-${parseRoute(route).meta.icon}`"></component>
20
+    </el-icon>
21
+    <span>{{parseRoute(route).meta?.title||parseRoute(route).name}}</span>
22
+  </el-menu-item>
23
+</template>
24
+
25
+<script>
26
+  import { defineComponent } from 'vue'
27
+
28
+  export default defineComponent({
29
+    name: 'MenuItem',
30
+    props: {
31
+      route: {},
32
+    },
33
+    mounted () {
34
+    },
35
+    setup (props) {
36
+      //判断仅有一个子项的route
37
+      const parseRoute = (route) => {
38
+        if (route.children && route.children.filter(c => !c.meta?.hide).length === 1) {
39
+          return route.children.filter(c => !c.meta?.hide)[0]
40
+        } else {
41
+          return route
42
+        }
43
+      }
44
+      return {
45
+        parseRoute,
46
+      }
47
+    },
48
+  })
49
+</script>
50
+
51
+<style lang="scss" scoped>
52
+</style>

+ 140 - 0
src/layout/components/setting/index.vue

@@ -0,0 +1,140 @@
1
+<template>
2
+  <div class="setting">
3
+    <el-dropdown class="menu-item">
4
+      <div class="title">
5
+        <!--        <el-image class="avatar" :src="user.avatar"></el-image>-->
6
+        <span class="nickname">{{ user.username }}</span>
7
+        <el-icon>
8
+          <el-icon-arrow-down/>
9
+        </el-icon>
10
+      </div>
11
+
12
+      <template #dropdown>
13
+        <el-dropdown-menu>
14
+          <el-dropdown-item @click="showChangePwd">修改密码</el-dropdown-item>
15
+          <el-dropdown-item @click="logout">退出登录</el-dropdown-item>
16
+        </el-dropdown-menu>
17
+      </template>
18
+    </el-dropdown>
19
+    <el-dialog v-model="changePwdVisible" width="50%">
20
+      <el-form ref="cpwd" :model="changePwdForm" :rules="chagePwdRules" label-width="120px" style="margin-top: 20px">
21
+        <el-form-item label="旧密码" prop="old_password">
22
+          <el-input v-model="changePwdForm.old_password" show-password></el-input>
23
+        </el-form-item>
24
+        <el-form-item label="新密码" prop="new_password">
25
+          <el-input v-model="changePwdForm.new_password" show-password></el-input>
26
+        </el-form-item>
27
+        <el-form-item label="确认密码" prop="confirmPwd">
28
+          <el-input v-model="changePwdForm.confirmPwd" show-password></el-input>
29
+        </el-form-item>
30
+        <el-form-item>
31
+          <el-button @click="changePwdVisible=false">取消</el-button>
32
+          <el-button type="primary" @click="changePassword">确定</el-button>
33
+        </el-form-item>
34
+      </el-form>
35
+    </el-dialog>
36
+  </div>
37
+</template>
38
+
39
+<script setup>
40
+  import { useUserStore } from '@/store/user'
41
+  import { changeCurPwd } from '@/api/user'
42
+  import { ElMessage, ElMessageBox } from 'element-plus'
43
+  import { reactive, ref } from 'vue'
44
+
45
+  const userStore = useUserStore()
46
+  const user = userStore
47
+
48
+  const logout = () => {
49
+    userStore.logout()
50
+    window.location.reload()
51
+  }
52
+
53
+  const changePwdVisible = ref(false)
54
+  const showChangePwd = () => {
55
+    changePwdVisible.value = true
56
+    changePwdForm.old_password = ''
57
+    changePwdForm.new_password = ''
58
+    changePwdForm.confirmPwd = ''
59
+  }
60
+  const changePwdForm = reactive({
61
+    old_password: '',
62
+    new_password: '',
63
+    confirmPwd: '',
64
+  })
65
+  const chagePwdRules = reactive({
66
+    old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
67
+    new_password: [
68
+      { required: true, message: '请输入新密码', trigger: 'blur' },
69
+      {
70
+        validator: (rule, value, callback) => {
71
+          if (value === changePwdForm.old_password) {
72
+            callback(new Error('新密码不能与旧密码相同'))
73
+          } else {
74
+            callback()
75
+          }
76
+        },
77
+        trigger: 'blur',
78
+      }],
79
+    confirmPwd: [
80
+      { required: true, message: '请再次输入新密码', trigger: 'blur' },
81
+      {
82
+        validator: (rule, value, callback) => {
83
+          if (value !== changePwdForm.new_password) {
84
+            callback(new Error('两次输入密码不一致'))
85
+          } else {
86
+            callback()
87
+          }
88
+        },
89
+        trigger: 'blur',
90
+      },
91
+    ],
92
+  })
93
+  const cpwd = ref(null)
94
+  const changePassword = async () => {
95
+    //验证
96
+    const valid = await cpwd.value.validate().catch(_ => false)
97
+    if (!valid) {
98
+      return
99
+    }
100
+    console.log('changePassword')
101
+    const confirm = await ElMessageBox.confirm('确定修改密码么?', {
102
+      confirmButtonText: '确定',
103
+      cancelButtonText: '取消',
104
+    }).catch(_ => false)
105
+    if (!confirm) {
106
+      return
107
+    }
108
+    const res = await changeCurPwd(changePwdForm).catch(_ => false)
109
+    if (!res) {
110
+      return
111
+    }
112
+    ElMessageBox.alert('修改成功', '修改密码', {
113
+      autofocus: true,
114
+      confirmButtonText: 'OK',
115
+      callback: (action) => {
116
+        logout()
117
+      },
118
+    })
119
+  }
120
+</script>
121
+
122
+<style lang="scss" scoped>
123
+.setting {
124
+  margin-left: auto;
125
+  display: flex;
126
+  align-items: center;
127
+  justify-content: space-around;
128
+
129
+  .title {
130
+    color: #fff;
131
+    display: flex;
132
+    align-items: center;
133
+    justify-content: space-around;
134
+
135
+    .nickname {
136
+      padding: 0 10px;
137
+    }
138
+  }
139
+}
140
+</style>

+ 80 - 0
src/layout/components/tags/index.vue

@@ -0,0 +1,80 @@
1
+<template>
2
+  <el-tag v-for="(t, i) in tags"
3
+          :key="t.name"
4
+          class="tag"
5
+          :closable="t.closeable"
6
+          @close="close(t)"
7
+          @click="toTag(t)"
8
+          :type="t.active?'primary':'info'"
9
+          :effect="t.active?'dark':'plain'">
10
+    {{t.title}}
11
+  </el-tag>
12
+</template>
13
+
14
+<script>
15
+  import { defineComponent, ref, onMounted, watch } from 'vue'
16
+  import { useTagsStore } from '@/store/tags'
17
+  import { useRoute, useRouter } from 'vue-router'
18
+
19
+  export default defineComponent({
20
+    name: 'Index',
21
+    setup () {
22
+      const tags = ref([])
23
+      const tagsStore = useTagsStore()
24
+      const route = useRoute()
25
+      const router = useRouter()
26
+      tags.value = tagsStore.tags
27
+
28
+      const addTag = (route) => {
29
+        if (!route.meta?.hide && route.name) {
30
+          tagsStore.addTag(route)
31
+        }
32
+      }
33
+      const close = (tag) => {
34
+        tagsStore.removeTag(tag)
35
+        if (tag.active) {
36
+          toLastTag()
37
+        }
38
+      }
39
+      const toLastTag = () => {
40
+        if (tags.value.length) {
41
+          router.push({ name: tags.value[tags.value.length - 1].name })
42
+        }
43
+      }
44
+      const init = () => {
45
+        if (!tagsStore.tags.length) {
46
+          tagsStore.initTags()
47
+        }
48
+        addTag(route)
49
+      }
50
+
51
+      const toTag = (tag) => {
52
+        if (tag.name !== route.name) {
53
+          router.push({ name: tag.name })
54
+        }
55
+      }
56
+
57
+      onMounted(init)
58
+      watch(route, (val) => {
59
+        addTag(val)
60
+      })
61
+      return {
62
+        tags,
63
+        addTag,
64
+        close,
65
+        toLastTag,
66
+        toTag,
67
+      }
68
+    },
69
+  })
70
+</script>
71
+
72
+<style lang="scss" scoped>
73
+
74
+  .tag {
75
+    border-radius: 0;
76
+    cursor: pointer;
77
+    &.active {
78
+    }
79
+  }
80
+</style>

+ 85 - 0
src/layout/index.vue

@@ -0,0 +1,85 @@
1
+<template>
2
+  <el-container>
3
+    <el-aside :width="leftWidth" class="app-left">
4
+      <g-aside></g-aside>
5
+    </el-aside>
6
+    <el-container class="app-container ">
7
+      <el-header class="app-header">
8
+        <g-header></g-header>
9
+      </el-header>
10
+      <div class="header-tags">
11
+        <tags></tags>
12
+      </div>
13
+
14
+      <el-main class="app-main">
15
+        <router-view v-slot="{ Component }">
16
+          <transition mode="out-in" name="el-fade-in-linear">
17
+            <keep-alive :include="[...cachedTags]">
18
+              <component :is="Component"/>
19
+            </keep-alive>
20
+          </transition>
21
+        </router-view>
22
+      </el-main>
23
+    </el-container>
24
+  </el-container>
25
+</template>
26
+
27
+<script>
28
+  import { useUserStore } from '@/store/user'
29
+  import { useRouteStore } from '@/store/router'
30
+  import { useAppStore } from '@/store/app'
31
+  import { useTagsStore } from '@/store/tags'
32
+  import LayerHeader from '@/layout/components/header.vue'
33
+  import { defineComponent, ref, onMounted, watch, reactive, computed, toRef } from 'vue'
34
+  import Tags from '@/layout/components/tags/index.vue'
35
+  import GAside from '@/layout/components/aside.vue'
36
+  import GHeader from '@/layout/components/header.vue'
37
+
38
+  export default defineComponent({
39
+    name: 'Layout',
40
+    components: { LayerHeader, Tags, GAside, GHeader },
41
+    setup (props) {
42
+      const userStore = useUserStore()
43
+      const appStore = useAppStore()
44
+      const tagStore = useTagsStore()
45
+
46
+      const leftWidth = computed(() => appStore.setting.sideIsCollapse ? '64px' : '210px')
47
+
48
+      const cachedTags = ref([])
49
+
50
+      cachedTags.value = tagStore.cached
51
+
52
+      return {
53
+        cachedTags,
54
+        leftWidth,
55
+      }
56
+    },
57
+  })
58
+</script>
59
+
60
+<style lang="scss" scoped>
61
+  .app-header {
62
+    background-color: #3f454b;
63
+    color: var(--basicWhite);
64
+    display: flex;
65
+    height: 50px;
66
+  }
67
+
68
+  .header-tags {
69
+    height: auto;
70
+    border-bottom: 1px solid #eee;
71
+    display: flex;
72
+    padding: 0;
73
+  }
74
+
75
+  .app-left {
76
+    height: 100%;
77
+    transition: width 0.5s;
78
+  }
79
+
80
+  .app-container {
81
+    min-height: 100vh;
82
+  }
83
+</style>
84
+
85
+

+ 20 - 0
src/main.js

@@ -0,0 +1,20 @@
1
+import { createApp } from 'vue'
2
+import 'element-plus/dist/index.css'
3
+import App from './App.vue'
4
+import ElementPlus from 'element-plus'
5
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
6
+import { router } from '@/router'
7
+import 'normalize.css/normalize.css'
8
+import { pinia } from '@/store'
9
+import '@/permission'
10
+import '@/styles/style.scss'
11
+import * as ElementIcons from '@element-plus/icons'
12
+
13
+const app = createApp(App)
14
+app.use(ElementPlus, { locale: zhCn })
15
+app.use(pinia)
16
+app.use(router)
17
+for (let icon in ElementIcons){
18
+  app.component("ElIcon" +icon ,ElementIcons[icon])
19
+}
20
+app.mount('#app')

+ 50 - 0
src/permission.js

@@ -0,0 +1,50 @@
1
+import { router } from '@/router'
2
+import { useRouteStore } from '@/store/router'
3
+import { useUserStore } from '@/store/user'
4
+import { getToken } from '@/utils/auth'
5
+import { pinia } from '@/store'
6
+import NProgress from 'nprogress' // progress bar
7
+import 'nprogress/nprogress.css'
8
+import { useAppStore } from '@/store/app' // progress bar style
9
+
10
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
11
+
12
+const whiteList = ['/login']
13
+const routeStore = useRouteStore(pinia)
14
+const appStore = useAppStore(pinia)
15
+router.beforeEach(async (to, from, next) => {
16
+
17
+  document.title = (to.meta?.title || 'Rust-api-web') + '-' + appStore.setting.title
18
+  NProgress.start()
19
+
20
+  const token = getToken()
21
+  if (!token) {
22
+    //无token,跳转到登录
23
+    if (whiteList.indexOf(to.path) !== -1) {
24
+      next()
25
+    } else {
26
+      next(`/login?redirect=${to.path}`)
27
+    }
28
+
29
+  } else {
30
+    //有token
31
+
32
+    const userStore = useUserStore(pinia)
33
+
34
+    if (!userStore.route_names.length) {
35
+      const info = await userStore.info()
36
+      if (!info) {
37
+        userStore.logout()
38
+        next(`/login?redirect=${to.path}`)
39
+      } else {
40
+        next({ ...to, replace: true })
41
+      }
42
+    } else {
43
+      next()
44
+    }
45
+  }
46
+})
47
+
48
+router.afterEach(() => {
49
+  NProgress.done()
50
+})

+ 118 - 0
src/router/index.js

@@ -0,0 +1,118 @@
1
+import { createRouter, createWebHashHistory } from 'vue-router'
2
+
3
+const constantRoutes = [
4
+  {
5
+    path: '/login',
6
+    name: 'Login',
7
+    meta: { title: '登录' },
8
+    component: () => import('@/views/login/login.vue'),
9
+  },
10
+
11
+  {
12
+    path: '/404',
13
+    component: () => import('@/views/error-page/404.vue'),
14
+    hidden: true,
15
+  },
16
+
17
+]
18
+export const asyncRoutes = [
19
+  // {
20
+  //   path: '/',
21
+  //   name: 'Index',
22
+  //   redirect: '/Home',
23
+  //   meta: { title: '首页', icon: 'house' },
24
+  //   component: () => import('@/layout/index.vue'),
25
+  //   children: [
26
+  //     {
27
+  //       path: '/Home',
28
+  //       name: 'Home',
29
+  //       meta: { title: '首页', icon: 'house' },
30
+  //       component: () => import('@/views/index/index.vue'),
31
+  //     },
32
+  //
33
+  //   ],
34
+  // },
35
+  {
36
+    path: '/my',
37
+    name: 'My',
38
+    redirect: '/my/tag/index',
39
+    meta: { title: '我的', icon: 'UserFilled' },
40
+    component: () => import('@/layout/index.vue'),
41
+    children: [
42
+      {
43
+        path: '/',
44
+        name: 'MyAddressBookList',
45
+        meta: { title: '地址簿管理', icon: 'Notebook' /*keepAlive: true*/ },
46
+        component: () => import('@/views/my/address_book/index.vue'),
47
+      },
48
+      {
49
+        path: 'tag/index',
50
+        name: 'MyTagList',
51
+        meta: { title: '标签管理', icon: 'CollectionTag' /*keepAlive: true*/ },
52
+        component: () => import('@/views/my/tag/index.vue'),
53
+      },
54
+    ],
55
+  },
56
+  {
57
+    path: '/user',
58
+    name: 'User',
59
+    redirect: '/user/index',
60
+    meta: { title: '系统', icon: 'Setting' },
61
+    component: () => import('@/layout/index.vue'),
62
+    children: [
63
+      {
64
+        path: 'peer',
65
+        name: 'Peer',
66
+        meta: { title: '设备管理', icon: 'Monitor' /*keepAlive: true*/ },
67
+        component: () => import('@/views/peer/index.vue'),
68
+      },
69
+      {
70
+        path: 'group',
71
+        name: 'UserGroup',
72
+        meta: { title: '群组管理', icon: 'ChatRound' /*keepAlive: true*/ },
73
+        component: () => import('@/views/group/index.vue'),
74
+      },
75
+      {
76
+        path: 'index',
77
+        name: 'UserList',
78
+        meta: { title: '用户列表', icon: 'User' /*keepAlive: true*/ },
79
+        component: () => import('@/views/user/index.vue'),
80
+      },
81
+      {
82
+        path: 'add',
83
+        name: 'UserAdd',
84
+        meta: { title: '用户添加', hide: true },
85
+        component: () => import('@/views/user/edit.vue'),
86
+      },
87
+      {
88
+        path: 'edit/:id',
89
+        name: 'UserEdit',
90
+        meta: { title: '用户编辑', hide: true },
91
+        component: () => import('@/views/user/edit.vue'),
92
+      },
93
+
94
+      {
95
+        path: 'addressBook',
96
+        name: 'UserAddressBook',
97
+        meta: { title: '地址簿管理', icon: 'Notebook' /*keepAlive: true*/ },
98
+        component: () => import('@/views/address_book/index.vue'),
99
+      },
100
+      {
101
+        path: 'tag',
102
+        name: 'UserTag',
103
+        meta: { title: '标签管理', icon: 'CollectionTag' /*keepAlive: true*/ },
104
+        component: () => import('@/views/tag/index.vue'),
105
+      },
106
+
107
+    ],
108
+  },
109
+]
110
+export const lastRoutes = [
111
+  { path: '/:catchAll(.*)', redirect: '/404', meta: { hide: true } },
112
+]
113
+
114
+export const router = createRouter({
115
+  history: createWebHashHistory(),
116
+  routes: constantRoutes,
117
+})
118
+

+ 23 - 0
src/store/app.js

@@ -0,0 +1,23 @@
1
+import { defineStore, acceptHMRUpdate } from 'pinia'
2
+import logo from '@/assets/logo.png'
3
+
4
+export const useAppStore = defineStore({
5
+  id: 'App',
6
+  state: () => ({
7
+    setting: {
8
+      title: 'Gwen-Admin',
9
+      sideIsCollapse: false,
10
+      logo,
11
+    },
12
+  }),
13
+
14
+  actions: {
15
+    sideCollapse () {
16
+      this.setting.sideIsCollapse = !this.setting.sideIsCollapse
17
+    },
18
+  },
19
+})
20
+
21
+if (import.meta.hot) {
22
+  import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot))
23
+}

+ 3 - 0
src/store/index.js

@@ -0,0 +1,3 @@
1
+import { createPinia } from 'pinia'
2
+
3
+export const pinia = createPinia()

+ 64 - 0
src/store/router.js

@@ -0,0 +1,64 @@
1
+import { defineStore, acceptHMRUpdate } from 'pinia'
2
+import { lastRoutes, asyncRoutes, router } from '@/router'
3
+
4
+function filterRoute (routes, enableNames) {
5
+  return routes.filter(route => {
6
+    if (route.children && route.children.length) {
7
+      return enableNames.includes(route.name) || route.children.some(r => enableNames.includes(r.name))
8
+    } else {
9
+      return enableNames.includes(route.name)
10
+    }
11
+  }).map(route => {
12
+    if (route.children && route.children.length) {
13
+      return {
14
+        ...route,
15
+        children: filterRoute(route.children, enableNames),
16
+      }
17
+    } else {
18
+      return { ...route }
19
+    }
20
+  })
21
+}
22
+
23
+export const useRouteStore = defineStore({
24
+  id: 'router',
25
+  state: () => ({
26
+    routes: [],
27
+    activeRoute: '',
28
+    loaded: 0,
29
+    keepAlive: [],
30
+  }),
31
+  actions: {
32
+    addRoutes (accessRouteNames) {
33
+      if (accessRouteNames.includes('*')) {
34
+        this.routes = asyncRoutes
35
+      } else {
36
+        this.routes = filterRoute(asyncRoutes, accessRouteNames)
37
+      }
38
+
39
+      this.routes.forEach(route => {
40
+        router.addRoute(route)
41
+      })
42
+      lastRoutes.forEach(route => {
43
+        router.addRoute(route)
44
+      })
45
+      this.addKeepAlive(this.routes)
46
+    },
47
+    addKeepAlive (route) {
48
+      if (route instanceof Array) {
49
+        route.forEach(r => {
50
+          this.addKeepAlive(r)
51
+        })
52
+      } else if (route.children && route.children.length) {
53
+        this.addKeepAlive(route.children)
54
+      } else if (route.meta?.keepAlive && !this.keepAlive.includes(route.name)) {
55
+        this.keepAlive.push(route.name)
56
+      }
57
+    },
58
+
59
+  },
60
+})
61
+
62
+if (import.meta.hot) {
63
+  import.meta.hot.accept(acceptHMRUpdate(useRouteStore, import.meta.hot))
64
+}

+ 73 - 0
src/store/tags.js

@@ -0,0 +1,73 @@
1
+import { defineStore, acceptHMRUpdate } from 'pinia'
2
+
3
+export const useTagsStore = defineStore({
4
+  id: 'tags',
5
+  state: () => ({
6
+    tags: [],
7
+    cached: [],
8
+  }),
9
+  actions: {
10
+    initTags () {
11
+      this.tags.push(
12
+        {
13
+          name: 'Home',
14
+          path: '/Home',
15
+          title: '首页',
16
+          active: false,
17
+          closeable: false,
18
+          keepAlive: false,
19
+        })
20
+    },
21
+    addTag (route) {
22
+      const tags = this.tags
23
+      if (tags.find(t => t.name === route.name)) {
24
+        tags.forEach(t => t.active = false)
25
+        tags.find(t => t.name === route.name).active = true
26
+      } else {
27
+        tags.forEach(t => t.active = false)
28
+        if (route.meta?.keepAlive) {
29
+          this.addCachedTag(route.name)
30
+        }
31
+        tags.push({
32
+          name: route.name,
33
+          path: route.fullPath,
34
+          title: route.meta?.title || route.name,
35
+          active: true,
36
+          closeable: true,
37
+          keepAlive: route.meta?.keepAlive,
38
+        })
39
+
40
+      }
41
+      this.$patch({ tags })
42
+    },
43
+    removeTag (tag) {
44
+      let tags = this.tags
45
+      if (tags.find(t => t.name === tag.name)) {
46
+        const index = tags.findIndex(t => t.name === tag.name)
47
+        if (index > -1) {
48
+          if (tags[index].keepAlive) {
49
+            this.removeCachedTag(tags[index].name)
50
+          }
51
+          tags.splice(index, 1)
52
+        }
53
+      }
54
+      this.$patch({ tags })
55
+    },
56
+    addCachedTag (name) {
57
+      if (!this.cached.includes(name)) {
58
+        this.cached.push(name)
59
+      }
60
+    },
61
+    removeCachedTag (name) {
62
+      if (this.cached.includes(name)) {
63
+        this.cached.splice(this.cached.indexOf(name), 1)
64
+      }
65
+
66
+    },
67
+
68
+  },
69
+})
70
+
71
+if (import.meta.hot) {
72
+  import.meta.hot.accept(acceptHMRUpdate(useTagsStore, import.meta.hot))
73
+}

+ 62 - 0
src/store/user.js

@@ -0,0 +1,62 @@
1
+import { defineStore, acceptHMRUpdate } from 'pinia'
2
+import { current, login } from '@/api/user'
3
+import { setToken, removeToken } from '@/utils/auth'
4
+import { useRouteStore } from '@/store/router'
5
+
6
+export const useUserStore = defineStore({
7
+  id: 'user',
8
+  state: () => ({
9
+    nickname: '',
10
+    username: '',
11
+    token: '',
12
+    role: '',
13
+    avatar: '',
14
+    route_names: [],
15
+  }),
16
+
17
+  actions: {
18
+    logout () {
19
+      removeToken()
20
+      this.$patch({
21
+        name: '',
22
+        role: {},
23
+      })
24
+    },
25
+
26
+    async login (form) {
27
+      const res = await login(form).catch(_ => false)
28
+      if (res) {
29
+        const userData = res.data
30
+        setToken(userData.token)
31
+        //
32
+        localStorage.setItem('user_info', JSON.stringify({ name: userData.username }))
33
+        this.$patch({
34
+          ...userData,
35
+        })
36
+        if (userData.route_names && userData.route_names.length) {
37
+          useRouteStore().addRoutes(userData.route_names)
38
+        }
39
+        return userData
40
+      } else {
41
+        return false
42
+      }
43
+    },
44
+    async info () {
45
+      const res = await current().catch(_ => false)
46
+      if (res) {
47
+        const userData = res.data
48
+        setToken(userData.token)
49
+        this.$patch({
50
+          ...userData,
51
+        })
52
+        useRouteStore().addRoutes(userData.route_names)
53
+        return userData
54
+      }
55
+      return false
56
+    },
57
+  },
58
+})
59
+
60
+if (import.meta.hot) {
61
+  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
62
+}

+ 20 - 0
src/styles/style.scss

@@ -0,0 +1,20 @@
1
+$basicBlack: #000000;
2
+$basicWhite: #ffffff;
3
+
4
+$primaryColor: #409eff;
5
+$sideBarWidth: 210px;
6
+
7
+:root {
8
+  --basicBlack: #000000;
9
+  --basicWhite: #ffffff;
10
+  --primaryColor: #409eff;
11
+}
12
+
13
+.list-body{
14
+  margin: 10px 0;
15
+}
16
+
17
+.dialog-form{
18
+  max-width: 600px;
19
+  margin: 20px auto;
20
+}

+ 13 - 0
src/utils/auth.js

@@ -0,0 +1,13 @@
1
+const TokenKey = 'access_token'
2
+
3
+export function getToken () {
4
+  return localStorage.getItem(TokenKey)
5
+}
6
+
7
+export function setToken (token) {
8
+  return localStorage.setItem(TokenKey, token)
9
+}
10
+
11
+export function removeToken () {
12
+  return localStorage.removeItem(TokenKey)
13
+}

+ 4 - 0
src/utils/common_options.js

@@ -0,0 +1,4 @@
1
+
2
+
3
+export const ENABLE_STATUS = 1
4
+export const DISABLE_STATUS = 2

+ 34 - 0
src/utils/file.js

@@ -0,0 +1,34 @@
1
+export function get_suffix(filename) {
2
+  var pos = filename.lastIndexOf('.')
3
+  var suffix = ''
4
+  if (pos !== -1) {
5
+    suffix = filename.substring(pos)
6
+  }
7
+  return suffix
8
+}
9
+
10
+export function random_string(len) {
11
+  len = len || 32
12
+  var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
13
+  var maxPos = chars.length
14
+  var pwd = ''
15
+  for (let i = 0; i < len; i++) {
16
+    pwd += chars.charAt(Math.floor(Math.random() * maxPos))
17
+  }
18
+  return pwd
19
+}
20
+
21
+export function random_filename(filename) {
22
+  var suffix = get_suffix(filename)
23
+  var time = new Date()
24
+  var time2 = new Date('2020/01/01')
25
+  return Math.ceil((time.getTime() - time2.getTime()) / 1000) + '_' + random_string(10) + suffix
26
+}
27
+
28
+export function utf8_to_b64(str) {
29
+  return window.btoa(unescape(encodeURIComponent(str)))
30
+}
31
+
32
+export function b64_to_utf8(str) {
33
+  return decodeURIComponent(escape(window.atob(str)))
34
+}

File diff suppressed because it is too large
+ 4272 - 0
src/utils/pca.json


+ 80 - 0
src/utils/request.js

@@ -0,0 +1,80 @@
1
+import axios from 'axios'
2
+import { ElMessage } from 'element-plus'
3
+import { getToken, removeToken } from '@/utils/auth'
4
+import { useUserStore } from '@/store/user'
5
+import { pinia } from '@/store'
6
+
7
+// create an axios instance
8
+const service = axios.create({
9
+  baseURL: import.meta.env.VITE_SERVER_API,
10
+  withCredentials: true, // send cookies when cross-domain requests
11
+  timeout: 50000, // request timeout
12
+})
13
+
14
+// request interceptor
15
+service.interceptors.request.use(
16
+  config => {
17
+    const userStore = useUserStore(pinia)
18
+
19
+    const token = userStore.token || getToken()
20
+    if (token) {
21
+      if (!config.headers) {
22
+        config.headers = {}
23
+      }
24
+      config.headers['api-token'] = token
25
+    }
26
+    return config
27
+  },
28
+  error => {
29
+    // do something with request error
30
+    return Promise.reject(error)
31
+  },
32
+)
33
+
34
+// response interceptor
35
+service.interceptors.response.use(
36
+  /**
37
+   * If you want to get http information such as headers or status
38
+   * Please return  response => response
39
+   */
40
+
41
+  /**
42
+   * Determine the request status by custom code
43
+   * Here is just an example
44
+   * You can also judge the status by HTTP Status Code
45
+   */
46
+  response => {
47
+    const res = response.data
48
+
49
+    // if the custom code is not 20000, it is judged as an error.
50
+    if (res.code !== 0) {
51
+      ElMessage({
52
+        message: res.message || 'error',
53
+        type: 'error',
54
+        duration: 5 * 1000,
55
+      })
56
+
57
+      if (res.code === 403) {
58
+        removeToken()
59
+        window.location.reload()
60
+      }
61
+      return Promise.reject(res.message || 'error')
62
+    } else {
63
+      return res
64
+    }
65
+  },
66
+  error => {
67
+    if (error.code === 'ECONNABORTED'
68
+      && error.message.indexOf('timeout') > -1) {
69
+      error.message = '请求超时!'
70
+    }
71
+    ElMessage({
72
+      message: error.message,
73
+      type: 'error',
74
+      duration: 5 * 1000,
75
+    })
76
+    return Promise.reject(error)
77
+  },
78
+)
79
+
80
+export default service

+ 26 - 0
src/utils/webclient.js

@@ -0,0 +1,26 @@
1
+import { ref } from 'vue'
2
+import { config } from '@/api/rustdesk'
3
+
4
+export const toWebClientLink = (row) => {
5
+  window.open(`${rustdeskConfig.value.api_server}/webclient/#/?id=${row.id}`)
6
+}
7
+
8
+export function loadRustdeskConfig () {
9
+  const rustdeskConfig = ref({})
10
+  const fetchConfig = async () => {
11
+    const res = await config().catch(_ => false)
12
+    if (res) {
13
+      rustdeskConfig.value = res.data
14
+      localStorage.setItem('custom-rendezvous-server', res.data.id_server)
15
+      localStorage.setItem('key', res.data.key)
16
+      localStorage.setItem('api-server', res.data.api_server)
17
+    }
18
+  }
19
+  if (rustdeskConfig.value.id_server === undefined || rustdeskConfig.value.key === undefined) {
20
+    fetchConfig()
21
+  }
22
+  return {
23
+    rustdeskConfig,
24
+  }
25
+}
26
+const { rustdeskConfig } = loadRustdeskConfig()

+ 132 - 0
src/views/address_book/index.js

@@ -0,0 +1,132 @@
1
+import { onActivated, onMounted, reactive, ref, watch } from 'vue'
2
+import { create, list, remove, update } from '@/api/address_book'
3
+import { ElMessage, ElMessageBox } from 'element-plus'
4
+import { useRoute } from 'vue-router'
5
+
6
+export function useRepositories () {
7
+  const route = useRoute()
8
+  const user_id = route.query?.user_id
9
+
10
+  const listRes = reactive({
11
+    list: [], total: 0, loading: false,
12
+  })
13
+  const listQuery = reactive({
14
+    page: 1,
15
+    page_size: 10,
16
+    is_my: 0,
17
+    user_id: user_id ? parseInt(user_id) : null,
18
+  })
19
+
20
+  const getList = async () => {
21
+    listRes.loading = true
22
+    const res = await list(listQuery).catch(_ => false)
23
+    listRes.loading = false
24
+    if (res) {
25
+      listRes.list = res.data.list
26
+      listRes.total = res.data.total
27
+    }
28
+  }
29
+  const handlerQuery = () => {
30
+    if (listQuery.page === 1) {
31
+      getList()
32
+    } else {
33
+      listQuery.page = 1
34
+    }
35
+  }
36
+
37
+  const del = async (row) => {
38
+    const cf = await ElMessageBox.confirm('确定删除么?', {
39
+      confirmButtonText: '确定',
40
+      cancelButtonText: '取消',
41
+      type: 'warning',
42
+    }).catch(_ => false)
43
+    if (!cf) {
44
+      return false
45
+    }
46
+
47
+    const res = await remove({ row_id: row.row_id }).catch(_ => false)
48
+    if (res) {
49
+      ElMessage.success('操作成功')
50
+      getList()
51
+    }
52
+  }
53
+
54
+  const platformList = [
55
+    { label: 'Windows', value: 'Windows' },
56
+    { label: 'Linux', value: 'Linux' },
57
+    { label: 'Mac OS', value: 'Mac OS' },
58
+    { label: 'Android', value: 'Android' },
59
+  ]
60
+  const formVisible = ref(false)
61
+  const formData = reactive({
62
+    'row_id': 0,
63
+    'alias': '',
64
+    'force_always_relay': false,
65
+    'hash': '',
66
+    'hostname': '',
67
+    'id': '',
68
+    'login_name': '',
69
+    'online': false,
70
+    'password': '',
71
+    'platform': '',
72
+    'rdp_port': '',
73
+    'rdp_username': '',
74
+    'same_server': false,
75
+    'tags': [],
76
+    'user_id': null,
77
+    'username': '',
78
+  })
79
+
80
+  const toEdit = (row) => {
81
+    formVisible.value = true
82
+    //将row中的数据赋值给formData
83
+    Object.keys(formData).forEach(key => {
84
+      formData[key] = row[key]
85
+    })
86
+
87
+  }
88
+  const toAdd = () => {
89
+    formVisible.value = true
90
+    //重置formData
91
+    formData.row_id = 0
92
+    formData.alias = ''
93
+    formData.force_always_relay = false
94
+    formData.hash = ''
95
+    formData.hostname = ''
96
+    formData.id = ''
97
+    formData.login_name = ''
98
+    formData.online = false
99
+    formData.password = ''
100
+    formData.platform = ''
101
+    formData.rdp_port = ''
102
+    formData.rdp_username = ''
103
+    formData.same_server = false
104
+    formData.tags = []
105
+    formData.user_id = null
106
+    formData.username = ''
107
+
108
+  }
109
+  const submit = async () => {
110
+    const api = formData.row_id ? update : create
111
+    const res = await api(formData).catch(_ => false)
112
+    if (res) {
113
+      ElMessage.success('操作成功')
114
+      formVisible.value = false
115
+      getList()
116
+    }
117
+  }
118
+
119
+  return {
120
+    listRes,
121
+    listQuery,
122
+    getList,
123
+    handlerQuery,
124
+    del,
125
+    platformList,
126
+    formVisible,
127
+    formData,
128
+    toEdit,
129
+    toAdd,
130
+    submit,
131
+  }
132
+}

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

@@ -0,0 +1,212 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item label="用户">
6
+          <el-select v-model="listQuery.user_id" clearable>
7
+            <el-option
8
+                v-for="item in allUsers"
9
+                :key="item.id"
10
+                :label="item.username"
11
+                :value="item.id"
12
+            ></el-option>
13
+          </el-select>
14
+        </el-form-item>
15
+        <el-form-item>
16
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
17
+          <el-button type="danger" @click="toAdd">添加</el-button>
18
+        </el-form-item>
19
+      </el-form>
20
+    </el-card>
21
+    <el-card class="list-body" shadow="hover">
22
+      <!--      <el-tag type="danger" style="margin-bottom: 10px">不建议在此操作地址簿,可能会造成数据不同步</el-tag>-->
23
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
24
+        <el-table-column prop="id" label="id" align="center"/>
25
+        <el-table-column label="所属用户" align="center">
26
+          <template #default="{row}">
27
+            <span v-if="row.user_id"> <el-tag>{{ allUsers?.find(u => u.id === row.user_id)?.username }}</el-tag> </span>
28
+          </template>
29
+        </el-table-column>
30
+        <el-table-column prop="username" label="用户名" align="center"/>
31
+        <el-table-column prop="hostname" label="主机名" align="center"/>
32
+        <el-table-column prop="alias" label="别名" align="center"/>
33
+        <el-table-column prop="platform" label="平台" align="center"/>
34
+        <el-table-column prop="hash" label="hash" align="center"/>
35
+        <el-table-column prop="tags" label="标签" align="center"/>
36
+        <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
37
+        <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
38
+        <el-table-column label="操作" align="center" width="400">
39
+          <template #default="{row}">
40
+            <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
41
+            <el-button @click="toEdit(row)">编辑</el-button>
42
+            <el-button type="danger" @click="del(row)">删除</el-button>
43
+          </template>
44
+        </el-table-column>
45
+      </el-table>
46
+    </el-card>
47
+    <el-card class="list-page" shadow="hover">
48
+      <el-pagination background
49
+                     layout="prev, pager, next, sizes, jumper"
50
+                     :page-sizes="[10,20,50,100]"
51
+                     v-model:page-size="listQuery.page_size"
52
+                     v-model:current-page="listQuery.page"
53
+                     :total="listRes.total">
54
+      </el-pagination>
55
+    </el-card>
56
+    <el-dialog v-model="formVisible" width="800" :title="!formData.row_id?'创建':'修改'">
57
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
58
+        <el-form-item label="用户" prop="user_id" required>
59
+          <el-select v-model="formData.user_id" @change="changeUser">
60
+            <el-option
61
+                v-for="item in allUsers"
62
+                :key="item.id"
63
+                :label="item.username"
64
+                :value="item.id"
65
+            ></el-option>
66
+          </el-select>
67
+        </el-form-item>
68
+        <el-form-item label="id" prop="id" required>
69
+          <el-input v-model="formData.id"></el-input>
70
+        </el-form-item>
71
+        <el-form-item label="用户名" prop="username">
72
+          <el-input v-model="formData.username"></el-input>
73
+        </el-form-item>
74
+        <el-form-item label="别名" prop="alias">
75
+          <el-input v-model="formData.alias"></el-input>
76
+        </el-form-item>
77
+        <el-form-item label="hash" prop="hash">
78
+          <el-input v-model="formData.hash"></el-input>
79
+        </el-form-item>
80
+        <el-form-item label="主机名" prop="hostname">
81
+          <el-input v-model="formData.hostname"></el-input>
82
+        </el-form-item>
83
+        <el-form-item label="登录名" prop="login_name">
84
+          <el-input v-model="formData.login_name"></el-input>
85
+        </el-form-item>
86
+        <el-form-item label="密码" prop="password">
87
+          <el-input v-model="formData.password"></el-input>
88
+        </el-form-item>
89
+        <el-form-item label="平台" prop="platform">
90
+          <el-select v-model="formData.platform">
91
+            <el-option
92
+                v-for="item in platformList"
93
+                :key="item.value"
94
+                :label="item.label"
95
+                :value="item.value"
96
+            ></el-option>
97
+          </el-select>
98
+        </el-form-item>
99
+
100
+        <el-form-item label="标签" prop="tags">
101
+          <el-select v-model="formData.tags" multiple>
102
+            <el-option
103
+                v-for="item in tagList"
104
+                :key="item.name"
105
+                :label="item.name"
106
+                :value="item.name"
107
+            ></el-option>
108
+          </el-select>
109
+        </el-form-item>
110
+        <!-- <el-form-item label="强制中继" prop="force_always_relay" required>
111
+                <el-switch v-model="formData.force_always_relay"></el-switch>
112
+              </el-form-item>
113
+         <el-form-item label="在线" prop="online">
114
+                <el-switch v-model="formData.online"></el-switch>
115
+              </el-form-item>
116
+              <el-form-item label="rdp端口" prop="rdp_port">
117
+                <el-input v-model="formData.rdp_port"></el-input>
118
+              </el-form-item>
119
+              <el-form-item label="rdp用户名" prop="rdp_username">
120
+                <el-input v-model="formData.rdp_username"></el-input>
121
+              </el-form-item>
122
+              <el-form-item label="同一服务器" prop="same_server">
123
+                <el-switch v-model="formData.same_server"></el-switch>
124
+              </el-form-item>-->
125
+
126
+
127
+        <el-form-item>
128
+          <el-button @click="formVisible = false">取消</el-button>
129
+          <el-button @click="submit" type="primary">提交</el-button>
130
+        </el-form-item>
131
+      </el-form>
132
+    </el-dialog>
133
+  </div>
134
+</template>
135
+
136
+<script setup>
137
+  import { onActivated, onMounted, reactive, ref, watch } from 'vue'
138
+  import { create, list, remove, update } from '@/api/address_book'
139
+  import { list as fetchTagList } from '@/api/tag'
140
+  import { ElMessage, ElMessageBox } from 'element-plus'
141
+  import { loadAllUsers } from '@/global'
142
+  import { useRoute } from 'vue-router'
143
+  import { useRepositories } from '@/views/address_book/index'
144
+  import { toWebClientLink } from '@/utils/webclient'
145
+
146
+  const { allUsers, getAllUsers } = loadAllUsers()
147
+  getAllUsers()
148
+  const changeUser = (v) => {
149
+    formData.tags = []
150
+    fetchTagListData(v)
151
+  }
152
+  const tagList = ref([])
153
+  const fetchTagListData = async (user_id) => {
154
+    const res = await fetchTagList({ user_id }).catch(_ => false)
155
+    if (res) {
156
+      tagList.value = res.data.list
157
+    }
158
+  }
159
+
160
+  const {
161
+    listRes,
162
+    listQuery,
163
+    getList,
164
+    handlerQuery,
165
+    del,
166
+    formVisible,
167
+    platformList,
168
+    formData,
169
+    toEdit,
170
+    toAdd,
171
+    submit,
172
+    activeChange,
173
+    currentColor,
174
+  } = useRepositories()
175
+
176
+  onMounted(getList)
177
+  onActivated(getList)
178
+
179
+  watch(() => listQuery.page, getList)
180
+
181
+  watch(() => listQuery.page_size, handlerQuery)
182
+
183
+</script>
184
+
185
+<style scoped lang="scss">
186
+.list-query .el-select {
187
+  --el-select-width: 160px;
188
+}
189
+
190
+.colors {
191
+  display: flex;
192
+  justify-content: center;
193
+  align-items: center;
194
+
195
+  .colorbox {
196
+    width: 50px;
197
+    height: 30px;
198
+    display: flex;
199
+    justify-content: center;
200
+    align-items: center;
201
+
202
+    .dot {
203
+      width: 10px;
204
+      height: 10px;
205
+      display: block;
206
+      border-radius: 50%;
207
+    }
208
+  }
209
+
210
+}
211
+
212
+</style>

+ 13 - 0
src/views/error-page/404.vue

@@ -0,0 +1,13 @@
1
+<template>
2
+  <h1>404</h1>
3
+</template>
4
+
5
+<script>
6
+  export default {
7
+    name: '404',
8
+  }
9
+</script>
10
+
11
+<style scoped>
12
+
13
+</style>

+ 149 - 0
src/views/group/index.vue

@@ -0,0 +1,149 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <!--        <el-form-item label="名称">
6
+                  <el-input v-model="listQuery.name"></el-input>
7
+                </el-form-item>-->
8
+        <el-form-item>
9
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
10
+          <el-button type="danger" @click="toAdd">添加</el-button>
11
+        </el-form-item>
12
+      </el-form>
13
+    </el-card>
14
+    <el-card class="list-body" shadow="hover">
15
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
16
+        <el-table-column prop="id" label="id" align="center"></el-table-column>
17
+        <el-table-column prop="name" label="名称" align="center"/>
18
+        <el-table-column prop="created_at" label="创建时间" align="center"/>
19
+        <el-table-column prop="updated_at" label="更新时间" align="center"/>
20
+        <el-table-column label="操作" align="center">
21
+          <template #default="{row}">
22
+            <el-button @click="toEdit(row)">编辑</el-button>
23
+            <el-button type="danger" @click="del(row)">删除</el-button>
24
+          </template>
25
+        </el-table-column>
26
+      </el-table>
27
+    </el-card>
28
+    <el-card class="list-page" shadow="hover">
29
+      <el-pagination background
30
+                     layout="prev, pager, next, sizes, jumper"
31
+                     :page-sizes="[10,20,50,100]"
32
+                     v-model:page-size="listQuery.page_size"
33
+                     v-model:current-page="listQuery.page"
34
+                     :total="listRes.total">
35
+      </el-pagination>
36
+    </el-card>
37
+    <el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
38
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
39
+        <el-form-item label="名称" prop="name" required>
40
+          <el-input v-model="formData.name"></el-input>
41
+        </el-form-item>
42
+        <el-form-item label="类型" prop="type" required>
43
+          <el-radio-group v-model="formData.type">
44
+            <el-radio v-for="item in groupTypes" :key="item.value" :label="item.value" style="display: block">
45
+              {{ item.label }}
46
+            <span style="font-size: 12px;color: #999">{{item.note}}</span>
47
+            </el-radio>
48
+          </el-radio-group>
49
+        </el-form-item>
50
+        <el-form-item>
51
+          <el-button @click="formVisible = false">取消</el-button>
52
+          <el-button @click="submit" type="primary">提交</el-button>
53
+        </el-form-item>
54
+      </el-form>
55
+    </el-dialog>
56
+  </div>
57
+</template>
58
+
59
+<script setup>
60
+  import { onMounted, reactive, watch, ref, onActivated } from 'vue'
61
+  import { list, create, update, detail, remove } from '@/api/group'
62
+  import { ElMessage, ElMessageBox } from 'element-plus'
63
+
64
+  const listRes = reactive({
65
+    list: [], total: 0, loading: false,
66
+  })
67
+  const listQuery = reactive({
68
+    page: 1,
69
+    page_size: 10,
70
+  })
71
+
72
+  const getList = async () => {
73
+    listRes.loading = true
74
+    const res = await list(listQuery).catch(_ => false)
75
+    listRes.loading = false
76
+    if (res) {
77
+      listRes.list = res.data.list
78
+      listRes.total = res.data.total
79
+    }
80
+  }
81
+  const handlerQuery = () => {
82
+    if (listQuery.page === 1) {
83
+      getList()
84
+    } else {
85
+      listQuery.page = 1
86
+    }
87
+  }
88
+
89
+  const del = async (row) => {
90
+    const cf = await ElMessageBox.confirm('确定删除么?', {
91
+      confirmButtonText: '确定',
92
+      cancelButtonText: '取消',
93
+      type: 'warning',
94
+    }).catch(_ => false)
95
+    if (!cf) {
96
+      return false
97
+    }
98
+
99
+    const res = await remove({ id: row.id }).catch(_ => false)
100
+    if (res) {
101
+      ElMessage.success('操作成功')
102
+      getList()
103
+    }
104
+  }
105
+  onMounted(getList)
106
+  onActivated(getList)
107
+
108
+  watch(() => listQuery.page, getList)
109
+
110
+  watch(() => listQuery.page_size, handlerQuery)
111
+
112
+  const groupTypes = [
113
+    { label: '普通组', value: 1, note: '只有管理员能看到小组成员和成员地址簿' },
114
+    { label: '共享组', value: 2, note: '所有用户都能看到小组成员和成员地址簿' },
115
+  ]
116
+  const formVisible = ref(false)
117
+  const formData = reactive({
118
+    id: 0,
119
+    name: '',
120
+    type: 1
121
+  })
122
+
123
+  const toEdit = (row) => {
124
+    formVisible.value = true
125
+    formData.id = row.id
126
+    formData.name = row.name
127
+    formData.type = row.type
128
+  }
129
+  const toAdd = () => {
130
+    formVisible.value = true
131
+    formData.id = 0
132
+    formData.name = ''
133
+    formData.type = 1
134
+  }
135
+  const submit = async () => {
136
+    const api = formData.id ? update : create
137
+    const res = await api(formData).catch(_ => false)
138
+    if (res) {
139
+      ElMessage.success('操作成功')
140
+      formVisible.value = false
141
+      getList()
142
+    }
143
+  }
144
+
145
+</script>
146
+
147
+<style scoped lang="scss">
148
+
149
+</style>

+ 75 - 0
src/views/index/index.vue

@@ -0,0 +1,75 @@
1
+<template>
2
+  <div class="index">
3
+  </div>
4
+</template>
5
+
6
+<script>
7
+  import { defineComponent, ref, onMounted } from 'vue'
8
+
9
+  export default defineComponent({
10
+    name: 'Home',
11
+    setup () {
12
+      const todoList = ref([
13
+        {title:'修复bug'},
14
+        {title:'修复bug'},
15
+        {title:'修复bug'},
16
+        {title:'增加新功能'},
17
+      ])
18
+      return {
19
+        todoList
20
+      }
21
+    },
22
+  })
23
+</script>
24
+
25
+<style scoped lang="scss">
26
+  .index {
27
+    .counts {
28
+      display: flex;
29
+      justify-content: space-between;
30
+
31
+      .item {
32
+        width: 32.5%;
33
+
34
+        .num {
35
+          font-size: 28px;
36
+          display: flex;
37
+          justify-content: space-around;
38
+          align-items: center;
39
+
40
+          .before, .after {
41
+            font-size: 18px;
42
+
43
+            .exp {
44
+              font-size: 12px;
45
+              margin-right: 10px;
46
+            }
47
+
48
+            .red {
49
+              color: red;
50
+            }
51
+
52
+            .green {
53
+              color: green;
54
+            }
55
+          }
56
+
57
+          .middle {
58
+            .exp {
59
+              font-size: 20px;
60
+              margin-right: 10px;
61
+            }
62
+
63
+            span + span {
64
+              font-weight: bold;
65
+            }
66
+          }
67
+        }
68
+      }
69
+
70
+    }
71
+    .lans, .todo{
72
+      height: 250px
73
+    }
74
+  }
75
+</style>

+ 91 - 0
src/views/login/login.vue

@@ -0,0 +1,91 @@
1
+<template>
2
+  <div class="login">
3
+    <el-card class="login-card">
4
+      <h1>登录</h1>
5
+      <el-form label-width="60px">
6
+        <el-form-item label="用户名">
7
+          <el-input v-model="form.username"></el-input>
8
+        </el-form-item>
9
+        <el-form-item label="密码">
10
+          <el-input v-model="form.password" type="password" @keyup.enter.native="login" show-password></el-input>
11
+        </el-form-item>
12
+        <el-form-item>
13
+          <el-button @click="login" type="primary">登录</el-button>
14
+        </el-form-item>
15
+      </el-form>
16
+    </el-card>
17
+  </div>
18
+</template>
19
+
20
+<script>
21
+  import { defineComponent, reactive } from 'vue'
22
+  import { useUserStore } from '@/store/user'
23
+  import { ElMessage } from 'element-plus'
24
+  import { useRoute, useRouter } from 'vue-router'
25
+
26
+  export default defineComponent({
27
+    setup (props) {
28
+      const userStore = useUserStore()
29
+      const route = useRoute()
30
+      const router = useRouter()
31
+      const form = reactive({
32
+        username: 'admin',
33
+        password: 'admin',
34
+      })
35
+      const redirect = route.query?.redirect
36
+      const login = async () => {
37
+        const res = await userStore.login(form)
38
+        if (res) {
39
+          ElMessage.success('登录成功')
40
+          router.push({ path: redirect || '/', replace: true })
41
+        }
42
+      }
43
+      return {
44
+        redirect,
45
+        form,
46
+        login,
47
+      }
48
+    },
49
+  })
50
+</script>
51
+
52
+<style scoped lang="scss">
53
+  .login {
54
+    width: 100vw;
55
+    height: 100vh;
56
+    background-color: #2d3a4b;
57
+    padding-top: 200px;
58
+    box-sizing: border-box;
59
+    .tips {
60
+      font-size: 12px;
61
+      color: #fff;
62
+      margin-left: 60px;
63
+    }
64
+
65
+    .login-card {
66
+      width: 500px;
67
+      background-color: #283342;
68
+      color: #fff;
69
+      border: none;
70
+      margin: 0 auto;
71
+      .el-form-item {
72
+
73
+        ::v-deep(.el-form-item__label) {
74
+          color: #fff;
75
+        }
76
+
77
+        .el-input {
78
+
79
+          ::v-deep(.el-input__wrapper) {
80
+            border: 1px solid rgba(255, 255, 255, 0.1);
81
+            background: transparent;
82
+          }
83
+
84
+          ::v-deep(input) {
85
+            color: #fff;
86
+          }
87
+        }
88
+      }
89
+    }
90
+  }
91
+</style>

+ 204 - 0
src/views/my/address_book/index.vue

@@ -0,0 +1,204 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item>
6
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
7
+          <el-button type="danger" @click="toAdd">添加</el-button>
8
+        </el-form-item>
9
+      </el-form>
10
+    </el-card>
11
+    <el-card class="list-body" shadow="hover">
12
+      <!--      <el-tag type="danger" style="margin-bottom: 10px">不建议在此操作地址簿,可能会造成数据不同步</el-tag>-->
13
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
14
+        <el-table-column prop="id" label="id" align="center"/>
15
+        <el-table-column prop="username" label="用户名" align="center"/>
16
+        <el-table-column prop="hostname" label="主机名" align="center"/>
17
+        <el-table-column prop="alias" label="别名" align="center"/>
18
+        <el-table-column prop="platform" label="平台" align="center"/>
19
+        <el-table-column prop="hash" label="hash" align="center"/>
20
+        <el-table-column prop="tags" label="标签" align="center"/>
21
+        <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
22
+        <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
23
+        <el-table-column label="操作" align="center" width="400">
24
+          <template #default="{row}">
25
+            <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
26
+            <el-button @click="toEdit(row)">编辑</el-button>
27
+            <el-button type="danger" @click="del(row)">删除</el-button>
28
+          </template>
29
+        </el-table-column>
30
+      </el-table>
31
+    </el-card>
32
+    <el-card class="list-page" shadow="hover">
33
+      <el-pagination background
34
+                     layout="prev, pager, next, sizes, jumper"
35
+                     :page-sizes="[10,20,50,100]"
36
+                     v-model:page-size="listQuery.page_size"
37
+                     v-model:current-page="listQuery.page"
38
+                     :total="listRes.total">
39
+      </el-pagination>
40
+    </el-card>
41
+    <el-dialog v-model="formVisible" width="800" :title="!formData.row_id?'创建':'修改'">
42
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
43
+        <el-form-item label="id" prop="id" required>
44
+          <el-input v-model="formData.id"></el-input>
45
+        </el-form-item>
46
+        <el-form-item label="用户名" prop="username">
47
+          <el-input v-model="formData.username"></el-input>
48
+        </el-form-item>
49
+        <el-form-item label="别名" prop="alias">
50
+          <el-input v-model="formData.alias"></el-input>
51
+        </el-form-item>
52
+        <el-form-item label="hash" prop="hash">
53
+          <el-input v-model="formData.hash"></el-input>
54
+        </el-form-item>
55
+        <el-form-item label="主机名" prop="hostname">
56
+          <el-input v-model="formData.hostname"></el-input>
57
+        </el-form-item>
58
+        <el-form-item label="登录名" prop="login_name">
59
+          <el-input v-model="formData.login_name"></el-input>
60
+        </el-form-item>
61
+        <el-form-item label="密码" prop="password">
62
+          <el-input v-model="formData.password"></el-input>
63
+        </el-form-item>
64
+        <el-form-item label="平台" prop="platform">
65
+          <el-select v-model="formData.platform">
66
+            <el-option
67
+                v-for="item in platformList"
68
+                :key="item.value"
69
+                :label="item.label"
70
+                :value="item.value"
71
+            ></el-option>
72
+          </el-select>
73
+        </el-form-item>
74
+
75
+        <el-form-item label="标签" prop="tags">
76
+          <el-select v-model="formData.tags" multiple>
77
+            <el-option
78
+                v-for="item in tagList"
79
+                :key="item.name"
80
+                :label="item.name"
81
+                :value="item.name"
82
+            ></el-option>
83
+          </el-select>
84
+        </el-form-item>
85
+        <!-- <el-form-item label="强制中继" prop="force_always_relay" required>
86
+                <el-switch v-model="formData.force_always_relay"></el-switch>
87
+              </el-form-item>
88
+         <el-form-item label="在线" prop="online">
89
+                <el-switch v-model="formData.online"></el-switch>
90
+              </el-form-item>
91
+              <el-form-item label="rdp端口" prop="rdp_port">
92
+                <el-input v-model="formData.rdp_port"></el-input>
93
+              </el-form-item>
94
+              <el-form-item label="rdp用户名" prop="rdp_username">
95
+                <el-input v-model="formData.rdp_username"></el-input>
96
+              </el-form-item>
97
+              <el-form-item label="同一服务器" prop="same_server">
98
+                <el-switch v-model="formData.same_server"></el-switch>
99
+              </el-form-item>-->
100
+
101
+
102
+        <el-form-item>
103
+          <el-button @click="formVisible = false">取消</el-button>
104
+          <el-button @click="submit" type="primary">提交</el-button>
105
+        </el-form-item>
106
+      </el-form>
107
+    </el-dialog>
108
+  </div>
109
+</template>
110
+
111
+<script setup>
112
+  import { onActivated, onMounted, reactive, ref, watch } from 'vue'
113
+  import { create, list, remove, update } from '@/api/address_book'
114
+  import { list as fetchTagList } from '@/api/tag'
115
+  import { useRepositories } from '@/views/address_book'
116
+  import { toWebClientLink } from '@/utils/webclient'
117
+
118
+  const tagList = ref([])
119
+  const fetchTagListData = async () => {
120
+    const res = await fetchTagList({ is_my: 1 }).catch(_ => false)
121
+    if (res) {
122
+      tagList.value = res.data.list
123
+    }
124
+  }
125
+  fetchTagListData()
126
+
127
+  const {
128
+    listRes,
129
+    listQuery,
130
+    getList,
131
+    handlerQuery,
132
+    del,
133
+    formVisible,
134
+    platformList,
135
+    formData,
136
+    toEdit,
137
+    toAdd,
138
+    submit,
139
+    activeChange,
140
+    currentColor,
141
+  } = useRepositories()
142
+
143
+  listQuery.is_my = 1
144
+
145
+  onMounted(getList)
146
+  onActivated(getList)
147
+
148
+  watch(() => listQuery.page, getList)
149
+
150
+  watch(() => listQuery.page_size, handlerQuery)
151
+
152
+  watch(() => listRes.list, () => {
153
+        const peers = {}
154
+        listRes.list.map(item => {
155
+          peers[item.id] = {
156
+            'view-style': 'shrink',
157
+            tm: new Date().getTime(),
158
+            info: {
159
+              'id': item.id,
160
+              'username': item.username,
161
+              'hostname': item.hostname,
162
+              'alias': item.alias,
163
+              'platform': item.platform,
164
+              'hash': item.hash,
165
+              'tags': item.tags,
166
+            },
167
+          }
168
+        })
169
+        localStorage.setItem('peers', JSON.stringify(peers))
170
+      },
171
+      {
172
+        immediate: true,
173
+      })
174
+
175
+</script>
176
+
177
+<style scoped lang="scss">
178
+.list-query .el-select {
179
+  --el-select-width: 160px;
180
+}
181
+
182
+.colors {
183
+  display: flex;
184
+  justify-content: center;
185
+  align-items: center;
186
+
187
+  .colorbox {
188
+    width: 50px;
189
+    height: 30px;
190
+    display: flex;
191
+    justify-content: center;
192
+    align-items: center;
193
+
194
+    .dot {
195
+      width: 10px;
196
+      height: 10px;
197
+      display: block;
198
+      border-radius: 50%;
199
+    }
200
+  }
201
+
202
+}
203
+
204
+</style>

+ 133 - 0
src/views/my/tag/index.vue

@@ -0,0 +1,133 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item>
6
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
7
+          <el-button type="danger" @click="toAdd">添加</el-button>
8
+        </el-form-item>
9
+      </el-form>
10
+    </el-card>
11
+    <el-card class="list-body" shadow="hover">
12
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
13
+        <el-table-column prop="id" label="id" align="center"/>
14
+        <el-table-column prop="name" label="名称" align="center"/>
15
+        <el-table-column prop="color" label="颜色" align="center">
16
+          <template #default="{row}">
17
+            <div class="colors">
18
+              <div style="background-color: #efeff2" class="colorbox">
19
+                <div :style="{backgroundColor: row.color}" class="dot">
20
+                </div>
21
+              </div>
22
+              <div style="background-color: #24252b" class="colorbox">
23
+                <div :style="{backgroundColor: row.color}" class="dot">
24
+                </div>
25
+              </div>
26
+            </div>
27
+          </template>
28
+        </el-table-column>
29
+        <el-table-column prop="created_at" label="创建时间" align="center"/>
30
+        <el-table-column prop="updated_at" label="更新时间" align="center"/>
31
+        <el-table-column label="操作" align="center">
32
+          <template #default="{row}">
33
+            <el-button @click="toEdit(row)">编辑</el-button>
34
+            <el-button type="danger" @click="del(row)">删除</el-button>
35
+          </template>
36
+        </el-table-column>
37
+      </el-table>
38
+    </el-card>
39
+    <el-card class="list-page" shadow="hover">
40
+      <el-pagination background
41
+                     layout="prev, pager, next, sizes, jumper"
42
+                     :page-sizes="[10,20,50,100]"
43
+                     v-model:page-size="listQuery.page_size"
44
+                     v-model:current-page="listQuery.page"
45
+                     :total="listRes.total">
46
+      </el-pagination>
47
+    </el-card>
48
+    <el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
49
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
50
+        <el-form-item label="名称" prop="name" required>
51
+          <el-input v-model="formData.name"></el-input>
52
+        </el-form-item>
53
+        <el-form-item label="颜色" prop="color" required>
54
+          <el-color-picker v-model="formData.color" show-alpha @active-change="activeChange"></el-color-picker>
55
+          <br>
56
+          <div class="colors">
57
+            <div style="background-color: #efeff2" class="colorbox">
58
+              <div :style="{backgroundColor: currentColor}" class="dot">
59
+              </div>
60
+            </div>
61
+            <div style="background-color: #24252b" class="colorbox">
62
+              <div :style="{backgroundColor: currentColor}" class="dot">
63
+              </div>
64
+            </div>
65
+          </div>
66
+        </el-form-item>
67
+        <el-form-item>
68
+          <el-button @click="formVisible = false">取消</el-button>
69
+          <el-button @click="submit" type="primary">提交</el-button>
70
+        </el-form-item>
71
+      </el-form>
72
+    </el-dialog>
73
+  </div>
74
+</template>
75
+
76
+<script setup>
77
+  import { onMounted, watch, onActivated } from 'vue'
78
+  import { useRepositories } from '@/views/tag'
79
+
80
+  const {
81
+    listRes,
82
+    listQuery,
83
+    getList,
84
+    handlerQuery,
85
+    del,
86
+    formVisible,
87
+    formData,
88
+    toEdit,
89
+    toAdd,
90
+    submit,
91
+    activeChange,
92
+    currentColor,
93
+  } = useRepositories()
94
+
95
+  listQuery.is_my = 1
96
+
97
+  onMounted(getList)
98
+  onActivated(getList)
99
+
100
+  watch(() => listQuery.page, getList)
101
+
102
+  watch(() => listQuery.page_size, handlerQuery)
103
+
104
+</script>
105
+
106
+<style scoped lang="scss">
107
+.list-query .el-select {
108
+  --el-select-width: 160px;
109
+}
110
+
111
+.colors {
112
+  display: flex;
113
+  justify-content: center;
114
+  align-items: center;
115
+
116
+  .colorbox {
117
+    width: 50px;
118
+    height: 30px;
119
+    display: flex;
120
+    justify-content: center;
121
+    align-items: center;
122
+
123
+    .dot {
124
+      width: 10px;
125
+      height: 10px;
126
+      display: block;
127
+      border-radius: 50%;
128
+    }
129
+  }
130
+
131
+}
132
+
133
+</style>

+ 211 - 0
src/views/peer/index.vue

@@ -0,0 +1,211 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item>
6
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
7
+          <el-button type="danger" @click="toAdd">添加</el-button>
8
+        </el-form-item>
9
+      </el-form>
10
+    </el-card>
11
+    <el-card class="list-body" shadow="hover">
12
+      <el-table :data="listRes.list" v-loading="listRes.loading" border size="small">
13
+        <el-table-column prop="id" label="id" align="center"/>
14
+        <el-table-column prop="cpu" label="cpu" align="center"/>
15
+        <el-table-column prop="hostname" label="主机名" align="center"/>
16
+        <el-table-column prop="memory" label="内存" align="center"/>
17
+        <el-table-column prop="os" label="系统" align="center"/>
18
+        <el-table-column prop="username" label="username" align="center"/>
19
+        <el-table-column prop="uuid" label="uuid" align="center"/>
20
+        <el-table-column prop="version" label="版本号" align="center"/>
21
+        <el-table-column prop="created_at" label="创建时间" align="center"/>
22
+        <el-table-column prop="updated_at" label="更新时间" align="center"/>
23
+        <el-table-column label="操作" align="center" width="400">
24
+          <template #default="{row}">
25
+            <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
26
+            <el-button @click="toEdit(row)">编辑</el-button>
27
+            <el-button type="danger" @click="del(row)">删除</el-button>
28
+          </template>
29
+        </el-table-column>
30
+      </el-table>
31
+    </el-card>
32
+    <el-card class="list-page" shadow="hover">
33
+      <el-pagination background
34
+                     layout="prev, pager, next, sizes, jumper"
35
+                     :page-sizes="[10,20,50,100]"
36
+                     v-model:page-size="listQuery.page_size"
37
+                     v-model:current-page="listQuery.page"
38
+                     :total="listRes.total">
39
+      </el-pagination>
40
+    </el-card>
41
+    <el-dialog v-model="formVisible" :title="!formData.row_id?'创建':'修改'" width="800">
42
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
43
+        <el-form-item label="id" prop="id" required>
44
+          <el-input v-model="formData.id"></el-input>
45
+        </el-form-item>
46
+        <el-form-item label="用户名" prop="username">
47
+          <el-input v-model="formData.username"></el-input>
48
+        </el-form-item>
49
+        <el-form-item label="主机名" prop="hostname">
50
+          <el-input v-model="formData.hostname"></el-input>
51
+        </el-form-item>
52
+        <el-form-item label="cpu" prop="cpu">
53
+          <el-input v-model="formData.cpu"></el-input>
54
+        </el-form-item>
55
+        <el-form-item label="内存" prop="memory">
56
+          <el-input v-model="formData.memory"></el-input>
57
+        </el-form-item>
58
+        <el-form-item label="系统" prop="os">
59
+          <el-input v-model="formData.os"></el-input>
60
+        </el-form-item>
61
+        <el-form-item label="uuid" prop="uuid">
62
+          <el-input v-model="formData.uuid"></el-input>
63
+        </el-form-item>
64
+        <el-form-item label="版本" prop="version">
65
+          <el-input v-model="formData.version"></el-input>
66
+        </el-form-item>
67
+
68
+        <el-form-item>
69
+          <el-button @click="formVisible = false">取消</el-button>
70
+          <el-button @click="submit" type="primary">提交</el-button>
71
+        </el-form-item>
72
+      </el-form>
73
+    </el-dialog>
74
+  </div>
75
+</template>
76
+
77
+<script setup>
78
+  import { onActivated, onMounted, reactive, ref, watch } from 'vue'
79
+  import { create, list, remove, update } from '@/api/peer'
80
+  import { list as fetchTagList } from '@/api/tag'
81
+  import { ElMessage, ElMessageBox } from 'element-plus'
82
+  import { loadAllUsers } from '@/global'
83
+  import { toWebClientLink } from '@/utils/webclient'
84
+
85
+  const listRes = reactive({
86
+    list: [], total: 0, loading: false,
87
+  })
88
+  const listQuery = reactive({
89
+    page: 1,
90
+    page_size: 10,
91
+  })
92
+
93
+  const getList = async () => {
94
+    listRes.loading = true
95
+    const res = await list(listQuery).catch(_ => false)
96
+    listRes.loading = false
97
+    if (res) {
98
+      listRes.list = res.data.list
99
+      listRes.total = res.data.total
100
+    }
101
+  }
102
+  const handlerQuery = () => {
103
+    if (listQuery.page === 1) {
104
+      getList()
105
+    } else {
106
+      listQuery.page = 1
107
+    }
108
+  }
109
+
110
+  const del = async (row) => {
111
+    const cf = await ElMessageBox.confirm('确定删除么?', {
112
+      confirmButtonText: '确定',
113
+      cancelButtonText: '取消',
114
+      type: 'warning',
115
+    }).catch(_ => false)
116
+    if (!cf) {
117
+      return false
118
+    }
119
+
120
+    const res = await remove({ row_id: row.row_id }).catch(_ => false)
121
+    if (res) {
122
+      ElMessage.success('操作成功')
123
+      getList()
124
+    }
125
+  }
126
+  onMounted(getList)
127
+  onActivated(getList)
128
+
129
+  watch(() => listQuery.page, getList)
130
+
131
+  watch(() => listQuery.page_size, handlerQuery)
132
+
133
+  const platformList = [
134
+    { label: 'Windows', value: 'Windows' },
135
+    { label: 'Linux', value: 'Linux' },
136
+    { label: 'Mac OS', value: 'Mac OS' },
137
+    { label: 'Android', value: 'Android' },
138
+  ]
139
+  const formVisible = ref(false)
140
+  const formData = reactive({
141
+    row_id: 0,
142
+    cpu: '',
143
+    hostname: '',
144
+    id: '',
145
+    memory: '',
146
+    os: '',
147
+    username: '',
148
+    uuid: '',
149
+    version: '',
150
+  })
151
+
152
+  const toEdit = (row) => {
153
+    formVisible.value = true
154
+    //将row中的数据赋值给formData
155
+    Object.keys(formData).forEach(key => {
156
+      formData[key] = row[key]
157
+    })
158
+  }
159
+  const toAdd = () => {
160
+    formVisible.value = true
161
+    //重置formData
162
+    formData.row_id = 0
163
+    formData.cpu = ''
164
+    formData.hostname = ''
165
+    formData.id = ''
166
+    formData.memory = ''
167
+    formData.os = ''
168
+    formData.username = ''
169
+    formData.uuid = ''
170
+    formData.version = ''
171
+  }
172
+  const submit = async () => {
173
+    const api = formData.row_id ? update : create
174
+    const res = await api(formData).catch(_ => false)
175
+    if (res) {
176
+      ElMessage.success('操作成功')
177
+      formVisible.value = false
178
+      getList()
179
+    }
180
+  }
181
+
182
+</script>
183
+
184
+<style scoped lang="scss">
185
+.list-query .el-select {
186
+  --el-select-width: 160px;
187
+}
188
+
189
+.colors {
190
+  display: flex;
191
+  justify-content: center;
192
+  align-items: center;
193
+
194
+  .colorbox {
195
+    width: 50px;
196
+    height: 30px;
197
+    display: flex;
198
+    justify-content: center;
199
+    align-items: center;
200
+
201
+    .dot {
202
+      width: 10px;
203
+      height: 10px;
204
+      display: block;
205
+      border-radius: 50%;
206
+    }
207
+  }
208
+
209
+}
210
+
211
+</style>

+ 155 - 0
src/views/tag/index.js

@@ -0,0 +1,155 @@
1
+import { onActivated, onMounted, reactive, ref, watch } from 'vue'
2
+import { create, list, remove, update } from '@/api/tag'
3
+import { ElMessage, ElMessageBox } from 'element-plus'
4
+import { useRoute } from 'vue-router'
5
+
6
+export function useRepositories () {
7
+  //获取query
8
+  const route = useRoute()
9
+  const user_id = route.query?.user_id
10
+  const listRes = reactive({
11
+    list: [], total: 0, loading: false,
12
+  })
13
+  const listQuery = reactive({
14
+    page: 1,
15
+    page_size: 10,
16
+    is_my: 0,
17
+    user_id: user_id ? parseInt(user_id) : null,
18
+  })
19
+
20
+  const flutterColor2rgba = (color) => {
21
+    // color 是十进制的数字,先转成16进制
22
+    let hex = color.toString(16)
23
+    console.log('hex', hex)
24
+    //前两位是透明度
25
+    let alpha = hex.slice(0, 2)
26
+    //后六位是颜色
27
+    let rgba = hex.slice(2)
28
+    return `rgba(${parseInt(rgba.slice(0, 2), 16)}, ${parseInt(rgba.slice(2, 4), 16)}, ${parseInt(rgba.slice(4, 6), 16)}, ${parseInt(alpha, 16) / 255})`
29
+  }
30
+
31
+  const rgba2flutterColor = (color) => {
32
+    console.log('color', color)
33
+    //rgba(133, 33, 33, 0.81)
34
+    let rgba = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
35
+    console.log('rgba', rgba)
36
+    let alpha = Math.round(parseFloat(rgba[4]) * 255).toString(16)
37
+    let r = parseInt(rgba[1]).toString(16)
38
+    let g = parseInt(rgba[2]).toString(16)
39
+    let b = parseInt(rgba[3]).toString(16)
40
+    //如果是1位要补位
41
+    if (alpha.length === 1) {
42
+      alpha = '0' + alpha
43
+    }
44
+    if (r.length === 1) {
45
+      r = '0' + r
46
+    }
47
+    if (g.length === 1) {
48
+      g = '0' + g
49
+    }
50
+    if (b.length === 1) {
51
+      b = '0' + b
52
+    }
53
+    return parseInt(alpha + r + g + b, 16)
54
+  }
55
+
56
+  const getList = async () => {
57
+    listRes.loading = true
58
+    const res = await list(listQuery).catch(_ => false)
59
+    listRes.loading = false
60
+    if (res) {
61
+      listRes.list = res.data.list.map(item => {
62
+        item.color = flutterColor2rgba(item.color)
63
+        return item
64
+      })
65
+      listRes.total = res.data.total
66
+    }
67
+  }
68
+  const handlerQuery = () => {
69
+    if (listQuery.page === 1) {
70
+      getList()
71
+    } else {
72
+      listQuery.page = 1
73
+    }
74
+  }
75
+
76
+  const del = async (row) => {
77
+    const cf = await ElMessageBox.confirm('确定删除么?', {
78
+      confirmButtonText: '确定',
79
+      cancelButtonText: '取消',
80
+      type: 'warning',
81
+    }).catch(_ => false)
82
+    if (!cf) {
83
+      return false
84
+    }
85
+
86
+    const res = await remove({ id: row.id }).catch(_ => false)
87
+    if (res) {
88
+      ElMessage.success('操作成功')
89
+      getList()
90
+    }
91
+  }
92
+  onMounted(getList)
93
+  onActivated(getList)
94
+
95
+  watch(() => listQuery.page, getList)
96
+
97
+  watch(() => listQuery.page_size, handlerQuery)
98
+
99
+  const formVisible = ref(false)
100
+  const formData = reactive({
101
+    id: 0,
102
+    name: '',
103
+    color: 0,
104
+    user_id: 0,
105
+  })
106
+  const currentColor = ref('')
107
+  const activeChange = (c) => {
108
+    console.log(c)
109
+    currentColor.value = c
110
+  }
111
+  const toEdit = (row) => {
112
+    console.log('row', row)
113
+    formVisible.value = true
114
+    formData.id = row.id
115
+    formData.name = row.name
116
+    formData.color = row.color
117
+    formData.user_id = row.user_id
118
+  }
119
+  const toAdd = () => {
120
+    formVisible.value = true
121
+    formData.id = 0
122
+    formData.name = ''
123
+    formData.color = 0
124
+    formData.user_id = 0
125
+  }
126
+  const submit = async () => {
127
+    console.log(formData)
128
+    const api = formData.id ? update : create
129
+    const data = {
130
+      ...formData,
131
+      color: rgba2flutterColor(formData.color),
132
+    }
133
+    console.log(data)
134
+    const res = await api(data).catch(_ => false)
135
+    if (res) {
136
+      ElMessage.success('操作成功')
137
+      formVisible.value = false
138
+      getList()
139
+    }
140
+  }
141
+  return {
142
+    listRes,
143
+    listQuery,
144
+    getList,
145
+    handlerQuery,
146
+    del,
147
+    formVisible,
148
+    formData,
149
+    toEdit,
150
+    toAdd,
151
+    submit,
152
+    activeChange,
153
+    currentColor,
154
+  }
155
+}

+ 165 - 0
src/views/tag/index.vue

@@ -0,0 +1,165 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item label="用户">
6
+          <el-select v-model="listQuery.user_id" clearable>
7
+            <el-option
8
+                v-for="item in allUsers"
9
+                :key="item.id"
10
+                :label="item.username"
11
+                :value="item.id"
12
+            ></el-option>
13
+          </el-select>
14
+        </el-form-item>
15
+        <el-form-item>
16
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
17
+          <el-button type="danger" @click="toAdd">添加</el-button>
18
+        </el-form-item>
19
+      </el-form>
20
+    </el-card>
21
+    <el-card class="list-body" shadow="hover">
22
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
23
+        <el-table-column prop="id" label="id" align="center"/>
24
+        <el-table-column label="所属用户" align="center">
25
+          <template #default="{row}">
26
+            <span v-if="row.user_id"> <el-tag>{{ allUsers?.find(u => u.id === row.user_id)?.username }}</el-tag> </span>
27
+          </template>
28
+        </el-table-column>
29
+        <el-table-column prop="name" label="名称" align="center"/>
30
+        <el-table-column prop="color" label="颜色" align="center">
31
+          <template #default="{row}">
32
+            <div class="colors">
33
+              <div style="background-color: #efeff2" class="colorbox">
34
+                <div :style="{backgroundColor: row.color}" class="dot">
35
+                </div>
36
+              </div>
37
+              <div style="background-color: #24252b" class="colorbox">
38
+                <div :style="{backgroundColor: row.color}" class="dot">
39
+                </div>
40
+              </div>
41
+            </div>
42
+          </template>
43
+        </el-table-column>
44
+        <el-table-column prop="created_at" label="创建时间" align="center"/>
45
+        <el-table-column prop="updated_at" label="更新时间" align="center"/>
46
+        <el-table-column label="操作" align="center">
47
+          <template #default="{row}">
48
+            <el-button @click="toEdit(row)">编辑</el-button>
49
+            <el-button type="danger" @click="del(row)">删除</el-button>
50
+          </template>
51
+        </el-table-column>
52
+      </el-table>
53
+    </el-card>
54
+    <el-card class="list-page" shadow="hover">
55
+      <el-pagination background
56
+                     layout="prev, pager, next, sizes, jumper"
57
+                     :page-sizes="[10,20,50,100]"
58
+                     v-model:page-size="listQuery.page_size"
59
+                     v-model:current-page="listQuery.page"
60
+                     :total="listRes.total">
61
+      </el-pagination>
62
+    </el-card>
63
+    <el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
64
+      <el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
65
+        <el-form-item label="名称" prop="name" required>
66
+          <el-input v-model="formData.name"></el-input>
67
+        </el-form-item>
68
+        <el-form-item label="颜色" prop="color" required>
69
+          <el-color-picker v-model="formData.color" show-alpha @active-change="activeChange"></el-color-picker>
70
+          <br>
71
+          <div class="colors">
72
+            <div style="background-color: #efeff2" class="colorbox">
73
+              <div :style="{backgroundColor: currentColor}" class="dot">
74
+              </div>
75
+            </div>
76
+            <div style="background-color: #24252b" class="colorbox">
77
+              <div :style="{backgroundColor: currentColor}" class="dot">
78
+              </div>
79
+            </div>
80
+          </div>
81
+        </el-form-item>
82
+        <el-form-item label="用户" prop="user_id" required>
83
+          <el-select v-model="formData.user_id">
84
+            <el-option
85
+                v-for="item in allUsers"
86
+                :key="item.id"
87
+                :label="item.username"
88
+                :value="item.id"
89
+            ></el-option>
90
+          </el-select>
91
+        </el-form-item>
92
+        <el-form-item>
93
+          <el-button @click="formVisible = false">取消</el-button>
94
+          <el-button @click="submit" type="primary">提交</el-button>
95
+        </el-form-item>
96
+      </el-form>
97
+    </el-dialog>
98
+  </div>
99
+</template>
100
+
101
+<script setup>
102
+  import { onMounted, reactive, watch, ref, onActivated } from 'vue'
103
+  import { list, create, update, detail, remove } from '@/api/tag'
104
+  import { ElMessage, ElMessageBox } from 'element-plus'
105
+  import { loadAllUsers } from '@/global'
106
+  import { useRoute } from 'vue-router'
107
+  import { useRepositories } from '@/views/tag/index'
108
+
109
+
110
+
111
+  const { allUsers, getAllUsers } = loadAllUsers()
112
+  getAllUsers()
113
+  const {
114
+    listRes,
115
+    listQuery,
116
+    getList,
117
+    handlerQuery,
118
+    del,
119
+    formVisible,
120
+    formData,
121
+    toEdit,
122
+    toAdd,
123
+    submit,
124
+    activeChange,
125
+    currentColor,
126
+  } = useRepositories(0)
127
+
128
+  onMounted(getList)
129
+  onActivated(getList)
130
+
131
+  watch(() => listQuery.page, getList)
132
+
133
+  watch(() => listQuery.page_size, handlerQuery)
134
+
135
+
136
+</script>
137
+
138
+<style scoped lang="scss">
139
+.list-query .el-select {
140
+  --el-select-width: 160px;
141
+}
142
+
143
+.colors {
144
+  display: flex;
145
+  justify-content: center;
146
+  align-items: center;
147
+
148
+  .colorbox {
149
+    width: 50px;
150
+    height: 30px;
151
+    display: flex;
152
+    justify-content: center;
153
+    align-items: center;
154
+
155
+    .dot {
156
+      width: 10px;
157
+      height: 10px;
158
+      display: block;
159
+      border-radius: 50%;
160
+    }
161
+  }
162
+
163
+}
164
+
165
+</style>

+ 86 - 0
src/views/user/composables/edit.js

@@ -0,0 +1,86 @@
1
+import { ref, onMounted, reactive, watch } from 'vue'
2
+import { create, detail, update, remove } from '@/api/user'
3
+import { ElMessage, ElMessageBox } from 'element-plus'
4
+import { useRouter } from 'vue-router'
5
+import { list as groups } from '@/api/group'
6
+
7
+export function useGetDetail (id) {
8
+  let item = ref({})  //保留原始值
9
+  let form = ref({})
10
+  const groupsList = ref([])
11
+  const getDetail = async (id) => {
12
+    const res = await detail(id)
13
+    item.value = { ...res.data }
14
+    form.value = { ...res.data }
15
+  }
16
+  if (id > 0) {
17
+    onMounted(getDetail(id))
18
+  }
19
+
20
+  const getGroups = async () => {
21
+    const res = await groups({ page_size: 9999 }).catch(_ => false)
22
+    if (res) {
23
+      groupsList.value = res.data.list
24
+    }
25
+  }
26
+  onMounted(getGroups)
27
+  return {
28
+    form,
29
+    item,
30
+    getDetail,
31
+    groupsList
32
+  }
33
+}
34
+
35
+export function useSubmit (form, id) {
36
+  const root = ref(null)
37
+  const router = useRouter()
38
+  const rules = reactive({
39
+    username: [{ required: true, message: '用户名是必须的' }],
40
+    // nickname: [{ required: true, message: '昵称是必须的' }],
41
+    status: [{ required: true, message: '请选择状态' }],
42
+  })
43
+
44
+  const validate = async () => {
45
+    const res = await root.value.validate().catch(err => false)
46
+    return res
47
+  }
48
+
49
+  const submitCreate = async () => {
50
+    const res = await create(form.value).catch(_ => false)
51
+    return res.code === 0
52
+  }
53
+
54
+  const submitUpdate = async () => {
55
+    const res = await update(form.value).catch(_ => false)
56
+    return res.code === 0
57
+  }
58
+  const submitFunc = id > 0 ? submitUpdate : submitCreate
59
+
60
+  const submit = async () => {
61
+    const v = await validate()
62
+    if (!v) {
63
+      return
64
+    }
65
+
66
+    const res = await submitFunc()
67
+    if (res) {
68
+      ElMessage.success('操作成功')
69
+      router.back()
70
+    }
71
+  }
72
+
73
+  const cancel = () => {
74
+    router.back()
75
+  }
76
+
77
+  return {
78
+    root,
79
+    rules,
80
+    validate,
81
+    submit,
82
+    cancel,
83
+  }
84
+}
85
+
86
+

+ 124 - 0
src/views/user/composables/index.js

@@ -0,0 +1,124 @@
1
+import { onMounted, reactive, watch } from 'vue'
2
+import { list, remove, changePwd } from '@/api/user'
3
+import { list as groups } from '@/api/group'
4
+import { useRouter } from 'vue-router'
5
+import { ElMessageBox, ElMessage } from 'element-plus'
6
+
7
+export function useRepositories () {
8
+
9
+  const listRes = reactive({
10
+    list: [], total: 0, loading: false,
11
+    groups: [],
12
+  })
13
+  const listQuery = reactive({
14
+    page: 1,
15
+    page_size: 10,
16
+    username: '',
17
+  })
18
+
19
+  const getList = async () => {
20
+    listRes.loading = true
21
+    const res = await list(listQuery).catch(_ => false)
22
+    listRes.loading = false
23
+    if (res) {
24
+      listRes.list = res.data.list
25
+      listRes.total = res.data.total
26
+    }
27
+  }
28
+
29
+  const handlerQuery = () => {
30
+    if (listQuery.page === 1) {
31
+      getList()
32
+    } else {
33
+      listQuery.page = 1
34
+      //由watch 触发
35
+    }
36
+  }
37
+
38
+  const getGroups = async () => {
39
+    const res = await groups({ page_size: 9999 }).catch(_ => false)
40
+    if (res) {
41
+      listRes.groups = res.data.list
42
+    }
43
+  }
44
+  onMounted(getGroups)
45
+
46
+  onMounted(getList)
47
+
48
+  watch(() => listQuery.page, getList)
49
+  watch(() => listQuery.page_size, handlerQuery)
50
+  return {
51
+    listRes,
52
+    listQuery,
53
+    handlerQuery,
54
+    getList,
55
+    getGroups,
56
+  }
57
+}
58
+
59
+export function useToEditOrAdd () {
60
+  const router = useRouter()
61
+  const toEdit = (row) => {
62
+    router.push('/user/edit/' + row.id)
63
+  }
64
+  const toAdd = () => {
65
+    router.push('/user/add')
66
+  }
67
+  const toTag = (row) => {
68
+    router.push('/user/tag/?user_id=' + row.id)
69
+  }
70
+  const toAddressBook = (row) => {
71
+    router.push('/user/addressBook/?user_id=' + row.id)
72
+  }
73
+  return {
74
+    toAdd,
75
+    toEdit,
76
+    toTag,
77
+    toAddressBook
78
+  }
79
+}
80
+
81
+export function useDel () {
82
+  const del = async (id) => {
83
+    const cf = await ElMessageBox.confirm('确定删除么?', {
84
+      confirmButtonText: '确定',
85
+      cancelButtonText: '取消',
86
+      type: 'warning',
87
+    }).catch(_ => false)
88
+    if (!cf) {
89
+      return false
90
+    }
91
+
92
+    const res = remove({ id }).catch(_ => false)
93
+    return res
94
+  }
95
+  return {
96
+    del,
97
+  }
98
+}
99
+
100
+export function useChangePwd () {
101
+  const changePass = async (admin) => {
102
+    const input = await ElMessageBox.prompt('请输入新密码', '重置密码', {
103
+      confirmButtonText: '确定',
104
+      cancelButtonText: '取消',
105
+    }).catch(_ => false)
106
+    if (!input) {
107
+      return
108
+    }
109
+    const confirm = await ElMessageBox.confirm('确定重置密码么?', {
110
+      confirmButtonText: '确定',
111
+      cancelButtonText: '取消',
112
+    }).catch(_ => false)
113
+    if (!confirm) {
114
+      return
115
+    }
116
+    const res = await changePwd({ id: admin.id, password: input.value }).catch(_ => false)
117
+    if (!res) {
118
+      return
119
+    }
120
+    ElMessage.success('修改成功')
121
+  }
122
+
123
+  return { changePass }
124
+}

+ 76 - 0
src/views/user/edit.vue

@@ -0,0 +1,76 @@
1
+<template>
2
+  <div class="form-card">
3
+    <el-form ref="root" label-width="120px" :model="form" :rules="rules">
4
+      <el-form-item label="用户名" prop="username">
5
+        <el-input v-model="form.username"></el-input>
6
+      </el-form-item>
7
+      <el-form-item label="昵称" prop="nickname">
8
+        <el-input v-model="form.nickname"></el-input>
9
+      </el-form-item>
10
+      <el-form-item label="小组" prop="group_id">
11
+        <el-select v-model="form.group_id" placeholder="请选择小组">
12
+          <el-option
13
+              v-for="item in groupsList"
14
+              :key="item.id"
15
+              :label="item.name"
16
+              :value="item.id"
17
+          ></el-option>
18
+        </el-select>
19
+      </el-form-item>
20
+      <el-form-item label="是否是管理员" prop="is_admin">
21
+        <el-switch v-model="form.is_admin"
22
+                   :active-value="true"
23
+                   :inactive-value="false"
24
+        ></el-switch>
25
+      </el-form-item>
26
+      <el-form-item label="状态" prop="status">
27
+        <el-switch v-model="form.status"
28
+                   :active-value="ENABLE_STATUS"
29
+                   :inactive-value="DISABLE_STATUS"
30
+        ></el-switch>
31
+      </el-form-item>
32
+      <el-form-item>
33
+        <el-button @click="cancel">取消</el-button>
34
+        <el-button @click="submit" type="primary">提交</el-button>
35
+      </el-form-item>
36
+    </el-form>
37
+  </div>
38
+</template>
39
+
40
+<script>
41
+  import { defineComponent, toRef } from 'vue'
42
+  import { useRoute } from 'vue-router'
43
+  import { useGetDetail, useSubmit } from '@/views/user/composables/edit'
44
+  import { ENABLE_STATUS, DISABLE_STATUS } from '@/utils/common_options'
45
+
46
+  export default defineComponent({
47
+    name: 'UserEdit',
48
+    props: {},
49
+    setup (props, context) {
50
+
51
+      const route = useRoute()
52
+      const { form, item, getDetail, groupsList } = useGetDetail(route.params.id)
53
+
54
+      const { root, rules, validate, submit, cancel } = useSubmit(form, route.params.id)
55
+
56
+      return {
57
+        form,
58
+        item,
59
+        getDetail,
60
+
61
+        rules,
62
+        validate,
63
+        root,
64
+        submit,
65
+        cancel,
66
+        groupsList,
67
+        ENABLE_STATUS, DISABLE_STATUS,
68
+      }
69
+    },
70
+  })
71
+</script>
72
+
73
+<style lang="scss" scoped>
74
+.form-card {
75
+}
76
+</style>

+ 78 - 0
src/views/user/index.vue

@@ -0,0 +1,78 @@
1
+<template>
2
+  <div>
3
+    <el-card class="list-query" shadow="hover">
4
+      <el-form inline label-width="60px">
5
+        <el-form-item label="用户名">
6
+          <el-input v-model="listQuery.username"></el-input>
7
+        </el-form-item>
8
+        <el-form-item>
9
+          <el-button type="primary" @click="handlerQuery">筛选</el-button>
10
+          <el-button type="danger" @click="toAdd">添加</el-button>
11
+        </el-form-item>
12
+      </el-form>
13
+    </el-card>
14
+    <el-card class="list-body" shadow="hover">
15
+      <el-table :data="listRes.list" v-loading="listRes.loading" border>
16
+        <el-table-column prop="id" label="id" align="center"></el-table-column>
17
+        <el-table-column prop="username" label="用户名" align="center"/>
18
+        <el-table-column prop="nickname" label="昵称" align="center"/>
19
+        <el-table-column label="所在小组" align="center">
20
+          <template #default="{row}">
21
+            <span v-if="row.group_id"> <el-tag>{{ listRes.groups?.find(g => g.id === row.group_id)?.name }} </el-tag> </span>
22
+            <span v-else> 未分组 </span>
23
+          </template>
24
+        </el-table-column>
25
+        <el-table-column prop="created_at" label="创建时间" align="center"/>
26
+        <el-table-column prop="updated_at" label="更新时间" align="center"/>
27
+        <el-table-column label="操作" align="center" width="550">
28
+          <template #default="{row}">
29
+            <el-button @click="toTag(row)">他的标签</el-button>
30
+            <el-button @click="toAddressBook(row)">他的地址簿</el-button>
31
+            <el-button @click="toEdit(row)">编辑</el-button>
32
+            <el-button type="warning" @click="changePass(row)">重置密码</el-button>
33
+            <el-button type="danger" @click="remove(row)">删除</el-button>
34
+          </template>
35
+        </el-table-column>
36
+      </el-table>
37
+    </el-card>
38
+    <el-card class="list-page" shadow="hover">
39
+      <el-pagination background
40
+                     layout="prev, pager, next, sizes, jumper"
41
+                     :page-sizes="[10,20,50,100]"
42
+                     v-model:page-size="listQuery.page_size"
43
+                     v-model:current-page="listQuery.page"
44
+                     :total="listRes.total">
45
+      </el-pagination>
46
+    </el-card>
47
+  </div>
48
+</template>
49
+
50
+<script setup>
51
+  import { useRepositories, useDel, useToEditOrAdd, useChangePwd } from '@/views/user/composables'
52
+
53
+  //列表
54
+  const {
55
+    listRes,
56
+    listQuery,
57
+    handlerQuery,
58
+    getList,
59
+  } = useRepositories()
60
+
61
+  const { toEdit, toAdd, toAddressBook, toTag } = useToEditOrAdd()
62
+
63
+  const { changePass } = useChangePwd()
64
+
65
+  //删除
66
+  const { del } = useDel()
67
+  const remove = async (row) => {
68
+    const res = await del(row.id)
69
+    if (res) {
70
+      getList(listQuery)
71
+    }
72
+  }
73
+
74
+
75
+</script>
76
+
77
+<style scoped>
78
+</style>

+ 82 - 0
vite.config.js

@@ -0,0 +1,82 @@
1
+import { defineConfig } from 'vite'
2
+import * as path from 'path'
3
+import * as dotenv from 'dotenv'
4
+import * as fs from 'fs'
5
+import vue from '@vitejs/plugin-vue'
6
+
7
+const NODE_ENV = process.env.NODE_ENV || 'development'
8
+const envFile = `.env.${NODE_ENV}`
9
+const envConfig = dotenv.parse(fs.readFileSync(envFile))
10
+for (const k in envConfig) {
11
+  process.env[k] = envConfig[k]
12
+}
13
+
14
+let alias = {
15
+  '@': path.resolve(__dirname, './src'),
16
+  'vue$': 'vue/dist/vue.runtime.esm-bundler.js',
17
+}
18
+
19
+const conf = {
20
+  base: './', // index.html文件所在位置
21
+  root: './', // js导入的资源路径,src
22
+  server: {
23
+    open: true,
24
+    port: process.env.VITE_DEV_PORT,
25
+    proxy: {
26
+      [process.env.VITE_SERVER_API]: {
27
+        target: process.env.VITE_SERVER_PATH,
28
+        // rewrite: path => path.replace(/^\/api/, '/api'), //为了模拟
29
+        changeOrigin: true,
30
+      },
31
+    },
32
+  },
33
+  build: {
34
+    target: 'es2015',
35
+    minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
36
+    manifest: false, // 是否产出maifest.json
37
+    sourcemap: false, // 是否产出soucemap.json
38
+    emptyOutDir: true,
39
+    outDir: 'dist', // 产出目录
40
+    rollupOptions: {
41
+      output: {
42
+        manualChunks (id) {
43
+          if (id.includes('node_modules')) {
44
+            const arr = id.toString().split('node_modules/')[1].split('/')
45
+            switch (arr[0]) {
46
+              case '@popperjs':
47
+              case '@vue':
48
+              case 'axios':
49
+              case 'element-plus':
50
+              case '@element-plus':
51
+                return '_' + arr[0]
52
+              default :
53
+                return '__vendor'
54
+            }
55
+          }else if(id.includes('Gwen-admin/src')){
56
+            //src 下的都打包到一起 不然很多小文件
57
+            return 'gwen'
58
+          }
59
+        },
60
+        chunkFileNames: 'static/chunk/[name]-[hash].js',
61
+        entryFileNames: 'static/entry/[name]-[hash].js',
62
+        assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
63
+      },
64
+    },
65
+  },
66
+  css: {
67
+    preprocessorOptions: {
68
+      scss: {
69
+        javascriptEnabled: true,
70
+      },
71
+    },
72
+  },
73
+  resolve: {
74
+    alias,
75
+  },
76
+  plugins: [
77
+    vue(),
78
+  ],
79
+}
80
+
81
+// https://vitejs.dev/config/
82
+export default defineConfig(conf)