This commit is contained in:
2026-03-08 00:51:39 +08:00
parent e50a402152
commit 84e7ae4ef1
4 changed files with 204 additions and 79 deletions
+4
View File
@@ -162,6 +162,7 @@ class STPFile(Base):
analysis_metrics = relationship("AnalysisMetrics", back_populates="stp_file", uselist=False) analysis_metrics = relationship("AnalysisMetrics", back_populates="stp_file", uselist=False)
feature_detections = relationship("FeatureDetection", back_populates="stp_file") feature_detections = relationship("FeatureDetection", back_populates="stp_file")
design_recommendations = relationship("DesignRecommendation", back_populates="stp_file") design_recommendations = relationship("DesignRecommendation", back_populates="stp_file")
processing_tasks = relationship("ProcessingTask", back_populates="stp_file")
def __repr__(self): def __repr__(self):
return f"<STPFile(id={self.id}, original_filename='{self.original_filename}', status='{self.status}')>" return f"<STPFile(id={self.id}, original_filename='{self.original_filename}', status='{self.status}')>"
@@ -294,6 +295,9 @@ class ProcessingTask(Base):
# 处理参数 # 处理参数
parameters = Column(JSON, nullable=True) # 任务参数 parameters = Column(JSON, nullable=True) # 任务参数
# 关联关系
stp_file = relationship("STPFile", back_populates="processing_tasks")
def __repr__(self): def __repr__(self):
return f"<ProcessingTask(id={self.id}, task_id='{self.task_id}', status='{self.status}')>" return f"<ProcessingTask(id={self.id}, task_id='{self.task_id}', status='{self.status}')>"
+16 -4
View File
@@ -535,8 +535,12 @@ class StorageIntegrationService:
limit: int = 50 limit: int = 50
) -> list: ) -> list:
"""获取同一文件名的所有上传历史记录""" """获取同一文件名的所有上传历史记录"""
from models.database import ProcessingTask
from sqlalchemy.orm import joinedload
query = select(STPFile).where( query = select(STPFile).options(
joinedload(STPFile.processing_tasks)
).where(
STPFile.original_filename == filename STPFile.original_filename == filename
).order_by(STPFile.upload_time.desc()) ).order_by(STPFile.upload_time.desc())
@@ -546,11 +550,12 @@ class StorageIntegrationService:
query = query.limit(limit) query = query.limit(limit)
result = await session.execute(query) result = await session.execute(query)
files = result.scalars().all() files = result.unique().scalars().all()
return [ return [
{ {
'id': f.id, 'id': f.id,
'task_id': f.processing_tasks[0].task_id if f.processing_tasks else None,
'upload_batch': f.upload_batch, 'upload_batch': f.upload_batch,
'upload_time': f.upload_time.isoformat() if f.upload_time else None, 'upload_time': f.upload_time.isoformat() if f.upload_time else None,
'file_size': f.file_size, 'file_size': f.file_size,
@@ -572,6 +577,8 @@ class StorageIntegrationService:
"""获取所有文件分组(按文件名分组),包含每个文件的最新分析结果""" """获取所有文件分组(按文件名分组),包含每个文件的最新分析结果"""
from sqlalchemy import func, desc from sqlalchemy import func, desc
from sqlalchemy.orm import joinedload
from models.database import ProcessingTask
# 子查询:获取每个文件名的最新上传 # 子查询:获取每个文件名的最新上传
subquery = ( subquery = (
@@ -591,7 +598,9 @@ class StorageIntegrationService:
# 主查询:获取最新记录和统计信息 # 主查询:获取最新记录和统计信息
query = ( query = (
select(STPFile) select(STPFile).options(
joinedload(STPFile.processing_tasks)
)
.join( .join(
subquery, subquery,
(STPFile.original_filename == subquery.c.original_filename) & (STPFile.original_filename == subquery.c.original_filename) &
@@ -601,7 +610,7 @@ class StorageIntegrationService:
) )
result = await session.execute(query) result = await session.execute(query)
latest_files = result.scalars().all() latest_files = result.unique().scalars().all()
# 获取每个文件名的上传次数 # 获取每个文件名的上传次数
file_groups = [] file_groups = []
@@ -615,9 +624,12 @@ class StorageIntegrationService:
count_result = await session.execute(count_query) count_result = await session.execute(count_query)
upload_count = count_result.scalar() upload_count = count_result.scalar()
task_id = f.processing_tasks[0].task_id if f.processing_tasks else None
file_groups.append({ file_groups.append({
'filename': f.original_filename, 'filename': f.original_filename,
'latest_id': f.id, 'latest_id': f.id,
'latest_task_id': task_id,
'latest_upload_time': f.upload_time.isoformat() if f.upload_time else None, 'latest_upload_time': f.upload_time.isoformat() if f.upload_time else None,
'latest_status': f.status, 'latest_status': f.status,
'upload_count': upload_count, 'upload_count': upload_count,
+112
View File
@@ -525,6 +525,118 @@ body {
border: 1px solid rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.2);
} }
/* ================================
历史记录下拉展开
================================ */
.file-row {
cursor: pointer;
transition: background-color var(--duration-fast) var(--ease-default);
}
.file-row:hover {
background-color: var(--bg-secondary);
}
.expand-icon {
display: inline-block;
width: 20px;
color: var(--text-tertiary);
font-size: var(--text-xs);
transition: transform var(--duration-fast) var(--ease-default);
}
.history-detail-row {
background-color: var(--bg-secondary);
}
.history-detail-row td {
padding: 0;
}
.history-detail {
padding: var(--space-4) var(--space-6);
}
.inner-table {
background: var(--bg-primary);
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--border-light);
}
.inner-table th {
background: var(--bg-tertiary);
font-size: var(--text-xs);
}
.inner-table td {
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
}
.btn-sm {
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
}
/* ================================
进度条
================================ */
.file-row {
cursor: pointer;
transition: background-color var(--duration-fast) var(--ease-default);
}
.file-row:hover {
background-color: var(--bg-secondary);
}
.expand-icon {
display: inline-block;
width: 20px;
color: var(--text-tertiary);
font-size: var(--text-xs);
transition: transform var(--duration-fast) var(--ease-default);
}
.history-detail-row {
background-color: var(--bg-secondary);
}
.history-detail-row td {
padding: 0;
}
.history-detail {
padding: var(--space-4) var(--space-6);
}
.inner-table {
background: var(--bg-primary);
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--border-light);
}
.inner-table th {
background: var(--bg-tertiary);
font-size: var(--text-xs);
}
.inner-table td {
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
}
/* ================================ /* ================================
进度条 进度条
================================ */ ================================ */
+72 -75
View File
@@ -761,9 +761,7 @@ const MoldInsightView = {
dragOver: false, dragOver: false,
progress: 0, progress: 0,
history: null, history: null,
showFileHistory: false, expandedFiles: {}
selectedFileHistory: null,
fileHistoryRecords: []
}); });
const loadHistory = async () => { const loadHistory = async () => {
@@ -774,19 +772,21 @@ const MoldInsightView = {
} }
}; };
const viewFileHistory = async (filename) => { const toggleFileHistory = async (filename) => {
try { if (state.expandedFiles[filename]) {
state.selectedFileHistory = filename; state.expandedFiles[filename] = null;
state.fileHistoryRecords = await apiRequest(`/api/history/${encodeURIComponent(filename)}`); } else {
state.showFileHistory = true; try {
} catch (e) { const records = await apiRequest(`/api/history/${encodeURIComponent(filename)}`);
handleApiError(e, '加载文件历史'); state.expandedFiles[filename] = records;
} catch (e) {
handleApiError(e, '加载文件历史');
}
} }
}; };
const viewResult = (record) => { const viewResult = (record) => {
state.showFileHistory = false; router.push(`/moldinsight/result/${record.task_id}`);
router.push(`/moldinsight/result/${record.id}`);
}; };
const handleFileChange = (event) => { const handleFileChange = (event) => {
@@ -899,7 +899,7 @@ const MoldInsightView = {
uploadFile, uploadFile,
formatFileSize, formatFileSize,
formatDateTime, formatDateTime,
viewFileHistory, toggleFileHistory,
viewResult viewResult
}; };
}, },
@@ -968,74 +968,71 @@ const MoldInsightView = {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="file in state.history.files" :key="file.filename"> <template v-for="file in state.history.files" :key="file.filename">
<td>{{ file.filename }}</td> <tr class="file-row" @click="toggleFileHistory(file.filename)">
<td>
<span class="badge badge-info">{{ file.upload_count }} 次</span>
</td>
<td>{{ formatFileSize(file.file_size) }}</td>
<td>
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ file.latest_status }}
</span>
</td>
<td>{{ formatDateTime(file.latest_upload_time) }}</td>
<td>
<div class="action-buttons">
<button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="router.push('/moldinsight/result/' + file.latest_id)">
查看最新
</button>
<button class="btn-sm btn-secondary" @click="viewFileHistory(file.filename)">
历史记录
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="state.showFileHistory" class="modal-overlay" @click.self="state.showFileHistory = false">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>{{ state.selectedFileHistory }} - 历史记录</h2>
<button class="modal-close" @click="state.showFileHistory = false">×</button>
</div>
<div class="modal-body">
<table class="data-table">
<thead>
<tr>
<th>上传时间</th>
<th>文件大小</th>
<th>状态</th>
<th>体积 (mm³)</th>
<th>表面积 (mm²)</th>
<th>重量 (g)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in state.fileHistoryRecords" :key="record.id">
<td>{{ formatDateTime(record.upload_time) }}</td>
<td>{{ formatFileSize(record.file_size) }}</td>
<td> <td>
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']"> <span class="expand-icon">{{ state.expandedFiles[file.filename] ? '▼' : '▶' }}</span>
{{ record.status }} {{ file.filename }}
</td>
<td>
<span class="badge badge-info">{{ file.upload_count }} 次</span>
</td>
<td>{{ formatFileSize(file.file_size) }}</td>
<td>
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ file.latest_status }}
</span> </span>
</td> </td>
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td> <td>{{ formatDateTime(file.latest_upload_time) }}</td>
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
<td> <td>
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)"> <div class="action-buttons" @click.stop>
查看详情 <button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="viewResult({task_id: file.latest_task_id})">
</button> 查看最新
</button>
</div>
</td> </td>
</tr> </tr>
</tbody> <tr v-if="state.expandedFiles[file.filename]" class="history-detail-row">
</table> <td colspan="6">
</div> <div class="history-detail">
<table class="data-table inner-table">
<thead>
<tr>
<th>上传时间</th>
<th>文件大小</th>
<th>状态</th>
<th>体积 (mm³)</th>
<th>表面积 (mm²)</th>
<th>重量 (g)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in state.expandedFiles[file.filename]" :key="record.id">
<td>{{ formatDateTime(record.upload_time) }}</td>
<td>{{ formatFileSize(record.file_size) }}</td>
<td>
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ record.status }}
</span>
</td>
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td>
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
<td>
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)">
查看详情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>