feat(aigc): 新增图片生成动效视频的升级功能

- 新增图片转视频相关的API接口
- 调整AIGC详情页英雄区块高度至380px
- 添加结果详情页升级动效视频入口
- 完善升级流程的逻辑与费用计算
- 适配多类型生成任务的判断规则
This commit is contained in:
duanshuwen
2026-08-05 19:41:42 +08:00
parent 57f6cdd85e
commit 9f0881a284
4 changed files with 217 additions and 72 deletions

View File

@@ -3,20 +3,22 @@
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" @recharge="handleRecharge" /> <AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" @recharge="handleRecharge" />
<view class="aigc-detail-content"> <view class="aigc-detail-content">
<template v-if="taskDetail"> <template v-if="taskDetail">
<ResultPreview v-if="result.cover" class="aigc-detail-preview" :result="result" /> <ResultPreview v-if="result.cover" class="aigc-detail-preview" :result="result" />
<view class="aigc-detail-controls"> <view class="aigc-detail-controls">
<ResultMeta :items="metaItems" /> <!-- <ResultMeta :items="metaItems" /> -->
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)" <ContinuationCard v-if="result.mediaType === 'image'" :info="continuationInfo"
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" /> @upgrade="handleUpgradeToVideo" />
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
<view class="aigc-detail-text-actions"> :share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view> <view class="aigc-detail-text-actions">
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view> <view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
</view> <view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
</view> <view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
</template> </view>
</view>
</template>
</view> </view>
<Privacy :visible="privacyVisible" :contract-name="privacyContractName" @agree="handlePrivacyAgree" <Privacy :visible="privacyVisible" :contract-name="privacyContractName" @agree="handlePrivacyAgree"
@@ -39,11 +41,11 @@ import {
createAigcGeneratorTask, createAigcGeneratorTask,
createAigcGeneratorTaskShare, createAigcGeneratorTaskShare,
getAigcGeneratorTaskDetail, getAigcGeneratorTaskDetail,
} from "@/pages-aigc/request/AigcApi.js"; } from "@/pages-aigc/request/AigcApi.js";
import PointInsufficientDialog from "@/pages-aigc/useTemplate/components/PointInsufficientDialog/index.vue"; import PointInsufficientDialog from "@/pages-aigc/useTemplate/components/PointInsufficientDialog/index.vue";
import ContinuationCard from "./components/ContinuationCard/index.vue";
import RegenerateConfirmPopup from "./components/RegenerateConfirmPopup/index.vue"; import RegenerateConfirmPopup from "./components/RegenerateConfirmPopup/index.vue";
import ResultActions from "./components/ResultActions/index.vue"; import ResultActions from "./components/ResultActions/index.vue";
import ResultMeta from "./components/ResultMeta/index.vue";
import ResultPreview from "./components/ResultPreview/index.vue"; import ResultPreview from "./components/ResultPreview/index.vue";
const { pointBalance, fetchCurrentCredit } = useCurrentCredit(); const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
@@ -57,6 +59,7 @@ const privacyContractName = ref("隐私保护指引");
const pendingSaveAfterPrivacy = ref(false); const pendingSaveAfterPrivacy = ref(false);
const regeneratePopupRef = ref(null); const regeneratePopupRef = ref(null);
const isRegenerating = ref(false); const isRegenerating = ref(false);
const isUpgradingToVideo = ref(false);
const pointDialogVisible = ref(false); const pointDialogVisible = ref(false);
const GENERATOR_TYPE_TEXT = { const GENERATOR_TYPE_TEXT = {
@@ -89,7 +92,7 @@ const result = computed(() => {
const imageUrl = record.imageResultUrl || ""; const imageUrl = record.imageResultUrl || "";
const videoUrl = record.videoResultUrl || ""; const videoUrl = record.videoResultUrl || "";
const isVideo = Number(record.generatorType) === 1; const isVideo = [1, 2].includes(Number(record.generatorType));
const typeText = getMappedText(GENERATOR_TYPE_TEXT, record.generatorType, "类型"); const typeText = getMappedText(GENERATOR_TYPE_TEXT, record.generatorType, "类型");
const statusText = getMappedText(TASK_STATUS_TEXT, record.taskStatus, "状态"); const statusText = getMappedText(TASK_STATUS_TEXT, record.taskStatus, "状态");
const consumedText = const consumedText =
@@ -138,6 +141,12 @@ const metaItems = computed(() => {
const sharePreviewImage = computed(() => taskDetail.value?.imageResultUrl || ""); const sharePreviewImage = computed(() => taskDetail.value?.imageResultUrl || "");
const regenerateCost = computed(() => Math.max(0, Number(taskDetail.value?.generatorCost) || 0)); const regenerateCost = computed(() => Math.max(0, Number(taskDetail.value?.generatorCost) || 0));
const continuationInfo = {
title: "创意续作",
desc: "让这张图动起来 · 5秒内动效视频",
cost: "追加消耗 650积分",
buttonText: "升级为动效视频",
};
const showPlaceholderToast = (title) => { const showPlaceholderToast = (title) => {
uni.showToast({ uni.showToast({
@@ -182,6 +191,7 @@ onLoad((query = {}) => {
}); });
onShow(() => { onShow(() => {
isUpgradingToVideo.value = false;
fetchCurrentCredit(); fetchCurrentCredit();
}); });
@@ -249,6 +259,35 @@ const handleRecharge = () => {
}); });
}; };
const handleUpgradeToVideo = () => {
if (isUpgradingToVideo.value || result.value.mediaType !== "image") return;
const sourceTaskId = String(taskDetail.value?.taskId || taskId.value || "").trim();
if (!sourceTaskId) {
showPlaceholderToast("缺少任务ID");
return;
}
const templateId = String(taskDetail.value?.templateId || "").trim();
const templateItemId = String(taskDetail.value?.templateItemId || "").trim();
const query = [
templateId && `templateId=${encodeURIComponent(templateId)}`,
`upgradeTaskId=${encodeURIComponent(sourceTaskId)}`,
templateItemId && `upgradeTemplateItemId=${encodeURIComponent(templateItemId)}`,
]
.filter(Boolean)
.join("&");
isUpgradingToVideo.value = true;
uni.navigateTo({
url: `/pages-aigc/useTemplate/useTemplate?${query}`,
fail: () => {
isUpgradingToVideo.value = false;
showPlaceholderToast("打开图片生成页面失败");
},
});
};
const getMediaFileExtension = (url, mediaType) => { const getMediaFileExtension = (url, mediaType) => {
const pathname = String(url || "").split(/[?#]/)[0]; const pathname = String(url || "").split(/[?#]/)[0];
const matchedExtension = pathname.match(/\.([a-z0-9]{2,5})$/i)?.[1]?.toLowerCase(); const matchedExtension = pathname.match(/\.([a-z0-9]{2,5})$/i)?.[1]?.toLowerCase();
@@ -372,7 +411,7 @@ const removeDownloadedMedia = (filePath, shouldCleanup) => {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
wx.getFileSystemManager().unlink({ wx.getFileSystemManager().unlink({
filePath, filePath,
fail: () => {}, fail: () => { },
}); });
// #endif // #endif
}; };

View File

@@ -30,6 +30,21 @@ function createAigcGeneratorTask(args) {
); );
} }
function createAigcImageToVideoTask(args) {
return request.post(
"/hotelBiz/aigcGeneratorTask/createAigcImageToVideoTask",
args,
{ sensitive: true },
);
}
function getAigcImageToVideoItemDetail(args) {
return request.post(
"/hotelBiz/aigcGeneratorTask/aigcImageToVideoItemDetail",
args,
);
}
function getCurrentCredit() { function getCurrentCredit() {
return request.get("/hotelBiz/credit/current", {}); return request.get("/hotelBiz/credit/current", {});
} }
@@ -62,6 +77,8 @@ export {
getAigcGeneratorTaskList, getAigcGeneratorTaskList,
getAigcGeneratorTaskDetail, getAigcGeneratorTaskDetail,
createAigcGeneratorTask, createAigcGeneratorTask,
createAigcImageToVideoTask,
getAigcImageToVideoItemDetail,
getCurrentCredit, getCurrentCredit,
getCreditLedgerPage, getCreditLedgerPage,
getPointsTopUpAgreement, getPointsTopUpAgreement,

View File

@@ -1,6 +1,6 @@
.template-version-hero { .template-version-hero {
position: relative; position: relative;
height: 360px; height: 380px;
overflow: hidden; overflow: hidden;
border-radius: 24px; border-radius: 24px;
background: var(--template-accent, #0a4d46); background: var(--template-accent, #0a4d46);

View File

@@ -46,11 +46,14 @@ import { onLoad, onShow } from "@dcloudio/uni-app";
import Privacy from "@/components/Privacy/index.vue"; import Privacy from "@/components/Privacy/index.vue";
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue"; import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js"; import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import { getWechatLoginCode } from "@/pages-aigc/services/wechatLogin.js"; import { getWechatLoginCode } from "@/pages-aigc/services/wechatLogin.js";
import { import {
createAigcGeneratorTask, createAigcImageToVideoTask,
getAigcTemplateItemList, createAigcGeneratorTask,
getAigcTemplateList, getAigcImageToVideoItemDetail,
getAigcGeneratorTaskDetail,
getAigcTemplateItemList,
getAigcTemplateList,
} from "@/pages-aigc/request/AigcApi.js"; } from "@/pages-aigc/request/AigcApi.js";
import { updateImageFile } from "@/request/api/UpdateFile.js"; import { updateImageFile } from "@/request/api/UpdateFile.js";
import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue"; import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue";
@@ -72,14 +75,27 @@ const uploadedImageUrl = ref("");
const currentStep = ref("version"); const currentStep = ref("version");
const pointDialogVisible = ref(false); const pointDialogVisible = ref(false);
const isUploading = ref(false); const isUploading = ref(false);
const isCreatingTask = ref(false); const isCreatingTask = ref(false);
const privacyVisible = ref(false); const privacyVisible = ref(false);
const privacyContractName = ref("隐私保护指引"); const privacyContractName = ref("隐私保护指引");
const pendingImageSourceType = ref(""); const pendingImageSourceType = ref("");
const upgradeTaskId = ref("");
const upgradeTemplateItemId = ref("");
const imageToVideoItemDetail = ref(null);
const upgradeSourceTaskDetail = ref(null);
const isImageToVideoUpgrade = computed(() => Boolean(upgradeTaskId.value));
const IMAGE_GENERATION_COST = 350;
const VIDEO_GENERATION_COST = 1000;
const currentTemplate = computed(() => { const currentTemplate = computed(() => {
return templates.value.find((item) => item.templateId === templateId.value) || templates.value[0] || {}; const template =
}); templates.value.find((item) => item.templateId === templateId.value) || templates.value[0] || {};
const imageResultUrl = String(upgradeSourceTaskDetail.value?.imageResultUrl || "").trim();
if (!isImageToVideoUpgrade.value || !imageResultUrl) return template;
return { ...template, templateContentUrl: imageResultUrl };
});
const currentTemplateItem = computed(() => { const currentTemplateItem = computed(() => {
return ( return (
@@ -89,16 +105,54 @@ const currentTemplateItem = computed(() => {
); );
}); });
const currentCost = computed(() => { const currentCost = computed(() => {
return Number(currentTemplateItem.value.generatorCost) || 0; return isImageToVideoUpgrade.value
}); ? Number(imageToVideoItemDetail.value?.generatorCost) || 0
: Number(currentTemplateItem.value.generatorType) === 1
? VIDEO_GENERATION_COST
: IMAGE_GENERATION_COST;
});
const showPlaceholderToast = (title) => { const showPlaceholderToast = (title) => {
uni.showToast({ uni.showToast({
title, title,
icon: "none", icon: "none",
}); });
}; };
const fetchImageToVideoItemDetail = async () => {
if (!isImageToVideoUpgrade.value) return;
try {
const res = await getAigcImageToVideoItemDetail({ taskId: upgradeTaskId.value });
if (res?.code === 0 && res.data && typeof res.data === "object" && !Array.isArray(res.data)) {
imageToVideoItemDetail.value = res.data;
return;
}
showPlaceholderToast(res?.msg || "获取升级视频费用失败");
} catch (error) {
console.warn("获取AIGC图片升级视频生成项详情失败", error);
showPlaceholderToast("获取升级视频费用失败");
}
};
const fetchUpgradeSourceTaskDetail = async () => {
if (!isImageToVideoUpgrade.value) return;
try {
const res = await getAigcGeneratorTaskDetail({ taskId: upgradeTaskId.value });
if (res?.code === 0 && res.data && typeof res.data === "object" && !Array.isArray(res.data)) {
upgradeSourceTaskDetail.value = res.data;
return;
}
showPlaceholderToast("获取原图片失败");
} catch (error) {
console.warn("获取AIGC原图片任务详情失败", error);
showPlaceholderToast("获取原图片失败");
}
};
const resetSelectedPhoto = () => { const resetSelectedPhoto = () => {
selectedImageLocalPath.value = ""; selectedImageLocalPath.value = "";
@@ -127,25 +181,39 @@ const fetchTemplateItemList = async () => {
} }
try { try {
const res = await getAigcTemplateItemList({ templateId: templateId.value }); const res = await getAigcTemplateItemList({ templateId: templateId.value });
if (res?.code === 0 && Array.isArray(res.data)) { if (res?.code === 0 && Array.isArray(res.data)) {
templateItems.value = res.data; templateItems.value = res.data;
selectedTemplateItemId.value = res.data[0]?.templateItemId || ""; const selectedUpgradeItem = res.data.find(
} (item) =>
String(item.templateItemId || "") === upgradeTemplateItemId.value &&
Number(item.generatorType) === 0,
);
const firstImageItem = res.data.find((item) => Number(item.generatorType) === 0);
selectedTemplateItemId.value =
(isImageToVideoUpgrade.value ? selectedUpgradeItem || firstImageItem : res.data[0])?.templateItemId || "";
if (isImageToVideoUpgrade.value) {
currentStep.value = "generateConfirm";
}
}
} catch (error) { } catch (error) {
console.warn("获取AIGC模板生成项失败", error); console.warn("获取AIGC模板生成项失败", error);
} }
}; };
const fetchPageData = async () => { const fetchPageData = async () => {
await fetchTemplateList(); await fetchTemplateList();
await fetchTemplateItemList(); await fetchImageToVideoItemDetail();
}; await fetchUpgradeSourceTaskDetail();
await fetchTemplateItemList();
};
onLoad((query = {}) => { onLoad((query = {}) => {
templateId.value = query.templateId || ""; templateId.value = String(query.templateId || "").trim();
fetchPageData(); upgradeTaskId.value = String(query.upgradeTaskId || "").trim();
}); upgradeTemplateItemId.value = String(query.upgradeTemplateItemId || "").trim();
fetchPageData();
});
onShow(() => { onShow(() => {
fetchCurrentCredit(); fetchCurrentCredit();
@@ -317,26 +385,31 @@ const handleClosePhotoConfirm = () => {
currentStep.value = "photoPick"; currentStep.value = "photoPick";
}; };
const handleConfirmPhoto = () => { const handleConfirmPhoto = () => {
if (!uploadedImageUrl.value) { if (!uploadedImageUrl.value) {
showPlaceholderToast("请先上传图片"); showPlaceholderToast("请先上传图片");
currentStep.value = "photoPick"; currentStep.value = "photoPick";
return; return;
} }
currentStep.value = "generateConfirm"; currentStep.value = "generateConfirm";
}; };
const handleGenerate = async () => { const handleGenerate = async () => {
if (isCreatingTask.value) return; if (isCreatingTask.value) return;
const templateItemId = currentTemplateItem.value.templateItemId; const templateItemId = currentTemplateItem.value.templateItemId;
if (!templateItemId) { if (!templateItemId) {
showPlaceholderToast("请选择生成类型"); showPlaceholderToast("请选择生成类型");
return; return;
} }
if (isImageToVideoUpgrade.value && !imageToVideoItemDetail.value) {
showPlaceholderToast("升级视频费用加载失败,请稍后重试");
return;
}
if (!uploadedImageUrl.value) { if (!isImageToVideoUpgrade.value && !uploadedImageUrl.value) {
showPlaceholderToast("请先上传图片"); showPlaceholderToast("请先上传图片");
currentStep.value = "photoPick"; currentStep.value = "photoPick";
return; return;
@@ -354,17 +427,33 @@ const handleGenerate = async () => {
uni.showLoading({ uni.showLoading({
title: "创建中", title: "创建中",
mask: true, mask: true,
}); });
try { try {
const wxLoginCode = await getWechatLoginCode(); let res;
const res = await createAigcGeneratorTask({ if (isImageToVideoUpgrade.value) {
templateItemId, const wxLoginCode = await getWechatLoginCode();
imageUrlList: [uploadedImageUrl.value], res = await createAigcImageToVideoTask({
wxLoginCode, taskId: upgradeTaskId.value,
}); wxLoginCode,
});
const taskId = String(res?.data || "").trim(); } else {
const wxLoginCode = await getWechatLoginCode();
res = await createAigcGeneratorTask({
templateItemId,
imageUrlList: [uploadedImageUrl.value],
wxLoginCode,
});
}
if (isImageToVideoUpgrade.value && String(res?.msg || "").includes("积分余额不足")) {
pointDialogVisible.value = true;
return;
}
const taskId = String(
typeof res?.data === "object" && res.data !== null ? res.data.taskId : res?.data || "",
).trim();
if (res?.code === 0 && taskId) { if (res?.code === 0 && taskId) {
fetchCurrentCredit(); fetchCurrentCredit();
uni.redirectTo({ uni.redirectTo({