This commit is contained in:
2023-10-12 15:03:29 +08:00
parent 8e8da1c446
commit 59471e0f1a
8 changed files with 728 additions and 5 deletions

View File

@ -0,0 +1,52 @@
<template>
<view class="signature" style="background-color: #F4F5F7;height: 800rpx;">
<Signature @init="onSignInit" style='background-color: #fff;height: 700rpx;width: 100%;'></Signature>
<view class="btns">
<button @click="clear">清空</button>
<button @click="revoke">撤回</button>
<button @click="saveTempFilePath">保存</button>
</view>
</view>
</template>
<script>
import Signature from '@/components/v-sign/v-sign.vue'
export default {
components: {
Signature
},
methods: {
onSignInit(signCtx) {
this.signCtx = signCtx
},
//
clear() {
this.signCtx.clear()
},
//
revoke() {
this.signCtx.revoke()
},
// h5 base64
async saveTempFilePath() {
const res = await this.signCtx.canvasToTempFilePath()
this.$emit('userSignaturePictureUrl', res)
},
}
}
</script>
<style lang='scss'>
.signature {
height: 800rpx;
position: relative;
}
.btns {
margin-top: 50rpx;
display: flex;
}
button {
width: 30%;
}
</style>

View File

@ -0,0 +1,15 @@
/**
* 判断是否未数值
* @param {Object} val
*/
export function isNumber(val) {
return !isNaN(Number(val))
}
/**
* 处理大小单位
* @param {Object} val
*/
export function formatSize(val) {
return isNumber(val) ? `${val}rpx` : val
}

View File

