-
+
@@ -198,66 +198,66 @@ import { getSimulationHistory } from '../api/simulation'
const router = useRouter()
const route = useRoute()
-// 状态
+// State
const projects = ref([])
const loading = ref(true)
const isExpanded = ref(false)
const hoveringCard = ref(null)
const historyContainer = ref(null)
-const selectedProject = ref(null) // 当前选中的项目(用于弹窗)
+const selectedProject = ref(null) // Currently selected project (for modal)
let observer = null
-let isAnimating = false // 动画锁,防止闪烁
-let expandDebounceTimer = null // 防抖定时器
-let pendingState = null // 记录待执行的目标状态
+let isAnimating = false // Animation lock to prevent flickering
+let expandDebounceTimer = null // Debounce timer
+let pendingState = null // Records the target state to be executed
-// 卡片布局配置 - 调整为更宽的比例
+// Card layout configuration - adjusted to wider aspect ratio
const CARDS_PER_ROW = 4
-const CARD_WIDTH = 280
-const CARD_HEIGHT = 280
+const CARD_WIDTH = 280
+const CARD_HEIGHT = 280
const CARD_GAP = 24
-// 动态计算容器高度样式
+// Dynamically calculate container height style
const containerStyle = computed(() => {
if (!isExpanded.value) {
- // 折叠态:固定高度
+ // Collapsed state: fixed height
return { minHeight: '420px' }
}
-
- // 展开态:根据卡片数量动态计算高度
+
+ // Expanded state: dynamically calculate height based on card count
const total = projects.value.length
if (total === 0) {
return { minHeight: '280px' }
}
-
+
const rows = Math.ceil(total / CARDS_PER_ROW)
- // 计算实际需要的高度:行数 * 卡片高度 + (行数-1) * 间距 + 少量底部间距
+ // Calculate actual required height: rows * card height + (rows-1) * gap + small bottom spacing
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
-
+
return { minHeight: `${expandedHeight}px` }
})
-// 获取卡片样式
+// Get card style
const getCardStyle = (index) => {
const total = projects.value.length
-
+
if (isExpanded.value) {
- // 展开态:网格布局
+ // Expanded state: grid layout
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
const col = index % CARDS_PER_ROW
const row = Math.floor(index / CARDS_PER_ROW)
-
- // 计算当前行的卡片数量,确保每行居中
+
+ // Calculate card count for current row, ensure each row is centered
const currentRowStart = row * CARDS_PER_ROW
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
-
+
const rowWidth = currentRowCards * CARD_WIDTH + (currentRowCards - 1) * CARD_GAP
-
+
const startX = -(rowWidth / 2) + (CARD_WIDTH / 2)
const colInRow = index % CARDS_PER_ROW
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
-
- // 向下展开,增加与标题的间距
+
+ // Expand downward, increase spacing from title
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
return {
@@ -267,18 +267,18 @@ const getCardStyle = (index) => {
transition: transition
}
} else {
- // 折叠态:扇形堆叠
+ // Collapsed state: fan-shaped stacking
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
const centerIndex = (total - 1) / 2
const offset = index - centerIndex
-
+
const x = offset * 35
- // 调整起始位置,靠近标题但保持适当间距
+ // Adjust starting position, close to title but maintain proper spacing
const y = 25 + Math.abs(offset) * 8
const r = offset * 3
const s = 0.95 - Math.abs(offset) * 0.05
-
+
return {
transform: `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`,
zIndex: 10 + index,
@@ -288,24 +288,24 @@ const getCardStyle = (index) => {
}
}
-// 根据轮数进度获取样式类
+// Get style class based on round progress
const getProgressClass = (simulation) => {
const current = simulation.current_round || 0
const total = simulation.total_rounds || 0
-
+
if (total === 0 || current === 0) {
- // 未开始
+ // Not started
return 'not-started'
} else if (current >= total) {
- // 已完成
+ // Completed
return 'completed'
} else {
- // 进行中
+ // In progress
return 'in-progress'
}
}
-// 格式化日期(只显示日期部分)
+// Format date (only display date part)
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
@@ -316,7 +316,7 @@ const formatDate = (dateStr) => {
}
}
-// 格式化时间(显示时:分)
+// Format time (display hours:minutes)
const formatTime = (dateStr) => {
if (!dateStr) return ''
try {
@@ -329,35 +329,35 @@ const formatTime = (dateStr) => {
}
}
-// 截断文本
+// Truncate text
const truncateText = (text, maxLength) => {
if (!text) return ''
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}
-// 从模拟需求生成标题(取前20字)
+// Generate title from simulation requirement (first 20 characters)
const getSimulationTitle = (requirement) => {
- if (!requirement) return '未命名模拟'
+ if (!requirement) return 'Unnamed Simulation'
const title = requirement.slice(0, 20)
return requirement.length > 20 ? title + '...' : title
}
-// 格式化 simulation_id 显示(截取前6位)
+// Format simulation_id display (first 6 characters)
const formatSimulationId = (simulationId) => {
if (!simulationId) return 'SIM_UNKNOWN'
const prefix = simulationId.replace('sim_', '').slice(0, 6)
return `SIM_${prefix.toUpperCase()}`
}
-// 格式化轮数显示(当前轮/总轮数)
+// Format round display (current round/total rounds)
const formatRounds = (simulation) => {
const current = simulation.current_round || 0
const total = simulation.total_rounds || 0
- if (total === 0) return '未开始'
- return `${current}/${total} 轮`
+ if (total === 0) return 'Not Started'
+ return `${current}/${total} rounds`
}
-// 获取文件类型(用于样式)
+// Get file type (for styling)
const getFileType = (filename) => {
if (!filename) return 'other'
const ext = filename.split('.').pop()?.toLowerCase()
@@ -373,35 +373,35 @@ const getFileType = (filename) => {
return typeMap[ext] || 'other'
}
-// 获取文件类型标签文本
+// Get file type label text
const getFileTypeLabel = (filename) => {
if (!filename) return 'FILE'
const ext = filename.split('.').pop()?.toUpperCase()
return ext || 'FILE'
}
-// 截断文件名(保留扩展名)
+// Truncate filename (preserve extension)
const truncateFilename = (filename, maxLength) => {
- if (!filename) return '未知文件'
+ if (!filename) return 'Unknown File'
if (filename.length <= maxLength) return filename
-
+
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
const nameWithoutExt = filename.slice(0, filename.length - ext.length)
const truncatedName = nameWithoutExt.slice(0, maxLength - ext.length - 3) + '...'
return truncatedName + ext
}
-// 打开项目详情弹窗
+// Open project details modal
const navigateToProject = (simulation) => {
selectedProject.value = simulation
}
-// 关闭弹窗
+// Close modal
const closeModal = () => {
selectedProject.value = null
}
-// 导航到图谱构建页面(Project)
+// Navigate to Graph Construction page (Project)
const goToProject = () => {
if (selectedProject.value?.project_id) {
router.push({
@@ -412,7 +412,7 @@ const goToProject = () => {
}
}
-// 导航到环境配置页面(Simulation)
+// Navigate to Environment Setup page (Simulation)
const goToSimulation = () => {
if (selectedProject.value?.simulation_id) {
router.push({
@@ -423,7 +423,7 @@ const goToSimulation = () => {
}
}
-// 导航到分析报告页面(Report)
+// Navigate to Analysis Report page (Report)
const goToReport = () => {
if (selectedProject.value?.report_id) {
router.push({
@@ -434,7 +434,7 @@ const goToReport = () => {
}
}
-// 加载历史项目
+// Load history projects
const loadHistory = async () => {
try {
loading.value = true
@@ -443,65 +443,65 @@ const loadHistory = async () => {
projects.value = response.data || []
}
} catch (error) {
- console.error('加载历史项目失败:', error)
+ console.error('Failed to load history projects:', error)
projects.value = []
} finally {
loading.value = false
}
}
-// 初始化 IntersectionObserver
+// Initialize IntersectionObserver
const initObserver = () => {
if (observer) {
observer.disconnect()
}
-
+
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const shouldExpand = entry.isIntersecting
-
- // 更新待执行的目标状态(无论是否在动画中都要记录最新的目标状态)
+
+ // Update target state to execute (always record latest target state regardless of animation)
pendingState = shouldExpand
-
- // 清除之前的防抖定时器(新的滚动意图会覆盖旧的)
+
+ // Clear previous debounce timer (new scroll intent overrides old)
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
}
-
- // 如果正在动画中,只记录状态,等动画结束后处理
+
+ // If animating, only record state and process after animation completes
if (isAnimating) return
-
- // 如果目标状态与当前状态相同,不需要处理
+
+ // If target state matches current state, no need to process
if (shouldExpand === isExpanded.value) {
pendingState = null
return
}
-
- // 使用防抖延迟状态切换,防止快速闪烁
- // 展开时延迟较短(50ms),收起时延迟较长(200ms)以增加稳定性
+
+ // Use debounce to delay state switch and prevent rapid flickering
+ // Shorter delay when expanding (50ms), longer when collapsing (200ms) for stability
const delay = shouldExpand ? 50 : 200
-
+
expandDebounceTimer = setTimeout(() => {
- // 检查是否正在动画
+ // Check if animating
if (isAnimating) return
-
- // 检查待执行状态是否仍需要执行(可能已被后续滚动覆盖)
+
+ // Check if pending state still needs execution (may have been overridden by subsequent scrolls)
if (pendingState === null || pendingState === isExpanded.value) return
-
- // 设置动画锁
+
+ // Set animation lock
isAnimating = true
isExpanded.value = pendingState
pendingState = null
-
- // 动画完成后解除锁定,并检查是否有待处理的状态变化
+
+ // Release lock after animation completes and check for pending state changes
setTimeout(() => {
isAnimating = false
-
- // 动画结束后,检查是否有新的待执行状态
+
+ // After animation, check for new pending state
if (pendingState !== null && pendingState !== isExpanded.value) {
- // 延迟一小段时间再执行,避免太快切换
+ // Delay slightly before executing to avoid too-quick switching
expandDebounceTimer = setTimeout(() => {
if (pendingState !== null && pendingState !== isExpanded.value) {
isAnimating = true
@@ -518,20 +518,20 @@ const initObserver = () => {
})
},
{
- // 使用多个阈值,使检测更平滑
+ // Use multiple thresholds for smoother detection
threshold: [0.4, 0.6, 0.8],
- // 调整 rootMargin,视口底部向上收缩,需要滚动更多才触发展开
+ // Adjust rootMargin: shrink viewport bottom upward, require more scrolling to trigger expansion
rootMargin: '0px 0px -150px 0px'
}
)
-
- // 开始观察
+
+ // Start observing
if (historyContainer.value) {
observer.observe(historyContainer.value)
}
}
-// 监听路由变化,当返回首页时重新加载数据
+// Watch for route changes and reload data when returning to home page
watch(() => route.path, (newPath) => {
if (newPath === '/') {
loadHistory()
@@ -539,28 +539,28 @@ watch(() => route.path, (newPath) => {
})
onMounted(async () => {
- // 确保 DOM 渲染完成后再加载数据
+ // Ensure DOM rendering is complete before loading data
await nextTick()
await loadHistory()
-
- // 等待 DOM 渲染后初始化观察器
+
+ // Initialize observer after DOM rendering
setTimeout(() => {
initObserver()
}, 100)
})
-// 如果使用 keep-alive,在组件激活时重新加载数据
+// If using keep-alive, reload data when component is activated
onActivated(() => {
loadHistory()
})
onUnmounted(() => {
- // 清理 Intersection Observer
+ // Clean up Intersection Observer
if (observer) {
observer.disconnect()
observer = null
}
- // 清理防抖定时器
+ // Clean up debounce timer
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
@@ -569,7 +569,7 @@ onUnmounted(() => {
+
diff --git a/frontend/src/views/InteractionView.vue b/frontend/src/views/InteractionView.vue
index b153590d7..b2eae1ed0 100644
--- a/frontend/src/views/InteractionView.vue
+++ b/frontend/src/views/InteractionView.vue
@@ -3,7 +3,7 @@
@@ -23,7 +23,7 @@
-
+