forked from natuka/web.puabadge.com
修改:后视图生成
This commit is contained in:
1 parent
43471c3a76
commit
9ede62f450
16 files changed
+921
-74
No files matched your search
+36
-67
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user