refactor(simulation): enhance simulation data retrieval and project file handling
- 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.
This commit is contained in:
parent
9d5fad8854
commit
992f7d13c3
2 changed files with 279 additions and 224 deletions
|
|
@ -849,41 +849,48 @@ def get_simulation_history():
|
||||||
manager = SimulationManager()
|
manager = SimulationManager()
|
||||||
simulations = manager.list_simulations()[:limit]
|
simulations = manager.list_simulations()[:limit]
|
||||||
|
|
||||||
# 增强模拟数据,添加项目详情
|
# 增强模拟数据,只从 Simulation 文件读取
|
||||||
enriched_simulations = []
|
enriched_simulations = []
|
||||||
for sim in simulations:
|
for sim in simulations:
|
||||||
sim_dict = sim.to_dict()
|
sim_dict = sim.to_dict()
|
||||||
|
|
||||||
# 获取关联的项目信息
|
# 获取模拟配置信息(从 simulation_config.json 读取 simulation_requirement)
|
||||||
project = ProjectManager.get_project(sim.project_id)
|
|
||||||
if project:
|
|
||||||
sim_dict["project_name"] = project.name
|
|
||||||
sim_dict["simulation_requirement"] = project.simulation_requirement
|
|
||||||
else:
|
|
||||||
sim_dict["project_name"] = "未知项目"
|
|
||||||
sim_dict["simulation_requirement"] = ""
|
|
||||||
|
|
||||||
# 获取模拟配置信息
|
|
||||||
config = manager.get_simulation_config(sim.simulation_id)
|
config = manager.get_simulation_config(sim.simulation_id)
|
||||||
if config:
|
if config:
|
||||||
|
sim_dict["simulation_requirement"] = config.get("simulation_requirement", "")
|
||||||
time_config = config.get("time_config", {})
|
time_config = config.get("time_config", {})
|
||||||
sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0)
|
sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0)
|
||||||
sim_dict["total_rounds"] = int(
|
# 推荐轮数(后备值)
|
||||||
|
recommended_rounds = int(
|
||||||
time_config.get("total_simulation_hours", 0) * 60 /
|
time_config.get("total_simulation_hours", 0) * 60 /
|
||||||
max(time_config.get("minutes_per_round", 60), 1)
|
max(time_config.get("minutes_per_round", 60), 1)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
sim_dict["simulation_requirement"] = ""
|
||||||
sim_dict["total_simulation_hours"] = 0
|
sim_dict["total_simulation_hours"] = 0
|
||||||
sim_dict["total_rounds"] = 0
|
recommended_rounds = 0
|
||||||
|
|
||||||
# 获取运行状态
|
# 获取运行状态(从 run_state.json 读取用户设置的实际轮数)
|
||||||
run_state = SimulationRunner.get_run_state(sim.simulation_id)
|
run_state = SimulationRunner.get_run_state(sim.simulation_id)
|
||||||
if run_state:
|
if run_state:
|
||||||
sim_dict["current_round"] = run_state.current_round
|
sim_dict["current_round"] = run_state.current_round
|
||||||
sim_dict["runner_status"] = run_state.runner_status.value
|
sim_dict["runner_status"] = run_state.runner_status.value
|
||||||
|
# 使用用户设置的 total_rounds,若无则使用推荐轮数
|
||||||
|
sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds
|
||||||
else:
|
else:
|
||||||
sim_dict["current_round"] = 0
|
sim_dict["current_round"] = 0
|
||||||
sim_dict["runner_status"] = "idle"
|
sim_dict["runner_status"] = "idle"
|
||||||
|
sim_dict["total_rounds"] = recommended_rounds
|
||||||
|
|
||||||
|
# 获取关联项目的文件列表(最多3个)
|
||||||
|
project = ProjectManager.get_project(sim.project_id)
|
||||||
|
if project and hasattr(project, 'files') and project.files:
|
||||||
|
sim_dict["files"] = [
|
||||||
|
{"filename": f.get("filename", "未知文件")}
|
||||||
|
for f in project.files[:3]
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
sim_dict["files"] = []
|
||||||
|
|
||||||
# 添加版本号
|
# 添加版本号
|
||||||
sim_dict["version"] = "v1.0.2"
|
sim_dict["version"] = "v1.0.2"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="history-database"
|
class="history-database"
|
||||||
@mouseenter="handleMouseEnter"
|
ref="historyContainer"
|
||||||
@mouseleave="handleMouseLeave"
|
|
||||||
>
|
>
|
||||||
<!-- 背景装饰:技术网格线(使用CSS背景,固定间距正方形网格) -->
|
<!-- 背景装饰:技术网格线(使用CSS背景,固定间距正方形网格) -->
|
||||||
<div class="tech-grid-bg">
|
<div class="tech-grid-bg">
|
||||||
|
|
@ -10,16 +9,11 @@
|
||||||
<div class="gradient-overlay"></div>
|
<div class="gradient-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CTA 按钮 - 位置固定不变 -->
|
<!-- 标题区域 -->
|
||||||
<div
|
<div class="section-header">
|
||||||
class="cta-button"
|
<div class="section-line"></div>
|
||||||
@click="toggleExpand"
|
<span class="section-title">HISTORY DATABASE</span>
|
||||||
>
|
<div class="section-line"></div>
|
||||||
<div class="cta-inner">
|
|
||||||
<span class="cta-icon">◎</span>
|
|
||||||
<span class="cta-text">HISTORY DATABASE ({{ projects.length }})</span>
|
|
||||||
<span class="cta-arrow" :class="{ expanded: isExpanded }">→</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 卡片容器 -->
|
<!-- 卡片容器 -->
|
||||||
|
|
@ -34,39 +28,47 @@
|
||||||
@mouseleave="hoveringCard = null"
|
@mouseleave="hoveringCard = null"
|
||||||
@click="navigateToProject(project)"
|
@click="navigateToProject(project)"
|
||||||
>
|
>
|
||||||
<!-- 卡片头部:ID和状态 -->
|
<!-- 卡片头部:simulation_id和状态 -->
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-id">ID_{{ String(index + 1).padStart(3, '0') }}</span>
|
<span class="card-id">{{ formatSimulationId(project.simulation_id) }}</span>
|
||||||
<span class="card-status" :class="getStatusClass(project.status)">
|
<span class="card-status" :class="getStatusClass(project.status)">
|
||||||
<span class="status-dot">●</span> {{ getStatusText(project.status) }}
|
<span class="status-dot">●</span> {{ getStatusText(project.status) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 卡片图片区域(带角落装饰) -->
|
<!-- 文件列表区域 -->
|
||||||
<div class="card-image-wrapper">
|
<div class="card-files-wrapper">
|
||||||
<!-- 角落装饰 - 取景框风格 -->
|
<!-- 角落装饰 - 取景框风格 -->
|
||||||
<div class="corner-mark top-left-only"></div>
|
<div class="corner-mark top-left-only"></div>
|
||||||
|
|
||||||
<!-- 图片 -->
|
<!-- 文件列表 -->
|
||||||
<img
|
<div class="files-list" v-if="project.files && project.files.length > 0">
|
||||||
class="card-image"
|
<div
|
||||||
:src="getRandomImageUrl(project.simulation_id, index)"
|
v-for="(file, fileIndex) in project.files.slice(0, 3)"
|
||||||
:alt="project.project_name"
|
:key="fileIndex"
|
||||||
loading="lazy"
|
class="file-item"
|
||||||
@error="handleImageError($event, index)"
|
>
|
||||||
/>
|
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
|
||||||
|
<span class="file-name">{{ truncateFilename(file.filename, 20) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 无文件时的占位 -->
|
||||||
|
<div class="files-empty" v-else>
|
||||||
|
<span class="empty-file-icon">◇</span>
|
||||||
|
<span class="empty-file-text">暂无文件</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 卡片标题 -->
|
<!-- 卡片标题(使用模拟需求的前20字作为标题) -->
|
||||||
<h3 class="card-title">{{ project.project_name || 'Unnamed Project' }}</h3>
|
<h3 class="card-title">{{ getSimulationTitle(project.simulation_requirement) }}</h3>
|
||||||
|
|
||||||
<!-- 卡片描述 -->
|
<!-- 卡片描述(模拟需求完整展示) -->
|
||||||
<p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
|
<p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
|
||||||
|
|
||||||
<!-- 卡片底部 -->
|
<!-- 卡片底部 -->
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span class="card-date">{{ formatDate(project.created_at) }}</span>
|
<span class="card-date">{{ formatDate(project.created_at) }}</span>
|
||||||
<span class="card-version">{{ project.version || 'v1.0.2' }}</span>
|
<span class="card-rounds">{{ formatRounds(project) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部装饰线 (hover时展开) -->
|
<!-- 底部装饰线 (hover时展开) -->
|
||||||
|
|
@ -89,7 +91,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getSimulationHistory } from '../api/simulation'
|
import { getSimulationHistory } from '../api/simulation'
|
||||||
|
|
||||||
|
|
@ -100,38 +102,14 @@ const projects = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const isExpanded = ref(false)
|
const isExpanded = ref(false)
|
||||||
const hoveringCard = ref(null)
|
const hoveringCard = ref(null)
|
||||||
const imageErrors = ref({}) // 追踪图片加载错误
|
const historyContainer = ref(null)
|
||||||
|
let observer = null
|
||||||
|
|
||||||
// 卡片布局配置 - 调整为更宽的比例
|
// 卡片布局配置 - 调整为更宽的比例
|
||||||
const CARDS_PER_ROW = 4
|
const CARDS_PER_ROW = 4
|
||||||
const CARD_WIDTH = 280
|
const CARD_WIDTH = 280
|
||||||
const CARD_HEIGHT = 280
|
const CARD_HEIGHT = 280
|
||||||
const CARD_GAP = 24
|
const CARD_GAP = 24
|
||||||
const EXPANDED_ROW_HEIGHT = 230 // 行高 230px (Requirements)
|
|
||||||
const EXPANDED_COL_WIDTH = 280 // 列宽 (Requirements spacing 280px)
|
|
||||||
|
|
||||||
// 随机图片服务配置(中国可访问)
|
|
||||||
const IMAGE_SERVICES = {
|
|
||||||
// Lorem Picsum - 国际服务,中国大部分地区可访问
|
|
||||||
picsum: (seed, width, height) =>
|
|
||||||
`https://picsum.photos/seed/${seed}/${width}/${height}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成随机图片URL - 调整图片比例为超扁平 (280x64)
|
|
||||||
const getRandomImageUrl = (simulationId, index) => {
|
|
||||||
if (imageErrors.value[index]) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const seed = simulationId || `project-${index}`
|
|
||||||
// 宽280,高64,约4.4:1比例,极度扁平
|
|
||||||
return IMAGE_SERVICES.picsum(seed, 280, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理图片加载错误
|
|
||||||
const handleImageError = (event, index) => {
|
|
||||||
imageErrors.value[index] = true
|
|
||||||
event.target.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取卡片样式
|
// 获取卡片样式
|
||||||
const getCardStyle = (index) => {
|
const getCardStyle = (index) => {
|
||||||
|
|
@ -139,7 +117,6 @@ const getCardStyle = (index) => {
|
||||||
|
|
||||||
if (isExpanded.value) {
|
if (isExpanded.value) {
|
||||||
// 展开态:网格布局
|
// 展开态:网格布局
|
||||||
// 物理特性:Easing: cubic-bezier(0.23, 1, 0.32, 1), Duration: 700ms
|
|
||||||
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 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 col = index % CARDS_PER_ROW
|
||||||
|
|
@ -149,64 +126,38 @@ const getCardStyle = (index) => {
|
||||||
const currentRowStart = row * CARDS_PER_ROW
|
const currentRowStart = row * CARDS_PER_ROW
|
||||||
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
|
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
|
||||||
|
|
||||||
// 水平居中偏移
|
|
||||||
// 间距 280px (Based on CARD_WIDTH being 280px. Assuming standard grid gap is included or minimal)
|
|
||||||
// Using CARD_WIDTH + CARD_GAP for spacing calculation to be safe, but requirements said "spacing 280px".
|
|
||||||
// If spacing means column width, then grid width is ColWidth * count.
|
|
||||||
// Let's stick to the previous logic but ensure center alignment.
|
|
||||||
|
|
||||||
const rowWidth = currentRowCards * CARD_WIDTH + (currentRowCards - 1) * CARD_GAP
|
const rowWidth = currentRowCards * CARD_WIDTH + (currentRowCards - 1) * CARD_GAP
|
||||||
const containerWidth = CARDS_PER_ROW * CARD_WIDTH + (CARDS_PER_ROW - 1) * CARD_GAP // Full width of a complete row
|
|
||||||
|
|
||||||
// Calculate offset to center the current row relative to the full container width
|
|
||||||
// Actually, the requirements say "translateX: based on colIndex, centered per row"
|
|
||||||
// So for a row with 3 items, they should be centered.
|
|
||||||
// The visual center is 0.
|
|
||||||
// Leftmost item x = - (rowWidth / 2) + (CARD_WIDTH / 2)
|
|
||||||
// Next item x += CARD_WIDTH + CARD_GAP
|
|
||||||
|
|
||||||
const startX = -(rowWidth / 2) + (CARD_WIDTH / 2)
|
const startX = -(rowWidth / 2) + (CARD_WIDTH / 2)
|
||||||
const offsetX = (col % CARDS_PER_ROW) * (CARD_WIDTH + CARD_GAP) // offset within the row
|
|
||||||
|
|
||||||
// Wait, the calculation needs to be based on the column index WITHIN the current row (0 to currentRowCards-1)
|
|
||||||
// Since col = index % 4, it resets for each row.
|
|
||||||
const colInRow = index % CARDS_PER_ROW
|
const colInRow = index % CARDS_PER_ROW
|
||||||
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
|
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
|
||||||
|
|
||||||
// translateY: 向下展开逻辑. 行高 300px (包含卡片高度280+间距).
|
// 向下展开
|
||||||
// Row 0 在顶部,后续行向下排列
|
|
||||||
const y = row * (CARD_HEIGHT + CARD_GAP)
|
const y = row * (CARD_HEIGHT + CARD_GAP)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `translate(${x}px, ${y}px) rotate(0deg) scale(1)`,
|
transform: `translate(${x}px, ${y}px) rotate(0deg) scale(1)`,
|
||||||
zIndex: 100 + index, // Requirements: 100 + gridIndex
|
zIndex: 100 + index,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: transition
|
transition: transition
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 折叠态:扇形堆叠
|
// 折叠态:扇形堆叠
|
||||||
// 物理特性:Easing: cubic-bezier(0.23, 1, 0.32, 1), Duration: 700ms
|
|
||||||
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 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 // Center index (float)
|
const centerIndex = (total - 1) / 2
|
||||||
const offset = index - centerIndex // Offset from center
|
const offset = index - centerIndex
|
||||||
|
|
||||||
// translateX: offset * 35px
|
|
||||||
const x = offset * 35
|
const x = offset * 35
|
||||||
|
// 调整起始位置,更靠近标题
|
||||||
// translateY: 130px + Math.abs(offset) * 8px
|
const y = 40 + Math.abs(offset) * 8
|
||||||
const y = 130 + Math.abs(offset) * 8
|
|
||||||
|
|
||||||
// rotate: offset * 3deg
|
|
||||||
const r = offset * 3
|
const r = offset * 3
|
||||||
|
|
||||||
// scale: 0.95 - Math.abs(offset) * 0.05
|
|
||||||
const s = 0.95 - Math.abs(offset) * 0.05
|
const s = 0.95 - Math.abs(offset) * 0.05
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`,
|
transform: `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`,
|
||||||
zIndex: 10 + index, // Requirements: 10 + index
|
zIndex: 10 + index,
|
||||||
opacity: 1, // Collapsed cards are usually fully opaque in the stack
|
opacity: 1,
|
||||||
transition: transition
|
transition: transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,32 +206,68 @@ const truncateText = (text, maxLength) => {
|
||||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 从模拟需求生成标题(取前20字)
|
||||||
const handleMouseEnter = () => {
|
const getSimulationTitle = (requirement) => {
|
||||||
isExpanded.value = true
|
if (!requirement) return '未命名模拟'
|
||||||
|
const title = requirement.slice(0, 20)
|
||||||
|
return requirement.length > 20 ? title + '...' : title
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
// 格式化 simulation_id 显示(截取前6位)
|
||||||
isExpanded.value = false
|
const formatSimulationId = (simulationId) => {
|
||||||
|
if (!simulationId) return 'SIM_UNKNOWN'
|
||||||
|
const prefix = simulationId.replace('sim_', '').slice(0, 6)
|
||||||
|
return `SIM_${prefix.toUpperCase()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleExpand = () => {
|
// 格式化轮数显示(当前轮/总轮数)
|
||||||
isExpanded.value = !isExpanded.value
|
const formatRounds = (simulation) => {
|
||||||
|
const current = simulation.current_round || 0
|
||||||
|
const total = simulation.total_rounds || 0
|
||||||
|
if (total === 0) return '未开始'
|
||||||
|
return `${current}/${total} 轮`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导航到项目
|
// 获取文件类型(用于样式)
|
||||||
const navigateToProject = (project) => {
|
const getFileType = (filename) => {
|
||||||
if (project.status === 'completed' || project.status === 'running' || project.status === 'ready') {
|
if (!filename) return 'other'
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase()
|
||||||
|
const typeMap = {
|
||||||
|
'pdf': 'pdf',
|
||||||
|
'doc': 'doc', 'docx': 'doc',
|
||||||
|
'xls': 'xls', 'xlsx': 'xls', 'csv': 'xls',
|
||||||
|
'ppt': 'ppt', 'pptx': 'ppt',
|
||||||
|
'txt': 'txt', 'md': 'txt', 'json': 'code',
|
||||||
|
'jpg': 'img', 'jpeg': 'img', 'png': 'img', 'gif': 'img',
|
||||||
|
'zip': 'zip', 'rar': 'zip', '7z': 'zip'
|
||||||
|
}
|
||||||
|
return typeMap[ext] || 'other'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件类型标签文本
|
||||||
|
const getFileTypeLabel = (filename) => {
|
||||||
|
if (!filename) return 'FILE'
|
||||||
|
const ext = filename.split('.').pop()?.toUpperCase()
|
||||||
|
return ext || 'FILE'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截断文件名(保留扩展名)
|
||||||
|
const truncateFilename = (filename, maxLength) => {
|
||||||
|
if (!filename) return '未知文件'
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到模拟详情页
|
||||||
|
const navigateToProject = (simulation) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'SimulationRun',
|
name: 'SimulationRun',
|
||||||
params: { simulationId: project.simulation_id }
|
params: { simulationId: simulation.simulation_id }
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
router.push({
|
|
||||||
name: 'Process',
|
|
||||||
params: { projectId: project.project_id }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载历史项目
|
// 加载历史项目
|
||||||
|
|
@ -301,6 +288,37 @@ const loadHistory = async () => {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadHistory()
|
loadHistory()
|
||||||
|
|
||||||
|
// 使用 Intersection Observer 监听滚动,自动展开/收起卡片
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
isExpanded.value = true
|
||||||
|
} else {
|
||||||
|
isExpanded.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: [0.5],
|
||||||
|
rootMargin: '0px 0px -150px 0px'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 等待 DOM 渲染后开始观察
|
||||||
|
setTimeout(() => {
|
||||||
|
if (historyContainer.value) {
|
||||||
|
observer.observe(historyContainer.value)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -333,7 +351,6 @@ onMounted(() => {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
/* 40px x 40px 的正方形网格 */
|
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||||
|
|
@ -347,59 +364,38 @@ onMounted(() => {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
/* 四边渐变遮罩,让网格在边缘淡出 */
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(to right, rgba(255, 255, 255, 0.9) 0%, transparent 15%, transparent 85%, rgba(255, 255, 255, 0.9) 100%),
|
linear-gradient(to right, rgba(255, 255, 255, 0.9) 0%, transparent 15%, transparent 85%, rgba(255, 255, 255, 0.9) 100%),
|
||||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, transparent 20%, transparent 80%, rgba(255, 255, 255, 0.8) 100%);
|
linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, transparent 20%, transparent 80%, rgba(255, 255, 255, 0.8) 100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CTA 按钮 - 位置固定不变 */
|
/* 标题区域 */
|
||||||
.cta-button {
|
.section-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-inner {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
justify-content: center;
|
||||||
padding: 14px 32px;
|
gap: 24px;
|
||||||
background: #FFFFFF;
|
margin-bottom: 24px;
|
||||||
border: 1px solid #E0E0E0;
|
|
||||||
border-radius: 30px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* 加深阴影 */
|
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||||
font-size: 0.78rem;
|
padding: 0 40px;
|
||||||
font-weight: 600; /* 加粗 */
|
|
||||||
color: #1a1a1a;
|
|
||||||
letter-spacing: 1.2px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-inner:hover {
|
.section-line {
|
||||||
background: #FAFAFA;
|
flex: 1;
|
||||||
border-color: #CCCCCC;
|
height: 1px;
|
||||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
background: linear-gradient(90deg, transparent, #E5E7EB, transparent);
|
||||||
transform: translateY(-2px);
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-icon {
|
.section-title {
|
||||||
color: #666; /* 加深颜色 */
|
font-size: 0.8rem;
|
||||||
font-size: 1rem;
|
font-weight: 500;
|
||||||
}
|
color: #9CA3AF;
|
||||||
|
letter-spacing: 3px;
|
||||||
.cta-arrow {
|
text-transform: uppercase;
|
||||||
color: #666; /* 加深颜色 */
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-arrow.expanded {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 卡片容器 */
|
/* 卡片容器 */
|
||||||
|
|
@ -407,37 +403,32 @@ onMounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start; /* 从顶部开始排列 */
|
align-items: flex-start;
|
||||||
min-height: 420px; /* 折叠时的最小高度 */
|
min-height: 420px;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1); /* Match card duration */
|
transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-container.expanded {
|
.cards-container.expanded {
|
||||||
min-height: 620px; /* 展开时增加高度,页面自动向下延长 */
|
min-height: 620px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 项目卡片 - 完全参照参考图 */
|
/* 项目卡片 */
|
||||||
.project-card {
|
.project-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 280px; /* 调整宽度 */
|
width: 280px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border: 1px solid #E5E7EB; /* border-gray-200 */
|
border: 1px solid #E5E7EB;
|
||||||
border-radius: 0; /* 直角或极小圆角 */
|
border-radius: 0;
|
||||||
padding: 14px; /* 稍微减小内边距,让内容更紧凑 */
|
padding: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* Transitions are handled inline for transform/opacity, CSS for others */
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
|
|
||||||
/* Remove transition property from here as it's overridden by inline styles for transform/opacity */
|
|
||||||
/* Add specific transitions for border and shadow */
|
|
||||||
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬停效果 - 黑色粗边框,阴影加深 */
|
|
||||||
/* Micro-interaction: Hover: border-black/40 shadow-lg */
|
|
||||||
.project-card:hover {
|
.project-card:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
border-color: rgba(0, 0, 0, 0.4); /* border-black/40 */
|
border-color: rgba(0, 0, 0, 0.4);
|
||||||
z-index: 1000 !important;
|
z-index: 1000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,13 +443,13 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid #F3F4F6; /* 增加分割线 */
|
border-bottom: 1px solid #F3F4F6;
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-id {
|
.card-id {
|
||||||
color: #6B7280; /* 加深灰色 */
|
color: #6B7280;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
@ -477,61 +468,120 @@ onMounted(() => {
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status.completed {
|
.card-status.completed { color: #10B981; }
|
||||||
color: #10B981; /* 更鲜艳的绿 */
|
.card-status.processing { color: #F59E0B; }
|
||||||
|
.card-status.ready { color: #3B82F6; }
|
||||||
|
.card-status.failed { color: #EF4444; }
|
||||||
|
.card-status.pending { color: #9CA3AF; }
|
||||||
|
|
||||||
|
/* 文件列表区域 */
|
||||||
|
.card-files-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 64px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f4 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status.processing {
|
.files-list {
|
||||||
color: #F59E0B;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status.ready {
|
.file-item {
|
||||||
color: #3B82F6;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status.failed {
|
.file-item:hover {
|
||||||
color: #EF4444;
|
background: rgba(255, 255, 255, 1);
|
||||||
|
transform: translateX(2px);
|
||||||
|
border-color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-status.pending {
|
/* 简约文件标签样式 */
|
||||||
|
.file-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 低饱和度配色方案 - Morandi色系 */
|
||||||
|
.file-tag.pdf { background: #f2e6e6; color: #a65a5a; }
|
||||||
|
.file-tag.doc { background: #e6eff5; color: #5a7ea6; }
|
||||||
|
.file-tag.xls { background: #e6f2e8; color: #5aa668; }
|
||||||
|
.file-tag.ppt { background: #f5efe6; color: #a6815a; }
|
||||||
|
.file-tag.txt { background: #f0f0f0; color: #757575; }
|
||||||
|
.file-tag.code { background: #eae6f2; color: #815aa6; }
|
||||||
|
.file-tag.img { background: #e6f2f2; color: #5aa6a6; }
|
||||||
|
.file-tag.zip { background: #f2f0e6; color: #a69b5a; }
|
||||||
|
.file-tag.other { background: #f3f4f6; color: #6b7280; }
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #4b5563;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无文件时的占位 */
|
||||||
|
.files-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 48px;
|
||||||
color: #9CA3AF;
|
color: #9CA3AF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 卡片图片区域 */
|
.empty-file-icon {
|
||||||
.card-image-wrapper {
|
font-size: 1rem;
|
||||||
position: relative;
|
opacity: 0.5;
|
||||||
width: 100%;
|
|
||||||
height: 64px; /* 极度压扁,复刻参考图的宽银幕感 */
|
|
||||||
margin-bottom: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-image {
|
.empty-file-text {
|
||||||
width: 100%;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
height: 100%;
|
font-size: 0.7rem;
|
||||||
object-fit: cover;
|
letter-spacing: 0.5px;
|
||||||
/* Micro-interaction: Default: opacity-80 grayscale */
|
|
||||||
filter: grayscale(100%);
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: all 500ms ease; /* Duration 500ms */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬停时图片变彩色 */
|
/* 悬停时文件区域效果 */
|
||||||
/* Micro-interaction: Hover: opacity-100 grayscale-0 */
|
.project-card:hover .card-files-wrapper {
|
||||||
.project-card:hover .card-image {
|
border-color: #d1d5db;
|
||||||
filter: grayscale(0%);
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 角落装饰 - 只保留左上角,颜色加深 */
|
/* 角落装饰 */
|
||||||
.corner-mark.top-left-only {
|
.corner-mark.top-left-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
left: 6px;
|
left: 6px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-top: 1.5px solid rgba(0, 0, 0, 0.4); /* 加粗一点,颜色更深 */
|
border-top: 1.5px solid rgba(0, 0, 0, 0.4);
|
||||||
border-left: 1.5px solid rgba(0, 0, 0, 0.4);
|
border-left: 1.5px solid rgba(0, 0, 0, 0.4);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
@ -540,10 +590,10 @@ onMounted(() => {
|
||||||
/* 卡片标题 */
|
/* 卡片标题 */
|
||||||
.card-title {
|
.card-title {
|
||||||
font-family: 'Inter', -apple-system, sans-serif;
|
font-family: 'Inter', -apple-system, sans-serif;
|
||||||
font-size: 0.9rem; /* 稍微调小一点点 */
|
font-size: 0.9rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
margin: 0 0 6px 0; /* 减小间距 */
|
margin: 0 0 6px 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -551,19 +601,18 @@ onMounted(() => {
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬停时标题变蓝 - 参考图细节 */
|
|
||||||
.project-card:hover .card-title {
|
.project-card:hover .card-title {
|
||||||
color: #2563EB; /* 蓝色 */
|
color: #2563EB;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 卡片描述 */
|
/* 卡片描述 */
|
||||||
.card-desc {
|
.card-desc {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #6B7280; /* 灰色 */
|
color: #6B7280;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
height: 34px; /* 两行高度 */
|
height: 34px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
|
@ -572,12 +621,12 @@ onMounted(() => {
|
||||||
|
|
||||||
/* 卡片底部 */
|
/* 卡片底部 */
|
||||||
.card-footer {
|
.card-footer {
|
||||||
position: relative; /* For absolute positioning of the line */
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid #F3F4F6; /* 增加分割线 */
|
border-top: 1px solid #F3F4F6;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: #9CA3AF;
|
color: #9CA3AF;
|
||||||
|
|
@ -585,7 +634,6 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部装饰线 */
|
/* 底部装饰线 */
|
||||||
/* Micro-interaction: Height 2px, bg-black, Default w-0, Hover w-full */
|
|
||||||
.card-bottom-line {
|
.card-bottom-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -594,7 +642,7 @@ onMounted(() => {
|
||||||
width: 0;
|
width: 0;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
transition: width 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: width 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
z-index: 20; /* 确保在内容之上 */
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card:hover .card-bottom-line {
|
.project-card:hover .card-bottom-line {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue