feat(aigc): add video save support and improve consent flow

- add support for saving both image and video AIGC generation results
- refactor ResultActions component to accept mediaType prop and use dynamic save button text
- remove AIGC consent dialog from ChatQuickAccess and implement it on AIGC home page
- add WeChat mini-program privacy authorization check before saving media
- create reusable AigcConsentDialog component with proper styling
This commit is contained in:
duanshuwen
2026-07-16 21:49:00 +08:00
parent 38dbf7afc8
commit 52b6722842
6 changed files with 188 additions and 66 deletions

View File

@@ -6,8 +6,8 @@
<template v-if="taskDetail">
<ResultPreview v-if="result.cover" :result="result" />
<ResultMeta :items="metaItems" />
<ResultActions :saving="isSaving" :share-ready="Boolean(shareKey)" :share-preparing="isPreparingShare"
@save="handleSave" @prepare-share="handlePrepareShare" />
<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>
@@ -17,12 +17,15 @@
</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 {
@@ -39,6 +42,9 @@ 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: "图片版",
@@ -220,7 +226,7 @@ const handleRecharge = () => {
showPlaceholderToast("积分充值");
};
const downloadImage = (url) =>
const downloadMedia = (url) =>
new Promise((resolve, reject) => {
if (!/^https?:\/\//i.test(url)) {
resolve(url);
@@ -249,6 +255,46 @@ const saveImageToAlbum = (filePath) =>
});
});
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 (
@@ -258,10 +304,11 @@ const isAlbumPermissionDenied = (error) => {
);
};
const promptAlbumPermission = () => {
const promptAlbumPermission = (mediaType) => {
const mediaName = mediaType === "video" ? "视频" : "图片";
uni.showModal({
title: "需要相册权限",
content: "请在设置中允许保存图片到相册。",
content: `请在设置中允许保存${mediaName}到相册。`,
confirmText: "去设置",
success: ({ confirm }) => {
if (confirm) {
@@ -271,51 +318,104 @@ const promptAlbumPermission = () => {
});
};
const handleSave = async () => {
const saveResultMedia = async () => {
if (isSaving.value) return;
if (result.value.mediaType !== "image") {
showPlaceholderToast("当前结果不是图片");
return;
}
if (!result.value.cover) {
showPlaceholderToast("暂无可保存的图片");
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;
uni.showLoading({
title: "保存中",
mask: true,
});
let saveError = null;
let loadingVisible = false;
try {
const filePath = await downloadImage(result.value.cover);
await saveImageToAlbum(filePath);
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 {
uni.hideLoading();
if (loadingVisible) {
uni.hideLoading();
}
isSaving.value = false;
}
if (!saveError) {
uni.showToast({
title: "已保存到相册",
title: `${mediaName}已保存`,
icon: "success",
});
return;
}
if (isAlbumPermissionDenied(saveError)) {
promptAlbumPermission();
promptAlbumPermission(mediaType);
} else {
showPlaceholderToast("保存图片失败");
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("再生成一版");
};