tool.xpcool.com/src/views/morse-code/MorseCodeView.vue
夏犀麟 954b9e9d8a
All checks were successful
Build and Deploy (tool.xpcool.com) / build-and-deploy (push) Successful in 1m1s
feat(router): 新增多个工具分类和路由配置
- 添加 TerminalIcon, GitBranchIcon, ApiIcon, BrushIcon, SoundIcon,
Table1Icon, LogoGithubIcon, ContrastIcon, Edit2Icon, ChartBarIcon,
EarthIcon, GitMergeIcon, SettingIcon, CurrencyExchangeIcon, MobileIcon,
WifiIcon, RocketIcon 等图标导入
- 新增 dev, design, health, study, travel, fun, online 七个工具分类
- 添加 SQL格式化、文本对比、JSON转代码、渐变生成器、占位图生成、
ASCII艺术、摩斯电码、正则可视化、Markdown表格、API构建器、
Gitignore生成器、代码转图片、颜色命名器、缩进转换、代码统计、
Hello World大全、JSON对比、环境变量解析、汇率换算、快递查询、
手机号归属地、IP信息查询、网络测试、网络测速等多个工具路由配置
- 新增 .env.example 配置文件模板
- 创建 docs/api-contract.md 后端接口契约文档
- 新增 api-builder 和 ascii-art 工具页面组件
- 更新 agents.md 添加接手必读指引和维护约定
- 创建 CHANGELOG.md 变更日志文档和工作日志记录
2026-08-24 22:11:12 +08:00

