Browse Source

add share to guest by web client

ljw 1 year ago
parent
commit
bf98a51285

+ 6 - 3
package.json

@@ -7,14 +7,16 @@
7
     "serve": "vite preview"
7
     "serve": "vite preview"
8
   },
8
   },
9
   "dependencies": {
9
   "dependencies": {
10
-    "axios": "1.6.0",
10
+    "axios": "1.7.4",
11
     "element-plus": "^2.8.2",
11
     "element-plus": "^2.8.2",
12
     "js-cookie": "^3.0.1",
12
     "js-cookie": "^3.0.1",
13
     "normalize.css": "^8.0.1",
13
     "normalize.css": "^8.0.1",
14
     "nprogress": "^0.2.0",
14
     "nprogress": "^0.2.0",
15
     "pinia": "2.0.3",
15
     "pinia": "2.0.3",
16
     "vue": "3.2.37",
16
     "vue": "3.2.37",
17
-    "vue-router": "^4.0.12"
17
+    "vue-router": "^4.0.12",
18
+    "fast-sha256": "^1.3.0",
19
+    "clipboard": "2.0.4"
18
   },
20
   },
19
   "devDependencies": {
21
   "devDependencies": {
20
     "@element-plus/icons": "0.0.11",
22
     "@element-plus/icons": "0.0.11",
@@ -23,6 +25,7 @@
23
     "qs": "^6.10.2",
25
     "qs": "^6.10.2",
24
     "sass-loader": "^12.3.0",
26
     "sass-loader": "^12.3.0",
25
     "sass": "^1.43.4",
27
     "sass": "^1.43.4",
26
-    "vite": "^2.9.18"
28
+    "vite": "^2.9.18",
29
+    "ts-proto": "^1.141.1"
27
   }
30
   }
28
 }
31
 }

+ 9 - 0
src/api/address_book.js

@@ -44,3 +44,12 @@ export function batchCreate (data) {
44
     data,
44
     data,
45
   })
45
   })
46
 }
46
 }
47
+
48
+export function shareByWebClient (data) {
49
+  return request({
50
+    url: '/address_book/shareByWebClient',
51
+    method: 'post',
52
+    data,
53
+  })
54
+}
55
+

+ 36 - 0
src/utils/clipboard.js

@@ -0,0 +1,36 @@
1
+import Clipboard from 'clipboard'
2
+import { ElMessage } from 'element-plus'
3
+import { T } from '@/utils/i18n'
4
+
5
+export function handleClipboard (text, event) {
6
+  const clipboard = new Clipboard(event.target.toString(), {
7
+    text: () => text,
8
+  })
9
+  clipboard.on('success', () => {
10
+    ElMessage.success(T('CopySuccess'))
11
+    clipboard.destroy()
12
+  })
13
+  clipboard.on('error', () => {
14
+    ElMessage.error(T('CopyFailed'))
15
+    clipboard.destroy()
16
+  })
17
+  clipboard.onClick(event)
18
+}
19
+
20
+export function copyImage (targetNode) {
21
+  if (window.getSelection) {
22
+    // chrome等主流浏览器
23
+    var selection = window.getSelection()
24
+    selection.removeAllRanges()
25
+    var range = document.createRange()
26
+    range.selectNode(targetNode)
27
+    selection.addRange(range)
28
+  } else if (document.body.createTextRange) {
29
+    console.log('IE')
30
+    // ie
31
+    const range = document.body.createTextRange()
32
+    range.moveToElementText(targetNode)
33
+    range.select()
34
+  }
35
+  document.execCommand('copy')
36
+}

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

@@ -282,5 +282,70 @@
282
   },
282
   },
