|
26 | 26 | <!-- 密钥输入 --> |
27 | 27 | <div> |
28 | 28 | <label for="key" class="block text-sm font-medium text-[#000000e6] mb-2"> |
29 | | - <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
30 | | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/> |
31 | | - </svg> |
32 | 29 | 解密密钥 <span class="text-red-500">*</span> |
33 | 30 | </label> |
34 | | - <div class="relative"> |
35 | | - <input |
36 | | - id="key" |
37 | | - v-model="formData.key" |
38 | | - type="text" |
39 | | - placeholder="请输入64位十六进制密钥" |
40 | | - class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200" |
41 | | - :class="{ 'border-red-500': formErrors.key }" |
42 | | - required |
43 | | - /> |
44 | | - <div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2"> |
45 | | - <span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span> |
| 31 | + |
| 32 | + <div class="flex gap-3"> |
| 33 | + <div class="relative flex-1"> |
| 34 | + <input |
| 35 | + id="key" |
| 36 | + v-model="formData.key" |
| 37 | + type="text" |
| 38 | + placeholder="请输入64位十六进制密钥" |
| 39 | + class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200" |
| 40 | + :class="{ 'border-red-500': formErrors.key }" |
| 41 | + required |
| 42 | + /> |
| 43 | + <div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2"> |
| 44 | + <span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span> |
| 45 | + </div> |
46 | 46 | </div> |
| 47 | + |
| 48 | + <button |
| 49 | + type="button" |
| 50 | + @click="handleGetDbKey" |
| 51 | + :disabled="isGettingDbKey" |
| 52 | + class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap" |
| 53 | + > |
| 54 | + <svg v-if="isGettingDbKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"> |
| 55 | + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
| 56 | + <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> |
| 57 | + </svg> |
| 58 | + <svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 59 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> |
| 60 | + </svg> |
| 61 | + {{ isGettingDbKey ? '获取中...' : '自动获取' }} |
| 62 | + </button> |
47 | 63 | </div> |
48 | 64 | <p v-if="formErrors.key" class="mt-1 text-sm text-red-600 flex items-center"> |
49 | 65 | <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
55 | 71 | <svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
56 | 72 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> |
57 | 73 | </svg> |
58 | | - 使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串 |
| 74 | + 尝试自动获取,或者使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串 |
59 | 75 | </p> |
60 | 76 | </div> |
61 | 77 |
|
|
131 | 147 | <!-- 填写密钥 --> |
132 | 148 | <div class="mb-6"> |
133 | 149 | <div class="bg-gray-50 rounded-lg p-4"> |
| 150 | + |
| 151 | + <div class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200"> |
| 152 | + <span class="text-sm font-medium text-gray-500">手动输入或通过微信获取</span> |
| 153 | + <button |
| 154 | + type="button" |
| 155 | + @click="handleGetImageKey" |
| 156 | + :disabled="isGettingImageKey" |
| 157 | + class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap" |
| 158 | + > |
| 159 | + <svg v-if="isGettingImageKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"> |
| 160 | + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
| 161 | + <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> |
| 162 | + </svg> |
| 163 | + <svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 164 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> |
| 165 | + </svg> |
| 166 | + {{ isGettingImageKey ? '正在获取...' : '自动获取' }} |
| 167 | + </button> |
| 168 | + </div> |
| 169 | + |
134 | 170 | <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
135 | 171 | <div> |
136 | 172 | <label class="block text-sm font-medium text-[#000000e6] mb-2">XOR(必填)</label> |
|
158 | 194 | <svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
159 | 195 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> |
160 | 196 | </svg> |
161 | | - 使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥;AES 可选(V4-V2 需要) |
| 197 | + 尝试自动获取,或使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥;AES 可选(V4-V2 需要) |
162 | 198 | </p> |
163 | 199 | </div> |
164 | 200 | </div> |
|
325 | 361 | </div> |
326 | 362 | </div> |
327 | 363 | </div> |
| 364 | + |
| 365 | + <!-- 警告渲染 --> |
| 366 | + <transition name="fade"> |
| 367 | + <div v-if="warning" class="bg-amber-50 border border-amber-200 rounded-lg p-4 mt-6 flex items-start"> |
| 368 | + <svg class="h-5 w-5 mr-2 flex-shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 369 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> |
| 370 | + </svg> |
| 371 | + <div> |
| 372 | + <p class="font-semibold text-amber-800">温馨提示</p> |
| 373 | + <p class="text-sm mt-1 text-amber-700">{{ warning }}</p> |
| 374 | + </div> |
| 375 | + </div> |
| 376 | + </transition> |
328 | 377 |
|
329 | 378 | <!-- 错误提示 --> |
330 | 379 | <transition name="fade"> |
|
367 | 416 | import { ref, reactive, computed, onMounted } from 'vue' |
368 | 417 | import { useApi } from '~/composables/useApi' |
369 | 418 |
|
370 | | -const { decryptDatabase, saveMediaKeys, getSavedKeys } = useApi() |
| 419 | +const { decryptDatabase, saveMediaKeys, getSavedKeys, getDbKey, getImageKey, getWxStatus } = useApi() |
371 | 420 |
|
372 | 421 | const loading = ref(false) |
373 | 422 | const error = ref('') |
| 423 | +const warning = ref('') // 警告,用于密钥提示 |
374 | 424 | const currentStep = ref(0) |
375 | 425 | const mediaAccount = ref('') |
| 426 | +const isGettingDbKey = ref(false) |
| 427 | +const isGettingImageKey = ref(false) |
376 | 428 |
|
377 | 429 | // 步骤定义 |
378 | 430 | const steps = [ |
@@ -453,10 +505,89 @@ const prefillKeysForAccount = async (account) => { |
453 | 505 | } |
454 | 506 | } |
455 | 507 |
|
| 508 | +const handleGetDbKey = async () => { |
| 509 | + if (isGettingDbKey.value) return |
| 510 | + isGettingDbKey.value = true |
| 511 | +
|
| 512 | + error.value = '' |
| 513 | + warning.value = '' |
| 514 | + formErrors.key = '' |
| 515 | +
|
| 516 | + try { |
| 517 | + const statusRes = await getWxStatus() // pid不是主进程,但是没关系 |
| 518 | + const wxStatus = statusRes?.wx_status |
| 519 | +
|
| 520 | + if (wxStatus?.is_running) { |
| 521 | + warning.value = '检测到微信正在运行,5秒后将终止进程并重启以获取密钥!!' |
| 522 | + await new Promise(resolve => setTimeout(resolve, 5000)) |
| 523 | + } else { |
| 524 | + // 没有逻辑 |
| 525 | + } |
| 526 | +
|
| 527 | + warning.value = '正在启动微信以获取密钥,请确保微信未开启“自动登录”,并在启动后 1 分钟内完成登录操作。' |
| 528 | +
|
| 529 | + const res = await getDbKey() |
| 530 | +
|
| 531 | + if (res && res.status === 0) { |
| 532 | + if (res.data?.db_key) { |
| 533 | + formData.key = res.data.db_key |
| 534 | + warning.value = '' |
| 535 | + } |
| 536 | +
|
| 537 | + if (res.errmsg && res.errmsg !== 'ok') { |
| 538 | + warning.value = res.errmsg |
| 539 | + } |
| 540 | + } else { |
| 541 | + error.value = '获取失败: ' + (res?.errmsg || '未知错误') |
| 542 | + } |
| 543 | + } catch (e) { |
| 544 | + console.error(e) |
| 545 | + error.value = '系统错误: ' + e.message |
| 546 | + } finally { |
| 547 | + isGettingDbKey.value = false |
| 548 | + } |
| 549 | +} |
| 550 | +
|
| 551 | +const handleGetImageKey = async () => { |
| 552 | + if (isGettingImageKey.value) return |
| 553 | + isGettingImageKey.value = true |
| 554 | + manualKeyErrors.xor_key = '' |
| 555 | + manualKeyErrors.aes_key = '' |
| 556 | +
|
| 557 | + error.value = '' |
| 558 | + warning.value = '' |
| 559 | +
|
| 560 | + try { |
| 561 | + const res = await getImageKey() |
| 562 | +
|
| 563 | + if (res && res.status === 0) { |
| 564 | + if (res.data?.aes_key) { |
| 565 | + manualKeys.aes_key = res.data.aes_key |
| 566 | + } |
| 567 | + if (res.data?.xor_key) { |
| 568 | + // 后端记得处理为16进制再返回!!! |
| 569 | + manualKeys.xor_key = res.data.xor_key |
| 570 | + } |
| 571 | +
|
| 572 | + if (res.errmsg && res.errmsg !== 'ok') { |
| 573 | + error.value = res.errmsg |
| 574 | + } |
| 575 | + } else { |
| 576 | + error.value = '获取失败: ' + (res?.errmsg || '未知错误') |
| 577 | + } |
| 578 | + } catch (e) { |
| 579 | + console.error(e) |
| 580 | + error.value = '系统错误: ' + e.message |
| 581 | + } finally { |
| 582 | + isGettingImageKey.value = false |
| 583 | + } |
| 584 | +} |
| 585 | +
|
456 | 586 | const applyManualKeys = () => { |
457 | 587 | manualKeyErrors.xor_key = '' |
458 | 588 | manualKeyErrors.aes_key = '' |
459 | 589 | error.value = '' |
| 590 | + warning.value = '' |
460 | 591 |
|
461 | 592 | const aes = normalizeAesKey(manualKeys.aes_key) |
462 | 593 | if (!aes.ok) { |
@@ -550,6 +681,7 @@ const handleDecrypt = async () => { |
550 | 681 | |
551 | 682 | loading.value = true |
552 | 683 | error.value = '' |
| 684 | + warning.value = '' |
553 | 685 | |
554 | 686 | try { |
555 | 687 | const result = await decryptDatabase({ |
@@ -596,6 +728,7 @@ const decryptAllImages = async () => { |
596 | 728 | mediaDecrypting.value = true |
597 | 729 | mediaDecryptResult.value = null |
598 | 730 | error.value = '' |
| 731 | + warning.value = '' |
599 | 732 | |
600 | 733 | // 重置进度 |
601 | 734 | decryptProgress.current = 0 |
@@ -671,6 +804,7 @@ const decryptAllImages = async () => { |
671 | 804 | // 从密钥步骤进入图片解密步骤 |
672 | 805 | const goToMediaDecryptStep = async () => { |
673 | 806 | error.value = '' |
| 807 | + warning.value = '' |
674 | 808 | // 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示) |
675 | 809 | const ok = applyManualKeys() |
676 | 810 | if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return |
|
0 commit comments