278 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-wrap">
<h2 class="page-title">摩斯电码</h2>
<p class="page-desc">英文数字中文与摩斯码互转中文按拼音编码还可以播放滴滴答答的电报声</p>
<t-tabs v-model="tab">
<!-- 编码文本 摩斯 -->
<t-tab-panel value="encode" label="文本 → 摩斯">
<t-card class="tool-section" title="输入文本(支持中文 / 英文 / 数字)">
<t-textarea
v-model="textInput"
:autosize="{ minRows: 4, maxRows: 10 }"
placeholder="例如SOS 或 你好世界"
/>
<div class="form-actions">
<t-button theme="primary" @click="encode">编码</t-button>
<t-button variant="outline" @click="loadSample">示例</t-button>
</div>
</t-card>
<ErrorAlert :message="error1" @close="error1 = ''" />
<t-card v-if="morseOut" class="tool-section" title="摩斯电码">
<div class="morse-display">{{ morseOut }}</div>
<div class="form-actions">
<t-button theme="primary" variant="outline" @click="playMorse">
<template #icon><SoundIcon /></template>
播放电报声
</t-button>
<t-button variant="outline" @click="stopPlay" :disabled="!playing">停止</t-button>
<CopyButton :text="morseOut" />
</div>
</t-card>
</t-tab-panel>
<!-- 解码:摩斯 → 文本 -->
<t-tab-panel value="decode" label="摩斯 → 文本">
<t-card class="tool-section" title="输入摩斯电码">
<t-textarea
v-model="morseInput"
:autosize="{ minRows: 4, maxRows: 10 }"
placeholder="例如:... --- ..."
/>
<div class="form-actions">
<t-button theme="primary" @click="decode">解码</t-button>
</div>
</t-card>
<ErrorAlert :message="error2" @close="error2 = ''" />
<t-card v-if="textOut" class="tool-section" title="解码结果">
<div class="morse-display">{{ textOut }}</div>
<div class="form-actions">
<CopyButton :text="textOut" />
</div>
<p class="note">中文以拼音形式返回(摩斯码不含声调与汉字信息),英文、数字、标点可完整还原。</p>
</t-card>
</t-tab-panel>
</t-tabs>
<HistoryPanel
:items="history.items"
@select="onHistorySelect"
@remove="history.remove"
@clear="history.clear"
/>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue'
import { SoundIcon } from 'tdesign-icons-vue-next'
import { pinyin } from 'pinyin-pro'
import ErrorAlert from '@/components/common/ErrorAlert.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import HistoryPanel from '@/components/common/HistoryPanel.vue'
import { useHistory } from '@/composables/useHistory'
const history = useHistory('morse-code')
const tab = ref('encode')
const textInput = ref('')
const morseOut = ref('')
const morseInput = ref('')
const textOut = ref('')
const error1 = ref('')
const error2 = ref('')
/** 标准国际摩斯码表ITU-R M.1677-1 */
const MORSE: Record<string, string> = {
A: '.-', B: '-...', C: '-.-.', D: '-..', E: '.', F: '..-.', G: '--.', H: '....',
I: '..', J: '.---', K: '-.-', L: '.-..', M: '--', N: '-.', O: '---', P: '.--.',
Q: '--.-', R: '.-.', S: '...', T: '-', U: '..-', V: '...-', W: '.--', X: '-..-',
Y: '-.--', Z: '--..',
'0': '-----', '1': '.----', '2': '..---', '3': '...--', '4': '....-', '5': '.....',
'6': '-....', '7': '--...', '8': '---..', '9': '----.',
'.': '.-.-.-', ',': '--..--', '?': '..--..', '!': '-.-.--', "'": '.----.',
'/': '-..-.', '(': '-.--.', ')': '-.--.-', '&': '.-...', ':': '---...',
';': '-.-.-.', '=': '-...-', '+': '.-.-.', '-': '-....-', '_': '..--.-',
'"': '.-..-.', '$': '...-..-', '@': '.--.-.',
}
const REVERSE: Record<string, string> = Object.fromEntries(
Object.entries(MORSE).map(([k, v]) => [v, k]),
)
/* ---------------- 编码 ---------------- */
function encode() {
error1.value = ''
morseOut.value = ''
const text = textInput.value.trim()
if (!text) {
error1.value = '请输入需要编码的文本'
return
}
// 中文整体先转拼音(无声调),其余字符逐个处理
const pinyinArray = pinyin(text, { toneType: 'none', type: 'array' })
const parts: string[] = []
for (let i = 0; i < text.length; i++) {
const ch = text[i]
if (ch === ' ' || ch === '\n') {
parts.push('/')
continue
}
if (/[\u4e00-\u9fa5]/.test(ch)) {
// 汉字:取该字的拼音,逐字母编码
const py = pinyinArray[i] || ''
const letters = py.toUpperCase()
if (letters) {
const code = letters
.split('')
.map((l) => MORSE[l])
.filter(Boolean)
.join(' ')
if (code) parts.push(code)
}
continue
}
const up = ch.toUpperCase()
const code = MORSE[up]
if (code) parts.push(code)
else parts.push('?') // 无法编码的字符
}
morseOut.value = parts.join(' / ')
history.add(text.length > 500 ? text.slice(0, 500) : text)
}
/* ---------------- 解码 ---------------- */
function decode() {
error2.value = ''
textOut.value = ''
const code = morseInput.value.trim()
if (!code) {
error2.value = '请输入摩斯电码'
return
}
const words = code.split('/')
const out: string[] = []
for (const word of words) {
const letters = word.trim().split(/\s+/)
let decoded = ''
for (const letter of letters) {
if (!letter) continue
decoded += REVERSE[letter] ?? '?'
}
out.push(decoded)
}
textOut.value = out.join(' ').trim()
history.add(code.length > 500 ? code.slice(0, 500) : code)
}
/* ---------------- 播放电报声 ---------------- */
const playing = ref(false)
let audioCtx: AudioContext | null = null
let stopFlag = false
/** 按摩斯码调度短音/长音/间隔(单元时长 90ms */
function playMorse() {
const code = morseOut.value
if (!code) return
const Ctor = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
if (!Ctor) return
if (!audioCtx) audioCtx = new Ctor()
void audioCtx.resume()
stopFlag = false
playing.value = true
const unit = 0.09
let t = audioCtx.currentTime + 0.15
const beep = (dur: number) => {
if (stopFlag) return
const osc = audioCtx!.createOscillator()
const gain = audioCtx!.createGain()
osc.type = 'sine'
osc.frequency.value = 750
gain.gain.setValueAtTime(0.0001, t)
gain.gain.exponentialRampToValueAtTime(0.5, t + 0.01)
gain.gain.setValueAtTime(0.5, t + dur - 0.01)
gain.gain.exponentialRampToValueAtTime(0.0001, t + dur)
osc.connect(gain).connect(audioCtx!.destination)
osc.start(t)
osc.stop(t + dur + 0.02)
}
for (const ch of code) {
if (stopFlag) break
if (ch === '.') {
beep(unit)
t += unit + unit
} else if (ch === '-') {
beep(unit * 3)
t += unit * 3 + unit
} else if (ch === ' ') {
t += unit * 2
} else if (ch === '/') {
t += unit * 4
}
}
// 结束后重置状态
window.setTimeout(() => {
if (!stopFlag) playing.value = false
}, (t - audioCtx.currentTime) * 1000 + 200)
}
function stopPlay() {
stopFlag = true
playing.value = false
}
onBeforeUnmount(() => {
stopFlag = true
if (audioCtx) void audioCtx.close()
})
/* ---------------- 其他 ---------------- */
function loadSample() {
textInput.value = '你好世界Hello World!'
encode()
}
function onHistorySelect(text: string) {
if (tab.value === 'encode') {
textInput.value = text
encode()
} else {
morseInput.value = text
decode()
}
}
</script>
<style scoped>
.page-wrap {
max-width: 860px;
}
.form-actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.morse-display {
padding: 16px;
border: 1px solid var(--td-border-level-1-color);
border-radius: 6px;
background: var(--td-bg-color-container);
font-family: ui-monospace, SFMono-Regular, Consolas, 'Courier New', monospace;
font-size: 16px;
line-height: 1.9;
word-break: break-all;
color: var(--td-text-color-primary);
}
.note {
margin: 12px 0 0;
font-size: 12px;
color: var(--td-text-color-placeholder);
}
</style>