微信闪照小程序实现

2,141 阅读8分钟

已经有一年半没有写文章了,今天给掘友们写一个闪照实现的demo,纯前端开发技术栈为uniapp+uni云开发;先贴出代码

首先是闪照的几个要点(小程序申请注册啥的就不说了,只说功能)

  1. 上传图片到uni云存储空间
  2. 上传图片需要做违规检测
  3. 闪照需要分享出去,微信分享功能
  4. 查看闪照时需要限时查看和防止手机截屏

image.png

image.png

	<view wx:if="{{!isBlackScreen}}" class="page-container {{isBlackScreen ? 'black-screen' : ''}} container">
		<view class="upload-area">
			<up-upload :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="1"
				width="400" height="500">
			</up-upload>
		</view>
		<!-- 按钮区域 -->
		<view class="button-group">
			<!-- 隐藏的上传组件 -->
			<up-upload ref="uploadRef" :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple
				:maxCount="1" style="display: none;"></up-upload>
			<u-button class="action-button" shape="circle" icon="photo" text="选择照片" @click="handleSelectPhoto" />
			<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />
		</view>
		<custom-tabbar :current="currentTab"></custom-tabbar>
	</view>
	<view wx:if="{{isBlackScreen}}" class="black-screen-overlay">
		<text>禁止截图或录屏</text>
	</view>
</template>

<script setup>
	import {
		ref
	} from 'vue'
	import {
		onLoad,
		onShow,
		onNavigationBarButtonTap,
		onPullDownRefresh,
		onReachBottom,
		onUnload,
		onShareAppMessage
	} from '@dcloudio/uni-app';
	import CustomTabbar from '../components/custom-tabber.vue'
	const currentTab = ref(0) //tabbar
	const fileList = ref([]);
	const subscribeNotify = ref(false);
	const allowForward = ref(false);
	const uploadRef = ref(null);
	const canShare = ref(false); // 新增:控制是否允许分享
	const handleSelectPhoto = () => {
		// 手动触发上传组件的选择文件
		uploadRef.value?.chooseFile();
	};
	const isBlackScreen = ref(false) // 是否显示黑屏
	onLoad(() => {
		wx.showShareMenu({
			menus: ['shareAppMessage', 'shareTimeline'],
			success() {
				console.log('分享功能已启用')
			}
		})
		wx.onUserCaptureScreen(() => {
			this.setData({
				isBlackScreen: true
			}); // 触发黑屏

			// 3秒后恢复(可选)
			setTimeout(() => {
				this.setData({
					isBlackScreen: false
				});
			}, 3000);
		});

	})
	onLoad(() => {
		
	})
	onUnload(() => {
		wx.offUserCaptureScreen(); // 移除监听
	});
	onShareAppMessage(() => {
		if (!canShare.value || !fileID.value) {
			uni.showToast({
				title: '请先上传图片',
				icon: 'none'
			});
			return {};
		}
		console.log(fileID.value); // 查看 fileID 是否正常
		return {
			title: '查看闪照',
			path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
			imageUrl: '/static/sz.png', // 分享图片
			success(res) {
				uni.showToast({
					title: '分享成功'
				})
			},
			fail(err) {
				console.log('分享失败', err)
			}
		}
	})
	// 删除图片
	const deletePic = (event) => {
		fileList.value.splice(event.index, 1);
		canShare.value = false; // 删除图片后禁止分享
	};
	const toview = () => {
		uni.navigateTo({
			url: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
		})
	}
	const handleToTop = () => {
		uni.navigateTo({
			url: '/pages/wgbtop/wgbtop',
		})
	}
	const afterRead = async (event) => {
		fileList.value = []
		canShare.value = false; // 开始上传时先禁止分享
		let lists = [].concat(event.file);
		console.log('选择的文件:', lists);
		let fileListLen = fileList.value.length;

		// 更新UI状态
		lists.map((item) => {
			fileList.value.push({
				...item,
				status: 'checking',
				message: '安全检测中',
			});
		});

		// 显示加载中状态
		uni.showLoading({
			title: '正在加载中...',
			mask: true
		});

		// 读取文件的辅助函数
		const readFileContent = async (fileItem) => {
			// H5环境
			if (fileItem.file && fileItem.file instanceof File) {
				return await new Promise((resolve, reject) => {
					const reader = new FileReader();
					reader.onload = (e) => resolve(e.target.result);
					reader.onerror = reject;
					reader.readAsArrayBuffer(fileItem.file);
				});
			}
			// 小程序环境
			else if (fileItem.url) {
				return await new Promise((resolve, reject) => {
					uni.getFileSystemManager().readFile({
						filePath: fileItem.url,
						encoding: 'binary',
						success: res => resolve(res.data),
						fail: reject
					});
				});
			}
			throw new Error('不支持的文件类型');
		};

		for (let i = 0; i < lists.length; i++) {
			let uploadResult = null;
			try {
				// 更新状态为上传中
				fileList.value[i].status = 'uploading';
				fileList.value[i].message = '正在加载...';

				// 读取文件内容
				const fileContent = await readFileContent(lists[i]);

				// 更新状态为检测中
				fileList.value[i].status = 'checking';
				fileList.value[i].message = '安全检测中...';
				// 调用云函数进行安全检测
				const checkResult = await uniCloud.callFunction({
					name: 'imgSecCheck',
					data: {
						fileContent: fileContent
					}
				});
				if (checkResult.result.code !== 0) {
					throw new Error(checkResult.result.message || '图片安全检测未通过');
				}
				console.log('安全检测通过', checkResult);
				// 更新状态为上传中
				fileList.value[i].status = 'uploading';
				// fileList.value[i].message = '正在上传...';
				// 安全检测通过后再上传到uniCloud
				uploadResult = await uploadToUniCloud(lists[i]);

				let item = fileList.value[fileListLen];
				fileList.value.splice(fileListLen, 1, {
					...item,
					status: 'success',
					message: '加载成功',
					url: uploadResult.fileID,
				});
				fileListLen++;
				
				// 上传成功,允许分享
				canShare.value = true;
				
				// 隐藏加载中
				uni.hideLoading();
				uni.showToast({
					title: '加载成功',
					icon: 'success',
					duration: 2000
				});
			} catch (error) {
				console.error('检测或上传失败:', error);
				let item = fileList.value[fileListLen];

				let message = '上传失败,图片可能包含违规内容';
				if (error.message && error.message.includes('违规')) {
					message = '图片包含违规内容';
				} else if (error.message && error.message.includes('大小')) {
					message = '图片大小超过限制(10MB)';
				} else if (error.errMsg && error.errMsg.includes('fail')) {
					message = '安全检测服务异常';
				}
				fileList.value.splice(fileListLen, 1, {
					...item,
					status: 'failed',
					message: message,
				});
				fileListLen++;
				// 上传失败,禁止分享
				canShare.value = false;
				// 隐藏加载中并显示错误
				uni.hideLoading();
				uni.showToast({
					title: message,
					icon: 'none',
					duration: 3000
				});
				// 如果上传了文件但检测失败,删除已上传的文件
				if (uploadResult && uploadResult.fileID) {
					try {
						await uniCloud.deleteFile({
							fileList: [uploadResult.fileID]
						});
						console.log('已删除未通过检测的文件');
					} catch (deleteError) {
						console.error('删除文件失败:', deleteError);
					}
				}
			}
		}
	};
	// 上传到uniCloud云存储
	const fileID = ref()
	const uploadToUniCloud = async (fileItem) => {
		// 如果是H5环境且有原始File对象
		if (fileItem.file && process.env.VUE_APP_PLATFORM === 'h5') {
			// H5方式上传
			const cloudPath = 'uploads/' + Date.now() + '-' + fileItem.file.name + '.png';
			const res = await uniCloud.uploadFile({
				filePath: fileItem.file,
				cloudPath: cloudPath
			});
			fileID.value = res.fileID
			return res;
		} else {
			// 小程序/APP方式上传
			const cloudPath = 'uploads/' + Date.now() + '-' + Math.random().toString(36).substring(2) + '.png';
			const res = await uniCloud.uploadFile({
				filePath: fileItem.url,
				cloudPath: cloudPath
			});
			fileID.value = res.fileID
			console.log(fileID.value)
			return res;
		}
	};
	onShow(() => {
		uni.hideTabBar()
		// 根据当前页面设置currentTab
		const pages = getCurrentPages()
		const page = pages[pages.length - 1]
		const route = page.route
		if (route === 'pages/index/index') {
			currentTab.value = 0
		} else if (route === 'pages/wgbtop/wgbtop') {
			currentTab.value = 1
		} else if (route === 'pages/user/user') {
			currentTab.value = 2
		}
	})
</script>

<style lang="scss" scoped>
	.container {
		// padding: 24rpx;
		padding: 20rpx;
		box-sizing: border-box;
		background-color: #f8f8f8;
		min-height: 100vh;
	}

	.black-screen-overlay {
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background-color: black;
		color: white;
		display: flex;
		justify-content: center;
		align-items: center;
		z-index: 9999;
	}

	.upload-area {
		height: 1000rpx;
		// background-color: #fff;
		border-radius: 16rpx;
		margin-bottom: 32rpx;
		display: flex;
		align-items: center;
		justify-content: center;
		// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
	}

	.button-group {
		display: flex;
		justify-content: space-between;
		margin-bottom: 32rpx;

		.action-button {
			flex: 1;
			height: 80rpx;
			font-size: 28rpx;
			background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
			border: none;
			color: #333;
			box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);

			&:active {
				opacity: 0.9;
			}

			&.share-button {
				margin-left: 24rpx;
				background: linear-gradient(135deg, #3c9cff 0%, #2b85e4 100%);
				color: #fff;
				
				&.u-button--disabled {
					opacity: 0.6;
				}
			}
		}
	}

	.settings-section {
		background-color: #fff;
		border-radius: 16rpx;
		overflow: hidden;
		box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);

		:deep(.u-cell) {
			padding: 28rpx 32rpx;
		}

		:deep(.u-cell_title) {
			font-size: 30rpx;
			color: #333;
			font-weight: 500;
		}
	}
