Skip to content

实现 全角半角转换 工具

分类:developer | 标签:全角、半角、转换 全角与半角字符互转

2.1 功能说明

全角与半角字符互转

核心能力

  • toHalf
  • toFull
  • all
  • letter
  • digit
  • punct
  • 类别
  • 半角
  • 全角
  • 码位关系

2.1.1 使用指南

功能说明

全角与半角是东亚文字排版中的概念。半角字符占一个标准字符宽度,就是我们熟悉的 ASCII 字符;全角字符占两个字符宽度,与汉字等宽,在 Unicode 中位于「半角及全角形式」区块。中文输入法在中文标点模式下常会输出全角字符,导致代码、配置文件、Excel 数据中混入肉眼难辨的全角符号,引发解析失败。本工具支持两个方向的批量转换,全部在浏览器本地完成。

转换规则

核心规则是一个固定偏移量 0xFEE0(十进制 65248)。半角可见 ASCII 字符位于 U+0021U+007E,对应的全角形式位于 U+FF01U+FF5E,两者一一对应,加上偏移即为全角,减去偏移即为半角。空格是唯一的例外:半角空格是 U+0020,全角空格是表意空格 U+3000,需要单独映射,因此本工具将其作为独立开关,默认不改动,避免误伤排版缩进。

转换范围说明

仅字母只处理 A 到 Z 与 a 到 z;仅数字只处理 0 到 9;仅标点处理其余可见符号,包括括号、引号、逗号、句号、冒号、斜杠、加减号等;全部则一次处理以上三类。实际工作中最常用的是「仅标点」,因为字母数字的全角形式相对少见,而全角标点是数据清洗的主要噪声来源。

注意事项

需要特别说明的是,中文特有的标点如顿号「、」、书名号「《》」、省略号「……」、破折号「——」以及中文引号「」在 ASCII 中没有对应的半角形式,它们不属于全角形式区块,本工具会原样保留而不会强行替换成英文标点。若你需要把中文标点整体替换为英文标点,那属于标点规范化而非全角半角转换,语义并不等价。另外日文半角片假名(U+FF61U+FF9F)虽然也在同一 Unicode 区块,但不遵循 0xFEE0 偏移规律,本工具同样不作处理。

使用场景

清洗从 Word、Excel、PDF 复制来的数据;修复代码中因输入法误切导致的全角括号与引号;统一数据库入库前的文本格式;处理表单校验中因全角数字导致的匹配失败。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • classify()
  • inScope()
  • convert()
  • swap()
  • loadSample()
  • clearAll()
  • showHint()
  • copyOut()
  • downloadOut()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'

const OFFSET = 0xfee0
const HALF_SPACE = 0x20
const FULL_SPACE = 0x3000

const mode = ref('toHalf')
const scope = ref('all')
const includeSpace = ref(false)
const input = ref('')
const output = ref('')
const hint = ref('')
const stats = ref({ total: 0, changed: 0, fullwidth: 0, halfwidth: 0 })

const sampleTable = [
  { type: '字母', half: 'A B a b', full: 'A B a b', code: 'U+0041 加 0xFEE0 得 U+FF21' },
  { type: '数字', half: '0 1 9', full: '0 1 9', code: 'U+0030 加 0xFEE0 得 U+FF10' },
  { type: '标点', half: ', . : ; ! ?', full: ', . : ; ! ?', code: 'U+002C 加 0xFEE0 得 U+FF0C' },
  { type: '括号', half: '( ) [ ] { }', full: '( ) [ ] { }', code: 'U+0028 加 0xFEE0 得 U+FF08' },
  { type: '符号', half: '@ # $ % & *', full: '@ # $ % & *', code: 'U+0040 加 0xFEE0 得 U+FF20' },
  { type: '空格', half: '半角空格', full: '全角空格', code: 'U+0020 与 U+3000 单独映射' }
]

// 依据字符的半角形式码位判断其类别
function classify(halfCode) {
  if ((halfCode >= 0x41 && halfCode <= 0x5a) || (halfCode >= 0x61 && halfCode <= 0x7a)) return 'letter'
  if (halfCode >= 0x30 && halfCode <= 0x39) return 'digit'
  return 'punct'
}

function inScope(halfCode) {
  return scope.value === 'all' || scope.value === classify(halfCode)
}

function convert() {
  const raw = input.value
  if (!raw) {
    output.value = ''
    stats.value = { total: 0, changed: 0, fullwidth: 0, halfwidth: 0 }
    return
  }

  let changed = 0
  let fullwidthCount = 0
  let halfwidthCount = 0
  const result = []

  // 按码点遍历,避免破坏 Emoji 等增补平面字符
  for (const ch of Array.from(raw)) {
    const code = ch.codePointAt(0)
    const isFullForm = code >= 0xff01 && code <= 0xff5e
    const isHalfForm = code >= 0x21 && code <= 0x7e
    if (isFullForm || code === FULL_SPACE) fullwidthCount++
    if (isHalfForm || code === HALF_SPACE) halfwidthCount++

    if (mode.value === 'toHalf') {
      if (isFullForm && inScope(code - OFFSET)) {
        result.push(String.fromCharCode(code - OFFSET))
        changed++
        continue
      }
      if (code === FULL_SPACE && includeSpace.value) {
        result.push(String.fromCharCode(HALF_SPACE))
        changed++
        continue
      }
    } else {
      if (isHalfForm && inScope(code)) {
        result.push(String.fromCharCode(code + OFFSET))
        changed++
        continue
      }
      if (code === HALF_SPACE && includeSpace.value) {
        result.push(String.fromCharCode(FULL_SPACE))
        changed++
        continue
      }
    }
    result.push(ch)
  }

  output.value = result.join('')
  stats.value = {
    total: Array.from(raw).length,
    changed,
    fullwidth: fullwidthCount,
    halfwidth: halfwidthCount
  }
}

function swap() {
  const previous = output.value
  mode.value = mode.value === 'toHalf' ? 'toFull' : 'toHalf'
  input.value = previous
  convert()
}

function loadSample() {
  input.value = mode.value === 'toHalf'
    ? 'ABC abc 123456(全角括号)[方括号]#@%&*,。:;!?\n混合示例:订单号 NO.202401 金额 ¥128.00'
    : 'ABC abc 123456 (half brackets) [square] #@%&*,.:;!?\n混合示例: NO.202401 amount $128.00'
  convert()
}

function clearAll() {
  input.value = ''
  output.value = ''
  stats.value = { total: 0, changed: 0, fullwidth: 0, halfwidth: 0 }
}

function showHint(text) {
  hint.value = text
  window.setTimeout(() => { hint.value = '' }, 2000)
}

async function copyOut() {
  try {
    await navigator.clipboard.writeText(output.value)
    showHint('已复制到剪贴板')
  } catch {
    showHint('复制失败,请手动选择文本复制')
  }
}

function downloadOut() {
  const blob = new Blob([output.value], { type: 'text/plain;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = mode.value === 'toHalf' ? 'halfwidth.txt' : 'fullwidth.txt'
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  URL.revokeObjectURL(url)
  showHint('已开始下载')
}

2.3 效果截图

全角半角转换 效果截图

工具访问地址:https://www.i91tools.com/tools/fullwidth-halfwidth