From 35d098a89613c96179391178b7f8143dec393ada Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 15:52:36 +0800
Subject: [PATCH] feat(报表): 汇总大表导出异步化 + 结果缓存 + 进度弹窗

---
 traffic-audit-web/src/views/ReportExport.vue                                                            |  137 +++++++++++++
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java   |  234 +++++++++++++++++++++++
 docs/功能测试报告/功能测试报告_汇总大表异步导出与缓存_2026-09-20.md                                                            |   86 ++++++++
 docs/工作日结/工作日结_2026-09-20.md                                                                            |   39 +++
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java       |   25 ++
 docs/功能测试报告/汇总大表异步导出进度弹窗_2026-09-20.png                                                                 |    0 
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java |   46 ++++
 7 files changed, 559 insertions(+), 8 deletions(-)

diff --git "a/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\344\270\216\347\274\223\345\255\230_2026-09-20.md" "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\344\270\216\347\274\223\345\255\230_2026-09-20.md"
new file mode 100644
index 0000000..6d90647
--- /dev/null
+++ "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\344\270\216\347\274\223\345\255\230_2026-09-20.md"
@@ -0,0 +1,86 @@
+锘�# 鍔熻兘娴嬭瘯鎶ュ憡_姹囨�诲ぇ琛ㄥ紓姝ュ鍑轰笌缂撳瓨_2026-09-20
+
+## 涓�銆佸姛鑳借鏄�
+
+鐢ㄦ埛涓婃姤锛堥棶棰� 3锛夛細**銆屾眹鎬诲ぇ琛ㄧ偣涓嬭浇鍚庤杩囦竴浼氭墠鍙嶅簲涓嬭浇锛岀幇鍦ㄦ暟鎹簱鐨勬暟鎹繕涓嶆槸寰堝锛岄偅鏁版嵁搴撶殑鏁版嵁澶氫簡杩欎釜绋嬪簭浼氬崱姝诲悧锛熴��**
+
+鑳屾櫙锛氭眹鎬诲ぇ琛ㄥ鍑烘槸**鍚屾璇锋眰**鈥斺�旂偣鍑诲悗娴忚鍣ㄨ绛� 10~20 绉掓墠鎷垮埌鏂囦欢锛堟瘝鐗堣鍙�+鍥炲~ 6.5s銆佹暣鏈害 4 涓囨牸鍏紡閲嶇畻 4.1s銆佸啓鐩樹紶杈� 3.4s锛夛紝鏈熼棿椤甸潰鏃犱换浣曞弽棣堬紱
+涓旂敓鎴愯繃绋嬩竴鐩村崰鐫� HTTP 璇锋眰绾跨▼锛屽浜哄悓鏃剁偣浼氳瀹瑰櫒绾跨▼姹犺闀夸换鍔″崰婊°��
+
+鏈鏀归�狅紙鐢ㄦ埛纭銆屽彲浠ャ�嶏級锛�
+
+1. **寮傛浠诲姟鍖�**锛氭柊澧� `ReportExportTaskService`锛堝浐瀹� 1 绾跨▼姹� `report-export-task`锛宒aemon锛夈��
+   鍓嶇鎻愪氦鍚庣珛鍗虫嬁鍒般�屼换鍔″彿銆嶏紝鍐嶆寜 700ms 杞杩涘害锛堟楠� + 鐧惧垎姣� + 宸茬敤鏃堕暱锛夛紝瀹屾垚鍚庡崟鐙笅杞芥枃浠躲��
+2. **缁撴灉缂撳瓨**锛氬悓涓�鎶ヨ〃鏈� 10 鍒嗛挓鍐呯殑鎴愬姛缁撴灉鐩存帴澶嶇敤锛坄fromCache=true`锛夛紝璺宠繃姣嶇増璇诲彇涓庡叕寮忛噸绠楋紱
+   缂撳瓨甯�**鏁版嵁鐗堟湰瀹堝崼**鈥斺�旀暟鎹増鏈彇 `SELECT IFNULL(MAX(id),0) FROM import_batch`锛屾湡闂村彧瑕佹湁鏂板鍏ュ氨鑷姩澶辨晥锛堝彇涓嶅埌鐗堟湰鏃剁鐢ㄧ紦瀛橈級銆�
+3. **鍘婚噸涓庢帓闃�**锛氬悓涓�鎶ヨ〃鏈熷凡鏈変换鍔″湪璺戞椂鐩存帴澶嶇敤璇ヤ换鍔★紝涓嶄細閲嶅鎺掗槦閲嶇畻銆�
+4. **鍓嶇杩涘害寮圭獥**锛氳繘搴︽潯 + 姝ラ鏂囨 + 宸茬敤鏃堕暱 + 缂撳瓨鏉ユ簮鏃堕棿 + 澶辫触鍘熷洜锛涚敓鎴愪腑鍙�屽悗鍙拌繍琛屻�嶏紙鏀跺脊绐楋紝瀹屾垚鍚庝粛鑷姩涓嬭浇锛夛紝瀹屾垚鍚庡彲銆屼繚瀛樻枃浠� / 閲嶆柊鐢熸垚 / 鍏抽棴銆嶃��
+5. **杩涘害鐐�**锛�2% 鏍¢獙姣嶇増 鈫� 6% 璇诲彇姣嶇増 鈫� 12% 杩樺師鏈堝害鍧� 鈫� 22% 鍥炲~褰撴湀鏁版嵁 鈫� 45% 鍒锋柊鎺掑悕椤典笌璺ㄩ〉鍚堟垚椤� 鈫� 70% 閲嶇畻鏁存湰鍏紡 鈫� 95% 鍐欏嚭鏂囦欢 鈫� 100% 瀹屾垚銆�
+
+鍥炵瓟鐢ㄦ埛鐤戦棶锛�**鏁版嵁閲忓澶т富瑕佹槸 SQL 鑱氬悎鍙樻參锛屼笉浼氥�屽崱姝汇��**锛涙敼閫犲悗鐢熸垚璧板悗鍙扮嚎绋嬨�佸悓鎶ヨ〃鏈熺粨鏋滃彲澶嶇敤銆佸悓鎶ヨ〃鏈熷苟鍙戣嚜鍔ㄥ幓閲嶏紝
+鍗充娇澶氫汉鍚屾椂鐐逛篃鍙槸鎺掗槦锛堝脊绐楁樉绀恒�屾帓闃熶腑銆嶏級锛屼笉浼氬崰婊� Web 绾跨▼銆�
+
+## 浜屻�佹祴璇曠幆澧�
+
+- 鍚庣 Spring Boot 8090锛坄Started TrafficAuditApplication` 15:10:53锛夈�佸墠绔� Vue 8080锛坄Compiled successfully`锛�
+- 鏁版嵁搴� MySQL localhost:3308/traffic_audit
+- 鎶ヨ〃鏈燂細2026-08锛堟湀鎶ワ紝姝e父鐢ㄤ緥锛�/ 2025-01锛堟棤鏁版嵁锛岃礋鍚戠敤渚嬶級
+- 鎺ュ彛鑴氭湰锛歚_qa_task.ps1`銆乣_qa_task2.ps1`銆乣_qa_task3.ps1`锛堜繚鐣欐湭鍒狅級
+- 娴忚鍣ㄥ疄娴嬶細Playwright 1.57 + 鏈満 Chrome锛堟棤澶达級锛岀湡瀹炵偣鍑汇�屾姤琛ㄧ敓鎴愩�嶉〉鎸夐挳
+- 璇佹嵁鎴浘锛歚docs/鍔熻兘娴嬭瘯鎶ュ憡/姹囨�诲ぇ琛ㄥ紓姝ュ鍑鸿繘搴﹀脊绐梍2026-09-20.png`
+
+## 涓夈�佺敤渚嬭〃
+
+### 3.1 鎺ュ彛灞�
+
+| 缂栧彿 | 鐢ㄤ緥 | 杈撳叆 | 棰勬湡 | 瀹為檯 | 缁撹 |
+| --- | --- | --- | --- | --- | --- |
+| T-A1 | 鎻愪氦寮傛浠诲姟 | `GET /api/report/exportTask/summaryWorkbook?period=2026-08&mode=month` | 绔嬪嵆杩斿洖 taskId锛屼笉闃诲 | 鎻愪氦鑰楁椂 **0.07s**锛岃繘搴� 6鈫�22鈫�45鈫�70鈫�95鈫�100锛岀粓鎬� DONE锛�**14.4s** | 閫氳繃 |
+| T-A2 | 涓嬭浇浠诲姟鏂囦欢 | `GET /api/report/exportTask/{id}/file` | 杩斿洖 xlsx | 1,325,109 瀛楄妭锛�**1.27s** | 閫氳繃 |
+| T-A3 | 10 鍒嗛挓鍐呭悓鎶ヨ〃鏈熷鐢� | 鍐嶆鎻愪氦鍚屽弬鏁� | fromCache=true銆佸悓 taskId銆佽繎涔庣灛鏃� | fromCache=true锛�**0.01s**锛宼askId 鐩稿悓 | 閫氳繃 |
+| T-A4 | 寮哄埗閲嶇畻 | 鎻愪氦 `refresh=true` | 鏂颁换鍔″彿銆佺湡瀹為噸绠� | 鏂� taskId锛�14.2s 閲嶇畻瀹屾垚 | 閫氳繃 |
+| T-A5 | 杩愯涓苟鍙戝幓閲� | 杩炲彂涓ゆ `refresh=true` | 杩斿洖鍚屼竴 taskId | 鍚屼竴 taskId锛�11.1s | 閫氳繃 |
+| T-A6 | 鏂板瀵煎叆浣跨紦瀛樺け鏁� | 涓存椂鎻掑叆涓�鏉� `import_batch`锛坕d 533鈫�534锛夊悗鎻愪氦 | fromCache=false锛岄噸鏂扮敓鎴� | fromCache=false锛�11.4s 閲嶇畻锛�**娴嬭瘯鍚庡凡 DELETE 娓呭洖 id=533** | 閫氳繃 |
+| T-A7 | 璐熷悜锛氫笉瀛樺湪鐨� taskId | `GET /exportTask/notexist123` / `.../file` | 杩斿洖鏄庣‘閿欒 | 鐘舵�佹帴鍙� `code=500`銆屽鍑轰换鍔′笉瀛樺湪鎴栧凡杩囨湡锛堢敓鎴愮粨鏋滀繚鐣� 30 鍒嗛挓锛夈�嶏紱鏂囦欢鎺ュ彛 HTTP 500 + 鍚屾彁绀� | 閫氳繃 |
+| T-A8 | 璐熷悜锛氭湭瀹屾垚灏变笅杞� | 浠诲姟 RUNNING 鏃惰姹� `/file` | 鎷掔粷骞惰鏄� | 鎶涖�屽鍑轰换鍔″皻鏈畬鎴愶紙褰撳墠鐘舵�侊細RUNNING锛夈�� | 閫氳繃 |
+
+### 3.2 娴忚鍣ㄥ疄娴嬶紙鐪熷疄鐐瑰嚮锛�
+
+| 缂栧彿 | 鐢ㄤ緥 | 杈撳叆 | 棰勬湡 | 瀹為檯 | 缁撹 |
+| --- | --- | --- | --- | --- | --- |
+| UI-1 | 杩涘害寮圭獥 | 鎶ヨ〃鐢熸垚 鈫� 姹囨�诲ぇ琛� 鈫� 2026-08 鈫� 鐐广�屽鍑恒�� | 绔嬪嵆寮圭獥骞堕�愭。鎺ㄨ繘 | 寮圭獥鍗虫椂鍑虹幇锛屽疄娴嬮噰鏍凤細0.7s 璇诲彇姣嶇増宸ヤ綔绨�(6%) 鈫� 1.5s 鍥炲~褰撴湀鏁版嵁(22%) 鈫� 2.2s 鍒锋柊鎺掑悕椤典笌璺ㄩ〉鍚堟垚椤�(45%) 鈫� 7.2s 閲嶇畻鏁存湰鍏紡锛堢害 4 涓囨牸锛�(70%) 鈫� 10.1s 鍐欏嚭鏂囦欢(95%) 鈫� 11.6s 瀹屾垚(100%) | 閫氳繃 |
+| UI-2 | 瀹屾垚鍚庤嚜鍔ㄤ笅杞� | 鍚屼笂 | 鑷姩瑙﹀彂淇濆瓨 | 涓嬭浇 `鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_2026-08.xlsx`锛�**1,325,109 瀛楄妭**锛屼笌鎺ュ彛浜х墿鍚屽搱甯� | 閫氳繃 |
+| UI-3 | 缂撳瓨鍛戒腑鎻愮ず | 鍏抽棴鍚庡啀娆$偣銆屽鍑恒�� | 鎻愮ず鏉ユ簮鏃堕棿 + 銆屼笅杞姐�嶆寜閽� | 銆屼笂娆$敓鎴愮敤鏃� 11.3 绉掋�嶃�屾湰娆$洿鎺ヤ娇鐢� 2026-09-20 15:43:50 鐢熸垚鐨勭粨鏋滐紙10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹級銆嶏紝鎸夐挳鍙樸�屼笅杞姐�嶏紝绉掔骇瀹屾垚 | 閫氳繃 |
+| UI-4 | 銆岄噸鏂扮敓鎴愩�� | 缂撳瓨鎬佺偣銆岄噸鏂扮敓鎴愩�� | 鐪熷疄閲嶇畻 | 杩涘害浠� 0 閲嶆柊鎺ㄨ繘锛堛�屽凡鐢� 2.1 绉掋�嶏級锛屾寜閽尯鍒囧洖銆屼繚瀛樻枃浠�/閲嶆柊鐢熸垚/鍚庡彴杩愯銆嶏紝23.4s 鍐呭畬鎴愬苟鑷姩涓嬭浇 | 閫氳繃 |
+| UI-5 | 銆屽悗鍙拌繍琛屻�� | 鐢熸垚涓偣銆屽悗鍙拌繍琛屻�� | 鏀惰捣寮圭獥銆佷换鍔$户缁�佸畬鎴愪粛鑷姩涓嬭浇 | 寮圭獥鏀惰捣 + 鎻愮ず銆屽凡杞叆鍚庡彴鐢熸垚锛屽畬鎴愬悗浼氳嚜鍔ㄤ笅杞姐�嶏紝闅忓悗鑷姩涓嬭浇 1,325,109 瀛楄妭 | 閫氳繃 |
+| UI-6 | 璐熷悜锛氭棤鏁版嵁鎶ヨ〃鏈� | 閫� 2025-01 鈫� 瀵煎嚭 鈫� 纭銆屼粛瑕佸鍑恒�� | 鏄剧ず澶辫触鍘熷洜銆佸彲鍏抽棴銆佷笉鍙繚瀛� | 寮圭獥鏄剧ず銆岀敓鎴愬け璐ワ綔鏈壘鍒版ā鏉挎枃浠�: 2025骞�1鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嶏紝X 鍙叧闂紝銆屼繚瀛樻枃浠躲�嶇鐢紝銆岄噸鏂扮敓鎴愩�嶅彲鐢� | 閫氳繃 |
+| UI-7 | 浜х墿涓�鑷存�� | 鍐峰惎鍔ㄤ欢 vs 鍒锋柊閲嶇畻浠� | 鏁版嵁涓�鑷� | 涓ゆ浜х墿瑙e寘鍚�**閫愭潯鐩竴鑷�**锛堟棤宸紓鏉$洰锛夛紱涓変唤缂撳瓨涓嬭浇浠� SHA256 瀹屽叏鐩稿悓 | 閫氳繃 |
+
+### 3.3 椤烘墜澶嶉獙锛堢敤鎴烽棶棰� 2锛�
+
+| 缂栧彿 | 鐢ㄤ緥 | 杈撳叆 | 棰勬湡 | 瀹為檯 | 缁撹 |
+| --- | --- | --- | --- | --- | --- |
+| P-1 | 棰勮鍚屾瘮椤规槸鍚﹁ˉ鍏� | 姹囨�诲ぇ琛ㄩ瑙堝脊绐� | 8 椤瑰悓姣旈兘鍦� | 12 琛岋紙璐ц繍/鍩庡競瀹㈣繍/涓彛寰� 脳 褰撴湀路绱 脳 閲徛峰懆杞級锛屽悓姣斿垪榻愬叏 | 閫氳繃 |
+| P-2 | 銆岀疮璁¤揣鐗╁懆杞噺路绱鍚屾瘮 0%銆嶆槸鍚︿负 bug | 鏌ュ簱 `scale_split_transport` | 搴斾负鍙栨暟婧愭湰韬殑鍊� | 搴撳唴 `total_yoy=0.00000346`锛堢害 0.0003%锛夆啋 涓�浣嶅皬鏁板洓鑸嶄簲鍏ュ嵆 **0%**锛岄潪璁$畻閿欒 | 閫氳繃锛堥潪缂洪櫡锛� |
+
+## 鍥涖�佸彂鐜扮殑闂涓庡缃�
+
+1. **`ReportExport.vue` 鍑虹幇閲嶅鐨� `computed` 閿紙鏈娴忚鍣ㄥ疄娴嬪彂鐜帮紝灞炰弗閲嶅洖褰掞級**
+   鏂板鐨勫紓姝ュ鍑鸿绠楀睘鎬э紙taskRunning/taskDone/taskCacheTime锛夊彟璧蜂簡涓�涓� `computed: {`锛�
+   JS 瀵硅薄瀛楅潰閲�**鍚屽悕閿悗鑰呰鐩栧墠鑰�**锛屽鑷村師鏈夌殑 `username / realName / myModules / visibleModules` 璁$畻灞炴�у叏閮ㄤ涪澶憋紝
+   銆屾姤琛ㄧ敓鎴愩�嶉〉 `created()` 鐩存帴鎶� `Cannot read properties of undefined (reading 'filter')`锛�**鏁撮〉鐧藉睆涓嶅彲鐢�**銆�
+   鈫� 宸叉妸 4 涓绠楀睘鎬у苟鍏ュ師 `computed` 鍧楋紙骞跺姞娉ㄩ噴璇存槑涓嶅彲鍙﹁捣鍚屽悕 key锛夛紝缂栬瘧閫氳繃骞舵祻瑙堝櫒澶嶉獙姝e父銆�
+   锛堟帴鍙e眰娴嬭瘯鏃犳硶鍙戠幇姝ょ被闂锛岃繖涔熸槸鏈鍧氭寔鍋氭祻瑙堝櫒鐐瑰嚮瀹炴祴鐨勫師鍥犮�傦級
+2. **鎻愪氦/杞澶辫触鏃跺脊绐椾細绌鸿浆**锛氬悗绔粺涓�杩斿洖 `Result`锛堥敊璇椂 HTTP 浠� 200銆乣data` 涓虹┖锛夛紝
+   鍘熶唬鐮佺洿鎺� `this.taskInfo = res.data || {}` 浼氬緱鍒颁竴涓┖瀵硅薄 鈫� 杩涘害鍋滃湪 0% 涓斾笉缁欐彁绀恒��
+   鈫� 鎻愪氦涓庤疆璇㈤兘鏀逛负鍏堝垽 `res.code !== 200 || !res.data`锛氭彁浜ゅけ璐ョ珛鍗虫樉绀哄け璐ュ師鍥狅紱杞鍒や负銆屼换鍔″凡澶辨晥銆嶅苟鍋滄杞銆�
+3. **鐢熸垚涓脊绐楁棤鍑哄彛**锛氬師銆屽叧闂�嶆寜閽� `:disabled="!taskDone"`锛岀敓鎴愪腑涓変釜鎸夐挳鍏ㄧ伆銆佸彸涓婅 X 涔熼殣钘忥紝鐢ㄦ埛琚攣鍦ㄥ脊绐楅噷绛� 10~20 绉掋��
+   鈫� 鏀逛负鐢熸垚涓樉绀恒�屽悗鍙拌繍琛屻�嶏紙鏀跺脊绐椼�佺户缁敓鎴愩�佸畬鎴愯嚜鍔ㄤ笅杞斤級锛岀粨鏉熷悗鎵嶆樉绀恒�屽叧闂�嶏紱X 鍦ㄦ垚鍔熸垨澶辫触鍚庡潎鍙銆�
+4. **缂撳瓨鎬佹枃妗堟涔�**锛氬懡涓紦瀛樻椂鍏冧俊鎭粛鍐欍�屽凡鐢� 11.3 绉掋�嶏紙鍏跺疄鏄笂娆$敓鎴愯�楁椂锛夈��
+   鈫� 缂撳瓨鎬佹敼涓恒�屼笂娆$敓鎴愮敤鏃躲�嶏紝閬垮厤璇互涓烘湰娆′篃绠椾簡 11.3 绉掋��
+
+## 浜斻�佺粨璁�
+
+- 鎺ュ彛鐢ㄤ緥 8 鏉°�佹祻瑙堝櫒瀹炴祴 7 鏉°�佸楠� 2 鏉★紝**鍏ㄩ儴閫氳繃**锛涙棤閬楃暀鏈獙璇侀」銆�
+- 姹囨�诲ぇ琛ㄥ鍑虹幇鍦ㄦ槸銆岀偣鍑诲嵆鍙嶉 鈫� 杩涘害鍙 鈫� 瀹屾垚鍗充笅杞姐�嶏紝缂撳瓨鍛戒腑鏃剁绾ц繑鍥烇紱鍚屼竴鎶ヨ〃鏈熷苟鍙戣嚜鍔ㄥ幓閲嶏紝鐢熸垚涓嶅崰 Web 绾跨▼銆�
+- 闄勫甫淇浜嗕細瀵艰嚧鎶ヨ〃鐢熸垚椤垫暣椤电櫧灞忕殑閲嶅 `computed` 鍥炲綊锛堣闂鍦ㄦ帴鍙f祴璇曚腑涓嶅彲瑙侊級銆�
\ No newline at end of file
diff --git "a/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\350\277\233\345\272\246\345\274\271\347\252\227_2026-09-20.png" "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\350\277\233\345\272\246\345\274\271\347\252\227_2026-09-20.png"
new file mode 100644
index 0000000..aa7599d
--- /dev/null
+++ "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\346\261\207\346\200\273\345\244\247\350\241\250\345\274\202\346\255\245\345\257\274\345\207\272\350\277\233\345\272\246\345\274\271\347\252\227_2026-09-20.png"
Binary files differ
diff --git "a/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-09-20.md" "b/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-09-20.md"
index 484d9d4..14124c7 100644
--- "a/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-09-20.md"
+++ "b/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-09-20.md"
@@ -68,6 +68,20 @@
   骞舵柊澧炪�屽鍑烘槑缁嗐�嶆寜閽紙CSV锛屽甫 BOM锛屽彲鐩存帴 Excel 鎵撳紑锛夛紝鐢ㄤ簬鐢ㄦ埛鑷鏍稿/淇敼涓嶄竴鑷村唴瀹广��
 - 瑕嗙洊 4 澶勶細鎵归噺瀵煎叆缁撴灉銆佸競宸炲崟琛ㄦ壒閲忓鍏ョ粨鏋溿�佸崟鏂囦欢瀵煎叆澶辫触銆佹姇璧勪笌鎶曡祫绯荤粺涓嶄竴鑷存竻鍗曘��
 
+### 1.7 姹囨�诲ぇ琛ㄥ鍑哄紓姝ュ寲 + 缁撴灉缂撳瓨锛堢敤鎴锋彁闂�岀偣涓嬭浇瑕佺瓑涓�浼氾紝鏁版嵁澶氫簡浼氫笉浼氬崱姝汇�嶏級
+
+- **鐜拌薄**锛氭眹鎬诲ぇ琛ㄧ偣銆屽鍑恒�嶅悗瑕佺瓑 10~20 绉掓墠瑙﹀彂涓嬭浇锛屾湡闂撮〉闈㈡棤浠讳綍鍙嶉锛涘悓姝ヨ姹備竴鐩村崰鐫� Web 绾跨▼銆�
+- **瀹氫綅**锛�14 绉掗噷姣嶇増璇诲彇+鍥炲~ 6.5s銆佹暣鏈害 4 涓囨牸鍏紡閲嶇畻 4.1s銆佸啓鐩樹紶杈� 3.4s鈥斺�旇�楁椂鏉ヨ嚜 Excel 澶勭悊锛�**涓庢暟鎹簱琛屾暟鍏崇郴涓嶅ぇ**锛屾暟鎹浜嗕富瑕佹槸 SQL 鑱氬悎鍙樻參锛屼笉浼氥�屽崱姝汇�嶃��
+- **鏀瑰姩**锛�
+  1. 鏂板 `ReportExportTaskService`锛堝浐瀹� 1 绾跨▼姹� daemon锛夛細鎻愪氦鍗宠繑鍥� taskId锛屽墠绔� 700ms 杞銆屾楠� + 鐧惧垎姣� + 宸茬敤鏃堕暱銆嶏紱缁撴灉淇濈暀 30 鍒嗛挓锛堝唴瀛樻�侊紝杩涚▼閲嶅惎澶辨晥锛夈��
+  2. **缁撴灉缂撳瓨 10 鍒嗛挓 + 鏁版嵁鐗堟湰瀹堝崼**锛氱増鏈彇 `SELECT IFNULL(MAX(id),0) FROM import_batch`锛屾湡闂村彧瑕佹湁鏂板鍏ュ氨鑷姩澶辨晥锛堝彇涓嶅埌鐗堟湰鏃剁鐢ㄧ紦瀛橈級锛涘悓涓�鎶ヨ〃鏈熷苟鍙戞彁浜よ嚜鍔ㄥ幓閲嶃�佸鐢ㄥ悓涓�浠诲姟銆�
+  3. `ReportExportService` 鏂板甯﹁繘搴﹀洖璋冪殑 `exportSummaryWorkbook(period, mode, toMonth, progress)` 閲嶈浇锛堝師涓や釜閲嶈浇濮旀墭瀹冿級锛岃繘搴︾偣 2%/6%/12%/22%/45%/70%/95%銆�
+  4. `ReportExportController` 鏂板 3 涓帴鍙o細`/exportTask/summaryWorkbook`锛堟彁浜わ級銆乣/exportTask/{taskId}`锛堣繘搴︼級銆乣/exportTask/{taskId}/file`锛堜笅杞斤級锛涘師鍚屾鎺ュ彛淇濈暀鍏煎銆�
+  5. 鍓嶇 `ReportExport.vue` 杩涘害寮圭獥锛氳繘搴︽潯 + 姝ラ + 鐢ㄦ椂 + 缂撳瓨鏉ユ簮鏃堕棿 + 澶辫触鍘熷洜锛屼互鍙娿�屼繚瀛樻枃浠� / 閲嶆柊鐢熸垚 / 鍚庡彴杩愯 / 鍏抽棴銆嶃��
+- **瀹炴祴**锛氭彁浜� 0.07s锛涜繘搴� 6鈫�22鈫�45鈫�70鈫�95鈫�100銆�14.4s 瀹屾垚锛涚紦瀛樺懡涓� 0.01s锛沗refresh=true` 鐪熷疄閲嶇畻锛涗复鏃舵彃涓�鏉″鍏ュ悗缂撳瓨鑷姩澶辨晥銆�
+- **鍚屾椂淇帀涓�涓弗閲嶅洖褰�**锛氭湰娆℃柊澧炵殑 `computed` 涓庢枃浠朵腑宸叉湁 `computed` **鍚屽悕閲嶅**锛孞S 瀵硅薄鍚庤�呰鐩栧墠鑰� 鈫� `username/myModules/visibleModules` 鍏ㄩ儴涓㈠け锛�
+  銆屾姤琛ㄧ敓鎴愩�嶉〉 `created()` 鐩存帴鎶涢敊銆�**鏁撮〉鐧藉睆**銆傛帴鍙f祴璇曞彂鐜颁笉浜嗭紝鏄祻瑙堝櫒瀹炴祴鎵嶆毚闇茬殑锛堣瑙佹祴璇曟姤鍛婄鍥涜妭锛夈��
+
 ## 浜屻�佹祴璇曠粨鏋滄憳瑕�
 
 銆婂姛鑳芥祴璇曟姤鍛奯鎶曡祫鏄庣粏琛ㄥ氨缁笌棰勮绫诲瀷琛ラ綈_2026-09-20.md銆嬶細**7 鏉$敤渚嬮�氳繃锛�1 鏉℃湭瀹炴祴**锛堢己姣嶇増鎻愮ず锛屾湭涓存椂绉诲姩鐢ㄦ埛姣嶇増鏂囦欢锛夈��
@@ -109,6 +123,22 @@
 | 鍏跺畠鎶曡祫鎶ヨ〃鍥炲綊 | investBillion/County/FiveYearLogistics 鍚� 136 琛屻�佷袱寮犳槑缁嗚〃 39/97 琛岋紝ready=true锛屽潎鍙鍑� |
 | 璐熷悜 | `types=foo` 鈫� ready=false锛宯ote銆屾湭鐭ユ姤琛ㄧ被鍨嬶細foo銆� |
 
+銆婂姛鑳芥祴璇曟姤鍛奯姹囨�诲ぇ琛ㄥ紓姝ュ鍑轰笌缂撳瓨_2026-09-20.md銆嬶細**17 鏉$敤渚嬮�氳繃锛堟帴鍙� 8 + 娴忚鍣ㄥ疄娴� 7 + 澶嶉獙 2锛�**銆�
+
+| 鏍¢獙椤� | 缁撴灉 |
+| --- | --- |
+| 鎻愪氦寮傛浠诲姟 | 0.07s 杩斿洖 taskId锛涜繘搴� 6鈫�22鈫�45鈫�70鈫�95鈫�100锛�14.4s 瀹屾垚 |
+| 浠诲姟鏂囦欢涓嬭浇 | 1,325,109 瀛楄妭锛�1.27s |
+| 10 鍒嗛挓鍐呯紦瀛樺鐢� | fromCache=true銆佸悓 taskId銆�0.01s |
+| refresh=true 寮哄埗閲嶇畻 | 鏂颁换鍔″彿锛岀湡瀹為噸绠� 14.2s |
+| 杩愯涓苟鍙戝幓閲� | 涓ゆ鎻愪氦杩斿洖鍚屼竴 taskId锛�11.1s锛� |
+| 鏂板瀵煎叆浣跨紦瀛樺け鏁� | import_batch id 533鈫�534 鍚� fromCache=false锛堟祴璇曞悗宸� DELETE 娓呭洖 533锛� |
+| 璐熷悜锛氫笉瀛樺湪 / 鏈畬鎴� taskId | 銆屽鍑轰换鍔′笉瀛樺湪鎴栧凡杩囨湡銆嶃�屽皻鏈畬鎴愶紙褰撳墠鐘舵�侊細RUNNING锛夈�� |
+| 娴忚鍣ㄥ疄娴� | 杩涘害寮圭獥閲囨牱銆佽嚜鍔ㄤ笅杞姐�佺紦瀛樻彁绀恒�侀噸鏂扮敓鎴愩�佸悗鍙拌繍琛屻�佸け璐ユ彁绀� 6 椤瑰叏閫氳繃 |
+| 浜х墿涓�鑷存�� | 缂撳瓨浠朵笌閲嶇畻浠惰В鍖呭悗閫愭潯鐩竴鑷达紱涓変唤缂撳瓨浠� SHA256 鐩稿悓 |
+| 闄勫甫淇 | `computed` 閲嶅閿鑷存姤琛ㄧ敓鎴愰〉鏁撮〉鐧藉睆锛堝凡淇骞舵祻瑙堝櫒澶嶉獙锛� |
+| 澶嶉獙锛堢敤鎴烽棶棰� 2锛� | 棰勮鍚屾瘮 12 琛岄綈鍏紱銆岀疮璁¤揣鐗╁懆杞噺 0%銆嶇粡鏌ュ簱涓哄彇鏁版簮鏈韩 `total_yoy=0.00000346`锛岄潪缂洪櫡 |
+
 ## 涓夈�侀仐鐣欓棶棰樹笌寰呭姙
 
 1. 姝︽眽鐗╂祦椤圭洰娓呭崟鍙e緞锛堝凡纭蹇界暐锛夈��
@@ -117,10 +147,9 @@
 4. ~~C 缁勶紙鍩庡競瀹㈣繍椤电粨鏋勫綊涓�锛岄渶鐢ㄦ埛鍏堝湪 WPS 鏁村垪鍒犻櫎 AJ 鍒楋級~~ **宸插畬鎴�**锛堟敼鐢� Excel COM 鏁村垪鍒犻櫎 + 鍏紡杩樺師锛夛紱
    D 缁勶紙澧為�熸帓鍚嶆帾杈烇級锛廈4锛堝鍑哄墠鍩烘暟浣撴 + 寮圭獥 + 銆婃暟鎹己澶辨竻鍗曘�嬮〉锛夊緟鎺掓湡銆�
 5. 缂烘瘝鐗堟彁绀猴紙T08锛夋湭瀹炴祴锛屽彲鍦ㄦ湁娴嬭瘯鍓湰鏃惰ˉ娴嬨��
-6. 姹囨�诲ぇ琛ㄥ鍑烘槸**鍚屾璇锋眰**锛屽綋鍓嶇害 14 绉掞紙姣忔閮借閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡锛夛細鏁版嵁閲忓啀娑ㄤ富瑕佹槸 SQL 鑱氬悎鍙樻參锛�
-   涓嶄細銆屽崱姝汇�嶏紝浣嗗缓璁悗缁仛鎴愬紓姝ヤ换鍔� + 杩涘害鎻愮ず銆�
-7. 閮ㄧ讲鍖� `deploy/traffic-audit-deploy-20260913/backend/docs/鐢熸垚姹囨�诲ぇ琛�/` 涓� `_deploytest/` 涓嬩粛鏄�**甯� AJ 鍒楃殑鏃ф瘝鐗�**锛�
-   涓嬫鎵撳寘鏃堕渶鍚屾锛堟湭缁忕敤鎴风‘璁や笉鎿呰嚜鏀归儴缃蹭骇鐗╋級銆�
+6. ~~姹囨�诲ぇ琛ㄥ鍑烘槸鍚屾璇锋眰锛屽缓璁仛鎴愬紓姝ヤ换鍔� + 杩涘害鎻愮ず~~ **宸插畬鎴�**锛堣 1.7锛夈�傜紦瀛樻槸鍐呭瓨鎬侊紝杩涚▼閲嶅惎鍗冲け鏁堬紱濡傞渶璺ㄩ噸鍚�/澶氫汉鍏辩敤鐨勩�屽鍑鸿褰曘�嶉〉鍙悗琛ャ��
+7. 閮ㄧ讲鍖� `deploy/traffic-audit-deploy-20260913/backend/docs/鐢熸垚姹囨�诲ぇ琛�/` 涓� `_deploytest/` 涓嬩粛鏄�**甯� AJ 鍒楃殑鏃ф瘝鐗�** 鈥斺��
+   鐢ㄦ埛 2026-09-20 鏄庣‘銆岄儴缃插寘鍏堜笉绠°�嶏紝**鏆備笉澶勭悊**锛堝緟瑕佸彂鍖呮椂鍐嶅悓姝ワ級銆�
 8. 涓彛寰勬槑缁� 4 鏍硷紙鍩庨檯鍩庝埂宸℃父鍑虹/缃戠害杞︼級鍥� 2025骞�8鏈堝熀鏁颁负 0锛屽悓姣旀樉绀� `#DIV/0!`锛堜笉鍙瘮锛夛紝濡傞渶鏀圭┖鐧介渶鐢ㄦ埛纭鍙e緞銆�
 
 ## 鍥涖�佹槑鏃ヨ鍒�
@@ -128,7 +157,7 @@
 1. 绛夊ぉ闂ㄦ簮鏁版嵁 / 鑽嗗窞榛勭煶鍙e緞纭鍚庤ˉ瀵煎苟澶嶉獙銆�
 2. 鎺ㄨ繘 C/D 缁勪笌 B4銆�
 3. 涓庣敤鎴风‘璁ゆ槸鍚︽妸 09-18 澶滈棿璧风殑鏈湴鎻愪氦涓�骞舵帹閫佸埌鍏徃 gitea锛堢敤鎴� 09-19 鏇捐姹傘�屼粖澶╁厛涓嶆帹鍏徃銆嶏級銆�
-4. 璇勪及姹囨�诲ぇ琛ㄥ鍑哄紓姝ュ寲锛堥暱浠诲姟杩涘害鏉� / 缁撴灉缂撳瓨锛夛紝骞剁‘璁ら儴缃插寘姣嶇増鍚屾鏂瑰紡銆�
+4. ~~璇勪及姹囨�诲ぇ琛ㄥ鍑哄紓姝ュ寲~~ **宸插畬鎴�**锛堣 1.7锛氬紓姝ヤ换鍔� + 杩涘害寮圭獥 + 10 鍒嗛挓缁撴灉缂撳瓨 + 鏁版嵁鐗堟湰瀹堝崼锛夛紱閮ㄧ讲鍖呮瘝鐗堟寜鐢ㄦ埛瑕佹眰鏆備笉澶勭悊銆�
 
 9. **8 鏈堢増鍗佸牥姹囨�昏〃鏈竻鐞�**锛氱敤鎴峰彧娓呬簡 7 鏈堢増锛�108032鈫�90112 瀛楄妭锛夛紝8 鏈堢増锛坢time 2026-08-20锛変粛鍚姹� 27 + 榛勭煶 4 娈碉紱
    鑻ラ噸瀵� 8 鏈堢墿娴侊紝宸插垹闄ょ殑 31 涓」鐩細閲嶆柊寤烘。 鈫� 閲嶅鍓嶉渶鍚屾牱娓呯悊锛堟垨鐢辨垜浠e仛锛屽緟鐢ㄦ埛纭锛夈��
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
index b955d53..f4e8e8a 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
@@ -2,12 +2,14 @@
 
 import com.trafficaudit.common.Result;
 import com.trafficaudit.reportexport.service.ReportExportService;
+import com.trafficaudit.reportexport.service.ReportExportTaskService;
 import com.trafficaudit.system.service.OperationLogService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
@@ -24,6 +26,8 @@
 
     @Resource
     private ReportExportService reportService;
+    @Resource
+    private ReportExportTaskService exportTaskService;
     @Resource
     private OperationLogService operationLogService;
 
@@ -91,6 +95,47 @@
                                                         @RequestParam(value = "toMonth", required = false) Integer toMonth) throws Exception {
         logExport("閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx", period);
         return buildResponse(reportService.exportSummaryWorkbook(period, mode, toMonth), String.format("鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_%s.xlsx", period));
+    }
+
+    /**
+     * 姹囨�诲ぇ琛ㄥ紓姝ュ鍑猴細绔嬪嵆杩斿洖浠诲姟鍙凤紝鍓嶇杞杩涘害銆佸畬鎴愬悗鍐嶄笅杞芥枃浠躲��
+     * 鐢熸垚涓�娆$害 10~20 绉掞紝璧板紓姝ュ彲閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴旓紱鍚屾姤琛ㄦ湡 10 鍒嗛挓鍐呯殑缁撴灉鐩存帴澶嶇敤锛堟湡闂存湁瀵煎叆鍒欒嚜鍔ㄥけ鏁堬級锛�
+     * refresh=true 鍙己鍒堕噸鏂扮敓鎴愩��
+     */
+    @GetMapping("/exportTask/summaryWorkbook")
+    public Result<java.util.Map<String, Object>> submitSummaryWorkbookTask(@RequestParam("period") String period,
+                                                                          @RequestParam(value = "mode", defaultValue = "month") String mode,
+                                                                          @RequestParam(value = "toMonth", required = false) Integer toMonth,
+                                                                          @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) {
+        ReportExportTaskService.Task task = exportTaskService.submit(
+                ReportExportTaskService.TYPE_SUMMARY_WORKBOOK, period, mode, toMonth, refresh);
+        if (!task.isFromCache()) {
+            logExport("閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx", period);
+        }
+        return Result.ok(task.toView());
+    }
+
+    /** 鏌ヨ寮傛瀵煎嚭浠诲姟杩涘害锛堢粨鏋滀繚鐣� 30 鍒嗛挓锛� */
+    @GetMapping("/exportTask/{taskId}")
+    public Result<java.util.Map<String, Object>> exportTaskStatus(@PathVariable("taskId") String taskId) {
+        ReportExportTaskService.Task task = exportTaskService.get(taskId);
+        if (task == null) {
+            return Result.error("瀵煎嚭浠诲姟涓嶅瓨鍦ㄦ垨宸茶繃鏈燂紙鐢熸垚缁撴灉淇濈暀 30 鍒嗛挓锛�");
+        }
+        return Result.ok(task.toView());
+    }
+
+    /** 涓嬭浇寮傛瀵煎嚭浠诲姟宸茬敓鎴愮殑鏂囦欢 */
+    @GetMapping("/exportTask/{taskId}/file")
+    public ResponseEntity<byte[]> exportTaskFile(@PathVariable("taskId") String taskId) throws Exception {
+        ReportExportTaskService.Task task = exportTaskService.get(taskId);
+        if (task == null) {
+            throw new RuntimeException("瀵煎嚭浠诲姟涓嶅瓨鍦ㄦ垨宸茶繃鏈燂紙鐢熸垚缁撴灉淇濈暀 30 鍒嗛挓锛�");
+        }
+        if (!task.isDone()) {
+            throw new RuntimeException("瀵煎嚭浠诲姟灏氭湭瀹屾垚锛堝綋鍓嶇姸鎬侊細" + task.getStatus() + "锛夛紝璇风◢鍚庡啀璇�");
+        }
+        return buildResponse(task.getData(), task.getFileName());
     }
 
     /** 鐢熸垚_缃戠害杞﹀杩愰噺鎷嗗垎琛�.xlsx锛堟湀搴︼細璁㈠崟闄勪欢1 + 鍏ㄧ渷 pin + 宸℃父鍑虹鏈堟姤锛� */
