Vue 3 弹出式计算器组件(源码 + 教程)

news2025/6/7 22:37:21

🧮 Vue 3 弹出式计算器组件(源码 + 教程)

📌 建议收藏 + 点赞 + 关注,本组件支持加减乘除、双向绑定、计算过程展示,适用于表单辅助输入场景。


在这里插入图片描述

🔧 一、完整源码(复制即用)

<!-- CalculatorInput.vue -->
<template>
  <div class="calculator-wrapper">
    <!-- 自定义输入框 -->
    <div class="custom-input-container">
      <input type="text" class="custom-input" :value="modelValue" @input="onInputChange" :placeholder="placeholder" />
      <div class="calculator-icon" @click="toggleCalculator"><span>🧮</span></div>
    </div>

    <!-- 计算器部分 -->
    <div v-show="showCalculator" class="calculator-container" ref="calculatorRef">
      <div class="calculator">
        <div class="display">
          <div class="calculation-process">{{ calculationProcess || '&nbsp;' }}</div>
          <div class="current-value">{{ currentValue || '0' }}</div>
        </div>
        <div class="buttons">
          <div class="button" @click="clear">C</div>
          <div class="button" @click="changeSign">+/-</div>
          <div class="button" @click="percentage">%</div>
          <div class="button operator" @click="setOperation('/')">/</div>

          <div class="button" @click="appendNumber('7')">7</div>
          <div class="button" @click="appendNumber('8')">8</div>
          <div class="button" @click="appendNumber('9')">9</div>
          <div class="button operator" @click="setOperation('*')">×</div>

          <div class="button" @click="appendNumber('4')">4</div>
          <div class="button" @click="appendNumber('5')">5</div>
          <div class="button" @click="appendNumber('6')">6</div>
          <div class="button operator" @click="setOperation('-')">-</div>

          <div class="button" @click="appendNumber('1')">1</div>
          <div class="button" @click="appendNumber('2')">2</div>
          <div class="button" @click="appendNumber('3')">3</div>
          <div class="button operator" @click="setOperation('+')">+</div>

          <div class="button zero" @click="appendNumber('0')">0</div>
          <div class="button" @click="appendDecimal">.</div>
          <div class="button operator" @click="calculate">=</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  modelValue: { type: String, default: '' },
  placeholder: { type: String, default: '请输入数值' },
  width: { type: String, default: '200px' }
});
const emit = defineEmits(['update:modelValue']);

const onInputChange = (e) => emit('update:modelValue', e.target.value);

const currentValue = ref('');
const previousValue = ref('');
const operation = ref(null);
const showCalculator = ref(false);
const calculationPerformed = ref(false);
const calculationProcess = ref('');
const calculatorRef = ref(null);

const toggleCalculator = () => (showCalculator.value = !showCalculator.value);

const handleClickOutside = (e) => {
  if (showCalculator.value && calculatorRef.value && !calculatorRef.value.contains(e.target) && !e.target.closest('.calculator-icon')) {
    showCalculator.value = false;
  }
};
const handleEscKey = (e) => {
  if (e.key === 'Escape') showCalculator.value = false;
};
onMounted(() => {
  document.addEventListener('click', handleClickOutside);
  document.addEventListener('keydown', handleEscKey);
});
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside);
  document.removeEventListener('keydown', handleEscKey);
});

watch(currentValue, (val) => {
  if (calculationPerformed.value && val !== '') emit('update:modelValue', val);
});