283
   "PleaseSelectData": {
283
   "PleaseSelectData": {
284
     "One": "Please select data"
284
     "One": "Please select data"
285
+  },
286
+  "PasswordType": {
287
+    "One": "Password Type"
288
+  },
289
+  "OncePassword": {
290
+    "One": "One-time Password"
291
+  },
292
+  "FixedPassword": {
293
+    "One": "Fixed Password"
294
+  },
295
+  "FixedPasswordWarning": {
296
+    "One": "Fixed passwords may be leaked, so please use them with caution and use one-time passwords is recommended"
297
+  },
298
+  "ExpireTime": {
299
+    "One": "Expire Time"
300
+  },
301
+  "ShareByWebClient": {
302
+    "One": "Share By Web Client"
303
+  },
304
+  "Minutes": {
305
+    "One": "{param} Minute",
306
+    "Other": "{param} Minutes"
307
+  },
308
+  "Hours": {
309
+    "One": "{param} Hour",
310
+    "Other": "{param} Hours"
311
+  },
312
+  "Days": {
313
+    "One": "{param} Day",
314
+    "Other": "{param} Days"
315
+  },
316
+  "Weeks": {
317
+    "One": "{param} Week",
318
+    "Other": "{param} Weeks"
319
+  },
320
+  "Months": {
321
+    "One": "{param} Month",
322
+    "Other": "{param} Months"
323
+  },
324
+  "Forever": {
325
+    "One": "Forever"
326
+  },
327
+  "Error": {
328
+    "One": "Error"
329
+  },
330
+  "IDNotExist": {
331
+    "One": "ID does not exist"
332
+  },
333
+  "RemoteDesktopOffline": {
334
+    "One": "Remote desktop is offline"
335
+  },
336
+  "KeyMismatch": {
337
+    "One": "Key mismatch"
338
+  },
339
+  "KeyOveruse": {
340
+    "One": "Key overuse"
341
+  },
342
+  "Link": {
343
+    "One": "Link"
344
+  },
345
+  "CopySuccess": {
346
+    "One": "Copy Success"
347
+  },
348
+  "CopyFailed": {
349
+    "One": "Copy Failed"
285
   }
350
   }
286
 }
351
 }

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

@@ -274,5 +274,65 @@
274
   },
274
   },
275
   "PleaseSelectData": {
275
   "PleaseSelectData": {
276
     "One": "请选择数据"
276
     "One": "请选择数据"
277
+  },
278
+  "PasswordType": {
279
+    "One": "密码类型"
280
+  },
281
+  "OncePassword": {
282
+    "One": "一次性密码"
283
+  },
284
+  "FixedPassword": {
285
+    "One": "固定密码"
286
+  },
287
+  "FixedPasswordWarning": {
288
+    "One": "固定密码可能存在泄露风险,请谨慎使用,建议使用一次性密码"
289
+  },
290
+  "ExpireTime": {
291
+    "One": "过期时间"
292
+  },
293
+  "ShareByWebClient": {
294
+    "One": "通过 Web Client 分享"
295
+  },
296
+  "Minutes": {
297
+    "One": "{param} 分钟"
298
+  },
299
+  "Hours": {
300
+    "One": "{param} 小时"
301
+  },
302
+  "Days": {
303
+    "One": "{param} 天"
304
+  },
305
+  "Weeks": {
306
+    "One": "{param} 周"
307
+  },
308
+  "Months": {
309
+    "One": "{param} 月"
310
+  },
311
+  "Forever": {
312
+    "One": "永久"
313
+  },
314
+  "Error": {
315
+    "One": "错误"
316
+  },
317
+  "IDNotExist": {
318
+    "One": "ID 不存在"
319
+  },
320
+  "RemoteDesktopOffline": {
321
+    "One": "远程电脑不在线"
322
+  },
323
+  "KeyMismatch": {
324
+    "One": "KEY不匹配"
325
+  },
326
+  "KeyOveruse": {
327
+    "One": "KEY使用过度"
328
+  },
329
+  "Link": {
330
+    "One": "链接"
331
+  },
332
+  "CopySuccess": {
333
+    "One": "复制成功"
334
+  },
335
+  "CopyFailed": {
336
+    "One": "复制失败"
277
   }
337
   }
278
 }
338
 }

+ 1 - 1
src/utils/request.js

