advanced_viewer.html 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>施工方案审查结果 - 高级分析工具</title>
  7. <style>
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13. body {
  14. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  15. background: #0f172a;
  16. min-height: 100vh;
  17. color: #e2e8f0;
  18. }
  19. .container {
  20. max-width: 1600px;
  21. margin: 0 auto;
  22. padding: 20px;
  23. }
  24. .header {
  25. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  26. border: 1px solid #334155;
  27. border-radius: 16px;
  28. padding: 30px;
  29. margin-bottom: 20px;
  30. }
  31. .header h1 {
  32. font-size: 28px;
  33. color: #f8fafc;
  34. margin-bottom: 8px;
  35. display: flex;
  36. align-items: center;
  37. gap: 12px;
  38. }
  39. .header .subtitle {
  40. color: #94a3b8;
  41. font-size: 14px;
  42. }
  43. .controls {
  44. display: flex;
  45. gap: 15px;
  46. margin-top: 20px;
  47. flex-wrap: wrap;
  48. align-items: center;
  49. }
  50. .btn {
  51. padding: 12px 24px;
  52. border: none;
  53. border-radius: 8px;
  54. cursor: pointer;
  55. font-size: 14px;
  56. font-weight: 500;
  57. transition: all 0.3s ease;
  58. display: inline-flex;
  59. align-items: center;
  60. gap: 8px;
  61. }
  62. .btn-primary {
  63. background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  64. color: white;
  65. }
  66. .btn-primary:hover {
  67. transform: translateY(-2px);
  68. box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
  69. }
  70. .btn-secondary {
  71. background: #334155;
  72. color: #e2e8f0;
  73. border: 1px solid #475569;
  74. }
  75. .btn-secondary:hover {
  76. background: #475569;
  77. }
  78. .btn-danger {
  79. background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  80. color: white;
  81. }
  82. .btn-success {
  83. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  84. color: white;
  85. }
  86. input[type="file"] {
  87. display: none;
  88. }
  89. /* Dashboard */
  90. .dashboard {
  91. display: grid;
  92. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  93. gap: 20px;
  94. margin-bottom: 20px;
  95. }
  96. .stat-card {
  97. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  98. border: 1px solid #334155;
  99. border-radius: 16px;
  100. padding: 24px;
  101. position: relative;
  102. overflow: hidden;
  103. }
  104. .stat-card::before {
  105. content: '';
  106. position: absolute;
  107. top: 0;
  108. left: 0;
  109. right: 0;
  110. height: 4px;
  111. }
  112. .stat-card.total::before { background: #3b82f6; }
  113. .stat-card.high::before { background: #ef4444; }
  114. .stat-card.medium::before { background: #f59e0b; }
  115. .stat-card.low::before { background: #10b981; }
  116. .stat-icon {
  117. font-size: 32px;
  118. margin-bottom: 12px;
  119. }
  120. .stat-value {
  121. font-size: 36px;
  122. font-weight: bold;
  123. color: #f8fafc;
  124. margin-bottom: 4px;
  125. }
  126. .stat-label {
  127. color: #94a3b8;
  128. font-size: 14px;
  129. }
  130. .stat-change {
  131. font-size: 12px;
  132. margin-top: 8px;
  133. padding: 4px 8px;
  134. border-radius: 4px;
  135. display: inline-block;
  136. }
  137. .stat-change.positive {
  138. background: rgba(16, 185, 129, 0.2);
  139. color: #10b981;
  140. }
  141. .stat-change.negative {
  142. background: rgba(239, 68, 68, 0.2);
  143. color: #ef4444;
  144. }
  145. /* Charts Section */
  146. .charts-section {
  147. display: grid;
  148. grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  149. gap: 20px;
  150. margin-bottom: 20px;
  151. }
  152. .chart-card {
  153. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  154. border: 1px solid #334155;
  155. border-radius: 16px;
  156. padding: 24px;
  157. }
  158. .chart-card h3 {
  159. font-size: 16px;
  160. color: #f8fafc;
  161. margin-bottom: 20px;
  162. display: flex;
  163. align-items: center;
  164. gap: 8px;
  165. }
  166. /* Progress bars */
  167. .progress-list {
  168. display: flex;
  169. flex-direction: column;
  170. gap: 16px;
  171. }
  172. .progress-item {
  173. display: flex;
  174. flex-direction: column;
  175. gap: 8px;
  176. }
  177. .progress-header {
  178. display: flex;
  179. justify-content: space-between;
  180. align-items: center;
  181. }
  182. .progress-label {
  183. font-size: 14px;
  184. color: #e2e8f0;
  185. }
  186. .progress-value {
  187. font-size: 14px;
  188. color: #94a3b8;
  189. }
  190. .progress-bar {
  191. height: 8px;
  192. background: #334155;
  193. border-radius: 4px;
  194. overflow: hidden;
  195. }
  196. .progress-fill {
  197. height: 100%;
  198. border-radius: 4px;
  199. transition: width 0.5s ease;
  200. }
  201. .progress-fill.high { background: linear-gradient(90deg, #ef4444, #f87171); }
  202. .progress-fill.medium { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
  203. .progress-fill.low { background: linear-gradient(90deg, #10b981, #34d399); }
  204. .progress-fill.total { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
  205. /* Filters */
  206. .filters {
  207. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  208. border: 1px solid #334155;
  209. border-radius: 16px;
  210. padding: 20px;
  211. margin-bottom: 20px;
  212. display: flex;
  213. gap: 20px;
  214. flex-wrap: wrap;
  215. align-items: center;
  216. }
  217. .filter-group {
  218. display: flex;
  219. flex-direction: column;
  220. gap: 8px;
  221. }
  222. .filter-group label {
  223. font-size: 12px;
  224. color: #94a3b8;
  225. text-transform: uppercase;
  226. letter-spacing: 0.5px;
  227. font-weight: 600;
  228. }
  229. .filter-group select,
  230. .filter-group input {
  231. padding: 10px 16px;
  232. background: #0f172a;
  233. border: 1px solid #334155;
  234. border-radius: 8px;
  235. font-size: 14px;
  236. color: #e2e8f0;
  237. min-width: 150px;
  238. }
  239. .filter-group select:focus,
  240. .filter-group input:focus {
  241. outline: none;
  242. border-color: #3b82f6;
  243. }
  244. /* View Toggle */
  245. .view-toggle {
  246. display: flex;
  247. gap: 10px;
  248. margin-bottom: 20px;
  249. }
  250. .view-btn {
  251. padding: 10px 20px;
  252. background: #1e293b;
  253. border: 1px solid #334155;
  254. border-radius: 8px;
  255. color: #94a3b8;
  256. cursor: pointer;
  257. transition: all 0.3s;
  258. }
  259. .view-btn.active {
  260. background: #3b82f6;
  261. color: white;
  262. border-color: #3b82f6;
  263. }
  264. /* Cards View */
  265. .cards-container {
  266. display: grid;
  267. grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
  268. gap: 20px;
  269. }
  270. .review-card {
  271. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  272. border: 1px solid #334155;
  273. border-radius: 16px;
  274. overflow: hidden;
  275. transition: all 0.3s ease;
  276. position: relative;
  277. }
  278. .review-card::before {
  279. content: '';
  280. position: absolute;
  281. top: 0;
  282. left: 0;
  283. right: 0;
  284. height: 4px;
  285. }
  286. .review-card.high::before { background: #ef4444; }
  287. .review-card.medium::before { background: #f59e0b; }
  288. .review-card.low::before { background: #10b981; }
  289. .review-card.none::before { background: #64748b; }
  290. .review-card:hover {
  291. transform: translateY(-4px);
  292. border-color: #475569;
  293. box-shadow: 0 20px 40px rgba(0,0,0,0.3);
  294. }
  295. .card-header {
  296. padding: 20px;
  297. border-bottom: 1px solid #334155;
  298. }
  299. .card-header-top {
  300. display: flex;
  301. justify-content: space-between;
  302. align-items: flex-start;
  303. margin-bottom: 12px;
  304. }
  305. .chapter-tag {
  306. padding: 6px 12px;
  307. background: rgba(59, 130, 246, 0.2);
  308. color: #60a5fa;
  309. border-radius: 6px;
  310. font-size: 12px;
  311. font-weight: 600;
  312. }
  313. .risk-tag {
  314. padding: 6px 12px;
  315. border-radius: 6px;
  316. font-size: 12px;
  317. font-weight: 600;
  318. }
  319. .risk-tag.high {
  320. background: rgba(239, 68, 68, 0.2);
  321. color: #f87171;
  322. }
  323. .risk-tag.medium {
  324. background: rgba(245, 158, 11, 0.2);
  325. color: #fbbf24;
  326. }
  327. .risk-tag.low {
  328. background: rgba(16, 185, 129, 0.2);
  329. color: #34d399;
  330. }
  331. .risk-tag.none {
  332. background: rgba(100, 116, 139, 0.2);
  333. color: #94a3b8;
  334. }
  335. .check-item-title {
  336. font-size: 16px;
  337. font-weight: 600;
  338. color: #f8fafc;
  339. }
  340. .check-item-code {
  341. font-size: 12px;
  342. color: #64748b;
  343. font-family: monospace;
  344. margin-top: 4px;
  345. }
  346. .card-body {
  347. padding: 20px;
  348. }
  349. .info-section {
  350. margin-bottom: 16px;
  351. }
  352. .info-section:last-child {
  353. margin-bottom: 0;
  354. }
  355. .info-label {
  356. font-size: 11px;
  357. color: #64748b;
  358. text-transform: uppercase;
  359. letter-spacing: 0.5px;
  360. margin-bottom: 6px;
  361. display: flex;
  362. align-items: center;
  363. gap: 6px;
  364. }
  365. .info-content {
  366. font-size: 14px;
  367. color: #e2e8f0;
  368. line-height: 1.6;
  369. padding: 12px;
  370. background: #0f172a;
  371. border-radius: 8px;
  372. border-left: 3px solid #334155;
  373. }
  374. .info-content.location { border-left-color: #3b82f6; }
  375. .info-content.suggestion { border-left-color: #8b5cf6; }
  376. .info-content.reason { border-left-color: #ec4899; }
  377. .info-content.issue { border-left-color: #ef4444; }
  378. .card-footer {
  379. padding: 16px 20px;
  380. background: rgba(15, 23, 42, 0.5);
  381. border-top: 1px solid #334155;
  382. display: flex;
  383. justify-content: space-between;
  384. align-items: center;
  385. }
  386. .status-badge {
  387. display: flex;
  388. align-items: center;
  389. gap: 8px;
  390. padding: 6px 12px;
  391. border-radius: 6px;
  392. font-size: 13px;
  393. font-weight: 500;
  394. }
  395. .status-badge.has-issue {
  396. background: rgba(239, 68, 68, 0.2);
  397. color: #f87171;
  398. }
  399. .status-badge.no-issue {
  400. background: rgba(16, 185, 129, 0.2);
  401. color: #34d399;
  402. }
  403. .status-dot {
  404. width: 8px;
  405. height: 8px;
  406. border-radius: 50%;
  407. }
  408. .status-dot.has-issue {
  409. background: #ef4444;
  410. animation: pulse 2s infinite;
  411. }
  412. .status-dot.no-issue {
  413. background: #10b981;
  414. }
  415. @keyframes pulse {
  416. 0%, 100% { opacity: 1; }
  417. 50% { opacity: 0.5; }
  418. }
  419. /* Table View */
  420. .table-container {
  421. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  422. border: 1px solid #334155;
  423. border-radius: 16px;
  424. overflow: hidden;
  425. display: none;
  426. }
  427. .table-container.show {
  428. display: block;
  429. }
  430. table {
  431. width: 100%;
  432. border-collapse: collapse;
  433. }
  434. th, td {
  435. padding: 16px;
  436. text-align: left;
  437. border-bottom: 1px solid #334155;
  438. }
  439. th {
  440. background: #0f172a;
  441. color: #94a3b8;
  442. font-size: 12px;
  443. text-transform: uppercase;
  444. letter-spacing: 0.5px;
  445. font-weight: 600;
  446. }
  447. td {
  448. color: #e2e8f0;
  449. font-size: 14px;
  450. }
  451. tr:hover td {
  452. background: rgba(59, 130, 246, 0.1);
  453. }
  454. .table-risk {
  455. padding: 4px 10px;
  456. border-radius: 4px;
  457. font-size: 12px;
  458. font-weight: 600;
  459. }
  460. .table-risk.high {
  461. background: rgba(239, 68, 68, 0.2);
  462. color: #f87171;
  463. }
  464. .table-risk.medium {
  465. background: rgba(245, 158, 11, 0.2);
  466. color: #fbbf24;
  467. }
  468. .table-risk.low {
  469. background: rgba(16, 185, 129, 0.2);
  470. color: #34d399;
  471. }
  472. .table-risk.none {
  473. background: rgba(100, 116, 139, 0.2);
  474. color: #94a3b8;
  475. }
  476. /* Empty State */
  477. .empty-state {
  478. text-align: center;
  479. padding: 100px 20px;
  480. }
  481. .empty-state-icon {
  482. font-size: 80px;
  483. margin-bottom: 24px;
  484. opacity: 0.5;
  485. }
  486. .empty-state h2 {
  487. font-size: 24px;
  488. color: #f8fafc;
  489. margin-bottom: 8px;
  490. }
  491. .empty-state p {
  492. color: #64748b;
  493. }
  494. /* Toast */
  495. .toast {
  496. position: fixed;
  497. bottom: 30px;
  498. right: 30px;
  499. background: #1e293b;
  500. border: 1px solid #334155;
  501. padding: 16px 24px;
  502. border-radius: 12px;
  503. display: flex;
  504. align-items: center;
  505. gap: 12px;
  506. transform: translateX(400px);
  507. transition: transform 0.3s ease;
  508. z-index: 1000;
  509. box-shadow: 0 10px 40px rgba(0,0,0,0.3);
  510. }
  511. .toast.show {
  512. transform: translateX(0);
  513. }
  514. .toast.success { border-left: 4px solid #10b981; }
  515. .toast.error { border-left: 4px solid #ef4444; }
  516. /* Export Panel */
  517. .export-panel {
  518. background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
  519. border: 1px solid #334155;
  520. border-radius: 16px;
  521. padding: 20px;
  522. margin-bottom: 20px;
  523. display: none;
  524. }
  525. .export-panel.show {
  526. display: block;
  527. }
  528. .export-options {
  529. display: flex;
  530. gap: 15px;
  531. margin-top: 15px;
  532. flex-wrap: wrap;
  533. }
  534. /* File List */
  535. .file-list {
  536. display: flex;
  537. flex-wrap: wrap;
  538. gap: 10px;
  539. margin-top: 15px;
  540. }
  541. .file-tag {
  542. padding: 8px 16px;
  543. background: rgba(59, 130, 246, 0.2);
  544. border: 1px solid #3b82f6;
  545. border-radius: 8px;
  546. font-size: 13px;
  547. color: #60a5fa;
  548. display: flex;
  549. align-items: center;
  550. gap: 8px;
  551. }
  552. .file-tag .remove {
  553. cursor: pointer;
  554. opacity: 0.7;
  555. }
  556. .file-tag .remove:hover {
  557. opacity: 1;
  558. }
  559. @media (max-width: 768px) {
  560. .cards-container {
  561. grid-template-columns: 1fr;
  562. }
  563. .charts-section {
  564. grid-template-columns: 1fr;
  565. }
  566. .filters {
  567. flex-direction: column;
  568. align-items: stretch;
  569. }
  570. th, td {
  571. padding: 12px;
  572. font-size: 13px;
  573. }
  574. }
  575. </style>
  576. </head>
  577. <body>
  578. <div class="container">
  579. <div class="header">
  580. <h1>🔍 施工方案审查结果 - 高级分析工具</h1>
  581. <p class="subtitle">支持多文件对比、风险分析和数据导出</p>
  582. <div class="controls">
  583. <label class="btn btn-primary">
  584. 📁 选择JSON文件
  585. <input type="file" id="fileInput" accept=".json" multiple>
  586. </label>
  587. <button class="btn btn-secondary" onclick="loadFromDefault()">
  588. 📂 加载示例数据
  589. </button>
  590. <button class="btn btn-success" onclick="toggleExport()">
  591. 📊 导出报告
  592. </button>
  593. <button class="btn btn-danger" onclick="clearAll()">
  594. 🗑️ 清空数据
  595. </button>
  596. </div>
  597. <div class="file-list" id="fileList"></div>
  598. </div>
  599. <div class="export-panel" id="exportPanel">
  600. <h3>导出选项</h3>
  601. <div class="export-options">
  602. <button class="btn btn-primary" onclick="exportToExcel()">📄 导出Excel</button>
  603. <button class="btn btn-primary" onclick="exportToPDF()">📑 导出PDF</button>
  604. <button class="btn btn-secondary" onclick="exportToJSON()">📋 导出JSON</button>
  605. </div>
  606. </div>
  607. <div class="dashboard" id="dashboard" style="display: none;">
  608. <div class="stat-card total">
  609. <div class="stat-icon">📊</div>
  610. <div class="stat-value" id="totalCount">0</div>
  611. <div class="stat-label">总检查项</div>
  612. </div>
  613. <div class="stat-card high">
  614. <div class="stat-icon">🔴</div>
  615. <div class="stat-value" id="highRiskCount">0</div>
  616. <div class="stat-label">高风险</div>
  617. </div>
  618. <div class="stat-card medium">
  619. <div class="stat-icon">🟡</div>
  620. <div class="stat-value" id="mediumRiskCount">0</div>
  621. <div class="stat-label">中风险</div>
  622. </div>
  623. <div class="stat-card low">
  624. <div class="stat-icon">🟢</div>
  625. <div class="stat-value" id="lowRiskCount">0</div>
  626. <div class="stat-label">低风险</div>
  627. </div>
  628. </div>
  629. <div class="charts-section" id="chartsSection" style="display: none;">
  630. <div class="chart-card">
  631. <h3>📊 风险分布</h3>
  632. <div class="progress-list" id="riskDistribution"></div>
  633. </div>
  634. <div class="chart-card">
  635. <h3>📈 章节统计</h3>
  636. <div class="progress-list" id="chapterDistribution"></div>
  637. </div>
  638. </div>
  639. <div class="filters" id="filters" style="display: none;">
  640. <div class="filter-group">
  641. <label>风险等级</label>
  642. <select id="riskFilter" onchange="applyFilters()">
  643. <option value="all">全部</option>
  644. <option value="high">高风险</option>
  645. <option value="medium">中风险</option>
  646. <option value="low">低风险</option>
  647. <option value="none">无风险</option>
  648. </select>
  649. </div>
  650. <div class="filter-group">
  651. <label>章节</label>
  652. <select id="chapterFilter" onchange="applyFilters()">
  653. <option value="all">全部</option>
  654. </select>
  655. </div>
  656. <div class="filter-group">
  657. <label>问题状态</label>
  658. <select id="issueFilter" onchange="applyFilters()">
  659. <option value="all">全部</option>
  660. <option value="has-issue">存在问题</option>
  661. <option value="no-issue">无问题</option>
  662. </select>
  663. </div>
  664. <div class="filter-group">
  665. <label>搜索</label>
  666. <input type="text" id="searchInput" placeholder="关键词..." oninput="applyFilters()">
  667. </div>
  668. </div>
  669. <div class="view-toggle" id="viewToggle" style="display: none;">
  670. <button class="view-btn active" onclick="switchView('cards')">🎴 卡片视图</button>
  671. <button class="view-btn" onclick="switchView('table')">📋 表格视图</button>
  672. </div>
  673. <div id="content">
  674. <div class="empty-state">
  675. <div class="empty-state-icon">📂</div>
  676. <h2>请加载审查结果文件</h2>
  677. <p>支持多文件同时加载进行对比分析</p>
  678. </div>
  679. </div>
  680. </div>
  681. <div class="toast" id="toast">
  682. <span class="toast-message">操作成功</span>
  683. </div>
  684. <script>
  685. let allReviewItems = [];
  686. let filteredItems = [];
  687. let loadedFiles = [];
  688. let currentView = 'cards';
  689. document.getElementById('fileInput').addEventListener('change', handleFileSelect);
  690. function handleFileSelect(event) {
  691. const files = Array.from(event.target.files);
  692. files.forEach(file => {
  693. const reader = new FileReader();
  694. reader.onload = function(e) {
  695. try {
  696. let content = e.target.result;
  697. content = content.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f]/g, '');
  698. const data = JSON.parse(content);
  699. processData(data, file.name);
  700. addFileToList(file.name);
  701. showToast(`已加载: ${file.name}`, 'success');
  702. } catch (err) {
  703. showToast(`${file.name} 解析失败: ${err.message}`, 'error');
  704. }
  705. };
  706. reader.readAsText(file);
  707. });
  708. }
  709. function addFileToList(filename) {
  710. if (!loadedFiles.includes(filename)) {
  711. loadedFiles.push(filename);
  712. renderFileList();
  713. }
  714. }
  715. function renderFileList() {
  716. const container = document.getElementById('fileList');
  717. container.innerHTML = loadedFiles.map(file => `
  718. <div class="file-tag">
  719. ${file}
  720. <span class="remove" onclick="removeFile('${file}')">✕</span>
  721. </div>
  722. `).join('');
  723. }
  724. function removeFile(filename) {
  725. loadedFiles = loadedFiles.filter(f => f !== filename);
  726. renderFileList();
  727. }
  728. function loadFromDefault() {
  729. const sampleFiles = [
  730. 'f926d2ad4428bfcbe12be8702e2c32ce-1773041592.json'
  731. ];
  732. sampleFiles.forEach(filename => {
  733. fetch(`../../temp/construction_review/final_result/${filename}`)
  734. .then(response => {
  735. if (!response.ok) throw new Error('文件加载失败');
  736. return response.text();
  737. })
  738. .then(text => {
  739. text = text.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f]/g, '');
  740. const data = JSON.parse(text);
  741. processData(data, filename);
  742. addFileToList(filename);
  743. showToast(`已加载: ${filename}`, 'success');
  744. })
  745. .catch(err => {
  746. showToast('加载失败: ' + err.message, 'error');
  747. });
  748. });
  749. }
  750. function processData(data, filename) {
  751. const aiReviewResult = data.ai_review_result || {};
  752. const reviewResults = aiReviewResult.review_results || [];
  753. reviewResults.forEach(result => {
  754. Object.values(result).forEach(unit => {
  755. if (unit.review_lists) {
  756. unit.review_lists.forEach(item => {
  757. allReviewItems.push({
  758. ...item,
  759. sourceFile: filename
  760. });
  761. });
  762. }
  763. });
  764. });
  765. updateUI();
  766. }
  767. function updateUI() {
  768. populateChapterFilter();
  769. updateStats();
  770. updateCharts();
  771. applyFilters();
  772. document.getElementById('dashboard').style.display = 'grid';
  773. document.getElementById('chartsSection').style.display = 'grid';
  774. document.getElementById('filters').style.display = 'flex';
  775. document.getElementById('viewToggle').style.display = 'flex';
  776. }
  777. function populateChapterFilter() {
  778. const chapters = [...new Set(allReviewItems.map(item => item.chapter_code))];
  779. const select = document.getElementById('chapterFilter');
  780. select.innerHTML = '<option value="all">全部章节</option>';
  781. chapters.forEach(chapter => {
  782. if (chapter) {
  783. const option = document.createElement('option');
  784. option.value = chapter;
  785. option.textContent = chapter;
  786. select.appendChild(option);
  787. }
  788. });
  789. }
  790. function updateStats() {
  791. let high = 0, medium = 0, low = 0, none = 0;
  792. allReviewItems.forEach(item => {
  793. const riskLevel = getRiskLevel(item);
  794. switch(riskLevel) {
  795. case 'high': high++; break;
  796. case 'medium': medium++; break;
  797. case 'low': low++; break;
  798. default: none++; break;
  799. }
  800. });
  801. document.getElementById('totalCount').textContent = allReviewItems.length;
  802. document.getElementById('highRiskCount').textContent = high;
  803. document.getElementById('mediumRiskCount').textContent = medium;
  804. document.getElementById('lowRiskCount').textContent = low;
  805. }
  806. function updateCharts() {
  807. // Risk Distribution
  808. const riskCounts = { high: 0, medium: 0, low: 0, none: 0 };
  809. allReviewItems.forEach(item => {
  810. riskCounts[getRiskLevel(item)]++;
  811. });
  812. const total = allReviewItems.length || 1;
  813. const riskLabels = {
  814. high: '高风险',
  815. medium: '中风险',
  816. low: '低风险',
  817. none: '无风险'
  818. };
  819. document.getElementById('riskDistribution').innerHTML =
  820. Object.entries(riskCounts).map(([key, count]) => `
  821. <div class="progress-item">
  822. <div class="progress-header">
  823. <span class="progress-label">${riskLabels[key]}</span>
  824. <span class="progress-value">${count} (${Math.round(count/total*100)}%)</span>
  825. </div>
  826. <div class="progress-bar">
  827. <div class="progress-fill ${key}" style="width: ${count/total*100}%"></div>
  828. </div>
  829. </div>
  830. `).join('');
  831. // Chapter Distribution
  832. const chapterCounts = {};
  833. allReviewItems.forEach(item => {
  834. const chapter = item.chapter_code || '未知';
  835. chapterCounts[chapter] = (chapterCounts[chapter] || 0) + 1;
  836. });
  837. const sortedChapters = Object.entries(chapterCounts)
  838. .sort((a, b) => b[1] - a[1])
  839. .slice(0, 5);
  840. document.getElementById('chapterDistribution').innerHTML =
  841. sortedChapters.map(([chapter, count]) => `
  842. <div class="progress-item">
  843. <div class="progress-header">
  844. <span class="progress-label">${chapter}</span>
  845. <span class="progress-value">${count}</span>
  846. </div>
  847. <div class="progress-bar">
  848. <div class="progress-fill total" style="width: ${count/total*100}%"></div>
  849. </div>
  850. </div>
  851. `).join('');
  852. }
  853. function getRiskLevel(item) {
  854. const riskInfo = item.risk_info || {};
  855. const riskLevel = (riskInfo.risk_level || '').toLowerCase();
  856. if (riskLevel.includes('高') || riskLevel.includes('high')) return 'high';
  857. if (riskLevel.includes('中') || riskLevel.includes('medium')) return 'medium';
  858. if (riskLevel.includes('低') || riskLevel.includes('low')) return 'low';
  859. return 'none';
  860. }
  861. function applyFilters() {
  862. const riskFilter = document.getElementById('riskFilter').value;
  863. const chapterFilter = document.getElementById('chapterFilter').value;
  864. const issueFilter = document.getElementById('issueFilter').value;
  865. const searchInput = document.getElementById('searchInput').value.toLowerCase();
  866. filteredItems = allReviewItems.filter(item => {
  867. if (riskFilter !== 'all' && getRiskLevel(item) !== riskFilter) {
  868. return false;
  869. }
  870. if (chapterFilter !== 'all' && item.chapter_code !== chapterFilter) {
  871. return false;
  872. }
  873. if (issueFilter !== 'all') {
  874. const hasIssue = item.exist_issue === true;
  875. if (issueFilter === 'has-issue' && !hasIssue) return false;
  876. if (issueFilter === 'no-issue' && hasIssue) return false;
  877. }
  878. if (searchInput) {
  879. const searchText = [
  880. item.check_item,
  881. item.check_item_code,
  882. item.chapter_code,
  883. item.check_result,
  884. item.location,
  885. item.suggestion,
  886. item.reason
  887. ].join(' ').toLowerCase();
  888. if (!searchText.includes(searchInput)) return false;
  889. }
  890. return true;
  891. });
  892. renderContent();
  893. }
  894. function renderContent() {
  895. if (currentView === 'cards') {
  896. renderCards();
  897. } else {
  898. renderTable();
  899. }
  900. }
  901. function switchView(view) {
  902. currentView = view;
  903. document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
  904. event.target.classList.add('active');
  905. renderContent();
  906. }
  907. function renderCards() {
  908. const content = document.getElementById('content');
  909. if (filteredItems.length === 0) {
  910. content.innerHTML = `
  911. <div class="empty-state">
  912. <div class="empty-state-icon">🔍</div>
  913. <h2>没有找到匹配的结果</h2>
  914. <p>请尝试调整筛选条件</p>
  915. </div>
  916. `;
  917. return;
  918. }
  919. const cardsHtml = filteredItems.map(item => {
  920. const riskLevel = getRiskLevel(item);
  921. const hasIssue = item.exist_issue === true;
  922. return `
  923. <div class="review-card ${riskLevel}">
  924. <div class="card-header">
  925. <div class="card-header-top">
  926. <span class="chapter-tag">${escapeHtml(item.chapter_code || '未知章节')}</span>
  927. <span class="risk-tag ${riskLevel}">${getRiskLabel(riskLevel)}</span>
  928. </div>
  929. <div class="check-item-title">${escapeHtml(item.check_item || '未命名检查项')}</div>
  930. <div class="check-item-code">${escapeHtml(item.check_item_code || '')}</div>
  931. </div>
  932. <div class="card-body">
  933. <div class="info-section">
  934. <div class="info-label">📍 问题位置</div>
  935. <div class="info-content location">${escapeHtml(item.location || item.check_result || '未指定')}</div>
  936. </div>
  937. ${item.suggestion ? `
  938. <div class="info-section">
  939. <div class="info-label">💡 修改建议</div>
  940. <div class="info-content suggestion">${escapeHtml(item.suggestion)}</div>
  941. </div>
  942. ` : ''}
  943. ${item.reason ? `
  944. <div class="info-section">
  945. <div class="info-label">📝 审查依据</div>
  946. <div class="info-content reason">${escapeHtml(item.reason)}</div>
  947. </div>
  948. ` : ''}
  949. </div>
  950. <div class="card-footer">
  951. <div class="status-badge ${hasIssue ? 'has-issue' : 'no-issue'}">
  952. <span class="status-dot ${hasIssue ? 'has-issue' : 'no-issue'}"></span>
  953. <span>${hasIssue ? '存在问题' : '无问题'}</span>
  954. </div>
  955. <span style="color: #64748b; font-size: 12px;">${item.sourceFile || ''}</span>
  956. </div>
  957. </div>
  958. `;
  959. }).join('');
  960. content.innerHTML = `<div class="cards-container">${cardsHtml}</div>`;
  961. }
  962. function renderTable() {
  963. const content = document.getElementById('content');
  964. if (filteredItems.length === 0) {
  965. content.innerHTML = `
  966. <div class="empty-state">
  967. <div class="empty-state-icon">🔍</div>
  968. <h2>没有找到匹配的结果</h2>
  969. <p>请尝试调整筛选条件</p>
  970. </div>
  971. `;
  972. return;
  973. }
  974. const rows = filteredItems.map(item => {
  975. const riskLevel = getRiskLevel(item);
  976. const hasIssue = item.exist_issue === true;
  977. return `
  978. <tr>
  979. <td><span class="chapter-tag">${escapeHtml(item.chapter_code || '-')}</span></td>
  980. <td>${escapeHtml(item.check_item || '-')}</td>
  981. <td><span class="table-risk ${riskLevel}">${getRiskLabel(riskLevel)}</span></td>
  982. <td>${escapeHtml((item.location || item.check_result || '-').substring(0, 50))}...</td>
  983. <td>
  984. <span class="status-badge ${hasIssue ? 'has-issue' : 'no-issue'}">
  985. <span class="status-dot ${hasIssue ? 'has-issue' : 'no-issue'}"></span>
  986. ${hasIssue ? '存在问题' : '无问题'}
  987. </span>
  988. </td>
  989. <td style="color: #64748b; font-size: 12px;">${item.sourceFile || '-'}</td>
  990. </tr>
  991. `;
  992. }).join('');
  993. content.innerHTML = `
  994. <div class="table-container show">
  995. <table>
  996. <thead>
  997. <tr>
  998. <th>章节</th>
  999. <th>检查项</th>
  1000. <th>风险等级</th>
  1001. <th>问题描述</th>
  1002. <th>状态</th>
  1003. <th>来源文件</th>
  1004. </tr>
  1005. </thead>
  1006. <tbody>${rows}</tbody>
  1007. </table>
  1008. </div>
  1009. `;
  1010. }
  1011. function getRiskLabel(riskLevel) {
  1012. const labels = {
  1013. high: '高风险',
  1014. medium: '中风险',
  1015. low: '低风险',
  1016. none: '无风险'
  1017. };
  1018. return labels[riskLevel] || '未知';
  1019. }
  1020. function escapeHtml(text) {
  1021. if (!text) return '';
  1022. const div = document.createElement('div');
  1023. div.textContent = text;
  1024. return div.innerHTML;
  1025. }
  1026. function toggleExport() {
  1027. const panel = document.getElementById('exportPanel');
  1028. panel.classList.toggle('show');
  1029. }
  1030. function exportToExcel() {
  1031. if (filteredItems.length === 0) {
  1032. showToast('没有可导出的数据', 'error');
  1033. return;
  1034. }
  1035. let csv = '\uFEFF章节,检查项,检查项代码,风险等级,问题位置,问题描述,修改建议,审查依据,是否存在问题,来源文件\n';
  1036. filteredItems.forEach(item => {
  1037. const row = [
  1038. item.chapter_code || '',
  1039. item.check_item || '',
  1040. item.check_item_code || '',
  1041. getRiskLabel(getRiskLevel(item)),
  1042. (item.location || '').replace(/,/g, ','),
  1043. (item.check_result || '').replace(/,/g, ','),
  1044. (item.suggestion || '').replace(/,/g, ','),
  1045. (item.reason || '').replace(/,/g, ','),
  1046. item.exist_issue ? '是' : '否',
  1047. item.sourceFile || ''
  1048. ];
  1049. csv += row.join(',') + '\n';
  1050. });
  1051. const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  1052. const link = document.createElement('a');
  1053. link.href = URL.createObjectURL(blob);
  1054. link.download = `审查结果_${new Date().toISOString().slice(0,10)}.csv`;
  1055. link.click();
  1056. showToast('Excel导出成功', 'success');
  1057. }
  1058. function exportToPDF() {
  1059. showToast('PDF导出功能开发中...', 'success');
  1060. }
  1061. function exportToJSON() {
  1062. if (filteredItems.length === 0) {
  1063. showToast('没有可导出的数据', 'error');
  1064. return;
  1065. }
  1066. const data = {
  1067. exportTime: new Date().toISOString(),
  1068. totalCount: filteredItems.length,
  1069. items: filteredItems
  1070. };
  1071. const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  1072. const link = document.createElement('a');
  1073. link.href = URL.createObjectURL(blob);
  1074. link.download = `审查结果_${new Date().toISOString().slice(0,10)}.json`;
  1075. link.click();
  1076. showToast('JSON导出成功', 'success');
  1077. }
  1078. function clearAll() {
  1079. allReviewItems = [];
  1080. filteredItems = [];
  1081. loadedFiles = [];
  1082. renderFileList();
  1083. document.getElementById('dashboard').style.display = 'none';
  1084. document.getElementById('chartsSection').style.display = 'none';
  1085. document.getElementById('filters').style.display = 'none';
  1086. document.getElementById('viewToggle').style.display = 'none';
  1087. document.getElementById('exportPanel').classList.remove('show');
  1088. document.getElementById('content').innerHTML = `
  1089. <div class="empty-state">
  1090. <div class="empty-state-icon">📂</div>
  1091. <h2>数据已清空</h2>
  1092. <p>请加载新的审查结果文件</p>
  1093. </div>
  1094. `;
  1095. showToast('数据已清空', 'success');
  1096. }
  1097. function showToast(message, type) {
  1098. const toast = document.getElementById('toast');
  1099. toast.className = `toast ${type} show`;
  1100. toast.querySelector('.toast-message').textContent = message;
  1101. setTimeout(() => {
  1102. toast.classList.remove('show');
  1103. }, 3000);
  1104. }
  1105. </script>
  1106. </body>
  1107. </html>