feat(aigc/detail): add share preview popup

create reusable SharePreviewPopup component for share preview
update ResultActions to emit share event instead of direct native share
add share preview flow in aigc detail page
update native share config with correct image and fallback title
This commit is contained in:
duanshuwen
2026-07-16 11:25:39 +08:00
parent 23f8453868
commit dcf67dedfc
4 changed files with 316 additions and 6 deletions

View File

@@ -4,7 +4,7 @@
{{ saving ? "保存中..." : "保存图片" }}
</view>
<button class="result-action is-secondary" open-type="share" hover-class="none">分享好友</button>
<button class="result-action is-secondary" hover-class="none" @tap="emit('share')">分享好友</button>
</view>
</template>
@@ -16,7 +16,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["save"]);
const emit = defineEmits(["save", "share"]);
const handleSave = () => {
if (!props.saving) {

View File

@@ -0,0 +1,99 @@
<template>
<uni-popup ref="popupRef" type="center" background-color="transparent"
mask-background-color="rgba(15, 23, 42, 0.58)" :safe-area="false" :is-mask-click="false">
<view class="share-preview-popup">
<text class="share-preview-badge">分享预览</text>
<text class="share-preview-heading">微信小程序分享卡片</text>
<view class="share-preview-card">
<view class="share-preview-brand">
<view class="share-preview-brand-mark">{{ brandInitial }}</view>
<view class="share-preview-brand-copy">
<text class="share-preview-brand-name">{{ displayAppName }}</text>
<text class="share-preview-brand-type">微信小程序</text>
</view>
</view>
<text class="share-preview-title">{{ displayTitle }}</text>
<text class="share-preview-variant">{{ displayVariantLabel }}</text>
<image v-if="imageUrl" class="share-preview-image" :src="imageUrl" mode="aspectFill" />
<view v-else class="share-preview-image share-preview-image-placeholder">
<text>作品预览</text>
</view>
<view class="share-preview-footer">
<text class="share-preview-footer-label">{{ displayAppName }}旅行作品</text>
<text class="share-preview-footer-action">打开小程序查看</text>
</view>
</view>
<button class="share-preview-share-button" open-type="share" hover-class="is-pressed"
@tap="handleShare">
分享给好友
</button>
<view class="share-preview-cancel-button" hover-class="is-pressed" @tap="handleCancel">
取消
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed, nextTick, ref } from "vue";
const props = defineProps({
appName: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
variantLabel: {
type: String,
default: "",
},
imageUrl: {
type: String,
default: "",
},
});
const emit = defineEmits(["close", "share"]);
const popupRef = ref(null);
const displayAppName = computed(() => props.appName.trim() || "微信小程序");
const displayTitle = computed(() => props.title.trim() || "旅行作品");
const displayVariantLabel = computed(() => props.variantLabel.trim() || "生成作品");
const brandInitial = computed(() => displayAppName.value.slice(-1) || "旅");
const open = async () => {
await nextTick();
popupRef.value?.open();
};
const close = () => {
popupRef.value?.close();
};
const handleShare = () => {
close();
emit("share");
};
const handleCancel = () => {
close();
emit("close");
};
defineExpose({
open,
close,
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,200 @@
.share-preview-popup {
width: calc(100vw - 28px);
max-width: 420px;
max-height: calc(100vh - 56px);
overflow-y: auto;
padding: 18px 16px 14px;
border: 1px solid rgba(73, 143, 205, 0.42);
border-radius: 20px;
background:
radial-gradient(90% 72% at 100% 0%, rgba(32, 207, 117, 0.15), rgba(32, 207, 117, 0) 60%),
linear-gradient(180deg, #dceeff 0%, #cfe7fb 100%);
box-shadow: 0 22px 54px rgba(16, 44, 73, 0.24);
box-sizing: border-box;
}
.share-preview-badge,
.share-preview-heading,
.share-preview-title,
.share-preview-variant,
.share-preview-brand-name,
.share-preview-brand-type,
.share-preview-footer-label,
.share-preview-footer-action {
display: block;
}
.share-preview-badge {
width: max-content;
margin: 0 auto;
padding: 3px 9px;
border-radius: 999px;
background: rgba(32, 207, 117, 0.13);
color: #0e9f61;
font-size: 10px;
line-height: 16px;
font-weight: 900;
}
.share-preview-heading {
margin-top: 7px;
color: #173a5f;
font-size: 20px;
line-height: 26px;
font-weight: 900;
text-align: center;
}
.share-preview-card {
margin-top: 12px;
overflow: hidden;
border: 1px solid rgba(73, 143, 205, 0.26);
border-radius: 12px;
background: rgba(255, 255, 255, 0.62);
box-shadow: 0 10px 24px rgba(23, 58, 95, 0.08);
}
.share-preview-brand {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 12px 7px;
}
.share-preview-brand-mark {
flex: 0 0 30px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #20cf75;
color: #173a5f;
font-size: 15px;
line-height: 30px;
font-weight: 900;
}
.share-preview-brand-copy {
min-width: 0;
}
.share-preview-brand-name {
overflow: hidden;
color: #173a5f;
font-size: 13px;
line-height: 18px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.share-preview-brand-type {
margin-top: 1px;
color: #6b8299;
font-size: 9px;
line-height: 12px;
font-weight: 700;
}
.share-preview-title {
overflow: hidden;
padding: 4px 12px 0;
color: #172033;
font-size: 17px;
line-height: 23px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.share-preview-variant {
padding: 1px 12px 10px;
color: #4e7394;
font-size: 11px;
line-height: 16px;
font-weight: 800;
}
.share-preview-image {
width: 100%;
height: 232px;
display: block;
background: #e8f2f8;
}
.share-preview-image-placeholder {
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(80% 85% at 50% 15%, rgba(32, 207, 117, 0.18), rgba(32, 207, 117, 0) 70%),
#e8f2f8;
color: #6b8299;
font-size: 13px;
line-height: 18px;
font-weight: 800;
}
.share-preview-footer {
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 12px;
}
.share-preview-footer-label,
.share-preview-footer-action {
min-width: 0;
overflow: hidden;
color: #4e7394;
font-size: 10px;
line-height: 14px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.share-preview-footer-action {
flex: 0 0 auto;
color: #173a5f;
}
.share-preview-share-button,
.share-preview-cancel-button {
width: 100%;
height: 44px;
margin: 12px 0 0;
padding: 0;
border: 0;
border-radius: 10px;
box-sizing: border-box;
font-size: 14px;
line-height: 44px;
font-weight: 900;
text-align: center;
}
.share-preview-share-button {
background: #20cf75;
color: #ffffff;
box-shadow: 0 8px 18px rgba(32, 207, 117, 0.2);
}
.share-preview-share-button::after {
border: 0;
}
.share-preview-cancel-button {
margin-top: 8px;
background: rgba(255, 255, 255, 0.45);
color: #385875;
}
.share-preview-share-button.is-pressed,
.share-preview-cancel-button.is-pressed {
opacity: 0.82;
}

View File

@@ -6,7 +6,7 @@
<template v-if="taskDetail">
<ResultPreview v-if="result.cover" :result="result" />
<ResultMeta :items="metaItems" />
<ResultActions :saving="isSaving" @save="handleSave" />
<ResultActions :saving="isSaving" @save="handleSave" @share="handleOpenSharePreview" />
<view class="aigc-detail-text-actions">
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
@@ -15,23 +15,30 @@
</view>
</template>
</view>
<SharePreviewPopup ref="sharePreviewPopup" :app-name="currentClientName" :title="result.title"
:variant-label="result.variantLabel" :image-url="sharePreviewImage" />
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShareAppMessage, onShow } from "@dcloudio/uni-app";
import { getCurrentConfig } from "@/constant/base.js";
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import { 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";
import SharePreviewPopup from "./components/SharePreviewPopup/index.vue";
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
const taskId = ref("");
const taskDetail = ref(null);
const isSaving = ref(false);
const sharePreviewPopup = ref(null);
const currentClientName = String(getCurrentConfig()?.name || "").trim() || "微信小程序";
const GENERATOR_TYPE_TEXT = {
0: "图片版",
@@ -102,6 +109,8 @@ const metaItems = computed(() => {
];
});
const sharePreviewImage = computed(() => taskDetail.value?.imageResultUrl || "");
const showPlaceholderToast = (title) => {
uni.showToast({
title,
@@ -147,15 +156,14 @@ onShow(() => {
fetchCurrentCredit();
});
onShareAppMessage(() => {
const shareInfo = {
title: taskDetail.value?.itemTitle,
title: taskDetail.value?.itemTitle || "分享旅行作品",
path: taskId.value
? `/pages-aigc/detail/detail?taskId=${encodeURIComponent(taskId.value)}`
: "/pages-aigc/home/home",
};
const imageUrl = taskDetail.value?.imageResultUrl || "";
const imageUrl = sharePreviewImage.value;
if (imageUrl) {
shareInfo.imageUrl = imageUrl;
@@ -164,6 +172,9 @@ onShareAppMessage(() => {
return shareInfo;
});
const handleOpenSharePreview = () => {
sharePreviewPopup.value?.open();
};
const handleBack = () => {
const pages = getCurrentPages();