@@ -28,7 +28,7 @@ service.interceptors.request.use(
28
     const app = useAppStore()
28
     const app = useAppStore()
29
     const lang = app.setting.lang
29
     const lang = app.setting.lang
30
     if (lang) {
30
     if (lang) {
31
-      console.log('lang', lang)
31
+      // console.log('lang', lang)
32
       config.headers['Accept-Language'] = lang
32
       config.headers['Accept-Language'] = lang
33
     }
33
     }
34
 
34
 

+ 84 - 1
src/utils/webclient.js

@@ -1,5 +1,10 @@
1
 import { ref } from 'vue'
1
 import { ref } from 'vue'
2
 import { config } from '@/api/rustdesk'
2
 import { config } from '@/api/rustdesk'
3
+import Websock from '@/utils/webclient/websock'
4
+import * as rendezvous from '@/utils/webclient/rendezvous'
5
+import * as message from '@/utils/webclient/message'
6
+import { ElMessageBox } from 'element-plus'
7
+import { T } from '@/utils/i18n'
3
 
8
 
4
 export const toWebClientLink = (row) => {
9
 export const toWebClientLink = (row) => {
5
   window.open(`${rustdeskConfig.value.api_server}/webclient/#/?id=${row.id}`)
10
   window.open(`${rustdeskConfig.value.api_server}/webclient/#/?id=${row.id}`)
@@ -23,4 +28,82 @@ export function loadRustdeskConfig () {
23
     rustdeskConfig,
28
     rustdeskConfig,
24
   }
29
   }
25
 }
30
 }
26
-const { rustdeskConfig } = loadRustdeskConfig()
31
+
32
+export const { rustdeskConfig } = loadRustdeskConfig()
33
+export async function getPeerSlat (id) {
34
+  const [addr, port] = rustdeskConfig.value.id_server.split(':')
35
+  if (!addr) {
36
+    return
37
+  }
38
+  const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
39
+  const ws = new Websock(`${scheme}://${addr}:21118`, true)
40
+  await ws.open()
41
+  const conn_type = rendezvous.ConnType.DEFAULT_CONN
42
+  const nat_type = rendezvous.NatType.SYMMETRIC
43
+  const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
44
+    id,
45
+    licence_key: rustdeskConfig.value.key || undefined,
46
+    conn_type,
47
+    nat_type,
48
+    token: undefined,
49
+  })
50
+  ws.sendRendezvous({ punch_hole_request })
51
+  //rendezvous.RendezvousMessage
52
+  const msg = (await ws.next())
53
+  ws.close()
54
+  console.log(new Date() + ': Got relay response', msg)
55
+  const phr = msg.punch_hole_response
56
+  const rr = msg.relay_response
57
+  if (phr) {
58
+    if (phr?.other_failure) {
59
+      this.msgbox('error', 'Error', phr?.other_failure)
60
+      return
61
+    }
62
+    if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) {
63
+      switch (phr?.failure) {
64
+        case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST:
65
+          ElMessageBox.alert(T('IDNotExist'), T('Error'))
66
+          break
67
+        case rendezvous.PunchHoleResponse_Failure.OFFLINE:
68
+          ElMessageBox.alert(T('RemoteDesktopOffline'), T('Error'))
69
+          break
70
+        case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH:
71
+          ElMessageBox.alert(T('KeyMismatch'), T('Error'))
72
+          break
73
+        case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE:
74
+          ElMessageBox.alert(T('KeyOveruse'), T('Error'))
75
+          break
76
+      }
77
+    }
78
+    return false
79
+  } else if (rr) {
80
+    const uuid = rr.uuid
81
+    console.log(new Date() + ': Connecting to relay server')
82
+
83
+    const _ws = new Websock(`${scheme}://${addr}:21119`, false)
84
+    await _ws.open()
85
+    console.log(new Date() + ': Connected to relay server')
86
+    const request_relay = rendezvous.RequestRelay.fromPartial({
87
+      licence_key: rustdeskConfig.value.key || undefined,
88
+      uuid,
89
+    })
90
+    _ws.sendRendezvous({ request_relay })
91
+
92
+    //暂不支持pk
93
+    const public_key = message.PublicKey.fromPartial({})
94
+    _ws?.sendMessage({ public_key })
95
+    // const secure = (await this.secure(pk)) || false;
96
+    // globals.pushEvent("connection_ready", { secure, direct: false });
97
+    while (true) {
98
+      const msg = (await _ws?.next())
99
+      console.log('msg', msg)
100
+      if (msg?.hash) {
101
+        console.log('hash msg.....', msg.hash)
102
+        _ws.close()
103
+        return msg.hash
104
+      }
105
+    }
106
+    return false
107
+  }
108
+
109
+}