@@ -308,4 +353,3 @@
             .body(data);
     }
 }
-
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
index 50c6da6..d4673d6 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3507,7 +3507,7 @@
      * 淇濈暀鍏ㄩ儴椤电/鐗堝紡/鍏紡/2025 骞寸紦瀛樺�硷紝浠呮妸琛ㄥご骞翠唤鍔ㄦ�佸寲涓虹洰鏍囧勾锛涙暟鎹~鍏呮寜 P2 璧烽�愭鎺ュ叆銆�
      */
     public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
-        return exportSummaryWorkbook(period, mode, null);
+        return exportSummaryWorkbook(period, mode, null, null);
     }
 
     /** 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛圥0-P1 楠ㄦ灦锛屾敮鎸� toMonth 瑁佸壀锛夛細
@@ -3515,6 +3515,13 @@
      *  toMonth 闈炵┖涓斿皬浜� M 鏃讹紝鎶� 8 涓彴璐�/鍚堟垚椤佃鍒� toMonth 鏈堬紙toMonth+1..M 鐨勫綋鏈堝��/鍚屾瘮鍒楁竻绌恒�佷繚鐣欒〃澶达紝
      *  绱涓庣疮璁″悓姣斿叕寮忔敼涓烘寜 toMonth 鏈堥噸绠楋級锛岀敤浜庘�滃厛鐢熸垚 1-6 鏈堛��1-7 鏈堟墿鍒椻�濋獙璇併�� */
     public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception {
+        return exportSummaryWorkbook(period, mode, toMonth, null);
+    }
+
+    /** 鍚屼笂锛岄澶栭�氳繃 progress 鍥炶皟鎶ュ憡杩涘害锛堝紓姝ュ鍑轰换鍔$敤锛氭楠ょ櫨鍒嗘瘮 + 姝ラ鏂囨锛� */
+    public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth,
+                                        java.util.function.BiConsumer<Integer, String> progress) throws Exception {
+        reportProgress(progress, 2, "鏍¢獙鎶ヨ〃鏈熶笌姣嶇増");
         int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
         int month = period.contains("-") ? monthOf(period, mode) : 12;
         File template = resolveSummaryMother(period, mode);
@@ -3522,9 +3529,11 @@
             throw new RuntimeException("鏈壘鍒版眹鎬昏〃姣嶇増銆�" + year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙docs/鐢熸垚姹囨�诲ぇ琛級");
         }
         double savedZipRatio = ZipSecureFile.getMinInflateRatio();
+        reportProgress(progress, 6, "璇诲彇姣嶇増宸ヤ綔绨�");
         ZipSecureFile.setMinInflateRatio(0.0001); // 鏂扮増姣嶇増 styles.xml 鐢� openpyxl 楂樺帇缂╋紙0.0099<0.01锛夎Е鍙� POI Zip-bomb 璇姤锛岃鍙栧悗杩樺師
         try (InputStream in = new FileInputStream(template);
              XSSFWorkbook wb = new XSSFWorkbook(in)) {
+            reportProgress(progress, 12, "杩樺師鍚屾湡鏈堝害鍧�");
             File donor = resolveSummaryDonor(year, month);
             if (donor != null) {
                 try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
@@ -3543,11 +3552,13 @@
             int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
             List<String> dbProblems = new ArrayList<>();
             String effPeriod = String.format("%04d-%02d", year, effMonth);
+            reportProgress(progress, 22, "鍥炲~褰撴湀鏁版嵁");
             int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
             int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
             int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
             // 绱鍚屾瘮鍒嗘瘝淇锛氭瘝鐗堟妸銆屽幓骞村悇鏈堛�嶄笌銆屽幓骞�1-M鏈堢疮璁°�嶈緟鍔╁垪鍚屾椂璁″叆锛堥噸澶嶈/婕忓綋鏈堬級
             int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(wb, year, effMonth);
+            reportProgress(progress, 45, "鍒锋柊鎺掑悕椤典笌璺ㄩ〉鍚堟垚椤�");
             int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
             int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // 鍥涘紶璺ㄩ〉鍚堟垚椤甸殢鎶ヨ〃鏈熷埛鏂�
             int widthApplied = applySummaryReferenceLayout(wb);
@@ -3558,16 +3569,28 @@
             applyTwoDecimalFormat(wb); // 鏁板�兼樉绀轰袱浣嶅皬鏁帮紙淇濈暀鍏ㄧ簿搴︼級锛�% / 鏃ユ湡绛夋棦鏈夋牱寮忎笉鏀瑰彉
             int inlineFixed = normalizeStaleInlineStrings(wb); // 娓呮帀銆岄潪 inlineStr 鍗存畫鐣� <is>銆嶇殑闈炴硶鏍硷紝鍚﹀垯 Excel 鎷掔粷鎵撳紑
             if (inlineFixed > 0) log.warn("姹囨�诲伐浣滅翱娓呮帀娈嬬暀 inlineStr 闈炴硶鏍� {} 涓紙姣嶇増鐢� openpyxl 鍐欏嚭鏃剁殑鍏稿瀷闂锛�", inlineFixed);
+            reportProgress(progress, 70, "閲嶇畻鏁存湰鍏紡锛堢害 4 涓囨牸锛�");
             int[] recalc = recalcWorkbookFormulas(wb); // 鍏紡閲嶇畻骞跺啓鍥炵紦瀛樺�硷細鍏ㄧ渷琛�(危甯傚窞)/绱鍒�/鎺掑悕 SUM 鎵嶈兘琚瑙堜笌浜屾澶勭悊璇诲埌
             log.info("姹囨�诲伐浣滅翱鍏紡閲嶇畻瀹屾垚锛氭垚鍔� {} 鏍硷紝澶辫触 {} 鏍硷紙澶辫触鏍间繚鐣欐瘝鐗堟棫缂撳瓨鍊硷紝鎵撳紑鏃舵寜 fullCalcOnLoad 閲嶇畻锛�", recalc[0], recalc[1]);
             if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
             wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
+            reportProgress(progress, 95, "鍐欏嚭鏂囦欢");
             return toBytes(wb);
         } finally {
             ZipSecureFile.setMinInflateRatio(savedZipRatio);
         }
     }
 
+    /** 瀵煎嚭杩涘害鍥炶皟锛堝紓姝ュ鍑轰换鍔$敤锛夛細鍥炶皟鑷韩寮傚父涓嶅奖鍝嶅鍑� */
+    private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) {
+        if (progress == null) return;
+        try {
+            progress.accept(percent, step);
+        } catch (Exception ignore) {
+            // 杩涘害鍥炶皟澶辫触涓嶅奖鍝嶅鍑�
+        }
+    }
+
     /**
      * 姹囨�诲伐浣滅翱鍏紡閲嶇畻锛歅OI 鍐欐枃浠朵笉甯︾紦瀛樺�硷紝瀵艰嚧銆屽叏鐪佽锛埼�17甯傚窞锛夈�嶃�岀疮璁″垪銆嶃�屾帓鍚� SUM銆�
      * 鍦ㄩ瑙�/浜屾澶勭悊閲屽彧鐪嬪埌姣嶇増涓婁竴鏈熺殑鏃у�硷紙2026-09-18 鐢ㄦ埛鎶モ��8鏈堟病鏁版嵁 / 鍏ㄧ渷琛屽仠鍦ㄤ笂涓�鏈熲�濈殑鏍瑰洜涔嬩竴锛夈��
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
new file mode 100644
index 0000000..ac01627
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
@@ -0,0 +1,234 @@
+package com.trafficaudit.reportexport.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.trafficaudit.dataimport.entity.ImportBatch;
+import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 鎶ヨ〃瀵煎嚭寮傛浠诲姟锛氭妸鑰楁椂鍗佸嚑绉掔殑銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬬敓鎴愭尓鍒板悗鍙扮嚎绋嬶紝
+ * 鍓嶇鍙嬁銆屼换鍔″彿 + 杩涘害锛堟楠�/鐧惧垎姣旓級銆嶏紝瀹屾垚鍚庡崟鐙笅杞芥枃浠躲��
+ *
+ * 鐩殑锛�2026-09-20 鐢ㄦ埛涓婃姤銆岀偣涓嬭浇瑕佺瓑涓�浼氾紝鏁版嵁澶氫簡浼氫笉浼氬崱姝汇�嶏級锛�
+ * 1) 鐐瑰嚮鍚庣珛鍒绘湁鍙嶉锛屼笉鍐嶉暱鏃堕棿鏃犲搷搴旓紱
+ * 2) 鐢熸垚杩囩▼涓嶅崰鐢� HTTP 璇锋眰绾跨▼锛岄伩鍏嶅浜哄悓鏃剁偣鎶婂鍣ㄦ嫋姝伙紙宸ヤ綔绾跨▼鍥哄畾 1 涓紝澶氫綑璇锋眰鏄剧ず銆屾帓闃熶腑銆嶏級锛�
+ * 3) 鍚屼竴鎶ヨ〃鏈� 10 鍒嗛挓鍐呯殑缁撴灉鍙鐢紝閬垮厤姣忔閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡銆�
+ *
+ * 缂撳瓨姝g‘鎬у畧鍗細鍙湁褰撱�岃窛涓婃鐢熸垚涓嶈秴杩� TTL銆嶄笖銆屾湡闂存病鏈変换浣曟柊鐨勬暟鎹鍏ワ紙import_batch 鏈�澶� id 鏈彉锛夈��
+ * 鏃舵墠澶嶇敤锛涘鍏ュ嵆澶辨晥锛岄伩鍏嶆妸杩囨湡鎶ヨ〃缁欑敤鎴枫�傚墠绔粛鎻愪緵銆岄噸鏂扮敓鎴愩�嶅己鍒跺埛鏂般��
+ */
+@Slf4j
+@Service
+public class ReportExportTaskService {
+
+    /** 姹囨�诲伐浣滅翱瀵煎嚭浠诲姟绫诲瀷 */
+    public static final String TYPE_SUMMARY_WORKBOOK = "summaryWorkbook";
+
+    /** 鐢熸垚缁撴灉淇濈暀鏃堕暱锛氭湡闂村彲鍙嶅涓嬭浇锛岃繃鍚庤嚜鍔ㄦ竻鐞� */
+    private static final long RESULT_TTL_MS = 30 * 60 * 1000L;
+    /** 鍚屼竴鎶ヨ〃鏈熺粨鏋滃鐢ㄦ椂闀匡紙涓旀湡闂存棤鏂板鍏ユ椂鎵嶅鐢級 */
+    private static final long CACHE_TTL_MS = 10 * 60 * 1000L;
+
+    @Resource
+    private ReportExportService reportService;
+    @Resource
+    private ImportBatchMapper importBatchMapper;
+
+    private final Map<String, Task> tasks = new ConcurrentHashMap<>();
+    /** 鍚屾姤琛ㄦ湡鏈�杩戜竴娆℃垚鍔熺粨鏋滐細key -> task */
+    private final Map<String, Task> cache = new ConcurrentHashMap<>();
+    private final ExecutorService pool = Executors.newFixedThreadPool(1, r -> {
+        Thread t = new Thread(r, "report-export-task");
+        t.setDaemon(true);
+        return t;
+    });
+
+    /** 涓�娆″鍑轰换鍔★紙鍐呭瓨鎬侊紝杩涚▼閲嶅惎鍗冲け鏁堬級 */
+    public static class Task {
+        private final String id = UUID.randomUUID().toString().replace("-", "");
+        private final String type;
+        private final String period;
+        private final String mode;
+        private final Integer toMonth;
+        private final long createdAt = System.currentTimeMillis();
+        /** 鐢熸垚鏃剁殑鏁版嵁鐗堟湰锛坕mport_batch 鏈�澶� id锛夛紝鐢ㄤ簬鍒ゆ柇缂撳瓨鏄惁杩囨湡 */
+        private final long importVersion;
+        private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED
+        private volatile int percent = 0;
+        private volatile String step = "鎺掗槦涓�";
+        private volatile String error;
+        private volatile byte[] data;
+        private volatile String fileName;
+        private volatile boolean fromCache;
+        private volatile long finishedAt;
+
+        Task(String type, String period, String mode, Integer toMonth, long importVersion) {
+            this.type = type;
+            this.period = period;
+            this.mode = mode;
+            this.toMonth = toMonth;
+            this.importVersion = importVersion;
+        }
+
+        void progress(int pct, String stepText) {
+            if (pct > this.percent) this.percent = Math.min(pct, 99);
+            if (stepText != null) this.step = stepText;
+        }
+
+        public String getId() { return id; }
+
+        public String getStatus() { return status; }
+
+        public boolean isDone() { return "DONE".equals(status) && data != null; }
+
+        public boolean isFromCache() { return fromCache; }
+
+        public byte[] getData() { return data; }
+
+        public String getFileName() { return fileName; }
+
+        public String getError() { return error; }
+
+        public Map<String, Object> toView() {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("taskId", id);
+            m.put("type", type);
+            m.put("period", period);
+            m.put("mode", mode);
+            m.put("toMonth", toMonth);
+            m.put("status", status);
+            m.put("percent", "DONE".equals(status) ? 100 : percent);
+            m.put("step", step);
+            m.put("fromCache", fromCache);
+            m.put("fileName", fileName);
+            m.put("error", error);
+            long end = finishedAt > 0 ? finishedAt : System.currentTimeMillis();
+            m.put("elapsedMs", end - createdAt);
+            m.put("finishedAt", finishedAt > 0 ? new Date(finishedAt) : null);
+            return m;
+        }
+    }
+
+    /**
+     * 鎻愪氦瀵煎嚭浠诲姟銆俧orceRefresh=true 鏃跺拷鐣ョ紦瀛橀噸鏂扮敓鎴愶紱
+     * 鍚﹀垯鍦ㄣ�屽悓涓�鎶ヨ〃鏈熴��10 鍒嗛挓鍐呫�佹湡闂存棤鏂板鍏ャ�嶇殑鍓嶆彁涓嬬洿鎺ュ鐢ㄤ笂娆$粨鏋溿��
+     * 鍚屼竴鎶ヨ〃鏈熷凡鏈変换鍔″湪璺戞椂鐩存帴杩斿洖璇ヤ换鍔★紝閬垮厤閲嶅鎺掗槦銆�
+     */
+    public Task submit(String type, String period, String mode, Integer toMonth, boolean forceRefresh) {
+        cleanupExpired();
+        String key = cacheKey(type, period, mode, toMonth);
+        if (!forceRefresh) {
+            Task hit = cache.get(key);
+            if (cacheUsable(hit)) {
+                hit.fromCache = true;
+                log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛�", type, period, new Date(hit.finishedAt));
+                return hit;
+            }
+        }
+        Task running = findRunning(key);
+        if (running != null) {
+            log.info("鎶ヨ〃瀵煎嚭浠诲姟宸插湪鎵ц锛屽鐢ㄨ繘琛屼腑鐨勪换鍔★細{} {}锛坽}锛�", type, period, running.id);
+            return running;
+        }
+        Task task = new Task(type, period, mode, toMonth, importVersion());
+        tasks.put(task.id, task);
+        pool.submit(() -> run(task));
+        return task;
+    }
+
+    public Task get(String taskId) {
+        if (taskId == null) return null;
+        cleanupExpired();
+        return tasks.get(taskId);
+    }
+
+    private void run(Task t) {
+        t.status = "RUNNING";
+        t.step = "寮�濮嬬敓鎴�";
+        long start = System.currentTimeMillis();
+        try {
+            byte[] data = reportService.exportSummaryWorkbook(t.period, t.mode, t.toMonth, t::progress);
+            t.data = data;
+            t.fileName = "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_" + t.period + ".xlsx";
+            t.step = "瀹屾垚";
+            t.status = "DONE";
+            t.finishedAt = System.currentTimeMillis();
+            cache.put(cacheKey(t.type, t.period, t.mode, t.toMonth), t);
+            log.info("鎶ヨ〃瀵煎嚭浠诲姟瀹屾垚锛歿} {} 鐢ㄦ椂 {} ms锛寋} 瀛楄妭", t.type, t.period, t.finishedAt - start, data.length);
+        } catch (Throwable e) {
+            t.status = "FAILED";
+            t.error = e.getMessage() == null ? e.toString() : e.getMessage();
+            t.step = "鐢熸垚澶辫触";
+            t.finishedAt = System.currentTimeMillis();
+            log.error("鎶ヨ〃瀵煎嚭浠诲姟澶辫触锛歿} {}锛堢敤鏃� {} ms锛�", t.type, t.period, t.finishedAt - start, e);
+        }
+    }
+
+    private Task findRunning(String key) {
+        for (Task t : tasks.values()) {
+            if (!key.equals(cacheKey(t.type, t.period, t.mode, t.toMonth))) continue;
+            if ("PENDING".equals(t.status) || "RUNNING".equals(t.status)) return t;
+        }
+        return null;
+    }
+
+    private boolean cacheUsable(Task t) {
+        if (t == null || !"DONE".equals(t.status) || t.data == null) return false;
+        if (System.currentTimeMillis() - t.finishedAt > CACHE_TTL_MS) return false;
+        long now = importVersion();
+        if (now < 0 || t.importVersion < 0) return false; // 鍙栦笉鍒版暟鎹増鏈椂涓嶅啋椋庨櫓澶嶇敤
+        return now == t.importVersion;
+    }
+
+    /** 鏁版嵁鐗堟湰锛漣mport_batch 鏈�澶� id锛氫换浣曚竴娆″鍏ラ兘浼氳瀹冨彉鍖栵紝鍙栦笉鍒拌繑鍥� -1锛堟鏃剁鐢ㄧ紦瀛橈級 */
+    private long importVersion() {
+        try {
+            List<Object> v = importBatchMapper.selectObjs(new QueryWrapper<ImportBatch>().select("IFNULL(MAX(id),0)"));
+            if (v == null || v.isEmpty() || v.get(0) == null) return 0L;
+            return ((Number) v.get(0)).longValue();
+        } catch (Exception e) {
+            log.warn("璇诲彇瀵煎叆鏁版嵁鐗堟湰澶辫触锛屾湰娆$鐢ㄥ鍑虹紦瀛橈細{}", e.getMessage());
+            return -1L;
+        }
+    }
+
+    private String cacheKey(String type, String period, String mode, Integer toMonth) {
+        return type + "|" + period + "|" + mode + "|" + (toMonth == null ? "" : toMonth);
+    }
+
+    private void cleanupExpired() {
+        long now = System.currentTimeMillis();
+        for (Iterator<Map.Entry<String, Task>> it = tasks.entrySet().iterator(); it.hasNext(); ) {
+            Task t = it.next().getValue();
+            if (t.finishedAt > 0 && now - t.finishedAt > RESULT_TTL_MS) it.remove();
+        }
+        for (Iterator<Map.Entry<String, Task>> it = cache.entrySet().iterator(); it.hasNext(); ) {
+            Task t = it.next().getValue();
+            if (t.finishedAt <= 0 || now - t.finishedAt > CACHE_TTL_MS) it.remove();
+        }
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        pool.shutdownNow();
+        try {
+            pool.awaitTermination(3, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+}
diff --git a/traffic-audit-web/src/views/ReportExport.vue b/traffic-audit-web/src/views/ReportExport.vue
index f78d81a..22e12cf 100644
--- a/traffic-audit-web/src/views/ReportExport.vue
+++ b/traffic-audit-web/src/views/ReportExport.vue
@@ -160,6 +160,25 @@
             <el-button size="small" type="primary" @click="saveNote">淇濆瓨</el-button>
           </span>
         </el-dialog>
+
+        <!-- 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愯繘搴︼紙鐢熸垚涓�娆$害 10~20 绉掞紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴旓級 -->
+        <el-dialog title="姝e湪鐢熸垚姹囨�诲ぇ琛�" :visible.sync="taskVisible" width="460px"
+                   :close-on-click-modal="false" :show-close="taskFinished">
+          <div class="task-body">
+            <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)"
+                         :stroke-width="14"></el-progress>
+            <p class="task-step">{{ taskInfo.step || '鎺掗槦涓�' }}</p>
+            <p class="task-meta" v-if="taskInfo.period">鎶ヨ〃鏈� {{ taskInfo.period }}銆�{{ taskInfo.fromCache ? '涓婃鐢熸垚鐢ㄦ椂' : '宸茬敤' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} 绉�</p>
+            <p class="task-meta" v-if="taskInfo.fromCache">鏈鐩存帴浣跨敤 {{ taskCacheTime }} 鐢熸垚鐨勭粨鏋滐紙10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹級銆傚闇�鎸夋渶鏂版暟鎹噸绠楋紝璇风偣銆岄噸鏂扮敓鎴愩�嶃��</p>
+            <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p>
+          </div>
+          <span slot="footer">
+            <el-button size="small" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ taskInfo.fromCache ? '涓嬭浇' : '淇濆瓨鏂囦欢' }}</el-button>
+            <el-button size="small" :disabled="taskRunning" @click="restartTask">閲嶆柊鐢熸垚</el-button>
+            <el-button size="small" v-if="taskRunning" @click="minimizeTask">鍚庡彴杩愯</el-button>
+            <el-button size="small" v-else @click="closeTask">鍏抽棴</el-button>
+          </span>
+        </el-dialog>
       </el-main>
     </el-container>
   </el-container>
@@ -189,6 +208,11 @@
       noteDialogVisible: false,
       noteEditingKey: '',
       noteDraft: '',
+      // 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愪换鍔�
+      taskVisible: false,
+      taskInfo: {},
+      taskTimer: null,
+      taskKey: 'summaryWorkbook',
       modules: [
         {
           key: 'summary', title: '姹囨�诲ぇ琛�', items: [
@@ -277,6 +301,26 @@
         options: months.map(mo => ({ label: y + '骞�' + mo + '鏈�', value: y + '-' + String(mo).padStart(2, '0') }))
       }))
     },
