|
|
|
@ -103,9 +103,149 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<!-- 后视图 --> |
|
|
|
<!-- 后视图 --> |
|
|
|
<div> |
|
|
|
<div v-if="isViews === 1" 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> |
|
|
|
</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;"> |
|
|
|
<div style="padding: 0 16px;"> |
|
|
|
<van-divider /> |
|
|
|
<van-divider /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -179,9 +319,9 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
<script setup lang="ts"> |
|
|
|
<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 { 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 { localStorage } from '@/utils/local-storage' |
|
|
|
import * as badgeApi from '@/api/badge' |
|
|
|
import * as badgeApi from '@/api/badge' |
|
|
|
import { areaList } from '@/utils/area' |
|
|
|
import { areaList } from '@/utils/area' |
|
|
|
@ -966,6 +1106,7 @@ const orderStat = ref({}) |
|
|
|
const typeId = ref(0) |
|
|
|
const typeId = ref(0) |
|
|
|
const subjectId = ref(0) |
|
|
|
const subjectId = ref(0) |
|
|
|
const typeName = ref('') |
|
|
|
const typeName = ref('') |
|
|
|
|
|
|
|
const isViews = ref(0) |
|
|
|
const getOrderStat = () => { |
|
|
|
const getOrderStat = () => { |
|
|
|
badgeApi.getOrderStat({}).then((res: any) => { |
|
|
|
badgeApi.getOrderStat({}).then((res: any) => { |
|
|
|
console.log('getOrderStat', res) |
|
|
|
console.log('getOrderStat', res) |
|
|
|
@ -974,15 +1115,373 @@ const getOrderStat = () => { |
|
|
|
subjectId.value = res.subject_id |
|
|
|
subjectId.value = res.subject_id |
|
|
|
typeName.value = res.type_name |
|
|
|
typeName.value = res.type_name |
|
|
|
badgeTypeId.value = res.type_id |
|
|
|
badgeTypeId.value = res.type_id |
|
|
|
|
|
|
|
isViews.value = res.is_views || 0 |
|
|
|
getSizeList() |
|
|
|
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) { |
|
|
|
function handleBeforeUnload(_event: BeforeUnloadEvent) { |
|
|
|
// code 的清除已在 main.ts 中统一处理,这里只处理页面特定的清理逻辑 |
|
|
|
// code 的清除已在 main.ts 中统一处理,这里只处理页面特定的清理逻辑 |
|
|
|
// 清除轮询和进度条计时器 |
|
|
|
// 清除轮询和进度条计时器 |
|
|
|
clearInterval(timer.value) |
|
|
|
clearInterval(timer.value) |
|
|
|
clearInterval(progressTimer.value) |
|
|
|
clearInterval(progressTimer.value) |
|
|
|
|
|
|
|
stopRearViewPolling() |
|
|
|
// userId 的清除由 main.ts 统一处理 |
|
|
|
// userId 的清除由 main.ts 统一处理 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -1018,11 +1517,31 @@ onMounted(() => { |
|
|
|
onUnmounted(() => { |
|
|
|
onUnmounted(() => { |
|
|
|
clearInterval(timer.value) |
|
|
|
clearInterval(timer.value) |
|
|
|
clearInterval(progressTimer.value) |
|
|
|
clearInterval(progressTimer.value) |
|
|
|
|
|
|
|
stopRearViewPolling() |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const searchResult = ref([]) |
|
|
|
const searchResult = ref([]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 isViews 变化,如果变为 1 则加载后视图列表 |
|
|
|
|
|
|
|
watch(() => isViews.value, (val) => { |
|
|
|
|
|
|
|
if (val === 1 && pid.value) { |
|
|
|
|
|
|
|
loadRearViewList() |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
stopRearViewPolling() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
<style scoped> |
|
|
|
<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 { |
|
|
|
.preview-container { |
|
|
|
min-height: 100vh; |
|
|
|
min-height: 100vh; |
|
|
|
font-family: 'PingFang SC', Arial, sans-serif; |
|
|
|
font-family: 'PingFang SC', Arial, sans-serif; |
|
|
|
@ -1481,4 +2000,143 @@ const searchResult = ref([]) |
|
|
|
.van-address-edit { |
|
|
|
.van-address-edit { |
|
|
|
padding: 0!important; |
|
|
|
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> |
|
|
|
</style> |