原文再续上一集
感谢各位的点赞和收藏,本文继续讲解h5项目中,使用企业微信的jssdk实现图片的上传。效果如下:

惯例先看官方文档:
https://developer.work.weixin.qq.com/document/path/100608
要实现图片的上传、预览、下载,需要用到一下几个api:
必须:chooseImage、 getLocalImgData
可选:previewImage、uploadImage

关键点在与,返回值localids的使用,安卓和苹果的差异见文档说明,因此我们能拿到的图片格式是base64格式的,因为上传到自己的服务器,要有对应的接收方法。
前端代码
<template>
<view class="report-container">
<!-- 头部导航 -->
<view class="header">
<text class="title">违规停车上报</text>
</view>
<!-- 表单区域 -->
<view class="form-container">
<!-- 车牌号码 -->
<view class="form-item">
<text class="label">车牌号码</text>
<view class="plate-input">
<view class="plate-province">
<picker :range="provinces" @change="onProvinceChange">
<text>{{ formdata.province }}</text>
</picker>
</view>
<view class="plate-number">
<input v-model="formdata.plateNumber" maxlength="6" placeholder="请输入车牌号码" />
</view>
</view>
</view>
<!-- 违规类型 -->
<view class="form-item">
<text class="label">违规类型</text>
<picker :range="violationTypes" @change="onViolationTypeChange">
<text>{{ formdata.reason || '请选择违规类型' }}</text>
</picker>
</view>
<!-- 停放位置 -->
<view class="form-item">
<text class="label">停放位置</text>
<picker :range="areas" @change="onAreasChange">
<text>{{ formdata.position || '请选择停放位置' }}</text>
</picker>
</view>
<!-- 处分方式 -->
<view class="form-item">
<text class="label">处分方式</text>
<picker :range="punishment" @change="onPunishmentChange">
<text>{{ formdata.punishment || '请选择处分方式' }}</text>
</picker>
</view>
<!-- 上传图片 -->
<view class="form-item">
<text class="label">上传图片</text>
<view class="image-upload-container">
<view class="upload-btn" @click="chooseImage">
<uni-icons type="camera-filled" size="30" color="#999" />
<text class="upload-text">拍照/相册</text>
</view>
<view class="image-list">
<view class="image-item" v-for="(image, index) in images" :key="index">
<image :src="image" mode="aspectFill" @click="previewImage(index)" />
<view class="delete-btn" @click="deleteImage(index)">
<uni-icons type="close" size="20" color="#fff" />
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-btn-container">
<button class="submit-btn" @click="submitReport" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交上报' }}
</button>
</view>
<!-- 底部导航(使用uni-ui图标) -->
<CustomTabBar :tabList="tabList" :activeColor="'#409EFF'" :normalColor="'#666'" />
</view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import CustomTabBar from '@/components/CustomTabBar.vue'
import { useRoute, useRouter } from 'vue-router'
// 底部导航配置
const tabList = ref([
{
name: 'record',
path: '/pages/ffep/car/car',
text: '过夜登记',
icon: 'checkbox',
activeIcon: 'checkbox-filled'
},
{
name: 'violation',
path: 'parkingViolation',
text: '违规登记',
icon: 'close',
activeIcon: 'clear'
},
{
name: 'statistics',
path: '/pages/ffep/car/statistics',
text: '统计',
icon: 'info',
activeIcon: 'info-filled'
}
])
// 微信JSSDK相关
import * as ww from '@wecom/jssdk'
import request from '../../../utils/http2';
// 微信相关配置
let wwconfig = reactive({})
// 车牌省份简称
const provinces = ref([
'京', '津', '冀', '晋', '蒙', '辽', '吉', '黑',
'沪', '苏', '浙', '皖', '闽', '赣', '鲁', '豫',
'鄂', '湘', '粤', '桂', '琼', '渝', '川', '贵',
'云', '藏', '陕', '甘', '青', '宁', '新'
]);
// 违规类型
const violationTypes = ref(['占用多位', '在禁停区域停放']);
// 区域
const areas = ref(['名店街东区', '北广场', '西区', '地库']);
// 处分方式
const punishment = ref(['警告', '收取违规管理费50元', '收取违规管理费100元']);
const images = ref<string[]>([]);
const locationInfo = ref('');
const isSubmitting = ref(false);
// 表单数据
let formdata = reactive({
province: '粤',
plateNumber: '',
position: '',
violationTime: '',
reason: '',
punishment: '',
pic: '',
sms: '',
tel: '',
})
// 路由
const router = useRouter();
// 页面加载时初始化
onMounted(() => {
initWecomJssdk();
});
// 初始化企业微信JS-SDK
async function initWecomJssdk() {
try {
console.log('wecom/jssdk', ww.SDK_VERSION)
// 检查 ww 对象是否存在
if (typeof ww === 'undefined') {
throw new Error('企业微信JS-SDK未加载');
}
// 获取当前页面URL(不含hash)
// const currentUrl = window.location.href.split('#')[0];
const currentUrl = "https://datalab.ffep.online:20093/web/pages/ffep/car/parkingViolation";
// 请求后端获取JS-SDK配置
const res = await request({
url: '/api/ffep/Wecom/wwregister',
method: 'POST',
data: { url: currentUrl },
requiresAuth: false
});
if (res.code === 1) {
// 注册企业微信配置
ww.register({
corpId: res.data.corpId,
agentId: res.data.agentId,
jsApiList: res.data.jsApiList,
// 企业级签名回调函数
// getConfigSignature: () => res.data.configSignature,
// 应用级签名回调函数
// getAgentConfigSignature: () => res.data.agentConfigSignature,
// 企业级签名回调函数
getConfigSignature: () => ww.getSignature(res.data.tk1),
// 应用级签名回调函数
getAgentConfigSignature: () => ww.getSignature(res.data.tk2)
});
}
} catch (error) {
console.error('初始化企业微信JS-SDK异常', error);
uni.showToast({ title: error.message, icon: 'none' });
}
}
// 选择图片
const chooseImage = () => {
if (images.value.length >= 5) {
uni.showToast({
title: '最多上传5张图片',
icon: 'none'
});
return;
}
ww.chooseImage({
count: 5 - images.value.length, // 最多可以选择的图片张数
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: (res : any) => {
// 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
const localIds = res.localIds;
localIds.forEach((localId : string) => {
// 上传图片到微信服务器并获取服务器ID
// uploadImageToWechat(localId);
// 通过 getLocalImgData 获取图片 base64 数据,再上传到自己的服务器
ww.getLocalImgData({
localId: localId,
success:(res=>{
// 将serverId保存到images数组
images.value.push(res.localData);
// 调用后端接口将图片的 base64 数据上传到自己的服务器
uploadImageToServer(res.localData);
}),
fail:(err)=>{
console.log(err);
}
})
});
},
fail: (err : any) => {
console.error('选择图片失败', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
};
// 上传图片到微信服务器
const uploadImageToWechat = (localId : string) => {
ww.uploadImage({
localId: localId, // 需要上传的图片的本地ID,由chooseImage接口获得
isShowProgressTips: true, // 显示进度提示
success: (res : any) => {
// 返回图片的服务器端ID
const serverId = res.serverId;
// 获取本地图片
},
fail: (err : any) => {
console.error('上传图片到微信服务器失败', err);
uni.showToast({
title: '上传图片失败',
icon: 'none'
});
}
});
};
// 上传图片到自己的服务器
const uploadImageToServer = (data:any) => {
// 发送请求
request({
url: '/api/ffep/parking/upload_base64',
method: 'POST',
data: {
base64_img: data
},
requiresAuth:false
}).then(res => {
if (res.code === 1) {
uni.showToast({
title: res.msg,
icon: 'success',
duration: 1500
});
} else {
// 处理具体错误
let errorMsg = res.msg || '图片上传失败';
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2500
});
}
})
};
// 删除图片
const deleteImage = (index : number) => {
images.value.splice(index, 1);
};
// 预览图片
const previewImage = (index : number) => {
uni.previewImage({
current: images.value[index],
urls: images.value
});
};
// 省份选择变更
const onProvinceChange = (e : any) => {
formdata.province = provinces.value[e.detail.value];
};
// 违规类型选择变更
const onViolationTypeChange = (e : any) => {
formdata.reason = violationTypes.value[e.detail.value];
};
// 停放位置选择变更
const onAreasChange = (e : any) => {
formdata.position = areas.value[e.detail.value];
};
// 处分方式选择变更
const onPunishmentChange = (e : any) => {
formdata.punishment = punishment.value[e.detail.value];
};
// 提交表单
const submitReport = () => {
// 表单验证
if (!formdata.plateNumber.trim()) {
uni.showToast({
title: '请输入车牌号码',
icon: 'none'
});
return;
}
if (!formdata.reason) {
uni.showToast({
title: '请选择违规类型',
icon: 'none'
});
return;
}
if (!formdata.position) {
uni.showToast({
title: '请选择停放位置',
icon: 'none'
});
return;
}
if (!formdata.punishment) {
uni.showToast({
title: '请选择处分方式',
icon: 'none'
});
return;
}
if (images.value.length === 0) {
uni.showToast({
title: '请上传违规图片',
icon: 'none'
});
// return;
}
// 提交数据
// isSubmitting.value = true;
request({
url: '/api/ffep/parking/add',
method: 'POST',
data: formdata,
}).then(res => {
if (res.code) {
uni.showToast({
title: '上报成功',
icon: 'success'
});
uni.showModal({
title: res.msg,
content: "是否发送违规停车短信?",
success: function (r) {
if (r.confirm) {
sendsms(res.data.id)
} else if (r.cancel) {
}
}
});
// 延迟后返回上一页
setTimeout(() => {
// router.back();
}, 1500);
}
});
}
// 发送违规停车短信
const sendsms = async (id : any) => {
const res = await request({
url: '/api/ffep/parking/sendsms',
method: 'POST',
data: id,
})
if (res.code) {
uni.showToast({
title: res.msg,
icon: 'success'
});
} else {
uni.showToast({
title: res.msg,
icon: 'error'
});
}
}
</script>
<style lang="scss" scoped>
.report-container {
background-color: #f5f5f5;
min-height: 100vh;
/* 添加底部边距,避免内容被底部导航栏遮挡 */
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
}
.header {
height: 100rpx;
background-color: #0f80ff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.title {
font-size: 36rpx;
font-weight: 500;
}
}
.form-container {
padding: 24rpx;
}
.form-item {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 24rpx;
padding: 24rpx;
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
input,
textarea {
width: 100%;
font-size: 28rpx;
color: #666;
padding: 12rpx 0;
}
textarea {
min-height: 120rpx;
}
picker {
display: block;
width: 100%;
padding: 12rpx 0;
font-size: 28rpx;
color: #666;
}
}
.plate-input {
display: flex;
.plate-province {
width: 120rpx;
margin-right: 20rpx;
border: 1rpx solid #e5e6eb;
border-radius: 8rpx;
text-align: center;
padding: 12rpx 0;
picker {
padding: 0;
}
}
.plate-number {
flex: 1;
border: 1rpx solid #e5e6eb;
border-radius: 8rpx;
padding: 0 16rpx;
}
}
.image-upload-container {
.upload-btn {
width: 160rpx;
height: 160rpx;
border: 2rpx dashed #e5e6eb;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
background-color: #fafbfc;
.upload-text {
margin-top: 8rpx;
font-size: 24rpx;
}
}
.image-list {
display: flex;
flex-wrap: wrap;
margin-top: 20rpx;
.image-item {
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
margin-bottom: 20rpx;
position: relative;
image {
width: 100%;
height: 100%;
border-radius: 12rpx;
object-fit: cover;
}
.delete-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 36rpx;
height: 36rpx;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.location-item {
.location-info {
display: flex;
align-items: center;
color: #0f80ff;
.location-text {
margin-left: 12rpx;
}
}
}
.submit-btn-container {
padding: 40rpx 24rpx;
.submit-btn {
width: 100%;
height: 96rpx;
background-color: #0f80ff;
color: #fff;
font-size: 32rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:disabled {
background-color: #c9cdcf;
}
}
}
</style>
附上我的后端写法
/**
* base64上传文件
*/
public function upload_base64() {
if($this->request->isPost()) {
$param = $this->request->param();
$validate = new \think\Validate([
'base64_img' => 'require',
]);
$validate->message([
'base64_img.require' => '缺少图片base64数据!',
]);
if (!$validate->check($param)) {
$this->error($validate->getError());
}
$path = '/uploads';
$attachment = $this->base64_image_content($param['base64_img'],$path);
if(!$attachment) {
$this->error('上传失败');
}
$this->success(__('Uploaded successful'), ['url' => $attachment['url'], 'fullurl' => cdnurl($attachment['url'], true)]);
}
}
/**
* [将Base64图片转换为本地图片并保存]
* @param [Base64] $base64_image_content [要保存的Base64]
* @param [目录] $path [要保存的路径]
*/
private function base64_image_content($base64_image_content,$path){
//匹配出图片的格式
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_image_content, $result)){
$file_type = $result[2]; // 获取图片类型
$file_path = date('Ymd',time())."/";
$new_file = '.'.$path."/".$file_path;
if(!file_exists($new_file)){
//检查是否有该文件夹,如果没有就创建,并给予最高权限
mkdir($new_file, 0777,true);
}
// 根据图片类型设置正确的扩展名
$ext = strtolower($file_type);
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
// 默认使用jpg,但最好记录日志以便后续处理
$ext = 'jpg';
}
$new_file = $new_file.time().".".$ext;
// 保存图片数据
$res = file_put_contents($new_file, base64_decode(str_replace($result[1], '', $base64_image_content)));
if ($res){
// 根据图片类型使用正确的GD函数
switch ($ext) {
case 'jpg':
case 'jpeg':
$img = imagecreatefromjpeg($new_file);
$mimetype = 'image/jpeg';
break;
case 'png':
$img = imagecreatefrompng($new_file);
$mimetype = 'image/png';
break;
case 'gif':
$img = imagecreatefromgif($new_file);
$mimetype = 'image/gif';
break;
default:
// 对于不支持的类型,尝试使用jpeg函数
$img = @imagecreatefromjpeg($new_file);
$mimetype = 'image/jpeg';
break;
}
if (!$img) {
// 创建图像失败,可能是文件损坏或不支持的格式
@unlink($new_file); // 删除损坏的文件
return false;
}
$params = array(
'admin_id' => (int)session('admin.id'),
'user_id' => (int)cookie('uid'),
'filename' => time().".".$ext,
'filesize' => filesize($new_file),
'imagewidth' => imagesx($img),
'imageheight' => imagesy($img),
'imagetype' => $ext,
'imageframes' => 0,
'mimetype' => $mimetype,
'url' => $path."/".$file_path.time().".".$ext,
'uploadtime' => time(),
'storage' => 'local',
'sha1' => hash_file('sha1', $new_file),
'extparam' => '',
);
return $params;
}else{
return false;
}
}else{
return false;
}
}