All checks were successful
Build and Deploy (tool.xpcool.com) / build-and-deploy (push) Successful in 1m1s
- 添加 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 变更日志文档和工作日志记录
278 lines
8.3 KiB
Vue
278 lines
8.3 KiB
Vue
<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>
|