library-picturebook-activity/frontend/src/views/workbench/ai-3d/Index.vue

1699 lines
37 KiB
Vue
Raw Normal View History

2026-01-13 14:01:17 +08:00
<template>
<div class="ai-3d-container">
2026-01-13 16:41:12 +08:00
<!-- Animated Background -->
<div class="bg-animation">
<div class="bg-gradient bg-gradient-1"></div>
<div class="bg-gradient bg-gradient-2"></div>
<div class="bg-gradient bg-gradient-3"></div>
</div>
2026-01-13 14:01:17 +08:00
<!-- 左侧生成栏 -->
<div class="left-panel">
2026-01-13 16:41:12 +08:00
<!-- Header with Logo -->
<div class="panel-logo">
2026-01-14 14:29:16 +08:00
<a-button type="text" class="back-btn" @click="handleBack">
<template #icon><ArrowLeftOutlined /></template>
</a-button>
2026-01-13 16:41:12 +08:00
<div class="logo-ring">
<div class="logo-icon">
<ThunderboltOutlined />
</div>
</div>
<div class="logo-text">
<h3>AI 3D Studio</h3>
<p>智能建模工作室</p>
</div>
</div>
2026-01-13 14:01:17 +08:00
<div class="panel-header">
<a-segmented
v-model:value="inputType"
:options="inputTypeOptions"
block
/>
</div>
<div class="panel-content">
<!-- 文生3D输入 -->
<div v-if="inputType === 'text'" class="text-input-section">
2026-01-13 16:41:12 +08:00
<div class="input-label">
<span class="label-icon"></span>
<span>创意描述</span>
</div>
2026-01-13 14:01:17 +08:00
<div class="input-hint">
2026-01-13 16:41:12 +08:00
用文字描述你的想法AI 将为你生成精美的 3D 模型
2026-01-13 14:01:17 +08:00
</div>
<a-textarea
v-model:value="textContent"
2026-01-13 16:41:12 +08:00
placeholder="例如:一只卡通风格的橙色小猫,蓝色的大眼睛,尾巴卷曲..."
2026-01-13 14:01:17 +08:00
:rows="6"
:maxlength="150"
show-count
class="text-input"
/>
2026-01-13 16:41:12 +08:00
<div class="sample-section">
<div class="sample-header">
<span class="sample-title">灵感推荐</span>
<ReloadOutlined class="refresh-icon" @click="refreshSamples" />
</div>
<div class="sample-prompts">
<span
v-for="(sample, index) in currentSamples"
:key="index"
class="sample-tag"
@click="applySample(sample)"
>
{{ sample }}
</span>
</div>
2026-01-13 14:01:17 +08:00
</div>
</div>
<!-- 图生3D上传 -->
<div v-else class="image-input-section">
2026-01-13 16:41:12 +08:00
<div class="input-label">
<span class="label-icon">🖼</span>
<span>参考图片</span>
</div>
2026-01-13 14:01:17 +08:00
<div class="input-hint">
2026-01-13 16:41:12 +08:00
上传参考图片AI 将智能识别并生成 3D 模型
2026-01-13 14:01:17 +08:00
</div>
<a-upload-dragger
v-model:file-list="imageFileList"
:before-upload="handleBeforeUpload"
:max-count="1"
accept="image/*"
class="image-upload"
>
2026-01-13 16:41:12 +08:00
<p class="upload-icon">
2026-01-13 14:01:17 +08:00
<PictureOutlined />
</p>
2026-01-13 16:41:12 +08:00
<p class="upload-text">点击或拖拽图片到此处</p>
<p class="upload-hint">支持 JPGPNG 格式最大 10MB</p>
2026-01-13 14:01:17 +08:00
</a-upload-dragger>
</div>
</div>
<div class="panel-footer">
2026-01-13 16:41:12 +08:00
<button
class="generate-btn"
:class="{ 'is-loading': generating, 'is-disabled': !canGenerate }"
2026-01-13 14:01:17 +08:00
:disabled="!canGenerate"
@click="handleGenerate"
>
2026-01-13 16:41:12 +08:00
<span class="btn-bg"></span>
<span class="btn-content">
<LoadingOutlined v-if="generating" class="spin-icon" />
<ThunderboltOutlined v-else />
2026-01-14 10:06:08 +08:00
<span>{{ generating ? "生成中..." : "立即生成" }}</span>
2026-01-13 16:41:12 +08:00
</span>
</button>
2026-01-13 14:01:17 +08:00
</div>
</div>
<!-- 右侧内容区 -->
<div class="right-panel">
<!-- 介绍区 -->
<div class="intro-section">
2026-01-13 16:41:12 +08:00
<div class="intro-badge">
<span class="badge-dot"></span>
<span>AI Powered</span>
</div>
2026-01-14 10:06:08 +08:00
<h1 class="intro-title">用一句话一张图<br />创造你的 3D 世界</h1>
2026-01-13 16:41:12 +08:00
<p class="intro-desc">
借助先进的 AI 技术将文字描述或图片瞬间转化为专业级 3D 模型
</p>
2026-01-13 14:01:17 +08:00
<div class="intro-features">
2026-01-13 16:41:12 +08:00
<div class="feature-card">
<div class="feature-icon gradient-1">
<span></span>
</div>
<div class="feature-info">
<h3>AI 智能建模</h3>
<p>输入文字或图片自动生成 3D 模型</p>
</div>
2026-01-13 14:01:17 +08:00
</div>
2026-01-13 16:41:12 +08:00
<div class="feature-card">
<div class="feature-icon gradient-2">
<span>👁</span>
</div>
<div class="feature-info">
<h3>实时预览</h3>
<p>支持旋转缩放全方位查看细节</p>
</div>
2026-01-13 14:01:17 +08:00
</div>
2026-01-13 16:41:12 +08:00
<div class="feature-card">
<div class="feature-icon gradient-3">
<span>📁</span>
</div>
<div class="feature-info">
<h3>作品管理</h3>
<p>自动保存至个人作品库随时访问</p>
</div>
2026-01-13 14:01:17 +08:00
</div>
2026-01-13 16:41:12 +08:00
<div class="feature-card">
<div class="feature-icon gradient-4">
<span>🔄</span>
</div>
<div class="feature-info">
<h3>迭代优化</h3>
<p>基于已有作品再次生成与优化</p>
</div>
2026-01-13 14:01:17 +08:00
</div>
</div>
</div>
<!-- 创作历史区 -->
<div class="history-section">
<div class="history-header">
2026-01-13 16:41:12 +08:00
<div class="header-left">
<h2 class="history-title">创作历史</h2>
2026-01-14 14:29:16 +08:00
<span class="history-count"
>{{ allHistoryTotal || historyList.length }} 个作品</span
>
2026-01-13 16:41:12 +08:00
</div>
2026-01-14 14:29:16 +08:00
<a class="view-all" @click="goToHistory">
2026-01-13 16:41:12 +08:00
查看全部
<ArrowRightOutlined />
</a>
2026-01-13 14:01:17 +08:00
</div>
<div v-if="historyLoading" class="history-loading">
2026-01-13 16:41:12 +08:00
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<p>加载中...</p>
2026-01-13 14:01:17 +08:00
</div>
<div v-else-if="historyList.length === 0" class="history-empty">
2026-01-13 16:41:12 +08:00
<div class="empty-icon">
<FileImageOutlined />
</div>
<p class="empty-title">还没有创作记录</p>
<p class="empty-text">开始你的第一次 3D 创作之旅吧</p>
2026-01-13 14:01:17 +08:00
</div>
2026-01-14 14:29:16 +08:00
<div v-else ref="historyGridRef" class="history-grid">
2026-01-13 14:01:17 +08:00
<div
2026-01-14 14:29:16 +08:00
v-for="task in displayedHistoryList"
2026-01-13 14:01:17 +08:00
:key="task.id"
class="history-card"
@click="handleViewTask(task)"
>
2026-01-13 16:41:12 +08:00
<div class="card-glow"></div>
2026-01-13 14:01:17 +08:00
<div class="card-preview">
<img
v-if="task.status === 'completed' && task.previewUrl"
:src="getPreviewUrl(task)"
alt="预览"
class="preview-image"
2026-01-14 10:06:08 +08:00
@error="handleImageError"
@load="handleImageLoad"
2026-01-13 14:01:17 +08:00
/>
<div
2026-01-14 10:06:08 +08:00
v-else-if="
task.status === 'processing' || task.status === 'pending'
"
2026-01-13 14:01:17 +08:00
class="preview-loading"
>
2026-01-13 16:41:12 +08:00
<div class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
<span class="loading-text">生成中</span>
2026-01-13 14:01:17 +08:00
</div>
<div
2026-01-14 10:06:08 +08:00
v-else-if="
task.status === 'failed' || task.status === 'timeout'
"
2026-01-13 14:01:17 +08:00
class="preview-failed"
>
2026-01-14 14:29:16 +08:00
<div class="failed-icon">
<CloseOutlined />
</div>
2026-01-13 14:01:17 +08:00
</div>
<div v-else class="preview-placeholder">
<FileImageOutlined />
</div>
2026-01-13 16:41:12 +08:00
<!-- Status Badge -->
<div class="status-badge" :class="`status-${task.status}`">
{{ getStatusText(task.status) }}
</div>
2026-01-13 14:01:17 +08:00
</div>
2026-01-13 16:41:12 +08:00
2026-01-13 14:01:17 +08:00
<div class="card-info">
<div class="card-desc" :title="task.inputContent">
{{ task.inputContent }}
</div>
<div class="card-meta">
2026-01-13 16:41:12 +08:00
<span class="card-time">
<ClockCircleOutlined />
{{ formatTime(task.createTime) }}
</span>
2026-01-13 14:01:17 +08:00
<div class="card-actions" @click.stop>
<a-tooltip v-if="task.status === 'completed'" title="预览">
2026-01-13 16:41:12 +08:00
<div class="action-btn" @click="handlePreview(task)">
<EyeOutlined />
</div>
2026-01-13 14:01:17 +08:00
</a-tooltip>
<a-tooltip
v-if="['failed', 'timeout'].includes(task.status)"
title="重试"
>
2026-01-13 16:41:12 +08:00
<div
class="action-btn"
2026-01-13 14:01:17 +08:00
:class="{ disabled: task.retryCount >= 3 }"
@click="handleRetry(task)"
2026-01-13 16:41:12 +08:00
>
<ReloadOutlined />
</div>
2026-01-13 14:01:17 +08:00
</a-tooltip>
<a-tooltip title="删除">
2026-01-13 16:41:12 +08:00
<div class="action-btn danger" @click="handleDelete(task)">
<DeleteOutlined />
</div>
2026-01-13 14:01:17 +08:00
</a-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
2026-01-14 14:29:16 +08:00
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue"
import { useRouter, useRoute } from "vue-router"
2026-01-13 14:01:17 +08:00
import { message, Modal } from "ant-design-vue"
import {
ReloadOutlined,
PictureOutlined,
ThunderboltOutlined,
LoadingOutlined,
2026-01-14 14:29:16 +08:00
CloseOutlined,
2026-01-13 14:01:17 +08:00
FileImageOutlined,
EyeOutlined,
DeleteOutlined,
2026-01-13 16:41:12 +08:00
ArrowRightOutlined,
2026-01-14 14:29:16 +08:00
ArrowLeftOutlined,
2026-01-13 16:41:12 +08:00
ClockCircleOutlined,
2026-01-13 14:01:17 +08:00
} from "@ant-design/icons-vue"
import type { UploadFile } from "ant-design-vue"
import {
createAI3DTask,
getAI3DTasks,
getAI3DTask,
retryAI3DTask,
deleteAI3DTask,
type AI3DTask,
} from "@/api/ai-3d"
import { uploadFile } from "@/api/upload"
2026-01-13 16:41:12 +08:00
import { useAuthStore } from "@/stores/auth"
2026-01-13 14:01:17 +08:00
import dayjs from "dayjs"
2026-01-13 16:41:12 +08:00
// 获取认证状态
const authStore = useAuthStore()
const router = useRouter()
2026-01-14 14:29:16 +08:00
const route = useRoute()
// 返回首页
const handleBack = () => {
const tenantCode = route.params.tenantCode as string
router.push(`/${tenantCode}`)
}
// 跳转到历史记录页面
const goToHistory = () => {
const tenantCode = route.params.tenantCode as string
router.push(`/${tenantCode}/workbench/3d-lab/history`)
}
2026-01-13 16:41:12 +08:00
2026-01-13 14:01:17 +08:00
// 输入类型选项
const inputTypeOptions = [
{ label: "文生3D", value: "text" },
{ label: "图生3D", value: "image" },
]
// 示例提示词
const samplePrompts = [
["啄木鸟", "尖锐的嘴", "金黄色"],
["可爱的猫咪", "卡通风格", "蓝色眼睛"],
["机器人", "金属质感", "未来风格"],
["中式花瓶", "青花瓷", "精致纹理"],
["小恐龙", "Q版造型", "绿色皮肤"],
["宇航员", "太空服", "写实风格"],
]
// 状态
const inputType = ref<"text" | "image">("text")
const textContent = ref("")
const imageFileList = ref<UploadFile[]>()
const imageUrl = ref("")
const generating = ref(false)
const currentSampleIndex = ref(0)
// 历史记录
const historyList = ref<AI3DTask[]>([])
const historyLoading = ref(false)
const allHistoryTotal = ref(0)
2026-01-14 14:29:16 +08:00
// 历史网格容器引用和宽度
const historyGridRef = ref<HTMLElement | null>(null)
const historyGridWidth = ref(0)
2026-01-13 14:01:17 +08:00
// 轮询定时器
let pollingTimer: number | null = null
2026-01-14 14:29:16 +08:00
// ResizeObserver 用于监听容器宽度变化
let resizeObserver: ResizeObserver | null = null
2026-01-13 14:01:17 +08:00
// 计算属性
const currentSamples = computed(() => samplePrompts[currentSampleIndex.value])
const canGenerate = computed(() => {
2026-01-13 16:41:12 +08:00
// 首先检查用户是否有创建权限
if (!authStore.hasPermission("ai-3d:create")) {
return false
}
// 检查输入内容是否有效
2026-01-13 14:01:17 +08:00
if (inputType.value === "text") {
return textContent.value.trim().length > 0
} else {
return imageUrl.value.length > 0
}
})
2026-01-14 14:29:16 +08:00
// 计算一行最多能展示的卡片数量
// 卡片宽度 240px + 间距 20px = 260px
const maxCardsPerRow = computed(() => {
if (historyGridWidth.value === 0) return 6 // 默认值
const cardWidth = 240
const gap = 20
// 可用宽度 = 容器宽度 - 左右 padding (48px * 2)
const availableWidth = historyGridWidth.value - 96
// 计算能放多少个:可用宽度 / (卡片宽度 + 间距)
const count = Math.floor(availableWidth / (cardWidth + gap))
return Math.max(1, count) // 至少显示1个
})
// 限制显示的历史记录数量
const displayedHistoryList = computed(() => {
return historyList.value.slice(0, maxCardsPerRow.value)
})
2026-01-13 14:01:17 +08:00
// 刷新示例
const refreshSamples = () => {
currentSampleIndex.value =
(currentSampleIndex.value + 1) % samplePrompts.length
}
// 应用示例
const applySample = (sample: string) => {
if (textContent.value) {
textContent.value += "" + sample
} else {
textContent.value = sample
}
}
// 聚焦输入
const focusInput = () => {
// 滚动到左侧面板
}
// 图片上传前处理
const handleBeforeUpload = async (file: File) => {
const isImage = file.type.startsWith("image/")
if (!isImage) {
message.error("只能上传图片文件")
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error("图片大小不能超过 10MB")
return false
}
// 上传图片
try {
const result = await uploadFile(file)
imageUrl.value = result.url
message.success("图片上传成功")
} catch (error) {
message.error("图片上传失败")
return false
}
return false // 阻止默认上传行为
}
// 生成3D模型
const handleGenerate = async () => {
if (!canGenerate.value) return
generating.value = true
try {
const content =
inputType.value === "text" ? textContent.value.trim() : imageUrl.value
2026-01-13 16:41:12 +08:00
const task = await createAI3DTask({
2026-01-13 14:01:17 +08:00
inputType: inputType.value,
inputContent: content,
})
// 清空输入
if (inputType.value === "text") {
textContent.value = ""
} else {
imageFileList.value = []
imageUrl.value = ""
}
2026-01-13 16:41:12 +08:00
// 跳转到生成页面
2026-01-14 10:06:08 +08:00
const taskData = task.data || task // 兼容不同的响应格式
2026-01-13 16:41:12 +08:00
router.push({
name: "AI3DGenerate",
2026-01-14 10:06:08 +08:00
params: { taskId: taskData.id },
2026-01-13 16:41:12 +08:00
})
2026-01-13 14:01:17 +08:00
} catch (error: any) {
message.error(error.response?.data?.message || "提交失败,请重试")
} finally {
generating.value = false
}
}
// 获取历史记录最近6条
const fetchHistory = async () => {
historyLoading.value = true
try {
const res = await getAI3DTasks({ page: 1, pageSize: 6 })
2026-01-14 10:06:08 +08:00
const data = res.data || res // 兼容不同的响应格式
historyList.value = data.list || []
2026-01-14 14:29:16 +08:00
// 如果还没有获取过总数,则保存总数
if (!allHistoryTotal.value && data.total) {
allHistoryTotal.value = data.total
}
2026-01-13 14:01:17 +08:00
} catch (error) {
console.error("获取历史记录失败:", error)
} finally {
historyLoading.value = false
}
}
// 开始轮询(检查处理中的任务)
const startPolling = () => {
if (pollingTimer) return
pollingTimer = window.setInterval(async () => {
const processingTasks = historyList.value.filter(
(t) => t.status === "pending" || t.status === "processing"
)
if (processingTasks.length === 0) {
stopPolling()
return
}
// 刷新历史记录
await fetchHistory()
}, 3000)
}
// 停止轮询
const stopPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer)
pollingTimer = null
}
}
// 预览3D模型
const handlePreview = (task: AI3DTask) => {
if (task.resultUrl) {
2026-01-14 14:29:16 +08:00
const tenantCode = route.params.tenantCode as string
router.push({
path: `/${tenantCode}/workbench/model-viewer`,
query: { url: task.resultUrl },
})
2026-01-13 14:01:17 +08:00
}
}
// 查看任务详情
const handleViewTask = (task: AI3DTask) => {
2026-01-13 16:41:12 +08:00
// 跳转到生成页面查看详情
router.push({
name: "AI3DGenerate",
params: { taskId: task.id },
})
2026-01-13 14:01:17 +08:00
}
// 重试任务
const handleRetry = async (task: AI3DTask) => {
if (task.retryCount >= 3) {
message.warning("已达到最大重试次数,请创建新任务")
return
}
try {
await retryAI3DTask(task.id)
message.success("重试已提交")
fetchHistory()
startPolling()
} catch (error: any) {
message.error(error.response?.data?.message || "重试失败")
}
}
// 删除任务
const handleDelete = (task: AI3DTask) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这条创作记录吗?",
okText: "删除",
okType: "danger",
cancelText: "取消",
async onOk() {
try {
await deleteAI3DTask(task.id)
message.success("删除成功")
fetchHistory()
} catch (error) {
message.error("删除失败")
}
},
})
}
2026-01-14 10:06:08 +08:00
// 获取预览图URL通过代理访问解决CORS问题
2026-01-13 14:01:17 +08:00
const getPreviewUrl = (task: AI3DTask) => {
if (task.previewUrl) {
2026-01-14 10:06:08 +08:00
// 如果是腾讯云COS链接通过代理访问
if (
task.previewUrl.includes("tencentcos.cn") ||
task.previewUrl.includes("qcloud.com")
) {
// 确保URL正确编码
const encodedUrl = encodeURIComponent(task.previewUrl)
return `/api/ai-3d/proxy-preview?url=${encodedUrl}`
}
// 其他URL直接返回
return task.previewUrl
2026-01-13 14:01:17 +08:00
}
return ""
}
// 获取状态文本
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
pending: "等待中",
processing: "生成中",
completed: "已完成",
failed: "失败",
2026-01-13 16:41:12 +08:00
timeout: "超时",
2026-01-13 14:01:17 +08:00
}
return texts[status] || status
}
2026-01-14 10:06:08 +08:00
// 图片加载错误处理
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
console.error("预览图加载失败:", img.src)
// 可以在这里添加错误提示或显示占位图
}
// 图片加载成功处理
const handleImageLoad = () => {
// 图片加载成功,可以在这里做一些处理
console.log("预览图加载成功")
}
2026-01-13 14:01:17 +08:00
// 格式化时间
const formatTime = (time: string) => {
return dayjs(time).format("MM-DD HH:mm")
}
2026-01-14 14:29:16 +08:00
// 更新容器宽度
const updateGridWidth = () => {
if (historyGridRef.value) {
historyGridWidth.value = historyGridRef.value.offsetWidth
}
}
2026-01-13 14:01:17 +08:00
// 页面加载
2026-01-14 14:29:16 +08:00
onMounted(async () => {
await fetchHistory()
// 等待 DOM 更新后计算容器宽度
await nextTick()
updateGridWidth()
// 监听容器宽度变化
if (historyGridRef.value) {
resizeObserver = new ResizeObserver(() => {
updateGridWidth()
})
resizeObserver.observe(historyGridRef.value)
}
2026-01-13 14:01:17 +08:00
// 检查是否有处理中的任务,有则开启轮询
const hasProcessing = historyList.value.some(
(t) => t.status === "pending" || t.status === "processing"
)
if (hasProcessing) {
startPolling()
}
})
// 页面卸载
onUnmounted(() => {
stopPolling()
2026-01-14 14:29:16 +08:00
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
2026-01-13 14:01:17 +08:00
})
</script>
<style scoped lang="scss">
2026-01-13 16:41:12 +08:00
// ==========================================
2026-01-14 14:29:16 +08:00
// 蓝色主题色彩方案 - 简洁统一
2026-01-13 16:41:12 +08:00
// ==========================================
2026-01-14 14:29:16 +08:00
// 主色调 - 蓝色系
$primary: #0958d9; // 主题蓝色(与系统统一)
$primary-light: #1677ff;
$primary-dark: #003eb3;
$secondary: #4096ff; // 浅蓝色作为辅助色
$accent: #1677ff; // 蓝色强调
$success: #52c41a;
$warning: #faad14;
$error: #ff4d4f;
// 背景色
$background: #f5f5f5; // 浅灰背景(与系统统一)
$surface: #ffffff; // 白色卡片
$surface-light: #fafafa;
// 文字色
$text: rgba(0, 0, 0, 0.85); // 深色文字
$text-secondary: rgba(0, 0, 0, 0.65);
$text-muted: rgba(0, 0, 0, 0.45);
$text-light: #ffffff; // 深色区域的白色文字
// 渐变 - 蓝色渐变
$gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
$gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
2026-01-13 16:41:12 +08:00
2026-01-13 14:01:17 +08:00
.ai-3d-container {
display: flex;
2026-01-14 14:29:16 +08:00
width: 100%;
min-height: 100vh;
2026-01-13 16:41:12 +08:00
background: $background;
position: relative;
overflow: hidden;
2026-01-14 14:29:16 +08:00
align-items: stretch;
2026-01-13 16:41:12 +08:00
}
// ==========================================
2026-01-14 14:29:16 +08:00
// 简化背景动画 - 保留微妙效果
2026-01-13 16:41:12 +08:00
// ==========================================
.bg-animation {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.bg-gradient {
position: absolute;
border-radius: 50%;
2026-01-14 14:29:16 +08:00
filter: blur(100px);
opacity: 0.15;
animation: float 30s ease-in-out infinite;
2026-01-13 16:41:12 +08:00
&.bg-gradient-1 {
width: 600px;
height: 600px;
2026-01-14 14:29:16 +08:00
background: $primary;
top: -200px;
left: -100px;
2026-01-13 16:41:12 +08:00
}
2026-01-14 14:29:16 +08:00
&.bg-gradient-2 {
2026-01-13 16:41:12 +08:00
width: 500px;
height: 500px;
2026-01-14 14:29:16 +08:00
background: $primary-light;
bottom: -150px;
right: -100px;
animation-delay: -10s;
2026-01-13 16:41:12 +08:00
}
2026-01-14 14:29:16 +08:00
&.bg-gradient-3 {
width: 400px;
height: 400px;
background: $secondary;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation-delay: -20s;
opacity: 0.1;
}
2026-01-13 16:41:12 +08:00
}
@keyframes float {
2026-01-14 10:06:08 +08:00
0%,
100% {
transform: translate(0, 0) scale(1);
}
2026-01-14 14:29:16 +08:00
33% {
transform: translate(30px, -30px) scale(1.05);
2026-01-14 10:06:08 +08:00
}
2026-01-14 14:29:16 +08:00
66% {
transform: translate(-20px, 20px) scale(0.95);
2026-01-14 10:06:08 +08:00
}
2026-01-13 16:41:12 +08:00
}
// ==========================================
2026-01-14 14:29:16 +08:00
// Left Panel - 浅色变体
2026-01-13 16:41:12 +08:00
// ==========================================
2026-01-13 14:01:17 +08:00
.left-panel {
2026-01-13 16:41:12 +08:00
width: 380px;
2026-01-14 14:29:16 +08:00
min-height: 100vh;
background: rgba($surface, 0.5);
backdrop-filter: blur(20px);
2026-01-13 14:01:17 +08:00
display: flex;
flex-direction: column;
2026-01-13 16:41:12 +08:00
position: relative;
z-index: 1;
2026-01-14 14:29:16 +08:00
align-self: stretch;
border-right: 1px solid rgba($primary, 0.1);
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.06);
2026-01-13 16:41:12 +08:00
}
.panel-logo {
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
border-bottom: 1px solid rgba($primary, 0.1);
2026-01-14 14:29:16 +08:00
flex-shrink: 0;
.back-btn {
color: $text !important;
width: 40px;
height: 40px;
border-radius: 10px !important;
border: 1px solid rgba($primary, 0.3) !important;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s !important;
flex-shrink: 0;
&:hover {
background: rgba($primary, 0.2) !important;
border-color: $primary !important;
transform: translateY(-1px);
}
}
2026-01-13 16:41:12 +08:00
.logo-ring {
position: relative;
width: 56px;
height: 56px;
border: 2px solid transparent;
border-radius: 50%;
2026-01-14 10:06:08 +08:00
background:
linear-gradient($surface, $surface) padding-box,
$gradient-primary border-box;
2026-01-13 16:41:12 +08:00
animation: rotateBorder 8s linear infinite;
}
.logo-icon {
position: absolute;
inset: 6px;
background: $gradient-primary;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.logo-text {
h3 {
margin: 0;
font-size: 18px;
font-weight: 700;
background: $gradient-primary;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
p {
margin: 4px 0 0;
font-size: 12px;
color: $text-muted;
}
}
}
@keyframes rotateBorder {
2026-01-14 10:06:08 +08:00
to {
transform: rotate(360deg);
}
2026-01-13 14:01:17 +08:00
}
.panel-header {
2026-01-13 16:41:12 +08:00
padding: 20px 24px;
border-bottom: 1px solid rgba($primary, 0.1);
2026-01-14 14:29:16 +08:00
flex-shrink: 0;
2026-01-13 14:01:17 +08:00
:deep(.ant-segmented) {
2026-01-13 16:41:12 +08:00
background: rgba($surface-light, 0.6);
border: 1px solid rgba($primary, 0.2);
2026-01-13 14:01:17 +08:00
.ant-segmented-item {
2026-01-13 16:41:12 +08:00
color: $text-muted;
2026-01-13 14:01:17 +08:00
&-selected {
2026-01-13 16:41:12 +08:00
background: $gradient-primary;
2026-01-13 14:01:17 +08:00
color: #fff;
}
}
}
}
.panel-content {
flex: 1;
2026-01-13 16:41:12 +08:00
padding: 24px;
2026-01-13 14:01:17 +08:00
overflow-y: auto;
2026-01-13 16:41:12 +08:00
min-height: 0;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba($primary, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba($primary, 0.3);
border-radius: 3px;
&:hover {
background: rgba($primary, 0.5);
}
}
}
.input-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: $text;
margin-bottom: 8px;
.label-icon {
font-size: 16px;
}
2026-01-13 14:01:17 +08:00
}
.input-hint {
font-size: 13px;
2026-01-13 16:41:12 +08:00
color: $text-muted;
margin-bottom: 16px;
2026-01-13 14:01:17 +08:00
line-height: 1.6;
}
.text-input {
2026-01-13 16:41:12 +08:00
background: rgba($surface-light, 0.6) !important;
border: 2px solid rgba($primary, 0.15) !important;
border-radius: 12px;
color: $text;
2026-01-13 14:01:17 +08:00
resize: none;
2026-01-13 16:41:12 +08:00
transition: all 0.3s;
2026-01-13 14:01:17 +08:00
&::placeholder {
2026-01-13 16:41:12 +08:00
color: rgba($text-muted, 0.5);
}
&:hover {
border-color: rgba($primary, 0.4) !important;
2026-01-13 14:01:17 +08:00
}
&:focus {
2026-01-13 16:41:12 +08:00
border-color: $primary !important;
box-shadow: 0 0 0 4px rgba($primary, 0.1) !important;
background: rgba($surface-light, 0.8) !important;
2026-01-13 14:01:17 +08:00
}
:deep(.ant-input-data-count) {
2026-01-13 16:41:12 +08:00
color: $text-muted;
2026-01-13 14:01:17 +08:00
}
}
2026-01-13 16:41:12 +08:00
.sample-section {
margin-top: 20px;
}
.sample-header {
2026-01-13 14:01:17 +08:00
display: flex;
2026-01-13 16:41:12 +08:00
justify-content: space-between;
2026-01-13 14:01:17 +08:00
align-items: center;
2026-01-13 16:41:12 +08:00
margin-bottom: 12px;
.sample-title {
font-size: 13px;
font-weight: 600;
color: $text;
}
2026-01-13 14:01:17 +08:00
.refresh-icon {
2026-01-13 16:41:12 +08:00
color: $text-muted;
2026-01-13 14:01:17 +08:00
cursor: pointer;
2026-01-13 16:41:12 +08:00
transition: all 0.3s;
font-size: 14px;
2026-01-13 14:01:17 +08:00
&:hover {
2026-01-14 14:29:16 +08:00
color: $primary-light;
2026-01-13 16:41:12 +08:00
transform: rotate(180deg);
2026-01-13 14:01:17 +08:00
}
}
2026-01-13 16:41:12 +08:00
}
.sample-prompts {
display: flex;
flex-wrap: wrap;
gap: 8px;
2026-01-13 14:01:17 +08:00
.sample-tag {
2026-01-13 16:41:12 +08:00
padding: 6px 14px;
background: rgba($primary, 0.1);
border: 1px solid rgba($primary, 0.2);
border-radius: 20px;
2026-01-13 14:01:17 +08:00
font-size: 12px;
2026-01-13 16:41:12 +08:00
color: $text;
2026-01-13 14:01:17 +08:00
cursor: pointer;
transition: all 0.3s;
&:hover {
2026-01-14 14:29:16 +08:00
background: rgba($primary-light, 0.2);
border-color: $primary-light;
2026-01-13 16:41:12 +08:00
transform: translateY(-2px);
2026-01-13 14:01:17 +08:00
}
}
}
.image-upload {
:deep(.ant-upload-drag) {
2026-01-13 16:41:12 +08:00
background: rgba($surface-light, 0.6) !important;
border: 2px dashed rgba($primary, 0.3) !important;
border-radius: 12px;
transition: all 0.3s;
2026-01-13 14:01:17 +08:00
&:hover {
2026-01-14 14:29:16 +08:00
border-color: $primary-light !important;
2026-01-13 16:41:12 +08:00
background: rgba($surface-light, 0.8) !important;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.upload-icon {
color: $primary-light;
font-size: 56px;
margin-bottom: 12px;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.upload-text {
color: $text;
font-size: 15px;
margin-bottom: 8px;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.upload-hint {
color: $text-muted;
font-size: 12px;
2026-01-13 14:01:17 +08:00
}
}
}
.panel-footer {
2026-01-13 16:41:12 +08:00
padding: 24px;
border-top: 1px solid rgba($primary, 0.1);
flex-shrink: 0;
}
.generate-btn {
width: 100%;
height: 56px;
border: none;
border-radius: 14px;
cursor: pointer;
position: relative;
overflow: hidden;
background: transparent;
.btn-bg {
position: absolute;
inset: 0;
background: $gradient-primary;
transition: all 0.4s;
}
.btn-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: white;
font-size: 16px;
font-weight: 600;
}
.spin-icon {
animation: spin 1s linear infinite;
}
&:hover:not(.is-disabled) {
transform: translateY(-2px);
box-shadow: 0 15px 35px rgba($primary, 0.4);
.btn-bg {
transform: scale(1.02);
}
}
&:active:not(.is-disabled) {
transform: translateY(0);
}
&.is-disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.is-loading {
.btn-bg {
animation: gradientShift 2s ease infinite;
}
}
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
@keyframes spin {
2026-01-14 10:06:08 +08:00
to {
transform: rotate(360deg);
}
2026-01-13 16:41:12 +08:00
}
@keyframes gradientShift {
2026-01-14 10:06:08 +08:00
0%,
100% {
background: $gradient-primary;
}
50% {
background: $gradient-secondary;
}
2026-01-13 16:41:12 +08:00
}
// ==========================================
// Right Panel
// ==========================================
2026-01-13 14:01:17 +08:00
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
2026-01-13 16:41:12 +08:00
position: relative;
z-index: 1;
2026-01-14 14:29:16 +08:00
height: 100vh;
background: rgba($surface, 0.3);
backdrop-filter: blur(20px);
2026-01-13 14:01:17 +08:00
}
.intro-section {
2026-01-13 16:41:12 +08:00
padding: 48px;
2026-01-14 14:29:16 +08:00
flex-shrink: 0;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.intro-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: rgba($primary, 0.15);
border: 1px solid rgba($primary, 0.3);
border-radius: 20px;
font-size: 11px;
2026-01-13 14:01:17 +08:00
font-weight: 600;
2026-01-13 16:41:12 +08:00
color: $primary-light;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 20px;
.badge-dot {
width: 6px;
height: 6px;
background: $accent;
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
}
}
@keyframes pulse {
2026-01-14 10:06:08 +08:00
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
2026-01-13 16:41:12 +08:00
}
.intro-title {
font-size: 36px;
font-weight: 700;
color: $text;
margin-bottom: 16px;
line-height: 1.3;
background: $gradient-primary;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.intro-desc {
font-size: 16px;
color: $text-muted;
margin-bottom: 32px;
line-height: 1.6;
2026-01-13 14:01:17 +08:00
}
.intro-features {
2026-01-13 16:41:12 +08:00
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.feature-card {
2026-01-13 14:01:17 +08:00
display: flex;
2026-01-13 16:41:12 +08:00
align-items: flex-start;
gap: 16px;
padding: 20px;
2026-01-14 14:29:16 +08:00
background: $surface-light;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
2026-01-13 16:41:12 +08:00
transition: all 0.3s;
2026-01-14 14:29:16 +08:00
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
2026-01-13 14:01:17 +08:00
2026-01-14 10:06:08 +08:00
&.gradient-1 {
2026-01-14 14:29:16 +08:00
background: linear-gradient(135deg, $primary 0%, $primary-light 100%);
2026-01-14 10:06:08 +08:00
}
&.gradient-2 {
2026-01-14 14:29:16 +08:00
background: linear-gradient(135deg, $primary-light 0%, $accent 100%);
2026-01-14 10:06:08 +08:00
}
&.gradient-3 {
2026-01-14 14:29:16 +08:00
background: linear-gradient(135deg, $accent 0%, $primary 100%);
2026-01-14 10:06:08 +08:00
}
&.gradient-4 {
2026-01-14 14:29:16 +08:00
background: linear-gradient(135deg, $primary 0%, $primary-dark 100%);
2026-01-14 10:06:08 +08:00
}
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
.feature-info {
flex: 1;
h3 {
margin: 0 0 4px;
font-size: 15px;
font-weight: 600;
color: $text;
}
p {
margin: 0;
font-size: 13px;
color: $text-muted;
line-height: 1.5;
}
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
// ==========================================
// History Section
// ==========================================
2026-01-13 14:01:17 +08:00
.history-section {
flex: 1;
2026-01-14 14:29:16 +08:00
min-height: 0;
2026-01-13 16:41:12 +08:00
padding: 32px 48px;
2026-01-13 14:01:17 +08:00
overflow-y: auto;
2026-01-13 16:41:12 +08:00
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba($primary, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba($primary, 0.3);
border-radius: 4px;
&:hover {
background: rgba($primary, 0.5);
}
}
2026-01-13 14:01:17 +08:00
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
2026-01-13 16:41:12 +08:00
margin-bottom: 24px;
}
.header-left {
display: flex;
align-items: baseline;
gap: 12px;
2026-01-13 14:01:17 +08:00
}
.history-title {
2026-01-13 16:41:12 +08:00
font-size: 24px;
font-weight: 600;
color: $text;
2026-01-13 14:01:17 +08:00
margin: 0;
}
2026-01-13 16:41:12 +08:00
.history-count {
font-size: 14px;
color: $text-muted;
}
2026-01-13 14:01:17 +08:00
.view-all {
2026-01-13 16:41:12 +08:00
display: flex;
align-items: center;
gap: 6px;
2026-01-14 14:29:16 +08:00
color: $primary-light;
2026-01-13 14:01:17 +08:00
cursor: pointer;
2026-01-13 16:41:12 +08:00
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
2026-01-13 14:01:17 +08:00
&:hover {
2026-01-14 14:29:16 +08:00
color: $primary;
2026-01-13 16:41:12 +08:00
transform: translateX(4px);
2026-01-13 14:01:17 +08:00
}
}
2026-01-13 16:41:12 +08:00
.history-loading {
2026-01-13 14:01:17 +08:00
display: flex;
2026-01-13 16:41:12 +08:00
flex-direction: column;
align-items: center;
2026-01-13 14:01:17 +08:00
justify-content: center;
2026-01-13 16:41:12 +08:00
min-height: 300px;
gap: 16px;
color: $text-muted;
}
.loading-spinner {
position: relative;
width: 60px;
height: 60px;
.spinner-ring {
position: absolute;
inset: 0;
border: 2px solid transparent;
border-radius: 50%;
animation: spin 2s linear infinite;
&:nth-child(1) {
border-top-color: $primary;
animation-duration: 1.5s;
}
&:nth-child(2) {
2026-01-14 14:29:16 +08:00
border-right-color: $primary-light;
2026-01-13 16:41:12 +08:00
animation-duration: 2s;
}
&:nth-child(3) {
border-bottom-color: $accent;
animation-duration: 2.5s;
}
}
}
.history-empty {
display: flex;
flex-direction: column;
2026-01-13 14:01:17 +08:00
align-items: center;
2026-01-13 16:41:12 +08:00
justify-content: center;
min-height: 300px;
gap: 12px;
.empty-icon {
width: 80px;
height: 80px;
background: rgba($primary, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: $primary-light;
margin-bottom: 8px;
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: $text;
margin: 0;
}
.empty-text {
font-size: 14px;
color: $text-muted;
margin: 0;
}
2026-01-13 14:01:17 +08:00
}
.history-grid {
2026-01-14 14:29:16 +08:00
display: flex;
flex-direction: row;
2026-01-13 16:41:12 +08:00
gap: 20px;
2026-01-14 14:29:16 +08:00
overflow: hidden; // 移除滚动,只展示一行
flex-wrap: nowrap; // 不换行
2026-01-13 14:01:17 +08:00
}
.history-card {
2026-01-14 14:29:16 +08:00
flex-shrink: 0;
width: 240px;
background: $surface;
border-radius: 8px;
2026-01-13 14:01:17 +08:00
overflow: hidden;
cursor: pointer;
2026-01-14 14:29:16 +08:00
transition: all 0.3s;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2026-01-13 16:41:12 +08:00
position: relative;
2026-01-13 14:01:17 +08:00
&:hover {
2026-01-14 14:29:16 +08:00
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
2026-01-13 16:41:12 +08:00
.card-glow {
2026-01-14 14:29:16 +08:00
opacity: 0.3;
2026-01-13 16:41:12 +08:00
}
2026-01-13 14:01:17 +08:00
}
}
2026-01-13 16:41:12 +08:00
.card-glow {
position: absolute;
inset: -2px;
2026-01-14 14:29:16 +08:00
background: $primary;
border-radius: 10px;
2026-01-13 16:41:12 +08:00
z-index: -1;
opacity: 0;
2026-01-14 14:29:16 +08:00
filter: blur(8px);
transition: opacity 0.3s;
2026-01-13 16:41:12 +08:00
}
2026-01-13 14:01:17 +08:00
.card-preview {
2026-01-13 16:41:12 +08:00
height: 160px;
background: rgba($surface-light, 0.8);
2026-01-13 14:01:17 +08:00
display: flex;
align-items: center;
justify-content: center;
2026-01-13 16:41:12 +08:00
position: relative;
overflow: hidden;
2026-01-13 14:01:17 +08:00
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
2026-01-13 16:41:12 +08:00
transition: transform 0.5s;
}
.history-card:hover & .preview-image {
transform: scale(1.1);
2026-01-13 14:01:17 +08:00
}
.preview-loading,
.preview-failed,
.preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
2026-01-13 16:41:12 +08:00
gap: 12px;
color: $text-muted;
font-size: 32px;
2026-01-13 14:01:17 +08:00
2026-01-13 16:41:12 +08:00
.loading-text {
font-size: 13px;
2026-01-13 14:01:17 +08:00
}
}
.preview-failed {
2026-01-14 14:29:16 +08:00
.failed-icon {
width: 56px;
height: 56px;
background: linear-gradient(135deg, rgba($error, 0.15) 0%, rgba($error, 0.25) 100%);
border: 2px solid rgba($error, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: $error;
animation: pulse-error 2s ease-in-out infinite;
}
}
}
@keyframes pulse-error {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba($error, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 20px 5px rgba($error, 0.15);
2026-01-13 16:41:12 +08:00
}
}
.loading-dots {
display: flex;
gap: 6px;
span {
width: 8px;
height: 8px;
2026-01-14 14:29:16 +08:00
background: $primary-light;
2026-01-13 16:41:12 +08:00
border-radius: 50%;
animation: dotPulse 1.4s ease-in-out infinite;
2026-01-14 10:06:08 +08:00
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
2026-01-13 16:41:12 +08:00
}
}
@keyframes dotPulse {
2026-01-14 10:06:08 +08:00
0%,
60%,
100% {
transform: scale(1);
opacity: 1;
}
30% {
transform: scale(1.5);
opacity: 0.7;
}
2026-01-13 16:41:12 +08:00
}
.status-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
&.status-completed {
background: rgba($success, 0.2);
color: $success;
border-color: rgba($success, 0.3);
}
&.status-processing,
&.status-pending {
2026-01-14 14:29:16 +08:00
background: rgba($primary-light, 0.2);
color: $primary-light;
border-color: rgba($primary-light, 0.3);
2026-01-13 16:41:12 +08:00
}
&.status-failed,
&.status-timeout {
2026-01-14 14:29:16 +08:00
background: rgba($error, 0.2);
color: $error;
border-color: rgba($error, 0.3);
2026-01-13 14:01:17 +08:00
}
}
.card-info {
2026-01-13 16:41:12 +08:00
padding: 16px;
2026-01-14 14:29:16 +08:00
background: rgba(9, 88, 217, 0.15);
2026-01-13 14:01:17 +08:00
}
.card-desc {
2026-01-13 16:41:12 +08:00
font-size: 14px;
color: $text;
2026-01-13 14:01:17 +08:00
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
2026-01-13 16:41:12 +08:00
margin-bottom: 12px;
font-weight: 500;
2026-01-13 14:01:17 +08:00
}
.card-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-time {
2026-01-13 16:41:12 +08:00
display: flex;
align-items: center;
gap: 6px;
2026-01-13 14:01:17 +08:00
font-size: 12px;
2026-01-13 16:41:12 +08:00
color: $text-muted;
2026-01-13 14:01:17 +08:00
}
.card-actions {
display: flex;
gap: 8px;
2026-01-13 16:41:12 +08:00
}
2026-01-13 14:01:17 +08:00
2026-01-13 16:41:12 +08:00
.action-btn {
width: 32px;
height: 32px;
background: rgba($primary, 0.1);
border: 1px solid rgba($primary, 0.2);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: $primary-light;
cursor: pointer;
transition: all 0.3s;
2026-01-13 14:01:17 +08:00
2026-01-13 16:41:12 +08:00
&:hover {
2026-01-14 14:29:16 +08:00
background: rgba($primary-light, 0.2);
border-color: $primary-light;
color: $primary-light;
2026-01-13 16:41:12 +08:00
transform: scale(1.1);
}
&.danger {
2026-01-13 14:01:17 +08:00
&:hover {
2026-01-13 16:41:12 +08:00
background: rgba($accent, 0.2);
border-color: $accent;
color: $accent;
2026-01-13 14:01:17 +08:00
}
2026-01-13 16:41:12 +08:00
}
2026-01-13 14:01:17 +08:00
2026-01-13 16:41:12 +08:00
&.disabled {
opacity: 0.4;
cursor: not-allowed;
2026-01-13 14:01:17 +08:00
2026-01-13 16:41:12 +08:00
&:hover {
background: rgba($primary, 0.1);
border-color: rgba($primary, 0.2);
color: $primary-light;
transform: none;
2026-01-13 14:01:17 +08:00
}
}
}
2026-01-13 16:41:12 +08:00
// ==========================================
// Responsive
// ==========================================
@media (max-width: 1024px) {
.left-panel {
2026-01-14 14:29:16 +08:00
width: 100%;
2026-01-13 16:41:12 +08:00
}
.intro-features {
grid-template-columns: 1fr;
}
2026-01-14 14:29:16 +08:00
.history-card {
width: 200px;
2026-01-13 16:41:12 +08:00
}
}
@media (max-width: 768px) {
.ai-3d-container {
flex-direction: column;
}
.left-panel {
width: 100%;
}
.intro-section {
padding: 32px 24px;
}
.history-section {
padding: 24px;
}
2026-01-14 14:29:16 +08:00
.history-card {
width: 180px;
2026-01-13 16:41:12 +08:00
}
}
2026-01-13 14:01:17 +08:00
</style>