666ghj commited on
Commit
51469e0
·
1 Parent(s): dded3fe

refactor(simulation): enhance simulation data retrieval and project file handling

Browse files

- Updated simulation history retrieval to read project details directly from the Simulation file.
- Improved simulation configuration handling by reading simulation requirements from JSON.
- Added project file listing to the simulation history, displaying up to three associated files.
- Refined card layout in HistoryDatabase.vue to accommodate new file display features and improved responsiveness.

backend/app/api/simulation.py CHANGED
@@ -849,41 +849,48 @@ def get_simulation_history():
849
  manager = SimulationManager()
850
  simulations = manager.list_simulations()[:limit]
851
 
852
- # 增强模拟数据,添加项目详情
853
  enriched_simulations = []
854
  for sim in simulations:
855
  sim_dict = sim.to_dict()
856
 
857
- # 获取关联的项目信息
858
- project = ProjectManager.get_project(sim.project_id)
859
- if project:
860
- sim_dict["project_name"] = project.name
861
- sim_dict["simulation_requirement"] = project.simulation_requirement
862
- else:
863
- sim_dict["project_name"] = "未知项目"
864
- sim_dict["simulation_requirement"] = ""
865
-
866
- # 获取模拟配置信息
867
  config = manager.get_simulation_config(sim.simulation_id)
868
  if config:
 
869
  time_config = config.get("time_config", {})
870
  sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0)
871
- sim_dict["total_rounds"] = int(
 
872
  time_config.get("total_simulation_hours", 0) * 60 /
873
  max(time_config.get("minutes_per_round", 60), 1)
874
  )
875
  else:
 
876
  sim_dict["total_simulation_hours"] = 0
877
- sim_dict["total_rounds"] = 0
878
 
879
- # 获取运行状态
880
  run_state = SimulationRunner.get_run_state(sim.simulation_id)
881
  if run_state:
882
  sim_dict["current_round"] = run_state.current_round
883
  sim_dict["runner_status"] = run_state.runner_status.value
 
 
884
  else:
885
  sim_dict["current_round"] = 0
886
  sim_dict["runner_status"] = "idle"
 
 
 
 
 
 
 
 
 
 
 
887
 
888
  # 添加版本号
889
  sim_dict["version"] = "v1.0.2"
 
849
  manager = SimulationManager()
850
  simulations = manager.list_simulations()[:limit]
851
 
852
+ # 增强模拟数据,只从 Simulation 文件读取
853
  enriched_simulations = []
854
  for sim in simulations:
855
  sim_dict = sim.to_dict()
856
 
857
+ # 获取模拟配置信息(从 simulation_config.json 读取 simulation_requirement)
 
 
 
 
 
 
 
 
 
858
  config = manager.get_simulation_config(sim.simulation_id)
859
  if config:
860
+ sim_dict["simulation_requirement"] = config.get("simulation_requirement", "")
861
  time_config = config.get("time_config", {})
862
  sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0)
863
+ # 推荐轮数(后备值)
864
+ recommended_rounds = int(
865
  time_config.get("total_simulation_hours", 0) * 60 /
866
  max(time_config.get("minutes_per_round", 60), 1)
867
  )
868
  else:
869
+ sim_dict["simulation_requirement"] = ""
870
  sim_dict["total_simulation_hours"] = 0
871
+ recommended_rounds = 0
872
 
873
+ # 获取运行状态(从 run_state.json 读取用户设置的实际轮数)
874
  run_state = SimulationRunner.get_run_state(sim.simulation_id)
875
  if run_state:
876
  sim_dict["current_round"] = run_state.current_round
877
  sim_dict["runner_status"] = run_state.runner_status.value
878
+ # 使用用户设置的 total_rounds,若无则使用推荐轮数
879
+ sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds
880
  else:
881
  sim_dict["current_round"] = 0
882
  sim_dict["runner_status"] = "idle"
883
+ sim_dict["total_rounds"] = recommended_rounds
884
+
885
+ # 获取关联项目的文件列表(最多3个)
886
+ project = ProjectManager.get_project(sim.project_id)
887
+ if project and hasattr(project, 'files') and project.files:
888
+ sim_dict["files"] = [
889
+ {"filename": f.get("filename", "未知文件")}
890
+ for f in project.files[:3]
891
+ ]
892
+ else:
893
+ sim_dict["files"] = []
894
 
895
  # 添加版本号
896
  sim_dict["version"] = "v1.0.2"
frontend/src/components/HistoryDatabase.vue CHANGED
@@ -1,8 +1,7 @@
1
  <template>
2
  <div
3
  class="history-database"
4
- @mouseenter="handleMouseEnter"
5
- @mouseleave="handleMouseLeave"
6
  >
7
  <!-- 背景装饰:技术网格线(使用CSS背景,固定间距正方形网格) -->
8
  <div class="tech-grid-bg">
@@ -10,16 +9,11 @@
10
  <div class="gradient-overlay"></div>
11
  </div>
12
 
