125125 </button >
126126 </div >
127127 </div >
128+
129+ <!-- 解密进度 -->
130+ <div v-if =" loading || dbDecryptProgress.total > 0" class =" mt-6" >
131+ <div class =" flex items-center justify-between mb-2" >
132+ <div class =" text-sm text-[#7F7F7F]" >
133+ {{ dbDecryptProgress.message || (loading ? '解密中...' : '') }}
134+ </div >
135+ <div v-if =" dbDecryptProgress.total > 0" class =" text-sm font-mono text-[#000000e6]" >
136+ {{ dbDecryptProgress.current }} / {{ dbDecryptProgress.total }}
137+ </div >
138+ </div >
139+
140+ <div class =" w-full bg-gray-200 rounded-full h-2 overflow-hidden" >
141+ <div
142+ class =" h-full bg-[#07C160] transition-all duration-300"
143+ :style =" { width: dbProgressPercent + '%' }"
144+ ></div >
145+ </div >
146+
147+ <div v-if =" dbDecryptProgress.current_file" class =" mt-2 text-xs text-[#7F7F7F] truncate font-mono" >
148+ {{ dbDecryptProgress.current_file }}
149+ </div >
150+
151+ <div v-if =" dbDecryptProgress.total > 0" class =" mt-3 grid grid-cols-2 gap-4 text-center" >
152+ <div class =" bg-gray-50 rounded-lg p-3" >
153+ <div class =" text-lg font-bold text-[#07C160]" >{{ dbDecryptProgress.success_count }}</div >
154+ <div class =" text-xs text-[#7F7F7F]" >成功</div >
155+ </div >
156+ <div class =" bg-gray-50 rounded-lg p-3" >
157+ <div class =" text-lg font-bold text-[#FA5151]" >{{ dbDecryptProgress.fail_count }}</div >
158+ <div class =" text-xs text-[#7F7F7F]" >失败</div >
159+ </div >
160+ </div >
161+ </div >
128162 </form >
129163 </div >
130164 </div >
413447 </style >
414448
415449<script setup>
416- import { ref , reactive , computed , onMounted } from ' vue'
450+ import { ref , reactive , computed , onMounted , onBeforeUnmount } from ' vue'
417451import { useApi } from ' ~/composables/useApi'
418452
419453const { decryptDatabase , saveMediaKeys , getSavedKeys , getDbKey , getImageKey , getWxStatus } = useApi ()
@@ -625,6 +659,22 @@ const clearManualKeys = () => {
625659const mediaDecryptResult = ref (null )
626660const mediaDecrypting = ref (false )
627661
662+ // 数据库解密进度(SSE)
663+ const dbDecryptProgress = reactive ({
664+ current: 0 ,
665+ total: 0 ,
666+ success_count: 0 ,
667+ fail_count: 0 ,
668+ current_file: ' ' ,
669+ status: ' ' ,
670+ message: ' '
671+ })
672+
673+ const dbProgressPercent = computed (() => {
674+ if (dbDecryptProgress .total === 0 ) return 0
675+ return Math .round ((dbDecryptProgress .current / dbDecryptProgress .total ) * 100 )
676+ })
677+
628678// 实时解密进度
629679const decryptProgress = reactive ({
630680 current: 0 ,
@@ -673,6 +723,27 @@ const validateForm = () => {
673723 return isValid
674724}
675725
726+ let dbDecryptEventSource = null
727+ onBeforeUnmount (() => {
728+ try {
729+ if (dbDecryptEventSource) dbDecryptEventSource .close ()
730+ } catch (e) {
731+ // ignore
732+ } finally {
733+ dbDecryptEventSource = null
734+ }
735+ })
736+
737+ const resetDbDecryptProgress = () => {
738+ dbDecryptProgress .current = 0
739+ dbDecryptProgress .total = 0
740+ dbDecryptProgress .success_count = 0
741+ dbDecryptProgress .fail_count = 0
742+ dbDecryptProgress .current_file = ' '
743+ dbDecryptProgress .status = ' '
744+ dbDecryptProgress .message = ' '
745+ }
746+
676747// 处理解密
677748const handleDecrypt = async () => {
678749 if (! validateForm ()) {
@@ -682,43 +753,142 @@ const handleDecrypt = async () => {
682753 loading .value = true
683754 error .value = ' '
684755 warning .value = ' '
756+
757+ resetDbDecryptProgress ()
685758
686759 try {
687- const result = await decryptDatabase ({
688- key: formData .key ,
689- db_storage_path: formData .db_storage_path
690- })
691-
692- if (result .status === ' completed' ) {
693- // 解密成功,保存结果并进入下一步
694- decryptResult .value = result
695- if (process .client && typeof window !== ' undefined' ) {
696- sessionStorage .setItem (' decryptResult' , JSON .stringify (result))
760+ const canSse = process .client && typeof window !== ' undefined' && typeof EventSource !== ' undefined'
761+
762+ // Fallback: 如果环境不支持 SSE,则使用普通 POST(无进度)。
763+ if (! canSse) {
764+ const result = await decryptDatabase ({
765+ key: formData .key ,
766+ db_storage_path: formData .db_storage_path
767+ })
768+
769+ if (result .status === ' completed' ) {
770+ decryptResult .value = result
771+ if (process .client && typeof window !== ' undefined' ) {
772+ sessionStorage .setItem (' decryptResult' , JSON .stringify (result))
773+ }
774+ try {
775+ const accounts = Object .keys (result .account_results || {})
776+ if (accounts .length > 0 ) mediaAccount .value = accounts[0 ]
777+ } catch (e) {}
778+
779+ clearManualKeys ()
780+ currentStep .value = 1
781+ await prefillKeysForAccount (mediaAccount .value )
782+ } else if (result .status === ' failed' ) {
783+ if (result .failure_count > 0 && result .success_count === 0 ) {
784+ error .value = result .message || ' 所有文件解密失败'
785+ } else {
786+ error .value = ' 部分文件解密失败,请检查密钥是否正确'
787+ }
788+ } else {
789+ error .value = result .message || ' 解密失败,请检查输入信息'
697790 }
698- // 记录当前账号(用于图片解密/密钥保存)
791+
792+ loading .value = false
793+ return
794+ }
795+
796+ // SSE: 解密过程实时推送进度
797+ if (dbDecryptEventSource) {
798+ try {
799+ dbDecryptEventSource .close ()
800+ } catch (e) {}
801+ dbDecryptEventSource = null
802+ }
803+
804+ const params = new URLSearchParams ()
805+ params .set (' key' , formData .key )
806+ params .set (' db_storage_path' , formData .db_storage_path )
807+ const url = ` http://localhost:8000/api/decrypt_stream?${ params .toString ()} `
808+
809+ dbDecryptProgress .message = ' 连接中...'
810+ const eventSource = new EventSource (url)
811+ dbDecryptEventSource = eventSource
812+
813+ eventSource .onmessage = async (event ) => {
699814 try {
700- const accounts = Object .keys (result .account_results || {})
701- if (accounts .length > 0 ) mediaAccount .value = accounts[0 ]
815+ const data = JSON .parse (event .data )
816+
817+ if (data .type === ' scanning' ) {
818+ dbDecryptProgress .message = data .message || ' 正在扫描数据库文件...'
819+ } else if (data .type === ' start' ) {
820+ dbDecryptProgress .total = data .total || 0
821+ dbDecryptProgress .message = data .message || ' 开始解密...'
822+ } else if (data .type === ' progress' ) {
823+ dbDecryptProgress .current = data .current || 0
824+ dbDecryptProgress .total = data .total || 0
825+ dbDecryptProgress .success_count = data .success_count || 0
826+ dbDecryptProgress .fail_count = data .fail_count || 0
827+ dbDecryptProgress .current_file = data .current_file || ' '
828+ dbDecryptProgress .status = data .status || ' '
829+ dbDecryptProgress .message = data .message || ' '
830+ } else if (data .type === ' phase' ) {
831+ // e.g. building cache
832+ dbDecryptProgress .message = data .message || ' '
833+ } else if (data .type === ' complete' ) {
834+ dbDecryptProgress .status = ' complete'
835+ dbDecryptProgress .current = data .total_databases || dbDecryptProgress .total
836+ dbDecryptProgress .total = data .total_databases || dbDecryptProgress .total
837+ dbDecryptProgress .success_count = data .success_count || 0
838+ dbDecryptProgress .fail_count = data .failure_count || 0
839+ dbDecryptProgress .message = data .message || ' 解密完成'
840+
841+ decryptResult .value = data
842+ if (process .client && typeof window !== ' undefined' ) {
843+ sessionStorage .setItem (' decryptResult' , JSON .stringify (data))
844+ }
845+
846+ try {
847+ const accounts = Object .keys (data .account_results || {})
848+ if (accounts .length > 0 ) mediaAccount .value = accounts[0 ]
849+ } catch (e) {}
850+
851+ try {
852+ eventSource .close ()
853+ } catch (e) {}
854+ dbDecryptEventSource = null
855+ loading .value = false
856+
857+ if (data .status === ' completed' ) {
858+ clearManualKeys ()
859+ currentStep .value = 1
860+ await prefillKeysForAccount (mediaAccount .value )
861+ } else if (data .status === ' failed' ) {
862+ error .value = data .message || ' 所有文件解密失败'
863+ } else {
864+ error .value = data .message || ' 解密失败,请检查输入信息'
865+ }
866+ } else if (data .type === ' error' ) {
867+ error .value = data .message || ' 解密失败,请检查输入信息'
868+ try {
869+ eventSource .close ()
870+ } catch (e) {}
871+ dbDecryptEventSource = null
872+ loading .value = false
873+ }
702874 } catch (e) {
703- // ignore
875+ console . error ( ' 解析SSE消息失败: ' , e)
704876 }
877+ }
705878
706- // 进入图片密钥填写步骤
707- clearManualKeys ( )
708- currentStep . value = 1
709- await prefillKeysForAccount ( mediaAccount . value )
710- } else if ( result . status === ' failed ' ) {
711- if ( result . failure_count > 0 && result . success_count === 0 ) {
712- error .value = result . message || ' 所有文件解密失败 '
713- } else {
714- error .value = ' 部分文件解密失败,请检查密钥是否正确 '
879+ eventSource . onerror = ( e ) => {
880+ console . error ( ' SSE连接错误: ' , e )
881+ try {
882+ eventSource . close ( )
883+ } catch (err ) {}
884+ dbDecryptEventSource = null
885+ if ( loading .value ) {
886+ error . value = ' SSE连接中断,请重试 '
887+ loading .value = false
715888 }
716- } else {
717- error .value = result .message || ' 解密失败,请检查输入信息'
718889 }
719890 } catch (err) {
720891 error .value = err .message || ' 解密过程中发生错误'
721- } finally {
722892 loading .value = false
723893 }
724894}
0 commit comments