File diff suppressed because it is too large
+ 6073 - 0
src/utils/webclient/message.ts


File diff suppressed because it is too large
+ 2025 - 0
src/utils/webclient/rendezvous.ts


+ 183 - 0
src/utils/webclient/websock.ts

@@ -0,0 +1,183 @@
1
+import * as message from "./message.js";
2
+import * as rendezvous from "./rendezvous.js";
3
+
4
+type Keys = "message" | "open" | "close" | "error";
5
+
6
+export default class Websock {
7
+  _websocket: WebSocket;
8
+  _eventHandlers: { [key in Keys]: Function };
9
+  _buf: (rendezvous.RendezvousMessage | message.Message)[];
10
+  _status: any;
11
+  _latency: number;
12
+  _secretKey: [Uint8Array, number, number] | undefined;
13
+  _uri: string;
14
+  _isRendezvous: boolean;
15
+
16
+  constructor(uri: string, isRendezvous: boolean = true) {
17
+    this._eventHandlers = {
18
+      message: (_: any) => {},
19
+      open: () => {},
20
+      close: () => {},
21
+      error: () => {},
22
+    };
23
+    this._uri = uri;
24
+    this._status = "";
25
+    this._buf = [];
26
+    this._websocket = new WebSocket(uri);
27
+    this._websocket.onmessage = this._recv_message.bind(this);
28
+    this._websocket.binaryType = "arraybuffer";
29
+    this._latency = new Date().getTime();
30
+    this._isRendezvous = isRendezvous;
31
+  }
32
+
33
+  latency(): number {
34
+    return this._latency;
35
+  }
36
+
37
+  setSecretKey(key: Uint8Array) {
38
+    this._secretKey = [key, 0, 0];
39
+  }
40
+
41
+  sendMessage(json: message.DeepPartial<message.Message>) {
42
+    let data = message.Message.encode(
43
+      message.Message.fromPartial(json)
44
+    ).finish();
45
+    // let k = this._secretKey;
46
+    // if (k) {
47
+    //   k[1] += 1;
48
+    //   data = globals.encrypt(data, k[1], k[0]);
49
+    // }
50
+    this._websocket.send(data);
51
+  }
52
+
53
+  sendRendezvous(data: rendezvous.DeepPartial<rendezvous.RendezvousMessage>) {
54
+    this._websocket.send(
55
+      rendezvous.RendezvousMessage.encode(
56
+        rendezvous.RendezvousMessage.fromPartial(data)
57
+      ).finish()
58
+    );
59
+  }
60
+
61
+  parseMessage(data: Uint8Array) {
62
+    return message.Message.decode(data);
63
+  }
64
+
65
+  parseRendezvous(data: Uint8Array) {
66
+    return rendezvous.RendezvousMessage.decode(data);
67
+  }
68
+
69
+  // Event Handlers
70
+  off(evt: Keys) {
71
+    this._eventHandlers[evt] = () => {};
72
+  }
73
+
74
+  on(evt: Keys, handler: Function) {
75
+    this._eventHandlers[evt] = handler;
76
+  }
77
+
78
+  async open(timeout: number = 12000): Promise<Websock> {
79
+    return new Promise((resolve, reject) => {
80
+      setTimeout(() => {
81
+        if (this._status != "open") {
82
+          reject(this._status || "Timeout");
83
+        }
84
+      }, timeout);
85
+      this._websocket.onopen = () => {
86
+        this._latency = new Date().getTime() - this._latency;
87
+        this._status = "open";
88
+        console.debug(">> WebSock.onopen");
89
+        if (this._websocket?.protocol) {
90
+          console.info(
91
+            "Server choose sub-protocol: " + this._websocket.protocol
92
+          );
93
+        }
94
+
95
+        this._eventHandlers.open();
96
+        console.info("WebSock.onopen");
97
+        resolve(this);
98
+      };
99
+      this._websocket.onclose = (e) => {
100
+        if (this._status == "open") {
101
+          // e.code 1000 means that the connection was closed normally.
102
+          //
103
+        }
104
+        this._status = e;
105
+        console.error("WebSock.onclose: ");
106
+        console.error(e);
107
+        this._eventHandlers.close(e);
108
+        reject("Reset by the peer");
109
+      };
110
+      this._websocket.onerror = (e: any) => {
111
+        if (!this._status) {
112
+          reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server");
113
+          return;
114
+        }
115
+        this._status = e;
116
+        console.error("WebSock.onerror: ")
117
+        console.error(e);
118
+        this._eventHandlers.error(e);
119
+      };
120
+    });
121
+  }
122
+
123
+  async next(
124
+    timeout = 12000
125
+  ): Promise<rendezvous.RendezvousMessage | message.Message> {
126
+    const func = (
127
+      resolve: (value: rendezvous.RendezvousMessage | message.Message) => void,
128
+      reject: (reason: any) => void,
129
+      tm0: number
130
+    ) => {
131
+      // console.log('next')
132
+      if (this._buf.length) {
133
+        resolve(this._buf[0]);
134
+        this._buf.splice(0, 1);
135
+      } else {
136
+        if (this._status != "open") {
137
+          reject(this._status);
138
+          return;
139
+        }
140
+        if (new Date().getTime() > tm0 + timeout) {
141
+          reject("Timeout");
142
+        } else {
143
+          setTimeout(() => func(resolve, reject, tm0), 1);
144
+        }
145
+      }
146
+    };
147
+    return new Promise((resolve, reject) => {
148
+      func(resolve, reject, new Date().getTime());
149
+    });
150
+  }
151
+
152
+  close() {
153
+    this._status = "";
154
+    if (this._websocket) {
155
+      if (
156
+        this._websocket.readyState === WebSocket.OPEN ||
157
+        this._websocket.readyState === WebSocket.CONNECTING
158
+      ) {
159
+        console.info("Closing WebSocket connection");
160
+        this._websocket.close();
161
+      }
162
+
163
+      this._websocket.onmessage = () => {};
164
+    }
165
+  }
166
+
167
+  _recv_message(e: any) {
168
+    if (e.data instanceof window.ArrayBuffer) {
169
+      let bytes = new Uint8Array(e.data);
170
+      // const k = this._secretKey;
171
+      // if (k) {
172
+      //   k[2] += 1;
173
+      //   bytes = globals.decrypt(bytes, k[2], k[0]);
174
+      // }
175
+      this._buf.push(
176
+        this._isRendezvous
177
+          ? this.parseRendezvous(bytes)
178
+          : this.parseMessage(bytes)
179
+      );
180
+    }
181
+    this._eventHandlers.message(e.data);
182
+  }
183
+}