13
- <!-- CTA 按钮 - 位置固定不变 -->
14
- <div
15
- class="cta-button"
16
- @click="toggleExpand"
17
- >
18
- <div class="cta-inner">
19
- <span class="cta-icon">◎</span>
20
- <span class="cta-text">HISTORY DATABASE ({{ projects.length }})</span>
21
- <span class="cta-arrow" :class="{ expanded: isExpanded }">→</span>
22
- </div>
23
  </div>
24
 
25
  <!-- 卡片容器 -->
@@ -34,39 +28,47 @@
34
  @mouseleave="hoveringCard = null"
35
  @click="navigateToProject(project)"
36
  >
37
- <!-- 卡片头部:ID和状态 -->
38
  <div class="card-header">
39
- <span class="card-id">ID_{{ String(index + 1).padStart(3, '0') }}</span>
40
  <span class="card-status" :class="getStatusClass(project.status)">
41
  <span class="status-dot">●</span> {{ getStatusText(project.status) }}
42
  </span>
43
  </div>
44
 
45
- <!-- 卡片图片区域(带角落装饰) -->
46
- <div class="card-image-wrapper">
47
  <!-- 角落装饰 - 取景框风格 -->
48
  <div class="corner-mark top-left-only"></div>
49
-
50
- <!-- 图片 -->
51
- <img
52
- class="card-image"
53
- :src="getRandomImageUrl(project.simulation_id, index)"
54
- :alt="project.project_name"
55
- loading="lazy"
56
- @error="handleImageError($event, index)"
57
- />
 
 
 
 
 
 
 
 
58
  </div>
59
 
60
- <!-- 卡片标题 -->
61
- <h3 class="card-title">{{ project.project_name || 'Unnamed Project' }}</h3>
62
 
63
- <!-- 卡片描述 -->
64
  <p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
65
 
66
  <!-- 卡片底部 -->
67
  <div class="card-footer">
68
  <span class="card-date">{{ formatDate(project.created_at) }}</span>
69
- <span class="card-version">{{ project.version || 'v1.0.2' }}</span>
70
  </div>
71
 
72
  <!-- 底部装饰线 (hover时展开) -->
@@ -89,7 +91,7 @@
89
  </template>
90
 
91
  <script setup>
92
- import { ref, computed, onMounted, onUnmounted } from 'vue'
93
  import { useRouter } from 'vue-router'
94
  import { getSimulationHistory } from '../api/simulation'
95
 
@@ -100,38 +102,14 @@ const projects = ref([])
100
  const loading = ref(true)
101
  const isExpanded = ref(false)
102
  const hoveringCard = ref(null)
103
- const imageErrors = ref({}) // 追踪图片加载错误
 
104
 
105
  // 卡片布局配置 - 调整为更宽的比例
106
  const CARDS_PER_ROW = 4
107
  const CARD_WIDTH = 280
108
  const CARD_HEIGHT = 280
109
  const CARD_GAP = 24
110
- const EXPANDED_ROW_HEIGHT = 230 // 行高 230px (Requirements)
111
- const EXPANDED_COL_WIDTH = 280 // 列宽 (Requirements spacing 280px)
112
-
113
- // 随机图片服务配置(中国可访问)
114
- const IMAGE_SERVICES = {
115
- // Lorem Picsum - 国际服务,中国大部分地区可访问
116
- picsum: (seed, width, height) =>
117
- `https://picsum.photos/seed/${seed}/${width}/${height}`,
118
- }
119
-
120
- // 生成随机图片URL - 调整图片比例为超扁平 (280x64)
121
- const getRandomImageUrl = (simulationId, index) => {
122
- if (imageErrors.value[index]) {
123
- return null
124
- }
125
- const seed = simulationId || `project-${index}`
126
- // 宽280,高64,约4.4:1比例,极度扁平
127
- return IMAGE_SERVICES.picsum(seed, 280, 64)
128
- }
129
-
130
- // 处理图片加载错误
131
- const handleImageError = (event, index) => {
132
- imageErrors.value[index] = true
133
- event.target.style.display = 'none'
134
- }
135
 
136
  // 获取卡片样式
