Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

517 rindas
12 KiB

  1. <template>
  2. <view class="u-signature">
  3. <view class="u-signature__canvas-wrap" :style="{background: resolvedBgColor}">
  4. <up-canvas
  5. ref="signatureCanvas"
  6. :canvas-id="canvasId"
  7. :width="canvasWidth"
  8. :height="canvasHeight"
  9. :bg-color="resolvedBgColor"
  10. @touchstart="touchStart"
  11. @touchmove="touchMove"
  12. @touchend="touchEnd"
  13. :disable-scroll="true"
  14. class="u-signature__canvas"
  15. :style="{
  16. width: canvasWidth + 'px',
  17. height: canvasHeight + 'px',
  18. }">
  19. </up-canvas>
  20. </view>
  21. <view v-if="showToolbar" class="u-signature__toolbar">
  22. <view class="u-signature__toolbar-icons u-flex u-flex-x">
  23. <view class="u-signature__toolbar-icon" @click="undo">
  24. <up-icon name="arrow-left" size="22" :color="pathStack.length === 0 ? iconDisabledColor : iconDefaultColor"></up-icon>
  25. </view>
  26. <view class="u-signature__toolbar-icon" @click="clear">
  27. <up-icon name="trash" size="25" :color="iconDefaultColor"></up-icon>
  28. </view>
  29. <view class="u-signature__toolbar-icon" @click="toggleBrushSettings">
  30. <up-icon name="edit-pen" size="25" :color="iconDefaultColor"></up-icon>
  31. </view>
  32. <view class="u-signature__toolbar-icon" @click="toggleColorSettings">
  33. <up-icon name="grid" size="24" :color="iconDefaultColor"></up-icon>
  34. </view>
  35. <view class="u-signature__toolbar-icon" @click="exportSignature">
  36. <up-icon name="checkmark" size="25" :color="isEmpty ? iconDisabledColor : iconDefaultColor"></up-icon>
  37. </view>
  38. </view>
  39. <!-- 笔画设置 -->
  40. <view v-if="showBrushSettings" class="u-signature__brush-settings">
  41. <view class="u-signature__progress">
  42. <text class="u-signature__progress-label">{{ t("up.signature.penSize") }}:</text>
  43. <up-slider
  44. v-model="lineWidth"
  45. :min="1"
  46. :max="20"
  47. :step="1"
  48. @show-value="true"
  49. :value-show="(lineWidth)"
  50. ></up-slider>
  51. </view>
  52. </view>
  53. <!-- 颜色设置 -->
  54. <view v-if="showColorSettings" class="u-signature__color-settings">
  55. <view class="u-signature__color-picker">
  56. <text class="u-signature__color-label">{{ t("up.signature.penColor") }}:</text>
  57. <view class="u-signature__colors">
  58. <view
  59. v-for="(color, index) in presetColors"
  60. :key="index"
  61. class="u-signature__color-item"
  62. :class="{'u-signature__color-item--active': lineColor === color}"
  63. :style="{ backgroundColor: color }"
  64. @click="selectColor(color)"
  65. ></view>
  66. </view>
  67. </view>
  68. </view>
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. import { t } from '../../libs/i18n'
  74. export default {
  75. name: 'u-signature',
  76. props: {
  77. // 画布宽度
  78. width: {
  79. type: [String, Number],
  80. default: 300
  81. },
  82. // 画布高度
  83. height: {
  84. type: [String, Number],
  85. default: 200
  86. },
  87. // 背景颜色
  88. bgColor: {
  89. type: String,
  90. default: '#ffffff'
  91. },
  92. // 默认笔画颜色
  93. color: {
  94. type: String,
  95. default: '#000000'
  96. },
  97. // 默认笔画粗细
  98. thickness: {
  99. type: [String, Number],
  100. default: 3
  101. },
  102. // 是否显示工具栏
  103. showToolbar: {
  104. type: Boolean,
  105. default: true
  106. }
  107. },
  108. data() {
  109. return {
  110. canvasId: 'u-signature-' + Math.random().toString(36).substr(2, 9),
  111. canvasWidth: 300,
  112. canvasHeight: 200,
  113. lineColor: '',
  114. lineWidth: 3,
  115. isDrawing: false,
  116. pathStack: [], // 存储绘制路径用于回退
  117. currentPath: [], // 当前绘制路径
  118. isEmpty: true,
  119. presetColors: [
  120. '#000000', // 黑色
  121. '#ff0000', // 红色
  122. '#00ff00', // 绿色
  123. '#0000ff', // 蓝色
  124. '#ffff00', // 黄色
  125. '#00ffff', // 青色
  126. '#ff00ff', // 紫色
  127. '#ffffff' // 白色
  128. ],
  129. showBrushSettings: false,
  130. showColorSettings: false,
  131. lastPoint: null, // 保存上一个点的坐标
  132. canvasInstance: null // 缓存canvas实例
  133. }
  134. },
  135. computed: {
  136. resolvedBgColor() {
  137. return this.bgColor === '#ffffff'
  138. ? (this.upThemeIsDark ? '#1c1c1e' : '#ffffff')
  139. : this.bgColor
  140. },
  141. iconDefaultColor() {
  142. return this.upThemeVar('--up-content-color', '#999999')
  143. },
  144. iconDisabledColor() {
  145. return this.upThemeVar('--up-disabled-color', '#c8c9cc')
  146. }
  147. },
  148. mounted() {
  149. // 初始化时获取canvas实例
  150. this.$nextTick(() => {
  151. this.getCanvasInstance();
  152. this.clearCanvas();
  153. });
  154. },
  155. watch: {
  156. width: {
  157. handler(newVal) {
  158. this.canvasWidth = Number(newVal)
  159. },
  160. immediate: true
  161. },
  162. height: {
  163. handler(newVal) {
  164. this.canvasHeight = Number(newVal)
  165. },
  166. immediate: true
  167. },
  168. color: {
  169. handler(newVal) {
  170. this.lineColor = this.resolveStrokeColor(newVal)
  171. },
  172. immediate: true
  173. },
  174. thickness: {
  175. handler(newVal) {
  176. this.lineWidth = Number(newVal)
  177. },
  178. immediate: true
  179. }
  180. },
  181. methods: {
  182. t,
  183. resolveStrokeColor(color) {
  184. if (color === '#000000') {
  185. return this.upThemeIsDark ? '#f5f5f5' : '#000000'
  186. }
  187. return color
  188. },
  189. // 获取签名画布实例
  190. getCanvasInstance() {
  191. if (this.canvasInstance) {
  192. return this.canvasInstance;
  193. }
  194. const canvasRef = this.$refs.signatureCanvas;
  195. if (canvasRef) {
  196. this.canvasInstance = canvasRef;
  197. return canvasRef;
  198. }
  199. return null;
  200. },
  201. touchStart(e) {
  202. if (!this.canvasInstance || !this.canvasInstance.ctx) {
  203. this.getCanvasInstance();
  204. }
  205. if (!this.canvasInstance || !this.canvasInstance.ctx) return;
  206. this.isDrawing = true;
  207. this.isEmpty = false;
  208. this.currentPath = [];
  209. const { x, y } = this.getCanvasPoint(e);
  210. // 设置线条样式
  211. this.canvasInstance.setLineStyle(this.lineColor, this.lineWidth);
  212. // 开始路径
  213. this.canvasInstance.beginPath();
  214. this.canvasInstance.moveTo(x, y);
  215. // 记录起始点
  216. this.currentPath.push({
  217. x,
  218. y,
  219. type: 'start',
  220. color: this.lineColor,
  221. width: this.lineWidth
  222. });
  223. // 保存上一个点
  224. this.lastPoint = { x, y };
  225. // 阻止默认事件以提高性能
  226. e.preventDefault();
  227. },
  228. touchMove(e) {
  229. if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
  230. // 阻止默认事件以提高性能
  231. e.preventDefault();
  232. const { x, y } = this.getCanvasPoint(e);
  233. // 从上一个点画线到当前点
  234. this.canvasInstance.lineTo(x, y);
  235. this.canvasInstance.stroke(); // 实时绘制当前线段
  236. this.currentPath.push({
  237. x,
  238. y,
  239. type: 'move'
  240. });
  241. this.canvasInstance.draw(false);
  242. // 更新上一个点
  243. this.lastPoint = { x, y };
  244. },
  245. touchEnd(e) {
  246. if (!this.isDrawing || !this.canvasInstance || !this.canvasInstance.ctx) return;
  247. this.isDrawing = false;
  248. this.canvasInstance.closePath();
  249. this.lastPoint = null;
  250. // 将当前路径加入栈中用于回退
  251. if (this.currentPath.length > 0) {
  252. this.pathStack.push([...this.currentPath]);
  253. }
  254. // 最后统一执行一次绘制
  255. this.canvasInstance.draw(true);
  256. },
  257. // 同步获取canvas坐标点(兼容处理)
  258. getCanvasPoint(e) {
  259. // #ifdef MP-WEIXIN
  260. const touch = e.touches && e.touches[0] ? e.touches[0] : e.mp.touches[0];
  261. // #endif
  262. // #ifndef MP-WEIXIN
  263. const touch = e.touches[0];
  264. // #endif
  265. // 计算相对于canvas的坐标
  266. // 由于无法直接获取canvas位置,这里简化处理
  267. // 实际应用中可能需要通过uni.createSelectorQuery获取canvas位置
  268. return {
  269. x: touch.x,
  270. y: touch.y
  271. };
  272. },
  273. // 选择颜色
  274. selectColor(color) {
  275. this.lineColor = color
  276. },
  277. // 回退操作
  278. undo() {
  279. if (this.pathStack.length === 0) return
  280. // 弹出最后一个路径
  281. this.pathStack.pop()
  282. // 重新绘制
  283. this.redraw()
  284. },
  285. // 重新绘制所有路径
  286. redraw() {
  287. if (!this.canvasInstance) {
  288. this.getCanvasInstance();
  289. }
  290. if (!this.canvasInstance) return;
  291. // 先清空画布
  292. this.canvasInstance.clearCanvas();
  293. if (this.pathStack.length === 0) {
  294. this.isEmpty = true;
  295. return;
  296. }
  297. this.isEmpty = false;
  298. // 逐个绘制路径
  299. this.pathStack.forEach(path => {
  300. if (path.length === 0) return;
  301. this.canvasInstance.beginPath();
  302. let lastPoint = null;
  303. path.forEach((point, index) => {
  304. if (index === 0 && point.type === 'start') {
  305. // 设置线条样式
  306. this.canvasInstance.setLineStyle(point.color, point.width);
  307. this.canvasInstance.moveTo(point.x, point.y);
  308. lastPoint = { x: point.x, y: point.y };
  309. } else if (point.type === 'move') {
  310. this.canvasInstance.lineTo(point.x, point.y);
  311. lastPoint = { x: point.x, y: point.y };
  312. }
  313. });
  314. this.canvasInstance.stroke();
  315. this.canvasInstance.draw(true);
  316. });
  317. },
  318. // 清空画布内容
  319. clearCanvas() {
  320. if (!this.canvasInstance) {
  321. this.getCanvasInstance();
  322. }
  323. if (!this.canvasInstance) return;
  324. this.canvasInstance.clearCanvas();
  325. },
  326. // 对外暴露的清空方法(供工具栏与ref调用)
  327. clear() {
  328. this.pathStack = []
  329. this.currentPath = []
  330. this.lastPoint = null
  331. this.isDrawing = false
  332. this.isEmpty = true
  333. this.clearCanvas()
  334. this.$emit('clear')
  335. },
  336. // 导出签名图片
  337. async exportSignature() {
  338. if (this.isEmpty) {
  339. console.warn('签名为空,无法导出');
  340. return;
  341. }
  342. if (!this.canvasInstance) {
  343. this.getCanvasInstance();
  344. }
  345. if (!this.canvasInstance) {
  346. console.error('无法获取画布实例');
  347. return;
  348. }
  349. try {
  350. // 先重绘整个签名内容
  351. this.redraw();
  352. // 导出图片
  353. const imagePath = await this.canvasInstance.exportImage('png', 1);
  354. this.$emit('confirm', imagePath);
  355. } catch (error) {
  356. console.error('导出签名图片失败:', error);
  357. this.$emit('error', error);
  358. }
  359. },
  360. // 切换笔画设置显示
  361. toggleBrushSettings() {
  362. this.showBrushSettings = !this.showBrushSettings;
  363. if (this.showBrushSettings) {
  364. this.showColorSettings = false;
  365. }
  366. },
  367. // 切换颜色设置显示
  368. toggleColorSettings() {
  369. this.showColorSettings = !this.showColorSettings;
  370. if (this.showColorSettings) {
  371. this.showBrushSettings = false;
  372. }
  373. },
  374. }
  375. }
  376. </script>
  377. <style lang="scss" scoped>
  378. .u-signature {
  379. display: flex;
  380. flex-direction: column;
  381. &__canvas-wrap {
  382. border: 1px solid var(--up-border-color, #e0e0e0);
  383. border-radius: 4px;
  384. overflow: hidden;
  385. }
  386. &__canvas {
  387. width: 100%;
  388. height: 100%;
  389. }
  390. &__toolbar {
  391. margin-top: 5px;
  392. background-color: var(--up-card-bg-color, #fff);
  393. }
  394. &__toolbar-icons {
  395. display: flex;
  396. justify-content: space-between;
  397. align-items: center;
  398. padding: 1px 0;
  399. // border: 1px solid #e0e0e0;
  400. border-radius: 4px;
  401. }
  402. &__toolbar-icon {
  403. padding: 5px;
  404. }
  405. &__brush-settings,
  406. &__color-settings {
  407. margin-top: 15px;
  408. padding: 1px;
  409. // border: 1px solid #e0e0e0;
  410. border-radius: 4px;
  411. }
  412. &__progress {
  413. &-label {
  414. display: block;
  415. margin-bottom: 10px;
  416. font-size: 14px;
  417. color: var(--up-tips-color, #999);
  418. }
  419. }
  420. &__color-picker {
  421. margin-bottom: 10px;
  422. }
  423. &__color-label {
  424. display: block;
  425. margin-bottom: 10px;
  426. font-size: 14px;
  427. color: var(--up-tips-color, #999);
  428. }
  429. &__colors {
  430. display: flex;
  431. flex-direction: row;
  432. flex-wrap: wrap;
  433. gap: 10px;
  434. }
  435. &__color-item {
  436. width: 30px;
  437. height: 30px;
  438. border-radius: 50%;
  439. border: 2px solid var(--up-border-color, #f0f0f0);
  440. cursor: pointer;
  441. &--active {
  442. border-color: var(--up-primary, #2979ff);
  443. transform: scale(1.1);
  444. }
  445. }
  446. &__actions {
  447. display: flex;
  448. flex-direction: row;
  449. gap: 10px;
  450. justify-content: center;
  451. }
  452. }
  453. </style>