2025-09-27 17:41:14 +08:00
|
|
|
|
<template>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
|
|
|
|
|
|
<div class="qrcode-popup-container">
|
|
|
|
|
|
<div class="qrcode-content">
|
|
|
|
|
|
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
|
|
|
|
|
|
@change="onSwipeChange">
|
|
|
|
|
|
<van-swipe-item v-for="(_, index) in posterImages" :key="index">
|
|
|
|
|
|
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
|
|
|
|
|
|
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</van-swipe-item>
|
|
|
|
|
|
</van-swipe>
|
|
|
|
|
|
</div>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
<div v-if="mode === 'promote'"
|
|
|
|
|
|
class="swipe-tip text-center text-gray-700 text-xs sm:text-sm mb-1 sm:mb-2 px-2">
|
2025-09-27 17:41:14 +08:00
|
|
|
|
<span class="swipe-icon">←</span> 左右滑动切换海报
|
|
|
|
|
|
<span class="swipe-icon">→</span>
|
|
|
|
|
|
</div>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
|
|
|
|
|
|
<!-- 微信环境:显示分享、保存和复制按钮 -->
|
|
|
|
|
|
<template v-if="isWeChat">
|
|
|
|
|
|
<!-- <div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToFriend">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_friends.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
分享给好友
|
|
|
|
|
|
</div>
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</div>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToTimeline">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_wechat.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
分享到朋友圈
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div> -->
|
|
|
|
|
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePosterForWeChat">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_img.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
保存图片
|
|
|
|
|
|
</div>
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</div>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_url.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
复制链接
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- 非微信环境:显示保存和复制按钮 -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_img.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
保存图片
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
|
|
|
|
|
<img src="@/assets/images/icon_share_url.svg"
|
|
|
|
|
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
|
|
|
|
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
|
|
|
|
复制链接
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</van-popup>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 图片保存指引遮罩层 -->
|
|
|
|
|
|
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
|
|
|
|
|
|
@close="closeImageGuide" />
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, watch, nextTick, computed, onMounted, toRefs } from "vue";
|
|
|
|
|
|
import QRCode from "qrcode";
|
|
|
|
|
|
import { showToast } from "vant";
|
2025-10-24 17:08:42 +08:00
|
|
|
|
import { useWeixinShare } from "@/composables/useWeixinShare";
|
|
|
|
|
|
import ImageSaveGuide from "./ImageSaveGuide.vue";
|
2025-09-27 17:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
linkIdentifier: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
mode: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "promote", // 例如 "promote" | "invitation"
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const { linkIdentifier, mode } = toRefs(props);
|
|
|
|
|
|
const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
|
|
|
|
|
|
const currentIndex = ref(0); // 当前显示的海报索引
|
|
|
|
|
|
const postersGenerated = ref([]); // 标记海报是否已经生成过,将在onMounted中初始化
|
|
|
|
|
|
const show = defineModel("show");
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 微信环境检测
|
|
|
|
|
|
const isWeChat = computed(() => {
|
|
|
|
|
|
return /MicroMessenger/i.test(navigator.userAgent);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 微信分享功能
|
|
|
|
|
|
const { configWeixinShare } = useWeixinShare();
|
|
|
|
|
|
|
|
|
|
|
|
// 图片保存指引遮罩层相关
|
|
|
|
|
|
const showImageGuide = ref(false);
|
|
|
|
|
|
const currentImageUrl = ref('');
|
|
|
|
|
|
const imageGuideTitle = ref('');
|
2025-09-27 17:41:14 +08:00
|
|
|
|
const url = computed(() => {
|
|
|
|
|
|
const baseUrl = window.location.origin; // 获取当前站点的域名
|
|
|
|
|
|
return mode.value === "promote"
|
|
|
|
|
|
? `${baseUrl}/agent/promotionInquire/` // 使用动态的域名
|
|
|
|
|
|
: `${baseUrl}/agent/invitationAgentApply/`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 海报图片数组
|
|
|
|
|
|
const posterImages = ref([]);
|
|
|
|
|
|
|
|
|
|
|
|
// QR码位置配置(为每个海报单独配置)
|
|
|
|
|
|
const qrCodePositions = ref({
|
|
|
|
|
|
promote: [
|
2025-12-16 12:58:09 +08:00
|
|
|
|
{ x: 180, y: 1440, size: 300 }, // tg_qrcode_1.png
|
|
|
|
|
|
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_2.jpg
|
|
|
|
|
|
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_3.jpg
|
|
|
|
|
|
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_4.jpg
|
|
|
|
|
|
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_5.jpg
|
|
|
|
|
|
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_6.jpg
|
|
|
|
|
|
{ x: 255, y: 940, size: 250 }, // tg_qrcode_7.jpg
|
|
|
|
|
|
{ x: 255, y: 940, size: 250 }, // tg_qrcode_8.jpg
|
2025-09-27 17:41:14 +08:00
|
|
|
|
],
|
|
|
|
|
|
// invitation模式的配置 (yq_qrcode)
|
|
|
|
|
|
invitation: [
|
2025-12-16 12:58:09 +08:00
|
|
|
|
{ x: 360, y: -1370, size: 360 }, // yq_qrcode_1.png
|
2025-09-27 17:41:14 +08:00
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理轮播图切换事件
|
|
|
|
|
|
const onSwipeChange = (index) => {
|
|
|
|
|
|
currentIndex.value = index;
|
|
|
|
|
|
if (!postersGenerated.value[index]) {
|
|
|
|
|
|
generatePoster(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载海报图片
|
|
|
|
|
|
const loadPosterImages = async () => {
|
|
|
|
|
|
const images = [];
|
|
|
|
|
|
const basePrefix = mode.value === "promote" ? "tg_qrcode_" : "yq_qrcode_";
|
|
|
|
|
|
|
2025-10-24 14:39:32 +08:00
|
|
|
|
// 根据模式确定要加载的图片编号
|
2025-12-16 12:58:09 +08:00
|
|
|
|
const imageNumbers = mode.value === "promote" ? [1, 2, 3, 4, 5, 6, 7, 8] : [1];
|
2025-09-27 17:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载图片
|
2025-10-24 14:39:32 +08:00
|
|
|
|
for (const i of imageNumbers) {
|
2025-09-27 17:41:14 +08:00
|
|
|
|
// 尝试加载 .png 文件
|
|
|
|
|
|
try {
|
|
|
|
|
|
const module = await import(
|
|
|
|
|
|
`@/assets/images/${basePrefix}${i}.png`
|
|
|
|
|
|
);
|
|
|
|
|
|
images.push(module.default);
|
|
|
|
|
|
continue; // 如果成功加载了 png,则跳过后续的 jpg 尝试
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`Image ${basePrefix}${i}.png not found, trying jpg...`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 .png 不存在,尝试加载 .jpg 文件
|
|
|
|
|
|
try {
|
|
|
|
|
|
const module = await import(
|
|
|
|
|
|
`@/assets/images/${basePrefix}${i}.jpg`
|
|
|
|
|
|
);
|
|
|
|
|
|
images.push(module.default);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`Image ${basePrefix}${i}.jpg not found either, using fallback.`
|
|
|
|
|
|
);
|
|
|
|
|
|
if (i === 1) {
|
|
|
|
|
|
// 如果第一张也不存在,创建一个空白图片
|
|
|
|
|
|
const emptyImg = new Image();
|
|
|
|
|
|
emptyImg.width = 600;
|
|
|
|
|
|
emptyImg.height = 800;
|
|
|
|
|
|
images.push(emptyImg.src);
|
|
|
|
|
|
} else if (images.length > 0) {
|
|
|
|
|
|
images.push(images[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return images;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
posterImages.value = await loadPosterImages();
|
|
|
|
|
|
// 根据加载的图片数量初始化postersGenerated数组
|
|
|
|
|
|
postersGenerated.value = Array(posterImages.value.length).fill(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 生成海报并合成二维码
|
|
|
|
|
|
const generatePoster = async (index) => {
|
|
|
|
|
|
// 如果已经生成过海报,就直接返回
|
|
|
|
|
|
if (postersGenerated.value[index]) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 DOM 已经渲染完成
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = posterCanvasRefs.value[index];
|
|
|
|
|
|
if (!canvas) return; // 如果 canvas 元素为空则直接返回
|
|
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 加载海报图片
|
|
|
|
|
|
const posterImg = new Image();
|
|
|
|
|
|
posterImg.src = posterImages.value[index];
|
|
|
|
|
|
|
|
|
|
|
|
posterImg.onload = () => {
|
|
|
|
|
|
// 设置 canvas 尺寸与海报图一致
|
|
|
|
|
|
canvas.width = posterImg.width;
|
|
|
|
|
|
canvas.height = posterImg.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 绘制海报图片
|
|
|
|
|
|
ctx.drawImage(posterImg, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 生成二维码
|
|
|
|
|
|
QRCode.toDataURL(
|
|
|
|
|
|
generalUrl(),
|
|
|
|
|
|
{ width: 150, margin: 0 },
|
|
|
|
|
|
(err, qrCodeUrl) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 加载二维码图片
|
|
|
|
|
|
const qrCodeImg = new Image();
|
|
|
|
|
|
qrCodeImg.src = qrCodeUrl;
|
|
|
|
|
|
qrCodeImg.onload = () => {
|
|
|
|
|
|
// 获取当前海报的二维码位置配置
|
|
|
|
|
|
const positions = qrCodePositions.value[mode.value];
|
|
|
|
|
|
const position = positions[index] || positions[0]; // 如果没有对应索引的配置,则使用第一个配置
|
|
|
|
|
|
|
|
|
|
|
|
// 计算Y坐标(负值表示从底部算起的位置)
|
|
|
|
|
|
const qrY =
|
|
|
|
|
|
position.y < 0
|
|
|
|
|
|
? posterImg.height + position.y
|
|
|
|
|
|
: position.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制二维码
|
|
|
|
|
|
ctx.drawImage(
|
|
|
|
|
|
qrCodeImg,
|
|
|
|
|
|
position.x,
|
|
|
|
|
|
qrY,
|
|
|
|
|
|
position.size,
|
|
|
|
|
|
position.size
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 标记海报已生成
|
|
|
|
|
|
postersGenerated.value[index] = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 show 变化,show 为 true 时生成海报
|
|
|
|
|
|
watch(show, (newVal) => {
|
|
|
|
|
|
if (newVal && !postersGenerated.value[currentIndex.value]) {
|
|
|
|
|
|
generatePoster(currentIndex.value); // 当弹窗显示且当前海报未生成时生成海报
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-24 17:08:42 +08:00
|
|
|
|
// 分享给好友
|
|
|
|
|
|
const shareToFriend = () => {
|
|
|
|
|
|
if (!isWeChat.value) {
|
|
|
|
|
|
showToast({ message: "请在微信中打开" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const shareUrl = generalUrl();
|
|
|
|
|
|
const shareConfig = {
|
|
|
|
|
|
title: mode.value === "promote"
|
2026-02-25 17:49:30 +08:00
|
|
|
|
? "天远数据 - 推广链接"
|
|
|
|
|
|
: "天远数据 - 邀请链接",
|
2025-10-24 17:08:42 +08:00
|
|
|
|
desc: mode.value === "promote"
|
2026-02-25 17:49:30 +08:00
|
|
|
|
? "扫码查看天远数据推广信息"
|
|
|
|
|
|
: "扫码申请天远数据代理权限",
|
2025-10-24 17:08:42 +08:00
|
|
|
|
link: shareUrl,
|
2025-10-30 13:34:28 +08:00
|
|
|
|
imgUrl: "https://www.tianyuandb.com/logo.jpg"
|
2025-10-24 17:08:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
configWeixinShare(shareConfig);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示分享指引
|
|
|
|
|
|
showShareGuide("好友");
|
2025-09-27 17:41:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-24 17:08:42 +08:00
|
|
|
|
// 分享到朋友圈
|
|
|
|
|
|
const shareToTimeline = () => {
|
|
|
|
|
|
if (!isWeChat.value) {
|
|
|
|
|
|
showToast({ message: "请在微信中打开" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const shareUrl = generalUrl();
|
|
|
|
|
|
const shareConfig = {
|
|
|
|
|
|
title: mode.value === "promote"
|
2026-02-25 17:49:30 +08:00
|
|
|
|
? "天远数据 - 推广链接"
|
|
|
|
|
|
: "天远数据 - 邀请链接",
|
2025-10-24 17:08:42 +08:00
|
|
|
|
desc: mode.value === "promote"
|
2026-02-25 17:49:30 +08:00
|
|
|
|
? "扫码查看天远数据推广信息"
|
|
|
|
|
|
: "扫码申请天远数据代理权限",
|
2025-10-24 17:08:42 +08:00
|
|
|
|
link: shareUrl,
|
2025-10-30 13:34:28 +08:00
|
|
|
|
imgUrl: "https://www.tianyuandb.com/logo.jpg"
|
2025-10-24 17:08:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
configWeixinShare(shareConfig);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示分享指引
|
|
|
|
|
|
showShareGuide("朋友圈");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示分享指引
|
|
|
|
|
|
const showShareGuide = (target) => {
|
|
|
|
|
|
// 设置遮罩层内容
|
|
|
|
|
|
currentImageUrl.value = ''; // 分享指引不需要图片
|
|
|
|
|
|
imageGuideTitle.value = `分享到${target}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示遮罩层
|
|
|
|
|
|
showImageGuide.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 微信环境保存图片
|
|
|
|
|
|
const savePosterForWeChat = () => {
|
|
|
|
|
|
const canvas = posterCanvasRefs.value[currentIndex.value];
|
|
|
|
|
|
const dataURL = canvas.toDataURL("image/png");
|
|
|
|
|
|
|
|
|
|
|
|
// 设置遮罩层内容
|
|
|
|
|
|
currentImageUrl.value = dataURL;
|
|
|
|
|
|
imageGuideTitle.value = '保存图片到相册';
|
|
|
|
|
|
|
|
|
|
|
|
// 显示遮罩层
|
|
|
|
|
|
showImageGuide.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭图片保存指引
|
|
|
|
|
|
const closeImageGuide = () => {
|
|
|
|
|
|
showImageGuide.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存海报图片 - 多种保存方式(非微信环境)
|
2025-09-27 17:41:14 +08:00
|
|
|
|
const savePoster = () => {
|
|
|
|
|
|
const canvas = posterCanvasRefs.value[currentIndex.value];
|
2025-10-24 17:08:42 +08:00
|
|
|
|
const dataURL = canvas.toDataURL("image/png");
|
|
|
|
|
|
|
|
|
|
|
|
// 检测环境
|
|
|
|
|
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
|
|
|
|
|
|
|
|
|
|
if (isMobile) {
|
|
|
|
|
|
// 手机浏览器环境
|
|
|
|
|
|
saveForMobile(dataURL);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// PC浏览器环境
|
|
|
|
|
|
saveForPC(dataURL);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// PC浏览器保存方式
|
|
|
|
|
|
const saveForPC = (dataURL) => {
|
2025-09-27 17:41:14 +08:00
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
|
a.href = dataURL;
|
2026-02-25 17:49:30 +08:00
|
|
|
|
a.download = "天远数据海报.png";
|
2025-09-27 17:41:14 +08:00
|
|
|
|
a.click();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-24 17:08:42 +08:00
|
|
|
|
// 手机浏览器保存方式
|
|
|
|
|
|
const saveForMobile = async (dataURL) => {
|
|
|
|
|
|
// 方法1: 尝试使用 File System Access API (Chrome 86+)
|
|
|
|
|
|
const fileSystemSuccess = await saveWithFileSystemAPI(dataURL);
|
|
|
|
|
|
if (fileSystemSuccess) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 方法2: 尝试使用 Blob 和 URL.createObjectURL
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = dataURLToBlob(dataURL);
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
|
a.href = url;
|
2026-02-25 17:49:30 +08:00
|
|
|
|
a.download = "天远数据海报.png";
|
2025-10-24 17:08:42 +08:00
|
|
|
|
a.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理 URL 对象
|
|
|
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
|
|
|
|
|
|
|
|
|
|
showToast({ message: "图片已保存到相册" });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Blob保存失败:", error);
|
|
|
|
|
|
// 方法3: 尝试使用 share API (支持分享到其他应用)
|
|
|
|
|
|
const shareSuccess = await tryShareAPI(dataURL);
|
|
|
|
|
|
if (!shareSuccess) {
|
|
|
|
|
|
// 方法4: 降级到长按保存提示
|
|
|
|
|
|
showLongPressTip(dataURL);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示长按保存提示(非微信环境使用)
|
|
|
|
|
|
const showLongPressTip = (dataURL) => {
|
|
|
|
|
|
// 设置遮罩层内容
|
|
|
|
|
|
currentImageUrl.value = dataURL;
|
|
|
|
|
|
imageGuideTitle.value = '保存图片到相册';
|
|
|
|
|
|
|
|
|
|
|
|
// 显示遮罩层
|
|
|
|
|
|
showImageGuide.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将 dataURL 转换为 Blob
|
|
|
|
|
|
const dataURLToBlob = (dataURL) => {
|
|
|
|
|
|
const arr = dataURL.split(',');
|
|
|
|
|
|
const mime = arr[0].match(/:(.*?);/)[1];
|
|
|
|
|
|
const bstr = atob(arr[1]);
|
|
|
|
|
|
let n = bstr.length;
|
|
|
|
|
|
const u8arr = new Uint8Array(n);
|
|
|
|
|
|
while (n--) {
|
|
|
|
|
|
u8arr[n] = bstr.charCodeAt(n);
|
|
|
|
|
|
}
|
|
|
|
|
|
return new Blob([u8arr], { type: mime });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 备用保存方法 - 使用 File System Access API (现代浏览器)
|
|
|
|
|
|
const saveWithFileSystemAPI = async (dataURL) => {
|
|
|
|
|
|
if ('showSaveFilePicker' in window) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = dataURLToBlob(dataURL);
|
|
|
|
|
|
const fileHandle = await window.showSaveFilePicker({
|
2026-02-25 17:49:30 +08:00
|
|
|
|
suggestedName: '天远数据海报.png',
|
2025-10-24 17:08:42 +08:00
|
|
|
|
types: [{
|
|
|
|
|
|
description: 'PNG images',
|
|
|
|
|
|
accept: { 'image/png': ['.png'] }
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const writable = await fileHandle.createWritable();
|
|
|
|
|
|
await writable.write(blob);
|
|
|
|
|
|
await writable.close();
|
|
|
|
|
|
|
|
|
|
|
|
showToast({ message: "图片已保存" });
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("File System API 保存失败:", error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试使用 Share API
|
|
|
|
|
|
const tryShareAPI = async (dataURL) => {
|
|
|
|
|
|
if (navigator.share && navigator.canShare) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = dataURLToBlob(dataURL);
|
2026-02-25 17:49:30 +08:00
|
|
|
|
const file = new File([blob], '天远数据海报.png', { type: 'image/png' });
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
|
|
|
|
|
if (navigator.canShare({ files: [file] })) {
|
|
|
|
|
|
await navigator.share({
|
2026-02-25 17:49:30 +08:00
|
|
|
|
title: '天远数据海报',
|
2025-10-24 17:08:42 +08:00
|
|
|
|
text: '分享海报图片',
|
|
|
|
|
|
files: [file]
|
|
|
|
|
|
});
|
|
|
|
|
|
showToast({ message: "图片已分享" });
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Share API 失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
const generalUrl = () => {
|
|
|
|
|
|
return url.value + encodeURIComponent(linkIdentifier.value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const copyUrl = () => {
|
|
|
|
|
|
copyToClipboard(generalUrl());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 复制链接
|
|
|
|
|
|
const copyToClipboard = (text) => {
|
|
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
|
|
// 支持 Clipboard API
|
|
|
|
|
|
navigator.clipboard
|
|
|
|
|
|
.writeText(text)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
showToast({ message: "链接已复制!" });
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
|
console.error("复制失败:", err);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 对于不支持 Clipboard API 的浏览器,使用 fallback 方法
|
|
|
|
|
|
const textArea = document.createElement("textarea");
|
|
|
|
|
|
textArea.value = text;
|
|
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
|
|
textArea.select();
|
|
|
|
|
|
try {
|
|
|
|
|
|
document.execCommand("copy");
|
|
|
|
|
|
showToast({ message: "链接已复制!" });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("复制失败:", err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
document.body.removeChild(textArea);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2025-10-24 17:08:42 +08:00
|
|
|
|
.qrcode-popup-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
max-height: 95vh;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qrcode-content {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 小屏设备优化 */
|
|
|
|
|
|
@media (max-width: 375px) {
|
|
|
|
|
|
.qrcode-content {
|
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 中等及以上屏幕 */
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.qrcode-content {
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
.poster-swiper {
|
2025-10-24 17:08:42 +08:00
|
|
|
|
height: calc(95vh - 180px);
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
max-height: 500px;
|
2025-09-27 17:41:14 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 17:08:42 +08:00
|
|
|
|
/* 小屏设备:更小的海报高度 */
|
|
|
|
|
|
@media (max-width: 375px) {
|
|
|
|
|
|
.poster-swiper {
|
|
|
|
|
|
height: calc(95vh - 160px);
|
|
|
|
|
|
min-height: 280px;
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 中等屏幕 */
|
|
|
|
|
|
@media (min-width: 640px) and (max-width: 767px) {
|
|
|
|
|
|
.poster-swiper {
|
|
|
|
|
|
height: calc(95vh - 190px);
|
|
|
|
|
|
max-height: 520px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 大屏幕 */
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
.poster-swiper {
|
|
|
|
|
|
height: calc(95vh - 200px);
|
|
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
.poster-canvas {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.swipe-tip {
|
|
|
|
|
|
animation: fadeInOut 2s infinite;
|
2025-10-24 17:08:42 +08:00
|
|
|
|
flex-shrink: 0;
|
2025-09-27 17:41:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.swipe-icon {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
animation: slideLeftRight 1.5s infinite;
|
2025-10-24 17:08:42 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.swipe-icon {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.share-icon {
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.share-icon:active {
|
|
|
|
|
|
transform: scale(0.95);
|
2025-09-27 17:41:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeInOut {
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
}
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
50% {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideLeftRight {
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
2025-09-27 17:41:14 +08:00
|
|
|
|
50% {
|
|
|
|
|
|
transform: translateX(5px);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 17:08:42 +08:00
|
|
|
|
|
|
|
|
|
|
/* 优化 van-divider 在小屏幕上的间距 */
|
|
|
|
|
|
:deep(.van-divider) {
|
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
:deep(.van-divider) {
|
|
|
|
|
|
margin: 0.75rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-27 17:41:14 +08:00
|
|
|
|
</style>
|