1681 lines
37 KiB
Vue
1681 lines
37 KiB
Vue
<template>
|
||
<div class="ai-3d-container">
|
||
<!-- 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 class="bg-grid"></div>
|
||
</div>
|
||
|
||
<!-- 左侧生成栏 -->
|
||
<div class="left-panel">
|
||
<!-- Header with Logo -->
|
||
<div class="panel-logo">
|
||
<div class="logo-ring">
|
||
<div class="logo-icon">
|
||
<ThunderboltOutlined />
|
||
</div>
|
||
</div>
|
||
<div class="logo-text">
|
||
<h3>AI 3D Studio</h3>
|
||
<p>智能建模工作室</p>
|
||
</div>
|
||
</div>
|
||
|
||
<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">
|
||
<div class="input-label">
|
||
<span class="label-icon">✨</span>
|
||
<span>创意描述</span>
|
||
</div>
|
||
<div class="input-hint">
|
||
用文字描述你的想法,AI 将为你生成精美的 3D 模型
|
||
</div>
|
||
<a-textarea
|
||
v-model:value="textContent"
|
||
placeholder="例如:一只卡通风格的橙色小猫,蓝色的大眼睛,尾巴卷曲..."
|
||
:rows="6"
|
||
:maxlength="150"
|
||
show-count
|
||
class="text-input"
|
||
/>
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图生3D上传 -->
|
||
<div v-else class="image-input-section">
|
||
<div class="input-label">
|
||
<span class="label-icon">🖼️</span>
|
||
<span>参考图片</span>
|
||
</div>
|
||
<div class="input-hint">
|
||
上传参考图片,AI 将智能识别并生成 3D 模型
|
||
</div>
|
||
<a-upload-dragger
|
||
v-model:file-list="imageFileList"
|
||
:before-upload="handleBeforeUpload"
|
||
:max-count="1"
|
||
accept="image/*"
|
||
class="image-upload"
|
||
>
|
||
<p class="upload-icon">
|
||
<PictureOutlined />
|
||
</p>
|
||
<p class="upload-text">点击或拖拽图片到此处</p>
|
||
<p class="upload-hint">支持 JPG、PNG 格式,最大 10MB</p>
|
||
</a-upload-dragger>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-footer">
|
||
<button
|
||
class="generate-btn"
|
||
:class="{ 'is-loading': generating, 'is-disabled': !canGenerate }"
|
||
:disabled="!canGenerate"
|
||
@click="handleGenerate"
|
||
>
|
||
<span class="btn-bg"></span>
|
||
<span class="btn-content">
|
||
<LoadingOutlined v-if="generating" class="spin-icon" />
|
||
<ThunderboltOutlined v-else />
|
||
<span>{{ generating ? '生成中...' : '立即生成' }}</span>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容区 -->
|
||
<div class="right-panel">
|
||
<!-- 介绍区 -->
|
||
<div class="intro-section">
|
||
<div class="intro-badge">
|
||
<span class="badge-dot"></span>
|
||
<span>AI Powered</span>
|
||
</div>
|
||
<h1 class="intro-title">用一句话、一张图<br/>创造你的 3D 世界</h1>
|
||
<p class="intro-desc">
|
||
借助先进的 AI 技术,将文字描述或图片瞬间转化为专业级 3D 模型
|
||
</p>
|
||
|
||
<div class="intro-features">
|
||
<div class="feature-card">
|
||
<div class="feature-icon gradient-1">
|
||
<span>✨</span>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3>AI 智能建模</h3>
|
||
<p>输入文字或图片,自动生成 3D 模型</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<div class="feature-icon gradient-2">
|
||
<span>👁</span>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3>实时预览</h3>
|
||
<p>支持旋转、缩放,全方位查看细节</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<div class="feature-icon gradient-3">
|
||
<span>📁</span>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3>作品管理</h3>
|
||
<p>自动保存至个人作品库,随时访问</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<div class="feature-icon gradient-4">
|
||
<span>🔄</span>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h3>迭代优化</h3>
|
||
<p>基于已有作品再次生成与优化</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创作历史区 -->
|
||
<div class="history-section">
|
||
<div class="history-header">
|
||
<div class="header-left">
|
||
<h2 class="history-title">创作历史</h2>
|
||
<span class="history-count">{{ historyList.length }} 个作品</span>
|
||
</div>
|
||
<a class="view-all" @click="showAllHistory = true">
|
||
查看全部
|
||
<ArrowRightOutlined />
|
||
</a>
|
||
</div>
|
||
|
||
<div v-if="historyLoading" class="history-loading">
|
||
<div class="loading-spinner">
|
||
<div class="spinner-ring"></div>
|
||
<div class="spinner-ring"></div>
|
||
<div class="spinner-ring"></div>
|
||
</div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<div v-else-if="historyList.length === 0" class="history-empty">
|
||
<div class="empty-icon">
|
||
<FileImageOutlined />
|
||
</div>
|
||
<p class="empty-title">还没有创作记录</p>
|
||
<p class="empty-text">开始你的第一次 3D 创作之旅吧</p>
|
||
</div>
|
||
|
||
<div v-else class="history-grid">
|
||
<div
|
||
v-for="task in historyList"
|
||
:key="task.id"
|
||
class="history-card"
|
||
@click="handleViewTask(task)"
|
||
>
|
||
<div class="card-glow"></div>
|
||
<div class="card-preview">
|
||
<img
|
||
v-if="task.status === 'completed' && task.previewUrl"
|
||
:src="getPreviewUrl(task)"
|
||
alt="预览"
|
||
class="preview-image"
|
||
/>
|
||
<div
|
||
v-else-if="task.status === 'processing' || task.status === 'pending'"
|
||
class="preview-loading"
|
||
>
|
||
<div class="loading-dots">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</div>
|
||
<span class="loading-text">生成中</span>
|
||
</div>
|
||
<div
|
||
v-else-if="task.status === 'failed' || task.status === 'timeout'"
|
||
class="preview-failed"
|
||
>
|
||
<ExclamationCircleOutlined />
|
||
<span>{{ task.status === 'timeout' ? '已超时' : '生成失败' }}</span>
|
||
</div>
|
||
<div v-else class="preview-placeholder">
|
||
<FileImageOutlined />
|
||
</div>
|
||
|
||
<!-- Status Badge -->
|
||
<div class="status-badge" :class="`status-${task.status}`">
|
||
{{ getStatusText(task.status) }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-info">
|
||
<div class="card-desc" :title="task.inputContent">
|
||
{{ task.inputContent }}
|
||
</div>
|
||
<div class="card-meta">
|
||
<span class="card-time">
|
||
<ClockCircleOutlined />
|
||
{{ formatTime(task.createTime) }}
|
||
</span>
|
||
<div class="card-actions" @click.stop>
|
||
<a-tooltip v-if="task.status === 'completed'" title="预览">
|
||
<div class="action-btn" @click="handlePreview(task)">
|
||
<EyeOutlined />
|
||
</div>
|
||
</a-tooltip>
|
||
<a-tooltip
|
||
v-if="['failed', 'timeout'].includes(task.status)"
|
||
title="重试"
|
||
>
|
||
<div
|
||
class="action-btn"
|
||
:class="{ disabled: task.retryCount >= 3 }"
|
||
@click="handleRetry(task)"
|
||
>
|
||
<ReloadOutlined />
|
||
</div>
|
||
</a-tooltip>
|
||
<a-tooltip title="删除">
|
||
<div class="action-btn danger" @click="handleDelete(task)">
|
||
<DeleteOutlined />
|
||
</div>
|
||
</a-tooltip>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 全部历史记录抽屉 -->
|
||
<a-drawer
|
||
v-model:open="showAllHistory"
|
||
title="全部创作历史"
|
||
placement="right"
|
||
width="600px"
|
||
class="history-drawer"
|
||
>
|
||
<a-list
|
||
:data-source="allHistoryList"
|
||
:loading="allHistoryLoading"
|
||
:pagination="pagination"
|
||
>
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta>
|
||
<template #avatar>
|
||
<div class="list-preview">
|
||
<img
|
||
v-if="item.status === 'completed' && item.previewUrl"
|
||
:src="getPreviewUrl(item)"
|
||
alt="预览"
|
||
/>
|
||
<div v-else class="list-preview-placeholder">
|
||
<LoadingOutlined
|
||
v-if="['pending', 'processing'].includes(item.status)"
|
||
spin
|
||
/>
|
||
<ExclamationCircleOutlined
|
||
v-else-if="['failed', 'timeout'].includes(item.status)"
|
||
/>
|
||
<FileImageOutlined v-else />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template #title>
|
||
<span class="list-desc">{{ item.inputContent }}</span>
|
||
</template>
|
||
<template #description>
|
||
<div class="list-meta">
|
||
<a-tag :color="getStatusColor(item.status)">{{
|
||
getStatusText(item.status)
|
||
}}</a-tag>
|
||
<span>{{ formatTime(item.createTime) }}</span>
|
||
</div>
|
||
</template>
|
||
</a-list-item-meta>
|
||
<template #actions>
|
||
<a v-if="item.status === 'completed'" @click="handlePreview(item)"
|
||
>预览</a
|
||
>
|
||
<a
|
||
v-if="
|
||
['failed', 'timeout'].includes(item.status) &&
|
||
item.retryCount < 3
|
||
"
|
||
@click="handleRetry(item)"
|
||
>重试</a
|
||
>
|
||
<a class="danger-link" @click="handleDelete(item)">删除</a>
|
||
</template>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onUnmounted } from "vue"
|
||
import { useRouter } from "vue-router"
|
||
import { message, Modal } from "ant-design-vue"
|
||
import {
|
||
ReloadOutlined,
|
||
PictureOutlined,
|
||
ThunderboltOutlined,
|
||
LoadingOutlined,
|
||
ExclamationCircleOutlined,
|
||
FileImageOutlined,
|
||
EyeOutlined,
|
||
DeleteOutlined,
|
||
ArrowRightOutlined,
|
||
ClockCircleOutlined,
|
||
} 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"
|
||
import { useAuthStore } from "@/stores/auth"
|
||
import dayjs from "dayjs"
|
||
|
||
// 获取认证状态
|
||
const authStore = useAuthStore()
|
||
const router = useRouter()
|
||
|
||
// 输入类型选项
|
||
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 showAllHistory = ref(false)
|
||
const allHistoryList = ref<AI3DTask[]>([])
|
||
const allHistoryLoading = ref(false)
|
||
const allHistoryTotal = ref(0)
|
||
const allHistoryPage = ref(1)
|
||
|
||
// 轮询定时器
|
||
let pollingTimer: number | null = null
|
||
|
||
// 计算属性
|
||
const currentSamples = computed(() => samplePrompts[currentSampleIndex.value])
|
||
|
||
const canGenerate = computed(() => {
|
||
// 首先检查用户是否有创建权限
|
||
if (!authStore.hasPermission("ai-3d:create")) {
|
||
return false
|
||
}
|
||
|
||
// 检查输入内容是否有效
|
||
if (inputType.value === "text") {
|
||
return textContent.value.trim().length > 0
|
||
} else {
|
||
return imageUrl.value.length > 0
|
||
}
|
||
})
|
||
|
||
const pagination = computed(() => ({
|
||
current: allHistoryPage.value,
|
||
pageSize: 10,
|
||
total: allHistoryTotal.value,
|
||
onChange: (page: number) => {
|
||
allHistoryPage.value = page
|
||
fetchAllHistory()
|
||
},
|
||
}))
|
||
|
||
// 刷新示例
|
||
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
|
||
|
||
const task = await createAI3DTask({
|
||
inputType: inputType.value,
|
||
inputContent: content,
|
||
})
|
||
|
||
// 清空输入
|
||
if (inputType.value === "text") {
|
||
textContent.value = ""
|
||
} else {
|
||
imageFileList.value = []
|
||
imageUrl.value = ""
|
||
}
|
||
|
||
// 跳转到生成页面
|
||
router.push({
|
||
name: "AI3DGenerate",
|
||
params: { taskId: task.id },
|
||
})
|
||
} 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 })
|
||
historyList.value = res.list || []
|
||
} catch (error) {
|
||
console.error("获取历史记录失败:", error)
|
||
} finally {
|
||
historyLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取全部历史记录
|
||
const fetchAllHistory = async () => {
|
||
allHistoryLoading.value = true
|
||
try {
|
||
const res = await getAI3DTasks({ page: allHistoryPage.value, pageSize: 10 })
|
||
allHistoryList.value = res.list || []
|
||
allHistoryTotal.value = res.total || 0
|
||
} catch (error) {
|
||
console.error("获取全部历史记录失败:", error)
|
||
} finally {
|
||
allHistoryLoading.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) {
|
||
const viewerUrl = `/model-viewer?url=${encodeURIComponent(task.resultUrl)}`
|
||
window.open(viewerUrl, "_blank")
|
||
}
|
||
}
|
||
|
||
// 查看任务详情
|
||
const handleViewTask = (task: AI3DTask) => {
|
||
// 跳转到生成页面查看详情
|
||
router.push({
|
||
name: "AI3DGenerate",
|
||
params: { taskId: task.id },
|
||
})
|
||
}
|
||
|
||
// 重试任务
|
||
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()
|
||
if (showAllHistory.value) {
|
||
fetchAllHistory()
|
||
}
|
||
} catch (error) {
|
||
message.error("删除失败")
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
// 获取预览图URL
|
||
const getPreviewUrl = (task: AI3DTask) => {
|
||
if (task.previewUrl) {
|
||
return task.previewUrl.startsWith("http")
|
||
? task.previewUrl
|
||
: task.previewUrl
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = (status: string) => {
|
||
const colors: Record<string, string> = {
|
||
pending: "default",
|
||
processing: "processing",
|
||
completed: "success",
|
||
failed: "error",
|
||
timeout: "warning",
|
||
}
|
||
return colors[status] || "default"
|
||
}
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status: string) => {
|
||
const texts: Record<string, string> = {
|
||
pending: "等待中",
|
||
processing: "生成中",
|
||
completed: "已完成",
|
||
failed: "失败",
|
||
timeout: "超时",
|
||
}
|
||
return texts[status] || status
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string) => {
|
||
return dayjs(time).format("MM-DD HH:mm")
|
||
}
|
||
|
||
// 页面加载
|
||
onMounted(() => {
|
||
fetchHistory()
|
||
|
||
// 检查是否有处理中的任务,有则开启轮询
|
||
const hasProcessing = historyList.value.some(
|
||
(t) => t.status === "pending" || t.status === "processing"
|
||
)
|
||
if (hasProcessing) {
|
||
startPolling()
|
||
}
|
||
})
|
||
|
||
// 页面卸载
|
||
onUnmounted(() => {
|
||
stopPolling()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
// ==========================================
|
||
// Energetic Modern Color Palette
|
||
// ==========================================
|
||
$primary: #7c3aed;
|
||
$primary-light: #a78bfa;
|
||
$primary-dark: #5b21b6;
|
||
$secondary: #06b6d4;
|
||
$accent: #f43f5e;
|
||
$success: #10b981;
|
||
$background: #0a0a12;
|
||
$surface: #12121e;
|
||
$surface-light: #1a1a2e;
|
||
$text: #e2e8f0;
|
||
$text-muted: #94a3b8;
|
||
|
||
// Gradients
|
||
$gradient-primary: linear-gradient(135deg, $primary 0%, #ec4899 100%);
|
||
$gradient-secondary: linear-gradient(135deg, $secondary 0%, $primary 100%);
|
||
|
||
.ai-3d-container {
|
||
display: flex;
|
||
min-height: calc(100vh - 64px);
|
||
background: $background;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// ==========================================
|
||
// Animated Background
|
||
// ==========================================
|
||
.bg-animation {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.bg-gradient {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(120px);
|
||
opacity: 0.4;
|
||
animation: float 25s ease-in-out infinite;
|
||
|
||
&.bg-gradient-1 {
|
||
width: 800px;
|
||
height: 800px;
|
||
background: $primary;
|
||
top: -300px;
|
||
left: -200px;
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
&.bg-gradient-2 {
|
||
width: 600px;
|
||
height: 600px;
|
||
background: $secondary;
|
||
bottom: -200px;
|
||
right: -150px;
|
||
animation-delay: -8s;
|
||
}
|
||
|
||
&.bg-gradient-3 {
|
||
width: 500px;
|
||
height: 500px;
|
||
background: $accent;
|
||
top: 40%;
|
||
right: 30%;
|
||
animation-delay: -16s;
|
||
opacity: 0.3;
|
||
}
|
||
}
|
||
|
||
.bg-grid {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba($primary, 0.03) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba($primary, 0.03) 1px, transparent 1px);
|
||
background-size: 50px 50px;
|
||
animation: gridMove 20s linear infinite;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||
25% { transform: translate(50px, -50px) scale(1.1); }
|
||
50% { transform: translate(-30px, 30px) scale(0.9); }
|
||
75% { transform: translate(-50px, -30px) scale(1.05); }
|
||
}
|
||
|
||
@keyframes gridMove {
|
||
0% { transform: translate(0, 0); }
|
||
100% { transform: translate(50px, 50px); }
|
||
}
|
||
|
||
// ==========================================
|
||
// Left Panel
|
||
// ==========================================
|
||
.left-panel {
|
||
width: 380px;
|
||
height: 100%;
|
||
background: rgba($surface, 0.8);
|
||
backdrop-filter: blur(40px);
|
||
border-right: 1px solid rgba($primary, 0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.panel-logo {
|
||
padding: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
border-bottom: 1px solid rgba($primary, 0.1);
|
||
|
||
.logo-ring {
|
||
position: relative;
|
||
width: 56px;
|
||
height: 56px;
|
||
border: 2px solid transparent;
|
||
border-radius: 50%;
|
||
background: linear-gradient($surface, $surface) padding-box,
|
||
$gradient-primary border-box;
|
||
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 {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.panel-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid rgba($primary, 0.1);
|
||
|
||
:deep(.ant-segmented) {
|
||
background: rgba($surface-light, 0.6);
|
||
border: 1px solid rgba($primary, 0.2);
|
||
|
||
.ant-segmented-item {
|
||
color: $text-muted;
|
||
|
||
&-selected {
|
||
background: $gradient-primary;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.panel-content {
|
||
flex: 1;
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.input-hint {
|
||
font-size: 13px;
|
||
color: $text-muted;
|
||
margin-bottom: 16px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.text-input {
|
||
background: rgba($surface-light, 0.6) !important;
|
||
border: 2px solid rgba($primary, 0.15) !important;
|
||
border-radius: 12px;
|
||
color: $text;
|
||
resize: none;
|
||
transition: all 0.3s;
|
||
|
||
&::placeholder {
|
||
color: rgba($text-muted, 0.5);
|
||
}
|
||
|
||
&:hover {
|
||
border-color: rgba($primary, 0.4) !important;
|
||
}
|
||
|
||
&:focus {
|
||
border-color: $primary !important;
|
||
box-shadow: 0 0 0 4px rgba($primary, 0.1) !important;
|
||
background: rgba($surface-light, 0.8) !important;
|
||
}
|
||
|
||
:deep(.ant-input-data-count) {
|
||
color: $text-muted;
|
||
}
|
||
}
|
||
|
||
.sample-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.sample-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.sample-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: $text;
|
||
}
|
||
|
||
.refresh-icon {
|
||
color: $text-muted;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-size: 14px;
|
||
|
||
&:hover {
|
||
color: $secondary;
|
||
transform: rotate(180deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.sample-prompts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
.sample-tag {
|
||
padding: 6px 14px;
|
||
background: rgba($primary, 0.1);
|
||
border: 1px solid rgba($primary, 0.2);
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
color: $text;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: rgba($secondary, 0.2);
|
||
border-color: $secondary;
|
||
transform: translateY(-2px);
|
||
}
|
||
}
|
||
}
|
||
|
||
.image-upload {
|
||
:deep(.ant-upload-drag) {
|
||
background: rgba($surface-light, 0.6) !important;
|
||
border: 2px dashed rgba($primary, 0.3) !important;
|
||
border-radius: 12px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: $secondary !important;
|
||
background: rgba($surface-light, 0.8) !important;
|
||
}
|
||
|
||
.upload-icon {
|
||
color: $primary-light;
|
||
font-size: 56px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.upload-text {
|
||
color: $text;
|
||
font-size: 15px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-hint {
|
||
color: $text-muted;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.panel-footer {
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
@keyframes gradientShift {
|
||
0%, 100% { background: $gradient-primary; }
|
||
50% { background: $gradient-secondary; }
|
||
}
|
||
|
||
// ==========================================
|
||
// Right Panel
|
||
// ==========================================
|
||
.right-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.intro-section {
|
||
padding: 48px;
|
||
background: rgba($surface, 0.4);
|
||
backdrop-filter: blur(20px);
|
||
border-bottom: 1px solid rgba($primary, 0.1);
|
||
}
|
||
|
||
.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;
|
||
font-weight: 600;
|
||
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 {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.5; transform: scale(0.8); }
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.intro-features {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.feature-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
padding: 20px;
|
||
background: rgba($surface-light, 0.4);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba($primary, 0.1);
|
||
border-radius: 16px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: rgba($surface-light, 0.6);
|
||
border-color: rgba($primary, 0.3);
|
||
transform: translateY(-4px);
|
||
}
|
||
}
|
||
|
||
.feature-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
|
||
&.gradient-1 { background: linear-gradient(135deg, $primary 0%, #ec4899 100%); }
|
||
&.gradient-2 { background: linear-gradient(135deg, $secondary 0%, #10b981 100%); }
|
||
&.gradient-3 { background: linear-gradient(135deg, $accent 0%, #f59e0b 100%); }
|
||
&.gradient-4 { background: linear-gradient(135deg, #8b5cf6 0%, $secondary 100%); }
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// History Section
|
||
// ==========================================
|
||
.history-section {
|
||
flex: 1;
|
||
padding: 32px 48px;
|
||
overflow-y: auto;
|
||
background: rgba($background, 0.5);
|
||
|
||
&::-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);
|
||
}
|
||
}
|
||
}
|
||
|
||
.history-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
}
|
||
|
||
.history-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: $text;
|
||
margin: 0;
|
||
}
|
||
|
||
.history-count {
|
||
font-size: 14px;
|
||
color: $text-muted;
|
||
}
|
||
|
||
.view-all {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: $secondary;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
color: $primary-light;
|
||
transform: translateX(4px);
|
||
}
|
||
}
|
||
|
||
.history-loading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
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) {
|
||
border-right-color: $secondary;
|
||
animation-duration: 2s;
|
||
}
|
||
|
||
&:nth-child(3) {
|
||
border-bottom-color: $accent;
|
||
animation-duration: 2.5s;
|
||
}
|
||
}
|
||
}
|
||
|
||
.history-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.history-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
.history-card {
|
||
background: rgba($surface, 0.6);
|
||
backdrop-filter: blur(20px);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: all 0.4s;
|
||
border: 1px solid rgba($primary, 0.1);
|
||
position: relative;
|
||
|
||
&:hover {
|
||
transform: translateY(-8px);
|
||
border-color: rgba($primary, 0.3);
|
||
|
||
.card-glow {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-glow {
|
||
position: absolute;
|
||
inset: -2px;
|
||
background: $gradient-primary;
|
||
border-radius: 18px;
|
||
z-index: -1;
|
||
opacity: 0;
|
||
filter: blur(20px);
|
||
transition: opacity 0.4s;
|
||
}
|
||
|
||
.card-preview {
|
||
height: 160px;
|
||
background: rgba($surface-light, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.5s;
|
||
}
|
||
|
||
.history-card:hover & .preview-image {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.preview-loading,
|
||
.preview-failed,
|
||
.preview-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: $text-muted;
|
||
font-size: 32px;
|
||
|
||
.loading-text {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.preview-failed {
|
||
color: $accent;
|
||
}
|
||
}
|
||
|
||
.loading-dots {
|
||
display: flex;
|
||
gap: 6px;
|
||
|
||
span {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: $secondary;
|
||
border-radius: 50%;
|
||
animation: dotPulse 1.4s ease-in-out infinite;
|
||
|
||
&:nth-child(1) { animation-delay: 0s; }
|
||
&:nth-child(2) { animation-delay: 0.2s; }
|
||
&:nth-child(3) { animation-delay: 0.4s; }
|
||
}
|
||
}
|
||
|
||
@keyframes dotPulse {
|
||
0%, 60%, 100% { transform: scale(1); opacity: 1; }
|
||
30% { transform: scale(1.5); opacity: 0.7; }
|
||
}
|
||
|
||
.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 {
|
||
background: rgba($secondary, 0.2);
|
||
color: $secondary;
|
||
border-color: rgba($secondary, 0.3);
|
||
}
|
||
|
||
&.status-failed,
|
||
&.status-timeout {
|
||
background: rgba($accent, 0.2);
|
||
color: $accent;
|
||
border-color: rgba($accent, 0.3);
|
||
}
|
||
}
|
||
|
||
.card-info {
|
||
padding: 16px;
|
||
}
|
||
|
||
.card-desc {
|
||
font-size: 14px;
|
||
color: $text;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.card-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-time {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: $text-muted;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.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;
|
||
|
||
&:hover {
|
||
background: rgba($secondary, 0.2);
|
||
border-color: $secondary;
|
||
color: $secondary;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
&.danger {
|
||
&:hover {
|
||
background: rgba($accent, 0.2);
|
||
border-color: $accent;
|
||
color: $accent;
|
||
}
|
||
}
|
||
|
||
&.disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
|
||
&:hover {
|
||
background: rgba($primary, 0.1);
|
||
border-color: rgba($primary, 0.2);
|
||
color: $primary-light;
|
||
transform: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// Drawer
|
||
// ==========================================
|
||
.history-drawer {
|
||
:deep(.ant-drawer-header) {
|
||
background: $surface;
|
||
border-bottom-color: rgba($primary, 0.1);
|
||
}
|
||
|
||
:deep(.ant-drawer-body) {
|
||
background: $background;
|
||
}
|
||
}
|
||
|
||
.list-preview {
|
||
width: 72px;
|
||
height: 72px;
|
||
background: $surface-light;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.list-preview-placeholder {
|
||
color: $text-muted;
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
.list-desc {
|
||
max-width: 340px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: $text;
|
||
}
|
||
|
||
.list-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 13px;
|
||
color: $text-muted;
|
||
}
|
||
|
||
.danger-link {
|
||
color: $accent;
|
||
|
||
&:hover {
|
||
color: #ff7875;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// Responsive
|
||
// ==========================================
|
||
@media (max-width: 1024px) {
|
||
.left-panel {
|
||
width: 320px;
|
||
}
|
||
|
||
.intro-features {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.history-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.ai-3d-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.left-panel {
|
||
width: 100%;
|
||
border-right: none;
|
||
border-bottom: 1px solid rgba($primary, 0.1);
|
||
}
|
||
|
||
.intro-section {
|
||
padding: 32px 24px;
|
||
}
|
||
|
||
.history-section {
|
||
padding: 24px;
|
||
}
|
||
|
||
.history-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
}
|
||
}
|
||
</style>
|