update oss static resource urls to oss.nianxx.cn refactor privacy component to be reusable, add wechat mini-program privacy authorization check before image picking add image urls to photo guide examples, refine photo guide ui styles and remove unused avatarSrc prop
409 lines
11 KiB
Vue
409 lines
11 KiB
Vue
<template>
|
||
<view class="aigc-use-template-page">
|
||
<AigcTopBar :points="pointBalance" recharge-label="充值" @back="handleBack" @history="handleHistory"
|
||
@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" />
|
||
|
||
<template v-else>
|
||
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
|
||
@select="handleSelectTemplateItem" />
|
||
|
||
<text class="aigc-use-template-note">
|
||
动效视频版会先生图,再升级为 5 秒内动效视频。
|
||
</text>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="currentStep === 'uploadGuide'" class="aigc-use-template-popup-host">
|
||
<PhotoGuidePanel :positive-examples="positiveExamples" :negative-examples="negativeExamples"
|
||
@close="handleCloseGuide" @confirm="handleConfirmGuide" />
|
||
</view>
|
||
<view v-if="currentStep === 'photoPick'" class="aigc-use-template-popup-host">
|
||
<PhotoPickDrawer :loading="isUploading" @close="handleCloseGuide" @camera="handlePickCamera"
|
||
@album="handlePickAlbum" />
|
||
</view>
|
||
<view v-if="currentStep === 'photoConfirm'" class="aigc-use-template-popup-host">
|
||
<PhotoConfirmDrawer :avatar-src="selectedImageLocalPath || guideAvatar" @close="handleClosePhotoConfirm"
|
||
@confirm="handleConfirmPhoto" />
|
||
</view>
|
||
<PointInsufficientDialog v-if="pointDialogVisible" :cost="currentCost" :balance="pointBalance"
|
||
@cancel="handleClosePointDialog" @recharge="handlePointRecharge" />
|
||
<Privacy :visible="privacyVisible" :contract-name="privacyContractName" @agree="handlePrivacyAgree"
|
||
@disagree="handlePrivacyDisagree" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from "vue";
|
||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||
import Privacy from "@/components/Privacy/index.vue";
|
||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||
import {
|
||
createAigcGeneratorTask,
|
||
getAigcTemplateItemList,
|
||
getAigcTemplateList,
|
||
} from "@/request/api/AigcApi.js";
|
||
import { updateImageFile } from "@/request/api/UpdateFile.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";
|
||
|
||
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
||
const templateId = ref("");
|
||
const templates = ref([]);
|
||
const templateItems = ref([]);
|
||
const selectedTemplateItemId = ref("");
|
||
const selectedImageLocalPath = ref("");
|
||
const uploadedImageUrl = ref("");
|
||
const createdTaskId = ref("");
|
||
const currentStep = ref("version");
|
||
const pointDialogVisible = ref(false);
|
||
const isUploading = ref(false);
|
||
const isCreatingTask = ref(false);
|
||
const privacyVisible = ref(false);
|
||
const privacyContractName = ref("隐私保护指引");
|
||
const pendingImageSourceType = ref("");
|
||
|
||
const currentTemplate = computed(() => {
|
||
return templates.value.find((item) => item.templateId === templateId.value) || templates.value[0] || {};
|
||
});
|
||
|
||
const currentTemplateItem = computed(() => {
|
||
return (
|
||
templateItems.value.find((item) => item.templateItemId === selectedTemplateItemId.value) ||
|
||
templateItems.value[0] ||
|
||
{}
|
||
);
|
||
});
|
||
|
||
const currentCost = computed(() => {
|
||
return Number(currentTemplateItem.value.generatorCost) || 0;
|
||
});
|
||
|
||
const showPlaceholderToast = (title) => {
|
||
uni.showToast({
|
||
title,
|
||
icon: "none",
|
||
});
|
||
};
|
||
|
||
const resetSelectedPhoto = () => {
|
||
selectedImageLocalPath.value = "";
|
||
uploadedImageUrl.value = "";
|
||
};
|
||
|
||
const fetchTemplateList = async () => {
|
||
try {
|
||
const res = await getAigcTemplateList();
|
||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||
templates.value = res.data;
|
||
if (!templateId.value && res.data[0]?.templateId) {
|
||
templateId.value = res.data[0].templateId;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn("获取AIGC模板列表失败", error);
|
||
}
|
||
};
|
||
|
||
const fetchTemplateItemList = async () => {
|
||
if (!templateId.value) {
|
||
templateItems.value = [];
|
||
selectedTemplateItemId.value = "";
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await getAigcTemplateItemList({ templateId: templateId.value });
|
||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||
templateItems.value = res.data;
|
||
selectedTemplateItemId.value = res.data[0]?.templateItemId || "";
|
||
}
|
||
} catch (error) {
|
||
console.warn("获取AIGC模板生成项失败", error);
|
||
}
|
||
};
|
||
|
||
const fetchPageData = async () => {
|
||
await fetchTemplateList();
|
||
await fetchTemplateItemList();
|
||
};
|
||
|
||
onLoad((query = {}) => {
|
||
templateId.value = query.templateId || "";
|
||
fetchPageData();
|
||
});
|
||
|
||
onShow(() => {
|
||
fetchCurrentCredit();
|
||
});
|
||
|
||
const handleBack = () => {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
uni.navigateBack();
|
||
}
|
||
};
|
||
|
||
const handleHistory = () => {
|
||
uni.navigateTo({
|
||
url: "/pages-aigc/record/record",
|
||
fail: () => showPlaceholderToast("最近任务"),
|
||
});
|
||
};
|
||
|
||
const handleRecharge = () => {
|
||
uni.navigateTo({
|
||
url: "/pages-aigc/recharge/recharge",
|
||
fail: () => showPlaceholderToast("积分充值"),
|
||
});
|
||
};
|
||
|
||
const handleSelectTemplateItem = (option) => {
|
||
if (!option?.templateItemId) return;
|
||
|
||
selectedTemplateItemId.value = option.templateItemId;
|
||
resetSelectedPhoto();
|
||
currentStep.value = "uploadGuide";
|
||
};
|
||
|
||
const handleCloseGuide = () => {
|
||
currentStep.value = "version";
|
||
};
|
||
|
||
const handleConfirmGuide = () => {
|
||
currentStep.value = "photoPick";
|
||
};
|
||
|
||
const getChosenFilePath = (res) => {
|
||
return res?.tempFiles?.[0]?.tempFilePath || res?.tempFilePaths?.[0] || res?.tempFiles?.[0]?.path || "";
|
||
};
|
||
|
||
const isChooseCanceled = (error) => {
|
||
const message = String(error?.errMsg || error?.message || "");
|
||
return message.includes("cancel") || message.includes("取消");
|
||
};
|
||
|
||
const handleChooseImageSuccess = async (res) => {
|
||
const filePath = getChosenFilePath(res);
|
||
if (!filePath) {
|
||
showPlaceholderToast("请选择图片");
|
||
return;
|
||
}
|
||
|
||
selectedImageLocalPath.value = filePath;
|
||
uploadedImageUrl.value = "";
|
||
isUploading.value = true;
|
||
uni.showLoading({
|
||
title: "上传中",
|
||
mask: true,
|
||
});
|
||
|
||
try {
|
||
const uploadRes = await updateImageFile(filePath);
|
||
if (uploadRes?.code === 0 && uploadRes.data) {
|
||
uploadedImageUrl.value = uploadRes.data;
|
||
currentStep.value = "photoConfirm";
|
||
} else {
|
||
resetSelectedPhoto();
|
||
showPlaceholderToast(uploadRes?.msg || "图片上传失败");
|
||
}
|
||
} catch (error) {
|
||
resetSelectedPhoto();
|
||
console.warn("上传AIGC生成图片失败", error);
|
||
showPlaceholderToast("图片上传失败");
|
||
} finally {
|
||
isUploading.value = false;
|
||
uni.hideLoading();
|
||
}
|
||
};
|
||
|
||
const handleChooseImageFail = (error) => {
|
||
if (!isChooseCanceled(error)) {
|
||
console.warn("选择AIGC生成图片失败", error);
|
||
showPlaceholderToast("选择图片失败");
|
||
}
|
||
};
|
||
|
||
const chooseImageWithSource = (sourceType) => {
|
||
// #ifdef MP-WEIXIN
|
||
uni.chooseMedia({
|
||
count: 1,
|
||
mediaType: ["image"],
|
||
sourceType: [sourceType],
|
||
sizeType: ["compressed"],
|
||
success: handleChooseImageSuccess,
|
||
fail: handleChooseImageFail,
|
||
});
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ["compressed"],
|
||
sourceType: [sourceType],
|
||
success: handleChooseImageSuccess,
|
||
fail: handleChooseImageFail,
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
const chooseAndUploadImage = (sourceType) => {
|
||
if (isUploading.value || pendingImageSourceType.value) return;
|
||
|
||
// #ifdef MP-WEIXIN
|
||
pendingImageSourceType.value = sourceType;
|
||
wx.getPrivacySetting({
|
||
success: (res) => {
|
||
if (res?.needAuthorization) {
|
||
privacyContractName.value = res.privacyContractName || "隐私保护指引";
|
||
privacyVisible.value = true;
|
||
return;
|
||
}
|
||
|
||
pendingImageSourceType.value = "";
|
||
chooseImageWithSource(sourceType);
|
||
},
|
||
fail: (error) => {
|
||
pendingImageSourceType.value = "";
|
||
console.warn("检查微信隐私授权失败", error);
|
||
showPlaceholderToast("隐私授权检查失败");
|
||
},
|
||
});
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
chooseImageWithSource(sourceType);
|
||
// #endif
|
||
};
|
||
|
||
const handlePrivacyAgree = () => {
|
||
const sourceType = pendingImageSourceType.value;
|
||
privacyVisible.value = false;
|
||
pendingImageSourceType.value = "";
|
||
|
||
if (sourceType) {
|
||
chooseImageWithSource(sourceType);
|
||
}
|
||
};
|
||
|
||
const handlePrivacyDisagree = () => {
|
||
privacyVisible.value = false;
|
||
pendingImageSourceType.value = "";
|
||
};
|
||
|
||
const handlePickCamera = () => {
|
||
chooseAndUploadImage("camera");
|
||
};
|
||
|
||
const handlePickAlbum = () => {
|
||
chooseAndUploadImage("album");
|
||
};
|
||
|
||
const handleClosePhotoConfirm = () => {
|
||
currentStep.value = "photoPick";
|
||
};
|
||
|
||
const handleConfirmPhoto = () => {
|
||
if (!uploadedImageUrl.value) {
|
||
showPlaceholderToast("请先上传图片");
|
||
currentStep.value = "photoPick";
|
||
return;
|
||
}
|
||
|
||
currentStep.value = "generateConfirm";
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (isCreatingTask.value) return;
|
||
|
||
const templateItemId = currentTemplateItem.value.templateItemId;
|
||
if (!templateItemId) {
|
||
showPlaceholderToast("请选择生成类型");
|
||
return;
|
||
}
|
||
|
||
if (!uploadedImageUrl.value) {
|
||
showPlaceholderToast("请先上传图片");
|
||
currentStep.value = "photoPick";
|
||
return;
|
||
}
|
||
|
||
const cost = currentCost.value;
|
||
const balance = Number(pointBalance.value) || 0;
|
||
|
||
if (balance < cost) {
|
||
pointDialogVisible.value = true;
|
||
return;
|
||
}
|
||
|
||
isCreatingTask.value = true;
|
||
uni.showLoading({
|
||
title: "创建中",
|
||
mask: true,
|
||
});
|
||
|
||
try {
|
||
const res = await createAigcGeneratorTask({
|
||
templateItemId,
|
||
imageUrlList: [uploadedImageUrl.value],
|
||
});
|
||
|
||
if (res?.code === 0 && res.data) {
|
||
createdTaskId.value = res.data;
|
||
currentStep.value = "generating";
|
||
fetchCurrentCredit();
|
||
} else {
|
||
showPlaceholderToast(res?.msg || "创建任务失败");
|
||
}
|
||
} catch (error) {
|
||
console.warn("创建AIGC生成任务失败", error);
|
||
showPlaceholderToast("创建任务失败");
|
||
} finally {
|
||
isCreatingTask.value = false;
|
||
uni.hideLoading();
|
||
}
|
||
};
|
||
|
||
const handleGenerateComplete = () => {
|
||
if (!createdTaskId.value) {
|
||
showPlaceholderToast("任务创建失败");
|
||
return;
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(createdTaskId.value)}`,
|
||
});
|
||
};
|
||
|
||
const handleClosePointDialog = () => {
|
||
pointDialogVisible.value = false;
|
||
};
|
||
|
||
const handlePointRecharge = () => {
|
||
pointDialogVisible.value = false;
|
||
handleRecharge();
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "./styles/index.scss";
|
||
</style>
|