replace placeholder toast with actual recharge page navigation, and add a failure callback to show error toast on navigation fail
450 lines
11 KiB
Vue
450 lines
11 KiB
Vue
<template>
|
|
<view class="aigc-detail-page">
|
|
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" @recharge="handleRecharge" />
|
|
|
|
<view class="aigc-detail-content">
|
|
<template v-if="taskDetail">
|
|
<ResultPreview v-if="result.cover" :result="result" />
|
|
<ResultMeta :items="metaItems" />
|
|
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
|
|
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
|
|
|
|
<view class="aigc-detail-text-actions">
|
|
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
|
|
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
|
|
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
|
|
</view>
|
|
</template>
|
|
</view>
|
|
|
|
<Privacy :visible="privacyVisible" :contract-name="privacyContractName" @agree="handlePrivacyAgree"
|
|
@disagree="handlePrivacyDisagree" />
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from "vue";
|
|
import { onLoad, onShareAppMessage, 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 {
|
|
createAigcGeneratorTaskShare,
|
|
getAigcGeneratorTaskDetail,
|
|
} from "@/request/api/AigcApi.js";
|
|
import ResultActions from "./components/ResultActions/index.vue";
|
|
import ResultMeta from "./components/ResultMeta/index.vue";
|
|
import ResultPreview from "./components/ResultPreview/index.vue";
|
|
|
|
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
|
const taskId = ref("");
|
|
const taskDetail = ref(null);
|
|
const isSaving = ref(false);
|
|
const isPreparingShare = ref(false);
|
|
const shareKey = ref("");
|
|
const privacyVisible = ref(false);
|
|
const privacyContractName = ref("隐私保护指引");
|
|
const pendingSaveAfterPrivacy = ref(false);
|
|
|
|
const GENERATOR_TYPE_TEXT = {
|
|
0: "图片版",
|
|
1: "视频版",
|
|
};
|
|
|
|
const TASK_STATUS_TEXT = {
|
|
0: "排队中",
|
|
1: "生成中",
|
|
2: "已完成",
|
|
3: "生成失败",
|
|
4: "视频生成中",
|
|
};
|
|
|
|
const getMappedText = (mapping, value, prefix) => {
|
|
if (Object.prototype.hasOwnProperty.call(mapping, value)) {
|
|
return mapping[value];
|
|
}
|
|
return value === undefined || value === null || value === "" ? "-" : `${prefix}${value}`;
|
|
};
|
|
|
|
const formatDateOnly = (value) => {
|
|
if (!value) return "-";
|
|
return String(value).match(/^\d{4}-\d{2}-\d{2}/)?.[0] || "-";
|
|
};
|
|
|
|
const result = computed(() => {
|
|
const record = taskDetail.value || {};
|
|
|
|
const imageUrl = record.imageResultUrl || "";
|
|
const videoUrl = record.videoResultUrl || "";
|
|
const isVideo = Number(record.generatorType) === 1;
|
|
const typeText = getMappedText(GENERATOR_TYPE_TEXT, record.generatorType, "类型");
|
|
const statusText = getMappedText(TASK_STATUS_TEXT, record.taskStatus, "状态");
|
|
const consumedText =
|
|
record.generatorCost === undefined || record.generatorCost === null || record.generatorCost === ""
|
|
? "-"
|
|
: `${record.generatorCost}积分`;
|
|
|
|
return {
|
|
id: record.taskId,
|
|
title: record.itemTitle || record.taskId || "生成结果",
|
|
cover: isVideo ? videoUrl : imageUrl,
|
|
imageResultUrl: imageUrl,
|
|
videoResultUrl: videoUrl,
|
|
mediaType: isVideo ? "video" : "image",
|
|
accent: "#0a4d46",
|
|
variantLabel: typeText,
|
|
resultLabel: statusText,
|
|
consumedText,
|
|
desc: record.itemSubTitle || statusText,
|
|
};
|
|
});
|
|
|
|
const metaItems = computed(() => {
|
|
const record = taskDetail.value;
|
|
if (!record) return [];
|
|
|
|
const completeTime = formatDateOnly(
|
|
record.videoGeneratorCompleteTime || record.imageGeneratorCompleteTime || record.createTime
|
|
);
|
|
|
|
return [
|
|
{
|
|
label: "状态",
|
|
value: result.value.resultLabel,
|
|
},
|
|
{
|
|
label: "已消耗",
|
|
value: result.value.consumedText,
|
|
},
|
|
{
|
|
label: "完成时间",
|
|
value: completeTime,
|
|
},
|
|
];
|
|
});
|
|
|
|
const sharePreviewImage = computed(() => taskDetail.value?.imageResultUrl || "");
|
|
|
|
const showPlaceholderToast = (title) => {
|
|
uni.showToast({
|
|
title,
|
|
icon: "none",
|
|
});
|
|
};
|
|
|
|
const fetchTaskDetail = async () => {
|
|
uni.showLoading({
|
|
title: "加载中",
|
|
mask: true,
|
|
});
|
|
|
|
try {
|
|
const res = await getAigcGeneratorTaskDetail({ taskId: taskId.value });
|
|
if (res?.code === 0 && res.data && typeof res.data === "object" && !Array.isArray(res.data)) {
|
|
taskDetail.value = res.data;
|
|
prepareShareKey();
|
|
return;
|
|
}
|
|
|
|
taskDetail.value = null;
|
|
showPlaceholderToast("获取任务详情失败");
|
|
} catch (error) {
|
|
console.error("获取AIGC生成任务详情失败", error);
|
|
taskDetail.value = null;
|
|
showPlaceholderToast("获取任务详情失败");
|
|
} finally {
|
|
uni.hideLoading();
|
|
}
|
|
};
|
|
|
|
onLoad((query = {}) => {
|
|
taskId.value = String(query.taskId || "").trim();
|
|
if (!taskId.value) {
|
|
showPlaceholderToast("缺少任务ID");
|
|
return;
|
|
}
|
|
|
|
fetchTaskDetail();
|
|
});
|
|
|
|
onShow(() => {
|
|
fetchCurrentCredit();
|
|
});
|
|
|
|
onShareAppMessage(() => {
|
|
const shareInfo = {
|
|
title: taskDetail.value?.itemTitle || "分享旅行作品",
|
|
path: shareKey.value
|
|
? `/pages-aigc/sharedWork/sharedWork?shareKey=${encodeURIComponent(shareKey.value)}`
|
|
: "/pages/index/index",
|
|
};
|
|
const imageUrl = sharePreviewImage.value;
|
|
|
|
if (imageUrl) {
|
|
shareInfo.imageUrl = imageUrl;
|
|
}
|
|
|
|
return shareInfo;
|
|
});
|
|
|
|
const prepareShareKey = async (showFeedback = false) => {
|
|
if (isPreparingShare.value || !taskId.value) return;
|
|
if (shareKey.value) return;
|
|
|
|
isPreparingShare.value = true;
|
|
|
|
try {
|
|
const res = await createAigcGeneratorTaskShare({ taskId: taskId.value });
|
|
const nextShareKey = String(res?.data?.shareKey || "").trim();
|
|
if (res?.code === 0 && nextShareKey) {
|
|
shareKey.value = nextShareKey;
|
|
if (showFeedback) {
|
|
showPlaceholderToast("分享已准备,请再次点击");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (showFeedback) {
|
|
showPlaceholderToast(res?.msg || "创建分享失败");
|
|
}
|
|
} catch (error) {
|
|
console.error("创建AIGC任务分享失败", error);
|
|
if (showFeedback) {
|
|
showPlaceholderToast("创建分享失败");
|
|
}
|
|
} finally {
|
|
isPreparingShare.value = false;
|
|
}
|
|
};
|
|
|
|
const handlePrepareShare = () => {
|
|
prepareShareKey(true);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
const pages = getCurrentPages();
|
|
if (pages.length > 1) {
|
|
uni.navigateBack();
|
|
}
|
|
};
|
|
|
|
const handleRecharge = () => {
|
|
uni.navigateTo({
|
|
url: "/pages-aigc/recharge/recharge",
|
|
fail: () => showPlaceholderToast("打开充值页面失败"),
|
|
});
|
|
};
|
|
|
|
const downloadMedia = (url) =>
|
|
new Promise((resolve, reject) => {
|
|
if (!/^https?:\/\//i.test(url)) {
|
|
resolve(url);
|
|
return;
|
|
}
|
|
|
|
uni.downloadFile({
|
|
url,
|
|
success: (res) => {
|
|
if (res.statusCode === 200 && res.tempFilePath) {
|
|
resolve(res.tempFilePath);
|
|
return;
|
|
}
|
|
reject(res);
|
|
},
|
|
fail: reject,
|
|
});
|
|
});
|
|
|
|
const saveImageToAlbum = (filePath) =>
|
|
new Promise((resolve, reject) => {
|
|
uni.saveImageToPhotosAlbum({
|
|
filePath,
|
|
success: resolve,
|
|
fail: reject,
|
|
});
|
|
});
|
|
|
|
const saveVideoToAlbum = (filePath) =>
|
|
new Promise((resolve, reject) => {
|
|
uni.saveVideoToPhotosAlbum({
|
|
filePath,
|
|
success: resolve,
|
|
fail: reject,
|
|
});
|
|
});
|
|
|
|
const ensureAlbumPermission = () =>
|
|
new Promise((resolve, reject) => {
|
|
// #ifdef MP-WEIXIN
|
|
uni.getSetting({
|
|
success: ({ authSetting = {} }) => {
|
|
const permission = authSetting["scope.writePhotosAlbum"];
|
|
if (permission === true) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (permission === false) {
|
|
reject({ errMsg: "scope.writePhotosAlbum permission denied" });
|
|
return;
|
|
}
|
|
|
|
uni.authorize({
|
|
scope: "scope.writePhotosAlbum",
|
|
success: resolve,
|
|
fail: reject,
|
|
});
|
|
},
|
|
fail: reject,
|
|
});
|
|
// #endif
|
|
|
|
// #ifndef MP-WEIXIN
|
|
resolve();
|
|
// #endif
|
|
});
|
|
|
|
const isAlbumPermissionDenied = (error) => {
|
|
const errorMessage = String(error?.errMsg || "").toLowerCase();
|
|
return (
|
|
errorMessage.includes("auth deny") ||
|
|
errorMessage.includes("authorize:no response") ||
|
|
errorMessage.includes("permission denied")
|
|
);
|
|
};
|
|
|
|
const promptAlbumPermission = (mediaType) => {
|
|
const mediaName = mediaType === "video" ? "视频" : "图片";
|
|
uni.showModal({
|
|
title: "需要相册权限",
|
|
content: `请在设置中允许保存${mediaName}到相册。`,
|
|
confirmText: "去设置",
|
|
success: ({ confirm }) => {
|
|
if (confirm) {
|
|
uni.openSetting();
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const saveResultMedia = async () => {
|
|
if (isSaving.value) return;
|
|
|
|
const mediaType = result.value.mediaType;
|
|
const isVideo = mediaType === "video";
|
|
const mediaName = isVideo ? "视频" : "图片";
|
|
const mediaUrl = isVideo ? result.value.videoResultUrl : result.value.imageResultUrl;
|
|
if (!mediaUrl) {
|
|
showPlaceholderToast(`暂无可保存的${mediaName}`);
|
|
return;
|
|
}
|
|
|
|
isSaving.value = true;
|
|
|
|
let saveError = null;
|
|
let loadingVisible = false;
|
|
try {
|
|
await ensureAlbumPermission();
|
|
uni.showLoading({
|
|
title: "保存中",
|
|
mask: true,
|
|
});
|
|
loadingVisible = true;
|
|
|
|
const filePath = await downloadMedia(mediaUrl);
|
|
if (isVideo) {
|
|
await saveVideoToAlbum(filePath);
|
|
} else {
|
|
await saveImageToAlbum(filePath);
|
|
}
|
|
} catch (error) {
|
|
saveError = error;
|
|
} finally {
|
|
if (loadingVisible) {
|
|
uni.hideLoading();
|
|
}
|
|
isSaving.value = false;
|
|
}
|
|
|
|
if (!saveError) {
|
|
uni.showToast({
|
|
title: `${mediaName}已保存`,
|
|
icon: "success",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isAlbumPermissionDenied(saveError)) {
|
|
promptAlbumPermission(mediaType);
|
|
} else {
|
|
showPlaceholderToast(`保存${mediaName}失败`);
|
|
}
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (isSaving.value || pendingSaveAfterPrivacy.value) return;
|
|
|
|
// #ifdef MP-WEIXIN
|
|
pendingSaveAfterPrivacy.value = true;
|
|
wx.getPrivacySetting({
|
|
success: (res) => {
|
|
if (res?.needAuthorization) {
|
|
privacyContractName.value = res.privacyContractName || "隐私保护指引";
|
|
privacyVisible.value = true;
|
|
return;
|
|
}
|
|
|
|
pendingSaveAfterPrivacy.value = false;
|
|
saveResultMedia();
|
|
},
|
|
fail: (error) => {
|
|
pendingSaveAfterPrivacy.value = false;
|
|
console.warn("检查微信隐私授权失败", error);
|
|
showPlaceholderToast("隐私授权检查失败");
|
|
},
|
|
});
|
|
// #endif
|
|
|
|
// #ifndef MP-WEIXIN
|
|
saveResultMedia();
|
|
// #endif
|
|
};
|
|
|
|
const handlePrivacyAgree = () => {
|
|
const shouldSave = pendingSaveAfterPrivacy.value;
|
|
privacyVisible.value = false;
|
|
pendingSaveAfterPrivacy.value = false;
|
|
|
|
if (shouldSave) {
|
|
saveResultMedia();
|
|
}
|
|
};
|
|
|
|
const handlePrivacyDisagree = () => {
|
|
privacyVisible.value = false;
|
|
pendingSaveAfterPrivacy.value = false;
|
|
};
|
|
|
|
const handleRegenerate = () => {
|
|
showPlaceholderToast("再生成一版");
|
|
};
|
|
|
|
const handleChangeTemplate = () => {
|
|
uni.navigateTo({
|
|
url: "/pages-aigc/home/home",
|
|
fail: () => showPlaceholderToast("换个模板"),
|
|
});
|
|
};
|
|
|
|
const handleOpenRecords = () => {
|
|
uni.navigateTo({
|
|
url: "/pages-aigc/record/record",
|
|
fail: () => showPlaceholderToast("历史记录"),
|
|
});
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import "./styles/index.scss";
|
|
</style>
|