137
  const getCardStyle = (index) => {
@@ -139,7 +117,6 @@ const getCardStyle = (index) => {
139
 
140
  if (isExpanded.value) {
141
  // 展开态:网格布局
142
- // 物理特性:Easing: cubic-bezier(0.23, 1, 0.32, 1), Duration: 700ms
143
  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'
144
 
145
  const col = index % CARDS_PER_ROW
@@ -149,64 +126,38 @@ const getCardStyle = (index) => {
149
  const currentRowStart = row * CARDS_PER_ROW
150
  const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
151
 
152
- // 水平居中偏移
153
- // 间距 280px (Based on CARD_WIDTH being 280px. Assuming standard grid gap is included or minimal)
154
- // Using CARD_WIDTH + CARD_GAP for spacing calculation to be safe, but requirements said "spacing 280px".
155
- // If spacing means column width, then grid width is ColWidth * count.
156
- // Let's stick to the previous logic but ensure center alignment.
157
-
158
  const rowWidth = currentRowCards * CARD_WIDTH + (currentRowCards - 1) * CARD_GAP
159
- const containerWidth = CARDS_PER_ROW * CARD_WIDTH + (CARDS_PER_ROW - 1) * CARD_GAP // Full width of a complete row
160
-
161
- // Calculate offset to center the current row relative to the full container width
162
- // Actually, the requirements say "translateX: based on colIndex, centered per row"
163
- // So for a row with 3 items, they should be centered.
164
- // The visual center is 0.
165
- // Leftmost item x = - (rowWidth / 2) + (CARD_WIDTH / 2)
166
- // Next item x += CARD_WIDTH + CARD_GAP
167
 
168
  const startX = -(rowWidth / 2) + (CARD_WIDTH / 2)
169
- const offsetX = (col % CARDS_PER_ROW) * (CARD_WIDTH + CARD_GAP) // offset within the row
170
-
171
- // Wait, the calculation needs to be based on the column index WITHIN the current row (0 to currentRowCards-1)
172
- // Since col = index % 4, it resets for each row.
173
  const colInRow = index % CARDS_PER_ROW
174
  const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
175
 
176
- // translateY: 向下展开逻辑. 行高 300px (包含卡片高度280+间距).
177
- // Row 0 在顶部,后续行向下排列
178
  const y = row * (CARD_HEIGHT + CARD_GAP)
179
 
180
  return {
181
  transform: `translate(${x}px, ${y}px) rotate(0deg) scale(1)`,
182
- zIndex: 100 + index, // Requirements: 100 + gridIndex
183
  opacity: 1,
184
  transition: transition
185
  }
186
  } else {
187
  // 折叠态:扇形堆叠
188
- // 物理特性:Easing: cubic-bezier(0.23, 1, 0.32, 1), Duration: 700ms
189
  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'
190
 
191
- const centerIndex = (total - 1) / 2 // Center index (float)
192
- const offset = index - centerIndex // Offset from center
193
 
194
- // translateX: offset * 35px
195
  const x = offset * 35
196
-
197
- // translateY: 130px + Math.abs(offset) * 8px
198
- const y = 130 + Math.abs(offset) * 8
199
-
200
- // rotate: offset * 3deg
201
  const r = offset * 3
202
-
203
- // scale: 0.95 - Math.abs(offset) * 0.05
204
  const s = 0.95 - Math.abs(offset) * 0.05
205
 
206
  return {
207
  transform: `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`,
208
- zIndex: 10 + index, // Requirements: 10 + index
209
- opacity: 1, // Collapsed cards are usually fully opaque in the stack
210
  transition: transition
211
  }
212
  }
@@ -255,32 +206,68 @@ const truncateText = (text, maxLength) => {
255
  return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
256
  }
257
 
258
- // 事件处理
259
- const handleMouseEnter = () => {
260
- isExpanded.value = true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
 
263
- const handleMouseLeave = () => {
264
- isExpanded.value = false
 
 
 
265
  }
266
 
267
- const toggleExpand = () => {
268
- isExpanded.value = !isExpanded.value
 
 
 
 
 
 
 
269
  }
270
 
271
- // 导航到项目
272
- const navigateToProject = (project) => {
273
- if (project.status === 'completed' || project.status === 'running' || project.status === 'ready') {
274
- router.push({
275
- name: 'SimulationRun',
276
- params: { simulationId: project.simulation_id }
277
- })
278
- } else {
279
- router.push({
280
- name: 'Process',
281
- params: { projectId: project.project_id }
282
- })
283
- }
284
  }
285
 
286
  // 加载历史项目
@@ -301,6 +288,37 @@ const loadHistory = async () => {
301
 
302
  onMounted(() => {
303
  loadHistory()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  })
305
  </script>
306
 
@@ -333,7 +351,6 @@ onMounted(() => {
333
  left: 0;
334
  right: 0;
335
  bottom: 0;
336
- /* 40px x 40px 的正方形网格 */
337
  background-image:
338
  linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
339
  linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
@@ -347,59 +364,38 @@ onMounted(() => {
347
  left: 0;
348
  right: 0;
349
  bottom: 0;
350
- /* 四边渐变遮罩,让网格在边缘淡出 */
351
  background:
352
  linear-gradient(to right, rgba(255, 255, 255, 0.9) 0%, transparent 15%, transparent 85%, rgba(255, 255, 255, 0.9) 100%),
353
  linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, transparent 20%, transparent 80%, rgba(255, 255, 255, 0.8) 100%);
354
  pointer-events: none;
355
  }
356
 
357
- /* CTA 按钮 - 位置固定不变 */
358
- .cta-button {
359
  position: relative;
360
  z-index: 100;
361
- display: flex;
362
- justify-content: center;
363
- margin-bottom: 48px;
364
- cursor: pointer;
365
- }
366
-
367
- .cta-inner {
368
  display: flex;
369
  align-items: center;
370
- gap: 12px;
371
- padding: 14px 32px;
372
- background: #FFFFFF;
373
- border: 1px solid #E0E0E0;
374
- border-radius: 30px;
375
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* 加深阴影 */
376
  font-family: 'JetBrains Mono', 'SF Mono', monospace;
377
- font-size: 0.78rem;
378
- font-weight: 600; /* 加粗 */
379
- color: #1a1a1a;
380
- letter-spacing: 1.2px;
381
- transition: all 0.3s ease;
382
- }
383
-
384
- .cta-inner:hover {
385
- background: #FAFAFA;
386
- border-color: #CCCCCC;
387
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
388
- transform: translateY(-2px);
389
- }
390
-
391
- .cta-icon {
392
- color: #666; /* 加深颜色 */
393
- font-size: 1rem;
394
  }
395
 
396
- .cta-arrow {
397
- color: #666; /* 加深颜色 */
398
- transition: transform 0.3s ease;
 
 
399
  }
400
 
401
- .cta-arrow.expanded {
402
- transform: rotate(90deg);
 
 
 
 
403
  }
404
 
405
  /* 卡片容器 */
@@ -407,37 +403,32 @@ onMounted(() => {
407
  position: relative;
408
  display: flex;
409
  justify-content: center;
410
- align-items: flex-start; /* 从顶部开始排列 */
411
- min-height: 420px; /* 折叠时的最小高度 */
412
  padding: 0 40px;
413
- transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1); /* Match card duration */
414
  }
415
 
416
  .cards-container.expanded {
417
- min-height: 620px; /* 展开时增加高度,页面自动向下延长 */
418
  }
419
 
420
- /* 项目卡片 - 完全参照参考图 */
421
  .project-card {
422
  position: absolute;
423
- width: 280px; /* 调整宽度 */
424
  background: #FFFFFF;
425
- border: 1px solid #E5E7EB; /* border-gray-200 */
426
- border-radius: 0; /* 直角或极小圆角 */
427
- padding: 14px; /* 稍微减小内边距,让内容更紧凑 */
428
  cursor: pointer;
429
- /* Transitions are handled inline for transform/opacity, CSS for others */
430
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
431
- /* Remove transition property from here as it's overridden by inline styles for transform/opacity */
432
- /* Add specific transitions for border and shadow */
433
  transition: box-shadow 0.3s ease, border-color 0.3s ease, transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1);
434
  }
435
 
436
- /* 悬停效果 - 黑色粗边框,阴影加深 */
437
- /* Micro-interaction: Hover: border-black/40 shadow-lg */
438
  .project-card:hover {
439
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
440
- border-color: rgba(0, 0, 0, 0.4); /* border-black/40 */
441
  z-index: 1000 !important;
442
  }
443
 
@@ -452,13 +443,13 @@ onMounted(() => {
452
  align-items: center;
453
  margin-bottom: 12px;
454
  padding-bottom: 12px;
455
- border-bottom: 1px solid #F3F4F6; /* 增加分割线 */
456
  font-family: 'JetBrains Mono', 'SF Mono', monospace;
457
  font-size: 0.7rem;
458
  }
459
 
460
  .card-id {
461
- color: #6B7280; /* 加深灰色 */
462
  letter-spacing: 0.5px;
463
  font-weight: 500;
464
  }
@@ -477,61 +468,120 @@ onMounted(() => {
477
  font-size: 0.5rem;
478
  }
479
 
480
- .card-status.completed {
481
- color: #10B981; /* 更鲜艳的绿 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  }
483
 
484
- .card-status.processing {
485
- color: #F59E0B;
 
 
486
  }
487
 
488
- .card-status.ready {
489
- color: #3B82F6;
 
 
 
 
 
 
490
  }
491
 
492
- .card-status.failed {
493
- color: #EF4444;
 
 
494
  }
495
 
496
- .card-status.pending {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  color: #9CA3AF;
498
  }
499
 
500
- /* 卡片图片区域 */
501
- .card-image-wrapper {
502
- position: relative;
503
- width: 100%;
504
- height: 64px; /* 极度压扁,复刻参考图的宽银幕感 */
505
- margin-bottom: 12px;
506
- overflow: hidden;
507
- background: #f0f0f0;
508
  }
509
 
510
- .card-image {
511
- width: 100%;
512
- height: 100%;
513
- object-fit: cover;
514
- /* Micro-interaction: Default: opacity-80 grayscale */
515
- filter: grayscale(100%);
516
- opacity: 0.8;
517
- transition: all 500ms ease; /* Duration 500ms */
518
  }
519
 
520
- /* 悬停时图片变彩色 */
521
- /* Micro-interaction: Hover: opacity-100 grayscale-0 */
522
- .project-card:hover .card-image {
523
- filter: grayscale(0%);
524
- opacity: 1;
525
  }
526
 
527
- /* 角落装饰 - 只保留左上角,颜色加深 */
528
  .corner-mark.top-left-only {
529
  position: absolute;
530
  top: 6px;
531
  left: 6px;
532
  width: 8px;
533
  height: 8px;
534
- border-top: 1.5px solid rgba(0, 0, 0, 0.4); /* 加粗一点,颜色更深 */
535
  border-left: 1.5px solid rgba(0, 0, 0, 0.4);
536
  pointer-events: none;
537
  z-index: 10;
@@ -540,10 +590,10 @@ onMounted(() => {
540
  /* 卡片标题 */
541
  .card-title {
542
  font-family: 'Inter', -apple-system, sans-serif;
543
- font-size: 0.9rem; /* 稍微调小一点点 */
544
  font-weight: 700;
545
  color: #111827;
546
- margin: 0 0 6px 0; /* 减小间距 */
547
  line-height: 1.4;
548
  white-space: nowrap;
549
  overflow: hidden;
@@ -551,19 +601,18 @@ onMounted(() => {
551
  transition: color 0.3s ease;
552
  }
553
 
554
- /* 悬停时标题变蓝 - 参考图细节 */
555
  .project-card:hover .card-title {
556
- color: #2563EB; /* 蓝色 */
557
  }
558
 
559
  /* 卡片描述 */
560
  .card-desc {
561
  font-family: 'Inter', sans-serif;
562
  font-size: 0.75rem;
563
- color: #6B7280; /* 灰色 */
564
  margin: 0 0 16px 0;
565
  line-height: 1.5;
566
- height: 34px; /* 两行高度 */
567
  overflow: hidden;
568
  display: -webkit-box;
569
  -webkit-line-clamp: 2;
@@ -572,12 +621,12 @@ onMounted(() => {
572
 
573
  /* 卡片底部 */
574
  .card-footer {
575
- position: relative; /* For absolute positioning of the line */
576
  display: flex;
577
  justify-content: space-between;
578
  align-items: center;
579
  padding-top: 12px;
580
- border-top: 1px solid #F3F4F6; /* 增加分割线 */
581
  font-family: 'JetBrains Mono', monospace;
582
  font-size: 0.65rem;
583
  color: #9CA3AF;
@@ -585,7 +634,6 @@ onMounted(() => {
585
  }
586
 
587
  /* 底部装饰线 */
588
- /* Micro-interaction: Height 2px, bg-black, Default w-0, Hover w-full */
589
  .card-bottom-line {
590
  position: absolute;
591
  bottom: 0;
@@ -594,7 +642,7 @@ onMounted(() => {
594
  width: 0;
595
  background-color: #000;
596
  transition: width 0.5s cubic-bezier(0.23, 1, 0.32, 1);
597
- z-index: 20; /* 确保在内容之上 */
598
  }
599
 
600
  .project-card:hover .card-bottom-line {
 
1
  <template>
2
  <div
3
  class="history-database"
4
+ ref="historyContainer"
 
5
  >
6
  <!-- 背景装饰:技术网格线(使用CSS背景,固定间距正方形网格) -->
7
  <div class="tech-grid-bg">
 
9
  <div class="gradient-overlay"></div>
10
  </div>
11
 
12
+ <!-- 标题区域 -->
13
+ <div class="section-header">
14
+ <div class="section-line"></div>
15
+ <span class="section-title">HISTORY DATABASE</span>
16
+ <div class="section-line"></div>
 
 
 
 
 
17
  </div>
18
 
19
  <!-- 卡片容器 -->
 
28
  @mouseleave="hoveringCard = null"
29
  @click="navigateToProject(project)"
30
  >
31
+ <!-- 卡片头部:simulation_id和状态 -->
32
  <div class="card-header">
33
+ <span class="card-id">{{ formatSimulationId(project.simulation_id) }}</span>
34
  <span class="card-status" :class="getStatusClass(project.status)">
35
  <span class="status-dot">●</span> {{ getStatusText(project.status) }}
36
  </span>
37
  </div>
38
 
39
+ <!-- 文件列表区域 -->
40
+ <div class="card-files-wrapper">
41
  <!-- 角落装饰 - 取景框风格 -->
42
  <div class="corner-mark top-left-only"></div>
43
+
44
+ <!-- 文件列表 -->
45
+ <div class="files-list" v-if="project.files && project.files.length > 0">
46
+ <div
47
+ v-for="(file, fileIndex) in project.files.slice(0, 3)"
48
+ :key="fileIndex"
49
+ class="file-item"
50
+ >
51
+ <span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
52
+ <span class="file-name">{{ truncateFilename(file.filename, 20) }}</span>
53
+ </div>
54
+ </div>
55
+ <!-- 无文件时的占位 -->
56
+ <div class="files-empty" v-else>
57
+ <span class="empty-file-icon">◇</span>
58
+ <span class="empty-file-text">暂无文件</span>
59
+ </div>
60
  </div>
61
 
62
+ <!-- 卡片标题(使用模拟需求的前20字作为标题) -->
63
+ <h3 class="card-title">{{ getSimulationTitle(project.simulation_requirement) }}</h3>
64
 
65
+ <!-- 卡片描述(模拟需求完整展示) -->
66
  <p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
67
 
68
  <!-- 卡片底部 -->
69
  <div class="card-footer">
70
  <span class="card-date">{{ formatDate(project.created_at) }}</span>
71
+ <span class="card-rounds">{{ formatRounds(project) }}</span>
72
  </div>
73
 
74
  <!-- 底部装饰线 (hover时展开) -->
 
91
  </template>
92
 
93
  <script setup>
94
+ import { ref, onMounted, onUnmounted } from 'vue'
95
  import { useRouter } from 'vue-router'
96
  import { getSimulationHistory } from '../api/simulation'
97
 
 
102
  const loading = ref(true)
103
  const isExpanded = ref(false)
104
  const hoveringCard = ref(null)
105
+ const historyContainer = ref(null)
106
+ let observer = null
107
 
108
  // 卡片布局配置 - 调整为更宽的比例
109
  const CARDS_PER_ROW = 4
110
  const CARD_WIDTH = 280
111
  const CARD_HEIGHT = 280
112
  const CARD_GAP = 24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  // 获取卡片样式
115
  const getCardStyle = (index) => {
 
117
 
118
  if (isExpanded.value) {
119
  // 展开态:网格布局
 
120
  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'
121
 
122
  const col = index % CARDS_PER_ROW
 
126
  const currentRowStart = row * CARDS_PER_ROW
127
  const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
128
 
 
 
 
 
 
 
129
  const rowWidth = currentRowCards * CARD_WIDTH + (currentRowCards - 1) * CARD_GAP
 
 
 
 
 
 
 
 
130
 
131
  const startX = -(rowWidth / 2) + (CARD_WIDTH / 2)
 
 
 
 
132
  const colInRow = index % CARDS_PER_ROW
133
  const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
134
 
135
+ // 向下展开
 
136
  const y = row * (CARD_HEIGHT + CARD_GAP)
137
 
138
  return {
139
  transform: `translate(${x}px, ${y}px) rotate(0deg) scale(1)`,
140
+ zIndex: 100 + index,
141
  opacity: 1,
142
  transition: transition
143
  }
144
  } else {
145
  // 折叠态:扇形堆叠
 
146
  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'
147
 
148
+ const centerIndex = (total - 1) / 2
149
+ const offset = index - centerIndex
150
 
 
151
  const x = offset * 35
152
+ // 调整起始位置,更靠近标题
153
+ const y = 40 + Math.abs(offset) * 8
 
 
 
154
  const r = offset * 3
 
 
155
  const s = 0.95 - Math.abs(offset) * 0.05
156
 
157
  return {
158
  transform: `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`,
159
+ zIndex: 10 + index,
160
+ opacity: 1,
161
  transition: transition
162
  }
163
  }
 
206
  return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
207
  }
208
 
209
+ // 从模拟需求生成标题(取前20字)
210
+ const getSimulationTitle = (requirement) => {
211
+ if (!requirement) return '未命名模拟'
212
+ const title = requirement.slice(0, 20)
213
+ return requirement.length > 20 ? title + '...' : title
214
+ }
215
+
216
+ // 格式化 simulation_id 显示(截取前6位)
217
+ const formatSimulationId = (simulationId) => {
218
+ if (!simulationId) return 'SIM_UNKNOWN'
219
+ const prefix = simulationId.replace('sim_', '').slice(0, 6)
220
+ return `SIM_${prefix.toUpperCase()}`
221
+ }
222
+
223
+ // 格式化轮数显示(当前轮/总轮数)
224
+ const formatRounds = (simulation) => {
225
+ const current = simulation.current_round || 0
226
+ const total = simulation.total_rounds || 0
227
+ if (total === 0) return '未开始'
228
+ return `${current}/${total} 轮`
229
+ }
230
+
231
+ // 获取文件类型(用于样式)
232
+ const getFileType = (filename) => {
233
+ if (!filename) return 'other'
234
+ const ext = filename.split('.').pop()?.toLowerCase()
235
+ const typeMap = {
236
+ 'pdf': 'pdf',
237
+ 'doc': 'doc', 'docx': 'doc',
238
+ 'xls': 'xls', 'xlsx': 'xls', 'csv': 'xls',
239
+ 'ppt': 'ppt', 'pptx': 'ppt',
240
+ 'txt': 'txt', 'md': 'txt', 'json': 'code',
241
+ 'jpg': 'img', 'jpeg': 'img', 'png': 'img', 'gif': 'img',
242
+ 'zip': 'zip', 'rar': 'zip', '7z': 'zip'
243
+ }
244
+ return typeMap[ext] || 'other'
245
  }
246
 
247
+ // 获取文件类型标签文本
248
+ const getFileTypeLabel = (filename) => {
249
+ if (!filename) return 'FILE'
250
+ const ext = filename.split('.').pop()?.toUpperCase()
251
+ return ext || 'FILE'
252
  }
253
 
254
+ // 截断文件名(保留扩展名)
255
+ const truncateFilename = (filename, maxLength) => {
256
+ if (!filename) return '未知文件'
257
+ if (filename.length <= maxLength) return filename
258
+
259
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
260
+ const nameWithoutExt = filename.slice(0, filename.length - ext.length)
261
+ const truncatedName = nameWithoutExt.slice(0, maxLength - ext.length - 3) + '...'
262
+ return truncatedName + ext
263
  }
264
 
265
+ // 导航到模拟详情页
266
+ const navigateToProject = (simulation) => {
267
+ router.push({
268
+ name: 'SimulationRun',
269
+ params: { simulationId: simulation.simulation_id }
270
+ })
 
 
 
 
 
 
 
271
  }
272
 
273
  // 加载历史项目
 
288
 
289
  onMounted(() => {
290
  loadHistory()
291
+
292
+ // 使用 Intersection Observer 监听滚动,自动展开/收起卡片
293
+ observer = new IntersectionObserver(
294
+ (entries) => {
295
+ entries.forEach((entry) => {
296
+ if (entry.isIntersecting) {
297
+ isExpanded.value = true
298
+ } else {
299
+ isExpanded.value = false
300
+ }
301
+ })
302
+ },
303
+ {
304
+ threshold: [0.5],
305
+ rootMargin: '0px 0px -150px 0px'
306
+ }
307
+ )
308
+
309
+ // 等待 DOM 渲染后开始观察
310
+ setTimeout(() => {
311
+ if (historyContainer.value) {
312
+ observer.observe(historyContainer.value)
313
+ }
314
+ }, 100)
315
+ })
316
+
317
+ onUnmounted(() => {
318
+ if (observer) {
319
+ observer.disconnect()
320
+ observer = null
321
+ }
322
  })
323
  </script>
324
 
 
351
  left: 0;
352
  right: 0;
353
  bottom: 0;
 
354
  background-image:
355
  linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
356
  linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
 
364
  left: 0;
365
  right: 0;
366
  bottom: 0;
 
367
  background:
368
  linear-gradient(to right, rgba(255, 255, 255, 0.9) 0%, transparent 15%, transparent 85%, rgba(255, 255, 255, 0.9) 100%),
369
  linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, transparent 20%, transparent 80%, rgba(255, 255, 255, 0.8) 100%);
370
  pointer-events: none;
371
  }
372
 
373
+ /* 标题区域 */
374
+ .section-header {
375
  position: relative;
376
  z-index: 100;
 
 
 
 
 
 
 
377
  display: flex;
378
  align-items: center;
379
+ justify-content: center;
380
+ gap: 24px;
381
+ margin-bottom: 24px;
 
 
 
382
  font-family: 'JetBrains Mono', 'SF Mono', monospace;
383
+ padding: 0 40px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
 
386
+ .section-line {
387
+ flex: 1;
388
+ height: 1px;
389
+ background: linear-gradient(90deg, transparent, #E5E7EB, transparent);
390
+ max-width: 200px;
391
  }
392
 
393
+ .section-title {
394
+ font-size: 0.8rem;
395
+ font-weight: 500;
396
+ color: #9CA3AF;
397
+ letter-spacing: 3px;
398
+ text-transform: uppercase;
399
  }
400
 
401
  /* 卡片容器 */
 
403
  position: relative;
404
  display: flex;
405
  justify-content: center;
406
+ align-items: flex-start;
407
+ min-height: 420px;
408
  padding: 0 40px;
409
+ transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1);
410
  }
411
 
412
  .cards-container.expanded {
413
+ min-height: 620px;
414
  }
415
 
416
+ /* 项目卡片 */
417
  .project-card {
418
  position: absolute;
419
+ width: 280px;
420
  background: #FFFFFF;
421
+ border: 1px solid #E5E7EB;
422
+ border-radius: 0;
423
+ padding: 14px;
424
  cursor: pointer;
425
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 
 
 
426
  transition: box-shadow 0.3s ease, border-color 0.3s ease, transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1);
427
  }
428
 
 
 
429
  .project-card:hover {
430
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
431
+ border-color: rgba(0, 0, 0, 0.4);
432
  z-index: 1000 !important;
433
  }
434
 
 
443
  align-items: center;
444
  margin-bottom: 12px;
445
  padding-bottom: 12px;
446
+ border-bottom: 1px solid #F3F4F6;
447
  font-family: 'JetBrains Mono', 'SF Mono', monospace;
448
  font-size: 0.7rem;
449
  }
450
 
451
  .card-id {
452
+ color: #6B7280;
453
  letter-spacing: 0.5px;
454
  font-weight: 500;
455
  }
 
468
  font-size: 0.5rem;
469
  }
470
 
471
+ .card-status.completed { color: #10B981; }
472
+ .card-status.processing { color: #F59E0B; }
473
+ .card-status.ready { color: #3B82F6; }
474
+ .card-status.failed { color: #EF4444; }
475
+ .card-status.pending { color: #9CA3AF; }
476
+
477
+ /* 文件列表区域 */
478
+ .card-files-wrapper {
479
+ position: relative;
480
+ width: 100%;
481
+ min-height: 64px;
482
+ margin-bottom: 12px;
483
+ padding: 8px 10px;
484
+ background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f4 100%);
485
+ border-radius: 4px;
486
+ border: 1px solid #e8eaed;
487
  }
488
 
489
+ .files-list {
490
+ display: flex;
491
+ flex-direction: column;
492
+ gap: 6px;
493
  }
494
 
495
+ .file-item {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 8px;
499
+ padding: 4px 6px;
500
+ background: rgba(255, 255, 255, 0.7);
501
+ border-radius: 3px;
502
+ transition: all 0.2s ease;
503
  }
504
 
505
+ .file-item:hover {
506
+ background: rgba(255, 255, 255, 1);
507
+ transform: translateX(2px);
508
+ border-color: #e5e7eb;
509
  }
510
 
511
+ /* 简约文件标签样式 */
512
+ .file-tag {
513
+ display: inline-flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ height: 16px;
517
+ padding: 0 4px;
518
+ border-radius: 2px;
519
+ font-family: 'JetBrains Mono', monospace;
520
+ font-size: 0.55rem;
521
+ font-weight: 600;
522
+ line-height: 1;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.2px;
525
+ flex-shrink: 0;
526
+ min-width: 28px;
527
+ }
528
+
529
+ /* 低饱和度配色方案 - Morandi色系 */
530
+ .file-tag.pdf { background: #f2e6e6; color: #a65a5a; }
531
+ .file-tag.doc { background: #e6eff5; color: #5a7ea6; }
532
+ .file-tag.xls { background: #e6f2e8; color: #5aa668; }
533
+ .file-tag.ppt { background: #f5efe6; color: #a6815a; }
534
+ .file-tag.txt { background: #f0f0f0; color: #757575; }
535
+ .file-tag.code { background: #eae6f2; color: #815aa6; }
536
+ .file-tag.img { background: #e6f2f2; color: #5aa6a6; }
537
+ .file-tag.zip { background: #f2f0e6; color: #a69b5a; }
538
+ .file-tag.other { background: #f3f4f6; color: #6b7280; }
539
+
540
+ .file-name {
541
+ font-family: 'Inter', sans-serif;
542
+ font-size: 0.7rem;
543
+ color: #4b5563;
544
+ white-space: nowrap;
545
+ overflow: hidden;
546
+ text-overflow: ellipsis;
547
+ letter-spacing: 0.1px;
548
+ }
549
+
550
+ /* 无文件时的占位 */
551
+ .files-empty {
552
+ display: flex;
553
+ align-items: center;
554
+ justify-content: center;
555
+ gap: 8px;
556
+ height: 48px;
557
  color: #9CA3AF;
558
  }
559
 
560
+ .empty-file-icon {
561
+ font-size: 1rem;
562
+ opacity: 0.5;
 
 
 
 
 
563
  }
564
 
565
+ .empty-file-text {
566
+ font-family: 'JetBrains Mono', monospace;
567
+ font-size: 0.7rem;
568
+ letter-spacing: 0.5px;
 
 
 
 
569
  }
570
 
571
+ /* 悬停时文件区域效果 */
572
+ .project-card:hover .card-files-wrapper {
573
+ border-color: #d1d5db;
574
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
 
575
  }
576
 
577
+ /* 角落装饰 */
578
  .corner-mark.top-left-only {
579
  position: absolute;
580
  top: 6px;
581
  left: 6px;
582
  width: 8px;
583
  height: 8px;
584
+ border-top: 1.5px solid rgba(0, 0, 0, 0.4);
585
  border-left: 1.5px solid rgba(0, 0, 0, 0.4);
586
  pointer-events: none;
587
  z-index: 10;
 
590
  /* 卡片标题 */
591
  .card-title {
592
  font-family: 'Inter', -apple-system, sans-serif;
593
+ font-size: 0.9rem;
594
  font-weight: 700;
595
  color: #111827;
596
+ margin: 0 0 6px 0;
597
  line-height: 1.4;
598
  white-space: nowrap;
599
  overflow: hidden;
 
601
  transition: color 0.3s ease;
602
  }
603
 
 
604
  .project-card:hover .card-title {
605
+ color: #2563EB;
606
  }
607
 
608
  /* 卡片描述 */
609
  .card-desc {
610
  font-family: 'Inter', sans-serif;
611
  font-size: 0.75rem;
612
+ color: #6B7280;
613
  margin: 0 0 16px 0;
614
  line-height: 1.5;
615
+ height: 34px;
616
  overflow: hidden;
617
  display: -webkit-box;
618
  -webkit-line-clamp: 2;
 
621
 
622
  /* 卡片底部 */
623
  .card-footer {
624
+ position: relative;
625
  display: flex;
626
  justify-content: space-between;
627
  align-items: center;
628
  padding-top: 12px;
629
+ border-top: 1px solid #F3F4F6;
630
  font-family: 'JetBrains Mono', monospace;
631
  font-size: 0.65rem;
632
  color: #9CA3AF;
 
634
  }
635
 
636
  /* 底部装饰线 */
 
637
  .card-bottom-line {
638
  position: absolute;
639
  bottom: 0;
 
642
  width: 0;
643
  background-color: #000;
644
  transition: width 0.5s cubic-bezier(0.23, 1, 0.32, 1);
645
+ z-index: 20;
646
  }
647
 
648
  .project-card:hover .card-bottom-line {