+ 144 - 0
src/views/address_book/components/shareByWebClient.vue

@@ -0,0 +1,144 @@
1
+<template>
2
+  <el-form ref="shareform" :model="formData" label-width="120px" label-suffix=" :">
3
+    <el-form-item :label="T('ID')" prop="id" required>
4
+      {{ formData.id }}
5
+    </el-form-item>
6
+    <el-form-item :label="T('PasswordType')">
7
+      <div>
8
+        <el-radio-group v-model="formData.password_type" @change="changePwdType">
9
+          <el-radio value="once">{{ T('OncePassword') }}</el-radio>
10
+          <el-radio value="fixed">{{ T('FixedPassword') }}</el-radio>
11
+        </el-radio-group>
12
+        <div v-if="formData.password_type==='fixed'" style="color: red">
13
+          {{ T('FixedPasswordWarning') }}
14
+        </div>
15
+      </div>
16
+    </el-form-item>
17
+    <el-form-item :label="T('Password')" prop="password" required>
18
+      <el-input v-model="formData.password" type="password" show-password></el-input>
19
+    </el-form-item>
20
+    <el-form-item :label="T('ExpireTime')" prop="expire" required>
21
+      <el-select v-model="formData.expire">
22
+        <el-option
23
+            v-for="item in expireTimes"
24
+            :key="item.value"
25
+            :label="item.label"
26
+            :value="item.value"
27
+        ></el-option>
28
+      </el-select>
29
+    </el-form-item>
30
+    <el-form-item v-if="link" :label="T('Link')">
31
+      <el-input v-model="link" readonly>
32
+        <template #append>
33
+          <el-button :icon="CopyDocument" @click="copyLink"/>
34
+        </template>
35
+      </el-input>
36
+    </el-form-item>
37
+    <el-form-item>
38
+      <el-button v-if="!link" @click="cancel">{{ T('Cancel') }}</el-button>
39
+      <el-button v-if="!link" :loading="loading" @click="submitShare" type="primary">{{ T('Submit') }}</el-button>
40
+      <el-button v-else @click="cancel" type="success">{{ T('Close') }}</el-button>
41
+    </el-form-item>
42
+  </el-form>
43
+</template>
44
+
45
+<script setup>
46
+  import { T } from '@/utils/i18n'
47
+  import { computed, reactive, ref, watch } from 'vue'
48
+  import { getPeerSlat, rustdeskConfig } from '@/utils/webclient'
49
+  import * as sha256 from 'fast-sha256'
50
+  import { shareByWebClient } from '@/api/address_book'
51
+  import { CopyDocument } from '@element-plus/icons'
52
+  import { handleClipboard } from '@/utils/clipboard'
53
+
54
+  const props = defineProps({
55
+    id: String,
56
+    hash: String,
57
+  })
58
+  const emits = defineEmits(['cancel', 'success'])
59
+  const formData = reactive({
60
+    id: props.id,
61
+    password_type: 'once',
62
+    password: '',
63
+    expire: 1800,
64
+    hash: props.hash,
65
+  })
66
+  watch(() => props.id, () => {
67
+    init()
68
+  })
69
+  const init = () => {
70
+    console.log('init')
71
+    formData.id = props.id
72
+    formData.hash = props.hash
73
+    formData.password = ''
74
+    formData.expire = 300
75
+    formData.password_type = 'once'
76
+    link.value = ''
77
+  }
78
+  const link = ref('')
79
+  const expireTimes = computed(() => [
80
+    { label: T('Minutes', { param: 5 }, 5), value: 300 },
81
+    { label: T('Minutes', { param: 30 }, 30), value: 1800 },
82
+    { label: T('Hours', { param: 1 }, 1), value: 3600 },
83
+    { label: T('Days', { param: 1 }, 1), value: 86400 },
84
+    { label: T('Weeks', { param: 1 }, 1), value: 604800 },
85
+    { label: T('Months', { param: 1 }, 1), value: 2592000 },
86
+    { label: T('Forever'), value: 0 },
87
+  ])
88
+  const changePwdType = (val) => {
89
+    if (val === 'fixed' && !formData.password) {
90
+      formData.password = props.hash
91
+    }
92
+    if (val === 'once') {
93
+      formData.password = ''
94
+    }
95
+  }
96
+  const cancel = () => {
97
+    loading.value = false
98
+    emits('cancel')
99
+    init()
100
+  }
101
+  const loading = ref(false)
102
+  const submitShare = async () => {
103
+    if (!formData.password) {
104
+      return
105
+    }
106
+    loading.value = true
107
+    const _formData = { ...formData }
108
+    if (formData.password !== formData.hash) {
109
+      const res = await getPeerSlat(formData.id).catch(_ => false)
110
+      if (!res) {
111
+        loading.value = false
112
+        return
113
+      }
114
+      const p = hash([formData.password, res.salt])
115
+      _formData.password = btoa(p.toString().split(',').map((v) => String.fromCharCode(v)).join(''))
116
+    }
117
+    const res = await shareByWebClient(_formData).catch(_ => false)
118
+    if (res) {
119
+      link.value = `${rustdeskConfig.value.api_server}/webclient/#/?share_token=${res.data.share_token}`
120
+      emits('success')
121
+    }
122
+    loading.value = false
123
+  }
124
+
125
+  const copyLink = (e) => {
126
+    handleClipboard(link.value, e)
127
+  }
128
+
129
+  const hash = (datas) => {
130
+    const hasher = new sha256.Hash()
131
+    datas.forEach((data) => {
132
+      if (typeof data == 'string') {
133
+        data = new TextEncoder().encode(data)
134
+      }
135
+      hasher.update(data)
136
+    })
137
+    return hasher.digest()
138
+  }
139
+
140
+</script>
141
+
142
+<style scoped lang="scss">
143
+
144
+</style>

