主进程
// 截图窗口
let cutWindow: BrowserWindow | null = null;
function createCutWindow(): void {
const { width, height } = getSize()
cutWindow = new BrowserWindow({
width,
height,
frame: false,
transparent: true,
fullscreen: true,
resizable: false,
alwaysOnTop:true,
skipTaskbar: true,
show: false,
icon: join(__dirname, '../../build/icon.jpg'),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
nodeIntegration: true,
contextIsolation: false,
}
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
cutWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/cut`)
} else {
cutWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/cut' })
}
cutWindow.setAlwaysOnTop(true, 'pop-up-menu')
cutWindow.setVisibleOnAllWorkspaces(true, { skipTransformProcessType: true })
cutWindow.on('ready-to-show', () => {
cutWindow.show()
cutWindow.focus()
})
}
// 关闭截图提问
function closeCutWindow() {
cutWindow && cutWindow.close()
cutWindow = null;
}
// 打开截图提问
ipcMain.on("open-cut-window", () => {
closeCutWindow()
mainWindow && mainWindow.minimize();
createCutWindow()
})
/*
* 关闭截图提问
* */
ipcMain.on("close-cut-window", (event) => {
closeCutWindow()
})
// 打开主窗口
ipcMain.handle('cut-open-main-window', async (_, params) => {
if (mainWindow) {
mainWindow.show()
// 向主窗口发送消息,要求打开特定页面
mainPageContainer.context.webContents.send('open-page', `/chat`)
mainPageContainer.context.webContents.send('send-cut-data', params)
}
})
// 截图提问:获取屏幕
ipcMain.handle("cut_get_screen_image", async () => {
let sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: getSize(),
});
return sources[0]
})
截图窗口文件
<!-- 截屏 -->
<template>
<div ref="containerRef" class="container" :style="'background-image:url(' + bg + ')'">
<!-- :style="'background-image:url(' + bg + ')'"-->
<div class="mark" v-if="bg"></div>
<v-stage :config="stageConfig1">
<v-layer>
<!-- 背景图像 -->
<v-image :config="imageConfig1" @image:load="handleImageLoad"/>
</v-layer>
</v-stage>
<div class="stage-wrap" :style="stageStyle" v-if="stageConfig.width">
<v-stage
ref="stageRef"
:config="stageConfig"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
>
<v-layer ref="layerRef">
<!-- 背景图像 -->
<v-image :config="imageConfig" @image:load="handleImageLoad"/>
<!-- 当前正在绘制的矩形 -->
<v-rect
v-if="rectConfig"
:config="rectConfig"
/>
<!-- 绘制所有矩形 -->
<v-rect
v-for="(item, index) in rectConfigList"
:key="index"
:config="item"
/>
<v-line v-for="(line, i) in lines" :key="i" :config="line" name="line"/>
<!-- 绘制所有箭头 -->
<v-arrow
v-for="(arrow, index) in arrows"
:key="index"
:config="arrow"
/>
<!-- 当前正在绘制的箭头 -->
<v-arrow
v-if="currentArrow"
:config="currentArrow"
/>
</v-layer>
</v-stage>
</div>
<div
v-if="isOperating"
class="operation"
:style="operatingStyle"
>
<div class="operation-box">
<div
class="item"
:class="{ active: active === item.name }"
v-for="item in operationList"
:key="item.name"
@click="handleOperation(item.name)">
<Tooltip :content="item.title">
<MyIcon class="icon" :icon="item.icon" ></MyIcon>
</Tooltip>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, computed, nextTick } from 'vue'
const { ipcRenderer } = window.electron
const containerRef = ref(null)
const type = ref("");
const bg = ref('')
const img = ref("")
const active = ref("")
const operationListData = [
{
title: '矩形',
icon: 'icon-xingzhuang',
name: "rect"
},
{
title: '箭头',
icon: 'icon-jiantou1',
name: "arrow"
},
{
title: '画笔',
icon: 'icon-huabi',
name: "line"
},
{
title: '撤回',
icon: "icon-chehui1",
name: "chehui"
},
{
title: "提取文字",
icon: "icon-tiquwenzi",
name: "tiquwenzi",
},
{
title: '取消',
icon: 'icon-quxiao1',
name: "close"
},
{
title: '完成',
icon: 'icon-queren',
name: "queren"
},
{
title: "问问灵犀",
icon: "icon-wenwenlingxi",
name: "wenwenlingxi"
}
]
const operationList = computed(() => {
return operationListData.reduce((pre: any, item: any) => {
if(item.name === "wenwenlingxi") {
if(type.value === "2") {
pre.push(item)
}
}else if(item.name === "queren") {
if(type.value === "1") {
pre.push(item)
}
}else {
pre.push(item)
}
return pre
}, [])
})
// 截图信息
const cutConfig = ref({
x: 0,
y: 0,
width: 0,
height: 0
})
// 屏幕
const winScreenConfig = reactive({
width: 0,
height: 0
})
// 操作位置
const operatingStyle = computed(() => {
let x = cutConfig.value.x;
let y = Math.ceil(cutConfig.value.y) + Math.ceil(cutConfig.value.height) + 10;
if(winScreenConfig.height - 50 <= y) {
return {
left: "50%",
bottom: "70px",
top: "unset",
transform: "translateX(-50%)"
}
}
return {
left: `${x}px`,
top: `${y}px`
}
})
// 是否显示操作
const isOperating = computed(() => {
if(isCut.value) {
return !isDrawing.value && cutConfig.value.width
}else {
return true
}
})
// 是否截图
const isCut = ref(true)
// 画布
const stageRef = ref(null)
const layerRef = ref(null)
const stageConfig = ref({})
// 样式
const stageStyle = computed(() => {
if(!isCut.value) {
return {
left: `${cutConfig.value.x}px`,
top: `${cutConfig.value.y}px`,
}
}
return {}
})
// 背景图片
const imageConfig = ref({
x: 0,
y: 0,
width: 0,
height: 0,
image: null
})
// 是否在绘画
const isDrawing = ref(false)
// 矩形
const rectConfig = ref(null)
// 起始点
const startPoint = ref({ x: 0, y: 0 })
// 矩形列表
const rectConfigList = ref([])
// 画笔
const lines = ref([])
const currentLine = ref(null);
const arrows = ref([]);
const currentArrow = ref(null);
// 画布操作记录
const canvasRecord = ref([])
const stageConfig1 = ref({})
// 背景图片
const imageConfig1 = ref({
x: 0,
y: 0,
width: 0,
height: 0,
image: null
})
// 获取屏幕
async function getCurrentScreenImage() {
let source = await ipcRenderer.invoke("cut_get_screen_image")
const clientHeight = containerRef.value.clientHeight
const clientWidth = containerRef.value.clientWidth
console.log("containerRef", clientWidth, clientHeight)
console.log("source", source)
const { thumbnail } = source;
const pngData = await thumbnail.toDataURL('image/png')
const size = await thumbnail.getSize()
console.log("size", size)
winScreenConfig.width = size.width;
winScreenConfig.height = size.height;
bg.value = pngData
stageConfig.value = {
width: size.width,
height: size.height
}
stageConfig1.value = {
width: size.width,
height: size.height
}
}
function handleImageLoad(): void {
console.log('图片已成功加载并显示');
}
function handleMouseDown(e): void {
isDrawing.value = true
operatingCanvas(e,"start")
}
function handleMouseMove(e): void {
if (!isDrawing.value) return
operatingCanvas(e,"move")
}
function handleMouseUp(e): void {
if (!isDrawing.value) return
isDrawing.value = false
operatingCanvas(e,"stop")
}
function operatingCanvas(e,type) {
let name = active.value;
if(name === 'rect' || isCut.value) {
createRect(type)
}else if(name === 'line') {
createPen(type)
}else if(name === 'arrow') {
createdArrow(e,type)
}
}
// 创建矩形
function createRect(type: string): void {
const pos = stageRef.value.getStage().getPointerPosition()
switch (type) {
// 开始
case "start":
startPoint.value = { x: pos.x, y: pos.y }
rectConfig.value = {
x: pos.x,
y: pos.y,
width: 0,
height: 0,
fill: 'transparent',
stroke: isCut.value ? '#24be58' : "red",
uuid: crypto.randomUUID(),
}
break;
// 绘制
case "move":
let width = pos.x - startPoint.value.x
let height = pos.y - startPoint.value.y
rectConfig.value = {
...rectConfig.value,
x: width > 0 ? startPoint.value.x : pos.x,
y: height > 0 ? startPoint.value.y : pos.y,
width: Math.abs(width),
height: Math.abs(height)
}
break;
// 结束
case "stop":
if(isCut.value) {
// 记录截图位置
cutConfig.value = {
x: rectConfig.value.x,
y: rectConfig.value.y,
width: rectConfig.value.width,
height: rectConfig.value.height
}
captureSelection()
}else {
rectConfigList.value.push({
...rectConfig.value
})
canvasRecord.value.push({
...rectConfig.value
})
rectConfig.value = null
}
break;
}
}
// 画笔
function createPen(type: string): void {
const pos = stageRef.value.getStage().getPointerPosition()
switch (type) {
// 开始
case "start":
currentLine.value = {
points: [pos.x, pos.y],
stroke: "red",
strokeWidth: 2,
lineCap: 'round',
lineJoin: 'round',
tension: 0.5,
draggable: false,
name: 'line',
uuid: crypto.randomUUID()
};
lines.value.push(currentLine.value);
canvasRecord.value.push(currentLine.value)
break;
// 绘制
case "move":
let newPoints = currentLine.value.points.concat([pos.x, pos.y]);
currentLine.value.points = newPoints;
layerRef.value.getNode().batchDraw();
break;
// 结束
case "stop":
currentLine.value = null;
break;
}
}
// 箭头
function createdArrow(e, type: string): void {
const pos = stageRef.value.getStage().getPointerPosition()
switch (type) {
// 开始
case "start":
currentArrow.value = {
points: [pos.x, pos.y, pos.x, pos.y],
pointerLength: 10,
pointerWidth: 10,
fill: 'red',
stroke: 'red',
strokeWidth: 2,
uuid: crypto.randomUUID()
};
break;
// 绘制
case "move":
let movePos = e.target.getStage().getPointerPosition();
currentArrow.value.points = [
currentArrow.value.points[0],
currentArrow.value.points[1],
movePos.x,
movePos.y
];
break;
// 结束
case "stop":
const stopPos = e.target.getStage().getPointerPosition();
currentArrow.value.points = [
currentArrow.value.points[0],
currentArrow.value.points[1],
stopPos.x,
stopPos.y
];
arrows.value.push({...currentArrow.value});
canvasRecord.value.push({...currentArrow.value});
currentArrow.value = null
break;
}
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
// 允许跨域图片(如果需要)
img.crossOrigin = 'anonymous';
// 图片加载成功回调
img.onload = () => resolve(img);
// 图片加载失败回调
img.onerror = (error) => reject(new Error(`图片加载失败: ${error.message}`));
// 开始加载图片
img.src = url;
});
}
// 捕获选择
async function captureSelection(): void {
const rect = rectConfig.value
let { base64, imageData } = await getCutImage(rect)
const loadedImage = await loadImage(base64);
if(isCut.value) {
imageConfig1.value = {
width: imageData.width,
height: imageData.height,
image: loadedImage,
x: rect.x,
y: rect.y,
}
}else {
imageConfig.value = {
width: imageData.width,
height: imageData.height,
image: loadedImage,
x: 0,
y: 0,
}
// 重新绘制画布
stageConfig.value = {
width: rect.width,
height: rect.height,
}
rectConfig.value = null
}
}
// 根据选择区域生成图片
async function getCutImage(info) {
const { x, y, width, height } = info;
let img = new Image();
img.src = bg.value;
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = ctx.width = width;
canvas.height = ctx.height = height;
ctx.drawImage(img, -x, -y, window.innerWidth, window.innerHeight);
return {
base64: canvas.toDataURL("image/png"),
imageData: ctx.getImageData(0, 0, width, height),
};
}
// 操作
async function handleOperation(name): void {
active.value = name;
if(isCut.value) {
isCut.value = false;
await captureSelection()
}
switch (name){
// 取消
case "close":
handleCancel()
break;
// 确认
case "queren":
case "wenwenlingxi":
saveCanvas();
break;
// 提前文字
case "tiquwenzi":
handleExtractText()
break;
// 撤回
case "chehui":
handleWithdraw()
break;
}
}
// 撤回
function handleWithdraw() {
if(canvasRecord.value.length) {
let last = canvasRecord.value.pop();
lines.value = lines.value.filter(item =>item.uuid !== last.uuid )
rectConfigList.value = rectConfigList.value.filter(item => item.uuid !== last.uuid)
arrows.value = arrows.value.filter(item => item.uuid !== last.uuid)
}
}
// 取消
function handleCancel(): void {
ipcRenderer.send("close-cut-window", )
}
// 画布内容转换为图片
function toDataURL() {
const stage = stageRef.value.getStage();
const dataURL = stage.toDataURL();
return {
data: dataURL
}
}
// 保存截图
function saveCanvas(): void {
let imgObj = toDataURL()
ipcRenderer.send("close-cut-window",)
ipcRenderer.invoke(
"cut-open-main-window",
{
type: "img",
data: imgObj.data
}
)
}
// 提取文字
function handleExtractText(): void {
let imgObj = toDataURL()
ipcRenderer.send("close-cut-window", )
ipcRenderer.send("open-extract-text-window")
window.localStorage.setItem("cutData", JSON.stringify({
type: "img",
data: imgObj.data
}))
}
onMounted( () => {
type.value = window.localStorage.getItem("cutType") || "1"
setTimeout(()=>{
getCurrentScreenImage()
},500)
})
</script>
<style>
html,
body,
#app {
margin: 0;
padding: 0;
background: transparent !important;
}
</style>
<style scoped lang="less">
.container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
background-size: 100% 100%;
background-repeat: no-repeat;
box-sizing: border-box;
border: 2px solid #24be58;
cursor: crosshair;
user-select: none;
}
.mark {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: rgba(0, 0, 0, 0.45);
//opacity: 0;
}
.operation {
position: fixed;
top: 0;
z-index: 1;
.operation-box {
display: flex;
align-items: center;
background-color: #fff;
padding: 2px 5px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
.item {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
& + .item{
margin-left: 5px;
}
.icon {
width: 20px;
height: 20px;
}
&.active {
.icon {
color: #0057ff;
}
}
}
}
}
.stage-wrap {
position: fixed;
top: 0;
left: 0;
z-index: 1;
&::before {
content: " ";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 2px solid #24be58;
}
}
.bg-transparent {
background-color: transparent;
}
</style>