小工具

master
lld 2026-02-12 00:34:51 +08:00
parent a9635d075e
commit 35ec6944c7
4 changed files with 346 additions and 6 deletions

View File

@ -71,6 +71,12 @@ export const constantRoutes = [
component: () => import('@/views/index'), component: () => import('@/views/index'),
name: 'Index', name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: { title: '首页', icon: 'dashboard', affix: true }
},
{
path: 'whiteboard',
component: () => import('@/views/tool/whiteboard.vue'),
name: 'Whiteboard',
meta: { title: '实时画板', icon: 'dashboard', affix: true }
} }
] ]
}, },

View File

@ -486,8 +486,5 @@ export default {
} }
</script> </script>
<style scoped> <style scoped>
/* 优化编辑框样式,贴合单元格 */
.el-input {
width: 70px;
}
</style> </style>

View File

@ -191,7 +191,7 @@
<el-table-column width="160" show-overflow-tooltip label="未触发原因" align="center" prop="noTaskReason" /> <el-table-column width="160" show-overflow-tooltip label="未触发原因" align="center" prop="noTaskReason" />
<el-table-column width="180" show-overflow-tooltip label="完整回执" align="center" prop="ack" /> <el-table-column width="180" show-overflow-tooltip label="完整回执" align="center" prop="ack" />
<el-table-column show-overflow-tooltip label="最终执行结果" align="center" prop="execResult"> <el-table-column show-overflow-tooltip label="最终执行结果" align="center" prop="execResult">
<template slot-scope="scope"> <template slot-scope="scope" v-if="scope.row.isTask===1">
<dict-tag :options="dict.type.sys_exec_result" :value="scope.row.execResult"/> <dict-tag :options="dict.type.sys_exec_result" :value="scope.row.execResult"/>
</template> </template>
</el-table-column> </el-table-column>
@ -201,7 +201,7 @@
<el-table-column width="100" show-overflow-tooltip label="操作人" align="center" prop="createBy" /> <el-table-column width="100" show-overflow-tooltip label="操作人" align="center" prop="createBy" />
<el-table-column width="180" show-overflow-tooltip label="操作时间" align="center" prop="createTime" > <el-table-column width="180" show-overflow-tooltip label="操作时间" align="center" prop="createTime" >
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span> <span>{{ scope.row.createTime }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -0,0 +1,337 @@
<template>
<div class="whiteboard-container">
<!-- 工具栏 -->
<el-card class="toolbar-card">
<div class="toolbar-wrapper">
<div class="tool-section">
<el-button-group>
<el-tooltip content="画笔" placement="top">
<el-button
:type="currentTool === 'pen' ? 'primary' : ''"
@click="setCurrentTool('pen')"
icon="Edit"
></el-button>
</el-tooltip>
<el-tooltip content="橡皮擦" placement="top">
<el-button
:type="currentTool === 'eraser' ? 'primary' : ''"
@click="setCurrentTool('eraser')"
icon="Delete"
></el-button>
</el-tooltip>
</el-button-group>
</div>
<div class="tool-section">
<el-color-picker
v-model="currentColor"
show-alpha
size="small"
></el-color-picker>
<el-slider
v-model="lineWidth"
:min="1"
:max="50"
size="small"
style="width: 120px; margin: 0 15px;"
></el-slider>
<span>{{ lineWidth }}px</span>
</div>
<div class="tool-section">
<el-button-group>
<el-tooltip content="撤销" placement="top">
<el-button
@click="undo"
:disabled="historyIndex <= 0"
icon="RefreshLeft"
></el-button>
</el-tooltip>
<el-tooltip content="重做" placement="top">
<el-button
@click="redo"
:disabled="historyIndex >= history.length - 1"
icon="RefreshRight"
></el-button>
</el-tooltip>
<el-tooltip content="清空画布" placement="top">
<el-button
@click="clearCanvas"
icon="Delete"
></el-button>
</el-tooltip>
<el-tooltip content="保存图片" placement="top">
<el-button
@click="saveImage"
type="success"
icon="Download"
></el-button>
</el-tooltip>
</el-button-group>
</div>
</div>
</el-card>
<!-- 画布区域 -->
<div class="canvas-wrapper">
<canvas
ref="canvas"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</div>
<!-- 状态栏 -->
<el-card class="status-card">
<div class="status-wrapper">
<div>当前工具: {{ currentTool === 'pen' ? '画笔' : '橡皮擦' }}</div>
<div>历史记录: {{ historyIndex + 1 }}/{{ history.length }}</div>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: 'Whiteboard',
data() {
return {
currentTool: 'pen',
currentColor: '#000000',
lineWidth: 5,
isDrawing: false,
lastX: 0,
lastY: 0,
history: [],
historyIndex: -1,
canvas: null,
ctx: null
}
},
mounted() {
this.initCanvas()
this.saveState()
},
methods: {
initCanvas() {
this.canvas = this.$refs.canvas
this.ctx = this.canvas.getContext('2d')
//
this.resizeCanvas()
window.addEventListener('resize', this.resizeCanvas)
//
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
},
resizeCanvas() {
const container = this.canvas.parentElement
this.canvas.width = container.clientWidth - 40
this.canvas.height = Math.max(500, window.innerHeight - 250)
this.redraw()
},
setCurrentTool(tool) {
this.currentTool = tool
},
startDrawing(e) {
this.isDrawing = true
const rect = this.canvas.getBoundingClientRect()
this.lastX = e.clientX - rect.left
this.lastY = e.clientY - rect.top
},
draw(e) {
if (!this.isDrawing) return
const rect = this.canvas.getBoundingClientRect()
const currentX = e.clientX - rect.left
const currentY = e.clientY - rect.top
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
if (this.currentTool === 'pen') {
this.ctx.strokeStyle = this.currentColor
this.ctx.lineWidth = this.lineWidth
} else {
this.ctx.strokeStyle = '#ffffff'
this.ctx.lineWidth = this.lineWidth * 2
}
this.ctx.lineTo(currentX, currentY)
this.ctx.stroke()
this.lastX = currentX
this.lastY = currentY
},
stopDrawing() {
if (this.isDrawing) {
this.isDrawing = false
this.saveState()
}
},
handleTouchStart(e) {
e.preventDefault()
const touch = e.touches[0]
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
})
this.canvas.dispatchEvent(mouseEvent)
},
handleTouchMove(e) {
e.preventDefault()
const touch = e.touches[0]
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
})
this.canvas.dispatchEvent(mouseEvent)
},
handleTouchEnd(e) {
e.preventDefault()
const mouseEvent = new MouseEvent('mouseup', {})
this.canvas.dispatchEvent(mouseEvent)
},
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.saveState()
},
saveState() {
this.historyIndex++
this.history.splice(this.historyIndex)
this.history.push(this.canvas.toDataURL())
},
undo() {
if (this.historyIndex > 0) {
this.historyIndex--
this.restoreState()
}
},
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++
this.restoreState()
}
},
restoreState() {
const img = new Image()
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.drawImage(img, 0, 0)
}
img.src = this.history[this.historyIndex]
},
redraw() {
if (this.historyIndex >= 0) {
this.restoreState()
}
},
saveImage() {
const link = document.createElement('a')
link.download = 'whiteboard-' + new Date().getTime() + '.png'
link.href = this.canvas.toDataURL()
link.click()
}
}
}
</script>
<style scoped>
.whiteboard-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.toolbar-card {
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.toolbar-wrapper {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: space-between;
}
.tool-section {
display: flex;
align-items: center;
gap: 10px;
}
.canvas-wrapper {
background: #f5f7fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
}
canvas {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: crosshair;
touch-action: none;
max-width: 100%;
}
.status-card {
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.status-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #606266;
}
@media (max-width: 768px) {
.toolbar-wrapper {
flex-direction: column;
align-items: stretch;
}
.tool-section {
justify-content: center;
margin-bottom: 10px;
}
.status-wrapper {
flex-direction: column;
gap: 5px;
text-align: center;
}
}
</style>