imageUpload.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. <template>
  2. <view class="con">
  3. <template v-if="viewWidth">
  4. <movable-area class="area" :style="{ height: areaHeight }" @mouseenter="mouseenter" @mouseleave="mouseleave">
  5. <movable-view
  6. v-for="(item, index) in imageList"
  7. :key="item.id"
  8. class="view"
  9. direction="all"
  10. :y="item.y"
  11. :x="item.x"
  12. :damping="40"
  13. :disabled="item.disable"
  14. @change="onChange($event, item)"
  15. @touchstart="touchstart(item)"
  16. @mousedown="touchstart(item)"
  17. @touchend="touchend(item)"
  18. @mouseup="touchend(item)"
  19. :style="{
  20. width: viewWidth + 'px',
  21. height: viewWidth + 'px',
  22. 'z-index': item.zIndex,
  23. opacity: item.opacity,
  24. }"
  25. >
  26. <view
  27. class="area-con"
  28. :style="{
  29. width: childWidth,
  30. height: childWidth,
  31. borderRadius: borderRadius + 'rpx',
  32. transform: 'scale(' + item.scale + ')',
  33. }"
  34. >
  35. <image class="pre-image" :src="item.src" mode="aspectFill"></image>
  36. <view class="del-con" @click="delImages(item, index)" @touchstart.stop="delImageMp(item, index)" @touchend.stop="nothing()" @mousedown.stop="nothing()" @mouseup.stop="nothing()">
  37. <view class="del-wrap">
  38. <image
  39. class="del-image"
  40. src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAhdEVYdENyZWF0aW9uIFRpbWUAMjAyMDowNzoyNSAyMTo1NDoyOU4TkJAAAADcSURBVFhH7ZfRCoMwDEXLvkjwwVf/bH/emmAyN6glTW9WBjsgwm28OeCLpj81Sil7zvlJ90UiONS/yY5VogsO6XrBg3IEQ5a/s8vRSWUAKmLqp2w5jz5BiNQEGMo3GbloDLtFXJ1IkaEuhAiiY6gEIqB4yqACSk9piIBiKQ8VUFpLviKg3C2rESKgWERCBZSWiEfgIfffYvrrsAgoISJ3Apy3zuTxcSxLQkV6ykNEPKVQkZEyiAiiZKgDIaC4upACSlcn5fM/+WuDCAHF1E/Z/N9AhkMZnPNDPI+UDjPIXgAQIGjNAAAAAElFTkSuQmCC"
  41. >
  42. </image>
  43. </view>
  44. </view>
  45. </view>
  46. </movable-view>
  47. <view class="add" v-if="imageList.length < number" :style="{ top: add.y, left: add.x, width: viewWidth + 'px', height: viewWidth + 'px' }" @click="addImages">
  48. <view class="add-wrap" :style="{ width: childWidth, height: childWidth, borderRadius: borderRadius + 'rpx' }">
  49. <image
  50. style="width: 54rpx; height: 54rpx"
  51. src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAA2CAYAAACMRWrdAAABIUlEQVRoQ+2a2w2DMAxFeQzWrsMUbadAsEw3S1CqVgppKwLX8BEOP4iHTXx8uUgWdVXoVhdaV0VhSmf7vr/H8V3XzY6V3P9iD+nYOI5P7/01LMI596AwoZV0TIBXIUWFXhKLFBWYSFGhhxQN6SFFQ5i4ogITKSr0cEVDekjRECauqMBEigq9U7piOk2yAti27SUe5ljlTfPEQ6KZecTvwl4P3ytvOv06R2HDMNzes7+6aRrvnHvtf50L13Lp50rx88zcvNlS3JpwKQ67XyK04nq2nFbk/LqVjin0TvmBNgQ2S4UUDcliHgpMpKjQwxUN6SFFQ5i4ogITKSr0cEVDekjRECauqMAsVoph+hVPtYr5+03p9tbYQ96xrYtT4ootbAJGVxxVTapVswAAAABJRU5ErkJggg=="
  52. >
  53. </image>
  54. </view>
  55. </view>
  56. </movable-area>
  57. </template>
  58. </view>
  59. </template>
  60. <script>
  61. export default {
  62. emits: ["input", "update:modelValue"],
  63. props: {
  64. // 排序图片
  65. value: {
  66. type: Array,
  67. default: function () {
  68. return [];
  69. },
  70. },
  71. // 排序图片
  72. modelValue: {
  73. type: Array,
  74. default: function () {
  75. return [];
  76. },
  77. },
  78. // 从 list 元素对象中读取的键名
  79. keyName: {
  80. type: String,
  81. default: null,
  82. },
  83. // 选择图片数量限制
  84. number: {
  85. type: Number,
  86. default: 6,
  87. },
  88. // 图片父容器宽度(实际显示的图片宽度为 imageWidth / 1.1 ),单位 rpx
  89. // imageWidth > 0 则 cols 无效
  90. imageWidth: {
  91. type: Number,
  92. default: 0,
  93. },
  94. // 图片列数
  95. cols: {
  96. type: Number,
  97. default: 3,
  98. },
  99. // 图片圆角,单位 rpx
  100. borderRadius: {
  101. type: Number,
  102. default: 0,
  103. },
  104. // 图片周围空白填充,单位 rpx
  105. padding: {
  106. type: Number,
  107. default: 10,
  108. },
  109. // 拖动图片时放大倍数 [0, ∞)
  110. scale: {
  111. type: Number,
  112. default: 1.1,
  113. },
  114. // 拖动图片时不透明度
  115. opacity: {
  116. type: Number,
  117. default: 0.7,
  118. },
  119. // 自定义添加
  120. addImage: {
  121. type: Function,
  122. default: null,
  123. },
  124. // 删除确认
  125. delImage: {
  126. type: Function,
  127. default: null,
  128. },
  129. },
  130. data() {
  131. return {
  132. imageList: [],
  133. width: 0,
  134. add: {
  135. x: 0,
  136. y: 0,
  137. },
  138. colsValue: 0,
  139. viewWidth: 0,
  140. tempItem: null,
  141. timer: null,
  142. changeStatus: true,
  143. preStatus: true,
  144. first: true,
  145. };
  146. },
  147. computed: {
  148. areaHeight() {
  149. let height = "";
  150. // return '355px'
  151. if (this.imageList.length < this.number) {
  152. height = (Math.ceil((this.imageList.length + 1) / this.colsValue) * this.viewWidth).toFixed() + "px";
  153. } else {
  154. height = (Math.ceil(this.imageList.length / this.colsValue) * this.viewWidth).toFixed() + "px";
  155. }
  156. return height;
  157. },
  158. childWidth() {
  159. return this.viewWidth - this.rpx2px(this.padding) * 2 + "px";
  160. },
  161. },
  162. watch: {
  163. value: {
  164. handler(n) {
  165. if (!this.first && this.changeStatus) {
  166. let flag = false;
  167. for (let i = 0; i < n.length; i++) {
  168. if (flag) {
  169. this.addProperties(this.getSrc(n[i]));
  170. continue;
  171. }
  172. if (this.imageList.length === i || this.imageList[i].src !== this.getSrc(n[i])) {
  173. flag = true;
  174. this.imageList.splice(i);
  175. this.addProperties(this.getSrc(n[i]));
  176. }
  177. }
  178. }
  179. },
  180. deep: true,
  181. },
  182. modelValue: {
  183. handler(n) {
  184. if (!this.first && this.changeStatus) {
  185. let flag = false;
  186. for (let i = 0; i < n.length; i++) {
  187. if (flag) {
  188. this.addProperties(this.getSrc(n[i]));
  189. continue;
  190. }
  191. if (this.imageList.length === i || this.imageList[i].src !== this.getSrc(n[i])) {
  192. flag = true;
  193. this.imageList.splice(i);
  194. this.addProperties(this.getSrc(n[i]));
  195. }
  196. }
  197. }
  198. },
  199. deep: true,
  200. },
  201. },
  202. created() {
  203. this.width = uni.getSystemInfoSync().windowWidth;
  204. },
  205. mounted() {
  206. const query = uni.createSelectorQuery().in(this);
  207. query.select(".con").boundingClientRect((data) => {
  208. this.colsValue = this.cols;
  209. this.viewWidth = data.width / this.cols;
  210. if (this.imageWidth > 0) {
  211. this.viewWidth = this.rpx2px(this.imageWidth);
  212. this.colsValue = Math.floor(data.width / this.viewWidth);
  213. }
  214. let list = this.value;
  215. // #ifdef VUE3
  216. list = this.modelValue;
  217. // #endif
  218. for (let item of list) {
  219. this.addProperties(this.getSrc(item));
  220. }
  221. this.first = false;
  222. });
  223. query.exec();
  224. },
  225. methods: {
  226. getSrc(item) {
  227. if (this.keyName !== null) {
  228. return item[this.keyName];
  229. }
  230. return item;
  231. },
  232. onChange(e, item) {
  233. if (!item) return;
  234. item.oldX = e.detail.x;
  235. item.oldY = e.detail.y;
  236. if (e.detail.source === "touch") {
  237. if (item.moveEnd) {
  238. item.offset = Math.sqrt(Math.pow(item.oldX - item.absX * this.viewWidth, 2) + Math.pow(item.oldY - item.absY * this.viewWidth, 2));
  239. }
  240. let x = Math.floor((e.detail.x + this.viewWidth / 2) / this.viewWidth);
  241. if (x >= this.colsValue) return;
  242. let y = Math.floor((e.detail.y + this.viewWidth / 2) / this.viewWidth);
  243. let index = this.colsValue * y + x;
  244. if (item.index != index && index < this.imageList.length) {
  245. this.changeStatus = false;
  246. for (let obj of this.imageList) {
  247. if (item.index > index && obj.index >= index && obj.index < item.index) {
  248. this.change(obj, 1);
  249. } else if (item.index < index && obj.index <= index && obj.index > item.index) {
  250. this.change(obj, -1);
  251. } else if (obj.id != item.id) {
  252. obj.offset = 0;
  253. obj.x = obj.oldX;
  254. obj.y = obj.oldY;
  255. setTimeout(() => {
  256. this.$nextTick(() => {
  257. obj.x = obj.absX * this.viewWidth;
  258. obj.y = obj.absY * this.viewWidth;
  259. });
  260. }, 0);
  261. }
  262. }
  263. item.index = index;
  264. item.absX = x;
  265. item.absY = y;
  266. if (!item.moveEnd) {
  267. setTimeout(() => {
  268. this.$nextTick(() => {
  269. item.x = item.absX * this.viewWidth;
  270. item.y = item.absY * this.viewWidth;
  271. });
  272. }, 0);
  273. }
  274. this.sortList();
  275. }
  276. }
  277. },
  278. change(obj, i) {
  279. obj.index += i;
  280. obj.offset = 0;
  281. obj.x = obj.oldX;
  282. obj.y = obj.oldY;
  283. obj.absX = obj.index % this.colsValue;
  284. obj.absY = Math.floor(obj.index / this.colsValue);
  285. setTimeout(() => {
  286. this.$nextTick(() => {
  287. obj.x = obj.absX * this.viewWidth;
  288. obj.y = obj.absY * this.viewWidth;
  289. });
  290. }, 0);
  291. },
  292. touchstart(item) {
  293. this.imageList.forEach((v) => {
  294. v.zIndex = v.index + 9;
  295. });
  296. item.zIndex = 99;
  297. item.moveEnd = true;
  298. this.tempItem = item;
  299. this.timer = setTimeout(() => {
  300. item.scale = this.scale;
  301. item.opacity = this.opacity;
  302. clearTimeout(this.timer);
  303. this.timer = null;
  304. }, 200);
  305. },
  306. touchend(item) {
  307. this.previewImage(item);
  308. item.scale = 1;
  309. item.opacity = 1;
  310. item.x = item.oldX;
  311. item.y = item.oldY;
  312. item.offset = 0;
  313. item.moveEnd = false;
  314. setTimeout(() => {
  315. this.$nextTick(() => {
  316. item.x = item.absX * this.viewWidth;
  317. item.y = item.absY * this.viewWidth;
  318. this.tempItem = null;
  319. this.changeStatus = true;
  320. });
  321. }, 0);
  322. },
  323. previewImage(item) {
  324. if (this.timer && this.preStatus && this.changeStatus && item.offset < 28.28) {
  325. clearTimeout(this.timer);
  326. this.timer = null;
  327. const list = this.value || this.modelValue;
  328. let srcList = list.map((v) => this.getSrc(v));
  329. uni.previewImage({
  330. urls: srcList,
  331. current: item.src,
  332. success: () => {
  333. this.preStatus = false;
  334. setTimeout(() => {
  335. this.preStatus = true;
  336. }, 600);
  337. },
  338. fail: (e) => {
  339. },
  340. });
  341. } else if (this.timer) {
  342. clearTimeout(this.timer);
  343. this.timer = null;
  344. }
  345. },
  346. mouseenter() {
  347. //#ifdef H5
  348. this.imageList.forEach((v) => {
  349. v.disable = false;
  350. });
  351. //#endif
  352. },
  353. mouseleave() {
  354. //#ifdef H5
  355. if (this.tempItem) {
  356. this.imageList.forEach((v) => {
  357. v.disable = true;
  358. v.zIndex = v.index + 9;
  359. v.offset = 0;
  360. v.moveEnd = false;
  361. if (v.id == this.tempItem.id) {
  362. if (this.timer) {
  363. clearTimeout(this.timer);
  364. this.timer = null;
  365. }
  366. v.scale = 1;
  367. v.opacity = 1;
  368. v.x = v.oldX;
  369. v.y = v.oldY;
  370. this.$nextTick(() => {
  371. v.x = v.absX * this.viewWidth;
  372. v.y = v.absY * this.viewWidth;
  373. this.tempItem = null;
  374. });
  375. }
  376. });
  377. this.changeStatus = true;
  378. }
  379. //#endif
  380. },
  381. addImages() {
  382. if (typeof this.addImage === "function") {
  383. this.addImage.bind(this.$parent)();
  384. } else {
  385. let checkNumber = this.number - this.imageList.length;
  386. uni.chooseImage({
  387. count: checkNumber,
  388. sourceType: ["album", "camera"],
  389. success: (res) => {
  390. let count = checkNumber <= res.tempFilePaths.length ? checkNumber : res.tempFilePaths.length;
  391. for (let i = 0; i < count; i++) {
  392. this.addProperties(res.tempFilePaths[i]);
  393. }
  394. this.sortList();
  395. },
  396. });
  397. }
  398. },
  399. delImages(item, index) {
  400. if (typeof this.delImage === "function") {
  401. this.delImage.bind(this.$parent)(() => {
  402. this.delImageHandle(item, index);
  403. });
  404. } else {
  405. this.delImageHandle(item, index);
  406. }
  407. },
  408. delImageHandle(item, index) {
  409. this.imageList.splice(index, 1);
  410. for (let obj of this.imageList) {
  411. if (obj.index > item.index) {
  412. obj.index -= 1;
  413. obj.x = obj.oldX;
  414. obj.y = obj.oldY;
  415. obj.absX = obj.index % this.colsValue;
  416. obj.absY = Math.floor(obj.index / this.colsValue);
  417. this.$nextTick(() => {
  418. obj.x = obj.absX * this.viewWidth;
  419. obj.y = obj.absY * this.viewWidth;
  420. });
  421. }
  422. }
  423. this.add.x = (this.imageList.length % this.colsValue) * this.viewWidth + "px";
  424. this.add.y = Math.floor(this.imageList.length / this.colsValue) * this.viewWidth + "px";
  425. this.sortList();
  426. },
  427. delImageMp(item, index) {
  428. //#ifdef MP
  429. this.delImages(item, index);
  430. //#endif
  431. },
  432. sortList() {
  433. const result = [];
  434. let source = this.value;
  435. // #ifdef VUE3
  436. source = this.modelValue;
  437. // #endif
  438. let list = this.imageList.slice();
  439. list.sort((a, b) => {
  440. return a.index - b.index;
  441. });
  442. for (let s of list) {
  443. let item = source.find((d) => this.getSrc(d) == s.src);
  444. if (item) {
  445. result.push(item);
  446. } else {
  447. if (this.keyName !== null) {
  448. result.push({
  449. [this.keyName]: s.src,
  450. });
  451. } else {
  452. result.push(s.src);
  453. }
  454. }
  455. }
  456. this.$emit("input", result);
  457. this.$emit("update:modelValue", result);
  458. },
  459. addProperties(item) {
  460. let absX = this.imageList.length % this.colsValue;
  461. let absY = Math.floor(this.imageList.length / this.colsValue);
  462. let x = absX * this.viewWidth;
  463. let y = absY * this.viewWidth;
  464. this.imageList.push({
  465. src: item,
  466. x,
  467. y,
  468. oldX: x,
  469. oldY: y,
  470. absX,
  471. absY,
  472. scale: 1,
  473. zIndex: 9,
  474. opacity: 1,
  475. index: this.imageList.length,
  476. id: this.guid(16),
  477. disable: false,
  478. offset: 0,
  479. moveEnd: false,
  480. });
  481. this.add.x = (this.imageList.length % this.colsValue) * this.viewWidth + "px";
  482. this.add.y = Math.floor(this.imageList.length / this.colsValue) * this.viewWidth + "px";
  483. },
  484. nothing() {},
  485. rpx2px(v) {
  486. return (this.width * v) / 750;
  487. },
  488. guid(len = 32) {
  489. const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
  490. const uuid = [];
  491. const radix = chars.length;
  492. for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
  493. uuid.shift();
  494. return `u${uuid.join("")}`;
  495. },
  496. },
  497. };
  498. </script>
  499. <style lang="scss" scoped>
  500. .con {
  501. // padding: 30rpx;
  502. .area {
  503. width: 100%;
  504. .view {
  505. display: flex;
  506. justify-content: center;
  507. align-items: center;
  508. .area-con {
  509. position: relative;
  510. overflow: hidden;
  511. .pre-image {
  512. width: 100%;
  513. height: 100%;
  514. }
  515. .del-con {
  516. position: absolute;
  517. top: 0rpx;
  518. right: 0rpx;
  519. padding: 0 0 20rpx 20rpx;
  520. .del-wrap {
  521. width: 36rpx;
  522. height: 36rpx;
  523. background-color: rgba(0, 0, 0, 0.4);
  524. border-radius: 0 0 0 10rpx;
  525. display: flex;
  526. justify-content: center;
  527. align-items: center;
  528. .del-image {
  529. width: 20rpx;
  530. height: 20rpx;
  531. }
  532. }
  533. }
  534. }
  535. }
  536. .add {
  537. position: absolute;
  538. display: flex;
  539. justify-content: center;
  540. align-items: center;
  541. .add-wrap {
  542. display: flex;
  543. justify-content: center;
  544. align-items: center;
  545. background-color: #eeeeee;
  546. }
  547. }
  548. }
  549. }
  550. </style>