feat(useTemplate): add point insufficient dialog and progress panel

Create PointInsufficientDialog component to notify users of insufficient points before generation. Add GeneratingProgressPanel to display generation progress, steps and cost freeze info. Integrate point balance validation into the generate workflow and update the main page to handle generating state.
This commit is contained in:
duanshuwen
2026-07-05 21:50:49 +08:00
parent 4b77df11a8
commit 190793b156
5 changed files with 311 additions and 1 deletions

View File

@@ -0,0 +1,56 @@
<template>
<view class="generating-progress-panel">
<view class="generating-progress-row">
<view class="generating-progress-track">
<view class="generating-progress-fill" :style="{ width: progressWidth }" />
</view>
<text class="generating-progress-text">{{ normalizedProgress }}%</text>
</view>
<view class="generating-step-list">
<view
v-for="(step, index) in steps"
:key="`${step}-${index}`"
class="generating-step-item"
>
<view class="generating-step-dot" />
<text class="generating-step-label">{{ step }}</text>
</view>
</view>
<text class="generating-freeze-text">
已冻结 {{ cost }} 积分生成成功后扣除失败自动退回
</text>
</view>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
progress: {
type: [Number, String],
default: 8,
},
cost: {
type: [Number, String],
default: 100,
},
steps: {
type: Array,
default: () => ["素材质检", "图片优化", "套入模板", "导出图片"],
},
});
const normalizedProgress = computed(() => {
const value = Number(props.progress);
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, Math.round(value)));
});
const progressWidth = computed(() => `${normalizedProgress.value}%`);
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,87 @@
.generating-progress-panel {
margin-top: 20px;
}
.generating-progress-row {
height: 18px;
display: grid;
grid-template-columns: minmax(0, 1fr) 34px;
align-items: center;
gap: 10px;
}
.generating-progress-track {
width: 100%;
height: 12px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
}
.generating-progress-fill {
height: 100%;
min-width: 24px;
border-radius: 999px;
background: linear-gradient(90deg, #20cf75 0%, #f2d646 100%);
}
.generating-progress-text {
color: #061e34;
font-size: 15px;
line-height: 18px;
font-weight: 900;
text-align: right;
white-space: nowrap;
}
.generating-step-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 16px;
}
.generating-step-item {
height: 40px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
box-sizing: border-box;
}
.generating-step-dot {
flex: 0 0 18px;
width: 18px;
height: 18px;
border: 2px solid #d6e8f4;
border-radius: 50%;
background: rgba(255, 255, 255, 0.7);
box-sizing: border-box;
}
.generating-step-label {
min-width: 0;
overflow: hidden;
color: #6c7e96;
font-size: 13px;
line-height: 18px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.generating-freeze-text {
display: block;
overflow: hidden;
margin: 18px 14px 0;
color: #7b8da4;
font-size: 12px;
line-height: 18px;
font-weight: 800;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -0,0 +1,65 @@
<template>
<uni-popup
ref="popupRef"
type="center"
background-color="transparent"
mask-background-color="rgba(0, 0, 0, 0.58)"
:safe-area="false"
:is-mask-click="false"
>
<view class="point-dialog">
<text class="point-dialog-title">积分不足</text>
<text class="point-dialog-desc">
本次需 {{ normalizedCost }} 积分当前仅 {{ normalizedBalance }} 积分还差 {{ shortage }} 积分
</text>
<view class="point-dialog-actions">
<view
class="point-dialog-button is-cancel"
hover-class="is-pressed"
@tap="emit('cancel')"
>
稍后再说
</view>
<view
class="point-dialog-button is-primary"
hover-class="is-pressed"
@tap="emit('recharge')"
>
去充值
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed, nextTick, onMounted, ref } from "vue";
const props = defineProps({
cost: {
type: [Number, String],
default: 100,
},
balance: {
type: [Number, String],
default: 0,
},
});
const emit = defineEmits(["cancel", "recharge"]);
const popupRef = ref(null);
const normalizedCost = computed(() => Math.max(0, Number(props.cost) || 0));
const normalizedBalance = computed(() => Math.max(0, Number(props.balance) || 0));
const shortage = computed(() => Math.max(normalizedCost.value - normalizedBalance.value, 0));
onMounted(async () => {
await nextTick();
popupRef.value?.open();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,70 @@
.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);
box-sizing: border-box;
}
.point-dialog-title {
display: block;
color: #102942;
font-size: 22px;
line-height: 29px;
font-weight: 900;
text-align: center;
}
.point-dialog-desc {
display: block;
overflow: hidden;
margin: 15px auto 0;
max-width: 228px;
color: #506985;
font-size: 14px;
line-height: 22px;
font-weight: 800;
text-align: center;
}
.point-dialog-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 28px;
}
.point-dialog-button {
min-width: 0;
height: 54px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 999px;
font-size: 16px;
line-height: 22px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.point-dialog-button.is-cancel {
border: 1px solid rgba(124, 168, 207, 0.28);
background: rgba(255, 255, 255, 0.14);
color: #102942;
}
.point-dialog-button.is-primary {
background: #061e34;
color: #ffffff;
}
.point-dialog-button.is-pressed {
opacity: 0.86;
}

View File

@@ -19,6 +19,11 @@
@generate="handleGenerate"
@recharge="handleRecharge"
/>
<GeneratingProgressPanel
v-else-if="currentStep === 'generating'"
:cost="currentVersion.cost || 100"
:progress="8"
/>
<template v-else>
<VersionOptionList
@@ -57,6 +62,13 @@
@confirm="handleConfirmPhoto"
/>
</view>
<PointInsufficientDialog
v-if="pointDialogVisible"
:cost="currentVersion.cost || 100"
:balance="pointBalance"
@cancel="handleClosePointDialog"
@recharge="handlePointRecharge"
/>
</view>
</template>
@@ -67,9 +79,11 @@ import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { aigcTemplates } from "@/pages-aigc/home/data/templates.js";
import guideAvatar from "./assets/xiaoqi-avatar.png";
import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue";
import GeneratingProgressPanel from "./components/GeneratingProgressPanel/index.vue";
import PhotoConfirmDrawer from "./components/PhotoConfirmDrawer/index.vue";
import PhotoGuidePanel from "./components/PhotoGuidePanel/index.vue";
import PhotoPickDrawer from "./components/PhotoPickDrawer/index.vue";
import PointInsufficientDialog from "./components/PointInsufficientDialog/index.vue";
import TemplateVersionHero from "./components/TemplateVersionHero/index.vue";
import VersionOptionList from "./components/VersionOptionList/index.vue";
import { negativeExamples, positiveExamples } from "./data/photoGuideExamples.js";
@@ -79,6 +93,7 @@ const pointBalance = ref(300);
const templateId = ref("");
const selectedVersion = ref("image");
const currentStep = ref("version");
const pointDialogVisible = ref(false);
const currentTemplate = computed(() => {
return aigcTemplates.find((item) => item.id === templateId.value) || aigcTemplates[0] || {};
@@ -150,7 +165,24 @@ const handleConfirmPhoto = () => {
};
const handleGenerate = () => {
showPlaceholderToast("开始生成");
const cost = Number(currentVersion.value.cost) || 0;
const balance = Number(pointBalance.value) || 0;
if (balance < cost) {
pointDialogVisible.value = true;
return;
}
currentStep.value = "generating";
};
const handleClosePointDialog = () => {
pointDialogVisible.value = false;
};
const handlePointRecharge = () => {
pointDialogVisible.value = false;
handleRecharge();
};
</script>