Sign.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <template>
  2. <view class="signature-wrap">
  3. <view v-if="!disabled" v-show="show" class="signature-contain">
  4. <view class="signature-main" style="z-index: 3000;">
  5. <view>
  6. <!-- <uni-icons custom-prefix="custom-icon" type="arrow-up" size="30"></uni-icons> -->
  7. <view class="signature-title">
  8. <text v-for="(t,i) in titles" :key="i">{{t}}</text>
  9. </view>
  10. </view>
  11. <canvas disable-scroll="true" class="signature" :class="cid" :canvas-id="cid" @touchstart="touchstart"
  12. @touchmove="touchmove" @touchend="touchend">
  13. <view class="tip" v-show="!hasDrew">
  14. 请在此区域手写签名
  15. </view>
  16. </canvas>
  17. <view class="signature-btns">
  18. <view class="btn btn-cancel cu-btn bg-main margin-tb-sm text-white" @tap="cancelSignature()">
  19. <text>取</text><text>消</text>
  20. </view>
  21. <view class="btn btn-clear cu-btn bg-main margin-tb-sm text-white" @tap="clearSignature();">
  22. <text>清</text><text>空</text>
  23. </view>
  24. <view class="btn btn-ok cu-btn bg-main margin-tb-sm text-white" @tap="onOK()">
  25. <text>确</text><text>定</text>
  26. </view>
  27. </view>
  28. </view>
  29. </view>
  30. </view>
  31. </template>
  32. <script>
  33. let _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  34. var _utf8_encode = function(string) {
  35. string = string.replace(/\r\n/g, "\n");
  36. var utftext = "";
  37. for (var n = 0; n < string.length; n++) {
  38. var c = string.charCodeAt(n);
  39. if (c < 128) {
  40. utftext += String.fromCharCode(c);
  41. } else if ((c > 127) && (c < 2048)) {
  42. utftext += String.fromCharCode((c >> 6) | 192);
  43. utftext += String.fromCharCode((c & 63) | 128);
  44. } else {
  45. utftext += String.fromCharCode((c >> 12) | 224);
  46. utftext += String.fromCharCode(((c >> 6) & 63) | 128);
  47. utftext += String.fromCharCode((c & 63) | 128);
  48. }
  49. }
  50. return utftext;
  51. }
  52. let base64encode = function(input) {
  53. var output = "";
  54. var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
  55. var i = 0;
  56. input = _utf8_encode(input);
  57. while (i < input.length) {
  58. chr1 = input.charCodeAt(i++);
  59. chr2 = input.charCodeAt(i++);
  60. chr3 = input.charCodeAt(i++);
  61. enc1 = chr1 >> 2;
  62. enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
  63. enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
  64. enc4 = chr3 & 63;
  65. if (isNaN(chr2)) {
  66. enc3 = enc4 = 64;
  67. } else if (isNaN(chr3)) {
  68. enc4 = 64;
  69. }
  70. output = output +
  71. _keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
  72. _keyStr.charAt(enc3) + _keyStr.charAt(enc4);
  73. }
  74. return output;
  75. }
  76. export default {
  77. cxt: null,
  78. data() {
  79. return {
  80. VERSION: '1.0.0',
  81. cid: 'cvs' + this.jnpf.idGenerator(),
  82. show: false,
  83. ctrl: null,
  84. listeners: [],
  85. prevView: '',
  86. draws: [],
  87. lines: [],
  88. line: null,
  89. hasDrew: false,
  90. };
  91. },
  92. props: {
  93. value: {
  94. default: '',
  95. },
  96. title: {
  97. type: String,
  98. default: '请签名',
  99. },
  100. disabled: {
  101. type: Boolean,
  102. default: false,
  103. },
  104. showBtn: {
  105. type: Boolean,
  106. default: false,
  107. },
  108. },
  109. watch: {
  110. value() {
  111. this.prevView = this.value;
  112. },
  113. lines: {
  114. deep: true,
  115. handler(val) {
  116. this.hasDrew = !!val.length
  117. }
  118. }
  119. },
  120. computed: {
  121. titles() {
  122. return this.title.split('')
  123. },
  124. absPrevView() {
  125. var pv = this.prevView;
  126. // if(pv){
  127. // pv = this.$wrapUrl(pv)
  128. // }
  129. return pv;
  130. }
  131. },
  132. mounted() {
  133. this.prevView = this.value;
  134. },
  135. methods: {
  136. onOK() {
  137. let data = this.ctrl.getValue();
  138. this.$emit('input', data);
  139. this.prevView = data;
  140. this.hideSignature();
  141. let f = this.listeners.shift();
  142. if (f) {
  143. f(data);
  144. }
  145. },
  146. touchSignature() {
  147. let sig = this.prevView
  148. if (!sig || !sig.length) {
  149. this.showSignature()
  150. }
  151. },
  152. showSignature() {
  153. if (this.disabled) return;
  154. if (!this.ctrl) {
  155. this.initCtrl();
  156. } else if (!this.show) {
  157. this.clearSignature();
  158. this.show = true;
  159. }
  160. },
  161. async getSyncSignature() {
  162. this.showSignature();
  163. return await new Promise(async (resolve, reject) => {
  164. this.listeners.push((res) => {
  165. resolve(res);
  166. });
  167. });
  168. },
  169. cancelSignature() {
  170. this.listeners.map((f) => {
  171. f(null);
  172. })
  173. this.hideSignature();
  174. },
  175. hideSignature() {
  176. this.ctrl && this.ctrl.clear();
  177. this.show = false;
  178. },
  179. clearSignature() {
  180. this.ctrl && this.ctrl.clear();
  181. },
  182. async initCtrl() {
  183. this.show = true;
  184. let cxt = uni.createCanvasContext(this.cid, this);
  185. this.cxt = cxt;
  186. // cxt.clearRect(0,0,c.width,c.height);
  187. this.ctrl = {
  188. width: 0,
  189. height: 0,
  190. clear: () => {
  191. this.lines = [];
  192. let info = uni.createSelectorQuery().in(this).select("." + this.cid);
  193. info.boundingClientRect((data) => {
  194. if (data) {
  195. cxt.clearRect(0, 0, data.width, data.height);
  196. if (data.width && data.height) {
  197. this.ctrl.width = data.width;
  198. this.ctrl.height = data.height;
  199. }
  200. }
  201. }).exec();
  202. this.redraw();
  203. },
  204. getValue: () => {
  205. if (!this.lines.length) return '';
  206. let svg = this._get_svg();
  207. // new Buff
  208. let b64 = base64encode(svg);
  209. let data = 'data:image/svg+xml;base64,' + b64;
  210. return data;
  211. },
  212. };
  213. this.$nextTick(function() {
  214. this.ctrl.clear();
  215. })
  216. },
  217. _get_svg() {
  218. let r = -90;
  219. let paths = [];
  220. let raww = this.ctrl.width;
  221. let rawh = this.ctrl.height;
  222. let width = Math.abs(r) != 90 ? raww : rawh;
  223. let height = Math.abs(r) == 90 ? raww : rawh;
  224. let cx = raww / 2;
  225. let cy = rawh / 2;
  226. let PI = Math.PI;
  227. let R = (r || 0) % 360;
  228. let cosv = Math.cos(R * PI / 180);
  229. let sinv = Math.sin(R * PI / 180);
  230. let dcx = (width - raww) / 2;
  231. let dcy = (height - rawh) / 2;
  232. let trans = function(p) {
  233. if (!R) {
  234. return p;
  235. } else {
  236. let nx = (p.x - cx) * cosv - (p.y - cy) * sinv + cx;
  237. let ny = (p.x - cx) * sinv + (p.y - cy) * cosv + cy;
  238. return {
  239. x: nx + dcx,
  240. y: ny + dcy
  241. };
  242. }
  243. return p;
  244. }
  245. this.lines.map(l => {
  246. if (l.points.length < 2) {
  247. return;
  248. }
  249. let sp = trans(l.start)
  250. let pts = [`M ${sp.x} ${Number(sp.y)}`];
  251. l.points.map(p => {
  252. let np = trans(p)
  253. pts.push(`L ${np.x} ${Number(np.y)}`);
  254. });
  255. paths.push(
  256. `<path stroke-linejoin="round" stroke-linecap="round" stroke-width="3" stroke="rgb(0,0,0)" fill="none" d="${pts.join(' ')}"/>`
  257. );
  258. })
  259. let svg =
  260. `<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}" height="${height}">${paths.join('\n')}</svg>`;
  261. return svg;
  262. },
  263. _get_svg_raw() {
  264. let paths = [];
  265. this.lines.map(l => {
  266. if (l.points.length < 2) {
  267. return;
  268. }
  269. let pts = [`M ${l.start.x} ${Number(l.start.y)}`];
  270. l.points.map(p => {
  271. pts.push(`L ${p.x} ${Number(p.y)}`);
  272. });
  273. paths.push(
  274. `<path stroke-linejoin="round" stroke-linecap="round" stroke-width="3" stroke="rgb(0,0,0)" fill="none" d="${pts.join(' ')}"/>`
  275. );
  276. })
  277. let width = this.ctrl.width;
  278. let height = this.ctrl.height;
  279. let svg =
  280. `<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}" height="${height}" transform="rotate(-90)">${paths.join('\n')}</svg>`;
  281. return svg;
  282. },
  283. _get_point(e) {
  284. return {
  285. x: e.changedTouches[0].x.toFixed(1),
  286. y: e.changedTouches[0].y.toFixed(1),
  287. }
  288. },
  289. touchstart(e) {
  290. let p = this._get_point(e);
  291. this.line = {
  292. start: p,
  293. points: [p],
  294. }
  295. this.lines.push(this.line);
  296. },
  297. touchmove(e) {
  298. let p = this._get_point(e);
  299. this.line.points.push(p)
  300. if (!this.tm) {
  301. this.tm = setTimeout(() => {
  302. this.redraw();
  303. this.tm = 0;
  304. }, 10)
  305. }
  306. },
  307. touchend(e) {
  308. let p = this._get_point(e);
  309. this.line.points.push(p)
  310. this.line.end = p
  311. this.redraw()
  312. },
  313. redraw() {
  314. let cxt = this.cxt;
  315. cxt.setStrokeStyle("#000");
  316. cxt.setLineWidth(3);
  317. var last = null;
  318. this.lines.map(l => {
  319. cxt.beginPath();
  320. if (l.points.length < 2) {
  321. return;
  322. }
  323. cxt.moveTo(l.start.x, l.start.y);
  324. l.points.map(p => {
  325. cxt.lineTo(p.x, p.y)
  326. })
  327. cxt.stroke()
  328. })
  329. cxt.draw()
  330. },
  331. canvasIdErrorCallback: function(e) {}
  332. }
  333. }
  334. </script>
  335. <style lang="scss">
  336. .signature-wrap {
  337. height: 100%;
  338. .img-wrap {
  339. width: 100%;
  340. display: flex;
  341. align-items: flex-start;
  342. text-align: left;
  343. align-content: flex-start;
  344. justify-content: flex-start;
  345. color: #2A79F9;
  346. flex-direction: column;
  347. image {
  348. width: 100%;
  349. }
  350. }
  351. }
  352. .signature-contain {
  353. z-index: 9000;
  354. position: fixed;
  355. left: 0;
  356. top: 0;
  357. width: 100%;
  358. .signature-main {
  359. background: white;
  360. flex-direction: row-reverse;
  361. display: flex;
  362. align-items: stretch;
  363. height: 101%;
  364. overflow: scroll;
  365. }
  366. .signature-title {
  367. font-weight: bold;
  368. font-size: 18px;
  369. display: flex;
  370. padding: 0 20rpx;
  371. flex-direction: column;
  372. justify-content: center;
  373. height: 100vh;
  374. color: $uni-text-color;
  375. text {
  376. transform: rotate(90deg);
  377. }
  378. }
  379. .tip {
  380. position: absolute;
  381. top: 50%;
  382. left: 50%;
  383. white-space: nowrap;
  384. transform: translate(-50%, -50%)rotate(90deg);
  385. }
  386. .signature {
  387. background: #f7f7f7;
  388. width: 90vw;
  389. height: 95vh;
  390. align-self: center;
  391. // pointer-events:none;
  392. }
  393. .signature-btns {
  394. display: flex;
  395. padding: 2px;
  396. // margin-right: 5px;
  397. flex-direction: column;
  398. .btn {
  399. flex-grow: 1;
  400. flex-shrink: 0;
  401. padding: 20rpx;
  402. font-size: 20px;
  403. margin: 0;
  404. text-align: center;
  405. text-decoration: none;
  406. height: 30vh;
  407. display: flex;
  408. align-content: center;
  409. justify-content: center;
  410. flex-direction: column;
  411. text {
  412. transform: rotate(90deg);
  413. }
  414. &+.btn {
  415. border-top: 1px solid #eee;
  416. }
  417. &.btn-clear {
  418. // background-color: #fc2a07;
  419. color: $uni-color-warning;
  420. }
  421. &.btn-cancel {
  422. // background-color: #eff4f4;
  423. color: #606266;
  424. }
  425. &.btn-ok {
  426. // background-color: $uni-color-success;
  427. color: $uni-color-primary;
  428. }
  429. }
  430. }
  431. }
  432. </style>