const clear = () => {
  currentValue.value = '';
  previousValue.value = '';
  operation.value = null;
  calculationProcess.value = '';
  emit('update:modelValue', '');
};
const appendNumber = (n) => {
  if (calculationPerformed.value) {
    currentValue.value = '';
    calculationPerformed.value = false;
    calculationProcess.value = '';
  }
  currentValue.value += n;
  updateCalculationProcess();
};
const appendDecimal = () => {
  if (calculationPerformed.value) {
    currentValue.value = '0';
    calculationPerformed.value = false;
    calculationProcess.value = '';
  }
  if (!currentValue.value.includes('.')) currentValue.value += '.';
  updateCalculationProcess();
};
const setOperation = (op) => {
  if (!currentValue.value) return;
  if (previousValue.value) calculate();
  operation.value = op;
  previousValue.value = currentValue.value;
  currentValue.value = '';
  updateCalculationProcess();
};
const calculate = () => {
  if (!operation.value || !previousValue.value || !currentValue.value) {
    if (currentValue.value) {
      emit('update:modelValue', currentValue.value);
      if (calculationProcess.value === currentValue.value)
        calculationProcess.value = `${currentValue.value} =`;
      calculationPerformed.value = true;
    }
    return;
  }
  const prev = parseFloat(previousValue.value);
  const current = parseFloat(currentValue.value);
  let result = 0;
  switch (operation.value) {
    case '+': result = prev + current; break;
    case '-': result = prev - current; break;
    case '*': result = prev * current; break;
    case '/': result = prev / current; break;
  }
  const symbol = operation.value === '*' ? '×' : operation.value === '/' ? '÷' : operation.value;
  calculationProcess.value = `${previousValue.value} ${symbol} ${currentValue.value} =`;
  currentValue.value = result.toString();
  previousValue.value = '';
  operation.value = null;
  calculationPerformed.value = true;
};
const updateCalculationProcess = () => {
  const symbol = operation.value === '*' ? '×' : operation.value === '/' ? '÷' : operation.value;
  if (!operation.value) {
    calculationProcess.value = currentValue.value;
  } else if (!currentValue.value) {
    calculationProcess.value = `${previousValue.value} ${symbol}`;
  } else {
    calculationProcess.value = `${previousValue.value} ${symbol} ${currentValue.value}`;
  }
};
const changeSign = () => {
  currentValue.value = currentValue.value.charAt(0) === '-' ? currentValue.value.slice(1) : `-${currentValue.value}`;
  updateCalculationProcess();
};
const percentage = () => {
  currentValue.value = (parseFloat(currentValue.value) / 100).toString();
  updateCalculationProcess();
};
</script>

