修改:后视图生成

This commit is contained in:
Linzm committed 2026-01-22 16:33:55 +08:00
1 parent 43471c3a76
commit 9ede62f450
16 files changed
+921 -74

No files matched your search

+36 -67
View File
@@ -88,53 +88,6 @@
</div>
</div>
</div>
<div class="step-line" v-if="shouldShowPhotoExample">
<div class="step-line-item">
<div class="step-line-item-title">
<span style="display: flex; align-items: center;justify-content: center;">
<span :style="`height: 1px; background: ${config.styles.colors.border};width: 50px;`"></span>
<span :style="`margin: 0 8px; color: ${config.styles.colors.textTertiary};`">{{ toValueWithout("照片示例") }}</span>
<span :style="`height: 1px; background: ${config.styles.colors.border};width: 50px;`"></span>
</span>
</div>
<div class="step-line-item-desc">
<div class="step-line-item-desc-item">
<img :src="example1" :style="`width: 20vw;height: 20vw;border-radius: 8px;`" alt="">
</div>
<div class="step-line-item-desc-item">
<img :src="example2" :style="`width: 20vw;height: 20vw;border-radius: 8px;`" alt="">
</div>
<div class="step-line-item-desc-item">
<img :src="example3" :style="`width: 20vw;height: 20vw;border-radius: 8px;`" alt="">
</div>
<div class="step-line-item-desc-item">
<img :src="example4" :style="`width: 20vw;height: 20vw;border-radius: 8px;`" alt="">
</div>
</div>
</div>
</div>
<div class="photo-upload-body">
<div v-if="!picture" class="photo-upload-box">
<div class="photo-upload-area">
<div class="photo-upload-plus">
+
</div>
<div class="photo-upload-text">
{{ toValueWithout("点击上传照片") }}
</div>
<div class="photo-upload-tip">
*{{ toValueWithout("上传照片时建议勾选[原图]") }}
</div>
</div>
<div class="photo-upload-footer">
{{ toValueWithout("请确定您对上传的照片拥有合法使用权利或已取得他人合法授权,且同意本平台分析图片信息以提供生成服务") }}
</div>
</div>
<img class="photo-upload-img" v-if="picture" :src="picture" alt="" srcset="">
<div class="photo-upload-box-1">
<h5-cropper :option="option" @getbase64Data="getbase64Data" @imgorigoinf="imgorigoinf"></h5-cropper>
</div>
</div>
<div v-if="shouldShowKindList">
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
{{ toValueWithout("风格类型") }}
@@ -147,7 +100,7 @@
</div>
</div>
</div>
<div v-if="shouldShowMultiView">
<div v-if="isViews == 1">
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
{{ toValueWithout("多视角参考图") }}
</div>
@@ -176,7 +129,7 @@
<div class="mounts-item-list">
<img
class="mounts-item-image"
:src="getImageUrl(item.cover_path)"
:src="item.image"
:alt="toValueWithout(item.name)"
/>
<van-icon
@@ -241,6 +194,28 @@
</div>
</div>
</div>
<div class="photo-upload-body">
<div v-if="!picture" class="photo-upload-box">
<div class="photo-upload-area">
<div class="photo-upload-plus">
+
</div>
<div class="photo-upload-text">
{{ toValueWithout("点击上传照片") }}
</div>
<div class="photo-upload-tip">
*{{ toValueWithout("上传照片时建议勾选[原图]") }}
</div>
</div>
<div class="photo-upload-footer">
{{ toValueWithout("请确定您对上传的照片拥有合法使用权利或已取得他人合法授权,且同意本平台分析图片信息以提供生成服务") }}
</div>
</div>
<img class="photo-upload-img" v-if="picture" :src="picture" alt="" srcset="">
<div class="photo-upload-box-1">
<h5-cropper :option="option" @getbase64Data="getbase64Data" @imgorigoinf="imgorigoinf"></h5-cropper>
</div>
</div>
<div :style="`height: 120px;`"></div>
<div class="design-action-bar">
<div class="design-left">
@@ -268,11 +243,6 @@ import { toValueWithout } from '@/lang/utils'
import { cartoonConfig } from '@/config/cartoon'
import leafIcon from '@/assets/badge/leaf.png'
import arrowIcon from '@/assets/badge/arrow.png'
import example1 from '@/assets/badge/1.png'
import example2 from '@/assets/badge/2.png'
import example3 from '@/assets/badge/3.png'
import example4 from '@/assets/badge/4.png'
// Element Plus 图标通过组件自动导入,这里使用文本符号作为替代
// 初始化翻译功能
const { t } = useTranslation();
@@ -332,6 +302,7 @@ const typeId = ref(0)
const subjectId = ref(0)
const typeName = ref('')
const subjectName = ref('')
const isViews = ref(0)
const getOrderStat = () => {
badgeApi.getOrderStat({}).then((res: any) => {
orderStat.value = res
@@ -341,9 +312,9 @@ const getOrderStat = () => {
typeName.value = res.type_name
subjectId.value = res.subject_id
subjectName.value = res.subject_name
if (res.type_id === 4) {
getKindList()
} else if (res.type_id === 8) {
isViews.value = res.is_views
getKindList()
if (res.type_id === 8) {
mountsId.value = 0
getmountsList()
} else if (res.type_id === 9) {
@@ -361,12 +332,7 @@ const shouldShowPhotoExample = computed(() => {
// 计算属性:是否显示风格类型
const shouldShowKindList = computed(() => {
return typeId.value === 4 && kindList.value.length > 0
})
// 计算属性:是否显示多视角参考图
const shouldShowMultiView = computed(() => {
return typeId.value === 4 && config.productLimits.multiViewSupported.includes(prodId.value)
return kindList.value.length > 0
})
const kindList = ref([])
@@ -403,7 +369,10 @@ const getmountsList = () => {
}
badgeApi.getMountsList({
prod_id: prodId.value,
type_id: typeId.value
type_id: typeId.value,
status: 1,
page: 1,
size: 1000
}).then((res: any) => {
if (res && res.list && res.list.length > 0) {
mountsList.value = res.list
@@ -645,7 +614,7 @@ const getPid = async () => {
sendToOss(imgurl.value, res.url),
]
await Promise.all(uploadTasks)
if (shouldShowMultiView.value && referPicture.value.length > 0) {
if (isViews.value == 1 && referPicture.value.length > 0) {
const multiUrlTasks = referPicture.value.map((item) => {
return badgeApi.getMultiUrl({
pid: pid.value,
@@ -1064,7 +1033,7 @@ onMounted(() => {
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
padding: 16px 36px;
padding: 16px;
}
.kind-box-item {
margin-right: 10px;
@@ -1114,7 +1083,7 @@ onMounted(() => {
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
border-radius: 8px;
transition: transform 0.2s;
}
+661 -3
View File
@@ -103,9 +103,149 @@
</div>
</div>
<!-- 后视图 -->
<div>
<div v-if="isViews === 0" style="padding: 0 16px; margin-top: 16px;">
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-bottom: 12px;`">
{{ toValueWithout("后视图") }}
</div>
<div :style="`font-size: 14px; color: ${config.styles.colors.textSecondary}; margin-bottom: 12px; line-height: 1.6;`">
<p>{{ toValueWithout("多主体手办下单前需先生成正确的后视图") }}</p>
</div>
<!-- 提示词输入框 -->
<div style="margin-bottom: 12px; position: relative;">
<el-input
v-model="rearViewPromptText"
type="textarea"
:rows="4"
:placeholder="toValueWithout('请输入提示词,描述需要生成的后视图要求')"
:disabled="isUsingPresetPrompt"
class="rear-view-textarea"
@input="handlePromptTextInput"
/>
<el-button
v-if="isUsingPresetPrompt"
type="text"
size="small"
style="position: absolute; bottom: 8px; right: 8px;"
@click="enableEditPrompt"
>
{{ toValueWithout("编辑") }}
</el-button>
</div>
<!-- 操作按钮 -->
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<el-button
type="success"
@click="handleGenerateRearView"
:loading="generatingRearView"
:disabled="hasGeneratingRearView || generatingRearView"
>
{{ toValueWithout("生成后视图") }}
</el-button>
<el-button type="default" @click="handlePresetRearViewPrompt">
{{ toValueWithout("预设提示词") }}
</el-button>
</div>
<!-- 后视图记录 -->
<div class="rear-view-record-section">
<el-tabs v-model="rearViewActiveTab">
<el-tab-pane :label="toValueWithout('后视图记录')" name="record">
<div class="rear-view-image-grid">
<!-- 生成中的占位 -->
<div v-if="generatingRearView && rearViewList.length === 0" class="rear-view-image-item loading">
<div class="loading-placeholder">
{{ toValueWithout("生成中...") }}
</div>
</div>
<!-- 已生成的后视图 -->
<div
v-for="(item, index) in rearViewList"
:key="item.unique_no || item.id || index"
class="rear-view-image-item"
:class="{ active: selectedRearViewIndex === index }"
@click="selectRearViewImage(index)"
>
<!-- 图片为空时显示生成中 -->
<div v-if="item.status == 0" class="loading-placeholder">
<van-loading color="#1989fa" />
<span>{{ toValueWithout("生成中,请稍后...") }}</span>
</div>
<el-image
v-else
:src="getRearViewImageUrl(item.url)"
fit="cover"
class="rear-view-image"
/>
<div v-if="selectedRearViewIndex === index" class="rear-view-image-actions">
<el-button
size="small"
:disabled="item.status == 0"
@click.stop="handleMirrorRearView(index)"
>
{{ toValueWithout("镜像") }}
</el-button>
<el-button
type="success"
size="small"
:disabled="item.status == 0"
@click.stop="confirmSelectRearView(index)"
>
{{ toValueWithout("选择") }}
</el-button>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="toValueWithout('后视图要求')" name="requirements">
<div class="rear-view-requirements-content">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
<div class="requirements-text">
<p>{{ toValueWithout("1. 后视图应与前视图保持角色和动作的一致性") }}</p>
<p>{{ toValueWithout("2. 模型需要水平旋转180度") }}</p>
<p>{{ toValueWithout("3. 确保后视图清晰可见,无明显遮挡") }}</p>
<p>{{ toValueWithout("4. 后视图应展示角色的背面特征") }}</p>
</div>
</template>
</el-alert>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
<!-- 预设提示词选择弹窗 -->
<el-dialog
:title="toValueWithout('选择预设提示词')"
v-model="presetRearViewPromptDialogVisible"
width="600px"
:close-on-click-modal="false"
>
<div class="preset-prompt-list">
<div
v-for="(tip, index) in rearViewTipsList"
:key="tip.id || index"
class="preset-prompt-item"
@click="selectPresetRearViewPrompt(tip)"
>
<div class="prompt-title">{{ tip.title || `${toValueWithout('提示词')} ${String(index + 1)}` }}</div>
<div class="prompt-content">{{ tip.content }}</div>
</div>
<div v-if="rearViewTipsList.length === 0" class="empty-tips">
{{ toValueWithout("暂无预设提示词") }}
</div>
</div>
<template #footer>
<el-button @click="presetRearViewPromptDialogVisible = false">{{ toValueWithout("取消") }}</el-button>
</template>
</el-dialog>
<div style="padding: 0 16px;">
<van-divider />
</div>
@@ -179,9 +319,9 @@
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted, nextTick } from 'vue';
import { onMounted, ref, onUnmounted, nextTick, watch, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { showSuccessToast, showToast, showConfirmDialog } from 'vant';
import { showSuccessToast, showToast, showConfirmDialog, Loading } from 'vant';
import { localStorage } from '@/utils/local-storage'
import * as badgeApi from '@/api/badge'
import { areaList } from '@/utils/area'
@@ -966,6 +1106,7 @@ const orderStat = ref({})
const typeId = ref(0)
const subjectId = ref(0)
const typeName = ref('')
const isViews = ref(0)
const getOrderStat = () => {
badgeApi.getOrderStat({}).then((res: any) => {
console.log('getOrderStat', res)
@@ -974,15 +1115,373 @@ const getOrderStat = () => {
subjectId.value = res.subject_id
typeName.value = res.type_name
badgeTypeId.value = res.type_id
isViews.value = res.is_views || 0
getSizeList()
// 如果是多视图,加载后视图列表
if (isViews.value === 1) {
loadRearViewList()
}
})
}
// 后视图相关状态
const rearViewActiveTab = ref('record')
const generatingRearView = ref(false)
const rearViewPromptText = ref('')
const isUsingPresetPrompt = ref(false) // 标记是否使用预设提示词
const rearViewList = ref<any[]>([])
const selectedRearViewIndex = ref<number | null>(null)
const selectedRearView = ref<any>(null)
const rearViewTipsList = ref<any[]>([])
const presetRearViewPromptDialogVisible = ref(false)
const rearViewPollingTimer = ref<NodeJS.Timeout | null>(null)
const isRearViewPolling = ref(false)
const isLoadingRearViewList = ref(false)
// 检查是否有正在生成的后视图
const hasGeneratingRearView = computed(() => {
return rearViewList.value.some((item: any) => item.status === 0)
})
// 获取后视图图片URL
function getRearViewImageUrl(url: string) {
if (!url) return ''
if (url.indexOf('http') === 0) {
return url
}
return `https://3dview.suwa3d.com/${url}?t=${Date.now()}`
}
// 处理预设提示词
function handlePresetRearViewPrompt() {
if (rearViewTipsList.value.length === 0) {
showToast({
message: toValueWithout('暂无预设提示词'),
duration: 2000,
})
return
}
// 如果只有一条数据,直接显示在输入框中
if (rearViewTipsList.value.length === 1) {
rearViewPromptText.value = rearViewTipsList.value[0].content || ''
isUsingPresetPrompt.value = true // 标记为使用预设提示词,禁用输入框
return
}
// 如果有多条数据,弹出选择弹窗
presetRearViewPromptDialogVisible.value = true
}
// 选择预设提示词
function selectPresetRearViewPrompt(tip: any) {
rearViewPromptText.value = tip.content || ''
isUsingPresetPrompt.value = true // 标记为使用预设提示词
presetRearViewPromptDialogVisible.value = false
}
// 处理提示词输入(用户手动输入时)
function handlePromptTextInput() {
// 如果用户手动输入,取消预设提示词标记
isUsingPresetPrompt.value = false
}
// 启用编辑提示词(从预设提示词切换到可编辑模式)
function enableEditPrompt() {
isUsingPresetPrompt.value = false
}
// 生成后视图
async function handleGenerateRearView() {
// 检查是否有正在生成的后视图
if (hasGeneratingRearView.value) {
showToast({
message: toValueWithout('请等待前一张后视图生成完成'),
duration: 2000,
})
return
}
if (!pid.value) {
showToast({
message: toValueWithout('订单ID不能为空'),
duration: 2000,
})
return
}
if (!rearViewPromptText.value || rearViewPromptText.value.trim() === '') {
showToast({
message: toValueWithout('请输入后视图提示词'),
duration: 2000,
})
return
}
try {
generatingRearView.value = true
// 调用生成后视图接口
await badgeApi.generateViews({
pid: pid.value,
pos: 3,
size_type: 2,
prompt: rearViewPromptText.value || ''
})
showSuccessToast({
message: toValueWithout('后视图生成提交成功'),
duration: 2000,
})
// 先加载一次数据
await loadRearViewList()
// 启动轮询
startRearViewPolling()
} catch (err: any) {
console.error('生成后视图提交失败:', err)
showToast({
message: err.message || toValueWithout('生成后视图提交失败,请重试'),
duration: 2000,
})
} finally {
generatingRearView.value = false
}
}
// 选择图片
function selectRearViewImage(index: number) {
selectedRearViewIndex.value = index
selectedRearView.value = rearViewList.value[index]
}
// 镜像处理
async function handleMirrorRearView(index: number) {
if (rearViewList.value[index]) {
await badgeApi.flipViews({
unique_no: rearViewList.value[index].unique_no,
}).then((res: any) => {
showSuccessToast({
message: toValueWithout('镜像处理成功'),
duration: 2000,
})
// 镜像处理后重新加载,但不影响轮询
loadRearViewList(false)
}).catch((err: any) => {
showToast({
message: toValueWithout('镜像处理失败'),
duration: 2000,
})
})
}
}
// 确认选择
function confirmSelectRearView(index: number) {
selectRearViewImage(index)
handleConfirmRearView()
}
// 确认后视图
function handleConfirmRearView() {
if (!selectedRearView.value) {
showToast({
message: toValueWithout('请先选择后视图'),
duration: 2000,
})
return
}
badgeApi.confirmViews({
unique_no: selectedRearView.value.unique_no,
}).then((res: any) => {
showSuccessToast({
message: toValueWithout('后视图确认成功'),
duration: 2000,
})
}).catch((err: any) => {
showToast({
message: err.message && err.message.indexOf('文件不存在') > -1 ? toValueWithout('图片不存在') : toValueWithout('后视图确认失败'),
duration: 2000,
})
})
}
// 加载后视图记录
// isPollingRequest: 是否为轮询请求(轮询请求不会更新 tips,避免闪烁)
async function loadRearViewList(isPollingRequest: boolean = false) {
if (!pid.value) return
// 防止并发请求
if (isLoadingRearViewList.value) {
return
}
try {
isLoadingRearViewList.value = true
const res: any = await badgeApi.listViews({
pid: pid.value,
})
console.log('后视图列表===》', res)
// 保存 tips 数据(只在非轮询请求时更新,避免闪烁)
if (!isPollingRequest) {
if (res && Array.isArray(res.tips)) {
rearViewTipsList.value = res.tips
} else {
rearViewTipsList.value = []
}
}
if (res && Array.isArray(res.list)) {
const newList = res.list.map((item: any) => ({
url: item.path,
id: item.id,
unique_no: item.unique_no,
status: item.status,
}))
// 无感更新:比较新旧数据,只更新变化的部分
await updateRearViewList(newList)
// 检查是否所有项都已完成(status=1),如果是则停止轮询
const hasGenerating = newList.some((item: any) => item.status === 0)
if (hasGenerating && isPollingRequest) {
// 如果还有生成中的项且是轮询请求,继续轮询
// 轮询逻辑在 startRearViewPolling 中处理
} else if (!hasGenerating) {
// 所有项都已完成,停止轮询
stopRearViewPolling()
}
}
} catch (err) {
console.error('加载后视图列表失败:', err)
// 如果轮询请求失败,停止轮询
if (isPollingRequest) {
stopRearViewPolling()
}
} finally {
isLoadingRearViewList.value = false
}
}
// 无感更新后视图列表,避免闪烁
async function updateRearViewList(newList: any[]) {
// 使用 nextTick 确保在 DOM 更新后执行
await nextTick()
// 如果列表为空,直接替换
if (rearViewList.value.length === 0) {
rearViewList.value = newList
return
}
// 创建以 unique_no 为 key 的映射,方便查找
const existingMap = new Map<number | string, any>()
rearViewList.value.forEach((item: any, index: number) => {
if (item.unique_no) {
existingMap.set(item.unique_no, { item, index })
}
})
// 记录需要移除的索引(从后往前删除,避免索引变化)
const toRemove: number[] = []
const newUniqueNos = new Set(newList.map((item: any) => item.unique_no).filter(Boolean))
// 标记需要移除的项
rearViewList.value.forEach((item: any, index: number) => {
if (item.unique_no && !newUniqueNos.has(item.unique_no)) {
toRemove.push(index)
}
})
// 从后往前删除,避免索引变化
toRemove.reverse().forEach((index: number) => {
rearViewList.value.splice(index, 1)
})
// 更新或添加新项
newList.forEach((newItem: any) => {
if (newItem.unique_no) {
const existing = existingMap.get(newItem.unique_no)
if (existing) {
// 更新现有项,使用 Object.assign 确保响应式更新
const existingItem = existing.item
const hasStatusChange = existingItem.status !== newItem.status
const hasUrlChange = existingItem.url !== newItem.url
if (hasStatusChange || hasUrlChange) {
// 只更新变化的属性,避免不必要的响应式触发
if (hasStatusChange) {
existingItem.status = newItem.status
}
if (hasUrlChange) {
existingItem.url = newItem.url
}
}
} else {
// 新项,添加到列表末尾
rearViewList.value.push(newItem)
}
}
})
// 保持选中状态
await nextTick()
if (selectedRearView.value && selectedRearView.value.unique_no) {
const foundIndex = rearViewList.value.findIndex(
(item: any) => item.unique_no === selectedRearView.value?.unique_no
)
if (foundIndex !== -1) {
selectedRearViewIndex.value = foundIndex
selectedRearView.value = rearViewList.value[foundIndex]
} else {
selectedRearViewIndex.value = null
selectedRearView.value = null
}
} else if (selectedRearViewIndex.value !== null && rearViewList.value[selectedRearViewIndex.value]) {
selectedRearView.value = rearViewList.value[selectedRearViewIndex.value]
}
}
// 启动轮询
function startRearViewPolling() {
// 先清除之前的轮询
stopRearViewPolling()
// 设置轮询标志
isRearViewPolling.value = true
// 每10秒轮询一次
rearViewPollingTimer.value = setInterval(async () => {
// 确保轮询标志仍然为 true(防止被其他操作中断)
if (isRearViewPolling.value && !isLoadingRearViewList.value) {
// 轮询请求,传入 true 标识
await loadRearViewList(true)
// 检查是否还有生成中的项,如果没有则停止轮询
const hasGenerating = rearViewList.value.some((item: any) => item.status === 0)
if (!hasGenerating) {
stopRearViewPolling()
}
}
}, 10000)
}
// 停止轮询
function stopRearViewPolling() {
isRearViewPolling.value = false
if (rearViewPollingTimer.value) {
clearInterval(rearViewPollingTimer.value)
rearViewPollingTimer.value = null
}
}
function handleBeforeUnload(_event: BeforeUnloadEvent) {
// code 的清除已在 main.ts 中统一处理,这里只处理页面特定的清理逻辑
// 清除轮询和进度条计时器
clearInterval(timer.value)
clearInterval(progressTimer.value)
stopRearViewPolling()
// userId 的清除由 main.ts 统一处理
}
@@ -1018,11 +1517,31 @@ onMounted(() => {
onUnmounted(() => {
clearInterval(timer.value)
clearInterval(progressTimer.value)
stopRearViewPolling()
})
const searchResult = ref([])
// 监听 isViews 变化,如果变为 1 则加载后视图列表
watch(() => isViews.value, (val) => {
if (val === 1 && pid.value) {
loadRearViewList()
} else {
stopRearViewPolling()
}
})
</script>
<style scoped>
/* 后视图标签页样式 */
.rear-view-record-section .el-tabs__item.is-active,
.rear-view-record-section .el-tabs__item:hover {
color: #07c160!important;
}
.rear-view-record-section .el-tabs__active-bar {
background-color: #07c160!important;
}
.preview-container {
min-height: 100vh;
font-family: 'PingFang SC', Arial, sans-serif;
@@ -1481,4 +2000,143 @@ const searchResult = ref([])
.van-address-edit {
padding: 0!important;
}
/* 后视图相关样式 */
.rear-view-textarea {
width: 100%;
}
.rear-view-record-section {
margin-top: 16px;
}
.rear-view-image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
padding: 10px 0;
max-height: 435px; /* 两行图片高度: 200px * 2 + 15px (gap) + 20px (padding) */
overflow-y: auto;
overflow-x: hidden;
}
.rear-view-image-item {
position: relative;
width: 100%;
height: 200px;
border: 2px solid transparent;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.rear-view-image-item:hover {
border-color: #07c160;
}
.rear-view-image-item.active {
border-color: #67c23a;
box-shadow: 0 0 10px rgba(103, 194, 58, 0.3);
}
.rear-view-image-item.loading {
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
border: 2px dashed #dcdfe6;
}
.rear-view-image-item .loading-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
height: 100%;
background-color: #f5f7fa;
color: #909399;
font-size: 14px;
}
.rear-view-image-item .loading-placeholder .el-icon {
font-size: 24px;
}
.rear-view-image {
width: 100%;
height: 100%;
}
.rear-view-image-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 5px;
padding: 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
opacity: 0;
transition: opacity 0.3s;
}
.rear-view-image-item:hover .rear-view-image-actions,
.rear-view-image-item.active .rear-view-image-actions {
opacity: 1;
}
.rear-view-requirements-content {
padding: 20px 0;
}
.rear-view-requirements-content .requirements-text {
line-height: 1.6;
}
.rear-view-requirements-content .requirements-text p {
margin: 10px 0;
}
.preset-prompt-list {
max-height: 400px;
overflow-y: auto;
}
.preset-prompt-item {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #dcdfe6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.preset-prompt-item:hover {
border-color: #07c160;
background-color: #f5f7fa;
}
.preset-prompt-item .prompt-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.preset-prompt-item .prompt-content {
font-size: 13px;
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
}
.empty-tips {
text-align: center;
padding: 40px 0;
color: #909399;
font-size: 14px;
}
</style>