详情小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
13 KiB

5 years ago
5 years ago
5 years ago
  1. <template>
  2. <view>
  3. <view class="upload-images" :style="{'overflow-x': overOnePage ? 'hidden' : 'initial'}">
  4. <!-- 上传按钮 TODO 应该移到和图片同一列 -->
  5. <view class="upload-image-item"
  6. :style="{order: uploadButton == 'front' ? '1' : '2', height: '222rpx', 'margin-bottom': '6px'}"
  7. @click="uploadImage"
  8. v-if="(count == -1) || (image_list.length < count)">
  9. <text class="lf-iconfont lf-icon-jia upload-image-item-after"></text>
  10. </view>
  11. <!-- 图片列表 -->
  12. <view class="item-wrap" :style="{'height': itemWrapHeight +'px', order: uploadButton == 'front' ? '2' : '1'}">
  13. <view class="item"
  14. :class="{'cur': cur == index, 'zIndex': curZ == index, 'itemTransition': itemTransition}"
  15. v-for="(item, index) in list"
  16. :key="index"
  17. :id="'item'+index"
  18. :data-key="item.key"
  19. :data-index="index"
  20. :style="[itemStyle(index, item)]"
  21. @longpress="longPress"
  22. @touchmove.stop="touchMove"
  23. @touchend.stop="touchEnd">
  24. <view class="info">
  25. <view class="upload-image-item" @click="lookImage(index)">
  26. <image :src="item.data" mode="aspectFill"></image>
  27. <view class="remove-image" @click.stop="removeInage(index)" v-if="showDelete">
  28. <text class="lf-iconfont lf-icon-shanchu"></text>
  29. </view>
  30. </view>
  31. </view>
  32. </view>
  33. </view>
  34. </view>
  35. <view v-if="overOnePage" class="indicator">
  36. <view>滑动此区域滚动页面</view>
  37. </view>
  38. </view>
  39. </template>
  40. <script>
  41. export default {
  42. props: {
  43. uploadButton: {
  44. type: String, // 上传按钮显示在哪里,front图片前面,behind图片后面
  45. default: 'front'
  46. },
  47. showDelete: {
  48. type: Boolean, // 是否可删除图片
  49. default: true
  50. },
  51. count: {
  52. type: Number, // 可上传多少张图, 默认6张
  53. default: 6
  54. },
  55. size: {
  56. type: Number, // 限制单张图上传大小,单位M
  57. default: 10
  58. },
  59. drag: {
  60. type: Boolean,
  61. default: false
  62. },
  63. padding: {
  64. type: Number, // 图片与图片之间的间距,默认5像素,单位px
  65. default: 6
  66. },
  67. width: {
  68. type: String, // 每张图片的宽度,支持百分比,像素,bisection表示等分
  69. default: 'bisection'
  70. },
  71. height: {
  72. type: String, // 每张图片高度
  73. default: '222rpx'
  74. }
  75. },
  76. data(){
  77. return {
  78. touch: false,
  79. startX: 0,
  80. startY: 0,
  81. columns: 3,
  82. item: {},
  83. tranX: 0,
  84. tranConstX: 0,
  85. itemWrap: {},
  86. tranY: 0,
  87. teanConstY: 0,
  88. overOnePage: false,
  89. windowHeight: 0,
  90. originKey: null,
  91. list: [],
  92. cur: -1,
  93. curZ: -1,
  94. image_list: [],
  95. itemWrapHeight: 0
  96. }
  97. },
  98. computed: {
  99. itemStyle(){
  100. let that = this;
  101. return function(index, item){
  102. let style_obj = {
  103. height: that.$props.height
  104. }
  105. let width = that.$props.width;
  106. if(width == 'bisection'){
  107. width = `calc(${100 / that.columns +'%'} - 4px)`
  108. }
  109. style_obj.width = width;
  110. let str = 'translate3d(';
  111. if(index === that.cur){
  112. str += that.tranX +'px';
  113. str += ', ';
  114. str += that.tranY +'px';
  115. }else{
  116. str += item.tranX +'px';
  117. str += ', ';
  118. str += item.tranY +'px';
  119. }
  120. str += ', 0px)';
  121. style_obj.transform = str;
  122. return style_obj;
  123. }
  124. }
  125. },
  126. mounted(){
  127. // 'https://picsum.photos/200',
  128. // 'https://picsum.photos/200/300'
  129. this.init();
  130. },
  131. methods: {
  132. // 上传凭证图片
  133. uploadImage(){
  134. let current_count = this.$props.count - this.image_list.length;
  135. if(this.$props.count == -1){
  136. current_count = 9;
  137. }
  138. if(current_count == 0) return;
  139. uni.chooseImage({
  140. count: current_count,
  141. complete: result => {
  142. if(result.errMsg == "chooseImage:fail cancel"){
  143. return; // 取消选择图片
  144. }
  145. let tempFiles = result.tempFiles;
  146. let image_list = [];
  147. let overstep = false;
  148. tempFiles.map(item => {
  149. // 上传的图片大小
  150. let size = Math.floor((Number(this.$props.size) || 1) * 1024 * 1024);
  151. if(item.size < size){
  152. image_list.push(item.path);
  153. }else{
  154. overstep = true;
  155. }
  156. })
  157. this.image_list.push(...image_list);
  158. if(overstep){
  159. uni.showModal({
  160. title: '温馨提示',
  161. content: '您上传的图片含有超出10M大小的限制,请优化大小后再上传!',
  162. showCancel: false
  163. })
  164. }else{
  165. this.init(); // TODO 优化每次上传都会造成重新初始化导致闪烁
  166. }
  167. }
  168. })
  169. },
  170. // 预览图片
  171. lookImage(current){
  172. if(this.image_list.length <= 0) return;
  173. this.$u.throttle(() => {
  174. uni.previewImage({
  175. urls: this.image_list,
  176. current: current
  177. });
  178. }, 500);
  179. },
  180. // 移除图片
  181. removeInage(current){
  182. // 移除已上传的图片
  183. this.image_list.splice(current, 1);
  184. // 移除定位的元素(重新初始化)
  185. let list = this.image_list.map((item, index) => {
  186. return {
  187. key: index,
  188. tranX: 0,
  189. tranY: 0,
  190. data: item
  191. }
  192. }) || [];
  193. this.list = list;
  194. this.getPosition(list, false); // 重新定位
  195. // 重新计算大盒子高度
  196. let rows = Math.ceil(list.length / this.columns);
  197. let itemWrapHeight = rows * this.item.height;
  198. itemWrapHeight += rows * this.$props.padding - this.$props.padding;
  199. this.itemWrapHeight = itemWrapHeight;
  200. },
  201. // 返回已上传的图片列表
  202. getUploadImage(){
  203. return this.image_list;
  204. },
  205. // 长按触发移动排序
  206. longPress(e) {
  207. this.touch = true;
  208. this.startX = e.changedTouches[0].pageX
  209. this.startY = e.changedTouches[0].pageY
  210. let index = e.currentTarget.dataset.index;
  211. if(this.columns === 1) { // 单列时候X轴初始不做位移
  212. this.tranConstX = 0;
  213. } else { // 多列的时候计算X轴初始位移, 使 item 水平中心移动到点击处
  214. this.tranConstX = this.startX - this.item.width / 2 - this.itemWrap.left;
  215. }
  216. // 计算Y轴初始位移, 使 item 垂直中心移动到点击处
  217. this.teanConstY = this.startY - this.item.height / 2 - this.itemWrap.top;
  218. this.tranY = this.teanConstY;
  219. this.tranX = this.tranConstX;
  220. this.cur = index;
  221. this.curZ = index;
  222. // #ifndef H5
  223. uni.vibrateShort();
  224. // #endif
  225. },
  226. // 拖拽中
  227. touchMove(e) {
  228. if (!this.touch) return;
  229. let tranX = e.touches[0].pageX - this.startX + this.tranConstX;
  230. let tranY = e.touches[0].pageY - this.startY + this.teanConstY;
  231. let overOnePage = this.overOnePage;
  232. // 判断是否超过一屏幕, 超过则需要判断当前位置动态滚动page的位置
  233. if(overOnePage) {
  234. if(e.touches[0].clientY > this.windowHeight - this.item.height) {
  235. uni.pageScrollTo({
  236. scrollTop: e.touches[0].pageY + this.item.height - this.windowHeight,
  237. duration: 300
  238. });
  239. } else if(e.touches[0].clientY < this.item.height) {
  240. uni.pageScrollTo({
  241. scrollTop: e.touches[0].pageY - this.item.height,
  242. duration: 300
  243. });
  244. }
  245. }
  246. this.tranX = tranX;
  247. this.tranY = tranY;
  248. let originKey = e.currentTarget.dataset.key;
  249. let endKey = this.calculateMoving(tranX, tranY);
  250. // 防止拖拽过程中发生乱序问题
  251. if (originKey == endKey || this.originKey == originKey) return;
  252. this.originKey = originKey;
  253. this.insert(originKey, endKey);
  254. },
  255. // 根据当前的手指偏移量计算目标key
  256. calculateMoving(tranX, tranY) {
  257. let rows = Math.ceil(this.list.length / this.columns) - 1;
  258. let i = Math.round(tranX / this.item.width);
  259. let j = Math.round(tranY / this.item.height);
  260. i = i > (this.columns - 1) ? (this.columns - 1) : i;
  261. i = i < 0 ? 0 : i;
  262. j = j < 0 ? 0 : j;
  263. j = j > rows ? rows : j;
  264. let endKey = i + this.columns * j;
  265. endKey = endKey >= this.list.length ? this.list.length - 1 : endKey;
  266. return endKey
  267. },
  268. // 根据起始key和目标key去重新计算每一项的新的key
  269. insert(origin, end) {
  270. let list;
  271. if (origin < end) {
  272. list = this.list.map((item) => {
  273. if (item.key > origin && item.key <= end) {
  274. item.key = item.key - 1;
  275. } else if (item.key == origin) {
  276. item.key = end;
  277. }
  278. return item
  279. });
  280. this.getPosition(list);
  281. } else if (origin > end) {
  282. list = this.list.map((item) => {
  283. if (item.key >= end && item.key < origin) {
  284. item.key = item.key + 1;
  285. } else if (item.key == origin) {
  286. item.key = end;
  287. }
  288. return item
  289. });
  290. this.getPosition(list);
  291. }
  292. },
  293. // 根据排序后 list 数据进行位移计算
  294. getPosition(data, vibrate = true) {
  295. let list = data.map((item, index) => {
  296. // X轴距离
  297. let tranX = this.item.width * (item.key % this.columns);
  298. let rows = Math.ceil((item.key+1) / this.columns);
  299. let numX = item.key;
  300. for(let i=0; i<rows-1; i++){
  301. numX -= this.columns;
  302. }
  303. numX = numX * this.$props.padding;
  304. tranX = tranX != 0 ? tranX + numX : tranX;
  305. item.tranX = tranX;
  306. // Y轴距离
  307. let tranY = Math.floor(item.key / this.columns) * this.item.height;
  308. if(rows > 1){
  309. let numY = (rows - 1) * this.$props.padding;
  310. tranY = tranY + numY;
  311. }
  312. item.tranY = tranY;
  313. return item
  314. });
  315. this.list = list;
  316. if(!vibrate) return;
  317. this.itemTransition = true;
  318. // #ifndef H5
  319. uni.vibrateShort();
  320. // #endif
  321. let image_list = [];
  322. list.forEach((item) => {
  323. image_list[item.key] = item.data
  324. });
  325. // 元素位置发生改变了,触发change事件告诉父元素更新
  326. this.$emit('change', {image_list: image_list});
  327. },
  328. // 拖拽结束
  329. touchEnd() {
  330. if (!this.touch) return;
  331. this.clearData();
  332. },
  333. // 清除参数
  334. clearData() {
  335. this.originKey = -1;
  336. this.touch = false;
  337. this.cur = -1;
  338. this.tranX = 0;
  339. this.tranY = 0;
  340. // 延迟清空
  341. setTimeout(() => {
  342. this.curZ = -1;
  343. }, 300)
  344. },
  345. // 初始化加载
  346. init() {
  347. // 拦截image_list为空未上传图片的情况
  348. if(!this.image_list.length) return;
  349. // 遍历数据源增加扩展项, 以用作排序使用
  350. let list = this.image_list.map((item, index) => {
  351. return {
  352. key: index,
  353. tranX: 0,
  354. tranY: 0,
  355. data: item
  356. }
  357. });
  358. this.list = list;
  359. this.itemTransition = false;
  360. this.windowHeight = uni.getSystemInfoSync().windowHeight;
  361. setTimeout(() => {
  362. // 获取每一项的宽高等属性
  363. this.createSelectorQuery().select(".item").boundingClientRect((res) => {
  364. let rows = Math.ceil(this.list.length / this.columns);
  365. this.item = res;
  366. this.getPosition(this.list, false);
  367. let itemWrapHeight = rows * res.height;
  368. itemWrapHeight += rows * this.$props.padding - this.$props.padding;
  369. this.itemWrapHeight = itemWrapHeight;
  370. this.createSelectorQuery().select(".item-wrap").boundingClientRect((res) => {
  371. this.itemWrap = res;
  372. let overOnePage = itemWrapHeight + res.top > this.windowHeight;
  373. this.overOnePage = overOnePage;
  374. }).exec();
  375. }).exec();
  376. }, 300)
  377. }
  378. }
  379. }
  380. </script>
  381. <style lang="scss" scoped="scoped">
  382. .upload-images{
  383. display: flex;
  384. flex-wrap: wrap;
  385. .upload-image-item{
  386. // width: 220rpx;
  387. // height: 220rpx;
  388. background: #DDDDDD;
  389. position: relative;
  390. // margin-right: 12rpx;
  391. width: 100%;
  392. height: 100%;
  393. &:nth-child(3n){
  394. margin-right: 0rpx;
  395. }
  396. &:nth-child(n+4){
  397. margin-top: 12rpx;
  398. }
  399. image{
  400. width: 100%;
  401. height: 100%;
  402. }
  403. .remove-image{
  404. position: absolute;
  405. right: -4rpx;
  406. top: -18rpx;
  407. color: #e74c3c;
  408. font-size: 40rpx;
  409. padding: 8rpx;
  410. }
  411. }
  412. .upload-image-item-after{
  413. position: absolute;
  414. width: 100%;
  415. height: 100%;
  416. display: flex;
  417. justify-content: center;
  418. align-items: center;
  419. font-size: 60rpx;
  420. color: #999999;
  421. }
  422. }
  423. .item-wrap {
  424. position: relative;
  425. width: 100%;
  426. .item {
  427. position: absolute;
  428. width: 100%;
  429. z-index: 1;
  430. &.itemTransition {
  431. transition: transform 0.3s;
  432. }
  433. &.zIndex {
  434. z-index: 2;
  435. }
  436. &.cur {
  437. background: #1998FE;
  438. transition: initial;
  439. }
  440. }
  441. }
  442. .info {
  443. position: relative;
  444. // padding-top: 100%;
  445. background: #ffffff;
  446. // margin-right: 5px;
  447. width: auto;
  448. height: 100%;
  449. &:nth-child(3n){
  450. margin-right: 0px;
  451. }
  452. & > view {
  453. position: absolute;
  454. top: 0;
  455. left: 0;
  456. width: 100%;
  457. height: 100%;
  458. overflow: hidden;
  459. box-sizing: border-box;
  460. image {
  461. width: 100%;
  462. height: 100%;
  463. }
  464. }
  465. }
  466. .indicator {
  467. position: fixed;
  468. z-index: 99999;
  469. right: 0rpx;
  470. top: 50%;
  471. margin-top: -250rpx;
  472. padding: 20rpx;
  473. & > view {
  474. width: 36rpx;
  475. height: 500rpx;
  476. background: #ffffff;
  477. border-radius: 30rpx;
  478. box-shadow: 0 0 10rpx -4rpx rgba(0, 0, 0, 0.5);
  479. color: pink;
  480. padding-top: 90rpx;
  481. box-sizing: border-box;
  482. font-size: 24rpx;
  483. text-align: center;
  484. opacity: 0.8;
  485. }
  486. }
  487. </style>