</style>
1.上传图片到uni云存储空间

首先上传图片用两个地方,一个是上传组件,一个是点击上传的按钮,所以我写了两个up-upload组件,一个是显示的,一个是隐藏的,隐藏的组件用于实现按钮点击上传,使用uploadRef.value?.chooseFile();来手动触发;我使用的是uni的云存储方法为uniCloud.uploadFile(),需要这个uniapp账号开通了云存储空间,可以免费开通看不懂的话可以点击这段去uni的云存储板块看教程

2.上传图片需要做违规检测

第二点就是上传时需要做违规检测,如果用户上传了色情恐怖等等就不让检测上传分享了,这一块微信有提供检测的api————api.weixin.qq.com/wxa/img_sec… ;检测的api有两个一个只需要token就可以了;我使用的就是这个。另一个需要用户的openid,由于我没有做登录所以要openid的我就没有使用,这一块主要是获取token去调用这个检测接口,我使用的是uni的云函数代码如下

exports.main = async (event, context) => {
  // 获取微信access_token
  const getAccessToken = async () => {
    const res = await uniCloud.httpclient.request(
      'https://api.weixin.qq.com/cgi-bin/token', 
      {
        method: 'GET',
        data: {
          grant_type: 'client_credential',
          appid: 替换为你的小程序AppID
          secret:替换为你的小程序AppSecret
        },
        dataType: 'json'
      }
    )
    return res.data.access_token
  }

  try {
    const access_token = await getAccessToken()
    
    // 阿里云不支持downloadFile,直接从event中获取文件内容
    const fileContent = event.fileContent
    
    // 调用微信安全检测接口
    const result = await uniCloud.httpclient.request(
      `https://api.weixin.qq.com/wxa/img_sec_check?access_token=${access_token}`,
      {
        method: 'POST',
        content: fileContent,
        headers: {
          'Content-Type': 'application/octet-stream'
        },
        dataType: 'json'
      }
    )
    
    if (result.data.errcode === 0) {
      return {
        code: 0,
        message: '检测成功',
        data: result.data
      }
    } else {
      return {
        code: result.data.errcode || -1,
        message: result.data.errmsg || '检测失败',
        data: result.data
      }
    }
  } catch (error) {
    return {
      code: -2,
      message: error.message || '检测异常',
      data: error
    }
  }
}
3.闪照需要分享出去,微信分享功能

微信的分享功能这一块没有啥好说的很简单,给按钮加上open-type="share",然后吧分享功能打开通过onShareAppMessage方法就可以分享了 主要代码如下

<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />


wx.showShareMenu({
			menus: ['shareAppMessage', 'shareTimeline'],
			success() {
				console.log('分享功能已启用')
			}
		})
                
                onShareAppMessage(() => {
		if (!canShare.value || !fileID.value) {
			uni.showToast({
				title: '请先上传图片',
				icon: 'none'
			});
			return {};
		}
		return {
			title: '查看闪照',
			path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享径
			imageUrl: '/static/sz.png', // 分享图片
			success(res) {
				uni.showToast({
					title: '分享成功'
				})
			},
			fail(err) {
				console.log('分享失败', err)
			}
		}
	})
4.查看闪照时需要限时查看和防止手机截屏

第四点主要是通过css模糊效果结合定时器来实现;判断是否看过的字段我存储在了本地存储中,防君子不防小人。防截屏使用的是微信提供的wx.setVisualEffectOnCapture方法;具体代码如下

<template>
	<view class="image-container">
		<!-- 使用两层图片结构,一层模糊层,一层清晰层 -->
		<image v-if="isBlurred" :src="imageSrc" mode="widthFix" class="blur-layer" 
			@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
		<image :src="imageSrc" mode="widthFix" :class="['sharp-layer', { 'visible': !isBlurred }]" 
			@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
		<view v-if="hasViewed" class="hint-text">
			<up-button :plain="true" class="" style="margin-top: 40rpx;width: 180rpx;" size='mini'
				@click="toIndex">我也要发照片</up-button>
		</view>
		<up-modal :show="show" :title="title" :content='content' @confirm="confirm" :closeOnClickOverlay="true"
			showCancelButton='true' @cancel='cancel'></up-modal>
		<view v-if="showBlackScreen" class="black-screen">
			<text class="hint-text">禁止截屏</text>
		</view>
	</view>