@ -0,0 +1,162 @@
<template>
<view class="v-sign-action" :style="[customStyle]">
<view
v-for="item in btns"
:key="item.label"
:class="['btn', { border: border }]"
:style="[{ 'margin-right': formatSize(space) }]"
@click="onBtnClick(item)"
>
<image :class="['icon', 'icon-' + item.action]" :src="item.icon"></image>
<text class="text">{{ item.label }}</text>
</view>
</view>
</template>
<script>
/**
* v-sign-action 控制按钮组v-sign 子组件
* @description 控制 v-sign 组件的一些按钮
* @tutorial
* @property {Array} actions 按钮配置 所有值 清空clear, 撤回prev 保存图片save
* @property {Boolean} border 按钮是否有边框
* @property {String/Number} space 按钮间隔
* @property {Object} customStyle 根元素自定义样式
* @event {Function} 点击对应类型按钮触发对应事件 例如点击 clear 则触发 clear 事件
* @example 示例
**/
import { formatSize } from './utils'
// v-sign
let vSignInterface
//
const btn_type = {
CLEAR: 'clear', //
PREV: 'prev', // /
// NEXT: 'next',
SAVE: 'save' //
}
const all_action = Object.values(btn_type)
const btnsConf = [
{
label: '清空',
action: btn_type.CLEAR,
icon: '/static/v-sign/clear.png'
},
{
label: '撤回',
action: btn_type.PREV,
icon: '/static/v-sign/prev.png'
},
// {
// label: '',
// action: btn_type.NEXT,
// icon: '/static/v-sign/next.png'
// },
{
label: '保存',
action: btn_type.SAVE,
icon: '/static/v-sign/save.png'
}
]
export default {
name: 'v-sign-action',
props: {
//
actions: {
type: Array,
default: () => all_action
},
//
border: {
type: Boolean,
default: true
},
//
space: {
type: [String, Number],
default: 12
},
//
customStyle: {
type: Object,
default: () => ({})
}
},
inject: ['getInterface'],
data() {
return {
formatSize
}
},
computed: {
btns() {
return btnsConf.filter(item => this.actions.includes(item.action))
}
},
mounted() {
vSignInterface = this.getInterface()
},
methods: {
async onBtnClick(btn) {
// console.log(btn, btn.action)
let emit_result
switch (btn.action) {
case btn_type.CLEAR:
vSignInterface.clear()
break
case btn_type.PREV:
vSignInterface.revoke()
break
// case btn_type.NEXT:
// console.log('next')
// break
case btn_type.SAVE:
emit_result = await vSignInterface.canvasToTempFilePath()
break
default:
break
}
// console.log(btn.action, emit_result);
//
this.$emit(btn.action, emit_result)
}
}
}
</script>
<style lang="scss" scoped>
.v-sign-action {
display: flex;
flex-wrap: wrap;
.btn {
display: flex;
align-items: center;
padding: 0 12rpx;
min-width: 88rpx;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.border {
border: 2rpx solid #666;
border-radius: 12rpx;
}
.icon {
width: 28rpx;
height: 28rpx;
&.icon-clear,
&.icon-prev,
&.icon-next {
margin-right: 4rpx;
}
&.icon-save {
}
}
.text {
color: #666;
font-size: 28rpx;
}
}
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<view class="v-sign-pen">
<view class="label" v-if="label">{{ label }}</view>
<view class="options">
<view
class="opt-item"
:style="{
minHeight: minWrapHeight,
marginRight: space + 'rpx'
}"
v-for="item in csizes"
:key="item.size"
@click="onItemClick(item)"
>
<view
:class="type"
:style="{
border:
border && currentSelect.size === item.size
? `${borderWidth}rpx solid ${activeColor}`
: ''
}"
>
<view class="inner" :style="[defaultInnerStyle(item)]"></view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* v-sign-pen 画笔v-sign 子组件
* @description 控制 v-sign 画笔的线宽
* @tutorial
* @property {String} type 选项样式 line / circle
* @property {String} label 标签
* @property {Array} sizes 画笔大小数组单位 px
* @property {String} color 选项颜色
* @property {String} activeColor 选中项颜色
* @property {Boolean} border 选中项是否有边框
* @property {Number} borderWidth 边框大小单位 rpx
* @property {String} space 选项间隙单位 rpx
* @property {Number} bigger 圆点变大变粗倍数
* @property {Number} minSize 圆点最小大小单位 px
* @event {Function} change 选择画笔大小时触发
* @example
**/
// v-sign
let vSignInterface
//
const type_style = {
CIRCLE: 'circle',
LINE: 'line'
}
export default {
name: 'v-sign-pen',
props: {
//
type: {
type: String,
default: type_style.CIRCLE
},
label: {
type: String
},
// px
sizes: {
type: Array,
default: () => [2, 4, 6, 8, 10]
},
//
color: {
type: String,
default: '#333'
},
//
activeColor: {
type: String,
default: '#333'
},
//
border: {
type: Boolean,
default: true
},
// , rpx
borderWidth: {
type: Number,
default: 4
},
// , rpx
space: {
type: Number,
default: 20
},
//
bigger: {
type: Number,
default: 2
},
// px
minSize: {
type: Number,
default: 4
}
},
inject: ['getInterface'],
data() {
return {
type_style,
currentSelect: null,
csizes: [],
maxSize: 0,
maxCsize: 0
}
},
computed: {
minWrapHeight() {
let height
switch (this.type) {
case type_style.CIRCLE:
height = this.maxCsize + 10 + 'px'
break
case type_style.LINE:
height = this.maxSize + 4 + 'px'
break
}
return height
}
},
created() {
this.csizes = this.sizes.map((size, index) => {
const csize = (index + 1) * this.bigger + this.minSize
this.maxSize = csize > this.maxSize ? csize : this.maxSize
this.maxCsize = csize > this.maxCsize ? csize : this.maxCsize
return {
size,
csize
}
})
this.currentSelect = this.csizes[0]
},
mounted() {
vSignInterface = this.getInterface()
this.setLineWidth()
},
methods: {
onItemClick(opt) {
this.currentSelect = opt
this.setLineWidth()
this.$emit('change', opt.size)
},
setLineWidth() {
vSignInterface.setLineWidth(this.currentSelect.size)
},
defaultInnerStyle(item) {
let width
let height
switch (this.type) {
case type_style.CIRCLE:
width = `${item.csize}px`
height = `${item.csize}px`
break
case type_style.LINE:
width = '20px'
height = `${item.size}px`
break
}
const background = this.currentSelect.size === item.size ? this.activeColor : this.color
return {
width,
height,
background
}
}
}
}
</script>
<style lang="scss" scoped>
.v-sign-pen {
padding: 12rpx;
.label {
font-size: 28rpx;
color: #333;
}
.options {
display: flex;
align-items: flex-end;
.opt-item {
display: flex;
align-items: flex-end;
justify-content: center;
&:last-child {
margin-right: 0;
}
.circle {
border-radius: 50%;
padding: 4rpx;
.inner {
border-radius: 50%;
}
}
.line {
padding: 4rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,238 @@
<template>
<view class="signature-wrap">
<canvas
:canvas-id="cid"
:id="cid"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
style="height:600rpx"
:style="[{ width: formatSize(width)}, customStyle]"
></canvas>
<slot />
</view>
</template>
<script>
/**
* sign canvas 手写签名
* @description 设置线条宽度颜色撤回清空
* @tutorial
* @property {String} cid canvas id 不设置则默认为 v-sign-时间戳
* @property {String, Number} width canvas 宽度
* @property {String, Number} height canvas 高度
* @property {Object} customStyle 自定义样式
* @property {String} lineColor 画笔颜色
* @property {Number} lineWidth 画笔大小权重大于 v-sign-pen 组件设置的画笔大小
* @event {Function} init 当创建完 canvas 实例后触发向外提供 canvas实例撤回清空方法
* @example <v-sign @init="signInit"></v-sign>
*/
import { formatSize } from './utils'
// convas
let canvasCtx
export default {
name: 'v-sign',
props: {
// canvas id
cid: {
type: String,
default: `v-sign-${Date.now()}`
// required: true
},
// canvas
width: {
type: [String, Number],
default: '100%'
},
// canvas
height: {
type: [String, Number],
default: 300
},
// v-sign-pen
lineWidth: {
type: Number
},
// 线
lineColor: {
type: String,
default: '#000'
},
// canvas
customStyle: {
type: Object,
default: () => ({})
}
},
provide() {
return {
getInterface: this.provideInterface
}
},
data() {
return {
formatSize,
lineData: [],
winWidth: 0,
winHeight: 0,
penLineWidth: null, // v-sign-pen
}
},
mounted() {
canvasCtx = uni.createCanvasContext(this.cid, this)
//
this.$emit('init', this.provideInterface())
//
uni.getSystemInfo({
success: res => {
this.winWidth = res.windowWidth
this.winHeight = res.windowHeight
}
})
},
methods: {
onTouchStart(e) {
const pos = e.touches[0]
this.lineData.push({
style: {
color: this.lineColor,
width: this.lineWidth || this.penLineWidth || 4
},
//
coordinates: [
{
type: e.type,
x: pos.x,
y: pos.y
}
]
})
this.drawLine()
},
onTouchMove(e) {
const pos = e.touches[0]
this.lineData[this.lineData.length - 1].coordinates.push({
type: e.type,
x: pos.x,
y: pos.y
})
this.drawLine()
},
onTouchEnd(e) {
// console.log(e.type, e)
},
//
clear() {
this.lineData = []
canvasCtx.clearRect(0, 0, this.winWidth, this.winHeight)
canvasCtx.draw()
},
//
revoke() {
this.lineData.pop()
this.lineData.forEach((item, index) => {
canvasCtx.beginPath()
canvasCtx.setLineCap('round')
canvasCtx.setStrokeStyle(item.style.color)
canvasCtx.setLineWidth(item.style.width)
item.coordinates.forEach(pos => {
if (pos.type == 'touchstart') {
canvasCtx.moveTo(pos.x, pos.y)
} else {
canvasCtx.lineTo(pos.x, pos.y)
}
})
canvasCtx.stroke()
})
canvasCtx.draw()
},
// 线
drawLine() {
const lineDataLen = this.lineData.length
if (!lineDataLen) return
const currentLineData = this.lineData[lineDataLen - 1]
const coordinates = currentLineData.coordinates
const coordinatesLen = coordinates.length
if (!coordinatesLen) return
let startPos
let endPos
if (coordinatesLen < 2) {
// only start, no move event
startPos = coordinates[coordinatesLen - 1]
endPos = { x: startPos.x + 1, y: startPos.y }
} else {
startPos = coordinates[coordinatesLen - 2]
endPos = coordinates[coordinatesLen - 1]
}
const style = currentLineData.style
canvasCtx.beginPath()
canvasCtx.setLineCap('round')
canvasCtx.setStrokeStyle(style.color)
canvasCtx.setLineWidth(style.width)
canvasCtx.moveTo(startPos.x, startPos.y)
canvasCtx.lineTo(endPos.x, endPos.y)
// const P1 = this.caculateBezier(startPos, endPos, centerPos)
// console.log(P1.x, P1.y)
// canvasCtx.moveTo(startPos.x, startPos.y)
// canvasCtx.quadraticCurveTo(P1.x, P1.y, endPos.x, endPos.y)
canvasCtx.stroke()
canvasCtx.draw(true)
},
canvasToTempFilePath(conf = {}) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: this.cid,
...conf,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
console.log('fail', err)
reject(err)
}
},
this
)
})
},
setLineWidth(numberVal) {
this.penLineWidth = numberVal
},
provideInterface() {
return {
cid: this.cid,
ctx: canvasCtx,
clear: this.clear,
revoke: this.revoke,
canvasToTempFilePath: this.canvasToTempFilePath,
setLineWidth: this.setLineWidth
}
},
/**
* 计算二次贝塞尔曲线 控制点 P1
* 起点 P0(x0,y0)控制点P1(x1, y1)P2(x2, y2)曲线上任意点B(x, y)
* 二次贝塞尔公式B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
* 代入坐标得
* x = (1-t)²*x0 + 2t(1-t)*x1 + *x2
* y = (1-t)²*y0 + 2t(1-t)*y1 + *y2
*/
caculateBezier(P0, P2, B, t = 0.5) {
const { x: x0, y: y0 } = P0
const { x: x2, y: y2 } = P2
const { x, y } = B
let x1 = (x - (1 - t) * (1 - t) * x0 - t * t * x2) / (2 * t * (1 - t))
let y1 = (y - (1 - t) * (1 - t) * y0 - t * t * y2) / (2 * t * (1 - t))
return { x: x1, y: y1 }
}
}
}
</script>
<style lang="scss" scoped>
.signature-wrap {
position: relative;
}
</style>

