微信小程序的图片/视频上传功能,小程序官网是提供了相关的API语法。本例使用了 wx.chooseMedia 选择或拍摄图片/视频附件,通过 wx.uploadFile 方法上传至服务器,在需要的地方将服务器存储的附件地址查询出来提供展示预览。预览主要实现了图片的手势缩放及托动,视频的可全屏播放等功能。
本例主要使用了taro及taro ui 组件开发,与小程序官方语言基本兼容,定义了附件的上传及预览taro组件,具体代码如下:
1.部分样式代码(customAnnex.styl):
.at-row {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
width: 100%; }
.at-row__direction--row {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row; }
.at-row__direction--column {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column; }
.at-row__direction--row-reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-webkit-flex-direction: row-reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse; }
.at-row__direction--column-reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-webkit-flex-direction: column-reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse; }
.at-row__align--start {
-webkit-align-items: flex-start;
-ms-flex-align: start;
align-items: flex-start;
-webkit-box-align: start; }
.at-row__align--end {
-webkit-align-items: flex-end;
-ms-flex-align: end;
align-items: flex-end;
-webkit-box-align: end; }
.at-row__align--center {
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-align: center; }
.at-row__align--stretch {
-webkit-align-items: stretch;
-ms-flex-align: stretch;
align-items: stretch;
-webkit-box-align: stretch; }
.at-row__align--baseline {
-webkit-align-items: baseline;
-ms-flex-align: baseline;
align-items: baseline;
-webkit-box-align: baseline; }
.at-row__justify--start {
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-pack: start; }
.at-row__justify--end {
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
-webkit-box-pack: end; }
.at-row__justify--center {
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-pack: center; }
.at-row__justify--between {
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-pack: justify; }
.at-row__justify--around {
-webkit-justify-content: space-around;
-ms-flex-pack: distribute;
justify-content: space-around;
-webkit-box-pack: space-around; }
.at-row__align-content--start {
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start; }
.at-row__align-content--end {
-webkit-align-content: flex-end;
-ms-flex-line-pack: end;
align-content: flex-end; }
.at-row__align-content--center {
-webkit-align-content: center;
-ms-flex-line-pack: center;
align-content: center; }
.at-row__align-content--between {
-webkit-align-content: space-between;
-ms-flex-line-pack: justify;
align-content: space-between; }
.at-row__align-content--around {
-webkit-align-content: space-around;
-ms-flex-line-pack: distribute;
align-content: space-around; }
.at-row__align-content--stretch {
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch; }
.at-row--no-wrap {
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap; }
.at-row--wrap {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap; }
.at-row--wrap-reverse {
-webkit-flex-wrap: wrap-reverse;
-ms-flex-wrap: wrap-reverse;
flex-wrap: wrap-reverse; }
.at-col {
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-box-flex: 1;
width: 100%;
display: block;
white-space: nowrap;
-webkit-box-sizing: border-box;
box-sizing: border-box; }
.at-col-1 {
-webkit-flex: 0 0 8.33333%;
-ms-flex: 0 0 8.33333%;
flex: 0 0 8.33333%;
-webkit-box-flex: 0;
max-width: 8.33333%; }
.at-col__offset-1 {
margin-left: 8.33333%; }
.at-col-2 {
-webkit-flex: 0 0 16.66667%;
-ms-flex: 0 0 16.66667%;
flex: 0 0 16.66667%;
-webkit-box-flex: 0;
max-width: 16.66667%; }
.at-col__offset-2 {
margin-left: 16.66667%; }
.at-col-3 {
-webkit-flex: 0 0 25%;
-ms-flex: 0 0 25%;
flex: 0 0 25%;
-webkit-box-flex: 0;
max-width: 25%; }
.at-col__offset-3 {
margin-left: 25%; }
.at-col-4 {
-webkit-flex: 0 0 33.33333%;
-ms-flex: 0 0 33.33333%;
flex: 0 0 33.33333%;
-webkit-box-flex: 0;
max-width: 33.33333%; }
.at-col__offset-4 {
margin-left: 33.33333%; }
.at-col-5 {
-webkit-flex: 0 0 41.66667%;
-ms-flex: 0 0 41.66667%;
flex: 0 0 41.66667%;
-webkit-box-flex: 0;
max-width: 41.66667%; }
.at-col__offset-5 {
margin-left: 41.66667%; }
.at-col-6 {
-webkit-flex: 0 0 50%;
-ms-flex: 0 0 50%;
flex: 0 0 50%;
-webkit-box-flex: 0;
max-width: 50%; }
.at-col__offset-6 {
margin-left: 50%; }
.at-col-7 {
-webkit-flex: 0 0 58.33333%;
-ms-flex: 0 0 58.33333%;
flex: 0 0 58.33333%;
-webkit-box-flex: 0;
max-width: 58.33333%; }
.at-col__offset-7 {
margin-left: 58.33333%; }
.at-col-8 {
-webkit-flex: 0 0 66.66667%;
-ms-flex: 0 0 66.66667%;
flex: 0 0 66.66667%;
-webkit-box-flex: 0;
max-width: 66.66667%; }
.at-col__offset-8 {
margin-left: 66.66667%; }
.at-col-9 {
-webkit-flex: 0 0 75%;
-ms-flex: 0 0 75%;
flex: 0 0 75%;
-webkit-box-flex: 0;
max-width: 75%; }
.at-col__offset-9 {
margin-left: 75%; }
.at-col-10 {
-webkit-flex: 0 0 83.33333%;
-ms-flex: 0 0 83.33333%;
flex: 0 0 83.33333%;
-webkit-box-flex: 0;
max-width: 83.33333%; }
.at-col__offset-10 {
margin-left: 83.33333%; }
.at-col-11 {
-webkit-flex: 0 0 91.66667%;
-ms-flex: 0 0 91.66667%;
flex: 0 0 91.66667%;
-webkit-box-flex: 0;
max-width: 91.66667%; }
.at-col__offset-11 {
margin-left: 91.66667%; }
.at-col-12 {
-webkit-flex: 0 0 100%;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
-webkit-box-flex: 0;
max-width: 100%; }
.at-col__offset-12 {
margin-left: 100%; }
.at-col__align--top {
-webkit-align-self: flex-start;
-ms-flex-item-align: start;
align-self: flex-start; }
.at-col__align--bottom {
-webkit-align-self: flex-end;
-ms-flex-item-align: end;
align-self: flex-end; }
.at-col__align--center {
-webkit-align-self: center;
-ms-flex-item-align: center;
align-self: center; }
.at-col--auto {
max-width: initial;
word-break: keep-all; }
.at-col--wrap {
white-space: normal;
word-wrap: break-word; }
.annex {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
visibility: hidden;
-webkit-transition: visibility 200ms ease-in;
-o-transition: visibility 200ms ease-in;
transition: visibility 200ms ease-in;
z-index: 1000;
}
.annex--active {
visibility: visible;
}
.annex--active .annex__overlay,
.annex--active .annex__container {
opacity: 1;
}
.annex__overlay {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
}
.annex__container {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 600rpx;
max-height: calc(100vh - 300rpx);
overflow: hidden;
border-radius: 12rpx;
}
.annex__overlay, .annex__container {
opacity: 0;
-webkit-transition: opacity 200ms ease-in;
-o-transition: opacity 200ms ease-in;
transition: opacity 200ms ease-in;
}
.at-relative {
position: relative;
}
.at-col-class {
width: 222rpx !important;
height: 222rpx !important;
margin: 5rpx;
text-align: center;
border: 2rpx #d6e4ef solid;
position: relative;
display: block;
white-space: nowrap;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.at-col-img {
width: 218rpx !important;
height: 218rpx !important;
}
.at-col-close {
width: 36rpx;
height: 36rpx;
position: absolute;
top: 2rpx;
right: 2rpx;
border-radius: 100%;
background-color: #ffffff;
z-index: 99;
}
.at-col-add-img {
width:47rpx;
height:47rpx;
margin: 69rpx 85rpx auto 85rpx;
}
.at-col-add-text {
margin-top: 30rpx;
color: #999999;
}
.resultFont {
font-size:32rpx;
margin-top:30rpx;
}
.count_color {
color: #CCCCCC;
}
.hide {
display: none !important;
}
.show {
display: block;
}
.w100 {
width: 100%;
}
2.部分逻辑代码(customAnnex.tsx):
import Taro, { Config, Component } from '@tarojs/taro'
import './customAnnex.styl'
import {View, Image, Video} from '@tarojs/components'
import annexAdd from '../../../asset/images/index/annex-add.png'
import annexClose from '../../../asset/images/index/annex-close.png'
import blankImg from '../../../asset/images/index/blank.png'
import TaroReq from '../../../constants/tokenHandle.js'
import api from "../../../constants/api.json"
export default class customAnnex extends Component {
/**自定义的组件
* 指定config的类型声明为: Taro.Config
*
* 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
* 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
* 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
*/
/**
* *************重要API*************
* 作用:附件的上传
* 其余的需要的时候再扩展
* 使用:1、导入组件:import Cannex from '../../components/custom-annex/customAnnex'
* 2、使用组件:
*
*/
config: Config = { };
maxNum: number = 3; // 最大附件上传数量
maxWidth: number = 300; // 最大预览宽度
constructor(props) {
super(props);
this.state = {
annexFiles: [], // 附件信息列表
addShow: true, // 是否展示添加设备
isOpened: false, // 展示的附件区域
annexShow: null, // 展示的附件内容
touch: {
distance: 0,
scale: 1,
baseWidth: null,
baseHeight: null,
scaleWidth: null,
scaleHeight: null,
startX: null,
startY: null,
moveTop: null,
moveLeft: null,
previousTwoFinger: false //此变量防止双指离开时,后离开的手指会触发图片移动
}
};
}
componentDidMount() {
try {
setTimeout(() => {
this.requstAnnex();
}, 500);
} catch (error) { }
}
//输入框赋值
touchSetHandle(valueMap: object) {
if (valueMap) {
for (let key in valueMap) {
this.state.touch[key]=valueMap[key];
}
this.setState({
touch: this.state.touch
});
}
}
// 结束
touchEndHandle() {
setTimeout(()=>{
this.touchSetHandle({
previousTwoFinger: false
})
}, 1000);
}
touchStartHandle(e) {
// 单手指缩放开始
if (e.touches.length == 1) {
if (!this.state.touch.previousTwoFinger){
let startX = e.touches[0].clientX;
let startY = e.touches[0].clientY;
this.touchSetHandle({ startX: startX, startY: startY });
}
} else {
// 注意touchstartCallback 真正代码的开始
// 当两根手指放上去的时候,就将distance 初始化。
let xMove = e.touches[1].clientX - e.touches[0].clientX;
let yMove = e.touches[1].clientY - e.touches[0].clientY;
let distance = Math.sqrt(xMove * xMove + yMove * yMove);
this.touchSetHandle({ distance: distance });
}
}
touchMoveHandle(e) {
let touch = this.state.touch;
// 单手指移动
if (e.touches.length == 1) {
// 一只手指触摸触发移动
if (!this.state.touch.previousTwoFinger){
let moveX = this.state.touch.moveLeft + e.touches[0].clientX - this.state.touch.startX;
let moveY = this.state.touch.moveTop + e.touches[0].clientY - this.state.touch.startY;
let deffWidth = this.state.touch.baseWidth - this.state.touch.scaleWidth;
let deffHeight = this.state.touch.baseHeight - this.state.touch.scaleHeight;
let calcFun = function (deff, move) {
if (deff >= 0 ) {
if (deff <= move) {
move = deff;
} else {
move = move > 0 ? move : 0;
}
} else {
if (move <= deff) {
move = deff;
} else {
move = move < 0 ? move : 0;
}
}
return move;
};
moveX = calcFun(deffWidth, moveX);
moveY = calcFun(deffHeight, moveY);
let startX = e.touches[0].clientX;
let startY = e.touches[0].clientY;
this.touchSetHandle({ moveLeft: moveX, moveTop: moveY, startX: startX, startY: startY });
}
} else {
let xMove = e.touches[1].clientX - e.touches[0].clientX;
let yMove = e.touches[1].clientY - e.touches[0].clientY;
// 新的 ditance
let distance = Math.sqrt(xMove * xMove + yMove * yMove);
let distanceDiff = distance - touch.distance;
let newScale = touch.scale + 0.005 * distanceDiff;
// 为了防止缩放得太大,所以scale需要限制,同理最小值也是
if (newScale >= 4) {
newScale = 4;
}
if (newScale <= 1) {
newScale = 1;
}
let scaleWidth = (newScale * touch.baseWidth).toFixed(2);
let scaleHeight = (newScale * touch.baseHeight).toFixed(2);
// 赋值 新的 => 旧的
this.touchSetHandle({
distance: distance,
scale: newScale,
scaleWidth: scaleWidth,
scaleHeight: scaleHeight,
diff: distanceDiff,
previousTwoFinger: true
});
}
}
imgLoad(e) {
// 这个api是组件的api类似的onload属性
let width = this.maxWidth; // 预览最大宽度
let height = (width / e.detail.width * e.detail.height).toFixed(2);
this.touchSetHandle({
baseWidth: width,
baseHeight: height,
scaleWidth: width,
scaleHeight: height,
moveTop: 0,
moveLeft: 0,
previousTwoFinger: false
});
}
/*控制附件新增按钮*/
setAddShow() {
if (this.state.annexFiles && this.state.annexFiles.length >= this.maxNum) {
this.setState({
addShow: false
});
} else {
this.setState({
addShow: true
});
}
}
/*添加附件*/
addAnnex() {
let that = this;
wx.chooseMedia({
count: 1,
mediaType: ['image','video'],
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'],
maxDuration: 20,
camera: 'back',
success(res) {
if (res && res.tempFiles && res.tempFiles.length > 0) {
let annexFiles: any[] = [...that.state.annexFiles];
let image = res.type=="video" ? res.tempFiles[0].thumbTempFilePath : res.tempFiles[0].tempFilePath;
annexFiles.push({ type: res.type, path: res.tempFiles[0].tempFilePath, iconPath: image });
that.setState({
annexFiles: annexFiles
}, () => {
that.setAddShow();
});
}
},
fail() {
Taro.showToast({
title: '附件添加失败!',
icon: 'none',
duration: 3000
})
}
});
}
/*删除附件*/
delAnnex(index) {
let annexFiles: any[] = this.state.annexFiles.filter((val, i) => i != index);
this.setState({
annexFiles: annexFiles
}, () => {
this.setAddShow();
});
}
/*展示附件*/
clickAnnexShow(annex) {
if (annex && annex.type) {
this.setState({
annexShow: annex,
isOpened: true
});
}
}
/*控制展示*/
clickAnnexHandle(flag) {
this.setState({
annexShow: null,
isOpened: flag
});
}
/*请求附件*/
requstAnnex() {
const { isUpload, idName, idValue } = this.props;
if(isUpload) { return; }
this.setState({
addShow: false,
});
if(idName && idValue) {
let formData: object = {page: 1, rows: 100};
formData[idName] = idValue;
TaroReq.get(api.url + api.attachMentList, formData).then(res => {
if (res.statusCode == 200) {
const data = res.data.rows || [];
let annexFiles: any = [];
for (let index=0; index {
success++;
},
fail: (res) => {
fail++;
},
complete: () => {
i++;
if (i == data.path.length) { //当图片传完时,停止调用
Taro.hideLoading();
const tip = fail > 0 ? (success+'个附件上传成功,' + fail + '个附件上传失败!') : ('附件上传成功!');
Taro.showToast({
title: tip,
icon: 'none',
duration: 3000
});
if (data.callFun && typeof data.callFun == "function") {
data.callFun();
}
} else { //若图片还没有传完,则继续调用函数
data.i = i;
data.success = success;
data.fail = fail;
that.uploadMuliteFile(data);//递归,回调自己
}
}
});
}
/*上传附件*/
uploadAnnex(params) {
const annexFilePaths: any[] = [];
for (let i = 0; i < this.state.annexFiles.length; i++) {
annexFilePaths.push(this.state.annexFiles[i].path);
}
if (annexFilePaths && annexFilePaths.length > 0) {
Taro.showLoading({
title: '附件上传中...',
mask: true
});
let reqToken = Taro.getStorageSync('access_token_' + 'TM-WXapplet');
let reqHeader = { 'content-type': params.contentType || 'multipart/form-data', Authorization: 'Bearer ' + reqToken };
this.uploadMuliteFile({
header: reqHeader,
url: params.url || (api.url + api.attachMentUpload),
path: annexFilePaths,
formData: params.formData || null,
callFun: params.callFun || null
});
}
}
render() {
const { isUpload, idName, idValue } = this.props;
const { annexFiles, addShow, isOpened, annexShow, touch } = this.state;
return (
{isUpload
?
上传附件
{annexFiles.length}/{this.maxNum}
{annexFiles.map((annexFile, index) =>
)}
视频/图片
:
{annexFiles.map((annexFile) =>
)}
}
{(annexShow.type && annexShow.path)
?
{annexShow.type=="video"
?
:
}
:
}
)
}
}
3.其他组件引用代码:
上传的时候调用:
import Cannex from '../../components/custom-annex/customAnnex' // 引入组件
cannex: any;
refCannex = (node) => { this.cannex = node }; // 获取组件实例
cannex.uploadAnnex({ // 调用组件上传方法
formData: { 'xxx': 'xxx'},
callFun: function () { }
});
// 组件视图
上传的时候效果:
预览时候的调用:
import Cannex from '../../components/custom-annex/customAnnex' // 引入组件
// 预览加载组件
预览时候的效果:
注意:wx.chooseMedia在临时拍照上传时,文件可能为 .unknown 后缀,需要服务器端处理替换为.jpg处理。