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:
666ghj 2026-01-07 18:54:33 +08:00
parent 9d5fad8854
commit 992f7d13c3
2 changed files with 279 additions and 224 deletions

View file

@ -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"

View file

@ -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}`
// 280644.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'
router.push({ const ext = filename.split('.').pop()?.toLowerCase()
name: 'SimulationRun', const typeMap = {
params: { simulationId: project.simulation_id } 'pdf': 'pdf',
}) 'doc': 'doc', 'docx': 'doc',
} else { 'xls': 'xls', 'xlsx': 'xls', 'csv': 'xls',
router.push({ 'ppt': 'ppt', 'pptx': 'ppt',
name: 'Process', 'txt': 'txt', 'md': 'txt', 'json': 'code',
params: { projectId: project.project_id } '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({
name: 'SimulationRun',
params: { simulationId: simulation.simulation_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 {