+ 13 - 1
src/views/address_book/index.js

@@ -116,7 +116,16 @@ export function useRepositories (user_id) {
116
       getList()
116
       getList()
117
     }
117
     }
118
   }
118
   }
119
-
119
+  const shareToWebClientVisible = ref(false)
120
+  const shareToWebClientForm = reactive({
121
+    id: '',
122
+    hash: '',
123
+  })
124
+  const toShowShare = (row) => {
125
+    shareToWebClientForm.id = row.id
126
+    shareToWebClientForm.hash = row.hash
127
+    shareToWebClientVisible.value = true
128
+  }
120
   return {
129
   return {
121
     listRes,
130
     listRes,
122
     listQuery,
131
     listQuery,
@@ -129,5 +138,8 @@ export function useRepositories (user_id) {
129
     toEdit,
138
     toEdit,
130
     toAdd,
139
     toAdd,
131
     submit,
140
     submit,
141
+    shareToWebClientVisible,
142
+    shareToWebClientForm,
143
+    toShowShare,
132
   }
144
   }
133
 }
145
 }

+ 15 - 4
src/views/address_book/index.vue

@@ -44,9 +44,10 @@
44
         <el-table-column prop="tags" :label="T('Tags')" align="center"/>
44
         <el-table-column prop="tags" :label="T('Tags')" align="center"/>
45
         <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
45
         <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
46
         <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
46
         <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
47
-        <el-table-column label="操作" align="center" width="400">
47
+        <el-table-column label="操作" align="center" width="500">
48
           <template #default="{row}">
48
           <template #default="{row}">
49
-            <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
49
+            <el-button type="success" @click="toWebClientLink(row)">Web Client</el-button>
50
+            <!--            <el-button type="primary" @click="toShowShare(row)">{{ T('ShareByWebClient') }}</el-button>-->
50
             <el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
51
             <el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
51
             <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
52
             <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
52
           </template>
53
           </template>
@@ -139,6 +140,12 @@
139
         </el-form-item>
140
         </el-form-item>
140
       </el-form>
141
       </el-form>
141
     </el-dialog>
142
     </el-dialog>
143
+<!--    <el-dialog v-model="shareToWebClientVisible" width="900" :close-on-click-modal="false">
144
+      <shareByWebClient :id="shareToWebClientForm.id"
145
+                        :hash="shareToWebClientForm.hash"
146
+                        @cancel="shareToWebClientVisible=false"
147
+                        @success=""/>
148
+    </el-dialog>-->
142
   </div>
149
   </div>
143
 </template>
150
 </template>
144
 
151
 
@@ -147,9 +154,10 @@
147
   import { list as fetchTagList } from '@/api/tag'
154
   import { list as fetchTagList } from '@/api/tag'
148
   import { loadAllUsers } from '@/global'
155
   import { loadAllUsers } from '@/global'
149
   import { useRepositories } from '@/views/address_book/index'
156
   import { useRepositories } from '@/views/address_book/index'
150
-  import { toWebClientLink } from '@/utils/webclient'
157
+  import { toWebClientLink, getPeerSlat } from '@/utils/webclient'
151
   import { T } from '@/utils/i18n'
158
   import { T } from '@/utils/i18n'
152
   import { useRoute } from 'vue-router'
159
   import { useRoute } from 'vue-router'
160
+  import shareByWebClient from '@/views/address_book/components/shareByWebClient.vue'
153
 
161
 
154
   const route = useRoute()
162
   const route = useRoute()
155
   const { allUsers, getAllUsers } = loadAllUsers()
