【企业微信开发】jssdk实现h5图片上传

原文再续上一集

企业微信wecom/jssdk的使用(入门)

感谢各位的点赞和收藏,本文继续讲解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;
        }
    }
相关推荐
hj104311 天前
企业微信wecom/jssdk的使用(入门)
uni-app·企业微信
企销客CRM11 天前
CRM管理系统的用户权限设置与管理技巧:构建安全高效的数字化运营体系
大数据·数据库·人工智能·数据分析·企业微信
挨踢诗人14 天前
Python实现企业微信Token自动获取到SQLite存储
python·sqlite·企业微信·数据集成
企销客CRM14 天前
企微CRM系统中的任务分配与效率提升技巧
大数据·数据库·人工智能·数据分析·企业微信
鼠鼠我捏,要死了捏14 天前
Java开发企业微信会话存档功能笔记小结(企业内部开发角度)
java·企业微信·会话存档
企销客CRM14 天前
SCRM软件数据分析功能使用指南:从数据挖掘到商业决策
数据库·人工智能·数据挖掘·数据分析·企业微信
企销客CRM20 天前
实施企业预算管理的企微CRM系统技巧:从成本控制到价值创造
大数据·数据库·人工智能·企业微信
九亿少女的梦@23 天前
企业微信对接:回调地址带#时返回地址参数位置不对的问题
vue·企业微信·企微单点登录
焚天&无夜1 个月前
企微获取会话内容,RSA 解密函数
java·企业微信·rsa解密·会话内容·pkcs1解密算法