HTML5 Canvas 简单窗口设计器
这是一个非常简单的示例,它绘制了一个窗口框架。
说明: 您可以更改它的宽度和高度
- Vanilla
- React
- Vue
import Konva from 'konva'; var width = window.innerWidth; var height = window.innerHeight; var stage = new Konva.Stage({ container: 'container', width: width, height: height, }); var layer = new Konva.Layer(); stage.add(layer); var widthInput = document.createElement('input'); widthInput.type = 'number'; widthInput.value = '1000'; var widthLabel = document.createElement('span'); widthLabel.innerText = '宽度: '; var widthContainer = document.createElement('div'); widthContainer.style.float = 'left'; widthContainer.style.padding = '10px'; widthContainer.appendChild(widthLabel); widthContainer.appendChild(widthInput); var heightInput = document.createElement('input'); heightInput.type = 'number'; heightInput.value = '2000'; var heightLabel = document.createElement('span'); heightLabel.innerText = '高度: '; var heightContainer = document.createElement('div'); heightContainer.style.float = 'left'; heightContainer.style.padding = '10px'; heightContainer.appendChild(heightLabel); heightContainer.appendChild(heightInput); var controls = document.createElement('div'); controls.style.position = 'absolute'; controls.style.top = '4px'; controls.style.left = '4px'; controls.appendChild(widthContainer); controls.appendChild(heightContainer); document.body.appendChild(controls); function createFrame(frameWidth, frameHeight) { var padding = 70; var group = new Konva.Group(); var top = new Konva.Line({ points: [ 0, 0, frameWidth, 0, frameWidth - padding, padding, padding, padding, ], fill: 'white', }); var left = new Konva.Line({ points: [ 0, 0, padding, padding, padding, frameHeight - padding, 0, frameHeight, ], fill: 'white', }); var bottom = new Konva.Line({ points: [ 0, frameHeight, padding, frameHeight - padding, frameWidth - padding, frameHeight - padding, frameWidth, frameHeight, ], fill: 'white', }); var right = new Konva.Line({ points: [ frameWidth, 0, frameWidth, frameHeight, frameWidth - padding, frameHeight - padding, frameWidth - padding, padding, ], fill: 'white', }); var glass = new Konva.Rect({ x: padding, y: padding, width: frameWidth - padding * 2, height: frameHeight - padding * 2, fill: 'lightblue', }); group.add(glass, top, left, bottom, right); group.find('Line').forEach((line) => { line.closed(true); line.stroke('black'); line.strokeWidth(1); }); return group; } function createInfo(frameWidth, frameHeight) { var offset = 20; var arrowOffset = offset / 2; var arrowSize = 5; var group = new Konva.Group(); var lines = new Konva.Shape({ sceneFunc: function (ctx) { ctx.fillStyle = 'grey'; ctx.lineWidth = 0.5; ctx.moveTo(0, 0); ctx.lineTo(-offset, 0); ctx.moveTo(0, frameHeight); ctx.lineTo(-offset, frameHeight); ctx.moveTo(0, frameHeight); ctx.lineTo(0, frameHeight + offset); ctx.moveTo(frameWidth, frameHeight); ctx.lineTo(frameWidth, frameHeight + offset); ctx.stroke(); }, }); var leftArrow = new Konva.Shape({ sceneFunc: function (ctx) { // 顶部箭头 ctx.moveTo(-arrowOffset - arrowSize, arrowSize); ctx.lineTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset + arrowSize, arrowSize); // 线 ctx.moveTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset, frameHeight); // 底部箭头 ctx.moveTo(-arrowOffset - arrowSize, frameHeight - arrowSize); ctx.lineTo(-arrowOffset, frameHeight); ctx.lineTo(-arrowOffset + arrowSize, frameHeight - arrowSize); ctx.strokeShape(this); }, stroke: 'grey', strokeWidth: 0.5, }); var bottomArrow = new Konva.Shape({ sceneFunc: function (ctx) { // 顶部箭头 ctx.translate(0, frameHeight + arrowOffset); ctx.moveTo(arrowSize, -arrowSize); ctx.lineTo(0, 0); ctx.lineTo(arrowSize, arrowSize); // 线 ctx.moveTo(0, 0); ctx.lineTo(frameWidth, 0); // 底部箭头 ctx.moveTo(frameWidth - arrowSize, -arrowSize); ctx.lineTo(frameWidth, 0); ctx.lineTo(frameWidth - arrowSize, arrowSize); ctx.strokeShape(this); }, stroke: 'grey', strokeWidth: 0.5, }); // 左边文本 var leftLabel = new Konva.Label(); leftLabel.add( new Konva.Tag({ fill: 'white', stroke: 'grey', }) ); var leftText = new Konva.Text({ text: heightInput.value + 'mm', padding: 2, fill: 'black', }); leftLabel.add(leftText); leftLabel.position({ x: -arrowOffset - leftText.width(), y: frameHeight / 2 - leftText.height() / 2, }); leftLabel.on('click tap', function () { createInput('height', this.getAbsolutePosition(), leftText.size()); }); // 底部文本 var bottomLabel = new Konva.Label(); bottomLabel.add( new Konva.Tag({ fill: 'white', stroke: 'grey', }) ); var bottomText = new Konva.Text({ text: widthInput.value + 'mm', padding: 2, fill: 'black', }); bottomLabel.add(bottomText); bottomLabel.position({ x: frameWidth / 2 - bottomText.width() / 2, y: frameHeight + arrowOffset, }); bottomLabel.on('click tap', function () { createInput('width', this.getAbsolutePosition(), bottomText.size()); }); group.add(lines, leftArrow, bottomArrow, leftLabel, bottomLabel); return group; } function createInput(metric, pos, size) { var wrap = document.createElement('div'); wrap.style.position = 'absolute'; wrap.style.backgroundColor = 'rgba(0,0,0,0.1)'; wrap.style.top = 0; wrap.style.left = 0; wrap.style.width = '100%'; wrap.style.height = '100%'; document.body.appendChild(wrap); var input = document.createElement('input'); input.type = 'number'; var similarInput = metric === 'width' ? widthInput : heightInput; input.value = similarInput.value; input.style.position = 'absolute'; input.style.top = pos.y + 3 + 'px'; input.style.left = pos.x + 'px'; input.style.height = size.height + 3 + 'px'; input.style.width = size.width + 3 + 'px'; wrap.appendChild(input); input.addEventListener('change', function () { similarInput.value = input.value; updateCanvas(); }); input.addEventListener('input', function () { similarInput.value = input.value; updateCanvas(); }); wrap.addEventListener('click', function (e) { if (e.target === wrap) { document.body.removeChild(wrap); } }); input.addEventListener('keyup', function (e) { if (e.key === 'Enter') { document.body.removeChild(wrap); } }); } function updateCanvas() { layer.children.forEach((child) => child.destroy()); var frameWidth = parseInt(widthInput.value, 10); var frameHeight = parseInt(heightInput.value, 10); var wr = stage.width() / frameWidth; var hr = stage.height() / frameHeight; var ratio = Math.min(wr, hr) * 0.8; var frameOnScreenWidth = frameWidth * ratio; var frameOnScreenHeight = frameHeight * ratio; var group = new Konva.Group({}); group.x(Math.round(stage.width() / 2 - frameOnScreenWidth / 2) + 0.5); group.y(Math.round(stage.height() / 2 - frameOnScreenHeight / 2) + 0.5); layer.add(group); var frameGroup = createFrame(frameWidth, frameHeight); frameGroup.scale({ x: ratio, y: ratio }); group.add(frameGroup); var infoGroup = createInfo(frameOnScreenWidth, frameOnScreenHeight); group.add(infoGroup); } widthInput.addEventListener('change', updateCanvas); widthInput.addEventListener('input', updateCanvas); heightInput.addEventListener('change', updateCanvas); heightInput.addEventListener('input', updateCanvas); updateCanvas();
import { Stage, Layer, Group, Line, Rect, Shape, Label, Tag, Text } from 'react-konva'; import { useState, useEffect, useRef, useCallback, useMemo, useReducer } from 'react'; // 常量 const MIN_DIMENSION = 100; const MAX_DIMENSION = 5000; const DEFAULT_WIDTH = 1000; const DEFAULT_HEIGHT = 2000; const PADDING = 70; // 用于维度状态的 reducer const dimensionsReducer = (state, action) => { switch (action.type) { case 'SET_WIDTH': return { ...state, width: Math.min(Math.max(parseInt(action.payload, 10) || MIN_DIMENSION, MIN_DIMENSION), MAX_DIMENSION) }; case 'SET_HEIGHT': return { ...state, height: Math.min(Math.max(parseInt(action.payload, 10) || MIN_DIMENSION, MIN_DIMENSION), MAX_DIMENSION) }; default: return state; } }; // 用 于窗口大小的自定义钩子 const useWindowSize = () => { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return size; }; // 用于覆盖输入的自定义钩子 const useInputOverlay = (dimensions, dispatch) => { const [overlay, setOverlay] = useState(null); // 处理创建覆盖层 const createOverlay = useCallback((metric, position, size) => { setOverlay({ metric, position, size }); }, []); // 处理关闭覆盖层 const closeOverlay = useCallback(() => { setOverlay(null); }, []); // 处理覆盖层效果 useEffect(() => { if (!overlay) return; // 创建覆盖层元素 const wrap = document.createElement('div'); wrap.style.position = 'absolute'; wrap.style.backgroundColor = 'rgba(0,0,0,0.1)'; wrap.style.top = 0; wrap.style.left = 0; wrap.style.width = '100%'; wrap.style.height = '100%'; wrap.style.zIndex = 999; wrap.setAttribute('aria-modal', 'true'); wrap.setAttribute('role', 'dialog'); const input = document.createElement('input'); input.type = 'number'; input.min = MIN_DIMENSION; input.max = MAX_DIMENSION; input.value = overlay.metric === 'width' ? dimensions.width : dimensions.height; input.style.position = 'absolute'; input.style.top = `${overlay.position.y + 3}px`; input.style.left = `${overlay.position.x}px`; input.style.width = `${overlay.size.width + 3}px`; input.style.height = `${overlay.size.height + 3}px`; input.setAttribute('aria-label', `编辑 ${overlay.metric}`); wrap.appendChild(input); document.body.appendChild(wrap); // 处理输入变化 const handleChange = () => { const value = input.value; dispatch({ type: overlay.metric === 'width' ? 'SET_WIDTH' : 'SET_HEIGHT', payload: value }); }; // 处理点击外部 const handleWrapClick = (e) => { if (e.target === wrap) { closeOverlay(); document.body.removeChild(wrap); } }; // 处理键盘事件 const handleKeyUp = (e) => { if (e.key === 'Enter' || e.key === 'Escape') { closeOverlay(); document.body.removeChild(wrap); } }; input.addEventListener('change', handleChange); input.addEventListener('input', handleChange); wrap.addEventListener('click', handleWrapClick); input.addEventListener('keyup', handleKeyUp); window.addEventListener('keyup', handleKeyUp); // 焦点到输入框 input.focus(); // 清理工作 return () => { input.removeEventListener('change', handleChange); input.removeEventListener('input', handleChange); wrap.removeEventListener('click', handleWrapClick); input.removeEventListener('keyup', handleKeyUp); window.removeEventListener('keyup', handleKeyUp); if (document.body.contains(wrap)) { document.body.removeChild(wrap); } }; }, [overlay, dimensions, dispatch, closeOverlay]); return { createOverlay, closeOverlay }; }; // WindowFrame 组件用于实际框架渲染 const WindowFrame = ({ width, height }) => { // 为每一侧框架生成点 const framePoints = useMemo(() => ({ top: [0, 0, width, 0, width - PADDING, PADDING, PADDING, PADDING], left: [0, 0, PADDING, PADDING, PADDING, height - PADDING, 0, height], bottom: [0, height, PADDING, height - PADDING, width - PADDING, height - PADDING, width, height], right: [width, 0, width, height, width - PADDING, height - PADDING, width - PADDING, PADDING] }), [width, height]); return ( <Group> {/* 玻璃面板 */} <Rect x={PADDING} y={PADDING} width={width - PADDING * 2} height={height - PADDING * 2} fill="lightblue" /> {/* 框架侧面 */} {Object.entries(framePoints).map(([key, points]) => ( <Line key={key} points={points} fill="white" closed={true} stroke="black" strokeWidth={1} /> ))} </Group> ); }; // MeasurementInfo 组件用于尺寸和箭头 const MeasurementInfo = ({ width, height, dimensions, createOverlay }) => { const offset = 20; const arrowOffset = offset / 2; const arrowSize = 5; // 处理标签点击 const handleLabelClick = useCallback((metric, e) => { const pos = e.target.getAbsolutePosition(); const size = e.target.getSize(); createOverlay(metric, pos, size); }, [createOverlay]); return ( <Group> {/* 指导线 */} <Shape sceneFunc={(ctx, shape) => { ctx.fillStyle = 'grey'; ctx.lineWidth = 0.5; ctx.moveTo(0, 0); ctx.lineTo(-offset, 0); ctx.moveTo(0, height); ctx.lineTo(-offset, height); ctx.moveTo(0, height); ctx.lineTo(0, height + offset); ctx.moveTo(width, height); ctx.lineTo(width, height + offset); ctx.stroke(); }} /> {/* 左箭头(高度) */} <Shape sceneFunc={(ctx, shape) => { // 顶部指针 ctx.moveTo(-arrowOffset - arrowSize, arrowSize); ctx.lineTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset + arrowSize, arrowSize); // 线 ctx.moveTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset, height); // 底部指针 ctx.moveTo(-arrowOffset - arrowSize, height - arrowSize); ctx.lineTo(-arrowOffset, height); ctx.lineTo(-arrowOffset + arrowSize, height - arrowSize); ctx.strokeShape(shape); }} stroke="grey" strokeWidth={0.5} /> {/* 底部箭头(宽度) */} <Shape sceneFunc={(ctx, shape) => { // translate for bottom arrow ctx.translate(0, height + arrowOffset); // 左指针 ctx.moveTo(arrowSize, -arrowSize); ctx.lineTo(0, 0); ctx.lineTo(arrowSize, arrowSize); // 线 ctx.moveTo(0, 0); ctx.lineTo(width, 0); // 右指针 ctx.moveTo(width - arrowSize, -arrowSize); ctx.lineTo(width, 0); ctx.lineTo(width - arrowSize, arrowSize); ctx.strokeShape(shape); }} stroke="grey" strokeWidth={0.5} /> {/* 高度标签 */} <Label x={-arrowOffset - 40} y={height / 2 - 10} onClick={(e) => handleLabelClick('height', e)} > <Tag fill="white" stroke="grey" cornerRadius={2} /> <Text text={`${dimensions.height}mm`} padding={2} fill="black" fontStyle="bold" /> </Label> {/* 宽度标签 */} <Label x={width / 2 - 20} y={height + arrowOffset} onClick={(e) => handleLabelClick('width', e)} > <Tag fill="white" stroke="grey" cornerRadius={2} /> <Text text={`${dimensions.width}mm`} padding={2} fill="black" fontStyle="bold" /> </Label> </Group> ); }; // DimensionControls 组件用于输入字段 const DimensionControls = ({ dimensions, dispatch }) => { // 样式 const inputStyle = { float: 'left', padding: '10px' }; const controlsStyle = { position: 'absolute', top: '4px', left: '4px' }; // 处理输入变化 const handleInputChange = useCallback((e, type) => { dispatch({ type: type === 'width' ? 'SET_WIDTH' : 'SET_HEIGHT', payload: e.target.value }); }, [dispatch]); return ( <div style={controlsStyle}> <div style={inputStyle}> 宽度: <input type="number" value={dimensions.width} onChange={(e) => handleInputChange(e, 'width')} min={MIN_DIMENSION} max={MAX_DIMENSION} aria-label="窗口宽度" /> </div> <div style={inputStyle}> 高度: <input type="number" value={dimensions.height} onChange={(e) => handleInputChange(e, 'height')} min={MIN_DIMENSION} max={MAX_DIMENSION} aria-label="窗口高度" /> </div> </div> ); }; // 主应用组件 const App = () => { // 状态 const [dimensions, dispatch] = useReducer(dimensionsReducer, { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }); // 获取窗口大小 const windowSize = useWindowSize(); // 设置覆盖管理 const { createOverlay } = useInputOverlay(dimensions, dispatch); // 计算框架位置 const frameCalculation = useMemo(() => { const { width, height } = dimensions; const wr = windowSize.width / width; const hr = windowSize.height / height; const ratio = Math.min(wr, hr) * 0.8; const frameOnScreenWidth = width * ratio; const frameOnScreenHeight = height * ratio; const x = Math.round(windowSize.width / 2 - frameOnScreenWidth / 2) + 0.5; const y = Math.round(windowSize.height / 2 - frameOnScreenHeight / 2) + 0.5; return { scale: ratio, position: { x, y }, screenWidth: frameOnScreenWidth, screenHeight: frameOnScreenHeight }; }, [dimensions, windowSize]); return ( <div style={{ position: 'relative', width: '100%', height: '100%' }} role="application" aria-label="窗口框架设计器" > {/* 画布 */} <Stage width={windowSize.width} height={windowSize.height} > <Layer> <Group x={frameCalculation.position.x} y={frameCalculation.position.y} > {/* 缩放后的窗口框架 */} <Group scale={{ x: frameCalculation.scale, y: frameCalculation.scale }}> <WindowFrame width={dimensions.width} height={dimensions.height} /> </Group> {/* 测量信息 */} <MeasurementInfo width={frameCalculation.screenWidth} height={frameCalculation.screenHeight} dimensions={dimensions} createOverlay={createOverlay} /> </Group> </Layer> </Stage> {/* 输入控件 */} <DimensionControls dimensions={dimensions} dispatch={dispatch} /> </div> ); }; export default App;
<template> <div ref="containerRef" style="position: relative; width: 100%; height: 100%"> <div :style="controlsStyle"> <div :style="inputStyle"> 宽度: <input type="number" v-model="dimensions.width" @input="updateCanvas" /> </div> <div :style="inputStyle"> 高度: <input type="number" v-model="dimensions.height" @input="updateCanvas" /> </div> </div> <v-stage :config="stageConfig"> <v-layer> <v-group :config="mainGroupConfig"> <v-group :config="frameScaleConfig"> <v-rect :config="glassConfig" /> <v-line v-for="(line, i) in frameLines" :key="i" :config="line" /> </v-group> <!-- 信息元素 --> <v-shape :config="linesConfig" /> <v-shape :config="leftArrowConfig" /> <v-shape :config="bottomArrowConfig" /> <!-- 标签 --> <v-label :config="heightLabelConfig" @click="handleLabelClick('height', $event)"> <v-tag :config="tagConfig" /> <v-text :config="heightTextConfig" /> </v-label> <v-label :config="widthLabelConfig" @click="handleLabelClick('width', $event)"> <v-tag :config="tagConfig" /> <v-text :config="widthTextConfig" /> </v-label> </v-group> </v-layer> </v-stage> </div> </template> <script setup> import { ref, computed, reactive, onMounted, onBeforeUnmount, watch } from 'vue'; const containerRef = ref(null); const stageSize = reactive({ width: window.innerWidth, height: window.innerHeight }); const dimensions = reactive({ width: 1000, height: 2000 }); const inputOverlay = ref(null); const padding = 70; const offset = 20; const arrowOffset = offset / 2; const arrowSize = 5; // 样式对象 const inputStyle = { float: 'left', padding: '10px' }; const controlsStyle = { position: 'absolute', top: '4px', left: '4px', zIndex: 100 }; // 窗口框架计算 const frameCalculation = computed(() => { const wr = stageSize.width / dimensions.width; const hr = stageSize.height / dimensions.height; const ratio = Math.min(wr, hr) * 0.8; const frameOnScreenWidth = dimensions.width * ratio; const frameOnScreenHeight = dimensions.height * ratio; const x = Math.round(stageSize.width / 2 - frameOnScreenWidth / 2) + 0.5; const y = Math.round(stageSize.height / 2 - frameOnScreenHeight / 2) + 0.5; return { scale: ratio, position: { x, y }, screenWidth: frameOnScreenWidth, screenHeight: frameOnScreenHeight }; }); // 画布配置 const stageConfig = computed(() => ({ width: stageSize.width, height: stageSize.height })); // 主组 const mainGroupConfig = computed(() => ({ x: frameCalculation.value.position.x, y: frameCalculation.value.position.y })); // 框架缩放组 const frameScaleConfig = computed(() => ({ scaleX: frameCalculation.value.scale, scaleY: frameCalculation.value.scale })); // 玻 璃配置 const glassConfig = computed(() => ({ x: padding, y: padding, width: dimensions.width - padding * 2, height: dimensions.height - padding * 2, fill: 'lightblue' })); // 框架线 const frameLines = computed(() => [ // 顶部线 { points: [ 0, 0, dimensions.width, 0, dimensions.width - padding, padding, padding, padding ], fill: 'white', closed: true, stroke: 'black', strokeWidth: 1 }, // 左侧线 { points: [ 0, 0, padding, padding, padding, dimensions.height - padding, 0, dimensions.height ], fill: 'white', closed: true, stroke: 'black', strokeWidth: 1 }, // 底部线 { points: [ 0, dimensions.height, padding, dimensions.height - padding, dimensions.width - padding, dimensions.height - padding, dimensions.width, dimensions.height ], fill: 'white', closed: true, stroke: 'black', strokeWidth: 1 }, // 右侧线 { points: [ dimensions.width, 0, dimensions.width, dimensions.height, dimensions.width - padding, dimensions.height - padding, dimensions.width - padding, padding ], fill: 'white', closed: true, stroke: 'black', strokeWidth: 1 } ]); // 信息元素 const linesConfig = computed(() => ({ sceneFunc: function(ctx, shape) { const frameWidth = frameCalculation.value.screenWidth; const frameHeight = frameCalculation.value.screenHeight; ctx.fillStyle = 'grey'; ctx.lineWidth = 0.5; ctx.moveTo(0, 0); ctx.lineTo(-offset, 0); ctx.moveTo(0, frameHeight); ctx.lineTo(-offset, frameHeight); ctx.moveTo(0, frameHeight); ctx.lineTo(0, frameHeight + offset); ctx.moveTo(frameWidth, frameHeight); ctx.lineTo(frameWidth, frameHeight + offset); ctx.stroke(); } })); const leftArrowConfig = computed(() => ({ sceneFunc: function(ctx, shape) { const frameHeight = frameCalculation.value.screenHeight; // 顶部指针 ctx.moveTo(-arrowOffset - arrowSize, arrowSize); ctx.lineTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset + arrowSize, arrowSize); // 线 ctx.moveTo(-arrowOffset, 0); ctx.lineTo(-arrowOffset, frameHeight); // 底部指针 ctx.moveTo(-arrowOffset - arrowSize, frameHeight - arrowSize); ctx.lineTo(-arrowOffset, frameHeight); ctx.lineTo(-arrowOffset + arrowSize, frameHeight - arrowSize); ctx.strokeShape(shape); }, stroke: 'grey', strokeWidth: 0.5 })); const bottomArrowConfig = computed(() => ({ sceneFunc: function(ctx, shape) { const frameWidth = frameCalculation.value.screenWidth; const frameHeight = frameCalculation.value.screenHeight; // translate for bottom arrow ctx.translate(0, frameHeight + arrowOffset); // 左指针 ctx.moveTo(arrowSize, -arrowSize); ctx.lineTo(0, 0); ctx.lineTo(arrowSize, arrowSize); // 线 ctx.moveTo(0, 0); ctx.lineTo(frameWidth, 0); // 右指针 ctx.moveTo(frameWidth - arrowSize, -arrowSize); ctx.lineTo(frameWidth, 0); ctx.lineTo(frameWidth - arrowSize, arrowSize); ctx.strokeShape(shape); }, stroke: 'grey', strokeWidth: 0.5 })); // 标签配置 const tagConfig = { fill: 'white', stroke: 'grey' }; const heightLabelConfig = computed(() => ({ x: -arrowOffset - 40, y: frameCalculation.value.screenHeight / 2 - 10 })); const heightTextConfig = computed(() => ({ text: `${dimensions.height}mm`, padding: 2, fill: 'black' })); const widthLabelConfig = computed(() => ({ x: frameCalculation.value.screenWidth / 2 - 20, y: frameCalculation.value.screenHeight + arrowOffset })); const widthTextConfig = computed(() => ({ text: `${dimensions.width}mm`, padding: 2, fill: 'black' })); // 事件处理程序 const handleLabelClick = (metric, e) => { const pos = e.target.getAbsolutePosition(); const size = { width: 50, height: 20 }; // 近似大小 createInputOverlay(metric, pos, size); }; const createInputOverlay = (metric, pos, size) => { // 创建覆盖层 const wrap = document.createElement('div'); wrap.style.position = 'absolute'; wrap.style.backgroundColor = 'rgba(0,0,0,0.1)'; wrap.style.top = 0; wrap.style.left = 0; wrap.style.width = '100%'; wrap.style.height = '100%'; wrap.style.zIndex = 999; const input = document.createElement('input'); input.type = 'number'; input.value = metric === 'width' ? dimensions.width : dimensions.height; input.style.position = 'absolute'; input.style.top = `${pos.y + 3}px`; input.style.left = `${pos.x}px`; input.style.width = `${size.width + 3}px`; input.style.height = `${size.height + 3}px`; wrap.appendChild(input); document.body.appendChild(wrap); const handleChange = () => { const value = parseInt(input.value, 10); dimensions[metric] = value; }; input.addEventListener('change', handleChange); input.addEventListener('input', handleChange); const handleWrapClick = (e) => { if (e.target === wrap) { document.body.removeChild(wrap); } }; const handleKeyUp = (e) => { if (e.key === 'Enter') { document.body.removeChild(wrap); } }; wrap.addEventListener('click', handleWrapClick); input.addEventListener('keyup', handleKeyUp); input.focus(); }; // 窗口调整大小处理程序 const handleResize = () => { stageSize.width = window.innerWidth; stageSize.height = window.innerHeight; }; // 设置和清理 onMounted(() => { window.addEventListener('resize', handleResize); }); onBeforeUnmount(() => { window.removeEventListener('resize', handleResize); // 清理任何输入覆盖层 if (inputOverlay.value) { if (document.body.contains(inputOverlay.value.wrap)) { document.body.removeChild(inputOverlay.value.wrap); } } }); const updateCanvas = () => { // 这由 Vue 反应式地处理 }; </script>