th-autograph.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <!-- 草稿卡片组件 -->
  2. <template>
  3. <view class="autograph-box">
  4. <canvas class="autograph" :canvas-id="canvasId" :id="canvasId" @touchstart="canvasStart($event)" @touchmove="canvasMove($event)">
  5. <view v-if="history.length==0" :class="['default-text',horizontalScreen?'rote-text':'']">草稿区域</view>
  6. </canvas>
  7. <view :class="['action-box',horizontalScreen?'horizontalScreen':'']">
  8. <view class="action-bar">
  9. <view :class="[actionShow?'action-open':'action-close']">
  10. <!-- <image src="../../static/th-autograph/pencli.svg" @click="openAction('thLine')" v-if="judge('pencli')"></image> -->
  11. <image src="../../static/th-autograph/color.svg" @click="openAction('thColor')" v-if="judge('color')"></image>
  12. <image src="../../static/th-autograph/back.svg" @click="goBack" v-if="judge('back')"></image>
  13. <image src="../../static/th-autograph/clear.svg" @click="clear" v-if="judge('clear')"></image>
  14. </view>
  15. <image src="../../static/th-autograph/checkRow.png" @click="checkAction"
  16. v-if="actionBar.length!=0"
  17. :class="[actionShow?'roteRight':'roteLeft']"></image>
  18. </view>
  19. <!-- <view class="th-submit" @click="saveCanvas">确定</view> -->
  20. </view>
  21. <th-color ref="thColor" @setColor="setColor"></th-color>
  22. <th-line ref="thLine" @setLine="setLine"></th-line>
  23. </view>
  24. </template>
  25. <script>
  26. /**
  27. * @property {Array} actionBar 操作按钮配置 pencli(线条) color(颜色) back(返回) clear(清空)
  28. * @property {String} canvasId CanvasId
  29. * @property {Boolean} isDownload 是否下载签名
  30. * @property {Boolean} horizontalScreen 是否横屏
  31. * @property {String} fileName 文件名称
  32. * @property {String} delineColor 线颜色
  33. * @property {Number} delineWidth 线宽度
  34. **/
  35. import thColor from "./th-color.vue"
  36. import thLine from "./th-line.vue"
  37. export default {
  38. props: {
  39. //canvasId
  40. canvasId: {
  41. type: String,
  42. default: `th-${Date.now()}`
  43. },
  44. //配置栏
  45. actionBar:{
  46. type:Array,
  47. default:()=>{
  48. return [
  49. 'pencli',
  50. 'color',
  51. 'back',
  52. 'clear'
  53. ]
  54. }
  55. },
  56. //是否下载签名
  57. isDownload: {
  58. type: Boolean,
  59. default: true
  60. },
  61. //是否横屏
  62. horizontalScreen: {
  63. type: Boolean,
  64. default: false
  65. },
  66. //文件名称
  67. fileName: {
  68. type: String,
  69. default: '签名'
  70. },
  71. //线颜色
  72. delineColor: {
  73. type: String,
  74. default: '#000'
  75. },
  76. //线宽度
  77. delineWidth: {
  78. type: Number,
  79. default: 4
  80. }
  81. },
  82. data() {
  83. return {
  84. context:"",
  85. actionShow:true,
  86. history:[],
  87. lineColor:"#000",
  88. lineWidth:4
  89. }
  90. },
  91. components:{
  92. thColor,thLine
  93. },
  94. mounted () {
  95. this.lineColor = this.delineColor
  96. this.lineWidth = this.delineWidth
  97. const ctx = uni.createCanvasContext(this.canvasId,this)
  98. this.context = ctx;
  99. },
  100. methods: {
  101. //操作栏显示控制
  102. judge(key) {
  103. if(this.actionBar.includes(key)){
  104. return true
  105. }else{
  106. return false;
  107. }
  108. },
  109. //打开选择器
  110. openAction(ref) {
  111. this.$refs[ref].checkModel()
  112. },
  113. //设置颜色
  114. setColor(color) {
  115. this.lineColor = color;
  116. },
  117. //设置线条
  118. setLine(width) {
  119. this.lineWidth = width;
  120. },
  121. //切换控制栏
  122. checkAction() {
  123. this.actionShow = !this.actionShow
  124. },
  125. //保存
  126. async saveCanvas() {
  127. const tempFilePath = await this.canvasToFilPath()
  128. if(!this.isDownload) {
  129. this.$emit('submit',tempFilePath)
  130. return false;
  131. }
  132. return new Promise((resolve, reject) => {
  133. // #ifdef H5
  134. try {
  135. const a = document.createElement('a')
  136. a.href = tempFilePath
  137. a.download = this.fileName
  138. document.body.appendChild(a)
  139. a.click()
  140. a.remove()
  141. resolve()
  142. } catch (e) {
  143. reject(e)
  144. }
  145. // #endif
  146. // #ifndef H5
  147. uni.saveImageToPhotosAlbum({
  148. filePath: tempFilePath,
  149. success(resObj) {
  150. this.$emit('submit',tempFilePath)
  151. resolve(resObj)
  152. },
  153. fail(err) {
  154. this.$emit('dowmloadErr',err)
  155. reject(err)
  156. }
  157. })
  158. // #endif
  159. })
  160. },
  161. // 保存临时路径
  162. canvasToFilPath(conf = {}) {
  163. return new Promise((resolve, reject) => {
  164. uni.canvasToTempFilePath(
  165. {
  166. canvasId: this.canvasId,
  167. success: res => {
  168. resolve(res.tempFilePath)
  169. },
  170. fail: err => {
  171. reject(err)
  172. }
  173. }
  174. )
  175. })
  176. },
  177. //撤回
  178. goBack() {
  179. this.context.draw()
  180. this.history.pop();
  181. this.history.forEach((item, index) => {
  182. let {color,width} = item.style
  183. this.context.beginPath()
  184. this.context.setLineCap('round')
  185. this.context.setStrokeStyle(color)
  186. this.context.setLineWidth(width)
  187. if (item.coordinates.length >= 2) {
  188. item.coordinates.forEach(it => {
  189. if (it.type == 'touchstart') {
  190. this.context.moveTo(it.x, it.y)
  191. } else {
  192. this.context.lineTo(it.x, it.y)
  193. }
  194. })
  195. } else {
  196. const point = item.coordinates[0]
  197. this.context.moveTo(point.x, point.y)
  198. this.context.lineTo(point.x + 1, point.y)
  199. }
  200. this.context.stroke()
  201. })
  202. this.context.draw(true)
  203. },
  204. //清空画布
  205. clear() {
  206. this.history = [];
  207. this.context.draw()
  208. },
  209. canvasStart(event) {
  210. let {x,y} = event.touches[0]
  211. this.history.push({
  212. style: {
  213. color: this.lineColor,
  214. width: this.lineWidth
  215. },
  216. coordinates: [
  217. {
  218. type: event.type,
  219. x: x,
  220. y: y
  221. }
  222. ]
  223. })
  224. this.drawGraphics()
  225. },
  226. canvasMove(e) {
  227. // e.preventDefault()
  228. let {x,y} = e.touches[0]
  229. this.history[this.history.length - 1].coordinates.push({
  230. type: e.type,
  231. x: x,
  232. y: y
  233. })
  234. this.drawGraphics()
  235. },
  236. //绘制
  237. drawGraphics() {
  238. let historyLen = this.history.length
  239. if (!historyLen) return
  240. let currentData = this.history[historyLen - 1]
  241. let coordinates = currentData.coordinates
  242. if (!coordinates.length) return
  243. let startPoint,endPoint;
  244. if (coordinates.length < 2) {
  245. startPoint = coordinates[coordinates.length - 1]
  246. endPoint = {
  247. x: startPoint.x + 1,
  248. y: startPoint.y
  249. }
  250. } else {
  251. startPoint = coordinates[coordinates.length - 2]
  252. endPoint = coordinates[coordinates.length - 1]
  253. }
  254. let style = currentData.style
  255. this.context.beginPath()
  256. this.context.setLineCap('round')
  257. this.context.setStrokeStyle(style.color)
  258. this.context.setLineWidth(style.width)
  259. this.context.moveTo(startPoint.x, startPoint.y)
  260. this.context.lineTo(endPoint.x, endPoint.y)
  261. this.context.stroke()
  262. this.context.draw(true)
  263. }
  264. }
  265. }
  266. </script>
  267. <style lang="scss" scoped>
  268. .autograph{
  269. width: 100%;
  270. height:100%;
  271. box-sizing: border-box;
  272. // border:1px dashed #ccc;
  273. }
  274. .horizontalScreen{
  275. left: -150upx !important;
  276. bottom: 0 !important;
  277. right: auto !important;
  278. transform: rotate(90deg);
  279. transform-origin:bottom right;
  280. }
  281. .rote-text{
  282. transform:rotate(90deg);
  283. }
  284. .action-box{
  285. position: absolute;
  286. bottom: 0;
  287. right: 0;
  288. z-index: 50;
  289. display: flex;
  290. flex-direction: column;
  291. align-items: flex-end;
  292. }
  293. .th-submit{
  294. width: 150upx;
  295. height: 100upx;
  296. background-color: #4169E1;
  297. border-radius: 70upx 0 0 0;
  298. display: flex;
  299. align-items: center;
  300. justify-content: center;
  301. color: #FFFFFF;
  302. font-size: 30upx;
  303. }
  304. .action-bar{
  305. margin-bottom: 30upx;
  306. margin-right: 35upx;
  307. image{
  308. width: 40upx;
  309. height: 40upx;
  310. }
  311. >image{
  312. transition: all 0.3s;
  313. }
  314. >view{
  315. display: flex;
  316. flex-direction: column;
  317. image{
  318. width: 40upx;
  319. height: 40upx;
  320. margin-bottom: 52upx;
  321. }
  322. }
  323. }
  324. .autograph-box{
  325. width: 100%;
  326. height: 100%;
  327. position: relative;
  328. .default-text{
  329. width: 100%;
  330. height: 100%;
  331. position: absolute;
  332. top: 0;
  333. left: 0;
  334. display: flex;
  335. align-items: center;
  336. z-index: -1;
  337. justify-content: center;
  338. font-size: 38upx;
  339. color: #C0C0C0;
  340. letter-spacing: 5upx;
  341. }
  342. }
  343. .roteRight{
  344. transform: rotate(136deg);
  345. }
  346. .roteLeft{
  347. transform: rotate(0deg);
  348. }
  349. .action-open{
  350. animation:bounceIn 1s;
  351. }
  352. .action-close{
  353. animation:bounceOut 0.5s forwards;
  354. }
  355. @keyframes bounceIn {
  356. 0%,
  357. 20%,
  358. 40%,
  359. 60%,
  360. 80%,
  361. to {
  362. -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
  363. animation-timing-function: cubic-bezier(.215, .61, .355, 1)
  364. }
  365. 0% {
  366. opacity: 0;
  367. -webkit-transform: scale3d(.3, .3, .3);
  368. transform: scale3d(.3, .3, .3)
  369. }
  370. 20% {
  371. -webkit-transform: scale3d(1.1, 1.1, 1.1);
  372. transform: scale3d(1.1, 1.1, 1.1)
  373. }
  374. 40% {
  375. -webkit-transform: scale3d(.9, .9, .9);
  376. transform: scale3d(.9, .9, .9)
  377. }
  378. 60% {
  379. opacity: 1;
  380. -webkit-transform: scale3d(1.03, 1.03, 1.03);
  381. transform: scale3d(1.03, 1.03, 1.03)
  382. }
  383. 80% {
  384. -webkit-transform: scale3d(.97, .97, .97);
  385. transform: scale3d(.97, .97, .97)
  386. }
  387. to {
  388. opacity: 1;
  389. -webkit-transform: scaleX(1);
  390. transform: scaleX(1)
  391. }
  392. }
  393. @keyframes bounceOut {
  394. 20% {
  395. -webkit-transform: scale3d(.9, .9, .9);
  396. transform: scale3d(.9, .9, .9)
  397. }
  398. 50%,
  399. 55% {
  400. opacity: 1;
  401. -webkit-transform: scale3d(1.1, 1.1, 1.1);
  402. transform: scale3d(1.1, 1.1, 1.1)
  403. }
  404. to {
  405. opacity: 0;
  406. -webkit-transform: scale3d(.3, .3, .3);
  407. transform: scale3d(.3, .3, .3);
  408. display: none;
  409. }
  410. }
  411. </style>