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">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<EditOutlined class="label-icon" />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
<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>
|
2026-01-14 14:48:35 +08:00
|
|
|
|
<div class="textarea-wrapper">
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="textContent"
|
|
|
|
|
|
placeholder="例如:一只卡通风格的橙色小猫,蓝色的大眼睛,尾巴卷曲..."
|
|
|
|
|
|
:rows="5"
|
|
|
|
|
|
:maxlength="1024"
|
|
|
|
|
|
class="text-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="char-count">{{ textContent.length }}/1024</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
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>
|
2026-01-14 14:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 模型设置 -->
|
|
|
|
|
|
<div class="model-settings">
|
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
|
<div class="setting-item">
|
|
|
|
|
|
<span class="setting-label">模型类型</span>
|
|
|
|
|
|
<a-select v-model:value="generateType" class="setting-select">
|
|
|
|
|
|
<a-select-option value="Normal">带纹理</a-select-option>
|
|
|
|
|
|
<a-select-option value="LowPoly">低多边形</a-select-option>
|
|
|
|
|
|
<a-select-option value="Geometry">白模</a-select-option>
|
|
|
|
|
|
<a-select-option value="Sketch">草图</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="setting-item">
|
|
|
|
|
|
<span class="setting-label">模型面数</span>
|
|
|
|
|
|
<a-select v-model:value="faceCount" class="setting-select">
|
|
|
|
|
|
<a-select-option :value="50000">5万面</a-select-option>
|
|
|
|
|
|
<a-select-option :value="100000">10万面</a-select-option>
|
|
|
|
|
|
<a-select-option :value="300000">30万面</a-select-option>
|
|
|
|
|
|
<a-select-option :value="500000">50万面</a-select-option>
|
|
|
|
|
|
<a-select-option :value="1000000">100万面</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-13 14:01:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 图生3D上传 -->
|
|
|
|
|
|
<div v-else class="image-input-section">
|
2026-01-13 16:41:12 +08:00
|
|
|
|
<div class="input-label">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<PictureOutlined class="label-icon" />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
<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
|
2026-01-14 14:48:35 +08:00
|
|
|
|
:file-list="[]"
|
2026-01-13 14:01:17 +08:00
|
|
|
|
:before-upload="handleBeforeUpload"
|
2026-01-14 14:48:35 +08:00
|
|
|
|
:show-upload-list="false"
|
2026-01-13 14:01:17 +08:00
|
|
|
|
:max-count="1"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
class="image-upload"
|
|
|
|
|
|
>
|
2026-01-14 14:48:35 +08:00
|
|
|
|
<template v-if="imageUrl">
|
|
|
|
|
|
<img :src="imageUrl" alt="预览" class="upload-preview-image" />
|
|
|
|
|
|
<div class="upload-change-hint">点击更换图片</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<p class="upload-icon">
|
|
|
|
|
|
<PictureOutlined />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="upload-text">点击或拖拽图片到此处</p>
|
|
|
|
|
|
<p class="upload-hint">支持 JPG、PNG 格式,最大 10MB</p>
|
|
|
|
|
|
</template>
|
2026-01-13 14:01:17 +08:00
|
|
|
|
</a-upload-dragger>
|
2026-01-14 14:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 模型类型选择 -->
|
|
|
|
|
|
<div class="model-settings">
|
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
|
<div class="setting-item full-width">
|
|
|
|
|
|
<span class="setting-label">模型类型</span>
|
|
|
|
|
|
<a-select v-model:value="generateType" class="setting-select">
|
|
|
|
|
|
<a-select-option value="Normal">带纹理</a-select-option>
|
|
|
|
|
|
<a-select-option value="LowPoly">低多边形</a-select-option>
|
|
|
|
|
|
<a-select-option value="Geometry">白模</a-select-option>
|
|
|
|
|
|
<a-select-option value="Sketch">草图</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-13 14:01:17 +08:00
|
|
|
|
</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-15 09:28:22 +08:00
|
|
|
|
<h1 class="intro-title">用一句话、一张图,创造你的 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">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<BulbOutlined />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
</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">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<EyeOutlined />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
</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">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<FolderOutlined />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
</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">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<SyncOutlined />
|
2026-01-13 16:41:12 +08:00
|
|
|
|
</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)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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-15 09:28:22 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 悬停显示的操作按钮 -->
|
|
|
|
|
|
<div class="card-actions-overlay" @click.stop>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="task.status === 'completed'"
|
|
|
|
|
|
class="overlay-btn btn-primary"
|
|
|
|
|
|
@click="handlePreview(task)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EyeOutlined />
|
|
|
|
|
|
<span>预览</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="['failed', 'timeout'].includes(task.status)"
|
|
|
|
|
|
class="overlay-btn btn-primary"
|
|
|
|
|
|
:disabled="task.retryCount >= 3"
|
|
|
|
|
|
@click="handleRetry(task)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ReloadOutlined />
|
|
|
|
|
|
<span>重试</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="overlay-btn btn-secondary"
|
|
|
|
|
|
@click="handleDelete(task)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DeleteOutlined />
|
|
|
|
|
|
<span>删除</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</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-15 09:28:22 +08:00
|
|
|
|
<span class="card-type">
|
|
|
|
|
|
{{ task.inputType === "text" ? "文生3D" : "图生3D" }}
|
|
|
|
|
|
</span>
|
2026-01-13 14:01:17 +08:00
|
|
|
|
</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-15 09:28:22 +08:00
|
|
|
|
BulbOutlined,
|
|
|
|
|
|
FolderOutlined,
|
|
|
|
|
|
SyncOutlined,
|
|
|
|
|
|
EditOutlined,
|
2026-01-13 14:01:17 +08:00
|
|
|
|
} from "@ant-design/icons-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 imageUrl = ref("")
|
|
|
|
|
|
const generating = ref(false)
|
|
|
|
|
|
const currentSampleIndex = ref(0)
|
|
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
// 模型设置
|
|
|
|
|
|
const generateType = ref<"Normal" | "LowPoly" | "Geometry" | "Sketch">("Normal")
|
|
|
|
|
|
const faceCount = ref(500000)
|
|
|
|
|
|
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 历史记录
|
|
|
|
|
|
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 {
|
2026-01-14 14:48:35 +08:00
|
|
|
|
const result: any = await uploadFile(file)
|
|
|
|
|
|
// 兼容不同的响应格式
|
|
|
|
|
|
const url = result.data?.url || result.url
|
|
|
|
|
|
if (url) {
|
|
|
|
|
|
imageUrl.value = url
|
|
|
|
|
|
message.success("图片上传成功")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error("上传失败:无法获取图片地址")
|
|
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
} catch (error) {
|
2026-01-14 14:48:35 +08:00
|
|
|
|
console.error("上传失败:", error)
|
2026-01-13 14:01:17 +08:00
|
|
|
|
message.error("图片上传失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-14 14:48:35 +08:00
|
|
|
|
// 构建请求参数
|
|
|
|
|
|
const params: any = {
|
2026-01-13 14:01:17 +08:00
|
|
|
|
inputType: inputType.value,
|
|
|
|
|
|
inputContent: content,
|
2026-01-14 14:48:35 +08:00
|
|
|
|
generateType: generateType.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 文生3D时添加模型面数参数
|
|
|
|
|
|
if (inputType.value === "text") {
|
|
|
|
|
|
params.faceCount = faceCount.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const task = await createAI3DTask(params)
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 清空输入
|
|
|
|
|
|
if (inputType.value === "text") {
|
|
|
|
|
|
textContent.value = ""
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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;
|
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;
|
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-15 09:28:22 +08:00
|
|
|
|
color: $primary;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
}
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
.textarea-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.char-count {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 8px;
|
|
|
|
|
|
right: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 14:01:17 +08:00
|
|
|
|
.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-14 14:48:35 +08:00
|
|
|
|
padding-bottom: 28px !important;
|
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
|
|
|
|
}
|
2026-01-14 14:48:35 +08:00
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
// 模型设置
|
|
|
|
|
|
.model-settings {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: rgba($primary, 0.05);
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.15);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.setting-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
&.full-width {
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-select {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-select-selector) {
|
|
|
|
|
|
background: rgba($surface, 0.8) !important;
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.2) !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
height: 36px !important;
|
|
|
|
|
|
|
|
|
|
|
|
.ant-select-selection-item {
|
|
|
|
|
|
line-height: 34px !important;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover :deep(.ant-select-selector) {
|
|
|
|
|
|
border-color: rgba($primary, 0.4) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.ant-select-focused :deep(.ant-select-selector) {
|
|
|
|
|
|
border-color: $primary !important;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba($primary, 0.1) !important;
|
|
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 16:41:12 +08:00
|
|
|
|
.sample-section {
|
2026-01-14 14:48:35 +08:00
|
|
|
|
margin-top: 16px;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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-14 14:48:35 +08:00
|
|
|
|
overflow: hidden;
|
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-14 14:48:35 +08:00
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
:deep(.ant-upload) {
|
|
|
|
|
|
padding: 24px !important;
|
|
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
:deep(.upload-icon) {
|
|
|
|
|
|
color: $primary-light;
|
|
|
|
|
|
font-size: 56px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
2026-01-14 14:48:35 +08:00
|
|
|
|
:deep(.upload-text) {
|
|
|
|
|
|
color: $text;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.upload-hint) {
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.upload-preview-image) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
max-height: 180px;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.upload-change-hint) {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-upload-drag:hover .upload-change-hint) {
|
|
|
|
|
|
color: $primary-light;
|
2026-01-13 14:01:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-footer {
|
2026-01-13 16:41:12 +08:00
|
|
|
|
padding: 24px;
|
|
|
|
|
|
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-15 09:28:22 +08:00
|
|
|
|
padding-top: 30px;
|
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-15 09:28:22 +08:00
|
|
|
|
text-align: center;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.intro-title {
|
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
line-height: 1.3;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
$primary 0%,
|
|
|
|
|
|
$primary-light 40%,
|
|
|
|
|
|
#8b5cf6 70%,
|
|
|
|
|
|
#a855f7 100%
|
|
|
|
|
|
);
|
2026-01-13 16:41:12 +08:00
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.intro-title {
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 16:41:12 +08:00
|
|
|
|
.intro-desc {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
line-height: 1.6;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
margin-right: auto;
|
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;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
color: #fff;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
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;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
border-radius: 12px;
|
2026-01-13 14:01:17 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
cursor: pointer;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
position: relative;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// border: 1px solid rgba(0, 0, 0, 0.06);
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.card-preview {
|
|
|
|
|
|
transform: scale(1.03);
|
|
|
|
|
|
}
|
2026-01-13 16:41:12 +08:00
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.card-preview .preview-image {
|
|
|
|
|
|
transform: scale(1.05);
|
2026-01-13 16:41:12 +08:00
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.card-actions-overlay {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// border-radius: 8px;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
rgba($surface-light, 0.9) 0%,
|
|
|
|
|
|
rgba($primary, 0.05) 100%
|
|
|
|
|
|
);
|
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-15 09:28:22 +08:00
|
|
|
|
transition: transform 0.3s ease;
|
2026-01-13 14:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
.preview-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 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;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
rgba($error, 0.15) 0%,
|
|
|
|
|
|
rgba($error, 0.25) 100%
|
|
|
|
|
|
);
|
2026-01-14 14:29:16 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 悬停时显示的操作按钮遮罩层
|
|
|
|
|
|
.card-actions-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
to top,
|
|
|
|
|
|
rgba(0, 0, 0, 0.6) 0%,
|
|
|
|
|
|
rgba(0, 0, 0, 0.2) 50%,
|
|
|
|
|
|
transparent 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
|
|
|
|
|
|
.overlay-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
|
|
|
|
|
|
&.btn-primary {
|
|
|
|
|
|
background: $primary;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: $primary-light;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.btn-secondary {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
color: $text;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:29:16 +08:00
|
|
|
|
@keyframes pulse-error {
|
2026-01-15 09:28:22 +08:00
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
2026-01-14 14:29:16 +08:00
|
|
|
|
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-15 09:28:22 +08:00
|
|
|
|
background: $surface;
|
|
|
|
|
|
border-top: 1px solid rgba($primary, 0.06);
|
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-15 09:28:22 +08:00
|
|
|
|
line-height: 1.4;
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.card-type {
|
|
|
|
|
|
display: inline-flex;
|
2026-01-13 16:41:12 +08:00
|
|
|
|
align-items: center;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
135deg,
|
|
|
|
|
|
rgba($primary, 0.1) 0%,
|
|
|
|
|
|
rgba($primary-light, 0.15) 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
border: 1px solid rgba($primary, 0.2);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: $primary;
|
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-15 09:28:22 +08:00
|
|
|
|
|
|
|
|
|
|
.card-actions-overlay {
|
|
|
|
|
|
.overlay-btn {
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 16:41:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 14:01:17 +08:00
|
|
|
|
</style>
|