| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>施工方案审查结果可视化工具</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- min-height: 100vh;
- padding: 20px;
- }
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
- .header {
- background: white;
- border-radius: 16px;
- padding: 30px;
- margin-bottom: 20px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
- }
- .header h1 {
- font-size: 28px;
- color: #1a1a2e;
- margin-bottom: 10px;
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .header h1::before {
- content: '📋';
- font-size: 32px;
- }
- .header .subtitle {
- color: #666;
- font-size: 14px;
- }
- .file-selector {
- margin-top: 20px;
- display: flex;
- gap: 15px;
- flex-wrap: wrap;
- align-items: center;
- }
- .file-selector input[type="file"] {
- display: none;
- }
- .btn {
- padding: 12px 24px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 500;
- transition: all 0.3s ease;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- }
- .btn-primary {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- }
- .btn-primary:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
- }
- .btn-secondary {
- background: #f0f0f0;
- color: #333;
- }
- .btn-secondary:hover {
- background: #e0e0e0;
- }
- .file-path {
- color: #666;
- font-size: 13px;
- padding: 8px 16px;
- background: #f8f9fa;
- border-radius: 6px;
- font-family: monospace;
- }
- .stats-bar {
- display: flex;
- gap: 20px;
- margin-top: 20px;
- flex-wrap: wrap;
- }
- .stat-card {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 20px 30px;
- border-radius: 12px;
- min-width: 150px;
- text-align: center;
- }
- .stat-card.warning {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- }
- .stat-card.success {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- }
- .stat-card.info {
- background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
- }
- .stat-value {
- font-size: 32px;
- font-weight: bold;
- margin-bottom: 5px;
- }
- .stat-label {
- font-size: 13px;
- opacity: 0.9;
- }
- .filters {
- background: white;
- border-radius: 16px;
- padding: 20px;
- margin-bottom: 20px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
- align-items: center;
- }
- .filter-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .filter-group label {
- font-size: 13px;
- color: #666;
- font-weight: 500;
- }
- .filter-group select,
- .filter-group input {
- padding: 10px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 14px;
- min-width: 150px;
- transition: border-color 0.3s;
- }
- .filter-group select:focus,
- .filter-group input:focus {
- outline: none;
- border-color: #667eea;
- }
- .cards-container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
- gap: 20px;
- }
- .review-card {
- background: white;
- border-radius: 16px;
- overflow: hidden;
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
- transition: all 0.3s ease;
- border-left: 5px solid #ddd;
- }
- .review-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 20px 60px rgba(0,0,0,0.15);
- }
- .review-card.high-risk {
- border-left-color: #e74c3c;
- }
- .review-card.medium-risk {
- border-left-color: #f39c12;
- }
- .review-card.low-risk {
- border-left-color: #27ae60;
- }
- .review-card.no-risk {
- border-left-color: #95a5a6;
- }
- .card-header {
- padding: 20px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- border-bottom: 1px solid #e0e0e0;
- }
- .card-header-top {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 10px;
- flex-wrap: wrap;
- gap: 10px;
- }
- .chapter-code {
- display: inline-block;
- padding: 6px 14px;
- background: #667eea;
- color: white;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
- .risk-badge {
- padding: 6px 14px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- }
- .risk-badge.high {
- background: #ffe5e5;
- color: #c0392b;
- }
- .risk-badge.medium {
- background: #fff3e0;
- color: #e67e22;
- }
- .risk-badge.low {
- background: #e8f5e9;
- color: #27ae60;
- }
- .risk-badge.none {
- background: #eceff1;
- color: #546e7a;
- }
- .check-item {
- font-size: 16px;
- font-weight: 600;
- color: #1a1a2e;
- margin-top: 10px;
- }
- .check-item-code {
- font-size: 12px;
- color: #888;
- font-family: monospace;
- margin-top: 4px;
- }
- .card-body {
- padding: 20px;
- }
- .info-row {
- margin-bottom: 16px;
- }
- .info-row:last-child {
- margin-bottom: 0;
- }
- .info-label {
- font-size: 12px;
- color: #888;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 6px;
- font-weight: 600;
- }
- .info-content {
- font-size: 14px;
- color: #333;
- line-height: 1.6;
- background: #f8f9fa;
- padding: 12px;
- border-radius: 8px;
- word-break: break-word;
- }
- .info-content.issue {
- background: #fff3e0;
- border-left: 4px solid #f39c12;
- }
- .info-content.suggestion {
- background: #e3f2fd;
- border-left: 4px solid #2196f3;
- }
- .info-content.reason {
- background: #f3e5f5;
- border-left: 4px solid #9c27b0;
- }
- .info-content.location {
- background: #e8f5e9;
- border-left: 4px solid #4caf50;
- }
- .card-footer {
- padding: 15px 20px;
- background: #f8f9fa;
- border-top: 1px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 10px;
- }
- .issue-status {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- font-weight: 600;
- }
- .issue-status.has-issue {
- color: #e74c3c;
- }
- .issue-status.no-issue {
- color: #27ae60;
- }
- .status-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- }
- .status-dot.has-issue {
- background: #e74c3c;
- animation: pulse 2s infinite;
- }
- .status-dot.no-issue {
- background: #27ae60;
- }
- @keyframes pulse {
- 0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); }
- 70% { box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); }
- 100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
- }
- .reference-source {
- font-size: 12px;
- color: #888;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .empty-state {
- text-align: center;
- padding: 80px 20px;
- color: white;
- }
- .empty-state-icon {
- font-size: 80px;
- margin-bottom: 20px;
- opacity: 0.8;
- }
- .empty-state h2 {
- font-size: 24px;
- margin-bottom: 10px;
- }
- .empty-state p {
- opacity: 0.8;
- }
- .loading {
- text-align: center;
- padding: 60px;
- color: white;
- }
- .loading-spinner {
- width: 50px;
- height: 50px;
- border: 4px solid rgba(255,255,255,0.3);
- border-top-color: white;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- .toast {
- position: fixed;
- bottom: 30px;
- right: 30px;
- background: white;
- padding: 16px 24px;
- border-radius: 12px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
- display: flex;
- align-items: center;
- gap: 12px;
- transform: translateX(400px);
- transition: transform 0.3s ease;
- z-index: 1000;
- }
- .toast.show {
- transform: translateX(0);
- }
- .toast-icon {
- font-size: 24px;
- }
- .toast.success .toast-icon { color: #27ae60; }
- .toast.error .toast-icon { color: #e74c3c; }
- @media (max-width: 768px) {
- .cards-container {
- grid-template-columns: 1fr;
- }
-
- .header h1 {
- font-size: 22px;
- }
-
- .filters {
- flex-direction: column;
- align-items: stretch;
- }
-
- .filter-group select,
- .filter-group input {
- width: 100%;
- }
- }
- .json-preview {
- background: #1e1e1e;
- color: #d4d4d4;
- padding: 20px;
- border-radius: 8px;
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 13px;
- overflow-x: auto;
- max-height: 300px;
- overflow-y: auto;
- display: none;
- }
- .json-preview.show {
- display: block;
- }
- .toggle-json {
- margin-top: 10px;
- color: #667eea;
- cursor: pointer;
- font-size: 13px;
- text-decoration: underline;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>施工方案可视化审查结果工具</h1>
- <p class="subtitle">直观展示AI审查结果,快速定位问题与风险</p>
-
- <div class="file-selector">
- <label class="btn btn-primary">
- 📁 选择JSON文件
- <input type="file" id="fileInput" accept=".json">
- </label>
- <button class="btn btn-secondary" onclick="loadFromDefault()">
- 📂 加载默认目录
- </button>
- <span class="file-path" id="filePath">temp\construction_review\final_result\</span>
- </div>
- <div class="stats-bar" id="statsBar" style="display: none;">
- <div class="stat-card">
- <div class="stat-value" id="totalCount">0</div>
- <div class="stat-label">总检查项</div>
- </div>
- <div class="stat-card warning">
- <div class="stat-value" id="highRiskCount">0</div>
- <div class="stat-label">高风险</div>
- </div>
- <div class="stat-card info">
- <div class="stat-value" id="mediumRiskCount">0</div>
- <div class="stat-label">中风险</div>
- </div>
- <div class="stat-card success">
- <div class="stat-value" id="lowRiskCount">0</div>
- <div class="stat-label">低风险/无风险</div>
- </div>
- </div>
- </div>
- <div class="filters" id="filters" style="display: none;">
- <div class="filter-group">
- <label>风险等级</label>
- <select id="riskFilter" onchange="applyFilters()">
- <option value="all">全部</option>
- <option value="high">高风险</option>
- <option value="medium">中风险</option>
- <option value="low">低风险</option>
- <option value="none">无风险</option>
- </select>
- </div>
- <div class="filter-group">
- <label>检查项类型</label>
- <select id="chapterFilter" onchange="applyFilters()">
- <option value="all">全部章节</option>
- </select>
- </div>
- <div class="filter-group">
- <label>问题状态</label>
- <select id="issueFilter" onchange="applyFilters()">
- <option value="all">全部</option>
- <option value="has-issue">存在问题</option>
- <option value="no-issue">无问题</option>
- </select>
- </div>
- <div class="filter-group">
- <label>搜索</label>
- <input type="text" id="searchInput" placeholder="搜索关键词..." oninput="applyFilters()">
- </div>
- </div>
- <div id="content">
- <div class="empty-state">
- <div class="empty-state-icon">📂</div>
- <h2>请选择审查结果文件</h2>
- <p>支持直接上传JSON文件或从默认目录加载</p>
- </div>
- </div>
- </div>
- <div class="toast" id="toast">
- <span class="toast-icon">✓</span>
- <span class="toast-message">操作成功</span>
- </div>
- <script>
- let allReviewItems = [];
- let filteredItems = [];
- document.getElementById('fileInput').addEventListener('change', handleFileSelect);
- function handleFileSelect(event) {
- const file = event.target.files[0];
- if (!file) return;
- const reader = new FileReader();
- reader.onload = function(e) {
- try {
- // 移除控制字符
- let content = e.target.result;
- content = content.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f]/g, '');
- const data = JSON.parse(content);
- processData(data);
- showToast('文件加载成功', 'success');
- } catch (err) {
- showToast('JSON解析错误: ' + err.message, 'error');
- console.error(err);
- }
- };
- reader.readAsText(file);
- }
- function loadFromDefault() {
- // 尝试加载默认目录中的第一个JSON文件
- fetch('../../temp/construction_review/final_result/')
- .then(response => {
- if (!response.ok) throw new Error('无法访问目录');
- return response.text();
- })
- .then(html => {
- // 解析目录列表
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const links = Array.from(doc.querySelectorAll('a'));
- const jsonFiles = links
- .map(a => a.href)
- .filter(href => href.endsWith('.json'))
- .map(href => href.split('/').pop());
-
- if (jsonFiles.length > 0) {
- loadJsonFile(jsonFiles[0]);
- } else {
- showToast('默认目录中没有找到JSON文件', 'error');
- }
- })
- .catch(err => {
- // 如果无法列出目录,尝试直接加载已知文件
- const defaultFiles = [
- 'f926d2ad4428bfcbe12be8702e2c32ce-1773041592.json'
- ];
- loadJsonFile(defaultFiles[0]);
- });
- }
- function loadJsonFile(filename) {
- fetch(`../../temp/construction_review/final_result/${filename}`)
- .then(response => {
- if (!response.ok) throw new Error('文件加载失败');
- return response.text();
- })
- .then(text => {
- // 移除控制字符
- text = text.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f]/g, '');
- const data = JSON.parse(text);
- processData(data);
- showToast(`已加载: ${filename}`, 'success');
- })
- .catch(err => {
- showToast('加载失败: ' + err.message, 'error');
- });
- }
- function processData(data) {
- allReviewItems = [];
-
- const aiReviewResult = data.ai_review_result || {};
- const reviewResults = aiReviewResult.review_results || [];
-
- reviewResults.forEach(result => {
- Object.values(result).forEach(unit => {
- if (unit.review_lists) {
- unit.review_lists.forEach(item => {
- allReviewItems.push(item);
- });
- }
- });
- });
- // 填充章节筛选器
- populateChapterFilter();
-
- // 显示统计
- updateStats();
-
- // 应用筛选并渲染
- applyFilters();
-
- // 显示筛选栏
- document.getElementById('filters').style.display = 'flex';
- document.getElementById('statsBar').style.display = 'flex';
- }
- function populateChapterFilter() {
- const chapters = [...new Set(allReviewItems.map(item => item.chapter_code))];
- const select = document.getElementById('chapterFilter');
- select.innerHTML = '<option value="all">全部章节</option>';
- chapters.forEach(chapter => {
- if (chapter) {
- const option = document.createElement('option');
- option.value = chapter;
- option.textContent = chapter;
- select.appendChild(option);
- }
- });
- }
- function updateStats() {
- let high = 0, medium = 0, low = 0, none = 0;
-
- allReviewItems.forEach(item => {
- const riskLevel = getRiskLevel(item);
- switch(riskLevel) {
- case 'high': high++; break;
- case 'medium': medium++; break;
- case 'low': low++; break;
- default: none++; break;
- }
- });
- document.getElementById('totalCount').textContent = allReviewItems.length;
- document.getElementById('highRiskCount').textContent = high;
- document.getElementById('mediumRiskCount').textContent = medium;
- document.getElementById('lowRiskCount').textContent = low + none;
- }
- function getRiskLevel(item) {
- const riskInfo = item.risk_info || {};
- const riskLevel = (riskInfo.risk_level || '').toLowerCase();
-
- if (riskLevel.includes('高') || riskLevel.includes('high')) return 'high';
- if (riskLevel.includes('中') || riskLevel.includes('medium')) return 'medium';
- if (riskLevel.includes('低') || riskLevel.includes('low')) return 'low';
- return 'none';
- }
- function getRiskClass(riskLevel) {
- switch(riskLevel) {
- case 'high': return 'high-risk';
- case 'medium': return 'medium-risk';
- case 'low': return 'low-risk';
- default: return 'no-risk';
- }
- }
- function getRiskBadgeClass(riskLevel) {
- switch(riskLevel) {
- case 'high': return 'high';
- case 'medium': return 'medium';
- case 'low': return 'low';
- default: return 'none';
- }
- }
- function getRiskLabel(riskLevel) {
- switch(riskLevel) {
- case 'high': return '高风险';
- case 'medium': return '中风险';
- case 'low': return '低风险';
- default: return '无风险';
- }
- }
- function applyFilters() {
- const riskFilter = document.getElementById('riskFilter').value;
- const chapterFilter = document.getElementById('chapterFilter').value;
- const issueFilter = document.getElementById('issueFilter').value;
- const searchInput = document.getElementById('searchInput').value.toLowerCase();
- filteredItems = allReviewItems.filter(item => {
- // 风险等级筛选
- if (riskFilter !== 'all') {
- const itemRisk = getRiskLevel(item);
- if (itemRisk !== riskFilter) return false;
- }
- // 章节筛选
- if (chapterFilter !== 'all' && item.chapter_code !== chapterFilter) {
- return false;
- }
- // 问题状态筛选
- if (issueFilter !== 'all') {
- const hasIssue = item.exist_issue === true;
- if (issueFilter === 'has-issue' && !hasIssue) return false;
- if (issueFilter === 'no-issue' && hasIssue) return false;
- }
- // 搜索筛选
- if (searchInput) {
- const searchText = [
- item.check_item,
- item.check_item_code,
- item.chapter_code,
- item.check_result,
- item.location,
- item.suggestion,
- item.reason
- ].join(' ').toLowerCase();
- if (!searchText.includes(searchInput)) return false;
- }
- return true;
- });
- renderCards();
- }
- function renderCards() {
- const content = document.getElementById('content');
-
- if (filteredItems.length === 0) {
- content.innerHTML = `
- <div class="empty-state">
- <div class="empty-state-icon">🔍</div>
- <h2>没有找到匹配的结果</h2>
- <p>请尝试调整筛选条件</p>
- </div>
- `;
- return;
- }
- const cardsHtml = filteredItems.map(item => {
- const riskLevel = getRiskLevel(item);
- const riskClass = getRiskClass(riskLevel);
- const badgeClass = getRiskBadgeClass(riskLevel);
- const riskLabel = getRiskLabel(riskLevel);
- const hasIssue = item.exist_issue === true;
-
- return `
- <div class="review-card ${riskClass}">
- <div class="card-header">
- <div class="card-header-top">
- <span class="chapter-code">${escapeHtml(item.chapter_code || '未知章节')}</span>
- <span class="risk-badge ${badgeClass}">${riskLabel}</span>
- </div>
- <div class="check-item">${escapeHtml(item.check_item || '未命名检查项')}</div>
- <div class="check-item-code">${escapeHtml(item.check_item_code || '')}</div>
- </div>
- <div class="card-body">
- <div class="info-row">
- <div class="info-label">📍 问题位置</div>
- <div class="info-content location">${escapeHtml(item.location || item.check_result || '未指定')}</div>
- </div>
-
- ${item.suggestion ? `
- <div class="info-row">
- <div class="info-label">💡 修改建议</div>
- <div class="info-content suggestion">${escapeHtml(item.suggestion)}</div>
- </div>
- ` : ''}
-
- ${item.reason ? `
- <div class="info-row">
- <div class="info-label">📝 审查依据</div>
- <div class="info-content reason">${escapeHtml(item.reason)}</div>
- </div>
- ` : ''}
-
- ${item.review_references ? `
- <div class="info-row">
- <div class="info-label">📚 参考标准</div>
- <div class="info-content">${escapeHtml(item.review_references)}</div>
- </div>
- ` : ''}
- </div>
- <div class="card-footer">
- <div class="issue-status ${hasIssue ? 'has-issue' : 'no-issue'}">
- <span class="status-dot ${hasIssue ? 'has-issue' : 'no-issue'}"></span>
- <span>${hasIssue ? '存在问题' : '无问题'}</span>
- </div>
- ${item.reference_source ? `
- <div class="reference-source" title="${escapeHtml(item.reference_source)}">
- 来源: ${escapeHtml(item.reference_source)}
- </div>
- ` : ''}
- </div>
- </div>
- `;
- }).join('');
- content.innerHTML = `<div class="cards-container">${cardsHtml}</div>`;
- }
- function escapeHtml(text) {
- if (!text) return '';
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- function showToast(message, type) {
- const toast = document.getElementById('toast');
- toast.className = `toast ${type} show`;
- toast.querySelector('.toast-icon').textContent = type === 'success' ? '✓' : '✗';
- toast.querySelector('.toast-message').textContent = message;
-
- setTimeout(() => {
- toast.classList.remove('show');
- }, 3000);
- }
- // 页面加载时尝试自动加载
- window.addEventListener('load', () => {
- // 可选:自动加载默认文件
- // loadFromDefault();
- });
- </script>
- </body>
- </html>
|