<style scoped>
.calculator-wrapper { position: relative; width: v-bind('width'); margin: 0 auto; }
.custom-input-container { position: relative; width: 100%; margin-bottom: 0.5rem; }
.custom-input { width: 100%; padding: 8px 30px 8px 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; transition: border-color 0.2s; box-sizing: border-box; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); }
.custom-input:focus { outline: none; border-color: #409eff; box-shadow: 0 0 5px rgba(64, 158, 255, 0.3); }
.calculator-icon { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); cursor: pointer; padding: 3px; font-size: 16px; color: #606266; transition: color 0.2s; }
.calculator-icon:hover { color: #409eff; }
.calculator-container { position: absolute; top: 100%; left: 0; width: 100%; z-index: 100; margin-top: 5px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15); border-radius: 8px; }
.calculator { width: 100%; background-color: #f0f0f0; border-radius: 8px; overflow: hidden; }
.display { background-color: #333; color: white; padding: 10px; text-align: right; font-family: monospace; }
.calculation-process { font-size: 0.8rem; color: #aaa; min-height: 16px; margin-bottom: 4px; }
.current-value { font-size: 1.2rem; min-height: 24px; }
.buttons { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background-color: #999; }
.button { background-color: #e0e0e0; padding: 10px; text-align: center; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s; }
.button:hover { background-color: #d0d0d0; }
.operator { background-color: #f8a51b; color: white; }
.operator:hover { background-color: #e09016; }
.zero { grid-column: span 2; }
</style>

🧠 二、组件结构与逻辑全解析

🎯 目标:理解该组件的 UI 组成、逻辑功能、数据响应过程,便于拓展与优化。


📦 1. 组件功能概览

模块功能说明
自定义输入框输入数值,展示已计算结果
弹出计算器模拟标准四则运算计算器,支持连算与过程展示
状态响应双向绑定 v-model,每次计算结果自动同步外部状态
UI 样式视觉风格轻盈,适配大多数表单/工具栏场景

🔍 2. 模板结构解析 (<template>)

<div class="custom-input-container">
  <input ... /> <!-- 用户输入框 -->
  <div class="calculator-icon"><span>🧮</span></div> <!-- 弹出按钮 -->
</div>

<div v-show="showCalculator" class="calculator-container">
  <div class="calculator"> <!-- 主计算器界面 -->
    <div class="display"> <!-- 显示屏:过程 + 当前值 -->
      <div class="calculation-process">{{ calculationProcess }}</div>
      <div class="current-value">{{ currentValue }}</div>
    </div>
    <div class="buttons"> <!-- 20 个按键布局 -->
      <!-- 数字、运算符、等号等 -->
    </div>
  </div>
</div>

📌 重点说明:

  • v-show="showCalculator" 控制计算器显示/隐藏;
  • custom-input-container 提供了清晰的输入交互入口;
  • 所有按钮通过事件绑定实现行为触发;

🧩 3. 脚本逻辑详解 (<script setup>)

📌 Props & Emits
const props = defineProps({
  modelValue: String,
  placeholder: String,
  width: String
});
const emit = defineEmits(['update:modelValue']);
  • 使用 v-model 实现组件外部与内部状态联动;
  • 支持自定义宽度与占位符提示。
⚙️ 核心响应式变量
const currentValue = ref('');         // 当前输入
const previousValue = ref('');        // 上一步数值
const operation = ref(null);          // 当前运算符
const calculationProcess = ref('');   // 顶部过程显示
const calculationPerformed = ref(false); // 是否已计算
const showCalculator = ref(false);    // 显示控制
🧠 主要功能函数
  • appendNumber(n) / appendDecimal():输入数字与小数点;
  • setOperation(op):设置运算符;
  • calculate():计算结果并展示过程;
  • clear():清空一切;
  • changeSign() / percentage():正负号、百分比扩展;
  • updateCalculationProcess():更新顶部计算过程字符串。
🖱️ 弹出交互逻辑
const handleClickOutside = (e) => {
  if (!calculatorRef.value.contains(e.target)) {
    showCalculator.value = false;
  }
};
  • 点击外部关闭;
  • 按下 ESC 键关闭;
  • 使用 ref + document.addEventListener 实现监听。

🎨 4. 样式美化亮点 (<style scoped>)

  • 采用浅灰 + 橙色运算符按钮突出视觉分组;
  • display 区域使用 monospace 字体,模拟真实计算器屏幕;
  • grid-template-columns: repeat(4, 1fr) 布局整齐 4 列按钮;
  • .zero 使用 grid-column: span 2 让 0 按钮宽两倍更真实;
  • 自定义输入框视觉统一,内嵌按钮轻巧无干扰。

🧩 三、拓展建议与实战应用

🚀 用好这组件,不止复制粘贴,更可以轻松定制与集成。

✅ 推荐使用场景

  • 💼 表单辅助数值输入(如商品价格、折扣设置)
  • 📊 表格单元格弹出式计算(配合 Element Plus 的表格)
  • 📱 移动端小屏工具栏简易计算

🔧 自定义拓展建议

功能需求实现方式
保留历史记录增加 historyList,每次计算后 push 新记录
支持快捷键输入监听键盘按键映射按钮功能
控制显示位置增加 placement props,实现顶部/右侧弹出方向切换
改为暗黑模式.calculator.display 的背景与字体颜色即可

✅ 总结:一个轻巧实用的 Vue 计算器组件

🔁 回顾一下:

  • ✅ 支持标准四则运算 + 输入框联动;
  • ✅ 样式美观、结构清晰、逻辑独立;
  • ✅ 适合复制粘贴,嵌入任何 Vue 3 项目中。

📌 如果这对你有帮助,欢迎收藏、点赞、关注!
📩 有问题评论区见,我会持续优化、发布更多实用组件 🚀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2403419.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

监测预警系统重塑隧道安全新范式

在崇山峻岭的脉络间延伸的隧道&#xff0c;曾是交通安全的薄弱环节。智慧隧道监测预警系统的诞生&#xff0c;正在彻底改变这种被动防御格局&#xff0c;通过数字神经网络的构建&#xff0c;为地下交通动脉注入智能守护基因。 一、安全防控体系的质变升级 1.风险感知维度革命…

技巧小结:外部总线访问FPGA寄存器

概述 需求&#xff1a;stm32的fsmc总线挂载fpga&#xff0c;stm32需要访问fpga内部寄存器 1、分散加载文件将变量存放到指定地址即FPGA寄存器地址 sct文件指定变量存储地址&#xff0c;从而可以直接访问外设&#xff0c;&#xff08;28335也可以&#xff0c;不过用的是cmd文件…

jenkins集成gitlab发布到远程服务器

jenkins集成gitlab发布到远程服务器 前面我们讲了通过创建maven项目部署在jenkins本地服务器&#xff0c;这次实验我们将部署在远程服务器&#xff0c;再以nginx作为前端项目做一个小小的举例 1、部署nginx服务 [rootweb ~]# docker pull nginx [rootweb ~]# docker images …

当主观认知遇上机器逻辑:减少大模型工程化中的“主观性”模糊

一、人类与机器的认知差异 当自动驾驶汽车遇到紧急情况需要做出选择时&#xff0c;人类的决策往往充满矛盾&#xff1a;有人会优先保护儿童和老人&#xff0c;有人坚持"不主动变道"的操作原则。这种差异背后&#xff0c;体现着人类特有的情感判断与价值选择。而机器的…

会计 - 金融负债和权益工具

一、金融负债和权益工具区分的基本原则 (1)是否存在无条件地避免交付现金或其他金融资产的合同义务 如果企业不能无条件地避免以交付现金或其他金融资产来履行一项合同义务,则该合同义务符合金融负债的义务。 常见的该类合同义务情形包括:- 不能无条件避免的赎回; -强制…

Dify工具插件开发和智能体开发全流程

想象一下&#xff0c;你正在开发一个 AI 聊天机器人&#xff0c;想让它能实时搜索 Google、生成图像&#xff0c;甚至自动规划任务&#xff0c;但手动集成这些功能耗时又复杂。Dify 来了&#xff01;这个开源的 AI 应用平台让你轻松开发工具插件和智能体策略插件&#xff0c;快…

AI书签管理工具开发全记录(十三):TUI基本框架搭建

文章目录 AI书签管理工具开发全记录&#xff08;十三&#xff09;&#xff1a;TUI基本框架搭建前言 &#x1f4dd;1.TUI介绍 &#x1f50d;2. 框架选择 ⚙️3. 功能梳理 &#x1f3af;4. 基础框架搭建⚙️4.1 安装4.2 参数设计4.3 绘制ui4.3.1 设计结构体4.3.2 创建头部4.3.3 创…

初识结构体,整型提升及操作符的属性

目录 一、结构体成员访问操作符1.1 结构体二、操作符的属性&#xff1a;优先级、结合性2.1 优先级2.2 结合性C 运算符优先级 三、表达式求值3.1 整型提升3.2 算数转化 总结 一、结构体成员访问操作符 1.1 结构体 C语言已经提供了内置类型&#xff0c;如&#xff1a;char,shor…

检测到 #include 错误。请更新 includePath。已为此翻译单元(D:\软件\vscode\test.c)禁用波形曲线

原文链接&#xff1a;【VScodeMinGw】安装配置教程 下载mingw64 打开可以看到bin文件夹下是多个.exe文件&#xff0c;gcc.exe地址在环境配置中要用到 原文链接&#xff1a;VSCode中出现“#include错误&#xff0c;请更新includePath“问题&#xff0c;解决方法 重新VScode后…

2025年,百度智能云打响AI落地升维战

如果说从AI到Agent是对于产品落地形态的共识&#xff0c;那么如今百度智能云打响的恰是一个基于Agent进行TO B行业表达的AI生产力升维战。 在这个新的工程体系能力里&#xff0c;除了之前百度Create大会上提出的面向Agent的RAG能力等通用能力模块&#xff0c;对更为专业、个性…

Seed1.5-VL登顶,国产闭源模型弯道超车丨多模态模型5月最新榜单揭晓

随着图像、文本、语音、视频等多模态信息融合能力的持续增强&#xff0c;多模态大模型在感知理解、逻辑推理和内容生成等任务中的综合表现不断提升&#xff0c;正在展现出愈发接近人类的智能水平。多模态能力也正在从底层的感知理解&#xff0c;迈向具备认知、推理、决策能力的…

第3章——SSM整合

一、整合持久层框架MyBatis 1.准备数据库表及数据 创建数据库&#xff1a;springboot 使用IDEA工具自带的mysql插件来完成表的创建和数据的准备&#xff1a; 创建表 表创建成功后&#xff0c;为表准备数据&#xff0c;如下&#xff1a; 2.创建SpringBoot项目 使用脚手架创建…

VTK 显示文字、图片及2D/3D图

1. 基本环境设置 首先确保你已经安装了VTK库&#xff0c;并配置好了C开发环境。 #include <vtkSmartPointer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkRenderer.h> 2. 显示文字 2D文字 #include &l…

小白如何在cursor中使用mcp服务——以使用notion的api为例

1. 首先安装node.js,在这一步的时候不要勾选不要勾选 2. 安装完之后,前往notion页面 我的创作者个人资料 | Notion 前往集成页面&#xff0c;添加新集成&#xff0c;自己输入名字&#xff0c;选择内部 新建完之后&#xff0c;进入选择只读 复制密匙 然后前往cursor页面 新建…

引领AI安全新时代 Accelerate 2025北亚巡展·北京站成功举办

6月5日&#xff0c;网络安全行业年度盛会——"Accelerate 2025北亚巡展北京站"圆满落幕&#xff01;来自智库、产业界、Fortinet管理层及技术团队的权威专家&#xff0c;与来自各行业的企业客户代表齐聚一堂&#xff0c;围绕"AI智御全球引领安全新时代"主题…

为什么说数列是特殊的函数

文章目录 前情概要函数特性特殊之处典例剖析前情概要 高三的学生几乎都听老师说过,数列是特殊的函数,那么如何理解这句话呢,无外乎需要关注两点:①函数性,②特殊性,以下举例说明,帮助各位学子理解。 函数特性 既然是按照一定的次序排列而成的一列数字,那么这些数字(…

解决uniapp开发app map组件最高层级 遮挡自定义解决底部tabbar方法

subNvue&#xff0c;是 vue 页面的原生子窗体&#xff0c;把weex渲染的原生界面当做 vue 页面的子窗体覆盖在页面上。它不是全屏页面&#xff0c;它给App平台vue页面中的层级覆盖和原生界面自定义提供了更强大和灵活的解决方案。它也不是组件&#xff0c;就是一个原生子窗体。 …

96. 2017年蓝桥杯省赛 - Excel地址(困难)- 进制转换

96. Excel地址&#xff08;进制转换&#xff09; 1. 2017年蓝桥杯省赛 - Excel地址&#xff08;困难&#xff09; 标签&#xff1a;2017 省赛 1.1 题目描述 Excel 单元格的地址表示很有趣&#xff0c;它使用字母来表示列号。 比如&#xff0c; A 表示第 1 列&#xff0c;…

PPT转图片拼贴工具 v1.0

软件介绍 这个软件的作用就是将单个PPT的每一页转换为单独的图片&#xff0c;然后将图片进行拼接起来。 但是我没有还没有解决一次性处理多个文件。 效果展示如下&#xff1a; 软件安装 软件源码 import os import re import win32com.client from PIL import Imagedef con…

【行驶证识别成表格】批量OCR行驶证识别与Excel自动化处理系统,行驶证扫描件和照片图片识别后保存为Excel表格,基于QT和华为ocr识别的实现教程

在车辆管理、物流运输、保险理赔等领域&#xff0c;经常需要处理大量的行驶证信息。传统的人工录入方式效率低、易出错&#xff0c;而使用 OCR 技术可以自动识别行驶证图片中的文字信息&#xff0c;极大提高数据处理效率。该系统可以应用于以下场景&#xff1a; 保险公司快速…