大屏与公共页面
This commit is contained in:
parent
53bd29677b
commit
4af02c6599
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<!-- 全局进度条组件 -->
|
||||||
|
<ProgressBar />
|
||||||
|
<router-view v-if="isReady"></router-view>
|
||||||
|
<div v-else class="loading-container">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">路由加载中...</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
|
import { useRouteStore } from '@/stores/modules/router'
|
||||||
|
import { addDynamicRoutes } from '@/router'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
|
||||||
|
const isReady = ref(false)
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const routerStore = useRouteStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('开始初始化应用...')
|
||||||
|
|
||||||
|
// 初始化用户信息
|
||||||
|
await userStore.init()
|
||||||
|
|
||||||
|
// 确保路由已加载
|
||||||
|
// if (routerStore.defaultRoutes.length === 0) {
|
||||||
|
// routerStore.initDefaultRoutes()
|
||||||
|
// // 强制加载动态路由
|
||||||
|
// addDynamicRoutes()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 等待一些时间确保路由已完全加载
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 检查路由表是否已加载
|
||||||
|
console.log(
|
||||||
|
'当前路由表:',
|
||||||
|
router.getRoutes().map((r) => r.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 标记应用已准备好
|
||||||
|
await nextTick()
|
||||||
|
isReady.value = true
|
||||||
|
console.log('应用初始化完成,准备渲染路由视图')
|
||||||
|
|
||||||
|
// 跳转
|
||||||
|
router.push(router)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #409eff;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,180 @@
|
||||||
|
<template>
|
||||||
|
<el-card :class="mergeParameters.cardClass">
|
||||||
|
<div class="clearfix" style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span><span style="color: red;" v-if="mergeParameters.required">*</span>{{ mergeParameters.title }}</span>
|
||||||
|
<el-upload
|
||||||
|
ref="upload"
|
||||||
|
:show-file-list="false"
|
||||||
|
:action="mergeParameters.url"
|
||||||
|
:multiple="mergeParameters.multiple"
|
||||||
|
:auto-upload="mergeParameters.autoUpload"
|
||||||
|
:on-error="mergeFileActions.error"
|
||||||
|
:on-change="mergeFileActions.change"
|
||||||
|
:on-success="mergeFileActions.success"
|
||||||
|
:headers="mergeParameters.headers">
|
||||||
|
<el-button v-if="mergeParameters.mode !== 'detail' && mergeParameters.isShowBtn" slot="trigger" type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<font-awesome-icon icon="upload"/>
|
||||||
|
</template>
|
||||||
|
选择文件
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-table :data="tableData" border style="width: 100%; padding-top: 5px;">
|
||||||
|
<el-table-column fixed prop="name" :label="mergeParameters.tableTitle + '名称'"/>
|
||||||
|
<el-table-column v-if="mergeParameters.isShowType" fixed prop="type"
|
||||||
|
:label="mergeParameters.tableTitle + '文件类型'" width="200" align="center"></el-table-column>
|
||||||
|
<el-table-column v-if="mergeParameters.autoUpload" fixed prop="uploadStatus" width="100" label="上传状态"></el-table-column>
|
||||||
|
<el-table-column fixed="right" label="操作" width="120" align="center" class-name="control-column">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-space>
|
||||||
|
<el-link type="primary" v-if="isDowload(scope.row)" size="small" @click="downloadFile(scope.row)">
|
||||||
|
{{ isPicture(scope.row.name) ? '查看' : '下载' }}
|
||||||
|
</el-link>
|
||||||
|
<el-link type="primary" size="small" v-if="mergeParameters.mode !== 'detail'"
|
||||||
|
@click="deleteRow(scope.$index, tableData,scope.row)">
|
||||||
|
删除
|
||||||
|
</el-link>
|
||||||
|
</el-space>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<PictureView ref="pictureViewRef"/>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {computed, defineEmits, defineProps, reactive, ref} from "vue"
|
||||||
|
import {merge} from "lodash"
|
||||||
|
import {ElMessage} from "element-plus"
|
||||||
|
import PictureView from "@pages/common/PictureView.vue";
|
||||||
|
const pictureViewRef = ref(null)
|
||||||
|
const props = defineProps({
|
||||||
|
parameters: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
fileActions: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
filesData: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onError = (err, file, fileList) => { // 文件上传失败时的钩子
|
||||||
|
ElMessage.error('上传失败!' + err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRow = (index, fileList ,row) => { // 点击删除时的方法
|
||||||
|
const source = Object.assign([], fileList)
|
||||||
|
let id = fileList[index].id// 记录删除的ID
|
||||||
|
fileList.splice(index, 1)
|
||||||
|
emit('on-remove', fileList, id, {index: index, source: source},row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewPicture = (scope) => { // 预览图片
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPicture = (fileName) => { // 是否是图片
|
||||||
|
return /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDowload = (file) => { // 是否显示下载按钮
|
||||||
|
return mergeParameters.mode !== 'add' && file.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFile = (row) => { // 下载文件
|
||||||
|
if (row && row.downloadUrl) {
|
||||||
|
if (/(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(row.type) || /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(row.name)) {
|
||||||
|
pictureViewRef.value.viewPicture(row.downloadUrl)
|
||||||
|
} else {
|
||||||
|
window.open(row.downloadUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (file, fileList) => { // 文件上传时的钩子
|
||||||
|
emit('on-change', file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSuccess = (response, file, fileList) => { // 文件上传成功时的钩子
|
||||||
|
let data = response.data
|
||||||
|
data.bz = mergeParameters.autoUpload ? mergeParameters.title : fileMap[file.uid] ? fileMap[file.uid] : '' // 当自动上传时,上传文件备注默认为标题
|
||||||
|
data.fileType = mergeParameters.fileType
|
||||||
|
if (response.result === 'success') {
|
||||||
|
data.uploadStatus = '上传成功'
|
||||||
|
if (mergeParameters.autoUpload) { // 自动上传
|
||||||
|
emit('on-success', data, file)
|
||||||
|
} else { // 手动上传回调
|
||||||
|
data.uid = file.uid
|
||||||
|
emit('on-manual-success', data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传失败!:'+response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultParameters = {
|
||||||
|
headers: {},
|
||||||
|
url: null,
|
||||||
|
title: '文件上传',
|
||||||
|
tableTitle: '材料',
|
||||||
|
autoUpload: false,
|
||||||
|
required: true,
|
||||||
|
multiple: true,
|
||||||
|
mode: 'add',
|
||||||
|
isShowBtn: true,
|
||||||
|
isShowType: true,
|
||||||
|
fileType: '',
|
||||||
|
cardClass: 'box-card'
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFileActions = {
|
||||||
|
error: onError,
|
||||||
|
change: onChange,
|
||||||
|
success: onSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMap = reactive(merge({}))
|
||||||
|
const mergeParameters = reactive(merge({}, defaultParameters, props.parameters))
|
||||||
|
const mergeFileActions = reactive(merge({}, defaultFileActions, props.fileActions))
|
||||||
|
|
||||||
|
const emit = defineEmits(['on-remove', 'on-success', 'on-manual-success', 'update:actions'])
|
||||||
|
|
||||||
|
const mergeActions = reactive(merge({}, defaultFileActions, props.actions))
|
||||||
|
emit('update:actions', mergeActions)
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
let arr = []
|
||||||
|
for (let data of props.filesData) {
|
||||||
|
arr.push(data)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.el-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkFix {
|
||||||
|
.el-link {
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
padding-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
<template>
|
||||||
|
<div class="pic-div">
|
||||||
|
<el-upload v-if="mergeParameters.mode !== 'detail'"
|
||||||
|
:action="mergeParameters.url"
|
||||||
|
list-type="picture-card"
|
||||||
|
:multiple="mergeParameters.multiple"
|
||||||
|
:headers="mergeParameters.headers"
|
||||||
|
:auto-upload="mergeParameters.autoUpload"
|
||||||
|
:limit="mergeParameters.limit"
|
||||||
|
:on-preview="handlePictureCardPreview"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:on-change="handleChange"
|
||||||
|
:on-success="onSuccess"
|
||||||
|
:on-error="onError"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:file-list="pictureList"
|
||||||
|
accept="image/gif,image/jpg,image/jpeg,image/png,image/GIF,image/JPG">
|
||||||
|
<el-icon>
|
||||||
|
<Plus/>
|
||||||
|
</el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<el-upload v-if="mergeParameters.mode === 'detail'"
|
||||||
|
class="noUpOnly"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="mergeParameters.autoUpload"
|
||||||
|
:on-preview="handlePictureCardPreview"
|
||||||
|
:file-list="pictureList">
|
||||||
|
<template #file="{ file }">
|
||||||
|
<img style="width: 148px; height: 148px;" class="el-upload-list__item-thumbnail" :src="file.url">
|
||||||
|
<div v-if="file.name">
|
||||||
|
<el-tooltip effect="dark" :content="file.name">
|
||||||
|
<span class="el-upload-list__item-actions">
|
||||||
|
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||||
|
<el-icon><zoom-in/></el-icon>
|
||||||
|
</span>
|
||||||
|
<span class="el-upload-list__item-delete" @click="handleDownload(file)">
|
||||||
|
<el-icon><Download/></el-icon>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="el-upload-list__item-actions">
|
||||||
|
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||||
|
<el-icon><zoom-in/></el-icon>
|
||||||
|
</span>
|
||||||
|
<span class="el-upload-list__item-delete" @click="handleDownload(file)">
|
||||||
|
<el-icon><Download/></el-icon>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" append-to-body>
|
||||||
|
<img width="100%" :src="dialogImageUrl" alt="">
|
||||||
|
</el-dialog>
|
||||||
|
<PictureView ref="pictureViewRef"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {ElMessage} from "element-plus"
|
||||||
|
import {defineEmits, defineProps, reactive, toRefs, ref, watch, toRaw} from "vue"
|
||||||
|
import {merge} from "lodash"
|
||||||
|
import PictureView from "@pages/common/PictureView.vue"
|
||||||
|
import {Delete, Download, Plus, ZoomIn} from '@element-plus/icons-vue'
|
||||||
|
import {useUserStore} from "@/stores/modules/user.js"
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const props = defineProps({
|
||||||
|
parameters: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogImageUrl: '',
|
||||||
|
dialogVisible: false,
|
||||||
|
pictureList: [],
|
||||||
|
disabled: false,
|
||||||
|
mpNum: 0,
|
||||||
|
modifyFileList: []
|
||||||
|
})
|
||||||
|
const pictureViewRef = ref(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
dialogImageUrl,
|
||||||
|
dialogVisible,
|
||||||
|
pictureList,
|
||||||
|
disabled
|
||||||
|
} = toRefs(state)
|
||||||
|
|
||||||
|
const defaultParameters = {
|
||||||
|
mode: 'add',
|
||||||
|
isAddCallback: true,
|
||||||
|
isModifyCallback: true,
|
||||||
|
multiple: true,
|
||||||
|
autoUpload: false,
|
||||||
|
limit: 5,
|
||||||
|
limitSize: 2,
|
||||||
|
industryCategory: 'PUB',
|
||||||
|
displayMsg: false,
|
||||||
|
specialTreatment: false,
|
||||||
|
backPic: false,
|
||||||
|
headers: {'industry-category': 'PUB', 'Authorization': `Bearer ${userStore.token}`},
|
||||||
|
url: '/api/uploads'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeParameters = reactive(merge({}, defaultParameters, props.parameters))
|
||||||
|
const emit = defineEmits(['on-remove', 'on-success', 'del-picture-id', 'on-change', 'on-pic-view', 'on-remove'])
|
||||||
|
|
||||||
|
function onSuccess(res, file, fileList) {
|
||||||
|
if (mergeParameters.autoUpload) {
|
||||||
|
emit('on-success', res.data, file, props.formData)
|
||||||
|
} else
|
||||||
|
emit('on-success', fileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(res) {
|
||||||
|
ElMessage.error('上传失败:', res)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(file, fileList) {
|
||||||
|
file.status = 'success'
|
||||||
|
emit('on-change', file, fileList, props.formData)
|
||||||
|
if(!mergeParameters.autoUpload)
|
||||||
|
pictureList.value = fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(file, fileList) { // 这里的file.id是已经存在服务器中的文件
|
||||||
|
pictureList.value = fileList
|
||||||
|
if (file.id) {
|
||||||
|
emit('del-picture', file, props.formData)
|
||||||
|
}
|
||||||
|
emit('on-remove', file, props.formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePictureCardPreview(file) {
|
||||||
|
if (mergeParameters.backPic)
|
||||||
|
emit('on-pic-view', file.url, pictureList.value.map(o => o.url))
|
||||||
|
else
|
||||||
|
pictureViewRef.value.viewPicture(file.url, pictureList.value.map(o => o.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownload(file) { // 下载文件
|
||||||
|
if (file && file.url) {
|
||||||
|
window.open(file.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExceed(files, fileList) {
|
||||||
|
ElMessage.warning(`当前限制选择 ${mergeParameters.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
|
||||||
|
}
|
||||||
|
|
||||||
|
pictureList.value = props.formData?.urls || []
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.pic-div {
|
||||||
|
.el-upload-list__item-thumbnail, .el-upload-list__item-actions, .el-upload-list__item, .el-upload--picture-card {
|
||||||
|
object-fit: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noUpOnly div.el-upload--picture-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="pic_image__preview">
|
||||||
|
<el-image ref="imageRef"
|
||||||
|
style="width: 0px; height: 0px"
|
||||||
|
:src="src"
|
||||||
|
:preview-src-list="srcList">
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {reactive, ref, toRefs} from "vue"
|
||||||
|
import defPic from '@/assets/image/pic.gif'
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
src: defPic,
|
||||||
|
srcList: [defPic]
|
||||||
|
})
|
||||||
|
const {src, srcList} = toRefs(state)
|
||||||
|
|
||||||
|
const imageRef = ref(null)
|
||||||
|
|
||||||
|
function viewPicture(url, urls) {
|
||||||
|
if (urls && urls.length > 1) srcList.value = urls
|
||||||
|
else if (url) srcList.value = [url]
|
||||||
|
imageRef.value.$el.querySelector('img').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
viewPicture
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.el-icon-circle-close {
|
||||||
|
color: red !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog :open="modelValue" title="在线签名" :show-footer="false" type="medium" @update:open="handleClose">
|
||||||
|
<div class="signature-container">
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
class="signature-canvas"
|
||||||
|
width="800"
|
||||||
|
height="300"
|
||||||
|
@mousedown="startDrawing"
|
||||||
|
@mousemove="draw"
|
||||||
|
@mouseup="stopDrawing"
|
||||||
|
@mouseleave="stopDrawing"/>
|
||||||
|
<div class="signature-actions">
|
||||||
|
<el-button type="danger" @click="clearCanvas">清除</el-button>
|
||||||
|
<el-button type="primary" @click="saveSignature">保存签名</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ElDialog from "@/components/ElDialog/index.vue"
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'save', dataUrl: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
let isDrawing = false
|
||||||
|
let lastX = 0
|
||||||
|
let lastY = 0
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
|
|
||||||
|
// 初始化画布
|
||||||
|
const initCanvas = () => {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
ctx = canvas.getContext('2d')
|
||||||
|
if (ctx) {
|
||||||
|
ctx.strokeStyle = '#000'
|
||||||
|
ctx.lineWidth = 4
|
||||||
|
ctx.lineJoin = 'round'
|
||||||
|
ctx.lineCap = 'round'
|
||||||
|
clearCanvas()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始绘制
|
||||||
|
const startDrawing = (e: MouseEvent) => {
|
||||||
|
if (!ctx) return
|
||||||
|
isDrawing = true
|
||||||
|
;[lastX, lastY] = [e.offsetX, e.offsetY]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制过程
|
||||||
|
const draw = (e: MouseEvent) => {
|
||||||
|
if (!isDrawing || !ctx) return
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(lastX, lastY)
|
||||||
|
ctx.lineTo(e.offsetX, e.offsetY)
|
||||||
|
ctx.stroke()
|
||||||
|
;[lastX, lastY] = [e.offsetX, e.offsetY]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止绘制
|
||||||
|
const stopDrawing = () => {
|
||||||
|
isDrawing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除画布
|
||||||
|
const clearCanvas = () => {
|
||||||
|
if (canvasRef.value && ctx) {
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存签名
|
||||||
|
const saveSignature = () => {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const dataUrl = canvasRef.value.toDataURL('image/png')
|
||||||
|
emit('save', dataUrl)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗打开状态,初始化画布
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initCanvas()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCanvas()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.signature-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-canvas {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue