feat(aigc/useTemplate): add multi-step template generation confirmation workflow

- add PhotoConfirmDrawer component for post-photo-pick quality verification
- add GenerateConfirmPanel to display generation point cost and available balance
- extend TemplateVersionHero to support custom variant styles
- refactor useTemplate page to use step-based workflow flow
This commit is contained in:
duanshuwen
2026-07-05 21:16:30 +08:00
parent d95e90127e
commit 4b77df11a8
8 changed files with 430 additions and 11 deletions

View File

@@ -0,0 +1,57 @@
<template>
<view class="generate-confirm-panel">
<view class="generate-points-summary">
<view class="generate-point-card">
<text class="generate-point-label">本次消耗</text>
<text class="generate-point-value">{{ cost }}积分</text>
</view>
<view class="generate-point-card">
<text class="generate-point-label">当前可用</text>
<text class="generate-point-value">{{ balance }}积分</text>
</view>
</view>
<view class="generate-freeze-note">
<text class="generate-freeze-title">积分冻结规则</text>
<text class="generate-freeze-desc">
开始生成后将先冻结 {{ cost }} 积分生成成功后扣除失败会自动退回
</text>
</view>
<view class="generate-confirm-actions">
<view
class="generate-action is-primary"
hover-class="is-pressed"
@tap="emit('generate')"
>
开始生成
</view>
<view
class="generate-action is-secondary"
hover-class="is-pressed"
@tap="emit('recharge')"
>
充值积分
</view>
</view>
</view>
</template>
<script setup>
defineProps({
cost: {
type: [Number, String],
default: 100,
},
balance: {
type: [Number, String],
default: 300,
},
});
const emit = defineEmits(["generate", "recharge"]);
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,124 @@
.generate-confirm-panel {
margin-top: 16px;
}
.generate-points-summary {
height: 80px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.generate-point-card {
min-width: 0;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 18px;
background: rgba(255, 255, 255, 0.68);
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.04);
box-sizing: border-box;
}
.generate-point-label,
.generate-point-value {
display: block;
max-width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.generate-point-label {
color: #7a8596;
font-size: 11px;
line-height: 16px;
font-weight: 900;
}
.generate-point-value {
margin-top: 7px;
color: #102942;
font-size: 21px;
line-height: 27px;
font-weight: 900;
}
.generate-freeze-note {
min-height: 86px;
display: flex;
flex-direction: column;
justify-content: center;
margin-top: 14px;
padding: 17px 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.04);
box-sizing: border-box;
}
.generate-freeze-title,
.generate-freeze-desc {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.generate-freeze-title {
color: #172033;
font-size: 14px;
line-height: 20px;
font-weight: 900;
white-space: nowrap;
}
.generate-freeze-desc {
margin-top: 6px;
color: #52637a;
font-size: 12px;
line-height: 18px;
font-weight: 800;
}
.generate-confirm-actions {
height: 48px;
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
gap: 10px;
margin-top: 18px;
}
.generate-action {
min-width: 0;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 999px;
font-size: 15px;
line-height: 22px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.generate-action.is-primary {
background: #20cf75;
color: #ffffff;
box-shadow: 0 12px 24px rgba(32, 207, 117, 0.25);
}
.generate-action.is-secondary {
background: rgba(255, 255, 255, 0.82);
color: #3d5066;
}
.generate-action.is-pressed {
opacity: 0.86;
}

View File

@@ -0,0 +1,56 @@
<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"
>
<view class="photo-confirm-drawer">
<view class="photo-confirm-close" @tap="emit('close')">
<uni-icons type="closeempty" size="30" color="#8fa2ba" />
</view>
<text class="photo-confirm-title">照片确认</text>
<view class="photo-confirm-card">
<view class="photo-confirm-avatar">
<image class="photo-confirm-avatar-image" :src="avatarSrc" mode="aspectFill" />
</view>
<view class="photo-confirm-copy">
<text class="photo-confirm-card-title">照片已通过基础质检</text>
<text class="photo-confirm-card-desc">正脸清晰 · 单人入镜 · 光线可用</text>
</view>
</view>
<view class="photo-confirm-primary" hover-class="is-pressed" @tap="emit('confirm')">
下一步确认
</view>
</view>
</uni-popup>
</template>
<script setup>
import { nextTick, onMounted, ref } from "vue";
import defaultAvatar from "../../assets/xiaoqi-avatar.png";
defineProps({
avatarSrc: {
type: String,
default: defaultAvatar,
},
});
const emit = defineEmits(["close", "confirm"]);
const popupRef = ref(null);
onMounted(async () => {
await nextTick();
popupRef.value?.open();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,111 @@
.photo-confirm-drawer {
position: relative;
width: 100vw;
max-width: 100%;
min-height: 446px;
margin-top: -34px;
padding: 30px 38px 28px;
border-radius: 28px 28px 0 0;
background: #ffffff;
box-shadow: 0 -8px 28px rgba(18, 35, 48, 0.08);
box-sizing: border-box;
}
.photo-confirm-close {
position: absolute;
left: 24px;
top: 28px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #9aa4b2;
}
.photo-confirm-title {
display: block;
margin: 6px 0 23px;
color: #111827;
font-size: 18px;
line-height: 24px;
font-weight: 900;
text-align: center;
}
.photo-confirm-card {
width: 100%;
min-height: 74px;
display: flex;
align-items: center;
gap: 14px;
padding: 13px 16px;
border-radius: 16px;
background: #f0fbfd;
box-sizing: border-box;
}
.photo-confirm-avatar {
flex: 0 0 48px;
width: 48px;
height: 48px;
overflow: hidden;
border-radius: 50%;
background: #d8f1f7;
}
.photo-confirm-avatar-image {
width: 64px;
height: 92px;
display: block;
margin: -14px 0 0 -8px;
}
.photo-confirm-copy {
min-width: 0;
flex: 1 1 auto;
}
.photo-confirm-card-title,
.photo-confirm-card-desc {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.photo-confirm-card-title {
color: #172033;
font-size: 13px;
line-height: 19px;
font-weight: 900;
}
.photo-confirm-card-desc {
margin-top: 2px;
color: #56708c;
font-size: 11px;
line-height: 16px;
font-weight: 800;
}
.photo-confirm-primary {
width: 100%;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 26px;
border-radius: 999px;
background: #061e34;
color: #ffffff;
font-size: 16px;
line-height: 22px;
font-weight: 900;
box-sizing: border-box;
}
.photo-confirm-primary.is-pressed {
opacity: 0.86;
}

View File

@@ -1,6 +1,7 @@
<template>
<view
class="template-version-hero"
:class="`is-${variant}`"
:style="{ '--template-accent': template.accent || '#0a4d46' }"
>
<image class="template-version-image" :src="template.cover" mode="aspectFill" />
@@ -20,6 +21,10 @@ defineProps({
type: Object,
default: () => ({}),
},
variant: {
type: String,
default: "default",
},
});
</script>

View File

@@ -8,6 +8,12 @@
box-shadow: 0 16px 30px rgba(15, 29, 37, 0.1);
}
.template-version-hero.is-confirm {
height: 248px;
border-radius: 18px;
box-shadow: 0 12px 24px rgba(15, 29, 37, 0.08);
}
.template-version-image {
position: absolute;
top: 0;
@@ -82,3 +88,28 @@
line-height: 18px;
font-weight: 800;
}
.template-version-hero.is-confirm .template-version-copy {
right: 16px;
bottom: 18px;
left: 16px;
}
.template-version-hero.is-confirm .template-version-badge {
height: 22px;
max-width: 88px;
line-height: 22px;
}
.template-version-hero.is-confirm .template-version-title {
margin-top: 9px;
font-size: 24px;
line-height: 30px;
}
.template-version-hero.is-confirm .template-version-desc {
max-width: 294px;
margin-top: 4px;
font-size: 12px;
line-height: 18px;
}

View File

@@ -28,7 +28,7 @@
height: 100%;
min-height: 0;
overflow: hidden;
padding: 24px 18px;
padding: 18px;
box-sizing: border-box;
}

View File

@@ -12,15 +12,25 @@
<view class="aigc-use-template-content">
<TemplateVersionHero :template="currentTemplate" />
<VersionOptionList
:options="versionOptions"
:selected-value="selectedVersion"
@select="handleSelectVersion"
<GenerateConfirmPanel
v-if="currentStep === 'generateConfirm'"
:cost="currentVersion.cost || 100"
:balance="pointBalance"
@generate="handleGenerate"
@recharge="handleRecharge"
/>
<text class="aigc-use-template-note">
动效视频版会先生成图片底图再升级为 5秒内动效视频
</text>
<template v-else>
<VersionOptionList
:options="versionOptions"
:selected-value="selectedVersion"
@select="handleSelectVersion"
/>
<text class="aigc-use-template-note">
动效视频版会先生成图片底图再升级为 5秒内动效视频
</text>
</template>
</view>
</view>
@@ -40,6 +50,13 @@
@album="handlePickAlbum"
/>
</view>
<view v-if="currentStep === 'photoConfirm'" class="aigc-use-template-popup-host">
<PhotoConfirmDrawer
:avatar-src="guideAvatar"
@close="handleClosePhotoConfirm"
@confirm="handleConfirmPhoto"
/>
</view>
</view>
</template>
@@ -49,6 +66,8 @@ import { onLoad } from "@dcloudio/uni-app";
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 PhotoConfirmDrawer from "./components/PhotoConfirmDrawer/index.vue";
import PhotoGuidePanel from "./components/PhotoGuidePanel/index.vue";
import PhotoPickDrawer from "./components/PhotoPickDrawer/index.vue";
import TemplateVersionHero from "./components/TemplateVersionHero/index.vue";
@@ -56,7 +75,7 @@ import VersionOptionList from "./components/VersionOptionList/index.vue";
import { negativeExamples, positiveExamples } from "./data/photoGuideExamples.js";
import { versionOptions } from "./data/versionOptions.js";
const pointBalance = ref(0);
const pointBalance = ref(300);
const templateId = ref("");
const selectedVersion = ref("image");
const currentStep = ref("version");
@@ -65,6 +84,10 @@ const currentTemplate = computed(() => {
return aigcTemplates.find((item) => item.id === templateId.value) || aigcTemplates[0] || {};
});
const currentVersion = computed(() => {
return versionOptions.find((item) => item.value === selectedVersion.value) || versionOptions[0] || {};
});
onLoad((query = {}) => {
templateId.value = query.templateId || "";
});
@@ -111,11 +134,23 @@ const handleConfirmGuide = () => {
};
const handlePickCamera = () => {
showPlaceholderToast("拍照");
currentStep.value = "photoConfirm";
};
const handlePickAlbum = () => {
showPlaceholderToast("相册");
currentStep.value = "photoConfirm";
};
const handleClosePhotoConfirm = () => {
currentStep.value = "photoPick";
};
const handleConfirmPhoto = () => {
currentStep.value = "generateConfirm";
};
const handleGenerate = () => {
showPlaceholderToast("开始生成");
};
</script>