</template>

<script setup>
	import {
		ref
	} from 'vue'
	import {
		onLoad,
		onShow,
		onNavigationBarButtonTap,
		onPullDownRefresh,
		onUnload,
		onHide
	} from '@dcloudio/uni-app';
	import {
		onUnmounted
	} from 'vue';
	const imageSrc = ref(
		'https://mp-57911374-353d-4222-b8c2-1a8948d61be7.cdn.bspapp.com/cloudstorage/4e16e15d-6660-4c24-af36-d6886d1e3a7e.'
	)
	const isBlurred = ref(true)
	const hasViewed = ref(false) // 是否已经查看过
	let timer = null
	const show = ref(false);
	const title = ref('提示');
	const content = ref('您已经查看过该图片');
	const imgArray = ref([])
	
	onLoad((options) => {
		if (uni.getStorageSync('imgArray')) {
			imgArray.value = uni.getStorageSync('imgArray')
		}
		if (options) {
			imageSrc.value = options.fileID
			const isExist = imgArray.value.some(item => item === imageSrc.value);
			if (isExist) {
				hasViewed.value = true
				isBlurred.value = true // 修改这里:已经查看过的图片保持模糊状态
			} else {
				hasViewed.value = false
				isBlurred.value = true
			}
		}
		wx.setVisualEffectOnCapture({
			visualEffect: 'hidden',
		});
	})
	
	onHide(() => {
		wx.setVisualEffectOnCapture({
			visualEffect: 'none',
		});
	})
	
	onUnload(() => {
		wx.setVisualEffectOnCapture({
			visualEffect: 'none',
		});
	})
	
	const handleTouchStart = () => {
		// 已经查看过,直接显示提示
		if (hasViewed.value) {
			show.value = true
			return
		}
		
		// 清除之前的定时器
		clearTimeout(timer)
		// 立即显示清晰图片
		isBlurred.value = false
		
		// 设置2秒后自动恢复模糊
		timer = setTimeout(() => {
			isBlurred.value = true
			hasViewed.value = true // 标记为已查看
			imgArray.value.push(imageSrc.value)
			uni.setStorageSync('imgArray', imgArray.value); //存本地
		}, 2000)
	}
	
	const handleTouchEnd = () => {
		// 已经查看过的不处理
		if (hasViewed.value) return
		// 如果触摸时间不足2秒就松手,也恢复模糊并标记为已查看
		clearTimeout(timer)
		isBlurred.value = true
		hasViewed.value = true
		imgArray.value.push(imageSrc.value)
		uni.setStorageSync('imgArray', imgArray.value);
	}
	
	// 去看广告
	const confirm = () => {
		show.value = false
	};
	// 不看
	const cancel = () => {
		show.value = false
	};
	
	onShow(() => {
		wx.setVisualEffectOnCapture({
			visualEffect: 'hidden',
		});
	})
	
	const toIndex = () => {
		uni.switchTab({
			url: '/pages/index/index'
		})
	}
	
	onUnmounted(() => {
		clearTimeout(timer)
	});
</script>

<style scoped>
	/* 容器确保图片比例不变形 */
	.image-container {
		width: 100%;
		height: 80vh;
		display: flex;
		align-items: center;
		justify-content: center;
		position: relative;
	}

	.black-screen {
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background-color: #000;
		z-index: 9999;
		display: flex;
		align-items: center;
		justify-content: center;
	}

	/* 模糊层 */
	.blur-layer {
		width: 100%;
		display: block;
		position: absolute;
		filter: blur(22px);
		transform: scale(1.02);
		transition: opacity 0.5s ease;
	}

	/* 清晰层 */
	.sharp-layer {
		width: 100%;
		display: block;
		position: absolute;
		opacity: 0;
		transition: opacity 0.5s ease;
	}

	.sharp-layer.visible {
		opacity: 1;
	}

	.hint-text {
		position: absolute;
		bottom: 50%;
		left: 0;
		right: 0;
		text-align: center;
		color: white;
		padding: 10rpx 20rpx;
		border-radius: 10rpx;
		margin: 0 auto;
		width: max-content;
		z-index: 10;
	}

	/* 性能优化 */
	@media (prefers-reduced-motion: reduce) {
		.blur-layer, .sharp-layer {
			transition: none;
		}
	}
</style>

到这里整个功能就已经实现完了,主要两个页面一个是上传图片和分享的页面;一个是查看闪照的页面。整篇文章都是干货无划水;喜欢的朋友可以点赞收藏一下,感谢了。下一篇我会分享纯前端实现的情侣互动点餐小程序。