Просмотр исходного кода

fix bugs & add batchdelete peer & add peer to ab

ljw 1 год назад
Родитель
Сommit
777510c7ec

+ 2 - 2
src/api/address_book.js

@@ -37,9 +37,9 @@ export function remove (data) {
37 37
   })
38 38
 }
39 39
 
40
-export function changePwd (data) {
40
+export function batchCreate (data) {
41 41
   return request({
42
-    url: '/address_book/changePwd',
42
+    url: '/address_book/batchCreate',
43 43
     method: 'post',
44 44
     data,
45 45
   })

+ 8 - 0
src/api/peer.js

@@ -36,3 +36,11 @@ export function remove (data) {
36 36
     data,
37 37
   })
38 38
 }
39
+
40
+export function batchRemove (data) {
41
+  return request({
42
+    url: '/peer/batchDelete',
43
+    method: 'post',
44
+    data,
45
+  })
46
+}

+ 1 - 1
src/store/app.js

@@ -11,7 +11,7 @@ export const useAppStore = defineStore({
11 11
       sideIsCollapse: false,
12 12
       logo,
13 13
       lang: localStorage.getItem('lang') || 'zh-CN',
14
-      locale: zhCn,
14
+      locale: localStorage.getItem('lang') === 'en' ? en : zhCn,
15 15
     },
16 16
   }),
17 17
 

+ 12 - 2
src/styles/style.scss

@@ -10,11 +10,21 @@ $sideBarWidth: 210px;
10 10
   --primaryColor: #409eff;
11 11
 }
12 12
 
