Appearance
实现 全角半角转换 工具
分类:developer | 标签:全角、半角、转换 全角与半角字符互转
2.1 功能说明
全角与半角字符互转
核心能力
- toHalf
- toFull
- all
- letter
- digit
- punct
- 类别
- 半角
- 全角
- 码位关系
2.1.1 使用指南
功能说明
全角与半角是东亚文字排版中的概念。半角字符占一个标准字符宽度,就是我们熟悉的 ASCII 字符;全角字符占两个字符宽度,与汉字等宽,在 Unicode 中位于「半角及全角形式」区块。中文输入法在中文标点模式下常会输出全角字符,导致代码、配置文件、Excel 数据中混入肉眼难辨的全角符号,引发解析失败。本工具支持两个方向的批量转换,全部在浏览器本地完成。
转换规则
核心规则是一个固定偏移量 0xFEE0(十进制 65248)。半角可见 ASCII 字符位于 U+0021 到 U+007E,对应的全角形式位于 U+FF01 到 U+FF5E,两者一一对应,加上偏移即为全角,减去偏移即为半角。空格是唯一的例外:半角空格是 U+0020,全角空格是表意空格 U+3000,需要单独映射,因此本工具将其作为独立开关,默认不改动,避免误伤排版缩进。
转换范围说明
仅字母只处理 A 到 Z 与 a 到 z;仅数字只处理 0 到 9;仅标点处理其余可见符号,包括括号、引号、逗号、句号、冒号、斜杠、加减号等;全部则一次处理以上三类。实际工作中最常用的是「仅标点」,因为字母数字的全角形式相对少见,而全角标点是数据清洗的主要噪声来源。
注意事项
需要特别说明的是,中文特有的标点如顿号「、」、书名号「《》」、省略号「……」、破折号「——」以及中文引号「」在 ASCII 中没有对应的半角形式,它们不属于全角形式区块,本工具会原样保留而不会强行替换成英文标点。若你需要把中文标点整体替换为英文标点,那属于标点规范化而非全角半角转换,语义并不等价。另外日文半角片假名(U+FF61 至 U+FF9F)虽然也在同一 Unicode 区块,但不遵循 0xFEE0 偏移规律,本工具同样不作处理。
使用场景
清洗从 Word、Excel、PDF 复制来的数据;修复代码中因输入法误切导致的全角括号与引号;统一数据库入库前的文本格式;处理表单校验中因全角数字导致的匹配失败。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-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 效果截图
