feat(aigc): refine generation UI and add task status tracking
Add real-time task status polling for active generation jobs. Overhaul GeneratingProgressPanel with dynamic step indicators, action buttons and updated styling. Update PointInsufficientDialog to show detailed credit balance and usage summary. Fix RecordCard media display and status logic for generating and completed tasks. Restyle PhotoPickDrawer and update its instructional copy. Add overflow styling for generating template content container.
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
<video v-if="isVideoRecord" class="record-card-image" :src="mediaUrl" :controls="false" :show-play-btn="false"
|
||||
:show-center-play-btn="false" object-fit="cover" />
|
||||
<image v-else-if="mediaUrl" class="record-card-image" :src="mediaUrl" mode="aspectFill" />
|
||||
<view v-else class="record-card-image record-card-media-placeholder" />
|
||||
<view v-else class="record-card-image record-card-media-placeholder">
|
||||
<text v-if="isGenerating" class="record-card-media-text">生成中</text>
|
||||
</view>
|
||||
|
||||
<view class="record-card-copy">
|
||||
<text class="record-card-title ellipsis-1">{{ title }}</text>
|
||||
@@ -11,7 +13,7 @@
|
||||
<text class="record-card-status ellipsis-1">{{ statusText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="record.taskStatus === 2" class="record-card-action" @tap="emit('view', record)">
|
||||
<view v-if="isCompleted" class="record-card-action" @tap="emit('view', record)">
|
||||
查看
|
||||
</view>
|
||||
</view>
|
||||
@@ -44,12 +46,17 @@ const TASK_STATUS_TEXT = {
|
||||
|
||||
const title = computed(() => props.record.itemTitle || props.record.taskId || "");
|
||||
|
||||
const normalizedTaskStatus = computed(() => Number(props.record.taskStatus));
|
||||
const isCompleted = computed(() => normalizedTaskStatus.value === 2);
|
||||
const isGenerating = computed(() => [0, 1].includes(normalizedTaskStatus.value));
|
||||
|
||||
const mediaUrl = computed(() => {
|
||||
if (!isCompleted.value) return "";
|
||||
return props.record.imageResultUrl || props.record.videoResultUrl || "";
|
||||
});
|
||||
|
||||
const isVideoRecord = computed(() => {
|
||||
return !props.record.imageResultUrl && Boolean(props.record.videoResultUrl);
|
||||
return isCompleted.value && !props.record.imageResultUrl && Boolean(props.record.videoResultUrl);
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
|
||||
@@ -26,9 +26,19 @@
|
||||
}
|
||||
|
||||
.record-card-media-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #edf5f1 0%, #f6faf8 100%);
|
||||
}
|
||||
|
||||
.record-card-media-text {
|
||||
color: #263f52;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.record-card-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<view class="generating-progress-panel">
|
||||
<view class="generating-task-panel">
|
||||
<view class="generating-task-status">
|
||||
<text class="generating-task-badge">{{ generatorTypeText }}</text>
|
||||
<text class="generating-task-title">接口处理中,可稍后查看</text>
|
||||
<text class="generating-task-desc">{{ displayItemTitle }} · 任务已进入后台队列</text>
|
||||
<text class="generating-task-hint">生成完成后可在最近任务查看结果</text>
|
||||
</view>
|
||||
|
||||
<view class="generating-progress-row">
|
||||
<view class="generating-progress-track">
|
||||
<view class="generating-progress-fill" :style="{ width: progressWidth }" />
|
||||
@@ -8,25 +15,37 @@
|
||||
</view>
|
||||
|
||||
<view class="generating-step-list">
|
||||
<view
|
||||
v-for="(step, index) in steps"
|
||||
:key="`${step}-${index}`"
|
||||
class="generating-step-item"
|
||||
:class="getStepClass(index)"
|
||||
>
|
||||
<view v-for="step in displaySteps" :key="step.label" class="generating-step-item"
|
||||
:class="{ 'is-complete': step.complete }">
|
||||
<view class="generating-step-dot" />
|
||||
<text class="generating-step-label">{{ step }}</text>
|
||||
<text class="generating-step-label">{{ step.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="generating-freeze-text">
|
||||
已冻结 {{ cost }} 积分,生成成功后扣除,失败自动退回。
|
||||
</text>
|
||||
<view class="generating-background-note">
|
||||
<text class="generating-note-title">不用停留等待</text>
|
||||
<text class="generating-note-desc">
|
||||
系统会继续处理当前任务,你可以先去制作其他模板,完成后会进入最近任务。
|
||||
</text>
|
||||
|
||||
<view class="generating-actions">
|
||||
<view class="generating-action is-primary" hover-class="is-pressed" @tap="emit('continue')">
|
||||
继续生成其他模板
|
||||
</view>
|
||||
<view class="generating-action is-secondary" hover-class="is-pressed" @tap="emit('view')">
|
||||
查看任务
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="generating-freeze-text">
|
||||
已冻结 {{ displayCost }} 积分,生成成功后扣除,失败自动退回。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
progress: {
|
||||
@@ -37,37 +56,42 @@ const props = defineProps({
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => ["素材质检", "图片优化", "套入模板", "导出图片"],
|
||||
generatorType: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
itemTitle: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
taskId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
taskStatus: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["complete"]);
|
||||
|
||||
const displayProgress = ref(0);
|
||||
let progressTimer = null;
|
||||
let hasCompleted = false;
|
||||
const emit = defineEmits(["continue", "view"]);
|
||||
|
||||
const normalizeProgress = (progress) => {
|
||||
const value = Number(progress);
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
return Math.max(0, Math.min(99, Math.round(value)));
|
||||
};
|
||||
|
||||
const normalizedProgress = computed(() => {
|
||||
return normalizeProgress(displayProgress.value);
|
||||
});
|
||||
const displayProgress = ref(normalizeProgress(props.progress));
|
||||
let progressTimer = null;
|
||||
|
||||
const normalizedProgress = computed(() => normalizeProgress(displayProgress.value));
|
||||
const progressWidth = computed(() => `${normalizedProgress.value}%`);
|
||||
|
||||
const currentStepIndex = computed(() => {
|
||||
const progress = normalizedProgress.value;
|
||||
if (progress <= 24) return 0;
|
||||
if (progress <= 49) return 1;
|
||||
if (progress <= 79) return 2;
|
||||
return 3;
|
||||
});
|
||||
const isAnimatingStatus = (status) => {
|
||||
if (status === null || status === undefined || status === "") return false;
|
||||
return [0, 1, 4].includes(Number(status));
|
||||
};
|
||||
|
||||
const clearProgressTimer = () => {
|
||||
if (progressTimer) {
|
||||
@@ -80,56 +104,66 @@ const getNextProgress = (progress) => {
|
||||
if (progress < 35) return Math.min(progress + 7, 35);
|
||||
if (progress < 70) return Math.min(progress + 5, 70);
|
||||
if (progress < 92) return Math.min(progress + 3, 92);
|
||||
if (progress < 99) return Math.min(progress + 1, 99);
|
||||
return 100;
|
||||
return Math.min(progress + 1, 99);
|
||||
};
|
||||
|
||||
const completeProgress = () => {
|
||||
const startProgressAnimation = () => {
|
||||
clearProgressTimer();
|
||||
if (hasCompleted) return;
|
||||
|
||||
hasCompleted = true;
|
||||
emit("complete");
|
||||
};
|
||||
|
||||
const startProgressTimer = () => {
|
||||
clearProgressTimer();
|
||||
|
||||
if (normalizedProgress.value >= 100) {
|
||||
completeProgress();
|
||||
return;
|
||||
}
|
||||
if (!isAnimatingStatus(props.taskStatus) || normalizedProgress.value >= 99) return;
|
||||
|
||||
progressTimer = setInterval(() => {
|
||||
displayProgress.value = getNextProgress(normalizedProgress.value);
|
||||
|
||||
if (normalizedProgress.value >= 100) {
|
||||
completeProgress();
|
||||
if (normalizedProgress.value >= 99) {
|
||||
clearProgressTimer();
|
||||
}
|
||||
}, 320);
|
||||
};
|
||||
|
||||
const resetProgress = () => {
|
||||
hasCompleted = false;
|
||||
displayProgress.value = normalizeProgress(props.progress);
|
||||
startProgressTimer();
|
||||
};
|
||||
const generatorTypeText = computed(() => {
|
||||
if (props.generatorType === "" || props.generatorType === null || props.generatorType === undefined) {
|
||||
return "生成任务";
|
||||
}
|
||||
if (Number(props.generatorType) === 0) return "图片版";
|
||||
if (Number(props.generatorType) === 1) return "视频版";
|
||||
return `类型${props.generatorType}`;
|
||||
});
|
||||
|
||||
const getStepClass = (index) => ({
|
||||
"is-complete": normalizedProgress.value >= 100 || index < currentStepIndex.value,
|
||||
"is-active": normalizedProgress.value < 100 && index === currentStepIndex.value,
|
||||
const displayItemTitle = computed(() => props.itemTitle || props.taskId || "生成任务");
|
||||
const displayCost = computed(() => Number(props.cost) || 0);
|
||||
|
||||
const displaySteps = computed(() => {
|
||||
const steps =
|
||||
Number(props.generatorType) === 1
|
||||
? ["素材质检", "生成图片底图", "升级动效视频", "导出视频"]
|
||||
: ["素材质检", "图片优化", "套入模板", "导出图片"];
|
||||
const thresholds = [20, 46, 72, 96];
|
||||
|
||||
return steps.map((label, index) => ({
|
||||
label,
|
||||
complete: normalizedProgress.value >= thresholds[index],
|
||||
}));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.progress,
|
||||
() => {
|
||||
resetProgress();
|
||||
(progress) => {
|
||||
const initialProgress = normalizeProgress(progress);
|
||||
if (!isAnimatingStatus(props.taskStatus) || displayProgress.value < initialProgress) {
|
||||
displayProgress.value = initialProgress;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
resetProgress();
|
||||
});
|
||||
watch(
|
||||
() => props.taskStatus,
|
||||
(status) => {
|
||||
clearProgressTimer();
|
||||
if (isAnimatingStatus(status)) {
|
||||
startProgressAnimation();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearProgressTimer();
|
||||
|
||||
@@ -1,13 +1,76 @@
|
||||
.generating-progress-panel {
|
||||
margin-top: 20px;
|
||||
.generating-task-panel {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generating-task-status {
|
||||
position: relative;
|
||||
min-height: 134px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
padding: 18px 18px 16px;
|
||||
border-radius: 22px;
|
||||
background:
|
||||
radial-gradient(96% 120% at 96% 0%, rgba(32, 207, 117, 0.1), rgba(32, 207, 117, 0) 52%),
|
||||
rgba(255, 255, 255, 0.84);
|
||||
box-shadow: 0 12px 28px rgba(15, 29, 37, 0.08);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generating-task-badge {
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(32, 207, 117, 0.13);
|
||||
color: #0e9f61;
|
||||
font-size: 11px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.generating-task-title,
|
||||
.generating-task-desc,
|
||||
.generating-task-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.generating-task-title {
|
||||
margin-top: 16px;
|
||||
color: #172033;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.generating-task-desc {
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
color: #526073;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generating-task-hint {
|
||||
margin-top: 10px;
|
||||
color: #91a0b1;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.generating-progress-row {
|
||||
height: 18px;
|
||||
height: 52px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 34px;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.generating-progress-track {
|
||||
@@ -15,35 +78,34 @@
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.generating-progress-fill {
|
||||
height: 100%;
|
||||
min-width: 24px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #20cf75 0%, #f2d646 100%);
|
||||
transition: width 0.28s ease;
|
||||
background: linear-gradient(90deg, #20cf75 0%, #f5bd32 100%);
|
||||
transition: width 280ms ease;
|
||||
}
|
||||
|
||||
.generating-progress-text {
|
||||
color: #061e34;
|
||||
color: #172033;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generating-step-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.generating-step-item {
|
||||
height: 40px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -53,39 +115,25 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generating-step-item.is-active {
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.generating-step-item.is-complete {
|
||||
background: rgba(236, 255, 244, 0.72);
|
||||
}
|
||||
|
||||
.generating-step-dot {
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #d6e8f4;
|
||||
border: 2px solid #c7d2df;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generating-step-item.is-active .generating-step-dot {
|
||||
.generating-step-item.is-complete .generating-step-dot {
|
||||
border-color: #20cf75;
|
||||
background: #20cf75;
|
||||
box-shadow: 0 0 0 4px rgba(32, 207, 117, 0.12);
|
||||
}
|
||||
|
||||
.generating-step-item.is-complete .generating-step-dot {
|
||||
border-color: rgba(32, 207, 117, 0.42);
|
||||
background: rgba(32, 207, 117, 0.22);
|
||||
box-shadow: inset 0 0 0 4px #ffffff;
|
||||
}
|
||||
|
||||
.generating-step-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #6c7e96;
|
||||
color: #7d899a;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
@@ -93,18 +141,85 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generating-step-item.is-active .generating-step-label,
|
||||
.generating-step-item.is-complete .generating-step-label {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.generating-background-note {
|
||||
margin-top: 16px;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(32, 207, 117, 0.11), rgba(255, 255, 255, 0.9) 54%),
|
||||
rgba(255, 255, 255, 0.84);
|
||||
color: #172033;
|
||||
box-shadow: 0 10px 22px rgba(18, 48, 56, 0.07);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generating-note-title,
|
||||
.generating-note-desc,
|
||||
.generating-freeze-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.generating-note-title {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.generating-note-desc {
|
||||
margin-top: 4px;
|
||||
color: #526073;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.generating-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.generating-action {
|
||||
min-width: 0;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
margin: 18px 14px 0;
|
||||
color: #7b8da4;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.generating-action.is-primary {
|
||||
background: #20cf75;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8px 16px rgba(32, 207, 117, 0.18);
|
||||
}
|
||||
|
||||
.generating-action.is-secondary {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: #526073;
|
||||
}
|
||||
|
||||
.generating-action.is-pressed {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.generating-freeze-text {
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
color: #8c98a8;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popupRef"
|
||||
type="bottom"
|
||||
background-color="transparent"
|
||||
mask-background-color="rgba(0, 0, 0, 0)"
|
||||
:safe-area="false"
|
||||
:is-mask-click="false"
|
||||
>
|
||||
<uni-popup ref="popupRef" type="bottom" background-color="transparent" mask-background-color="rgba(0, 0, 0, 0)"
|
||||
:safe-area="false" :is-mask-click="false">
|
||||
<view class="photo-pick-drawer">
|
||||
<view
|
||||
class="photo-pick-close"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
@tap="handleClose"
|
||||
>
|
||||
<view class="photo-pick-close" :class="{ 'is-disabled': loading }" @tap="handleClose">
|
||||
<uni-icons type="closeempty" size="30" color="#8fa2ba" />
|
||||
</view>
|
||||
|
||||
<text class="photo-pick-title">选择照片</text>
|
||||
|
||||
<view class="photo-pick-card">
|
||||
<view class="photo-pick-plus">+</view>
|
||||
<text class="photo-pick-card-title">上传照片素材</text>
|
||||
<text class="photo-pick-card-desc">
|
||||
选择后会自动进行清晰度、人脸数量和遮挡检测
|
||||
</text>
|
||||
<view class="photo-pick-card-title">上传素材要求</view>
|
||||
<view class="photo-pick-card-desc">
|
||||
请使用清晰半身照,避免墨镜、口罩、强逆光。选择后系统自动进行清晰度、人脸数量和遮挡检测。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="photo-pick-actions">
|
||||
<view
|
||||
class="photo-pick-action"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'"
|
||||
@tap="handleCamera"
|
||||
>
|
||||
拍照
|
||||
<view class="photo-pick-action is-primary" :class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'" @tap="handleCamera">
|
||||
<text class="photo-pick-action-title">拍照</text>
|
||||
<text class="photo-pick-action-desc">现场拍摄素材</text>
|
||||
</view>
|
||||
<view
|
||||
class="photo-pick-action"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'"
|
||||
@tap="handleAlbum"
|
||||
>
|
||||
相册
|
||||
<view class="photo-pick-action" :class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'" @tap="handleAlbum">
|
||||
<text class="photo-pick-action-title">相册</text>
|
||||
<text class="photo-pick-action-desc">从相册选择</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="photo-pick-hint">建议使用半身照,避免墨镜、口罩、强逆光。</text>
|
||||
<text class="photo-pick-hint">入口在这里,选择后进入照片质检与生成确认。</text>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
@@ -39,34 +39,14 @@
|
||||
|
||||
.photo-pick-card {
|
||||
width: 100%;
|
||||
min-height: 126px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 14px 18px;
|
||||
border: 1.5px dashed rgba(32, 207, 117, 0.46);
|
||||
border-radius: 20px;
|
||||
background: #f2fbf6;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.photo-pick-plus {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #20cf75;
|
||||
color: #ffffff;
|
||||
font-size: 25px;
|
||||
line-height: 42px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.photo-pick-card-title,
|
||||
.photo-pick-card-desc {
|
||||
display: block;
|
||||
@@ -75,16 +55,15 @@
|
||||
}
|
||||
|
||||
.photo-pick-card-title {
|
||||
max-width: 230px;
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.photo-pick-card-desc {
|
||||
max-width: 230px;
|
||||
color: #667386;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
@@ -95,22 +74,58 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.photo-pick-action {
|
||||
height: 42px;
|
||||
min-width: 0;
|
||||
min-height: 66px;
|
||||
padding: 8px 10px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
background: #f5f8f7;
|
||||
gap: 3px;
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
color: #172033;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.08);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.photo-pick-action.is-primary {
|
||||
background: #20cf75;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 12px 22px rgba(32, 207, 117, 0.22);
|
||||
}
|
||||
|
||||
.photo-pick-action-title,
|
||||
.photo-pick-action-desc {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.photo-pick-action-title {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.photo-pick-action-desc {
|
||||
color: #7a8596;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.photo-pick-action.is-primary .photo-pick-action-desc {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.photo-pick-action.is-pressed {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,28 @@
|
||||
:safe-area="false"
|
||||
:is-mask-click="false"
|
||||
>
|
||||
<view class="point-dialog">
|
||||
<text class="point-dialog-title">积分不足</text>
|
||||
<view class="point-dialog point-recharge-dialog">
|
||||
<text class="point-dialog-badge">充值提醒</text>
|
||||
<text class="point-dialog-title">当前积分不足</text>
|
||||
<text class="point-dialog-desc">
|
||||
本次需 {{ normalizedCost }} 积分,当前仅 {{ normalizedBalance }} 积分,还差 {{ shortage }} 积分。
|
||||
本次生成需要先冻结对应积分,充值后可继续当前素材生成。
|
||||
</text>
|
||||
|
||||
<view class="point-dialog-summary">
|
||||
<view class="point-summary-item">
|
||||
<text class="point-summary-label">当前积分</text>
|
||||
<text class="point-summary-value">{{ normalizedBalance }}</text>
|
||||
</view>
|
||||
<view class="point-summary-item">
|
||||
<text class="point-summary-label">本次需要</text>
|
||||
<text class="point-summary-value">{{ normalizedCost }}</text>
|
||||
</view>
|
||||
<view class="point-summary-item is-shortage">
|
||||
<text class="point-summary-label">需补足</text>
|
||||
<text class="point-summary-value">{{ shortage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="point-dialog-actions">
|
||||
<view
|
||||
class="point-dialog-button is-cancel"
|
||||
|
||||
@@ -1,36 +1,96 @@
|
||||
.point-dialog {
|
||||
width: 318px;
|
||||
max-width: calc(100vw - 56px);
|
||||
min-height: 220px;
|
||||
padding: 29px 24px 22px;
|
||||
border: 1px solid rgba(20, 143, 255, 0.8);
|
||||
border-radius: 20px;
|
||||
background: #dceeff;
|
||||
box-shadow: 0 18px 38px rgba(8, 28, 44, 0.22);
|
||||
padding: 30px 26px 24px;
|
||||
border-radius: 24px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.26);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.point-recharge-dialog {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.point-dialog-badge {
|
||||
width: fit-content;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto 12px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: #e9f9f1;
|
||||
color: #0fa567;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.point-dialog-title {
|
||||
display: block;
|
||||
color: #102942;
|
||||
font-size: 22px;
|
||||
line-height: 29px;
|
||||
margin-bottom: 14px;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.point-dialog-desc {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 15px auto 0;
|
||||
max-width: 228px;
|
||||
color: #506985;
|
||||
margin: 0;
|
||||
color: #667386;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.point-dialog-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.point-summary-item {
|
||||
min-width: 0;
|
||||
padding: 11px 6px 10px;
|
||||
border-radius: 15px;
|
||||
background: #f3fbf6;
|
||||
color: #172033;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.point-summary-label,
|
||||
.point-summary-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.point-summary-label {
|
||||
color: #7b8798;
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.point-summary-value {
|
||||
margin-top: 3px;
|
||||
color: #172033;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.point-summary-item.is-shortage .point-summary-value {
|
||||
color: #0fa567;
|
||||
}
|
||||
|
||||
.point-dialog-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -55,9 +115,9 @@
|
||||
}
|
||||
|
||||
.point-dialog-button.is-cancel {
|
||||
border: 1px solid rgba(124, 168, 207, 0.28);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #102942;
|
||||
border: 1px solid #e2e6ea;
|
||||
background: #ffffff;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.point-dialog-button.is-primary {
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-use-template-content.is-generating {
|
||||
overflow-y: auto;
|
||||
padding: 12px 12px 16px;
|
||||
}
|
||||
|
||||
.aigc-use-template-popup-host {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
|
||||
@@ -4,21 +4,26 @@
|
||||
@recharge="handleRecharge" />
|
||||
|
||||
<view class="aigc-use-template-body">
|
||||
<view class="aigc-use-template-content">
|
||||
<TemplateVersionHero :template="currentTemplate" />
|
||||
|
||||
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost" :balance="pointBalance"
|
||||
@generate="handleGenerate" @recharge="handleRecharge" />
|
||||
<GeneratingProgressPanel v-else-if="currentStep === 'generating'" :cost="currentCost" :progress="8"
|
||||
@complete="handleGenerateComplete" />
|
||||
<view class="aigc-use-template-content" :class="{ 'is-generating': currentStep === 'generating' }">
|
||||
<GeneratingProgressPanel v-if="currentStep === 'generating'" :cost="currentCost" :progress="8"
|
||||
:generator-type="currentTemplateItem.generatorType"
|
||||
:item-title="currentTemplateItem.itemTitle || currentTemplate.templateTitle" :task-id="createdTaskId"
|
||||
:task-status="taskStatus" @continue="handleContinueTemplates" @view="handleHistory" />
|
||||
|
||||
<template v-else>
|
||||
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
|
||||
@select="handleSelectTemplateItem" />
|
||||
<TemplateVersionHero :template="currentTemplate" />
|
||||
|
||||
<text class="aigc-use-template-note">
|
||||
动效视频版会先生图,再升级为 5 秒内动效视频。
|
||||
</text>
|
||||
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost"
|
||||
:balance="pointBalance" @generate="handleGenerate" @recharge="handleRecharge" />
|
||||
|
||||
<template v-else>
|
||||
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
|
||||
@select="handleSelectTemplateItem" />
|
||||
|
||||
<text class="aigc-use-template-note">
|
||||
动效视频版会先生图,再升级为 5 秒内动效视频。
|
||||
</text>
|
||||
</template>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
@@ -50,6 +55,7 @@ import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import {
|
||||
createAigcGeneratorTask,
|
||||
getAigcGeneratorTaskDetail,
|
||||
getAigcTemplateItemList,
|
||||
getAigcTemplateList,
|
||||
} from "@/request/api/AigcApi.js";
|
||||
@@ -73,6 +79,7 @@ const selectedTemplateItemId = ref("");
|
||||
const selectedImageLocalPath = ref("");
|
||||
const uploadedImageUrl = ref("");
|
||||
const createdTaskId = ref("");
|
||||
const taskStatus = ref(null);
|
||||
const currentStep = ref("version");
|
||||
const pointDialogVisible = ref(false);
|
||||
const isUploading = ref(false);
|
||||
@@ -331,6 +338,19 @@ const handleConfirmPhoto = () => {
|
||||
currentStep.value = "generateConfirm";
|
||||
};
|
||||
|
||||
const fetchCreatedTaskStatus = async () => {
|
||||
if (!createdTaskId.value) return;
|
||||
|
||||
try {
|
||||
const res = await getAigcGeneratorTaskDetail({ taskId: createdTaskId.value });
|
||||
if (res?.code === 0 && res.data) {
|
||||
taskStatus.value = res.data.taskStatus;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("获取AIGC生成任务状态失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (isCreatingTask.value) return;
|
||||
|
||||
@@ -355,10 +375,9 @@ const handleGenerate = async () => {
|
||||
}
|
||||
|
||||
isCreatingTask.value = true;
|
||||
uni.showLoading({
|
||||
title: "创建中",
|
||||
mask: true,
|
||||
});
|
||||
createdTaskId.value = "";
|
||||
taskStatus.value = null;
|
||||
currentStep.value = "generating";
|
||||
|
||||
try {
|
||||
const res = await createAigcGeneratorTask({
|
||||
@@ -368,28 +387,32 @@ const handleGenerate = async () => {
|
||||
|
||||
if (res?.code === 0 && res.data) {
|
||||
createdTaskId.value = res.data;
|
||||
currentStep.value = "generating";
|
||||
fetchCurrentCredit();
|
||||
await fetchCreatedTaskStatus();
|
||||
} else {
|
||||
currentStep.value = "generateConfirm";
|
||||
showPlaceholderToast(res?.msg || "创建任务失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("创建AIGC生成任务失败", error);
|
||||
currentStep.value = "generateConfirm";
|
||||
showPlaceholderToast("创建任务失败");
|
||||
} finally {
|
||||
isCreatingTask.value = false;
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateComplete = () => {
|
||||
if (!createdTaskId.value) {
|
||||
showPlaceholderToast("任务创建失败");
|
||||
const handleContinueTemplates = () => {
|
||||
const pages = getCurrentPages();
|
||||
const previousPage = pages[pages.length - 2];
|
||||
if (previousPage?.route === "pages-aigc/home/home") {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(createdTaskId.value)}`,
|
||||
uni.redirectTo({
|
||||
url: "/pages-aigc/home/home",
|
||||
fail: () => showPlaceholderToast("继续生成其他模板"),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user