+    // ===== 浠ヤ笅涓烘眹鎬诲ぇ琛ㄥ紓姝ョ敓鎴愬脊绐楃殑娲剧敓鐘舵�侊紙蹇呴』骞跺叆鍚屼竴涓� computed锛屽彟璧峰悓鍚� key 浼氳鐩栨帀涓婇潰鐨勮绠楀睘鎬э級=====
+    taskRunning() {
+      const s = this.taskInfo && this.taskInfo.status
+      return s === 'PENDING' || s === 'RUNNING'
+    },
+    taskDone() {
+      return this.taskInfo && this.taskInfo.status === 'DONE'
+    },
+    // 鐢熸垚缁撴潫锛堟垚鍔熸垨澶辫触锛夊悗鎵嶅厑璁告敹璧峰脊绐�
+    taskFinished() {
+      const s = this.taskInfo && this.taskInfo.status
+      return s === 'DONE' || s === 'FAILED'
+    },
+    taskCacheTime() {
+      const t = this.taskInfo && this.taskInfo.finishedAt
+      if (!t) return '涓婃'
+      const d = new Date(t)
+      const p = (n) => String(n).padStart(2, '0')
+      return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
+    }
   },
   watch: {
     activeKey() {
@@ -289,7 +333,11 @@
     this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
     this.ensureCycle()
     this.refreshAll()
-  },  methods: {
+  },
+  beforeDestroy() {
+    this.stopTaskPolling()
+  },
+  methods: {
     moduleTitle(key) {
       const m = this.modules.find(x => x.key === key)
       return m ? m.title : key
@@ -468,6 +516,86 @@
       if (continuous) return '1~' + cov[cov.length - 1] + '鏈堝潎鏈夋暟鎹�'
       return cov.map(x => x + '鏈�').join('銆�') + ' 鏈夋暟鎹�'
     },
+    // ==================== 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愶紙杩涘害寮圭獥锛� ====================
+    // 鍚庣 ReportExportTaskService锛氭彁浜や换鍔� -> 杞杩涘害 -> 瀹屾垚鍚庝笅杞芥枃浠躲��
+    // 鍚屾姤琛ㄦ湡 10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹椂锛屽悗绔洿鎺ヨ繑鍥炰笂娆$粨鏋滐紙寮圭獥鍐呬細璇存槑鏉ユ簮鏃堕棿锛夈��
+    startSummaryTask(refresh) {
+      if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鍛ㄦ湡鍜屾湀浠�'); return }
+      this.stopTaskPolling()
+      this.taskInfo = { status: 'PENDING', percent: 0, step: '鎻愪氦浠诲姟', period: this.period, elapsedMs: 0 }
+      this.taskVisible = true
+      this.loading = 'gen-' + this.curModule.key
+      const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' })
+      api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 })
+        .then(res => {
+          // 鍚庣缁熶竴杩斿洖 Result锛歝ode 闈� 200 鏃� data 涓虹┖锛屽繀椤绘彁绀鸿�屼笉鏄┖杞�
+          if (!res || res.code !== 200 || !res.data || !res.data.taskId) {
+            this.taskInfo = { status: 'FAILED', percent: 0, step: '鎻愪氦澶辫触', period: this.period, elapsedMs: 0,
+                              error: (res && res.message) || '鍚庣鏈繑鍥炰换鍔″彿' }
+            this.loading = ''
+            return
+          }
+          this.taskInfo = res.data
+          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return }
+          if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
+          this.pollTask()
+        })
+        .catch(err => {
+          this.stopTaskPolling()
+          this.loading = ''
+          this.taskVisible = false
+          this.failWith(err, '鐢熸垚')
+        })
+    },
+    pollTask() {
+      this.stopTaskPolling()
+      const id = this.taskInfo && this.taskInfo.taskId
+      if (!id) return
+      this.taskTimer = setInterval(() => {
+        api.get('/report/exportTask/' + id, { timeout: 30000 })
+          .then(res => {
+            if (!res || res.code !== 200 || !res.data) {
+              // 浠诲姟琚竻鐞�/杩囨湡锛氬仠姝㈣疆璇㈠苟缁欏嚭鍘熷洜锛屼笉璁╁脊绐椾竴鐩寸┖杞�
+              this.stopTaskPolling()
+              this.loading = ''
+              this.taskInfo = Object.assign({}, this.taskInfo, {
+                status: 'FAILED', step: '浠诲姟宸插け鏁�',
+                error: (res && res.message) || '瀵煎嚭浠诲姟宸插け鏁堬紝璇烽噸鏂扮敓鎴�'
+              })
+              return
+            }
+            this.taskInfo = res.data
+            if (this.taskInfo.status === 'DONE') {
+              this.stopTaskPolling()
+              this.loading = ''
+              this.downloadTaskFile()
+            } else if (this.taskInfo.status === 'FAILED') {
+              this.stopTaskPolling()
+              this.loading = ''
+              if (!this.taskVisible) this.$message.error('姹囨�诲ぇ琛ㄧ敓鎴愬け璐ワ細' + (this.taskInfo.error || '鏈煡鍘熷洜'))
+            }
+          })
+          .catch(() => { /* 鍗曟杞澶辫触蹇界暐锛屼笅涓�杞户缁� */ })
+      }, 700)
+    },
+    stopTaskPolling() {
+      if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
+    },
+    downloadTaskFile() {
+      const id = this.taskInfo && this.taskInfo.taskId
+      if (!id) return
+      const name = this.taskInfo.fileName || ('鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_' + this.period + '.xlsx')
+      api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 })
+        .then(res => { this.downloadBlob(res, name); this.$message.success('鎶ヨ〃瀵煎嚭鎴愬姛') })
+        .catch(err => this.failWith(err))
+    },
+    restartTask() { this.startSummaryTask(true) },
+    closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' },
+    // 鐢熸垚涓敹璧峰脊绐楋細浠诲姟缁х画鍦ㄥ悗鍙版墽琛岋紝瀹屾垚鍚庝粛浼氳嚜鍔ㄤ笅杞斤紙涓庡師鏉ュ悓姝ュ鍑虹殑銆岀偣瀹屽嵆涓嬭浇銆嶄竴鑷达級
+    minimizeTask() {
+      this.taskVisible = false
+      this.$message.info('宸茶浆鍏ュ悗鍙扮敓鎴愶紝瀹屾垚鍚庝細鑷姩涓嬭浇')
+    },
     exportChecked(itemList) {
       const m = this.curModule
       if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鍛ㄦ湡鍜屾湀浠�'); return }
@@ -477,6 +605,8 @@
       const params = this.buildParams()
       const doExport = () => {
         if (items.length === 1) {
+          // 姹囨�诲ぇ琛ㄧ敓鎴愪竴娆$害 10~20 绉掞紝璧板紓姝ヤ换鍔★紙杩涘害寮圭獥锛夛紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴�
+          if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return }
           api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
             .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('鎶ヨ〃瀵煎嚭鎴愬姛'); this.loading = '' })
             .catch(err => this.failWith(err))
@@ -621,6 +751,11 @@
 .wb-yoy { text-align:right; color:#606266; }
 .wb-yoy.yoy-empty { color:#C0C4CC; }
 .wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
+/* 寮傛鐢熸垚杩涘害寮圭獥 */
+.task-body { padding: 4px 2px; }
+.task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; }
+.task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; }
+.task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; }
 </style>
 
 <style>

--
Gitblit v1.9.1