x
This commit is contained in:
@@ -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}')>"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user