阅读视图

发现新文章,点击刷新页面。

微信闪照小程序实现

已经有一年半没有写文章了,今天给掘友们写一个闪照实现的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>

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

❌