163
   const { allUsers, getAllUsers } = loadAllUsers()
@@ -178,7 +186,9 @@
178
     toEdit,
186
     toEdit,
179
     toAdd,
187
     toAdd,
180
     submit,
188
     submit,
181
-    currentColor,
189
+    shareToWebClientVisible,
190
+    shareToWebClientForm,
191
+    toShowShare,
182
   } = useRepositories()
192
   } = useRepositories()
183
 
193
 
184
   if (route.query?.user_id) {
194
   if (route.query?.user_id) {
@@ -192,6 +202,7 @@
192
 
202
 
193
   watch(() => listQuery.page_size, handlerQuery)
203
   watch(() => listQuery.page_size, handlerQuery)
194
 
204
 
205
+
195
 </script>
206
 </script>
196
 
207
 
197
 <style scoped lang="scss">
208
 <style scoped lang="scss">

+ 13 - 2
src/views/my/address_book/index.vue

@@ -29,9 +29,10 @@
29
         <el-table-column prop="tags" :label="T('Tags')" align="center"/>
29
         <el-table-column prop="tags" :label="T('Tags')" align="center"/>
30
         <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
30
         <!--        <el-table-column prop="created_at" label="创建时间" align="center"/>-->
31
         <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
31
         <!--        <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
32
-        <el-table-column :label="T('Actions')" align="center" width="400">
32
+        <el-table-column :label="T('Actions')" align="center" width="500">
33
           <template #default="{row}">
33
           <template #default="{row}">
34
-            <el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
34
+            <el-button type="success" @click="toWebClientLink(row)">Web Client</el-button>
35
+            <el-button type="primary" @click="toShowShare(row)">{{ T('ShareByWebClient') }}</el-button>
35
             <el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
36
             <el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
36
             <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
37
             <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
37
           </template>
38
           </template>
@@ -114,6 +115,12 @@
114
         </el-form-item>
115
         </el-form-item>
115
       </el-form>
116
       </el-form>
116
     </el-dialog>
117
     </el-dialog>
118
+    <el-dialog v-model="shareToWebClientVisible" width="900" :close-on-click-modal="false">
119
+      <shareByWebClient :id="shareToWebClientForm.id"
120
+                        :hash="shareToWebClientForm.hash"
121
+                        @cancel="shareToWebClientVisible=false"
122
+                        @success=""/>
123
+    </el-dialog>
117
   </div>
124
   </div>
118
 </template>
125
 </template>
119
 
126
 
@@ -123,6 +130,7 @@
123
   import { useRepositories } from '@/views/address_book'
130
   import { useRepositories } from '@/views/address_book'
124
   import { toWebClientLink } from '@/utils/webclient'
131
   import { toWebClientLink } from '@/utils/webclient'
125
   import { T } from '@/utils/i18n'
132
   import { T } from '@/utils/i18n'
133
+  import shareByWebClient from '@/views/address_book/components/shareByWebClient.vue'
126
 
134
 
127
   const tagList = ref([])
135
   const tagList = ref([])
128
   const fetchTagListData = async () => {
136
   const fetchTagListData = async () => {
@@ -145,6 +153,9 @@
145
     toEdit,
153
     toEdit,
146
     toAdd,
154
     toAdd,
147
     submit,
155
     submit,
156
+    shareToWebClientVisible,
157
+    shareToWebClientForm,
158
+    toShowShare,
148
   } = useRepositories()
159
   } = useRepositories()
149
 
160
 
150
   listQuery.is_my = 1
161
   listQuery.is_my = 1

+ 2 - 2
vite.config.js

@@ -31,8 +31,8 @@ const conf = {
31
     },
31
     },
32
   },
32
   },
33
   build: {
33
   build: {
34
-    target: 'es2015',
35
-    minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
34
+    target: 'es2020',
35
+    minify: 'esbuild', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
36
     manifest: false, // 是否产出maifest.json
36
     manifest: false, // 是否产出maifest.json
37
     sourcemap: false, // 是否产出soucemap.json
37
     sourcemap: false, // 是否产出soucemap.json
38
     emptyOutDir: true,
38
     emptyOutDir: true,