13
-.list-body{
13
+.list-body {
14 14
   margin: 10px 0;
15 15
 }
16 16
 
17
-.dialog-form{
17
+.dialog-form {
18 18
   max-width: 600px;
19 19
   margin: 20px auto;
20 20
 }
21
+
22
+.list-query {
23
+  .el-select {
24
+    --el-select-width: 160px;
25
+  }
26
+
27
+  .el-input {
28
+    --el-input-width: 160px;
29
+  }
30
+}

+ 28 - 5
src/utils/file.js

@@ -1,4 +1,4 @@
1
-export function get_suffix(filename) {
1
+export function get_suffix (filename) {
2 2
   var pos = filename.lastIndexOf('.')
3 3
   var suffix = ''
4 4
   if (pos !== -1) {
@@ -7,7 +7,7 @@ export function get_suffix(filename) {
7 7
   return suffix
8 8
 }
9 9
 
10
-export function random_string(len) {
10
+export function random_string (len) {
11 11
   len = len || 32
12 12
   var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
13 13
   var maxPos = chars.length
@@ -18,17 +18,40 @@ export function random_string(len) {
18 18
   return pwd
19 19
 }
20 20
 
21
-export function random_filename(filename) {
21
+export function random_filename (filename) {
22 22
   var suffix = get_suffix(filename)
23 23
   var time = new Date()
24 24
   var time2 = new Date('2020/01/01')
25 25
   return Math.ceil((time.getTime() - time2.getTime()) / 1000) + '_' + random_string(10) + suffix
26 26
 }
27 27
 
28
-export function utf8_to_b64(str) {
28
+export function utf8_to_b64 (str) {
29 29
   return window.btoa(unescape(encodeURIComponent(str)))
30 30
 }
31 31
 
32
-export function b64_to_utf8(str) {
32
+export function b64_to_utf8 (str) {
33 33
   return decodeURIComponent(escape(window.atob(str)))
34 34
 }
35
+
36
+export function jsonToCsv (data) {
37
+  let csv = ''
38
+  let keys = Object.keys(data[0])
39
+  csv += keys.join(',') + '\n'
40
+  data.forEach(row => {
41
+    csv += keys.map(key => `"${row[key]}"`).join(',') + '\n'
42
+  })
43
+  return new Blob([csv], { type: 'text/csv' })
44
+}
45
+
46
+export function downBlob (blob, filename) {
47
+  const url = window.URL.createObjectURL(blob)
48
+  const a = document.createElement('a')
49
+  a.href = url
50
+  a.download = filename
51
+  document.body.appendChild(a)
52
+  a.click()
53
+  setTimeout(() => {
54
+    window.URL.revokeObjectURL(url)
55
+    document.body.removeChild(a)
56
+  })
57
+}

+ 2 - 3
src/utils/i18n.js

@@ -1,17 +1,16 @@
1 1
 import en from '@/utils/i18n/en.json'
2 2
 import zhCN from '@/utils/i18n/zh_CN.json'
3 3
 import { useAppStore } from '@/store/app'
4
-import { pinia } from '@/store'
5 4
 
6 5
 export function T (key, params, num = 0) {
7
-  const appStore = useAppStore(pinia)
6
+  const appStore = useAppStore()
8 7
   const lang = appStore.setting.lang
9 8
   const trans = lang === 'zh-CN' ? zhCN : en
10 9
   const tran = trans[key]
11 10
   if (!tran) {
12 11
     return key
13 12
   }
14
-  const msg = num > 0 ? (tran.Other ? tran.Other : tran.One) : tran.One
13
+  const msg = num > 1 ? (tran.Other ? tran.Other : tran.One) : tran.One
15 14
   //msg 是这样 {name} is name
16 15
   //params 是这样 {name: 'zhangsan'}
17 16
   //替换

+ 50 - 0
src/utils/i18n/en.json

@@ -232,5 +232,55 @@
232 232
   },
233 233
   "LoginLog": {
234 234
     "One": "Login Log"
235
+  },
236
+  "LastOnlineTime": {
237
+    "One": "Last Online Time"
238
+  },
239
+  "JustNow": {
240
+    "One": "Just Now"
241
+  },
242
+  "MinutesAgo": {
243
+    "One": "{param} Minute Ago",
244
+    "Other": "{param} Minutes Ago"
245
+  },
246
+  "HoursAgo": {
247
+    "One": "{param} Hour Ago",
248
+    "Other": "{param} Hours Ago"
249
+  },
250
+  "DaysAgo": {
251
+    "One": "{param} Day Ago",
252
+    "Other": "{param} Days Ago"
253
+  },
254
+  "MonthsAgo": {
255
+    "One": "{param} Month Ago",
256
+    "Other": "{param} Months Ago"
257
+  },
258
+  "YearsAgo": {
259
+    "One": "{param} Year Ago",
260
+    "Other": "{param} Years Ago"
261
+  },
262
+  "MinutesLess": {
263
+    "One": "Less than {param} minute",
264
+    "Other": "Less than {param} minutes"
265
+  },
266
+  "HoursLess": {
267
+    "One": "Less than {param} hour",
268
+    "Other": "Less than {param} hours"
269
+  },
270
+  "DaysLess": {
271
+    "One": "Less than {param} day",
272
+    "Other": "Less than {param} days"
273
+  },
274
+  "Export": {
275
+    "One": "Export"
276
+  },
277
+  "AddToAddressBook": {
278
+    "One": "Add To Address Book"
279
+  },
280
+  "BatchDelete": {
281
+    "One": "Batch Delete"
282
+  },
283
+  "PleaseSelectData": {
284
+    "One": "Please select data"
235 285
   }
236 286
 }

+ 42 - 0
src/utils/i18n/zh_CN.json

@@ -232,5 +232,47 @@
232 232
   },
233 233
   "LoginLog": {
234 234
     "One": "登录日志"
235
+  },
236
+  "LastOnlineTime": {
237
+    "One": "最后在线时间"
238
+  },
239
+  "JustNow": {
240
+    "One": "刚刚"
241
+  },
242
+  "MinutesAgo": {
243
+    "One": "{param} 分钟前"
244
+  },
245
+  "HoursAgo": {
246
+    "One": "{param} 小时前"
247
+  },
248
+  "DaysAgo": {
249
+    "One": "{param} 天前"
250
+  },
251
+  "MonthsAgo": {
252
+    "One": "{param} 月前"
253
+  },
254
+  "YearsAgo": {
255
+    "One": "{param} 年前"
256
+  },
257
+  "MinutesLess": {
258
+    "One": "{param} 分钟内"
259
+  },
260
+  "HoursLess": {
261
+    "One": "{param} 小时内"
262
+  },
263
+  "DaysLess": {
264
+    "One": "{param} 天内"
265
+  },
266
+  "Export": {
267
+    "One": "导出"
268
+  },
269
+  "AddToAddressBook": {
270
+    "One": "添加到地址簿"
271
+  },
272
+  "BatchDelete": {
273
+    "One": "批量删除"
274
+  },
275
+  "PleaseSelectData": {
276
+    "One": "请选择数据"
235 277
   }
236 278
 }

+ 25 - 0
src/utils/time.js

@@ -0,0 +1,25 @@
1
+import { T } from '@/utils/i18n'
2
+
3
+export function timeAgo (time) {
4
+  let now = new Date().getTime()
5
+  let after = new Date(time).getTime()
6
+  let dis = now - after
7
+  if (dis < 60 * 1000) {
8
+    return T('JustNow')
9
+  } else if (dis < 60 * 60 * 1000) {
10
+    const num = Math.floor(dis / (60 * 1000))
11
+    return T('MinutesAgo', { param: num }, num)
12
+  } else if (dis < 24 * 60 * 60 * 1000) {
13
+    const num = Math.floor(dis / (60 * 60 * 1000))
14
+    return T('HoursAgo', { param: num }, num)
15
+  } else if (dis < 30 * 24 * 60 * 60 * 1000) {
16
+    const num = Math.floor(dis / (24 * 60 * 60 * 1000))
17
+    return T('DaysAgo', { param: num }, num)
18
+  } else if (dis < 12 * 30 * 24 * 60 * 60 * 1000) {
19
+    const num = Math.floor(dis / (30 * 24 * 60 * 60 * 1000))
20
+    return T('MonthsAgo', { param: num }, num)
21
+  } else {
22
+    const num = Math.floor(dis / (12 * 30 * 24 * 60 * 60 * 1000))
23
+    return T('YearsAgo', { param: num }, num)
24
+  }
25
+}

+ 6 - 6
src/views/address_book/index.js

@@ -1,13 +1,9 @@
1 1
 import { reactive, ref } from 'vue'
2 2
 import { create, list, remove, update } from '@/api/address_book'
3 3
 import { ElMessage, ElMessageBox } from 'element-plus'
4
-import { useRoute } from 'vue-router'
5 4
 import { T } from '@/utils/i18n'
6 5
 
7
-export function useRepositories () {
8
-  const route = useRoute()
9
-  const user_id = route.query?.user_id
10
-
6
+export function useRepositories (user_id) {
11 7
   const listRes = reactive({
12 8
     list: [], total: 0, loading: false,
13 9
   })
@@ -15,7 +11,10 @@ export function useRepositories () {
15 11
     page: 1,
16 12
     page_size: 10,
17 13
     is_my: 0,
18
-    user_id: user_id ? parseInt(user_id) : null,
14
+    id: null,
15
+    user_id: null,
16
+    username: null,
17
+    hostname: null,
19 18
   })
20 19
 
21 20
   const getList = async () => {
@@ -75,6 +74,7 @@ export function useRepositories () {
75 74
     'sameServer': false,
76 75
     'tags': [],
77 76
     'user_id': null,
77
+    user_ids: [],
78 78
     'username': '',
79 79
   })
80 80
 

+ 21 - 7
src/views/address_book/index.vue

@@ -12,6 +12,15 @@
12 12
             ></el-option>
13 13
           </el-select>
14 14
         </el-form-item>
15
+        <el-form-item :label="T('Id')">
16
+          <el-input v-model="listQuery.id" clearable></el-input>
17
+        </el-form-item>
18
+        <el-form-item :label="T('Username')">
19
+          <el-input v-model="listQuery.username" clearable></el-input>
20
+        </el-form-item>
21
+        <el-form-item :label="T('Hostname')">
22
+          <el-input v-model="listQuery.hostname" clearable></el-input>
23
+        </el-form-item>
15 24
         <el-form-item>
16 25
           <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
17 26
           <el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
@@ -80,12 +89,12 @@
80 89
         <el-form-item :label="T('Hostname')" prop="hostname">
81 90
           <el-input v-model="formData.hostname"></el-input>
82 91
         </el-form-item>
83
-        <el-form-item :label="T('LoginName')" prop="loginName">
84
-          <el-input v-model="formData.loginName"></el-input>
85
-        </el-form-item>
86
-        <el-form-item :label="T('Password')" prop="password">
87
-          <el-input v-model="formData.password"></el-input>
88
-        </el-form-item>
92
+        <!--        <el-form-item :label="T('LoginName')" prop="loginName">
93
+                  <el-input v-model="formData.loginName"></el-input>
94
+                </el-form-item>
95
+                <el-form-item :label="T('Password')" prop="password">
96
+                  <el-input v-model="formData.password"></el-input>
97
+                </el-form-item>-->
89 98
         <el-form-item :label="T('Platform')" prop="platform">
90 99
           <el-select v-model="formData.platform">
91 100
             <el-option
@@ -140,7 +149,9 @@
140 149
   import { useRepositories } from '@/views/address_book/index'
141 150
   import { toWebClientLink } from '@/utils/webclient'
142 151
   import { T } from '@/utils/i18n'
152
+  import { useRoute } from 'vue-router'
143 153
 
154
+  const route = useRoute()
144 155
   const { allUsers, getAllUsers } = loadAllUsers()
145 156
   getAllUsers()
146 157
   const changeUser = (v) => {
@@ -167,10 +178,13 @@
167 178
     toEdit,
168 179
     toAdd,
169 180
     submit,
170
-    activeChange,
171 181
     currentColor,
172 182
   } = useRepositories()
173 183
 
184
+  if (route.query?.user_id) {
185
+    listQuery.user_id = parseInt(route.query.user_id)
186
+  }
187
+
174 188
   onMounted(getList)
175 189
   onActivated(getList)
176 190
 

+ 15 - 9
src/views/my/address_book/index.vue

@@ -2,6 +2,15 @@
2 2
   <div>
3 3
     <el-card class="list-query" shadow="hover">
4 4
       <el-form inline label-width="80px">
5
+        <el-form-item :label="T('Id')">
6
+          <el-input v-model="listQuery.id" clearable></el-input>
7
+        </el-form-item>
8
+        <el-form-item :label="T('Username')">
9
+          <el-input v-model="listQuery.username" clearable></el-input>
10
+        </el-form-item>
11
+        <el-form-item :label="T('Hostname')">
12
+          <el-input v-model="listQuery.hostname" clearable></el-input>
13
+        </el-form-item>
5 14
         <el-form-item>
6 15
           <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
7 16
           <el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
@@ -55,12 +64,12 @@
55 64
         <el-form-item :label="T('Hostname')" prop="hostname">
56 65
           <el-input v-model="formData.hostname"></el-input>
57 66
         </el-form-item>
58
-        <el-form-item :label="T('LoginName')" prop="loginName">
59
-          <el-input v-model="formData.loginName"></el-input>
60
-        </el-form-item>
61
-        <el-form-item :label="T('Password')" prop="password">
62
-          <el-input v-model="formData.password"></el-input>
63
-        </el-form-item>
67
+        <!--        <el-form-item :label="T('LoginName')" prop="loginName">
68
+                  <el-input v-model="formData.loginName"></el-input>
69
+                </el-form-item>-->
70
+        <!--        <el-form-item :label="T('Password')" prop="password">
71
+                          <el-input v-model="formData.password"></el-input>
72
+                        </el-form-item>-->
64 73
         <el-form-item :label="T('Platform')" prop="platform">
65 74
           <el-select v-model="formData.platform">
66 75
             <el-option
@@ -150,9 +159,6 @@
150 159
 </script>
151 160
 
152 161
 <style scoped lang="scss">
153
-.list-query .el-select {
154
-  --el-select-width: 160px;
155
-}
156 162
 
157 163
 .colors {
158 164
   display: flex;

+ 208 - 28
src/views/peer/index.vue

@@ -1,15 +1,29 @@
1 1
 <template>
2 2
   <div>
3 3
     <el-card class="list-query" shadow="hover">
4
-      <el-form inline label-width="80px">
4
+      <el-form inline label-width="150px">
5
+        <el-form-item :label="T('LastOnlineTime')">
6
+          <el-select v-model="listQuery.time_ago" clearable>
7
+            <el-option
8
+                v-for="item in timeFilters"
9
+                :key="item.value"
10
+                :label="item.text"
11
+                :value="item.value"
12
+                :disabled="item.value === 0"
13
+            ></el-option>
14
+          </el-select>
15
+        </el-form-item>
5 16
         <el-form-item>
6 17
           <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
7 18
           <el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
19
+          <el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
20
+          <el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
8 21
         </el-form-item>
9 22
       </el-form>
10 23
     </el-card>
11 24
     <el-card class="list-body" shadow="hover">
12
-      <el-table :data="listRes.list" v-loading="listRes.loading" border size="small">
25
+      <el-table :data="listRes.list" v-loading="listRes.loading" border size="small" @selection-change="handleSelectionChange">
26
+        <el-table-column type="selection" width="55" align="center"/>
13 27
         <el-table-column prop="id" label="id" align="center"/>
14 28
         <el-table-column prop="cpu" label="cpu" align="center"/>
15 29
         <el-table-column prop="hostname" :label="T('Hostname')" align="center"/>
@@ -17,12 +31,21 @@
17 31
         <el-table-column prop="os" :label="T('Os')" align="center"/>
18 32
         <el-table-column prop="username" :label="T('Username')" align="center"/>
19 33
         <el-table-column prop="uuid" :label="T('Uuid')" align="center"/>
20
-        <el-table-column prop="version" :label="T('Version')" align="center"/>
34
+        <el-table-column prop="version" :label="T('Version')" align="center" width="80"/>
21 35
         <el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
22 36
         <el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/>
23
-        <el-table-column :label="T('Actions')" align="center" width="400">
37
+        <el-table-column prop="last_online_time" :label="T('LastOnlineTime')" align="center">
38
+          <template #default="{row}">
39
+            <div class="last_oline_time">
40
+              <span> {{ row.last_online_time ? timeAgo(row.last_online_time * 1000) : '-' }}</span> <span class="dot" :class="{red: timeDis(row.last_online_time) >= 60, green: timeDis(row.last_online_time)< 60}"></span>
41
+            </div>
42
+
43
+          </template>
44
+        </el-table-column>
45
+        <el-table-column :label="T('Actions')" align="center" width="500">
24 46
           <template #default="{row}">
25 47
             <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
48
+            <el-button type="primary" @click="toAddressBook(row)">{{ T('AddToAddressBook') }}</el-button>
26 49
             <el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
27 50
             <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
28 51
           </template>
@@ -71,15 +94,73 @@
71 94
         </el-form-item>
72 95
       </el-form>
73 96
     </el-dialog>
97
+
98
+    <el-dialog v-model="ABFormVisible" width="800" :title="T('Create')">
99
+      <el-form class="dialog-form" ref="form" :model="ABFormData" label-width="120px">
100
+        <el-form-item :label="T('Owner')" prop="user_ids" required>
101
+          <el-select v-model="ABFormData.user_ids" multiple>
102
+            <el-option
103
+                v-for="item in allUsers"
104
+                :key="item.id"
105
+                :label="item.username"
106
+                :value="item.id"
107
+            ></el-option>
108
+          </el-select>
109
+        </el-form-item>
110
+        <el-form-item label="id" prop="id" required>
111
+          <el-input v-model="ABFormData.id"></el-input>
112
+        </el-form-item>
113
+        <el-form-item :label="T('Username')" prop="username">
114
+          <el-input v-model="ABFormData.username"></el-input>
115
+        </el-form-item>
116
+        <el-form-item :label="T('Alias')" prop="alias">
117
+          <el-input v-model="ABFormData.alias"></el-input>
118
+        </el-form-item>
119
+        <el-form-item :label="T('Hostname')" prop="hostname">
120
+          <el-input v-model="ABFormData.hostname"></el-input>
121
+        </el-form-item>
122
+        <el-form-item :label="T('Platform')" prop="platform">
123
+          <el-select v-model="ABFormData.platform">
124
+            <el-option
125
+                v-for="item in ABPlatformList"
126
+                :key="item.value"
127
+                :label="item.label"
128
+                :value="item.value"
129
+            ></el-option>
130
+          </el-select>
131
+        </el-form-item>
132
+
133
+        <el-form-item :label="T('Tags')" prop="tags">
134
+          <el-select v-model="ABFormData.tags" multiple>
135
+            <el-option
136
+                v-for="item in tagList"
137
+                :key="item.name"
138
+                :label="item.name"
139
+                :value="item.name"
140
+            ></el-option>
141
+          </el-select>
142
+        </el-form-item>
143
+        <el-form-item>
144
+          <el-button @click="ABFormVisible = false">{{ T('Cancel') }}</el-button>
145
+          <el-button @click="ABSubmit" type="primary">{{ T('Submit') }}</el-button>
146
+        </el-form-item>
147
+      </el-form>
148
+    </el-dialog>
74 149
   </div>
75 150
 </template>
76 151
 
77 152
 <script setup>
78
-  import { onActivated, onMounted, reactive, ref, watch } from 'vue'
79
-  import { create, list, remove, update } from '@/api/peer'
153
+  import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
154
+  import { batchRemove, create, list, remove, update } from '@/api/peer'
80 155
   import { ElMessage, ElMessageBox } from 'element-plus'
81 156
   import { toWebClientLink } from '@/utils/webclient'
82 157
   import { T } from '@/utils/i18n'
158
+  import { timeAgo } from '@/utils/time'
159
+  import { jsonToCsv, downBlob } from '@/utils/file'
160
+  import { useRepositories as useABRepositories } from '@/views/address_book/index'
161
+  import { loadAllUsers } from '@/global'
162
+  import { list as fetchTagList } from '@/api/tag'
163
+  import { batchCreate } from '@/api/address_book'
83 164
 
84 165
   const listRes = reactive({
85 166
     list: [], total: 0, loading: false,
@@ -87,6 +168,7 @@
87 168
   const listQuery = reactive({
88 169
     page: 1,
89 170
     page_size: 10,
171
+    time_ago: null,
90 172
   })
91 173
 
92 174
   const getList = async () => {
@@ -129,12 +211,6 @@
129 211
 
130 212
   watch(() => listQuery.page_size, handlerQuery)
131 213
 
132
-  const platformList = [
133
-    { label: 'Windows', value: 'Windows' },
134
-    { label: 'Linux', value: 'Linux' },
135
-    { label: 'Mac OS', value: 'Mac OS' },
136
-    { label: 'Android', value: 'Android' },
137
-  ]
138 214
   const formVisible = ref(false)
139 215
   const formData = reactive({
140 216
     row_id: 0,
@@ -178,33 +254,137 @@
178 254
     }
179 255
   }
180 256
 
257
+  const timeDis = (time) => {
258
+    let now = new Date().getTime()
259
+    let after = new Date(time * 1000).getTime()
260
+    return (now - after) / 1000
261
+  }
262
+
263
+  const timeFilters = computed(() => [
264
+    { text: T('MinutesLess', { param: 1 }, 1), value: -60 },
265
+    { text: T('HoursLess', { param: 1 }, 1), value: -3600 },
266
+    { text: T('DaysLess', { param: 1 }, 1), value: -86400 },
267
+    { text: '---------', value: 0 },
268
+    { text: T('MinutesAgo', { param: 1 }, 1), value: 60 },
269
+    { text: T('HoursAgo', { param: 1 }, 1), value: 3600 },
270
+    { text: T('DaysAgo', { param: 1 }, 1), value: 86400 },
271
+    { text: T('MonthsAgo', { param: 1 }, 1), value: 2592000 },
272
+    // { text: T('YearsAgo', { param: 1 }, 1), value: 31536000 },
273
+  ])
274
+
275
+  const toExport = async () => {
276
+    const q = { ...listQuery }
277
+    q.page_size = 10000
278
+    q.page = 1
279
+    const res = await list(q).catch(_ => false)
280
+    if (res) {
281
+      const data = res.data.list.map(item => {
282
+        item.last_online_time = item.last_online_time ? new Date(item.last_online_time * 1000).toLocaleString() : '-'
283
+        return item
284
+      })
285
+      const csv = jsonToCsv(data)
286
+      downBlob(csv, 'peers.csv')
287
+    }
288
+  }
289
+
290
+  const {
291
+    platformList: ABPlatformList,
292
+    formVisible: ABFormVisible,
293
+    formData: ABFormData,
294
+  } = useABRepositories()
295
+  const toAddressBook = (peer) => {
296
+    ABFormData.id = peer.id
297
+    ABFormData.username = peer.username
298
+    ABFormData.hostname = peer.hostname
299
+    //匹配os
300
+    if (peer.os.indexOf('windows') !== -1) {
301
+      ABFormData.platform = ABPlatformList.find(item => item.label === 'Windows').value
302
+    } else if (peer.os.indexOf('linux') !== -1) {
303
+      ABFormData.platform = ABPlatformList.find(item => item.label === 'Linux').value
304
+    } else if (peer.os.indexOf('android') !== -1) {
305
+      ABFormData.platform = ABPlatformList.find(item => item.label === 'Android').value
306
+    } else if (peer.os.indexOf('mac') !== -1) {
307
+      ABFormData.platform = ABPlatformList.find(item => item.label === 'Mac OS').value
308
+    }
309
+    ABFormData.uuid = peer.uuid
310
+    ABFormVisible.value = true
311
+
312
+  }
313
+  const ABSubmit = async () => {
314
+    const res = await batchCreate(ABFormData).catch(_ => false)
315
+    if (res) {
316
+      ElMessage.success(T('OperationSuccess'))
317
+      ABFormVisible.value = false
318
+    }
319
+  }
320
+
321
+  const { allUsers, getAllUsers } = loadAllUsers()
322
+  const tagList = ref([])
323
+  const fetchTagListData = async (user_id) => {
324
+    const res = await fetchTagList({ user_id }).catch(_ => false)
325
+    if (res) {
326
+      const ls = []
327
+      res.data.list.map(item => {
328
+        if (!ls.includes(item.name)) {
329
+          ls.push(item.name)
330
+        }
331
+      })
332
+      tagList.value = ls.map(item => ({ name: item }))
333
+    }
334
+  }
335
+  onMounted(getAllUsers)
336
+  onMounted(fetchTagListData)
337
+
338
+  const multipleSelection = ref([])
339
+  const handleSelectionChange = (val) => {
340
+    multipleSelection.value = val
341
+  }
342
+  const toBatchDelete = async () => {
343
+    if (!multipleSelection.value.length) {
344
+      ElMessage.warning(T('PleaseSelectData'))
345
+      return false
346
+    }
347
+    const cf = await ElMessageBox.confirm(T('Confirm?', { param: T('BatchDelete') }), {
348
+      confirmButtonText: T('Confirm'),
349
+      cancelButtonText: T('Cancel'),
350
+      type: 'warning',
351
+    }).catch(_ => false)
352
+    if (!cf) {
353
+      return false
354
+    }
355
+
356
+    const res = await batchRemove({ row_ids: multipleSelection.value.map(i => i.row_id) }).catch(_ => false)
357
+    if (res) {
358
+      ElMessage.success(T('OperationSuccess'))
359
+      getList()
360
+    }
361
+  }
181 362
 </script>
182 363
 
183 364
 <style scoped lang="scss">
184 365
 .list-query .el-select {
185
-  --el-select-width: 160px;
366
+  --el-select-width: 180px;
186 367
 }
187 368
 
188
-.colors {
369
+.last_oline_time {
189 370
   display: flex;
190 371
   justify-content: center;
191 372
   align-items: center;
373
+}
192 374
 
193
-  .colorbox {
194
-    width: 50px;
195
-    height: 30px;
196
-    display: flex;
197
-    justify-content: center;
198
-    align-items: center;
199
-
200
-    .dot {
201
-      width: 10px;
202
-      height: 10px;
203
-      display: block;
204
-      border-radius: 50%;
205
-    }
375
+.dot {
376
+  width: 6px;
377
+  height: 6px;
378
+  display: block;
379
+  border-radius: 50%;
380
+  margin-left: 10px;
381
+
382
+  &.red {
383
+    background-color: red;
206 384
   }
207 385
 
386
+  &.green {
387
+    background-color: green;
388
+  }
208 389
 }
209
-
210 390
 </style>

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

@@ -22,6 +22,10 @@ export function useRepositories () {
22 22
     // color 是十进制的数字,先转成16进制
23 23
     let hex = color.toString(16)
24 24
     console.log('hex', hex)
25
+    if (hex.length < 8) {
26
+      //前面补0
27
+      hex = '0'.repeat(8 - hex.length) + hex
28
+    }
25 29
     //前两位是透明度
26 30
     let alpha = hex.slice(0, 2)
27 31
     //后六位是颜色
@@ -51,6 +55,7 @@ export function useRepositories () {
51 55
     if (b.length === 1) {
52 56
       b = '0' + b
53 57
     }
58
+    console.log('to f color', alpha + r + g + b, parseInt(alpha + r + g + b, 16))
54 59
     return parseInt(alpha + r + g + b, 16)
55 60
   }
56 61