Merge branch 'aigc' of https://git.nianxx.cn/zoujing/YGChatCS into home3.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="result-actions">
|
||||
<view :class="['result-action', 'is-primary', { 'is-disabled': saving }]" @tap="handleSave">
|
||||
{{ saving ? "保存中..." : "保存图片" }}
|
||||
{{ saving ? "保存中..." : saveButtonText }}
|
||||
</view>
|
||||
|
||||
<button class="result-action is-secondary" :class="{ 'is-disabled': sharePreparing }"
|
||||
@@ -12,11 +12,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mediaType: {
|
||||
type: String,
|
||||
default: "image",
|
||||
},
|
||||
shareReady: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -28,6 +34,9 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["save", "prepare-share"]);
|
||||
const saveButtonText = computed(() =>
|
||||
props.mediaType === "video" ? "保存视频" : "保存图片"
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!props.saving) {
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
<video
|
||||
v-if="result.mediaType === 'video'"
|
||||
class="result-preview-image"
|
||||
:src="result.cover"
|
||||
:src="result.videoResultUrl"
|
||||
:poster="result.imageResultUrl"
|
||||
controls
|
||||
object-fit="cover"
|
||||
/>
|
||||
<image v-else class="result-preview-image" :src="result.cover" mode="aspectFill" />
|
||||
<image v-else class="result-preview-image" :src="result.imageResultUrl" mode="aspectFill" />
|
||||
<view class="result-preview-shade" />
|
||||
|
||||
<view class="result-preview-caption">
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
rgba(0, 0, 0, 0.16) 48%,
|
||||
rgba(0, 0, 0, 0.74) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.result-preview-caption {
|
||||
@@ -38,6 +39,7 @@
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
left: 18px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.result-preview-badge {
|
||||
|
||||
@@ -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: "图片版",
|
||||
@@ -60,12 +66,17 @@ const getMappedText = (mapping, value, prefix) => {
|
||||
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 = Boolean(videoUrl) && (record.generatorType === 1 || !imageUrl);
|
||||
const isVideo = Number(record.generatorType) === 1;
|
||||
const typeText = getMappedText(GENERATOR_TYPE_TEXT, record.generatorType, "类型");
|
||||
const statusText = getMappedText(TASK_STATUS_TEXT, record.taskStatus, "状态");
|
||||
const consumedText =
|
||||
@@ -76,7 +87,9 @@ const result = computed(() => {
|
||||
return {
|
||||
id: record.taskId,
|
||||
title: record.itemTitle || record.taskId || "生成结果",
|
||||
cover: isVideo ? videoUrl : imageUrl || videoUrl,
|
||||
cover: isVideo ? videoUrl : imageUrl,
|
||||
imageResultUrl: imageUrl,
|
||||
videoResultUrl: videoUrl,
|
||||
mediaType: isVideo ? "video" : "image",
|
||||
accent: "#0a4d46",
|
||||
variantLabel: typeText,
|
||||
@@ -90,8 +103,9 @@ const metaItems = computed(() => {
|
||||
const record = taskDetail.value;
|
||||
if (!record) return [];
|
||||
|
||||
const completeTime =
|
||||
record.videoGeneratorCompleteTime || record.imageGeneratorCompleteTime || record.createTime || "-";
|
||||
const completeTime = formatDateOnly(
|
||||
record.videoGeneratorCompleteTime || record.imageGeneratorCompleteTime || record.createTime
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -218,7 +232,7 @@ const handleRecharge = () => {
|
||||
showPlaceholderToast("积分充值");
|
||||
};
|
||||
|
||||
const downloadImage = (url) =>
|
||||
const downloadMedia = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
resolve(url);
|
||||
@@ -247,6 +261,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 (
|
||||
@@ -256,10 +310,11 @@ const isAlbumPermissionDenied = (error) => {
|
||||
);
|
||||
};
|
||||
|
||||
const promptAlbumPermission = () => {
|
||||
const promptAlbumPermission = (mediaType) => {
|
||||
const mediaName = mediaType === "video" ? "视频" : "图片";
|
||||
uni.showModal({
|
||||
title: "需要相册权限",
|
||||
content: "请在设置中允许保存图片到相册。",
|
||||
content: `请在设置中允许保存${mediaName}到相册。`,
|
||||
confirmText: "去设置",
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
@@ -269,51 +324,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("再生成一版");
|
||||
};
|
||||
|
||||
@@ -8,39 +8,36 @@
|
||||
:is-mask-click="false"
|
||||
@change="handlePopupChange"
|
||||
>
|
||||
<view
|
||||
class="aigc-consent-overlay"
|
||||
@touchmove.stop.prevent
|
||||
>
|
||||
<view class="aigc-consent-overlay" @touchmove.stop.prevent>
|
||||
<view class="aigc-consent-dialog">
|
||||
<text class="aigc-consent-title">温馨提示</text>
|
||||
|
||||
<view class="aigc-consent-content">
|
||||
<text v-if="content">{{ content }}</text>
|
||||
<template v-else>
|
||||
<text>{{ defaultContentPrefix }}</text>
|
||||
<text class="aigc-consent-rule" @tap.stop="handleRuleClick">
|
||||
《{{ ruleName }}》
|
||||
</text>
|
||||
<text>{{ defaultContentSuffix }}</text>
|
||||
</template>
|
||||
<text v-if="content">{{ content }}</text>
|
||||
<template v-else>
|
||||
<text>{{ defaultContentPrefix }}</text>
|
||||
<text class="aigc-consent-rule" @tap.stop="handleRuleClick">
|
||||
《{{ ruleName }}》
|
||||
</text>
|
||||
<text>{{ defaultContentSuffix }}</text>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="aigc-consent-actions">
|
||||
<button
|
||||
class="aigc-consent-button aigc-consent-cancel"
|
||||
type="default"
|
||||
@tap.stop="handleCancel"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="aigc-consent-button aigc-consent-agree"
|
||||
type="default"
|
||||
@tap.stop="handleAgree"
|
||||
>
|
||||
我同意
|
||||
</button>
|
||||
<button
|
||||
class="aigc-consent-button aigc-consent-cancel"
|
||||
type="default"
|
||||
@tap.stop="handleCancel"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="aigc-consent-button aigc-consent-agree"
|
||||
type="default"
|
||||
@tap.stop="handleAgree"
|
||||
>
|
||||
我同意
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
<FlowSteps :steps="steps" />
|
||||
</view>
|
||||
|
||||
<AigcConsentDialog
|
||||
v-model:visible="aigcConsentVisible"
|
||||
@agree="handleAigcConsentAgree"
|
||||
@cancel="handleAigcConsentCancel"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -16,15 +22,22 @@
|
||||
import { ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
import { getAigcGiftPointsToastClosed, setAigcGiftPointsToastClosed } from "@/constant/aigc.js";
|
||||
import {
|
||||
getAigcConsentAgreed,
|
||||
getAigcGiftPointsToastClosed,
|
||||
setAigcConsentAgreed,
|
||||
setAigcGiftPointsToastClosed,
|
||||
} from "@/constant/aigc.js";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import { getAigcTemplateList } from "@/request/api/AigcApi.js";
|
||||
import AigcConsentDialog from "./components/AigcConsentDialog/index.vue";
|
||||
import GiftPointsToast from "./components/GiftPointsToast/index.vue";
|
||||
import TemplateCarousel from "./components/TemplateCarousel/index.vue";
|
||||
import FlowSteps from "./components/FlowSteps/index.vue";
|
||||
|
||||
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
||||
const activeIndex = ref(0);
|
||||
const aigcConsentVisible = ref(false);
|
||||
const giftToastVisible = ref(!getAigcGiftPointsToastClosed());
|
||||
const templates = ref([]);
|
||||
const steps = ["选模板", "传素材", "添积分", "出结果"];
|
||||
@@ -42,6 +55,7 @@ const fetchTemplateList = async () => {
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
aigcConsentVisible.value = !getAigcConsentAgreed();
|
||||
fetchTemplateList();
|
||||
});
|
||||
|
||||
@@ -73,6 +87,22 @@ const handleCloseGiftToast = () => {
|
||||
setAigcGiftPointsToastClosed();
|
||||
};
|
||||
|
||||
const handleAigcConsentAgree = () => {
|
||||
setAigcConsentAgreed(true);
|
||||
};
|
||||
|
||||
const handleAigcConsentCancel = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index"
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectTemplate = (template) => {
|
||||
const templateId = template?.templateId || "";
|
||||
if (!templateId) return;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
<view class="shared-work-scroll">
|
||||
<template v-if="shareDetail">
|
||||
<video v-if="isVideo" class="shared-work-media is-video" :src="shareDetail.videoUrl"
|
||||
:poster="shareDetail.imageUrl" controls object-fit="cover" />
|
||||
<image v-else-if="shareDetail.imageUrl" class="shared-work-media" :src="shareDetail.imageUrl"
|
||||
<video v-if="isVideo" class="shared-work-media is-video" :src="videoResultUrl"
|
||||
:poster="imageResultUrl" controls object-fit="cover" />
|
||||
<image v-else-if="imageResultUrl" class="shared-work-media" :src="imageResultUrl"
|
||||
mode="aspectFill" />
|
||||
|
||||
<view class="shared-work-meta">
|
||||
@@ -37,8 +37,16 @@ import { getAigcGeneratorTaskShareDetail } from "@/request/api/AigcApi.js";
|
||||
const shareKey = ref("");
|
||||
const shareDetail = ref(null);
|
||||
|
||||
const imageResultUrl = computed(() => {
|
||||
return shareDetail.value?.imageResultUrl || shareDetail.value?.imageUrl || "";
|
||||
});
|
||||
|
||||
const videoResultUrl = computed(() => {
|
||||
return shareDetail.value?.videoResultUrl || shareDetail.value?.videoUrl || "";
|
||||
});
|
||||
|
||||
const isVideo = computed(() => {
|
||||
return Number(shareDetail.value?.generatorType) === 1 && Boolean(shareDetail.value?.videoUrl);
|
||||
return Number(shareDetail.value?.generatorType) === 1 && Boolean(videoResultUrl.value);
|
||||
});
|
||||
|
||||
const generatorTypeText = computed(() => {
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AigcConsentDialog v-model:visible="aigcConsentVisible" @agree="handleAigcConsentAgree" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -20,12 +18,9 @@
|
||||
import { ref } from "vue";
|
||||
import { Command } from "@/model/ChatModel";
|
||||
import { SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
|
||||
import { getAigcConsentAgreed, setAigcConsentAgreed } from "@/constant/aigc";
|
||||
import { checkToken } from "@/hooks/useGoLogin";
|
||||
import AigcConsentDialog from "../AigcConsentDialog/index.vue";
|
||||
|
||||
const AIGC_HOME_URL = "/pages-aigc/home/home";
|
||||
const aigcConsentVisible = ref(false);
|
||||
|
||||
const itemList = ref([
|
||||
{
|
||||
@@ -81,11 +76,7 @@ const sendReply = (item) => {
|
||||
// 旅行AIGC
|
||||
if (item.type === Command.travelAIGC) {
|
||||
checkToken().then(() => {
|
||||
if (getAigcConsentAgreed()) {
|
||||
navigateToAigcHome();
|
||||
} else {
|
||||
aigcConsentVisible.value = true;
|
||||
}
|
||||
navigateToAigcHome();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -96,11 +87,6 @@ const sendReply = (item) => {
|
||||
const navigateToAigcHome = () => {
|
||||
uni.navigateTo({ url: AIGC_HOME_URL });
|
||||
};
|
||||
|
||||
const handleAigcConsentAgree = () => {
|
||||
setAigcConsentAgreed(true);
|
||||
navigateToAigcHome();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user