View File

@ -103,7 +103,11 @@
<image src="../../static/huijiantou.png" mode=""></image>
</view>
</view>
<view class="signing">
<view class="name">
签约周期()
<u-input v-model="query.signYears" type='number' :border="true" placeholder="请输入签约周期(单位年)" />
</view>
<view class=" signing">
<view class="agency">
签约提醒:
</view>
@ -200,12 +204,19 @@
crowdNoList: null,
packageList: [],
packageNoList: null,
signYears: null,
}
}
},
watch: {
// 'query.signYears': {
// handler(newVal, oldVal) {},
// }
},
methods: {
updata() {
if (this.radio == 2) {
if (!this.query.signYears) this.query.signYears = 1
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;

View File

@ -1,21 +1,40 @@
<template>
<view class="app">
<u-parse :html="content"></u-parse>
<!-- <u-parse :html="content"></u-parse> -->
<view class="" @tap='show =true'>
111111111111111
</view>
<u-mask :show="show" @click="show = false">
<view style="position:absolute;bottom:0;height:900rpx;width:100%;background-color: #fff;" v-if='show'>
<signature @userSignaturePictureUrl='userSignaturePicture' @click.native.stop
style='background-color: #F4F5F7;width: 100%;height: 900rpx;'></signature>
</view>
</u-mask>
</view>
</template>
<script>
import signature from '@/components/signature/signature.vue'
import {
getContent
} from '@/api/pagesC/contractsigningprotocol/index.js'
export default {
components: {
signature
},
data() {
return {
orgNo: null,
content: null,
show: false,
};
},
methods: {
//
userSignaturePicture(data) {
console.log(data)
this.show = false
},
info() {
getContent(this.orgNo, '1').then(res => {
this.content = res.data.content
@ -23,7 +42,6 @@
},
},
onLoad(options) {
console.log(options)
this.orgNo = options.orgNo
this.info();
},
@ -33,5 +51,6 @@
<style lang="scss">
.app {
margin-top: 100rpx;
padding: 0 40rpx;
}
</style>

View File

@ -103,7 +103,22 @@
that.query.lng = resp.longitude
that.getNearbyOrginfo()
},
fail: function(err) {},
fail: function(err) {
// uni.openSetting({
// success(res) {
// if (res.authSetting['scope.userLocation']) {
// uni.getLocation({
// type: 'wgs84',
// success: function(resp) {
// that.query.lat = resp.latitude
// that.query.lng = resp.longitude
// that.getNearbyOrginfo()
// },
// });
// }
// }
// });
},
});
},
onReachBottom() { //
@ -277,4 +292,4 @@
::v-deep .u-cell-box {}
}
}
</style>
</style>